/** * Secret Storage * Reusable functions for storing encrypted secrets */ import { and, eq } from 'drizzle-orm'; import type { DbClient } from '../db/client'; import { getDb } from '../db/client'; import { systemSecrets } from '../db/schema'; import { encryptSecret } from './encryption'; import { getOrCreateMasterKey } from './master-key'; /** * Store system secret with encryption * * Policy function (Rule 10.1) - validates key before storing * Execution function - performs encryption and database write * * @param key - Secret key (e.g., 'proxmox.root_password') * @param value - Plaintext secret value * @param db - Database client (optional, for testing) * @returns True if successful * @throws Error if master key access fails or encryption fails */ export async function storeSystemSecret( key: string, value: string, db: DbClient = getDb(), ): Promise { // Get or create master key const masterKey = await getOrCreateMasterKey(); // Encrypt secret const encrypted = encryptSecret(value, masterKey); // Check if secret already exists const existingSecret = db.select().from(systemSecrets).where(eq(systemSecrets.key, key)).get(); if (existingSecret) { // Update existing secret db.update(systemSecrets) .set({ encryptedValue: encrypted.encryptedValue, iv: encrypted.iv, authTag: encrypted.authTag, updatedAt: new Date(), }) .where(eq(systemSecrets.id, existingSecret.id)) .run(); } else { // Insert new secret db.insert(systemSecrets) .values({ key, encryptedValue: encrypted.encryptedValue, iv: encrypted.iv, authTag: encrypted.authTag, }) .run(); } } /** * Get system secret and decrypt * * Execution function - performs database read and decryption * * @param key - Secret key * @param db - Database client (optional, for testing) * @returns Decrypted secret value or null if not found * @throws Error if decryption fails */ export async function getSystemSecret(key: string, db: DbClient = getDb()): Promise { const secret = db.select().from(systemSecrets).where(eq(systemSecrets.key, key)).get(); if (!secret) { return null; } const masterKey = await getOrCreateMasterKey(); const { decryptSecret } = await import('./encryption'); return decryptSecret( { encryptedValue: secret.encryptedValue, iv: secret.iv, authTag: secret.authTag, }, masterKey, ); } /** * Check if system secret exists * * Policy function - validates existence only * * @param key - Secret key * @param db - Database client (optional, for testing) * @returns True if secret exists */ export function systemSecretExists(key: string, db: DbClient = getDb()): boolean { const secret = db.select().from(systemSecrets).where(eq(systemSecrets.key, key)).get(); return secret !== undefined; } /** * Record a secret on a PROVIDER module on behalf of the framework. * * This is the named, single-purpose path that replaced the generic * return-value capture sweep (openspec/changes/hook-owned-state, D3/D5): * `container_created` used to persist whatever the provider hook returned * onto the provider module, keyed by the provider's own id — a write a * per-module hook accessor cannot express and must never be able to. The * framework still needs to mint provider secrets on a deploy's authority, * so that write keeps a broker-side home: an explicit call in framework * code, not reachable from any hook, not driven by a hook return value. * * Policy function (Rule 10.1) - validates name before storing * Execution function - performs encryption and database write * * @param db - Database client * @param providerModuleId - The module that OWNS the secret (the provider) * @param name - Secret name * @param value - Plaintext secret value * @throws Error if name or value is empty */ export async function recordProviderSecret( db: DbClient, providerModuleId: string, name: string, value: string, ): Promise { if (!providerModuleId.trim()) { throw new Error('recordProviderSecret: providerModuleId is required'); } if (!name.trim()) { throw new Error('recordProviderSecret: secret name is required'); } if (!value.trim()) { throw new Error('recordProviderSecret: secret value is required'); } const { secrets } = await import('../db/schema'); const masterKey = await getOrCreateMasterKey(); const encrypted = encryptSecret(value, masterKey); const existing = db .select() .from(secrets) .where(and(eq(secrets.moduleId, providerModuleId), eq(secrets.name, name))) .get(); if (existing) { db.update(secrets) .set({ encryptedValue: encrypted.encryptedValue, iv: encrypted.iv, authTag: encrypted.authTag, updatedAt: new Date(), }) .where(eq(secrets.id, existing.id)) .run(); } else { db.insert(secrets) .values({ moduleId: providerModuleId, name, encryptedValue: encrypted.encryptedValue, iv: encrypted.iv, authTag: encrypted.authTag, }) .run(); } }