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==}