feat(api): public API plan gating, docs auth, and dashboard snippets (#701)

* fix(automation): align public API plan gating across cloud and self-hosted

Add shared canUsePublicApi helper so cloud allows Early Adopter and
Enterprise while self-hosted requires Enterprise. Update the API key
dashboard page and backend key creation checks to use the same rules.

Co-authored-by: Rotimi Best <[email protected]>

* docs(api): document bearer auth on public API reference

Add shared OpenAPI enrichment with bearerAuth security scheme and
authentication instructions. The interactive reference now loads an
enriched spec with Authorize support, and the API guide explains how
to create and use org API keys.

Co-authored-by: Rotimi Best <[email protected]>

* feat(dashboard): add API quick-start code snippets on automation page

Mirror the MCP client setup section with cURL, JavaScript, and Python
examples that show how to authenticate public API requests with org keys.

Co-authored-by: Rotimi Best <[email protected]>

* ci(openapi): republish spec when public API OpenAPI helpers change

Co-authored-by: Rotimi Best <[email protected]>

* feat(dashboard): add Go snippet to API quick-start examples

Co-authored-by: Rotimi Best <[email protected]>

* docs(api): use /org/*/api and clarify dashboard key generation path

Co-authored-by: Rotimi Best <[email protected]>

* fix(api): escape backticks in OpenAPI description template literal

Greptile flagged that unescaped backticks around * in
PUBLIC_API_OPENAPI_DESCRIPTION evaluated to NaN and dropped auth docs
from the Scalar spec. Also use plan-specific snackbar message on upgrade.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-02 11:24:33 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 447e54b1f4
commit fe9f8c441f
25 changed files with 476 additions and 137 deletions
@@ -5,6 +5,7 @@ on:
branches: [main]
paths:
- 'apps/api/**'
- 'packages/utils/src/openapi/**'
workflow_dispatch:
env:
+3 -7
View File
@@ -9,6 +9,7 @@ import { app } from '../src/app';
import { env } from '@cio/core/config/env';
import { generateSpecs } from 'hono-openapi';
import { join } from 'path';
import { enrichPublicApiOpenApiSpec } from '@cio/utils/openapi/public-api';
interface UploadOptions {
key: string;
@@ -32,17 +33,12 @@ function filterPublicApiSpec(spec: Record<string, unknown>) {
})
: undefined;
return {
return enrichPublicApiOpenApiSpec({
...spec,
openapi: spec.openapi ?? '3.1.0',
info: {
title: 'ClassroomIO Public API',
version: '1.0.0',
description: 'Public API for managing organizations, audience members, and courses in ClassroomIO.'
},
paths: publicApiPaths,
tags
};
});
}
class OpenAPISpecGenerator {
@@ -1,3 +1,4 @@
import { env } from '@cio/core/config/env';
import { AppError, ErrorCodes } from '@api/utils/errors';
import {
countActiveOrganizationApiKeys,
@@ -12,11 +13,11 @@ import type { TOrganizationApiKey, TOrganizationApiKeyType, TPlan } from '@db/ty
import {
AUTOMATION_TYPE,
getMcpAutomationCategory,
canUsePublicApi,
getMcpAutomationLimits,
MCP_TOOL_CREDIT_COST,
type TMcpToolName
} from '@cio/utils/plans';
import { PLAN } from '@cio/utils/plans/constants';
export type OrganizationAutomationUsageSummary = {
type: TOrganizationApiKeyType;
@@ -59,8 +60,18 @@ export async function assertOrganizationAutomationKeyCreationAllowed(
): Promise<void> {
const planName = await getOrganizationPlanName(organizationId);
if (type === AUTOMATION_TYPE.API && planName === PLAN.BASIC) {
throw new AppError('Public API keys require an Early Adopter or Enterprise plan', ErrorCodes.UPGRADE_REQUIRED, 403);
if (type === AUTOMATION_TYPE.API) {
const isSelfHosted = env.PUBLIC_IS_SELFHOSTED === 'true';
if (!canUsePublicApi(planName, isSelfHosted)) {
throw new AppError(
isSelfHosted
? 'Public API keys require an Enterprise plan'
: 'Public API keys require an Early Adopter or Enterprise plan',
ErrorCodes.UPGRADE_REQUIRED,
403
);
}
}
if (type !== AUTOMATION_TYPE.MCP) {
@@ -1,9 +1,16 @@
<script lang="ts">
import { automationApi } from '$features/automation/api/automation.svelte';
import { getMaskedAutomationSecret } from '$features/automation/utils/automation-utils';
import { isOrgAdmin, isEnterprisePlan } from '$lib/utils/store/org';
import {
getMaskedAutomationSecret,
getPublicApiCurlSnippet,
getPublicApiGoSnippet,
getPublicApiJavaScriptSnippet,
getPublicApiPythonSnippet
} from '$features/automation/utils/automation-utils';
import { hasPublicApiAccess, isOrgAdmin } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
import { snackbar } from '$features/ui/snackbar/store';
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
import * as Alert from '@cio/ui/base/alert';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
@@ -13,18 +20,24 @@
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import * as Field from '@cio/ui/base/field';
import * as Table from '@cio/ui/base/table';
import * as Tabs from '@cio/ui/base/tabs';
import { InputField } from '@cio/ui/custom/input-field';
import { IconButton } from '@cio/ui/custom/icon-button';
import { UpgradeBanner } from '$features/ui';
import CodeIcon from '@lucide/svelte/icons/code';
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
import KeyIcon from '@lucide/svelte/icons/key';
import PlusIcon from '@lucide/svelte/icons/plus';
let activeSetupTab = $state('curl');
let generatedSecret = $state<string | null>(null);
let isCreateKeyModalOpen = $state(false);
let keyLabel = $state('');
const canCreateKey = $derived(keyLabel.trim().length > 0 && !automationApi.isLoading);
const upgradeMessageKey = $derived(
PUBLIC_IS_SELFHOSTED === 'true' ? 'upgrade.enterprise_required' : 'upgrade.public_api_required'
);
function resetCreateKeyModal() {
keyLabel = '';
@@ -32,8 +45,8 @@
}
function openCreateKeyModal() {
if (!$isEnterprisePlan) {
snackbar.error('upgrade.required');
if (!$hasPublicApiAccess) {
snackbar.error(upgradeMessageKey);
return;
}
resetCreateKeyModal();
@@ -41,7 +54,7 @@
}
async function onGenerateKey() {
if (!$isEnterprisePlan) return;
if (!$hasPublicApiAccess) return;
const result = await automationApi.createKey('api', keyLabel);
if (!result) return;
@@ -52,13 +65,13 @@
}
async function onRevokeKey(keyId: string) {
if (!$isEnterprisePlan) return;
if (!$hasPublicApiAccess) return;
if (!confirm(t.get('automation.keys.revoke_confirm'))) return;
await automationApi.revokeKey(keyId);
}
async function onRotateKey(keyId: string) {
if (!$isEnterprisePlan) return;
if (!$hasPublicApiAccess) return;
if (!confirm(t.get('automation.keys.rotate_confirm'))) return;
await automationApi.rotateKey(keyId);
generatedSecret = automationApi.generatedSecret;
@@ -66,10 +79,11 @@
}
const apiKeys = $derived(automationApi.keys.filter((key) => key.type === 'api'));
const hasActiveApiKey = $derived(apiKeys.some((key) => !key.revokedAt));
</script>
<Field.Group class="mx-auto w-full space-y-2">
<UpgradeBanner>{$t('upgrade.enterprise_required')}</UpgradeBanner>
<UpgradeBanner visible={!$hasPublicApiAccess}>{$t(upgradeMessageKey)}</UpgradeBanner>
{#if !$isOrgAdmin}
<Alert.Callout
@@ -89,7 +103,7 @@
{#if $isOrgAdmin}
<IconButton
onclick={openCreateKeyModal}
disabled={automationApi.isLoading || !$isEnterprisePlan}
disabled={automationApi.isLoading || !$hasPublicApiAccess}
tooltip={$t('automation.api.keys.generate')}
>
<PlusIcon size={16} />
@@ -174,7 +188,7 @@
variant="page"
>
{#if $isOrgAdmin}
<Button onclick={openCreateKeyModal} disabled={automationApi.isLoading || !$isEnterprisePlan}>
<Button onclick={openCreateKeyModal} disabled={automationApi.isLoading || !$hasPublicApiAccess}>
<PlusIcon size={16} />
{$t('automation.api.keys.generate')}
</Button>
@@ -183,6 +197,57 @@
{/if}
{/if}
</div>
{#if hasActiveApiKey}
<Field.Set class="gap-3!">
<Field.Legend class="flex items-center gap-2">
<CodeIcon class="size-5" />
{$t('automation.api.setup.title')}
</Field.Legend>
<Field.Description>{$t('automation.api.setup.description')}</Field.Description>
<Tabs.Root bind:value={activeSetupTab} class="w-full">
<Tabs.List class="inline-flex w-auto">
<Tabs.Trigger value="curl">{$t('automation.clients.curl')}</Tabs.Trigger>
<Tabs.Trigger value="javascript">{$t('automation.clients.javascript')}</Tabs.Trigger>
<Tabs.Trigger value="python">{$t('automation.clients.python')}</Tabs.Trigger>
<Tabs.Trigger value="go">{$t('automation.clients.go')}</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="curl" class="mt-4">
<Code.Overflow>
<Code.Root code={getPublicApiCurlSnippet(generatedSecret)} lang="bash">
<Code.CopyButton />
</Code.Root>
</Code.Overflow>
</Tabs.Content>
<Tabs.Content value="javascript" class="mt-4">
<Code.Overflow>
<Code.Root code={getPublicApiJavaScriptSnippet(generatedSecret)} lang="javascript">
<Code.CopyButton />
</Code.Root>
</Code.Overflow>
</Tabs.Content>
<Tabs.Content value="python" class="mt-4">
<Code.Overflow>
<Code.Root code={getPublicApiPythonSnippet(generatedSecret)} lang="python">
<Code.CopyButton />
</Code.Root>
</Code.Overflow>
</Tabs.Content>
<Tabs.Content value="go" class="mt-4">
<Code.Overflow>
<Code.Root code={getPublicApiGoSnippet(generatedSecret)} lang="go">
<Code.CopyButton />
</Code.Root>
</Code.Overflow>
</Tabs.Content>
</Tabs.Root>
</Field.Set>
{/if}
</Field.Group>
<Dialog.Root
@@ -1,9 +1,97 @@
import type { AutomationKeyType } from './types';
import { PUBLIC_API_BASE_URL } from '@cio/utils/openapi/public-api';
const PUBLIC_API_SETUP_SECRET_PLACEHOLDER = '<paste-your-api-key>';
function getServerPath() {
return 'npx -y @classroomio/mcp';
}
export function getPublicApiSetupSecret(secret: string | null) {
return secret ?? PUBLIC_API_SETUP_SECRET_PLACEHOLDER;
}
export function getPublicApiCurlSnippet(secret: string | null) {
const apiKey = getPublicApiSetupSecret(secret);
return `curl ${PUBLIC_API_BASE_URL}/audience \\
-H "Authorization: Bearer ${apiKey}"`;
}
export function getPublicApiJavaScriptSnippet(secret: string | null) {
const apiKey = getPublicApiSetupSecret(secret);
return `const response = await fetch('${PUBLIC_API_BASE_URL}/audience', {
headers: {
Authorization: 'Bearer ${apiKey}'
}
});
const { data } = await response.json();
console.log(data);`;
}
export function getPublicApiPythonSnippet(secret: string | null) {
const apiKey = getPublicApiSetupSecret(secret);
return `import requests
response = requests.get(
"${PUBLIC_API_BASE_URL}/audience",
headers={"Authorization": f"Bearer ${apiKey}"},
)
response.raise_for_status()
data = response.json()["data"]
print(data)`;
}
export function getPublicApiGoSnippet(secret: string | null) {
const apiKey = getPublicApiSetupSecret(secret);
return `package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
req, err := http.NewRequest(http.MethodGet, "${PUBLIC_API_BASE_URL}/audience", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer ${apiKey}")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
if res.StatusCode >= 400 {
panic(fmt.Sprintf("request failed: %s", body))
}
var payload struct {
Data json.RawMessage \`json:"data"\`
}
if err := json.Unmarshal(body, &payload); err != nil {
panic(err)
}
fmt.Println(string(payload.Data))
}`;
}
export function getAutomationSetupSecret(secret: string | null) {
return secret ?? '<paste-your-mcp-key>';
}
@@ -8,13 +8,16 @@
interface Props {
className?: string;
onClick?: any;
visible?: boolean;
children?: import('svelte').Snippet;
}
let { className = '', onClick = () => {}, ...restProps }: Props = $props();
let { className = '', onClick = () => {}, visible, ...restProps }: Props = $props();
const shouldShow = $derived(visible ?? $isFreePlan);
</script>
{#if $isFreePlan}
{#if shouldShow}
<HoverableItem>
{#snippet children(isHovered)}
<Item.Root variant="outline" size="sm" class="h-fit w-full border-blue-700! py-2! {className}">
+7 -1
View File
@@ -4,7 +4,7 @@ import merge from 'lodash/merge';
import type { AccountOrg } from '$features/app/types';
import type { OrgTeamMember } from '../types/org';
import { PLAN } from '@cio/utils/plans';
import { canUsePublicApi, PLAN } from '@cio/utils/plans';
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
import { ROLE, TENANT_ROOT_DOMAIN } from '@cio/utils/constants';
import { STEPS } from '../constants/quiz';
@@ -146,6 +146,12 @@ export const isEnterprisePlan = derived(currentOrg, ($currentOrg) => {
return plan?.planName === PLAN.ENTERPRISE;
});
export const hasPublicApiAccess = derived(currentOrg, ($currentOrg) => {
const plan = getActivePlan($currentOrg);
return canUsePublicApi(plan?.planName, PUBLIC_IS_SELFHOSTED === 'true');
});
export const currentOrgMaxAudience = derived(currentOrgPlan, ($plan) =>
!$plan
? 20
@@ -503,7 +503,8 @@
"download_lessons": "Opgrader for at downloade lektioner",
"download_course": "Opgrader for at downloade kursus",
"required": "Opgradering påkrævet",
"enterprise_required": "Denne funktion kræver en Enterprise-plan."
"enterprise_required": "Denne funktion kræver en Enterprise-plan.",
"public_api_required": "Opgrader til Early Adopter eller Enterprise for at få adgang til det offentlige API."
},
"analytics": {
"title": "Analytics",
@@ -3892,6 +3893,10 @@
"save": "Create key"
}
},
"setup": {
"title": "Hurtig start",
"description": "Brug din API-nøgle som Bearer-token på hver anmodning. Erstat pladsholderen med en nøgle ovenfor, eller indsæt hemmeligheden lige efter du genererer en."
},
"view_docs": "View Docs"
},
"keys": {
@@ -3916,7 +3921,11 @@
"claude_code": "Claude kode",
"codex": "Codex",
"cursor": "Markør",
"opencode": "OpenCode"
"opencode": "OpenCode",
"curl": "cURL",
"javascript": "JavaScript",
"python": "Python",
"go": "Go"
},
"tabs": {
"mcp": "MCP",
@@ -503,7 +503,8 @@
"download_lessons": "Führen Sie ein Upgrade durch, um Lektionen herunterzuladen",
"download_course": "Upgrade zum Download-Kurs",
"required": "Upgrade erforderlich",
"enterprise_required": "Für diese Funktion ist ein Enterprise-Plan erforderlich."
"enterprise_required": "Für diese Funktion ist ein Enterprise-Plan erforderlich.",
"public_api_required": "Upgrade auf Early Adopter oder Enterprise, um auf die öffentliche API zuzugreifen."
},
"analytics": {
"title": "Analytik",
@@ -3892,6 +3893,10 @@
"save": "Create key"
}
},
"setup": {
"title": "Schnellstart",
"description": "Verwenden Sie Ihren API-Schlüssel als Bearer-Token bei jeder Anfrage. Ersetzen Sie den Platzhalter durch einen Schlüssel von oben oder fügen Sie das Geheimnis direkt nach der Erstellung ein."
},
"view_docs": "View Docs"
},
"keys": {
@@ -3916,7 +3921,11 @@
"claude_code": "Claude Code",
"codex": "Kodex",
"cursor": "Cursor",
"opencode": "OpenCode"
"opencode": "OpenCode",
"curl": "cURL",
"javascript": "JavaScript",
"python": "Python",
"go": "Go"
},
"tabs": {
"mcp": "MCP",
@@ -3963,7 +3963,8 @@
"download_lessons": "Upgrade to download lessons",
"download_course": "Upgrade to download course",
"required": "Upgrade Required",
"enterprise_required": "This feature requires an Enterprise plan."
"enterprise_required": "This feature requires an Enterprise plan.",
"public_api_required": "Upgrade to Early Adopter or Enterprise to access the public API."
},
"automation": {
"title": "Automation",
@@ -3982,7 +3983,11 @@
"claude_code": "Claude Code",
"codex": "Codex",
"cursor": "Cursor",
"opencode": "OpenCode"
"opencode": "OpenCode",
"curl": "cURL",
"javascript": "JavaScript",
"python": "Python",
"go": "Go"
},
"keys": {
"active": "Active",
@@ -4056,6 +4061,10 @@
"save": "Create key"
}
},
"setup": {
"title": "Quick start",
"description": "Use your API key as a Bearer token on every request. Replace the placeholder with a key from above, or paste the secret immediately after you generate one."
},
"view_docs": "View Docs"
},
"zapier": {
@@ -503,7 +503,8 @@
"download_lessons": "Actualice para descargar lecciones",
"download_course": "Actualizar para descargar el curso",
"required": "Actualización requerida",
"enterprise_required": "Esta característica requiere un plan Enterprise."
"enterprise_required": "Esta característica requiere un plan Enterprise.",
"public_api_required": "Actualiza a Early Adopter o Enterprise para acceder a la API pública."
},
"analytics": {
"title": "Analítica",
@@ -3892,6 +3893,10 @@
"save": "Create key"
}
},
"setup": {
"title": "Inicio rápido",
"description": "Usa tu clave API como token Bearer en cada solicitud. Reemplaza el marcador de posición con una clave de arriba, o pega el secreto inmediatamente después de generar una."
},
"view_docs": "View Docs"
},
"keys": {
@@ -3916,7 +3921,11 @@
"claude_code": "Código Claude",
"codex": "Códice",
"cursor": "Cursor",
"opencode": "OpenCode"
"opencode": "OpenCode",
"curl": "cURL",
"javascript": "JavaScript",
"python": "Python",
"go": "Go"
},
"tabs": {
"mcp": "MCP",
@@ -503,7 +503,8 @@
"download_lessons": "Mettre à niveau pour télécharger les leçons",
"download_course": "Mettre à niveau pour télécharger le cours",
"required": "Mise à niveau requise",
"enterprise_required": "Cette fonctionnalité nécessite un forfait Entreprise."
"enterprise_required": "Cette fonctionnalité nécessite un forfait Entreprise.",
"public_api_required": "Passez à Early Adopter ou Enterprise pour accéder à l'API publique."
},
"analytics": {
"title": "Analyse",
@@ -3892,6 +3893,10 @@
"save": "Create key"
}
},
"setup": {
"title": "Démarrage rapide",
"description": "Utilisez votre clé API comme jeton Bearer sur chaque requête. Remplacez l'espace réservé par une clé ci-dessus, ou collez le secret immédiatement après l'avoir généré."
},
"view_docs": "View Docs"
},
"keys": {
@@ -3916,7 +3921,11 @@
"claude_code": "Claude Code",
"codex": "Codex",
"cursor": "Curseur",
"opencode": "OpenCode"
"opencode": "OpenCode",
"curl": "cURL",
"javascript": "JavaScript",
"python": "Python",
"go": "Go"
},
"tabs": {
"mcp": "PCM",
@@ -503,7 +503,8 @@
"download_lessons": "पाठ डाउनलोड करने के लिए अपग्रेड करें",
"download_course": "पाठ्यक्रम डाउनलोड करने के लिए अपग्रेड करें",
"required": "अपग्रेड आवश्यक",
"enterprise_required": "इस सुविधा के लिए एंटरप्राइज़ योजना की आवश्यकता है."
"enterprise_required": "इस सुविधा के लिए एंटरप्राइज़ योजना की आवश्यकता है.",
"public_api_required": "सार्वजनिक API तक पहुँचने के लिए Early Adopter या Enterprise में अपग्रेड करें।"
},
"analytics": {
"title": "विश्लेषिकी",
@@ -3892,6 +3893,10 @@
"save": "Create key"
}
},
"setup": {
"title": "त्वरित शुरुआत",
"description": "हर अनुरोध पर अपनी API कुंजी को Bearer टोकन के रूप में उपयोग करें। प्लेसहोल्डर को ऊपर की कुंजी से बदलें, या कुंजी बनाने के तुरंत बाद सीक्रेट पेस्ट करें।"
},
"view_docs": "View Docs"
},
"keys": {
@@ -3916,7 +3921,11 @@
"claude_code": "क्लाउड कोड",
"codex": "कोडेक्स",
"cursor": "कर्सर",
"opencode": "OpenCode"
"opencode": "OpenCode",
"curl": "cURL",
"javascript": "JavaScript",
"python": "Python",
"go": "Go"
},
"tabs": {
"mcp": "एम.सी.पी",
@@ -503,7 +503,8 @@
"download_lessons": "Uaktualnij, aby pobrać lekcje",
"download_course": "Uaktualnij, aby pobrać kurs",
"required": "Wymagana aktualizacja",
"enterprise_required": "Ta funkcja wymaga planu Enterprise."
"enterprise_required": "Ta funkcja wymaga planu Enterprise.",
"public_api_required": "Uaktualnij do Early Adopter lub Enterprise, aby uzyskać dostęp do publicznego API."
},
"analytics": {
"title": "Analityka",
@@ -3892,6 +3893,10 @@
"save": "Create key"
}
},
"setup": {
"title": "Szybki start",
"description": "Używaj klucza API jako tokena Bearer w każdym żądaniu. Zamień symbol zastępczy na klucz z powyższej listy lub wklej sekret zaraz po wygenerowaniu."
},
"view_docs": "View Docs"
},
"keys": {
@@ -3916,7 +3921,11 @@
"claude_code": "Claude'a Koda",
"codex": "Kodeks",
"cursor": "Kursor",
"opencode": "OpenCode"
"opencode": "OpenCode",
"curl": "cURL",
"javascript": "JavaScript",
"python": "Python",
"go": "Go"
},
"tabs": {
"mcp": "MCP",
@@ -503,7 +503,8 @@
"download_lessons": "Atualize para baixar lições",
"download_course": "Atualize para baixar o curso",
"required": "Atualização necessária",
"enterprise_required": "Este recurso requer um plano Enterprise."
"enterprise_required": "Este recurso requer um plano Enterprise.",
"public_api_required": "Faça upgrade para Early Adopter ou Enterprise para acessar a API pública."
},
"analytics": {
"title": "Análise",
@@ -3892,6 +3893,10 @@
"save": "Create key"
}
},
"setup": {
"title": "Início rápido",
"description": "Use sua chave de API como token Bearer em cada solicitação. Substitua o marcador por uma chave acima ou cole o segredo imediatamente após gerar uma."
},
"view_docs": "View Docs"
},
"keys": {
@@ -3916,7 +3921,11 @@
"claude_code": "Código Claude",
"codex": "Códice",
"cursor": "Cursor",
"opencode": "OpenCode"
"opencode": "OpenCode",
"curl": "cURL",
"javascript": "JavaScript",
"python": "Python",
"go": "Go"
},
"tabs": {
"mcp": "PCM",
@@ -503,7 +503,8 @@
"download_lessons": "Обновите, чтобы скачивать уроки",
"download_course": "Обновите, чтобы загрузить курс",
"required": "Требуется обновление",
"enterprise_required": "Для этой функции требуется план Enterprise."
"enterprise_required": "Для этой функции требуется план Enterprise.",
"public_api_required": "Перейдите на Early Adopter или Enterprise, чтобы получить доступ к публичному API."
},
"analytics": {
"title": "Аналитика",
@@ -3892,6 +3893,10 @@
"save": "Create key"
}
},
"setup": {
"title": "Быстрый старт",
"description": "Используйте ключ API как Bearer-токен в каждом запросе. Замените заполнитель ключом из списка выше или вставьте секрет сразу после генерации."
},
"view_docs": "View Docs"
},
"keys": {
@@ -3916,7 +3921,11 @@
"claude_code": "Клод Код",
"codex": "Кодекс",
"cursor": "Курсор",
"opencode": "OpenCode"
"opencode": "OpenCode",
"curl": "cURL",
"javascript": "JavaScript",
"python": "Python",
"go": "Go"
},
"tabs": {
"mcp": "МКП",
@@ -503,7 +503,8 @@
"download_lessons": "Nâng cấp để tải bài học",
"download_course": "Nâng cấp để tải xuống khóa học",
"required": "Yêu cầu nâng cấp",
"enterprise_required": "Tính năng này yêu cầu gói Enterprise."
"enterprise_required": "Tính năng này yêu cầu gói Enterprise.",
"public_api_required": "Nâng cấp lên Early Adopter hoặc Enterprise để truy cập API công khai."
},
"analytics": {
"title": "Phân tích",
@@ -3892,6 +3893,10 @@
"save": "Create key"
}
},
"setup": {
"title": "Bắt đầu nhanh",
"description": "Dùng khóa API làm Bearer token cho mọi yêu cầu. Thay chỗ giữ chỗ bằng khóa ở trên, hoặc dán secret ngay sau khi tạo khóa."
},
"view_docs": "View Docs"
},
"keys": {
@@ -3916,7 +3921,11 @@
"claude_code": "Mã Claude",
"codex": "Codex",
"cursor": "Con trỏ",
"opencode": "OpenCode"
"opencode": "OpenCode",
"curl": "cURL",
"javascript": "JavaScript",
"python": "Python",
"go": "Go"
},
"tabs": {
"mcp": "MCP",
+20 -5
View File
@@ -1,17 +1,32 @@
import { getHtmlDocument } from '@scalar/core/libs/html-rendering';
import {
enrichPublicApiOpenApiSpec,
PUBLIC_API_BEARER_SCHEME,
PUBLIC_API_OPENAPI_URL
} from '@cio/utils/openapi/public-api';
const publicApiSpecUrl = 'https://api.cdn.clsrio.com/openapi/public-api/openapi-latest.json';
export async function loader() {
const response = await fetch(PUBLIC_API_OPENAPI_URL);
if (!response.ok) {
throw new Response('Failed to load the public API OpenAPI specification.', { status: 502 });
}
const spec = enrichPublicApiOpenApiSpec((await response.json()) as Record<string, unknown>);
export function loader() {
return new Response(
getHtmlDocument({
url: publicApiSpecUrl,
content: JSON.stringify(spec),
pageTitle: 'ClassroomIO Public API Reference',
theme: 'none'
theme: 'none',
authentication: {
preferredSecurityScheme: PUBLIC_API_BEARER_SCHEME
}
}),
{
headers: {
'content-type': 'text/html; charset=utf-8'
'content-type': 'text/html; charset=utf-8',
'cache-control': 'public, max-age=300'
}
}
);
+42 -12
View File
@@ -4,6 +4,7 @@ description: Use the ClassroomIO public API to manage audience members and cours
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
The ClassroomIO public API is available under:
@@ -11,17 +12,53 @@ The ClassroomIO public API is available under:
https://api.classroomio.com/public-api/v1
```
Authentication uses organization-scoped bearer tokens created from `Automation -> API` in the ClassroomIO dashboard.
Every request must include an organization-scoped API key as a Bearer token.
## Get an API key
Generate keys in the ClassroomIO dashboard as an **organization admin**.
<Steps>
<Step>
Sign in to [ClassroomIO](https://app.classroomio.com).
</Step>
<Step>
In the org sidebar, open the **Automation** section, then **API**.
</Step>
<Step>
Or go directly to [`/org/*/api`](https://app.classroomio.com/org/*/api). The `*` picks your current organization automatically.
</Step>
<Step>
Click **Generate API key**, name the key, and copy the secret immediately. Keys are shown only once and look like `cio_api_...`.
</Step>
<Step>
Send the key on every request as `Authorization: Bearer cio_api_your_key_here`.
</Step>
</Steps>
<Callout type="warn" title="Paid Access">
On ClassroomIO Cloud, API key creation is available on **Early Adopter** and **Enterprise** plans. Self-hosted deployments require **Enterprise**.
</Callout>
## Interactive Reference
- [Open the interactive API reference](/docs/api/reference)
## Plan Requirement
The reference page includes an **Authorize** button. Paste your API key there to try endpoints without writing curl commands.
<Callout type="warn" title="Paid Access">
API key creation is available only on Early Adopter and Enterprise plans.
</Callout>
### Try requests in the reference UI
1. Open the [interactive API reference](/docs/api/reference).
2. Click **Authorize** (lock icon).
3. Select **bearerAuth** and paste your full `cio_api_...` key.
4. Use **Try it** on any endpoint.
### curl example
```bash
curl https://api.classroomio.com/public-api/v1/audience \
-H "Authorization: Bearer cio_api_your_key_here"
```
## Current Surface
@@ -50,13 +87,6 @@ Raw OpenAPI JSON:
- [Public API OpenAPI JSON](https://api.cdn.clsrio.com/openapi/public-api/openapi-latest.json)
- [Interactive API Reference](/docs/api/reference)
## Authentication Example
```bash
curl https://api.classroomio.com/public-api/v1/audience \
-H "Authorization: Bearer <cio_api_key>"
```
## Notes
- API keys are organization-scoped.
+1
View File
@@ -12,6 +12,7 @@
"postinstall": "fumadocs-mdx"
},
"dependencies": {
"@cio/utils": "workspace:*",
"@scalar/core": "^0.3.28",
"@react-router/dev": "^7.9.4",
"fumadocs-core": "16.2.1",
+4
View File
@@ -56,6 +56,10 @@
"./config/upload-limits": {
"types": "./dist/config/upload-limits.d.ts",
"default": "./dist/config/upload-limits.js"
},
"./openapi/public-api": {
"types": "./dist/openapi/public-api.d.ts",
"default": "./dist/openapi/public-api.js"
}
},
"scripts": {
+89
View File
@@ -0,0 +1,89 @@
export const PUBLIC_API_BASE_URL = 'https://api.classroomio.com/public-api/v1';
export const PUBLIC_API_OPENAPI_URL = 'https://api.cdn.clsrio.com/openapi/public-api/openapi-latest.json';
export const PUBLIC_API_BEARER_SCHEME = 'bearerAuth';
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'] as const;
export const PUBLIC_API_OPENAPI_DESCRIPTION = `# Authentication
All public API endpoints require an **organization-scoped API key** sent as a Bearer token.
## Get an API key
1. Sign in to [ClassroomIO](https://app.classroomio.com) as an **organization admin**.
2. In the org sidebar, open the **Automation** section, then **API** ([/org/*/api](https://app.classroomio.com/org/*/api) — \`*\` picks your current organization automatically).
3. Click **Generate API key** and copy the secret immediately (it is shown only once).
4. Keys look like \`cio_api_...\` and include the \`public_api:*\` scope.
## Send requests
Add the key to every request:
\`\`\`
Authorization: Bearer cio_api_your_key_here
\`\`\`
Base URL: \`${PUBLIC_API_BASE_URL}\`
## Try requests in this reference
Use the **Authorize** button (lock icon) on this page, choose **bearerAuth**, paste your full API key, then run **Try it** on any endpoint.
## Plan access
On ClassroomIO Cloud, API keys are available on **Early Adopter** and **Enterprise** plans. Self-hosted deployments require **Enterprise**.
`;
type OpenApiDocument = Record<string, unknown>;
function applySecurityToOperations(paths: Record<string, unknown>) {
for (const pathItem of Object.values(paths)) {
if (!pathItem || typeof pathItem !== 'object') {
continue;
}
for (const method of HTTP_METHODS) {
const operation = (pathItem as Record<string, unknown>)[method];
if (!operation || typeof operation !== 'object') {
continue;
}
(operation as Record<string, unknown>).security = [{ [PUBLIC_API_BEARER_SCHEME]: [] }];
}
}
}
export function enrichPublicApiOpenApiSpec<T extends OpenApiDocument>(spec: T): T {
const components = (spec.components ?? {}) as Record<string, unknown>;
const securitySchemes = (components.securitySchemes ?? {}) as Record<string, unknown>;
const paths = (spec.paths ?? {}) as Record<string, unknown>;
const info = (spec.info ?? {}) as Record<string, unknown>;
applySecurityToOperations(paths);
return {
...spec,
info: {
...info,
title: typeof info.title === 'string' ? info.title : 'ClassroomIO Public API',
version: typeof info.version === 'string' ? info.version : '1.0.0',
description: PUBLIC_API_OPENAPI_DESCRIPTION
},
paths,
components: {
...components,
securitySchemes: {
...securitySchemes,
[PUBLIC_API_BEARER_SCHEME]: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'API Key',
description:
'Organization API key from Automation → API in your org dashboard. Paste the full `cio_api_...` value as the Bearer token.'
}
}
},
security: [{ [PUBLIC_API_BEARER_SCHEME]: [] }]
};
}
+1
View File
@@ -1,5 +1,6 @@
export * from './constants';
export * from './automation';
export * from './public-api';
export * from './utils';
export * from './token-packs';
+13
View File
@@ -0,0 +1,13 @@
import { PLAN } from './constants';
export function canUsePublicApi(planName: string | null | undefined, isSelfHosted: boolean): boolean {
if (!planName || planName === PLAN.BASIC) {
return false;
}
if (isSelfHosted) {
return planName === PLAN.ENTERPRISE;
}
return planName === PLAN.EARLY_ADOPTER || planName === PLAN.ENTERPRISE;
}
+3 -77
View File
@@ -612,6 +612,9 @@ importers:
apps/docs:
dependencies:
'@cio/utils':
specifier: workspace:*
version: link:../../packages/utils
'@react-router/dev':
specifier: ^7.9.4
version: 7.13.2(@react-router/[email protected]([email protected]([email protected]([email protected]))([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected])([email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected](@cloudflare/[email protected])([email protected])([email protected]))([email protected])
@@ -4099,183 +4102,155 @@ packages:
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/[email protected]':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/[email protected]':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/[email protected]':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/[email protected]':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/[email protected]':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/[email protected]':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/[email protected]':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/[email protected]':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/[email protected]':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
@@ -4820,56 +4795,48 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-parser/[email protected]':
resolution: {integrity: sha512-3R0iqjM3xYOZCnwgcxOQXH7hrz64/USDIuLbNTM1kZqQzRqaR4w7SwoWKU934zABo8d0op2oSwOp+CV3hZnM7A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-parser/[email protected]':
resolution: {integrity: sha512-lAQf8PQxfgy7h0bmcfSVE3hg3qMueshPYULFsCrHM+8KefGZ9W+ZMvRyU33gLrB4w1O3Fz1orR0hmKMCRxXNrQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxc-parser/[email protected]':
resolution: {integrity: sha512-2QlvQBUhHuAE3ezD4X3CAEKMXdfgInggQ5Bj/7gb5NcYP3GyfLTj7c+mMu+BRwfC9B3AXBNyqHWbqEuuUvZyRQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxc-parser/[email protected]':
resolution: {integrity: sha512-v06iu0osHszgqJ1dLQRb6leWFU1sjG/UQk4MoVBtE6ZPewgfTkby6G9II1SpEAf2onnAuQceVYxQH9iuU3NJqw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxc-parser/[email protected]':
resolution: {integrity: sha512-+5HhNHtxsdcd7+ljXFnn9FOoCNXJX3UPgIfIE6vdwS1HqdGNH6eAcVobuqGOp54l8pvcxDQA6F4cPswCgLrQfQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxc-parser/[email protected]':
resolution: {integrity: sha512-jKwO7ZLNkjxwg7FoCLw+fJszooL9yXRZsDN0AQ1AQUTWq1l8GH/2e44k68N3fcP19jl8O8jGpqLAZcQTYk6skA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-parser/[email protected]':
resolution: {integrity: sha512-TYqnuKV/p3eOc+N61E0961nA7DC+gaCeJ3+V2LcjJdTwFMdikqWL6uVk1jlrpUCBrozHDATVUKDZYH7r4FQYjQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-parser/[email protected]':
resolution: {integrity: sha512-ZhrVmWFifVEFQX4XPwLoVFDHw9tAWH9p9vHsHFH+5uCKdfVR+jje4WxVo6YrokWCboGckoOzHq5KKMOcPZfkRg==}
@@ -4932,42 +4899,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/[email protected]':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/[email protected]':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/[email protected]':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/[email protected]':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/[email protected]':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/[email protected]':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -5604,67 +5565,56 @@ packages:
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/[email protected]':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/[email protected]':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/[email protected]':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/[email protected]':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/[email protected]':
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
@@ -6684,84 +6634,72 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/[email protected]':
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
@@ -10867,84 +10805,72 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
[email protected]:
resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
[email protected]:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
[email protected]:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
[email protected]:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
[email protected]:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
[email protected]:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
[email protected]:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
[email protected]:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
[email protected]:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
[email protected]:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
[email protected]:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
[email protected]:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}