fix: improve MCP page setup UX and remove automation credit quotas (#821)
* Remove MCP credit quotas and improve MCP setup page - Always show client setup snippets with placeholder keys - Add View Docs link to MCP page header - Remove monthly automation credit enforcement and UI - Update plan feature text to reflect free MCP access Co-authored-by: Rotimi Best <[email protected]> * fix: address PR review feedback for MCP page changes - Clear generatedSecret when revoking MCP keys - Use Button link mode for View Docs with rel noopener noreferrer - Update all dashboard locale files for MCP translations - Make MCP rate-limit admission atomic with transactional reservations - Wrap MCP route handlers with withMcpAutomationUsage helper Co-authored-by: Rotimi Best <[email protected]> * revert: remove atomic MCP rate-limit reservations The transactional advisory-lock reservation was unnecessary complexity for our MCP usage model. Restore the simpler check-then-record flow. Also only clear generatedSecret when revoking the key it belongs to. Co-authored-by: Rotimi Best <[email protected]> * fix: address remaining MCP page bot review feedback - Add blank lines after confirm guards in key handlers - Skip showing rotated secret if key was revoked during rotation - Fix vi keys_title and de rate_limits_title translations Co-authored-by: Rotimi Best <[email protected]> * fix: prevent late key rotation from un-revoking or exposing secrets - Rotate only when revoked_at IS NULL at update time (DB) - Reject rotate requests for already-revoked keys (API) - Ignore stale rotate responses in automation client state - Guard MCP UI against showing secrets for revoked keys Co-authored-by: Rotimi Best <[email protected]> ---------
This commit is contained in:
co-authored by
Rotimi Best
parent
1cb752d229
commit
9fb75e11d2
@@ -13,6 +13,7 @@ import {
|
||||
getActiveOrganizationApiKeyByHash,
|
||||
getOrganizationApiKeyById,
|
||||
listOrganizationApiKeys,
|
||||
rotateOrganizationApiKeyIfActive,
|
||||
updateOrganizationApiKey
|
||||
} from '@cio/db/queries/organization';
|
||||
import { assertOrganizationAutomationKeyCreationAllowed } from './automation-usage';
|
||||
@@ -135,16 +136,26 @@ export async function rotateOrganizationApiKeyService(organizationId: string, ke
|
||||
throw new AppError('Organization API key not found', ErrorCodes.ORGANIZATION_API_KEY_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
if (existing.revokedAt) {
|
||||
throw new AppError(
|
||||
'Cannot rotate a revoked organization API key',
|
||||
ErrorCodes.ORGANIZATION_API_KEY_ROTATE_FAILED,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const secret = generateOrganizationApiKeySecret(existing.type);
|
||||
const updated = await updateOrganizationApiKey(organizationId, keyId, {
|
||||
const updated = await rotateOrganizationApiKeyIfActive(organizationId, keyId, {
|
||||
secretPrefix: getSecretPrefix(secret),
|
||||
secretHash: hashSecret(secret),
|
||||
revokedAt: null,
|
||||
lastUsedAt: null
|
||||
secretHash: hashSecret(secret)
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
throw new AppError('Failed to rotate organization API key', ErrorCodes.ORGANIZATION_API_KEY_ROTATE_FAILED, 500);
|
||||
throw new AppError(
|
||||
'Organization API key was revoked before rotation completed',
|
||||
ErrorCodes.ORGANIZATION_API_KEY_ROTATE_FAILED,
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
countOrganizationAutomationUsageSinceByKey,
|
||||
createOrganizationAutomationUsage,
|
||||
getActiveOrganizationPlan,
|
||||
getOrganizationAutomationCreditsUsedSince,
|
||||
listRecentOrganizationAutomationUsage
|
||||
} from '@cio/db/queries/organization';
|
||||
import type { TOrganizationApiKey, TOrganizationApiKeyType, TPlan } from '@db/types';
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
getMcpAutomationCategory,
|
||||
canUsePublicApi,
|
||||
getMcpAutomationLimits,
|
||||
MCP_TOOL_CREDIT_COST,
|
||||
type TMcpToolName
|
||||
} from '@cio/utils/plans';
|
||||
|
||||
@@ -24,9 +22,6 @@ export type OrganizationAutomationUsageSummary = {
|
||||
planName: TPlan;
|
||||
billingPeriodStart: string;
|
||||
billingPeriodEnd: string;
|
||||
monthlyCreditsIncluded: number;
|
||||
monthlyCreditsUsed: number;
|
||||
monthlyCreditsRemaining: number;
|
||||
activeKeys: number;
|
||||
maxActiveKeys: number;
|
||||
rateLimits: ReturnType<typeof getMcpAutomationLimits>['rateLimits'];
|
||||
@@ -104,9 +99,6 @@ export async function getOrganizationAutomationUsageSummaryService(
|
||||
planName,
|
||||
billingPeriodStart: billingPeriodStart.toISOString(),
|
||||
billingPeriodEnd: billingPeriodEnd.toISOString(),
|
||||
monthlyCreditsIncluded: 0,
|
||||
monthlyCreditsUsed: 0,
|
||||
monthlyCreditsRemaining: 0,
|
||||
activeKeys: await countActiveOrganizationApiKeys(organizationId, type),
|
||||
maxActiveKeys: 0,
|
||||
rateLimits: {
|
||||
@@ -118,8 +110,7 @@ export async function getOrganizationAutomationUsageSummaryService(
|
||||
}
|
||||
|
||||
const limits = getMcpAutomationLimits(planName);
|
||||
const [monthlyCreditsUsed, activeKeys, recentActions] = await Promise.all([
|
||||
getOrganizationAutomationCreditsUsedSince(organizationId, type, billingPeriodStart.toISOString()),
|
||||
const [activeKeys, recentActions] = await Promise.all([
|
||||
countActiveOrganizationApiKeys(organizationId, type),
|
||||
listRecentOrganizationAutomationUsage(organizationId, type, 10)
|
||||
]);
|
||||
@@ -129,9 +120,6 @@ export async function getOrganizationAutomationUsageSummaryService(
|
||||
planName,
|
||||
billingPeriodStart: billingPeriodStart.toISOString(),
|
||||
billingPeriodEnd: billingPeriodEnd.toISOString(),
|
||||
monthlyCreditsIncluded: limits.monthlyCredits,
|
||||
monthlyCreditsUsed,
|
||||
monthlyCreditsRemaining: Math.max(limits.monthlyCredits - monthlyCreditsUsed, 0),
|
||||
activeKeys,
|
||||
maxActiveKeys: limits.maxActiveKeys,
|
||||
rateLimits: limits.rateLimits,
|
||||
@@ -151,24 +139,13 @@ export async function assertMcpAutomationUsageAllowed(
|
||||
const limits = getMcpAutomationLimits(planName);
|
||||
const category = getMcpAutomationCategory(toolName);
|
||||
const now = new Date();
|
||||
const billingPeriodStart = getBillingPeriodStart(now).toISOString();
|
||||
const minuteWindowStart = getMinuteWindowStart(now).toISOString();
|
||||
|
||||
const [creditsUsed, keyRequestsInWindow, orgRequestsInWindow] = await Promise.all([
|
||||
getOrganizationAutomationCreditsUsedSince(automationKey.organizationId, automationKey.type, billingPeriodStart),
|
||||
const [keyRequestsInWindow, orgRequestsInWindow] = await Promise.all([
|
||||
countOrganizationAutomationUsageSinceByKey(automationKey.id, category, minuteWindowStart),
|
||||
countOrganizationAutomationUsageSince(automationKey.organizationId, automationKey.type, category, minuteWindowStart)
|
||||
]);
|
||||
|
||||
const creditsNeeded = MCP_TOOL_CREDIT_COST[toolName];
|
||||
if (creditsUsed + creditsNeeded > limits.monthlyCredits) {
|
||||
throw new AppError(
|
||||
'Monthly automation credits exhausted for this organization',
|
||||
ErrorCodes.AUTOMATION_CREDIT_LIMIT_EXCEEDED,
|
||||
429
|
||||
);
|
||||
}
|
||||
|
||||
const keyLimit =
|
||||
category === 'read'
|
||||
? limits.rateLimits.perKey.readPerMinute
|
||||
@@ -199,7 +176,7 @@ export async function recordMcpAutomationUsage(
|
||||
type: automationKey.type,
|
||||
action: toolName,
|
||||
category: getMcpAutomationCategory(toolName),
|
||||
creditsConsumed: MCP_TOOL_CREDIT_COST[toolName],
|
||||
creditsConsumed: 0,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,6 +99,12 @@ class AutomationApi extends BaseApiWithErrors {
|
||||
requestFn: () => classroomio.organization.automation.keys[':keyId'].rotate.$post({ param: { keyId } }),
|
||||
logContext: 'rotating automation key',
|
||||
onSuccess: (response) => {
|
||||
const existingKey = this.keys.find((key) => key.id === response.data.key.id);
|
||||
if (existingKey?.revokedAt) {
|
||||
this.generatedSecret = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.generatedSecret = response.data.secret;
|
||||
this.keys = this.keys.map((key) => (key.id === response.data.key.id ? response.data.key : key));
|
||||
snackbar.success('snackbar.automation.rotated');
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
import * as Tabs from '@cio/ui/base/tabs';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import CoinsIcon from '@lucide/svelte/icons/coins';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
import GaugeIcon from '@lucide/svelte/icons/gauge';
|
||||
import KeyIcon from '@lucide/svelte/icons/key';
|
||||
@@ -32,6 +31,7 @@
|
||||
|
||||
let activeSetupTab = $state('cursor');
|
||||
let generatedSecret = $state<string | null>(null);
|
||||
let generatedSecretKeyId = $state<string | null>(null);
|
||||
let isCreateKeyModalOpen = $state(false);
|
||||
let keyLabel = $state('');
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
if (!result) return;
|
||||
|
||||
generatedSecret = automationApi.generatedSecret;
|
||||
generatedSecretKeyId = result.key.id;
|
||||
automationApi.clearGeneratedSecret();
|
||||
isCreateKeyModalOpen = false;
|
||||
resetCreateKeyModal();
|
||||
@@ -59,13 +60,25 @@
|
||||
|
||||
async function onRevokeKey(keyId: string) {
|
||||
if (!confirm(t.get('automation.keys.revoke_confirm'))) return;
|
||||
await automationApi.revokeKey(keyId);
|
||||
|
||||
const result = await automationApi.revokeKey(keyId);
|
||||
if (result && generatedSecretKeyId === keyId) {
|
||||
generatedSecret = null;
|
||||
generatedSecretKeyId = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function onRotateKey(keyId: string) {
|
||||
if (!confirm(t.get('automation.keys.rotate_confirm'))) return;
|
||||
await automationApi.rotateKey(keyId);
|
||||
|
||||
const result = await automationApi.rotateKey(keyId);
|
||||
if (!result || result.key.revokedAt) return;
|
||||
|
||||
const key = automationApi.keys.find((entry) => entry.id === keyId);
|
||||
if (!key || key.revokedAt) return;
|
||||
|
||||
generatedSecret = automationApi.generatedSecret;
|
||||
generatedSecretKeyId = keyId;
|
||||
automationApi.clearGeneratedSecret();
|
||||
}
|
||||
const mcpKeys = $derived(automationApi.keys.filter((key) => key.type === 'mcp'));
|
||||
@@ -73,13 +86,6 @@
|
||||
if (!automationApi.usage) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
icon: CoinsIcon,
|
||||
title: t.get('automation.mcp.usage.credits_title'),
|
||||
percentage: automationApi.usage.monthlyCreditsRemaining,
|
||||
description: `${t.get('automation.mcp.usage.used_label')} ${automationApi.usage.monthlyCreditsUsed} / ${automationApi.usage.monthlyCreditsIncluded}`,
|
||||
hidePercentage: true
|
||||
},
|
||||
{
|
||||
icon: KeyIcon,
|
||||
title: t.get('automation.mcp.usage.keys_title'),
|
||||
@@ -98,7 +104,6 @@
|
||||
});
|
||||
|
||||
const isLastUsageCard = (index: number) => index === usageCards.length - 1;
|
||||
const hasActiveMcpKey = $derived(mcpKeys.some((key) => !key.revokedAt));
|
||||
</script>
|
||||
|
||||
<Field.Group class="mx-auto w-full space-y-2">
|
||||
@@ -112,7 +117,7 @@
|
||||
{/if}
|
||||
|
||||
{#if $isOrgAdmin && automationApi.usage}
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
{#each usageCards as card, i (i)}
|
||||
<ActivityCard
|
||||
activity={card}
|
||||
@@ -228,8 +233,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if hasActiveMcpKey}
|
||||
<Field.Set class="gap-3!">
|
||||
<Field.Set class="gap-3!">
|
||||
<Field.Legend class="flex items-center gap-2">
|
||||
<SparklesIcon class="size-5" />
|
||||
{$t('automation.mcp.setup.title')}
|
||||
@@ -277,7 +281,6 @@
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</Field.Set>
|
||||
{/if}
|
||||
</Field.Group>
|
||||
|
||||
<Dialog.Root
|
||||
|
||||
@@ -4081,16 +4081,15 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Anvendelse og grænser",
|
||||
"description": "MCP er tilgængelig på alle abonnementer, men brugen styres med månedlige automatiseringskreditter og nøglegrænser.",
|
||||
"credits_title": "Kreditter tilbage",
|
||||
"credits_description": "Muterende MCP-handlinger bruger kreditter. Læsehandlinger er gratis.",
|
||||
"description": "MCP er tilgængelig på alle abonnementer. Brugen styres af nøglegrænser og hastighedsgrænser.",
|
||||
"keys_title": "Aktive nøgler",
|
||||
"keys_description": "Brug én MCP-nøgle pr. integration, så adgang kan roteres eller tilbagekaldes rent.",
|
||||
"rate_limits_title": "Satsgrænser",
|
||||
"rate_limits_description": "Læs/skriv/publicér anmodninger tilladt pr. minut.",
|
||||
"used_label": "Brugt:"
|
||||
},
|
||||
"page_subtitle": "Forbind AI-værktøjer til ClassroomIO med organisationsnøgler, du kan rotere når som helst."
|
||||
"page_subtitle": "Forbind AI-værktøjer til ClassroomIO med organisationsnøgler, du kan rotere når som helst.",
|
||||
"view_docs": "Se dokumentation"
|
||||
},
|
||||
"api": {
|
||||
"subtitle": "Administrer API-nøgler til integration med ClassroomIOs offentlige API.",
|
||||
|
||||
@@ -4081,16 +4081,15 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Nutzung und Grenzen",
|
||||
"description": "MCP ist in jedem Plan verfügbar, die Nutzung wird jedoch durch monatliche Automatisierungsguthaben und Schlüssellimits gesteuert.",
|
||||
"credits_title": "Verbleibende Credits",
|
||||
"credits_description": "Mutierende MCP-Aktionen verbrauchen Credits. Leseaktionen sind kostenlos.",
|
||||
"description": "MCP ist in jedem Plan verfügbar. Die Nutzung wird durch Schlüssellimits und Ratenlimits gesteuert.",
|
||||
"keys_title": "Aktive Schlüssel",
|
||||
"keys_description": "Verwenden Sie einen MCP-Schlüssel pro Integration, damit der Zugriff sauber rotiert oder widerrufen werden kann.",
|
||||
"rate_limits_title": "Tarifbegrenzungen",
|
||||
"rate_limits_title": "Anfragenlimits pro Minute",
|
||||
"rate_limits_description": "Zulässige Lese-/Schreib-/Veröffentlichungsanfragen pro Minute.",
|
||||
"used_label": "Verwendet:"
|
||||
},
|
||||
"page_subtitle": "Verbinden Sie KI-Tools mit ClassroomIO über organisationsbezogene Schlüssel, die Sie jederzeit rotieren können."
|
||||
"page_subtitle": "Verbinden Sie KI-Tools mit ClassroomIO über organisationsbezogene Schlüssel, die Sie jederzeit rotieren können.",
|
||||
"view_docs": "Dokumentation anzeigen"
|
||||
},
|
||||
"api": {
|
||||
"subtitle": "Verwalten Sie API-Schlüssel zur Integration mit der öffentlichen ClassroomIO-API.",
|
||||
|
||||
@@ -4015,9 +4015,7 @@
|
||||
"page_subtitle": "Connect AI tools to ClassroomIO with org-scoped keys you can rotate anytime.",
|
||||
"usage": {
|
||||
"title": "Usage and limits",
|
||||
"description": "MCP is available on every plan, but usage is controlled with monthly automation credits and key limits.",
|
||||
"credits_title": "Credits remaining",
|
||||
"credits_description": "Mutating MCP actions consume credits. Read actions are free.",
|
||||
"description": "MCP is available on every plan. Usage is controlled by key limits and rate limits.",
|
||||
"keys_title": "Active keys",
|
||||
"keys_description": "Use one MCP key per integration so access can be rotated or revoked cleanly.",
|
||||
"rate_limits_title": "Rate limits",
|
||||
@@ -4045,7 +4043,8 @@
|
||||
"codex_description": "Adds the ClassroomIO MCP server to Codex with the same org-scoped key.",
|
||||
"cursor_description": "Paste this into `~/.cursor/mcp.json` or your workspace MCP config.",
|
||||
"opencode_description": "Add this to `opencode.json` in your project root or `~/.config/opencode/opencode.json`."
|
||||
}
|
||||
},
|
||||
"view_docs": "View Docs"
|
||||
},
|
||||
"api": {
|
||||
"subtitle": "Manage API keys to integrate with the ClassroomIO public API.",
|
||||
|
||||
@@ -4081,16 +4081,15 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Uso y límites",
|
||||
"description": "MCP está disponible en todos los planes, pero el uso se controla con créditos de automatización mensuales y límites clave.",
|
||||
"credits_title": "Créditos restantes",
|
||||
"credits_description": "Las acciones mutantes de MCP consumen créditos. Las acciones de lectura son gratuitas.",
|
||||
"description": "MCP está disponible en todos los planes. El uso se controla mediante límites de claves y límites de tasa.",
|
||||
"keys_title": "Claves activas",
|
||||
"keys_description": "Utilice una clave MCP por integración para que el acceso se pueda rotar o revocar limpiamente.",
|
||||
"rate_limits_title": "Límites de tarifas",
|
||||
"rate_limits_description": "Solicitudes de lectura/escritura/publicación permitidas por minuto.",
|
||||
"used_label": "Usado:"
|
||||
},
|
||||
"page_subtitle": "Conecta herramientas de IA a ClassroomIO con claves con ámbito de organización que puedes rotar en cualquier momento."
|
||||
"page_subtitle": "Conecta herramientas de IA a ClassroomIO con claves con ámbito de organización que puedes rotar en cualquier momento.",
|
||||
"view_docs": "Ver documentación"
|
||||
},
|
||||
"api": {
|
||||
"subtitle": "Administra las claves API para integrarte con la API pública de ClassroomIO.",
|
||||
|
||||
@@ -4081,16 +4081,15 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Utilisation et limites",
|
||||
"description": "MCP est disponible sur chaque forfait, mais l'utilisation est contrôlée par des crédits d'automatisation mensuels et des limites clés.",
|
||||
"credits_title": "Crédits restants",
|
||||
"credits_description": "Les actions MCP en mutation consomment des crédits. Les actions de lecture sont gratuites.",
|
||||
"description": "MCP est disponible sur tous les forfaits. L'utilisation est contrôlée par les limites de clés et les limites de taux.",
|
||||
"keys_title": "Clés actives",
|
||||
"keys_description": "Utilisez une clé MCP par intégration afin que l'accès puisse être alterné ou révoqué proprement.",
|
||||
"rate_limits_title": "Limites de taux",
|
||||
"rate_limits_description": "Requêtes de lecture/écriture/publication autorisées par minute.",
|
||||
"used_label": "Utilisé :"
|
||||
},
|
||||
"page_subtitle": "Connectez des outils IA à ClassroomIO avec des clés à l'échelle de l'organisation que vous pouvez faire pivoter à tout moment."
|
||||
"page_subtitle": "Connectez des outils IA à ClassroomIO avec des clés à l'échelle de l'organisation que vous pouvez faire pivoter à tout moment.",
|
||||
"view_docs": "Voir la documentation"
|
||||
},
|
||||
"api": {
|
||||
"subtitle": "Gérez les clés API pour intégrer l'API publique ClassroomIO.",
|
||||
|
||||
@@ -4081,16 +4081,15 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "उपयोग और सीमाएँ",
|
||||
"description": "एमसीपी हर योजना पर उपलब्ध है, लेकिन उपयोग को मासिक स्वचालन क्रेडिट और प्रमुख सीमाओं के साथ नियंत्रित किया जाता है।",
|
||||
"credits_title": "क्रेडिट शेष",
|
||||
"credits_description": "MCP क्रियाओं को बदलने से क्रेडिट की खपत होती है। पढ़ें क्रियाएँ निःशुल्क हैं।",
|
||||
"description": "एमसीपी हर योजना पर उपलब्ध है। उपयोग कुंजी सीमाओं और दर सीमाओं से नियंत्रित होता है।",
|
||||
"keys_title": "सक्रिय कुंजियाँ",
|
||||
"keys_description": "प्रति एकीकरण एक एमसीपी कुंजी का उपयोग करें ताकि पहुंच को घुमाया जा सके या सफाई से रद्द किया जा सके।",
|
||||
"rate_limits_title": "दर सीमा",
|
||||
"rate_limits_description": "प्रति मिनट पढ़ने/लिखने/प्रकाशित करने के अनुरोधों की अनुमति है।",
|
||||
"used_label": "प्रयुक्त:"
|
||||
},
|
||||
"page_subtitle": "किसी भी समय घुमाने योग्य संगठन-स्तरीय कुंजियों के साथ AI टूल्स को ClassroomIO से कनेक्ट करें।"
|
||||
"page_subtitle": "किसी भी समय घुमाने योग्य संगठन-स्तरीय कुंजियों के साथ AI टूल्स को ClassroomIO से कनेक्ट करें।",
|
||||
"view_docs": "दस्तावेज़ देखें"
|
||||
},
|
||||
"api": {
|
||||
"subtitle": "ClassroomIO सार्वजनिक API के साथ एकीकृत करने के लिए API कुंजियाँ प्रबंधित करें।",
|
||||
|
||||
@@ -4081,16 +4081,15 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Zastosowanie i ograniczenia",
|
||||
"description": "MCP jest dostępne w każdym planie, ale wykorzystanie jest kontrolowane za pomocą miesięcznych kredytów automatyzacji i limitów kluczowych.",
|
||||
"credits_title": "Pozostały kredyty",
|
||||
"credits_description": "Mutowanie działań MCP zużywa kredyty. Czytanie akcji jest bezpłatne.",
|
||||
"description": "MCP jest dostępne w każdym planie. Wykorzystanie jest kontrolowane przez limity kluczy i limity żądań.",
|
||||
"keys_title": "Aktywne klucze",
|
||||
"keys_description": "Użyj jednego klucza MCP na każdą integrację, aby umożliwić łatwą rotację lub cofnięcie dostępu.",
|
||||
"rate_limits_title": "Limity stawek",
|
||||
"rate_limits_description": "Żądania odczytu/zapisu/publikowania dozwolone na minutę.",
|
||||
"used_label": "Używane:"
|
||||
},
|
||||
"page_subtitle": "Połącz narzędzia AI z ClassroomIO za pomocą kluczy o zasięgu organizacji, które możesz w dowolnym momencie obrócić."
|
||||
"page_subtitle": "Połącz narzędzia AI z ClassroomIO za pomocą kluczy o zasięgu organizacji, które możesz w dowolnym momencie obrócić.",
|
||||
"view_docs": "Zobacz dokumentację"
|
||||
},
|
||||
"api": {
|
||||
"subtitle": "Zarządzaj kluczami API, aby integrować się z publicznym API ClassroomIO.",
|
||||
|
||||
@@ -4081,16 +4081,15 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Uso e limites",
|
||||
"description": "O MCP está disponível em todos os planos, mas o uso é controlado com créditos mensais de automação e limites principais.",
|
||||
"credits_title": "Créditos restantes",
|
||||
"credits_description": "As ações mutantes do MCP consomem créditos. As ações de leitura são gratuitas.",
|
||||
"description": "O MCP está disponível em todos os planos. O uso é controlado por limites de chaves e limites de taxa.",
|
||||
"keys_title": "Chaves ativas",
|
||||
"keys_description": "Use uma chave MCP por integração para que o acesso possa ser alternado ou revogado de forma limpa.",
|
||||
"rate_limits_title": "Limites de taxa",
|
||||
"rate_limits_description": "Solicitações de leitura/gravação/publicação permitidas por minuto.",
|
||||
"used_label": "Usado:"
|
||||
},
|
||||
"page_subtitle": "Conecte ferramentas de IA ao ClassroomIO com chaves da organização que você pode rotacionar a qualquer momento."
|
||||
"page_subtitle": "Conecte ferramentas de IA ao ClassroomIO com chaves da organização que você pode rotacionar a qualquer momento.",
|
||||
"view_docs": "Ver documentação"
|
||||
},
|
||||
"api": {
|
||||
"subtitle": "Gerencie chaves de API para integrar com a API pública do ClassroomIO.",
|
||||
|
||||
@@ -4081,16 +4081,15 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Использование и ограничения",
|
||||
"description": "MCP доступен в каждом плане, но использование контролируется с помощью ежемесячных кредитов на автоматизацию и ключевых лимитов.",
|
||||
"credits_title": "Оставшиеся кредиты",
|
||||
"credits_description": "Мутирующие действия MCP потребляют кредиты. Действия чтения бесплатны.",
|
||||
"description": "MCP доступен в каждом плане. Использование контролируется лимитами ключей и лимитами запросов.",
|
||||
"keys_title": "Активные ключи",
|
||||
"keys_description": "Используйте один ключ MCP для каждой интеграции, чтобы можно было без проблем менять или отзывать доступ.",
|
||||
"rate_limits_title": "Ограничения ставок",
|
||||
"rate_limits_description": "Запросы на чтение/запись/публикацию разрешены в минуту.",
|
||||
"used_label": "Использовано:"
|
||||
},
|
||||
"page_subtitle": "Подключайте ИИ-инструменты к ClassroomIO с помощью ключей организации, которые можно менять в любое время."
|
||||
"page_subtitle": "Подключайте ИИ-инструменты к ClassroomIO с помощью ключей организации, которые можно менять в любое время.",
|
||||
"view_docs": "Документация"
|
||||
},
|
||||
"api": {
|
||||
"subtitle": "Управляйте API-ключами для интеграции с публичным API ClassroomIO.",
|
||||
|
||||
@@ -4081,16 +4081,15 @@
|
||||
},
|
||||
"usage": {
|
||||
"title": "Cách sử dụng và giới hạn",
|
||||
"description": "MCP có sẵn trên mọi gói, nhưng việc sử dụng được kiểm soát bằng tín dụng tự động hóa hàng tháng và các giới hạn chính.",
|
||||
"credits_title": "Tín dụng còn lại",
|
||||
"credits_description": "Các hành động MCP đột biến sẽ tiêu tốn tín dụng. Hành động đọc là miễn phí.",
|
||||
"keys_title": "Phím hoạt động",
|
||||
"description": "MCP có sẵn trên mọi gói. Việc sử dụng được kiểm soát bằng giới hạn khóa và giới hạn tần suất.",
|
||||
"keys_title": "Khóa đang hoạt động",
|
||||
"keys_description": "Sử dụng một khóa MCP cho mỗi lần tích hợp để có thể luân chuyển hoặc thu hồi quyền truy cập một cách dễ dàng.",
|
||||
"rate_limits_title": "Giới hạn tỷ lệ",
|
||||
"rate_limits_description": "Yêu cầu đọc/ghi/xuất bản được phép mỗi phút.",
|
||||
"used_label": "Đã sử dụng:"
|
||||
},
|
||||
"page_subtitle": "Kết nối công cụ AI với ClassroomIO bằng khóa trong phạm vi tổ chức mà bạn có thể xoay bất cứ lúc nào."
|
||||
"page_subtitle": "Kết nối công cụ AI với ClassroomIO bằng khóa trong phạm vi tổ chức mà bạn có thể xoay bất cứ lúc nào.",
|
||||
"view_docs": "Xem tài liệu"
|
||||
},
|
||||
"api": {
|
||||
"subtitle": "Quản lý khóa API để tích hợp với API công khai ClassroomIO.",
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { automationApi } from '$features/automation/api/automation.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import * as Page from '@cio/ui/base/page';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -24,6 +26,17 @@
|
||||
<Page.Title>{$t('automation.tabs.mcp')}</Page.Title>
|
||||
<Page.Subtitle>{$t('automation.mcp.page_subtitle')}</Page.Subtitle>
|
||||
</Page.HeaderContent>
|
||||
<Page.Action>
|
||||
<Button
|
||||
variant="outline"
|
||||
href="https://classroomio.com/docs/mcp"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ArrowUpRightIcon size={16} />
|
||||
{$t('automation.mcp.view_docs')}
|
||||
</Button>
|
||||
</Page.Action>
|
||||
</Page.Header>
|
||||
|
||||
<Page.Body>
|
||||
|
||||
@@ -119,3 +119,33 @@ export const updateOrganizationApiKey = async (
|
||||
throw new Error('Failed to update organization API key');
|
||||
}
|
||||
};
|
||||
|
||||
export const rotateOrganizationApiKeyIfActive = async (
|
||||
organizationId: string,
|
||||
keyId: string,
|
||||
data: Pick<TOrganizationApiKey, 'secretPrefix' | 'secretHash'>
|
||||
): Promise<TOrganizationApiKey | null> => {
|
||||
try {
|
||||
const [row] = await db
|
||||
.update(schema.organizationApiKey)
|
||||
.set({
|
||||
secretPrefix: data.secretPrefix,
|
||||
secretHash: data.secretHash,
|
||||
lastUsedAt: null,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(schema.organizationApiKey.organizationId, organizationId),
|
||||
eq(schema.organizationApiKey.id, keyId),
|
||||
isNull(schema.organizationApiKey.revokedAt)
|
||||
)
|
||||
)
|
||||
.returning();
|
||||
|
||||
return row || null;
|
||||
} catch (error) {
|
||||
console.error('rotateOrganizationApiKeyIfActive error:', error);
|
||||
throw new Error('Failed to rotate organization API key');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -36,7 +36,6 @@ export type TMcpToolName = keyof typeof MCP_TOOL_CREDIT_COST;
|
||||
export type TAutomationUsageCategory = (typeof AUTOMATION_USAGE_CATEGORY)[keyof typeof AUTOMATION_USAGE_CATEGORY];
|
||||
|
||||
export type McpAutomationLimits = {
|
||||
monthlyCredits: number;
|
||||
maxActiveKeys: number;
|
||||
rateLimits: {
|
||||
perKey: {
|
||||
@@ -54,7 +53,6 @@ export type McpAutomationLimits = {
|
||||
|
||||
export const MCP_AUTOMATION_LIMITS_BY_PLAN: Record<string, McpAutomationLimits> = {
|
||||
[PLAN.BASIC]: {
|
||||
monthlyCredits: 20,
|
||||
maxActiveKeys: 1,
|
||||
rateLimits: {
|
||||
perKey: {
|
||||
@@ -70,7 +68,6 @@ export const MCP_AUTOMATION_LIMITS_BY_PLAN: Record<string, McpAutomationLimits>
|
||||
}
|
||||
},
|
||||
[PLAN.EARLY_ADOPTER]: {
|
||||
monthlyCredits: 500,
|
||||
maxActiveKeys: 5,
|
||||
rateLimits: {
|
||||
perKey: {
|
||||
@@ -86,7 +83,6 @@ export const MCP_AUTOMATION_LIMITS_BY_PLAN: Record<string, McpAutomationLimits>
|
||||
}
|
||||
},
|
||||
[PLAN.ENTERPRISE]: {
|
||||
monthlyCredits: 5000,
|
||||
maxActiveKeys: 25,
|
||||
rateLimits: {
|
||||
perKey: {
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
'20 Students',
|
||||
'Advanced Course Builder (no video upload)',
|
||||
'500K AI credits / month',
|
||||
'MCP access with 20 automation credits / month',
|
||||
'MCP access',
|
||||
'ClassroomIO branding'
|
||||
],
|
||||
CTA: {
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
'10K Students',
|
||||
'Course Builder with Video Upload & Certificate',
|
||||
'3M AI credits / month',
|
||||
'MCP access with 500 automation credits / month',
|
||||
'MCP access',
|
||||
'Custom Branding',
|
||||
'Custom Domain',
|
||||
'API with limits',
|
||||
|
||||
Reference in New Issue
Block a user