export type InputRecord = Record export type ProviderId = | 'openai' | 'anthropic' | 'groq' | 'cloudflare' | 'openai-compatible' export const PROVIDER_IDS = [ 'openai', 'anthropic', 'groq', 'cloudflare', 'openai-compatible', ] as const satisfies ReadonlyArray const ACCOUNT_LABEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/ const SECRET_PREFIXES = [ 'openaiApiKey', 'anthropicApiKey', 'groqApiKey', 'cloudflareApiToken', 'openaiCompatibleApiKey', ] as const export type SecretPrefix = (typeof SECRET_PREFIXES)[number] export function inputRecord(value: unknown): InputRecord { if (value === null || typeof value !== 'object' || Array.isArray(value)) { throw new Error('Input must be an object.') } return value as InputRecord } export function optionalString( input: InputRecord, key: string, ): string | undefined { const value = input[key] if (value === undefined || value === null || value === '') return undefined if (typeof value !== 'string') throw new Error(key + ' must be a string.') const trimmed = value.trim() return trimmed || undefined } export function requiredString(input: InputRecord, key: string): string { const value = optionalString(input, key) if (!value) throw new Error(key + ' is required.') return value } export function optionalBoolean( input: InputRecord, key: string, ): boolean | undefined { const value = input[key] if (value === undefined || value === null) return undefined if (typeof value !== 'boolean') throw new Error(key + ' must be a boolean.') return value } export function accountLabel(value: string | undefined): string | null { const trimmed = (value ?? '').trim() if (!trimmed || trimmed === 'default') return null if (!ACCOUNT_LABEL_PATTERN.test(trimmed)) { throw new Error( 'account must be a short label such as "work" or "live" (letters, numbers, _ or -).', ) } return trimmed } export function isProviderId(value: string): value is ProviderId { return (PROVIDER_IDS as ReadonlyArray).includes(value) } export function parseProviderId(value: string | undefined): ProviderId | undefined { if (!value) return undefined if (!isProviderId(value)) { throw new Error( 'provider must be openai, anthropic, groq, cloudflare, or openai-compatible.', ) } return value } export function secretNameFor( prefix: SecretPrefix, account?: string, override?: string, ): string { if (override != null && override.trim()) { const name = override.trim() const allowed = SECRET_PREFIXES.some( (item) => name === item || name.startsWith(item + '-'), ) if (!allowed) { throw new Error( 'apiKeySecret must be one of ' + SECRET_PREFIXES.join(', ') + ' or the same name with an account suffix.', ) } return name } const label = accountLabel(account) return label ? prefix + '-' + label : prefix } export function secretPlaceholder(secretName: string): string { return '{{secret:' + secretName + '|scope=user}}' } /** * Settings writes default to dry-run. A live persist requires `confirm: true` * and `dryRun` not set to true. */ export function isMutationDryRun(input: { dryRun?: unknown confirm?: unknown }): boolean { if (input.dryRun === true) return true if (input.confirm === true) return false return true } export function isPreviewDryRun(input: { dryRun?: unknown }): boolean { return input.dryRun === true } export function boundedMaxTokens(value: unknown, fallback = 4096): number { if (value === undefined || value === null || value === '') return fallback if (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 32768) { throw new Error('maxTokens must be an integer from 1 through 32768.') } return Number(value) } export function assertHttpsUrl(value: string, key: string): string { let parsed: URL try { parsed = new URL(value) } catch { throw new Error(key + ' must be an https URL.') } if (parsed.protocol !== 'https:') { throw new Error(key + ' must be an https URL.') } if (parsed.username || parsed.password) { throw new Error(key + ' must not include credentials.') } return parsed.origin + parsed.pathname.replace(/\/$/, '') } export function hostFromHttpsUrl(value: string): string { return new URL(value).host } export function asRecord(value: unknown): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) return {} return value as Record } export function stringify(value: unknown): string { try { return JSON.stringify(value, null, 2) } catch { return String(value) } } export function clean(value: unknown): string { return String(value ?? '').trim() } export function assertNever(value: never, label: string): never { throw new Error('Unhandled ' + label + ': ' + String(value)) }