/** * Capability Secret Validation * Validates and prompts for missing capability secrets during module generation */ import type { Database } from 'bun:sqlite'; import { celiloIntro, promptPassword } from '../cli/prompts'; import type { ModuleManifest } from '../manifest/schema'; import { encryptSecret } from '../secrets/encryption'; import { getOrCreateMasterKey } from '../secrets/master-key'; export interface MissingCapabilitySecret { capabilityId: number; capabilityName: string; secretName: string; description: string | null; } export interface SecretValidationResult { success: boolean; error?: string; missingSecrets?: MissingCapabilitySecret[]; } /** * Check whether an unset capability-secret row is backed by a configured * provider module secret through `secret_ref`. * * Capability registration deliberately stores metadata-only rows even when * the manifest delegates storage to `$secret:`. In that case the NULL * capability value is expected and must not trigger a second secret prompt. */ function hasConfiguredSecretRef( moduleId: string, manifest: ModuleManifest | null, capabilityName: string, secretName: string, db: Database, ): boolean { const capability = manifest?.provides?.capabilities?.find( (candidate) => candidate.name === capabilityName, ); const secret = capability?.secrets?.find((candidate) => candidate.name === secretName); const match = secret?.secret_ref?.match(/^\$secret:(.+)$/); if (!match) { return false; } const referencedSecret = db .prepare( `SELECT 1 FROM secrets WHERE module_id = ? AND name = ? AND encrypted_value IS NOT NULL AND iv IS NOT NULL AND auth_tag IS NOT NULL LIMIT 1`, ) .get(moduleId, match[1]); return referencedSecret !== null && referencedSecret !== undefined; } /** * Check if module has any missing capability secrets * * Policy function (Rule 10.1) - reads database, no side effects * * @param moduleId - Module identifier * @param db - Database connection * @returns Validation result with list of missing secrets */ export async function validateCapabilitySecrets( moduleId: string, db: Database, ): Promise { // Get all capabilities provided by this module const moduleCapabilities = db .prepare('SELECT id, capability_name FROM capabilities WHERE module_id = ?') .all(moduleId) as Array<{ id: number; capability_name: string }>; if (moduleCapabilities.length === 0) { return { success: true }; // No capabilities = no secrets needed } const moduleResult = db.prepare('SELECT manifest_data FROM modules WHERE id = ?').get(moduleId) as | { manifest_data: string } | undefined; const manifest = moduleResult ? (JSON.parse(moduleResult.manifest_data) as ModuleManifest) : null; // Check for secrets with NULL encrypted_value const missingSecrets: MissingCapabilitySecret[] = []; for (const capability of moduleCapabilities) { const secrets = db .prepare( `SELECT name, description FROM capability_secrets WHERE capability_id = ? AND encrypted_value IS NULL`, ) .all(capability.id) as Array<{ name: string; description: string | null }>; for (const secret of secrets) { if (hasConfiguredSecretRef(moduleId, manifest, capability.capability_name, secret.name, db)) { continue; } missingSecrets.push({ capabilityId: capability.id, capabilityName: capability.capability_name, secretName: secret.name, description: secret.description, }); } } if (missingSecrets.length === 0) { return { success: true }; } // Build error message const secretList = missingSecrets .map((s) => ` • ${s.capabilityName}.${s.secretName}`) .join('\n'); return { success: false, error: `Missing required capability secrets:\n${secretList}\n\nRun generation in interactive mode to provide values.`, missingSecrets, }; } /** * Prompt user for missing capability secrets * * Execution function (Rule 10.1) - performs I/O (prompts, database writes) * * @param moduleId - Module identifier * @param missingSecrets - List of missing secrets to prompt for * @param db - Database connection * @returns Success/failure result */ export async function promptForMissingCapabilitySecrets( moduleId: string, missingSecrets: MissingCapabilitySecret[], db: Database, ): Promise<{ success: boolean; error?: string }> { await celiloIntro(`šŸ” Capability secrets needed for ${moduleId}`); console.log('\nThe following capability secrets are required:\n'); const masterKey = await getOrCreateMasterKey(); for (const secret of missingSecrets) { try { const description = secret.description || 'No description provided'; console.log(`\n${secret.capabilityName}.${secret.secretName}`); console.log(` ${description}\n`); const value = await promptPassword({ message: `${secret.secretName} (Press Enter to auto-generate)`, validate: (_val) => { // Allow empty input - will auto-generate return undefined; }, }); // Auto-generate if user pressed Enter let finalValue = value; if (!value || value.trim() === '') { const { randomBytes } = await import('node:crypto'); const randomValue = randomBytes(32); finalValue = randomValue.toString('base64'); console.log(' šŸ”‘ Auto-generated base64-encoded secret'); } // Encrypt and store the secret value const encrypted = encryptSecret(finalValue, masterKey); db.prepare( `UPDATE capability_secrets SET encrypted_value = ?, iv = ?, auth_tag = ?, updated_at = unixepoch() WHERE capability_id = ? AND name = ?`, ).run( encrypted.encryptedValue, encrypted.iv, encrypted.authTag, secret.capabilityId, secret.secretName, ); console.log(` āœ“ Saved ${secret.secretName}`); } catch (error) { console.error( `\nāœ— Failed to save ${secret.secretName}: ${error instanceof Error ? error.message : String(error)}`, ); return { success: false, error: `Failed to collect capability secret: ${secret.secretName}`, }; } } console.log('\nāœ… All capability secrets saved successfully!\n'); return { success: true }; }