/** * CLI-only labels, hints, prompts, and actions for runtime deployment capabilities. * Provider IDs and environment fields are checked against the application catalog at load time. * * @packageDocumentation */ import { ASYNC_JOBS_CAPABILITY, CACHE_CAPABILITY, type CapabilityDefinition, EMAIL_CAPABILITY, ENV_CAPABILITIES, type EnvCapabilityValues, getCapabilityFields, hasEnvCapabilityValue, KNOWLEDGE_EMBEDDINGS_CAPABILITY, OAUTH_CLIENT_CAPABILITIES, type OAuthClientCapabilityField, type OAuthClientCapabilityId, OCR_CAPABILITY, SANDBOX_CAPABILITY, STORAGE_CAPABILITY, } from '../../apps/sim/lib/core/config/env-capabilities.ts' export type SetupHint = | string | { development: string containerized: string } export type SetupCondition = | { kind: 'present'; key: string } | { kind: 'truthy'; key: string } | { kind: 'equals'; key: string; value: string } | { kind: 'all'; conditions: readonly SetupCondition[] } | { kind: 'any'; conditions: readonly SetupCondition[] } | { kind: 'not'; condition: SetupCondition } export type SetupPromptCondition = SetupCondition | { kind: 'provider-missing-field'; key: string } export interface SetupFieldPrompt { type: 'field' key: string input: 'text' | 'secret' message?: string hint?: string required?: boolean defaultValue?: string validate?: boolean when?: SetupPromptCondition } export interface SetupConfirmPrompt { type: 'confirm' key: string message: string defaultValue?: boolean when?: SetupPromptCondition } export interface SetupChoiceOption { id: string label: string hint?: string currentWhen?: SetupCondition env?: Readonly> prompts?: readonly SetupPrompt[] } export interface SetupChoicePrompt { type: 'choice' id: string message: string options: readonly SetupChoiceOption[] when?: SetupPromptCondition } export type SetupPrompt = SetupFieldPrompt | SetupConfirmPrompt | SetupChoicePrompt type ProviderId = TDefinition['providers'][number]['id'] export interface ProviderSetupDefinition { hint?: SetupHint env?: Readonly> prompts: readonly SetupPrompt[] currentWhen?: SetupCondition } type ProviderSetupMap = { [TId in ProviderId]: ProviderSetupDefinition } export interface SetupActionDefinition { label: string hint?: SetupHint env?: Readonly> currentWhen?: SetupCondition } type DefaultOptionId = TDefinition extends { defaultProvider: { kind: 'built-in'; id: infer TId extends string } } ? TId : never export interface CapabilitySetupDefinition< TDefinition extends CapabilityDefinition = CapabilityDefinition, TActions extends Readonly> = Readonly< Record >, > { definition: TDefinition label: string message: string providers: ProviderSetupMap actions: TActions defaultOption?: { hint?: SetupHint } optionOrder: readonly (ProviderId | DefaultOptionId | keyof TActions)[] } function promptKeys(prompts: readonly SetupPrompt[] = []): string[] { return prompts.flatMap((prompt) => { if (prompt.type === 'field' || prompt.type === 'confirm') return [prompt.key] return prompt.options.flatMap((option) => [ ...Object.keys(option.env ?? {}), ...promptKeys(option.prompts), ]) }) } function conditionKeys(condition: SetupPromptCondition | undefined): string[] { if (!condition) return [] if ( condition.kind === 'present' || condition.kind === 'truthy' || condition.kind === 'equals' || condition.kind === 'provider-missing-field' ) { return [condition.key] } if (condition.kind === 'all' || condition.kind === 'any') { return condition.conditions.flatMap(conditionKeys) } return conditionKeys(condition.condition) } function promptConditionKeys(prompts: readonly SetupPrompt[] = []): string[] { return prompts.flatMap((prompt) => [ ...conditionKeys(prompt.when), ...(prompt.type === 'choice' ? prompt.options.flatMap((option) => [ ...conditionKeys(option.currentWhen), ...promptConditionKeys(option.prompts), ]) : []), ]) } function requirementKeys( requirement: CapabilityDefinition['providers'][number]['requires'] ): string[] { return requirement.type === 'field' ? [requirement.key] : requirement.requirements.flatMap(requirementKeys) } function providerOwnedInputKeys( provider: CapabilityDefinition['providers'][number] ): readonly string[] { return [ ...requirementKeys(provider.requires), ...(provider.optionalFields ?? []).map((field) => field.key), ...(provider.pairedFields ?? []).flat(), ] } function providerOwnedSetupKeys( provider: CapabilityDefinition['providers'][number] ): readonly string[] { return [ ...providerOwnedInputKeys(provider), ...(provider.activation.mode === 'enabled' ? [provider.activation.key] : provider.activation.keys), ] } export function defineCapabilitySetup< const TDefinition extends CapabilityDefinition, const TActions extends Readonly>, >( definition: TDefinition, setup: Omit, 'definition'> ): CapabilitySetupDefinition { const providerIds = definition.providers.map((provider) => provider.id) const configuredProviderIds = Object.keys(setup.providers) const missingProviders = providerIds.filter((id) => !configuredProviderIds.includes(id)) const unknownProviders = configuredProviderIds.filter((id) => !providerIds.includes(id)) if (missingProviders.length > 0 || unknownProviders.length > 0) { throw new Error( `Setup ${definition.id} provider mapping drifted (missing: ${missingProviders.join(', ') || 'none'}; unknown: ${unknownProviders.join(', ') || 'none'})` ) } for (const provider of definition.providers) { const providerSetup = (setup.providers as Record)[provider.id] if (!providerSetup) throw new Error(`Setup ${definition.id} has no provider ${provider.id}`) const ownedFields = providerOwnedInputKeys(provider) const setupFields = providerOwnedSetupKeys(provider) const configuredFields = [ ...promptKeys(providerSetup.prompts), ...Object.keys(providerSetup.env ?? {}), ] const referencedFields = [ ...configuredFields, ...conditionKeys(providerSetup.currentWhen), ...promptConditionKeys(providerSetup.prompts), ] const unknownFields = referencedFields.filter((key) => !setupFields.includes(key)) const missingFields = ownedFields.filter((key) => !configuredFields.includes(key)) if (unknownFields.length > 0 || missingFields.length > 0) { throw new Error( `Setup ${definition.id}/${provider.id} field mapping drifted (missing: ${missingFields.join(', ') || 'none'}; unknown: ${unknownFields.join(', ') || 'none'})` ) } } const capabilityFields = getCapabilityFields(definition) for (const [actionId, action] of Object.entries(setup.actions)) { const unknownFields = [ ...Object.keys(action.env ?? {}), ...conditionKeys(action.currentWhen), ].filter((key) => !capabilityFields.includes(key)) if (unknownFields.length > 0) { throw new Error( `Setup ${definition.id}/${actionId} action writes unknown fields: ${unknownFields.join(', ')}` ) } } const optionIds = [ ...providerIds, ...Object.keys(setup.actions), ...(definition.strategy === 'selected' && definition.defaultProvider.kind === 'built-in' ? [definition.defaultProvider.id] : []), ] if ( setup.optionOrder.length !== optionIds.length || setup.optionOrder.some((id) => !optionIds.includes(String(id))) || optionIds.some((id) => !setup.optionOrder.includes(id)) ) { throw new Error(`Setup ${definition.id} option order must list every option once`) } return { definition, ...setup } } export const EMAIL_SETUP = defineCapabilitySetup(EMAIL_CAPABILITY, { label: 'Email delivery', message: 'Email sending?', actions: { none: { label: 'None', hint: 'emails are logged to the console — fine for local', }, }, providers: { resend: { hint: 'paste an API key', prompts: [ { type: 'field', key: 'RESEND_API_KEY', input: 'secret', required: true, }, ], }, ses: { hint: 'uses the AWS SDK credential chain', prompts: [ { type: 'field', key: 'AWS_SES_REGION', input: 'text', required: true, defaultValue: 'us-east-1', }, ], }, smtp: { hint: 'any SMTP relay', prompts: [ { type: 'field', key: 'SMTP_HOST', input: 'text', required: true }, { type: 'field', key: 'SMTP_PORT', input: 'text', required: true, defaultValue: '587', validate: true, }, { type: 'field', key: 'SMTP_USER', input: 'text', hint: 'leave empty for an unauthenticated relay', }, { type: 'field', key: 'SMTP_PASS', input: 'secret', required: true, when: { kind: 'present', key: 'SMTP_USER' }, }, ], }, azure: { hint: 'connection string', prompts: [ { type: 'field', key: 'AZURE_ACS_CONNECTION_STRING', input: 'secret', required: true, }, ], }, gmail: { hint: 'Workspace service account delegation', prompts: [ { type: 'field', key: 'GMAIL_CREDENTIALS_JSON', input: 'secret', required: true, validate: true, }, { type: 'field', key: 'GMAIL_SENDER', input: 'text', required: true }, ], }, }, optionOrder: ['none', 'resend', 'ses', 'smtp', 'azure', 'gmail'], }) export const STORAGE_SETUP = defineCapabilitySetup(STORAGE_CAPABILITY, { label: 'File storage', message: 'File storage?', actions: {}, defaultOption: { hint: { development: 'fine for local dev (external-fetch flows like Instagram publish need cloud storage)', containerized: 'files live in the container — LOST on restart; evaluation only', }, }, providers: { azure: { hint: 'connection string or account name + key', prompts: [ { type: 'field', key: 'AZURE_STORAGE_CONTAINER_NAME', input: 'text', required: true, defaultValue: 'sim-files', }, { type: 'choice', id: 'azure-credentials', message: 'Azure credentials?', options: [ { id: 'connection-string', label: 'Connection string', currentWhen: { kind: 'present', key: 'AZURE_CONNECTION_STRING' }, prompts: [ { type: 'field', key: 'AZURE_CONNECTION_STRING', input: 'secret', required: true, }, ], }, { id: 'account-key', label: 'Account name and key', currentWhen: { kind: 'any', conditions: [ { kind: 'present', key: 'AZURE_ACCOUNT_NAME' }, { kind: 'present', key: 'AZURE_ACCOUNT_KEY' }, ], }, prompts: [ { type: 'field', key: 'AZURE_ACCOUNT_NAME', input: 'text', required: true, }, { type: 'field', key: 'AZURE_ACCOUNT_KEY', input: 'secret', required: true, }, ], }, ], }, ], }, s3: { hint: 'AWS S3 or an S3-compatible endpoint', env: { S3_FORCE_PATH_STYLE: 'false' }, prompts: [ { type: 'field', key: 'S3_ENDPOINT', input: 'text', hint: 'optional for R2, MinIO, B2, or another S3-compatible service', validate: true, }, { type: 'confirm', key: 'S3_FORCE_PATH_STYLE', message: 'Force path-style addressing? (required for MinIO/Ceph, not for R2)', when: { kind: 'present', key: 'S3_ENDPOINT' }, }, { type: 'field', key: 'AWS_REGION', input: 'text', required: true }, { type: 'field', key: 'S3_BUCKET_NAME', input: 'text', required: true }, { type: 'choice', id: 'aws-credentials', message: 'AWS credentials?', options: [ { id: 'chain', label: 'Default credential chain', hint: 'IAM role, IRSA, profile, or other SDK source', currentWhen: { kind: 'all', conditions: [ { kind: 'present', key: 'S3_BUCKET_NAME' }, { kind: 'not', condition: { kind: 'any', conditions: [ { kind: 'present', key: 'AWS_ACCESS_KEY_ID' }, { kind: 'present', key: 'AWS_SECRET_ACCESS_KEY' }, ], }, }, ], }, }, { id: 'static', label: 'Access key and secret', hint: 'stored in AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY', currentWhen: { kind: 'any', conditions: [ { kind: 'present', key: 'AWS_ACCESS_KEY_ID' }, { kind: 'present', key: 'AWS_SECRET_ACCESS_KEY' }, ], }, prompts: [ { type: 'field', key: 'AWS_ACCESS_KEY_ID', input: 'secret', required: true, }, { type: 'field', key: 'AWS_SECRET_ACCESS_KEY', input: 'secret', required: true, }, ], }, ], }, ], }, gcs: { hint: 'bucket; credentials via ADC by default', prompts: [ { type: 'field', key: 'GCS_BUCKET_NAME', input: 'text', required: true, }, { type: 'field', key: 'GCS_PROJECT_ID', input: 'text', hint: 'optional; inferred from credentials or ADC when empty', }, { type: 'choice', id: 'gcs-credentials', message: 'Google Cloud credentials?', options: [ { id: 'adc', label: 'Application Default Credentials', hint: 'Workload Identity or GOOGLE_APPLICATION_CREDENTIALS', currentWhen: { kind: 'all', conditions: [ { kind: 'present', key: 'GCS_BUCKET_NAME' }, { kind: 'not', condition: { kind: 'present', key: 'GCS_CREDENTIALS_JSON' }, }, ], }, }, { id: 'json', label: 'Inline service account JSON', hint: 'stored in GCS_CREDENTIALS_JSON', currentWhen: { kind: 'present', key: 'GCS_CREDENTIALS_JSON' }, prompts: [ { type: 'field', key: 'GCS_CREDENTIALS_JSON', input: 'secret', required: true, validate: true, }, ], }, ], }, ], }, }, optionOrder: ['local', 's3', 'azure', 'gcs'], }) export const SANDBOX_SETUP = defineCapabilitySetup(SANDBOX_CAPABILITY, { label: 'Function sandboxes', message: 'Function sandbox provider?', actions: { disabled: { label: 'Disabled', hint: 'local JavaScript execution only', env: { NEXT_PUBLIC_E2B_ENABLED: 'false', NEXT_PUBLIC_SANDBOXES_ENABLED: 'false', }, currentWhen: { kind: 'all', conditions: [ { kind: 'not', condition: { kind: 'truthy', key: 'E2B_ENABLED' } }, { kind: 'not', condition: { kind: 'present', key: 'DAYTONA_API_KEY' }, }, { kind: 'not', condition: { kind: 'present', key: 'DAYTONA_FUNCTION_SNAPSHOT_ID' }, }, ], }, }, }, providers: { e2b: { hint: 'dedicated Function code and CLI sandboxes', env: { NEXT_PUBLIC_E2B_ENABLED: 'true', NEXT_PUBLIC_SANDBOXES_ENABLED: 'true', }, prompts: [ { type: 'field', key: 'E2B_API_KEY', input: 'secret', required: true }, { type: 'field', key: 'E2B_FUNCTION_TEMPLATE_ID', input: 'text', required: true, validate: true, hint: 'immutable