From fe9f8c441f6388e49b955f4e7cc9a81a45ad6a10 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Thu, 2 Jul 2026 11:24:33 +0100 Subject: [PATCH] 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 * 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 * 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 * ci(openapi): republish spec when public API OpenAPI helpers change Co-authored-by: Rotimi Best * feat(dashboard): add Go snippet to API quick-start examples Co-authored-by: Rotimi Best * docs(api): use /org/*/api and clarify dashboard key generation path Co-authored-by: Rotimi Best * 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 --------- Co-authored-by: Cursor Agent Co-authored-by: Rotimi Best --- .github/workflows/upload-openapi-spec.yml | 1 + apps/api/scripts/upload-openapi-spec.ts | 10 +-- .../services/organization/automation-usage.ts | 17 +++- .../lib/features/automation/pages/api.svelte | 85 +++++++++++++++--- .../automation/utils/automation-utils.ts | 88 ++++++++++++++++++ .../src/lib/features/ui/upgrade-banner.svelte | 7 +- apps/dashboard/src/lib/utils/store/org.ts | 8 +- .../src/lib/utils/translations/da.json | 13 ++- .../src/lib/utils/translations/de.json | 13 ++- .../src/lib/utils/translations/en.json | 13 ++- .../src/lib/utils/translations/es.json | 13 ++- .../src/lib/utils/translations/fr.json | 13 ++- .../src/lib/utils/translations/hi.json | 13 ++- .../src/lib/utils/translations/pl.json | 13 ++- .../src/lib/utils/translations/pt.json | 13 ++- .../src/lib/utils/translations/ru.json | 13 ++- .../src/lib/utils/translations/vi.json | 13 ++- apps/docs/app/routes/api-reference.ts | 25 ++++-- apps/docs/content/docs/api.mdx | 54 ++++++++--- apps/docs/package.json | 1 + packages/utils/package.json | 4 + packages/utils/src/openapi/public-api.ts | 89 +++++++++++++++++++ packages/utils/src/plans/index.ts | 1 + packages/utils/src/plans/public-api.ts | 13 +++ pnpm-lock.yaml | 80 +---------------- 25 files changed, 476 insertions(+), 137 deletions(-) create mode 100644 packages/utils/src/openapi/public-api.ts create mode 100644 packages/utils/src/plans/public-api.ts diff --git a/.github/workflows/upload-openapi-spec.yml b/.github/workflows/upload-openapi-spec.yml index e506c5e8..167cc2e8 100644 --- a/.github/workflows/upload-openapi-spec.yml +++ b/.github/workflows/upload-openapi-spec.yml @@ -5,6 +5,7 @@ on: branches: [main] paths: - 'apps/api/**' + - 'packages/utils/src/openapi/**' workflow_dispatch: env: diff --git a/apps/api/scripts/upload-openapi-spec.ts b/apps/api/scripts/upload-openapi-spec.ts index 0071acca..f51e7594 100644 --- a/apps/api/scripts/upload-openapi-spec.ts +++ b/apps/api/scripts/upload-openapi-spec.ts @@ -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) { }) : 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 { diff --git a/apps/api/src/services/organization/automation-usage.ts b/apps/api/src/services/organization/automation-usage.ts index f4fdbea7..c41fb960 100644 --- a/apps/api/src/services/organization/automation-usage.ts +++ b/apps/api/src/services/organization/automation-usage.ts @@ -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 { 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) { diff --git a/apps/dashboard/src/lib/features/automation/pages/api.svelte b/apps/dashboard/src/lib/features/automation/pages/api.svelte index 8a6b4e52..95b74ecb 100644 --- a/apps/dashboard/src/lib/features/automation/pages/api.svelte +++ b/apps/dashboard/src/lib/features/automation/pages/api.svelte @@ -1,9 +1,16 @@ - {$t('upgrade.enterprise_required')} + {$t(upgradeMessageKey)} {#if !$isOrgAdmin} @@ -174,7 +188,7 @@ variant="page" > {#if $isOrgAdmin} - @@ -183,6 +197,57 @@ {/if} {/if} + + {#if hasActiveApiKey} + + + + {$t('automation.api.setup.title')} + + {$t('automation.api.setup.description')} + + + + {$t('automation.clients.curl')} + {$t('automation.clients.javascript')} + {$t('automation.clients.python')} + {$t('automation.clients.go')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/if} = 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 ?? ''; } diff --git a/apps/dashboard/src/lib/features/ui/upgrade-banner.svelte b/apps/dashboard/src/lib/features/ui/upgrade-banner.svelte index 1bd9769c..e1279064 100644 --- a/apps/dashboard/src/lib/features/ui/upgrade-banner.svelte +++ b/apps/dashboard/src/lib/features/ui/upgrade-banner.svelte @@ -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); -{#if $isFreePlan} +{#if shouldShow} {#snippet children(isHovered)} diff --git a/apps/dashboard/src/lib/utils/store/org.ts b/apps/dashboard/src/lib/utils/store/org.ts index 992ff0ef..98a54880 100644 --- a/apps/dashboard/src/lib/utils/store/org.ts +++ b/apps/dashboard/src/lib/utils/store/org.ts @@ -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 diff --git a/apps/dashboard/src/lib/utils/translations/da.json b/apps/dashboard/src/lib/utils/translations/da.json index 82eb5285..75d9f42d 100644 --- a/apps/dashboard/src/lib/utils/translations/da.json +++ b/apps/dashboard/src/lib/utils/translations/da.json @@ -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", diff --git a/apps/dashboard/src/lib/utils/translations/de.json b/apps/dashboard/src/lib/utils/translations/de.json index 38b50088..ad454013 100644 --- a/apps/dashboard/src/lib/utils/translations/de.json +++ b/apps/dashboard/src/lib/utils/translations/de.json @@ -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", diff --git a/apps/dashboard/src/lib/utils/translations/en.json b/apps/dashboard/src/lib/utils/translations/en.json index 4cfdbb12..f81e66ba 100644 --- a/apps/dashboard/src/lib/utils/translations/en.json +++ b/apps/dashboard/src/lib/utils/translations/en.json @@ -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": { diff --git a/apps/dashboard/src/lib/utils/translations/es.json b/apps/dashboard/src/lib/utils/translations/es.json index 41222779..a07da282 100644 --- a/apps/dashboard/src/lib/utils/translations/es.json +++ b/apps/dashboard/src/lib/utils/translations/es.json @@ -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", diff --git a/apps/dashboard/src/lib/utils/translations/fr.json b/apps/dashboard/src/lib/utils/translations/fr.json index b32a15dd..f579dc80 100644 --- a/apps/dashboard/src/lib/utils/translations/fr.json +++ b/apps/dashboard/src/lib/utils/translations/fr.json @@ -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", diff --git a/apps/dashboard/src/lib/utils/translations/hi.json b/apps/dashboard/src/lib/utils/translations/hi.json index dbe3fc8b..93fc3343 100644 --- a/apps/dashboard/src/lib/utils/translations/hi.json +++ b/apps/dashboard/src/lib/utils/translations/hi.json @@ -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": "एम.सी.पी", diff --git a/apps/dashboard/src/lib/utils/translations/pl.json b/apps/dashboard/src/lib/utils/translations/pl.json index 707f2a68..7b64f62f 100644 --- a/apps/dashboard/src/lib/utils/translations/pl.json +++ b/apps/dashboard/src/lib/utils/translations/pl.json @@ -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", diff --git a/apps/dashboard/src/lib/utils/translations/pt.json b/apps/dashboard/src/lib/utils/translations/pt.json index c66e8134..13b98032 100644 --- a/apps/dashboard/src/lib/utils/translations/pt.json +++ b/apps/dashboard/src/lib/utils/translations/pt.json @@ -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", diff --git a/apps/dashboard/src/lib/utils/translations/ru.json b/apps/dashboard/src/lib/utils/translations/ru.json index 0e3b8c17..bf7dfb2b 100644 --- a/apps/dashboard/src/lib/utils/translations/ru.json +++ b/apps/dashboard/src/lib/utils/translations/ru.json @@ -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": "МКП", diff --git a/apps/dashboard/src/lib/utils/translations/vi.json b/apps/dashboard/src/lib/utils/translations/vi.json index 254924e0..543653e6 100644 --- a/apps/dashboard/src/lib/utils/translations/vi.json +++ b/apps/dashboard/src/lib/utils/translations/vi.json @@ -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", diff --git a/apps/docs/app/routes/api-reference.ts b/apps/docs/app/routes/api-reference.ts index fa139ac7..410748db 100644 --- a/apps/docs/app/routes/api-reference.ts +++ b/apps/docs/app/routes/api-reference.ts @@ -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); -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' } } ); diff --git a/apps/docs/content/docs/api.mdx b/apps/docs/content/docs/api.mdx index baa87c95..285506a1 100644 --- a/apps/docs/content/docs/api.mdx +++ b/apps/docs/content/docs/api.mdx @@ -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**. + + + + Sign in to [ClassroomIO](https://app.classroomio.com). + + + In the org sidebar, open the **Automation** section, then **API**. + + + Or go directly to [`/org/*/api`](https://app.classroomio.com/org/*/api). The `*` picks your current organization automatically. + + + Click **Generate API key**, name the key, and copy the secret immediately. Keys are shown only once and look like `cio_api_...`. + + + Send the key on every request as `Authorization: Bearer cio_api_your_key_here`. + + + + + On ClassroomIO Cloud, API key creation is available on **Early Adopter** and **Enterprise** plans. Self-hosted deployments require **Enterprise**. + ## 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. - - API key creation is available only on Early Adopter and Enterprise plans. - +### 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 " -``` - ## Notes - API keys are organization-scoped. diff --git a/apps/docs/package.json b/apps/docs/package.json index 2910d675..d5d3a02e 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -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", diff --git a/packages/utils/package.json b/packages/utils/package.json index 34eb53bc..b1d6842f 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -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": { diff --git a/packages/utils/src/openapi/public-api.ts b/packages/utils/src/openapi/public-api.ts new file mode 100644 index 00000000..d4424fd2 --- /dev/null +++ b/packages/utils/src/openapi/public-api.ts @@ -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; + +function applySecurityToOperations(paths: Record) { + for (const pathItem of Object.values(paths)) { + if (!pathItem || typeof pathItem !== 'object') { + continue; + } + + for (const method of HTTP_METHODS) { + const operation = (pathItem as Record)[method]; + + if (!operation || typeof operation !== 'object') { + continue; + } + + (operation as Record).security = [{ [PUBLIC_API_BEARER_SCHEME]: [] }]; + } + } +} + +export function enrichPublicApiOpenApiSpec(spec: T): T { + const components = (spec.components ?? {}) as Record; + const securitySchemes = (components.securitySchemes ?? {}) as Record; + const paths = (spec.paths ?? {}) as Record; + const info = (spec.info ?? {}) as Record; + + 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]: [] }] + }; +} diff --git a/packages/utils/src/plans/index.ts b/packages/utils/src/plans/index.ts index 4014e1a7..8568e059 100644 --- a/packages/utils/src/plans/index.ts +++ b/packages/utils/src/plans/index.ts @@ -1,5 +1,6 @@ export * from './constants'; export * from './automation'; +export * from './public-api'; export * from './utils'; export * from './token-packs'; diff --git a/packages/utils/src/plans/public-api.ts b/packages/utils/src/plans/public-api.ts new file mode 100644 index 00000000..75775dbb --- /dev/null +++ b/packages/utils/src/plans/public-api.ts @@ -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; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 585e8d83..7b11137d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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/serve@7.13.2(react-router@7.13.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3))(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(react-router@7.13.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(sass@1.97.1)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.97.1)(tsx@4.21.0)(yaml@2.8.2))(wrangler@4.86.0(@cloudflare/workers-types@4.20260511.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(yaml@2.8.2) @@ -4099,183 +4102,155 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm64@0.34.5': 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/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.5': 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/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': 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/sharp-linux-s390x@0.33.5': 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/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.33.5': 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/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': 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/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': 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/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.33.5': 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/binding-linux-arm64-musl@0.112.0': resolution: {integrity: sha512-3R0iqjM3xYOZCnwgcxOQXH7hrz64/USDIuLbNTM1kZqQzRqaR4w7SwoWKU934zABo8d0op2oSwOp+CV3hZnM7A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@oxc-parser/binding-linux-ppc64-gnu@0.112.0': resolution: {integrity: sha512-lAQf8PQxfgy7h0bmcfSVE3hg3qMueshPYULFsCrHM+8KefGZ9W+ZMvRyU33gLrB4w1O3Fz1orR0hmKMCRxXNrQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-riscv64-gnu@0.112.0': resolution: {integrity: sha512-2QlvQBUhHuAE3ezD4X3CAEKMXdfgInggQ5Bj/7gb5NcYP3GyfLTj7c+mMu+BRwfC9B3AXBNyqHWbqEuuUvZyRQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-riscv64-musl@0.112.0': resolution: {integrity: sha512-v06iu0osHszgqJ1dLQRb6leWFU1sjG/UQk4MoVBtE6ZPewgfTkby6G9II1SpEAf2onnAuQceVYxQH9iuU3NJqw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - libc: [musl] '@oxc-parser/binding-linux-s390x-gnu@0.112.0': resolution: {integrity: sha512-+5HhNHtxsdcd7+ljXFnn9FOoCNXJX3UPgIfIE6vdwS1HqdGNH6eAcVobuqGOp54l8pvcxDQA6F4cPswCgLrQfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-x64-gnu@0.112.0': resolution: {integrity: sha512-jKwO7ZLNkjxwg7FoCLw+fJszooL9yXRZsDN0AQ1AQUTWq1l8GH/2e44k68N3fcP19jl8O8jGpqLAZcQTYk6skA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.112.0': resolution: {integrity: sha512-TYqnuKV/p3eOc+N61E0961nA7DC+gaCeJ3+V2LcjJdTwFMdikqWL6uVk1jlrpUCBrozHDATVUKDZYH7r4FQYjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@oxc-parser/binding-openharmony-arm64@0.112.0': resolution: {integrity: sha512-ZhrVmWFifVEFQX4XPwLoVFDHw9tAWH9p9vHsHFH+5uCKdfVR+jje4WxVo6YrokWCboGckoOzHq5KKMOcPZfkRg==} @@ -4932,42 +4899,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -5604,67 +5565,56 @@ packages: resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.54.0': resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.54.0': resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.54.0': resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.54.0': resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.54.0': resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.54.0': resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.54.0': resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.54.0': resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.54.0': resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.54.0': resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openharmony-arm64@4.54.0': resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} @@ -6684,84 +6634,72 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-gnu@4.2.1': resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -10867,84 +10805,72 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-gnu@1.31.1: resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}