/** * Capability Registration * Handles registration of capabilities during module import */ import type { Database } from 'bun:sqlite'; import { randomBytes } from 'node:crypto'; import type { ModuleManifest } from '../manifest/schema'; import { encryptSecret } from '../secrets/encryption'; import type { EncryptedSecret } from '../secrets/encryption'; import { getOrCreateMasterKey } from '../secrets/master-key'; import { computedMarker } from '../variables/computed/marker'; export interface RegistrationResult { success: boolean; error?: string; details?: unknown; } export interface CapabilitySecretDefinition { name: string; type: 'string' | 'number' | 'boolean'; description?: string; readable_by?: string[]; } /** * Register all capabilities provided by a module * * Execution function (Rule 10.1) - performs database operations * * @param moduleId - Module identifier * @param manifest - Module manifest * @param db - Database connection * @param flags - CLI flags (e.g., auto-generate-secrets) * @returns Registration result */ export async function registerModuleCapabilities( moduleId: string, manifest: ModuleManifest, db: Database, _flags: Record = {}, ): Promise { if (!manifest.provides?.capabilities || manifest.provides.capabilities.length === 0) { return { success: true }; // No capabilities to register } try { // Ensure the master key exists (side effect); registration stores secret // metadata only, so the key itself isn't used here. await getOrCreateMasterKey(); for (const capability of manifest.provides.capabilities) { // Build capability data by resolving $self: variables const capabilityData = buildCapabilityData(capability, manifest); // Insert capability into database (with optional zones) const zones = capability.zones ? JSON.stringify(capability.zones) : null; const capabilityRecord = db .prepare( `INSERT INTO capabilities (module_id, capability_name, version, data, zones, registered_at) VALUES (?, ?, ?, ?, ?, unixepoch()) RETURNING id`, ) .get( moduleId, capability.name, capability.version, JSON.stringify(capabilityData), zones, ) as { id: number; }; // Register capability secret metadata (values will be prompted during generation) if (capability.secrets && capability.secrets.length > 0) { for (const secret of capability.secrets) { // Store secret metadata only (no value yet) db.prepare( `INSERT INTO capability_secrets (capability_id, name, description, created_at, updated_at) VALUES (?, ?, ?, unixepoch(), unixepoch())`, ).run(capabilityRecord.id, secret.name, secret.description || null); } } } return { success: true }; } catch (error) { return { success: false, error: 'Failed to register capabilities', details: error, }; } } /** * Build capability data by resolving $self: variables * * Policy function (Rule 10.1) - pure data transformation * * Note: Only resolves $self: variables. Other variable types ($system:, $capability:) * are resolved during generation, not registration. * * @param capability - Capability definition from manifest * @param manifest - Module manifest (for $self: variable resolution) * @returns Capability data with resolved variables */ export function buildCapabilityData( capability: { name?: string; data?: Record; computed?: Array<{ name: string; value: string }>; }, _manifest: ModuleManifest, ): Record { // Static data is stored verbatim (any $self: refs are resolved lazily at // read time by the variable resolver). const data: Record = capability.data ? structuredClone(capability.data) : {}; // Fold computed fields into the data namespace as markers. They resolve // lazily in the PROVIDER's context at read time — see resolver.ts and // src/variables/computed/. A computed name colliding with a static data // key is a manifest error. for (const field of capability.computed ?? []) { if (field.name in data) { throw new Error( `Capability '${capability.name ?? '?'}': computed field '${field.name}' collides with a static data key`, ); } data[field.name] = computedMarker(field.value); } return data; } /** * Prompt user for secret value or auto-generate * * Execution function (Rule 10.1) - performs I/O (console input) * * @param secretName - Name of the secret * @param description - Description of the secret * @param autoGenerate - Whether to auto-generate without prompting * @returns Secret value */ export async function promptForSecretValue( secretName: string, description: string, autoGenerate: boolean, ): Promise { if (autoGenerate) { // Auto-generate secret (TSIG keys are base64-encoded 32-byte values) const randomValue = randomBytes(32); return randomValue.toString('base64'); } // Check if stdin is available (TTY check) if (!process.stdin.isTTY) { throw new Error( `Cannot prompt for secret '${secretName}': stdin is not available.\nUse --auto-generate-secrets flag to auto-generate capability secrets.`, ); } // Prompt user for secret value console.log('\nCapability secret required:'); console.log(` • ${secretName}: ${description}`); console.log(`\nEnter value for ${secretName} (or press Enter to auto-generate):`); // Read from stdin const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { rl.question('> ', (answer) => { rl.close(); if (answer.trim() === '') { // User pressed Enter - auto-generate const randomValue = randomBytes(32); const generated = randomValue.toString('base64'); console.log(' ✓ Auto-generated secret (hmac-sha256)'); resolve(generated); } else { resolve(answer.trim()); } }); }); } /** * Encrypt and store capability secret * * Execution function (Rule 10.1) - performs database operations * * @param capabilityId - Capability ID from database * @param name - Secret name * @param value - Plaintext secret value * @param masterKey - Master encryption key * @param db - Database connection */ export async function storeCapabilitySecret( capabilityId: number, name: string, value: string, masterKey: Buffer, db: Database, ): Promise { const encrypted: EncryptedSecret = encryptSecret(value, masterKey); db.prepare( `INSERT INTO capability_secrets (capability_id, name, encrypted_value, iv, auth_tag, created_at, updated_at) VALUES (?, ?, ?, ?, ?, unixepoch(), unixepoch())`, ).run(capabilityId, name, encrypted.encryptedValue, encrypted.iv, encrypted.authTag); }