import { constants, realpathSync, statSync, accessSync } from "node:fs"; import { isAbsolute, relative } from "node:path"; import { buildInstallationContract, type InstallationSuffix } from "./contracts.js"; import { DWH_TRANSPORTS, validateWorkspaceDescriptor, type DwhTransport, type WorkspaceDescriptor, } from "./schema.js"; export interface ResolvedEvidenceBinding { values: Record; missing: string[]; } export interface RuntimeBindings { dwh: ResolvedBinding; evidence: ResolvedEvidenceBinding; } export interface ResolvedBinding { transport: DwhTransport; values: Record; missing: string[]; } const REQUIRED_SUFFIXES: Record = { postgres_direct: ["HOST", "PORT", "USER", "PASSWORD_FILE"], rest_api: ["BASE_URL", "API_KEY_FILE"], ssh_tunnel: [ "USER", "PASSWORD_FILE", "SSH_HOST", "SSH_PORT", "SSH_USER", "SSH_PRIVATE_KEY_FILE", "SSH_KNOWN_HOSTS_FILE", "SSH_TARGET_HOST", "SSH_TARGET_PORT", ], }; function isTransport(value: string | undefined): value is DwhTransport { return value !== undefined && (DWH_TRANSPORTS as readonly string[]).includes(value); } function isInside(path: string, root: string): boolean { const pathRelative = relative(root, path); return pathRelative !== "" && !pathRelative.startsWith("..") && !isAbsolute(pathRelative); } function safeSecretFilePath(path: string, secretRoots: readonly string[]): string | undefined { if (!isAbsolute(path)) return undefined; try { const resolvedPath = realpathSync(path); const resolvedRoots = secretRoots.map((root) => realpathSync(root)); if (!resolvedRoots.some((root) => isInside(resolvedPath, root))) return undefined; if (!statSync(resolvedPath).isFile()) return undefined; accessSync(resolvedPath, constants.R_OK); return resolvedPath; } catch { return undefined; } } function requireSupportedDescriptor(workspace: unknown): void { if (typeof workspace !== "object" || workspace === null) { throw new Error("Workspace bindings support only workspace schema version 3"); } const metadata = Reflect.get(workspace, "workspace"); if (typeof metadata !== "object" || metadata === null || Reflect.get(metadata, "schema_version") !== 3) { throw new Error("Workspace bindings support only workspace schema version 3"); } } function requiredSuffixes( workspace: WorkspaceDescriptor, transport: DwhTransport, ): readonly InstallationSuffix[] { const required = REQUIRED_SUFFIXES[transport]; return transport === "rest_api" && workspace.diagnostics?.dwh_rest?.auth === "none" ? required.filter((suffix) => suffix !== "API_KEY_FILE") : required; } /** * Resolve only installation-local values. Secret files remain file paths: their contents are * deliberately left for the harness secret-file loader, so bindings cannot leak credentials. */ export function resolveBinding( workspace: WorkspaceDescriptor, role: "DWH", env: NodeJS.ProcessEnv, secretRoots: readonly string[], ): ResolvedBinding { requireSupportedDescriptor(workspace); const descriptor = validateWorkspaceDescriptor(workspace); const contract = buildInstallationContract(descriptor); const variables = contract.variables.filter((variable) => variable.role === role); const transportVariable = variables.find((variable) => variable.suffix === "TRANSPORT"); const supported = descriptor.dwh.supported_transports; const selectedValue = transportVariable ? env[transportVariable.name] : undefined; const selectedTransport = isTransport(selectedValue) ? selectedValue : supported[0]; const missing: string[] = []; if (transportVariable && (!isTransport(selectedValue) || !supported.includes(selectedTransport))) { missing.push(transportVariable.name); } const required = new Set(requiredSuffixes(descriptor, selectedTransport)); const values: Record = {}; for (const variable of variables) { if (variable.suffix === "TRANSPORT") continue; if (variable.transports && !variable.transports.includes(selectedTransport)) continue; const value = env[variable.name]; const present = value !== undefined && value.trim() !== ""; const safePath = variable.secret && present ? safeSecretFilePath(value, secretRoots) : undefined; const safe = !variable.secret || safePath !== undefined; if ((required.has(variable.suffix) && !present) || (present && !safe)) { missing.push(variable.name); } if (present && safe) values[variable.name] = variable.secret ? safePath! : value; } return { transport: selectedTransport, values, missing }; } /** Resolve descriptor-selected Evidence credentials without reading any secret file contents. */ export function resolveEvidenceBinding( workspace: WorkspaceDescriptor, env: NodeJS.ProcessEnv, secretRoots: readonly string[], ): ResolvedEvidenceBinding { requireSupportedDescriptor(workspace); const descriptor = validateWorkspaceDescriptor(workspace); const variables = buildInstallationContract(descriptor).variables .filter((variable) => variable.role === "EVIDENCE"); if (variables.length === 0) return { values: {}, missing: [] }; const source = descriptor.evidence?.source; const required = new Set( source?.type === "http" ? ["SIGNED_URLS_FILE"] : source?.type === "s3" ? ["ACCESS_KEY_FILE", "SECRET_KEY_FILE"] : [], ); const values: Record = {}; const missing: string[] = []; for (const variable of variables) { const value = env[variable.name]; const present = value !== undefined && value.trim() !== ""; const safePath = present ? safeSecretFilePath(value, secretRoots) : undefined; if ((required.has(variable.suffix) && !present) || (present && safePath === undefined)) { missing.push(variable.name); } if (safePath !== undefined) values[variable.name] = safePath; } return { values, missing }; } /** Resolve the complete schema-v3 runtime binding set. */ export function resolveRuntimeBindings( workspace: WorkspaceDescriptor, env: NodeJS.ProcessEnv, secretRoots: readonly string[], ): RuntimeBindings { requireSupportedDescriptor(workspace); const descriptor = validateWorkspaceDescriptor(workspace); return { dwh: resolveBinding(descriptor, "DWH", env, secretRoots), evidence: resolveEvidenceBinding(descriptor, env, secretRoots), }; } /** SSH bindings remain diagnostic-only until the session runtime owns a long-lived tunnel. */ export function supportsSessionRuntime(bindings: RuntimeBindings): boolean { return bindings.dwh.transport !== "ssh_tunnel" && bindings.evidence.missing.length === 0; }