import type { ConsoleClient } from '~/services/consoleClient' import type { Database } from '~/types/database.types' import { createDefaultApiKey, findUsablePlainApiKey, shareInFlightApiKeyLoad } from '~/services/apikeys' import { checkPermissions } from '~/services/permissions' /** Console tasks a copied AI agent prompt can perform. */ export type AiAgentKeyTask = 'add-app' | 'upload-bundle' | 'create-channel' | 'request-build' /** * App roles whose key can finish each app task. Uploading to a channel needs * app.upload_bundle and channel.promote_bundle; creating a channel with --default or * --self-assign also needs channel.update_settings; a native build uses the signing stored * on Capgo and a new platform stores signing, both needing app settings rights. Only * app_admin grants those settings rights. */ export function agentKeyAppRoles(task: AiAgentKeyTask): readonly string[] | undefined { if (task === 'add-app') return undefined return task === 'upload-bundle' ? ['app_admin', 'app_developer', 'app_uploader'] : ['app_admin'] } export interface AgentKeyScope { userId: string orgId: string /** Every task except `add-app`. */ appId: string | null task: AiAgentKeyTask } interface AgentKeyPlan { /** A live key the user already owns that can do the task. */ existingKey: string | null /** The role a new key may get, or null when the user cannot create a key that works. */ createRole: 'org_admin' | 'app_admin' | 'app_developer' | null } /** * Which key the prompt would carry. Only someone who can manage user roles may give a key * org_admin or app_admin (the API rejects it otherwise). Uploads only need app_developer, * which an org admin or an app API key manager may create. */ async function agentKeyPlan(capgo: ConsoleClient, scope: AgentKeyScope): Promise { const appRoles = agentKeyAppRoles(scope.task) const [existingKey, canAssignAdminRoles] = await Promise.all([ findUsablePlainApiKey(capgo, scope.userId, scope.orgId, scope.appId, { appRoles }), checkPermissions(['org.update_user_roles', 'org.manage_apikeys'], { orgId: scope.orgId }).catch(() => false), ]) // The key goes to a third-party agent: an upload never gets more than app_developer. if (scope.task === 'upload-bundle') { if (!scope.appId) return { existingKey, createRole: null } const canCreateDeveloperKey = canAssignAdminRoles || await checkPermissions('app.manage_apikeys', { appId: scope.appId }).catch(() => false) return { existingKey, createRole: canCreateDeveloperKey ? 'app_developer' : null } } if (canAssignAdminRoles) return { existingKey, createRole: scope.task === 'add-app' ? 'org_admin' : 'app_admin' } return { existingKey, createRole: null } } /** Whether a copied prompt can carry a working key, so the AI option is never a dead end. */ export async function canPrepareAgentKey(capgo: ConsoleClient, scope: AgentKeyScope): Promise { try { const plan = await agentKeyPlan(capgo, scope) return !!plan.existingKey || !!plan.createRole } catch (error) { console.warn('Cannot check the API key for the AI agent prompt', error) return false } } /** Reuses a key the user owns for the task, otherwise creates the narrowest one they may create. */ export function loadAgentKey(capgo: ConsoleClient, scope: AgentKeyScope, keyName: string): Promise { const appRoles = agentKeyAppRoles(scope.task) return shareInFlightApiKeyLoad({ userId: scope.userId, orgId: scope.orgId, appId: scope.appId, appRoles }, async () => { const plan = await agentKeyPlan(capgo, scope) if (plan.existingKey) return plan.existingKey if (!plan.createRole) return null const { data, error } = await createDefaultApiKey(capgo, keyName, plan.createRole === 'org_admin' ? { orgId: scope.orgId } : { appId: scope.appId, appRole: plan.createRole }) if (error) throw error return typeof data?.key === 'string' ? data.key : findUsablePlainApiKey(capgo, scope.userId, scope.orgId, scope.appId, { appRoles }) }) }