import { evaluateComputed } from './computed/evaluate'; import { asComputedExpression } from './computed/marker'; import { buildProviderLookup } from './computed/provider-lookup'; import { applyIndex, parsePath, parseVariables } from './parser'; import type { ResolutionContext, ResolveResult, TemplateResolveResult, VariableReference, } from './types'; /** * Get nested value from object using dot notation * * @param obj - Object to search * @param path - Dot-separated path (e.g., 'dns_registrar.primary_domain') * @returns Value if found, undefined otherwise */ function getNestedValue(obj: Record, path: string): unknown { const parts = path.split('.'); let current: unknown = obj; for (const part of parts) { if (current === null || current === undefined) { return undefined; } if (typeof current !== 'object') { return undefined; } current = (current as Record)[part]; } return current; } /** * Resolve a single variable reference * * Execution function (Rule 10.1) - may perform database access for capability secrets * * @param variable - Variable reference to resolve * @param context - Resolution context with all data sources * @param db - Database connection * @returns Resolved value or error */ export async function resolveVariable( variable: VariableReference, context: ResolutionContext, db: ReturnType, ): Promise { switch (variable.type) { case 'self': { const value = context.selfConfig[variable.path]; if (value === undefined) { return { success: false, variable: variable.raw, error: `Self variable '${variable.path}' not found in module configuration`, }; } return { success: true, value }; } case 'system': { const value = context.systemConfig[variable.path]; if (value === undefined) { return { success: false, variable: variable.raw, error: `System variable '${variable.path}' not found`, }; } return { success: true, value }; } case 'infra': { // Format: $infra:. — references a deployed system by // its stable handle (requires.systems[].name). openspec/specs/module-systems-addressing/spec.md. const dot = variable.path.indexOf('.'); if (dot === -1) { return { success: false, variable: variable.raw, error: `Infra variable must be '$infra:.' (e.g. $infra:main.ipv4_address); got '${variable.path}'`, }; } const systemName = variable.path.slice(0, dot); const field = variable.path.slice(dot + 1); const systems = context.systems ?? {}; const system = systems[systemName]; if (!system) { const known = Object.keys(systems); return { success: false, variable: variable.raw, error: `No deployed system named '${systemName}' for module ${context.moduleId}${known.length ? ` (known: ${known.join(', ')})` : ' (none recorded yet)'}`, }; } const value = (system as unknown as Record)[field]; if (value === undefined || value === '') { return { success: false, variable: variable.raw, error: `Infra field '${field}' not available on system '${systemName}' (fields: name, hostname, ipv4_address, zone, cidr, vmid)`, }; } return { success: true, value: value as string }; } case 'system_secret': { const value = context.systemSecrets[variable.path]; if (value === undefined) { return { success: false, variable: variable.raw, error: `System secret '${variable.path}' not found`, }; } return { success: true, value }; } case 'secret': { const value = context.secrets[variable.path]; if (value === undefined) { return { success: false, variable: variable.raw, error: `Secret '${variable.path}' not found for module ${context.moduleId}`, }; } return { success: true, value }; } case 'capability': { // Format: capability_name.path.to.value const [capabilityName, ...pathParts] = variable.path.split('.'); if (!capabilityName) { return { success: false, variable: variable.raw, error: 'Capability variable must specify capability name (e.g., dns_registrar.primary_domain)', }; } if (pathParts.length === 0) { return { success: false, variable: variable.raw, error: `Capability variable must specify data path (e.g., ${capabilityName}.nameserver)`, }; } const fieldPath = pathParts.join('.'); // Check if this field is a secret const { isCapabilityFieldSecret, checkCapabilitySecretAccess, getCapabilitySecret } = await import('../capabilities/secrets'); const isSecret = isCapabilityFieldSecret(capabilityName, fieldPath, db.$client); if (isSecret) { // This is a secret - validate access and decrypt const canAccess = checkCapabilitySecretAccess( context.moduleId, capabilityName, fieldPath, db.$client, ); if (!canAccess) { return { success: false, variable: variable.raw, error: `Module '${context.moduleId}' does not have permission to access secret '${fieldPath}' from capability '${capabilityName}'`, }; } try { const secretValue = await getCapabilitySecret(capabilityName, fieldPath, db.$client); return { success: true, value: secretValue }; } catch (error) { return { success: false, variable: variable.raw, error: `Failed to retrieve secret '${fieldPath}' from capability '${capabilityName}': ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } // Not a secret - access capability data normally const capabilityData = context.capabilities[capabilityName]; if (!capabilityData) { return { success: false, variable: variable.raw, error: `Capability '${capabilityName}' not found or not registered`, }; } const value = getNestedValue(capabilityData, fieldPath); if (value === undefined) { return { success: false, variable: variable.raw, error: `Capability data '${fieldPath}' not found in '${capabilityName}'`, }; } // Computed field (openspec/specs/internal-dns-split-horizon/spec.md D1): // evaluate the DSL expression in the PROVIDER's context. Like the // lazy $self: block below, we look up the provider module, but here // we build a typed lookup over its config/secrets/system data. const computedExpr = asComputedExpression(value); if (computedExpr !== null) { // Find the provider module (raw SQL, matching the lazy $self: block). const providerResult = db.$client .prepare( `SELECT p.id FROM modules p JOIN capabilities c ON p.id = c.module_id WHERE c.capability_name = ? LIMIT 1`, ) .get(capabilityName) as { id: string } | undefined; if (!providerResult) { return { success: false, variable: variable.raw, error: `Capability '${capabilityName}' has a computed field '${fieldPath}' but no provider module was found`, }; } try { const lookup = await buildProviderLookup(providerResult.id, db); const result = evaluateComputed(computedExpr, lookup); // Computed results are often arrays/objects (e.g. domain_list). // In a string-template context we serialize non-scalars as JSON; // structured consumers read the evaluated value directly. const serialized = typeof result === 'string' || typeof result === 'number' || typeof result === 'boolean' ? String(result) : JSON.stringify(result); return { success: true, value: serialized }; } catch (error) { return { success: false, variable: variable.raw, error: `Failed to evaluate computed field '${fieldPath}' of '${capabilityName}': ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } // Check if value contains unresolved $self: variable (lazy resolution) if (typeof value === 'string' && value.startsWith('$self:')) { // Get provider module's config to resolve the variable. // The path can be a plain key ("primary_domain") or include // an array index ("domains[0]") — see parsePath/applyIndex // and the syntax note in the multi-domain DDNS design doc. const { name, index } = parsePath(value.substring(6)); // Get provider module ID const providerQuery = db.$client.prepare( `SELECT p.id FROM modules p JOIN capabilities c ON p.id = c.module_id WHERE c.capability_name = ? LIMIT 1`, ); const providerResult = providerQuery.get(capabilityName) as { id: string } | undefined; if (!providerResult) { return { success: false, variable: variable.raw, error: `Provider module not found for capability '${capabilityName}'`, }; } // Fetch both `value` and `value_json` so we can index into // arrays when the path contained `[N]`. const configQuery = db.$client.prepare( 'SELECT value, value_json FROM module_configs WHERE module_id = ? AND key = ?', ); const configResult = configQuery.get(providerResult.id, name) as | { value: string; value_json: string | null } | undefined; if (!configResult) { return { success: false, variable: variable.raw, error: `Provider module '${providerResult.id}' has not configured '${name}' (required by capability '${capabilityName}')`, }; } const rawValue: unknown = configResult.value_json ? JSON.parse(configResult.value_json) : configResult.value; const indexed = applyIndex(rawValue, index); if (indexed === undefined) { return { success: false, variable: variable.raw, error: index === undefined ? `Provider module '${providerResult.id}' has not configured '${name}'` : `'$self:${name}[${index}]' is out of bounds or '${name}' is not an array on '${providerResult.id}'`, }; } return { success: true, value: typeof indexed === 'string' ? indexed : String(indexed) }; } // Convert value to string if (typeof value === 'string') { return { success: true, value }; } if (typeof value === 'number' || typeof value === 'boolean') { return { success: true, value: String(value) }; } return { success: false, variable: variable.raw, error: `Capability data '${fieldPath}' is not a primitive value (got ${typeof value})`, }; } default: { return { success: false, variable: variable.raw, error: `Unknown variable type: ${(variable as VariableReference).type}`, }; } } } /** * Resolve all variables in a template * * Orchestration function (Rule 10.1) - coordinates parsing and resolution * * @param content - Template content with variables * @param context - Resolution context * @param db - Database connection * @returns Resolved template or errors */ export async function resolveTemplate( content: string, context: ResolutionContext, db: ReturnType, ): Promise { // Parse all variables const variables = parseVariables(content); if (variables.length === 0) { return { success: true, content }; } const errors: Array<{ variable: string; error: string }> = []; let resolvedContent = content; // Resolve each variable for (const variable of variables) { const result = await resolveVariable(variable, context, db); if (!result.success) { errors.push({ variable: result.variable, error: result.error }); } else { // Replace all occurrences of this variable resolvedContent = resolvedContent.replaceAll(variable.raw, result.value); } } if (errors.length > 0) { return { success: false, errors }; } return { success: true, content: resolvedContent }; }