/** * Secret Schema Loader * * Loads and parses schema/secrets.json files for modules * Provides metadata about secret generation behavior */ import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { eq } from 'drizzle-orm'; import { type DbClient, getDb } from '../db/client'; import { modules } from '../db/schema'; /** * Secret metadata from schema */ export interface SecretMetadata { name: string; source: 'generated' | 'user_provided' | 'user_password' | 'generated_optional'; format?: string; length?: number; deriveFrom?: string; deriveMethod?: string; title?: string; description?: string; } /** * Secrets schema structure */ export interface SecretsSchema { $schema?: string; type: 'object'; title?: string; description?: string; required?: string[]; properties: Record; } /** * Individual secret property schema */ export interface SecretPropertySchema { type: string; title?: string; description?: string; source?: 'generated' | 'user_provided' | 'user_password' | 'generated_optional'; format?: string; length?: number; derive_from?: string; derive_method?: string; minLength?: number; maxLength?: number; } /** * Get module source path from database */ function getModuleSourcePath(moduleId: string, db: DbClient = getDb()): string | null { try { const module = db.select().from(modules).where(eq(modules.id, moduleId)).get(); return module?.sourcePath || null; } catch (error) { console.error(`Failed to get module source path for ${moduleId}:`, error); return null; } } /** * Load secrets schema for a module * * Looks for schema/secrets.json in the module directory * * @param moduleId - Module identifier * @param db - Database client * @returns Secrets schema or null if not found */ export async function loadSecretsSchema( moduleId: string, db: DbClient = getDb(), ): Promise { const sourcePath = getModuleSourcePath(moduleId, db); if (!sourcePath) { return null; } const schemaPath = join(sourcePath, 'schema', 'secrets.json'); if (!existsSync(schemaPath)) { return null; } try { const schemaContent = await readFile(schemaPath, 'utf-8'); let schema: SecretsSchema; try { schema = JSON.parse(schemaContent) as SecretsSchema; } catch (parseError) { console.error( `Failed to parse secrets schema JSON for ${moduleId}:`, parseError instanceof Error ? parseError.message : parseError, ); return null; } // Validate basic structure if (!schema.properties || typeof schema.properties !== 'object') { console.error(`Invalid secrets schema for ${moduleId}: missing properties`); return null; } return schema; } catch (error) { console.error(`Failed to load secrets schema for ${moduleId}:`, error); return null; } } /** * Get metadata for a specific secret * * @param moduleId - Module identifier * @param secretName - Secret name * @param db - Database client * @returns Secret metadata or null if not found */ export async function getSecretMetadata( moduleId: string, secretName: string, db: DbClient = getDb(), ): Promise { const schema = await loadSecretsSchema(moduleId, db); if (!schema) { return null; } const propertySchema = schema.properties[secretName]; if (!propertySchema) { return null; } return { name: secretName, source: propertySchema.source || 'user_provided', // Safe default format: propertySchema.format, length: propertySchema.length, deriveFrom: propertySchema.derive_from, deriveMethod: propertySchema.derive_method, title: propertySchema.title, description: propertySchema.description, }; } /** * Get all secret metadata for a module * * @param moduleId - Module identifier * @param db - Database client * @returns Array of secret metadata */ export async function getAllSecretMetadata( moduleId: string, db: DbClient = getDb(), ): Promise { const schema = await loadSecretsSchema(moduleId, db); if (!schema) { return []; } const metadata: SecretMetadata[] = []; for (const [secretName, propertySchema] of Object.entries(schema.properties)) { metadata.push({ name: secretName, source: propertySchema.source || 'user_provided', // Safe default format: propertySchema.format, length: propertySchema.length, deriveFrom: propertySchema.derive_from, deriveMethod: propertySchema.derive_method, title: propertySchema.title, description: propertySchema.description, }); } return metadata; } /** * Validate secrets schema for circular derivation * * Builds derivation graph and checks for cycles using DFS * * @param schema - Secrets schema * @returns Error message if circular derivation detected, null otherwise */ export function validateDerivationGraph(schema: SecretsSchema): string | null { const graph: Record = {}; // Build derivation graph (secret -> derive_from) for (const [secretName, propertySchema] of Object.entries(schema.properties)) { if (propertySchema.derive_from) { graph[secretName] = propertySchema.derive_from; } } // Check for cycles using DFS const visited = new Set(); const recursionStack = new Set(); function hasCycle(node: string): boolean { if (recursionStack.has(node)) { return true; // Cycle detected } if (visited.has(node)) { return false; // Already processed } visited.add(node); recursionStack.add(node); const parent = graph[node]; if (parent && hasCycle(parent)) { return true; } recursionStack.delete(node); return false; } // Check each node in the graph for (const secretName of Object.keys(graph)) { if (hasCycle(secretName)) { return `Circular derivation detected involving: ${secretName}`; } } return null; }