/** * Cross-Module Data Manager * * Manages data exchange between modules, including: * - Storing/retrieving configuration data from function calls * - Encrypting sensitive data (secrets) * - Resolving variables in parameters * - Looking up capability providers * * Future: Full function call orchestration, lifecycle management */ import { and, eq } from 'drizzle-orm'; import { type DbClient, createDbClient } from '../db/client'; import { capabilities, moduleConfigs, secrets } from '../db/schema'; import { decryptSecret, encryptSecret } from '../secrets/encryption'; import { getOrCreateMasterKey } from '../secrets/master-key'; import { buildResolutionContext } from '../variables/context'; import { resolveTemplate } from '../variables/resolver'; import { upsertModuleConfig } from './module-config'; /** * Configuration data type */ export interface ConfigData { key: string; value: string | number | boolean | unknown[] | Record; isSecret: boolean; } /** * Capability lookup result */ export interface CapabilityInfo { moduleId: string; capabilityName: string; version: string; data: Record; } /** * Cross-module data manager * Orchestrates data exchange between modules */ export class CrossModuleDataManager { private db: DbClient; private masterKey: Buffer | null = null; constructor(db?: DbClient) { this.db = db || createDbClient(); } /** * Initialize manager (load master key) * Execution function - performs I/O */ async initialize(): Promise { this.masterKey = await getOrCreateMasterKey(); } /** * Ensure master key is loaded * Policy function - validates state */ private ensureMasterKey(): Buffer { if (!this.masterKey) { throw new Error('CrossModuleDataManager not initialized. Call initialize() first.'); } return this.masterKey; } /** * Store configuration data for a module * Handles both regular config and secrets * * Execution function - performs database writes and encryption * * @param moduleId - Module ID * @param key - Configuration key * @param value - Configuration value * @param isSecret - Whether to encrypt the value */ async storeConfigData( moduleId: string, key: string, value: string | number | boolean | unknown[] | Record, isSecret = false, ): Promise { if (isSecret) { await this.storeSecret(moduleId, key, String(value)); } else { await this.storeConfig(moduleId, key, value); } } /** * Store regular (non-secret) configuration * Execution function - performs database write */ private async storeConfig( moduleId: string, key: string, value: string | number | boolean | unknown[] | Record, ): Promise { upsertModuleConfig(this.db, moduleId, key, value); } /** * Store secret (encrypted) * Execution function - performs encryption and database write * * @param moduleId - Module ID * @param name - Secret name * @param value - Secret value (plaintext) */ async storeSecret(moduleId: string, name: string, value: string): Promise { const masterKey = this.ensureMasterKey(); // Encrypt the secret const encrypted = encryptSecret(value, masterKey); // Check if secret already exists const existing = this.db .select() .from(secrets) .where(and(eq(secrets.moduleId, moduleId), eq(secrets.name, name))) .get(); if (existing) { // Update existing this.db .update(secrets) .set({ encryptedValue: encrypted.encryptedValue, iv: encrypted.iv, authTag: encrypted.authTag, updatedAt: new Date(Date.now()), }) .where(eq(secrets.id, existing.id)) .run(); } else { // Insert new this.db .insert(secrets) .values({ moduleId, name, encryptedValue: encrypted.encryptedValue, iv: encrypted.iv, authTag: encrypted.authTag, }) .run(); } } /** * Get configuration data (non-secret) * Execution function - performs database read * * @param moduleId - Module ID * @param key - Configuration key * @returns Configuration value or null if not found */ getConfigData( moduleId: string, key: string, ): string | number | boolean | unknown[] | Record | null { const config = this.db .select() .from(moduleConfigs) .where(and(eq(moduleConfigs.moduleId, moduleId), eq(moduleConfigs.key, key))) .get(); if (!config) { return null; } // Return complex type from valueJson if (config.valueJson) { try { return JSON.parse(config.valueJson); } catch (error) { throw new Error( `Failed to parse config value for ${moduleId}.${key}: ${error instanceof Error ? error.message : 'Invalid JSON'}`, ); } } // Return primitive type from value // Parse to correct type if (config.value === 'true') return true; if (config.value === 'false') return false; const num = Number(config.value); if (!Number.isNaN(num)) { return num; } return config.value; } /** * Get secret (decrypted) * Execution function - performs database read and decryption * * @param moduleId - Module ID * @param name - Secret name * @returns Decrypted secret value or null if not found */ getSecret(moduleId: string, name: string): string | null { const masterKey = this.ensureMasterKey(); const secret = this.db .select() .from(secrets) .where(and(eq(secrets.moduleId, moduleId), eq(secrets.name, name))) .get(); if (!secret) { return null; } // Decrypt and return return decryptSecret( { encryptedValue: secret.encryptedValue, iv: secret.iv, authTag: secret.authTag, }, masterKey, ); } /** * Get all secrets for a module (decrypted) * Execution function - performs database read and decryption * * @param moduleId - Module ID * @returns Record of secret name -> decrypted value */ getAllSecrets(moduleId: string): Record { const masterKey = this.ensureMasterKey(); const secretRecords = this.db .select() .from(secrets) .where(eq(secrets.moduleId, moduleId)) .all(); const result: Record = {}; for (const secret of secretRecords) { result[secret.name] = decryptSecret( { encryptedValue: secret.encryptedValue, iv: secret.iv, authTag: secret.authTag, }, masterKey, ); } return result; } /** * Look up which module provides a capability * Execution function - performs database query * * @param capabilityName - Capability name (e.g., 'dns_registrar', 'idp') * @returns Capability info or null if not found */ findCapabilityProvider(capabilityName: string): CapabilityInfo | null { const capability = this.db .select() .from(capabilities) .where(eq(capabilities.capabilityName, capabilityName)) .get(); if (!capability) { return null; } return { moduleId: capability.moduleId, capabilityName: capability.capabilityName, version: capability.version, data: capability.data as Record, }; } /** * Resolve variables in parameters * Uses the variable resolver to handle $self:, $system:, $capability:, etc. * * Execution function - performs database reads for variable resolution * * @param moduleId - Module ID for $self: resolution * @param params - Parameters with variables to resolve * @returns Resolved parameters */ async resolveParameters( moduleId: string, params: Record, ): Promise> { // Build resolution context const context = await buildResolutionContext(moduleId, this.db); // Resolve each parameter value const resolved: Record = {}; for (const [key, value] of Object.entries(params)) { if (typeof value === 'string') { const result = await resolveTemplate(value, context, this.db); if (!result.success) { throw new Error( `Failed to resolve parameter '${key}': ${result.errors.map((e) => e.error).join(', ')}`, ); } resolved[key] = result.content; } else if (Array.isArray(value)) { // Resolve each array element resolved[key] = await Promise.all( value.map(async (item) => { if (typeof item === 'string') { const result = await resolveTemplate(item, context, this.db); if (!result.success) { throw new Error(`Failed to resolve array element in '${key}'`); } return result.content; } return item; }), ); } else if (typeof value === 'object' && value !== null) { // Recursively resolve object properties resolved[key] = await this.resolveParameters(moduleId, value as Record); } else { // Primitive value - keep as-is resolved[key] = value; } } return resolved; } /** * Generate TSIG key for DNS dynamic updates * Planning function (Rule 10.1) - decides what type of key to generate * * @param algorithm - TSIG algorithm (default: hmac-sha256) * @returns Base64-encoded TSIG key */ generateTSIGKey(algorithm = 'hmac-sha256'): string { // TSIG keys are typically 256-bit (32 bytes) for hmac-sha256 const keyLength = algorithm === 'hmac-sha256' ? 32 : 32; const key = new Uint8Array(keyLength); crypto.getRandomValues(key); return btoa(String.fromCharCode(...key)); } /** * Generate secure random secret * Planning function - decides length and encoding * * @param length - Length in bytes (default: 32) * @param encoding - Encoding format (default: hex) * @returns Random secret */ generateSecret(length = 32, encoding: 'hex' | 'base64' = 'hex'): string { const bytes = new Uint8Array(length); crypto.getRandomValues(bytes); if (encoding === 'base64') { return btoa(String.fromCharCode(...bytes)); } // Hex encoding return Array.from(bytes) .map((b) => b.toString(16).padStart(2, '0')) .join(''); } }