/** * Builds a {@link LookupFn} for evaluating a computed capability field in its * PROVIDER's context. * * A computed field's DSL refs (`secret.ddns_passwords`, `self.hostname`, * `system.primary_domain`, …) name values in the *provider* module's * namespace — not the consumer that reads * `$capability:dns_registrar.domain_list`. So when the resolver hits a * computed marker it builds this lookup over the provider and evaluates there * (the operator-chosen "evaluate in provider context, lazily" model). * * Values are returned TYPED — `module_configs.value_json` and JSON-encoded * secrets are parsed back into objects/arrays — so DSL functions like * `keys(secret.ddns_passwords)` operate on a real map, not a string. * * The lookup is built eagerly (it must `await getOrCreateMasterKey()` to * decrypt secrets, and {@link LookupFn} is synchronous), then returned as a * sync closure over the loaded namespaces. */ import { eq } from 'drizzle-orm'; import type { DbClient } from '../../db/client'; import { moduleConfigs, secrets, systemConfig, systemSecrets } from '../../db/schema'; import { decryptSecret } from '../../secrets/encryption'; import { getOrCreateMasterKey } from '../../secrets/master-key'; import { parseStoredConfigValue } from '../../services/module-config'; import type { LookupFn } from './evaluate'; /** Navigate a dotted path into a value; undefined if any segment is absent. */ function navigate(root: unknown, path: string[]): unknown { let current = root; for (const seg of path) { if (current && typeof current === 'object' && seg in (current as Record)) { current = (current as Record)[seg]; } else { return undefined; } } return current; } /** Best-effort recover a typed value from a stored string (JSON or scalar). */ function parseMaybeJson(raw: string): unknown { try { return JSON.parse(raw); } catch { return raw; } } /** * Build a lookup over `providerModuleId`'s config, secrets, and system data. * Eagerly loads every namespace (decrypting secrets with the master key) and * returns a synchronous lookup closure. */ export async function buildProviderLookup( providerModuleId: string, db: DbClient, ): Promise { const masterKey = await getOrCreateMasterKey(); // self: the provider module's config, typed via value_json. const selfMap: Record = {}; for (const row of db .select() .from(moduleConfigs) .where(eq(moduleConfigs.moduleId, providerModuleId)) .all()) { try { selfMap[row.key] = parseStoredConfigValue(row); } catch { // Pre-Defect-1 / unparseable row — fall back to the raw display value. selfMap[row.key] = row.value; } } // secret: the provider module's decrypted secrets, JSON-parsed where possible. const secretMap: Record = {}; for (const row of db.select().from(secrets).where(eq(secrets.moduleId, providerModuleId)).all()) { try { const plaintext = decryptSecret( { encryptedValue: row.encryptedValue, iv: row.iv, authTag: row.authTag }, masterKey, ); secretMap[row.name] = parseMaybeJson(plaintext); } catch { // Skip undecryptable secrets — a reference surfaces as "could not be // resolved" from the evaluator. } } // system: global config. const systemMap: Record = {}; for (const row of db.select().from(systemConfig).all()) { systemMap[row.key] = parseMaybeJson(row.value); } // system_secret: decrypted global secrets. const systemSecretMap: Record = {}; for (const row of db.select().from(systemSecrets).all()) { try { const plaintext = decryptSecret( { encryptedValue: row.encryptedValue, iv: row.iv, authTag: row.authTag }, masterKey, ); systemSecretMap[row.key] = parseMaybeJson(plaintext); } catch { // skip undecryptable } } return (root, path) => { switch (root) { case 'self': return navigate(selfMap, path); case 'secret': return navigate(secretMap, path); case 'system': return navigate(systemMap, path); case 'system_secret': return navigate(systemSecretMap, path); default: // `capability` cross-refs from inside a computed field are not // supported in v1 — they'd reintroduce the provider→provider coupling // computed variables exist to avoid. Returns undefined → a clear // "could not be resolved" error. return undefined; } }; }