import { constants } from 'node:fs'; import { open } from 'node:fs/promises'; import { parseEnv } from 'node:util'; import { fileURLToPath } from 'node:url'; export interface VaultProject { workspaceId: string; defaultEnvironment: string; domain: string; secretPath: string; } export const BOOTSTRAP_KEYS = ['INFISICAL_UNIVERSAL_AUTH_CLIENT_ID', 'INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET'] as const; export const APPLICATION_KEYS = [ 'EVE_CLIENT_ID', 'EVE_CLIENT_SECRET', 'TYPESAFE_API_KEY', 'TOKEN_ENCRYPTION_KEY', 'CONVEX_BRIDGE_SECRET', 'APP_AUTH_PRIVATE_KEY', 'APP_AUTH_JWKS', ] as const; export const REQUIRED_KEYS = ['EVE_CLIENT_ID', 'TOKEN_ENCRYPTION_KEY', 'CONVEX_BRIDGE_SECRET', 'APP_AUTH_PRIVATE_KEY', 'APP_AUTH_JWKS'] as const; export const ADMIN_KEY = 'CONVEX_SELF_HOSTED_ADMIN_KEY'; export type Environment = Record; export type CliRunner = (args: string[], env: Environment) => Promise; export function validateProject(input: unknown): VaultProject { if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Invalid Infisical project configuration'); const project = input as Record; if (typeof project.workspaceId !== 'string' || !/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(project.workspaceId)) throw new Error('Invalid Infisical project ID'); if (typeof project.defaultEnvironment !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(project.defaultEnvironment)) throw new Error('Invalid Infisical environment'); // This repository is deliberately pinned to the operator-authorized vault origin. // A repository config change alone must never send the bootstrap credential elsewhere. if (project.domain !== 'https://secrets.jb.codes') throw new Error('Infisical origin is not the approved instance'); if (typeof project.secretPath !== 'string' || !/^\/(?:[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*)?$/.test(project.secretPath)) throw new Error('Invalid Infisical secret path'); return project as unknown as VaultProject; } export async function readCredentials(path: string): Promise> { let handle; try { // Do not follow a symlink or read a world/group-accessible bootstrap secret. handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); const stat = await handle.stat(); if (!stat.isFile() || (stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.() || stat.size > 16_384) { throw new Error('Invalid credentials file'); } const parsed = parseEnv(await handle.readFile('utf8')); const selected: Record = {}; for (const name of BOOTSTRAP_KEYS) { if (!parsed[name]?.trim()) throw new Error('Missing bootstrap credential'); selected[name] = parsed[name]; } return selected; } catch { throw new Error('Cannot read Universal Auth credentials: use a regular, owner-owned 0600 file containing the two documented variables'); } finally { await handle?.close(); } } async function boundedOutput(stream: ReadableStream, limit: number): Promise { const reader = stream.getReader(); const chunks: Uint8Array[] = []; let bytes = 0; try { for (;;) { const { value, done } = await reader.read(); if (done) break; bytes += value.byteLength; if (bytes > limit) throw new Error('Infisical output exceeded its safety limit'); chunks.push(value); } } finally { reader.releaseLock(); } return Buffer.concat(chunks).toString('utf8'); } async function captureSecretCommand(argv: string[], env: Environment): Promise { const child = Bun.spawn(argv, { env, stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' }); const deadline = setTimeout(() => child.kill('SIGKILL'), 30_000); deadline.unref(); try { const [output, , code] = await Promise.all([ boundedOutput(child.stdout, 2 * 1024 * 1024), boundedOutput(child.stderr, 64 * 1024), child.exited, ]); if (code !== 0) throw new Error('CLI failed'); return output; } catch { // CLI errors can embed request details. Never echo raw stdout/stderr or credentials. throw new Error('Credential command failed; check permissions and connectivity (raw output suppressed)'); } finally { clearTimeout(deadline); if (child.exitCode === null) child.kill('SIGKILL'); } } export const runCli: CliRunner = async (args, env) => { const executable = fileURLToPath(new URL('../../node_modules/.bin/infisical', import.meta.url)); return captureSecretCommand([executable, ...args], env); }; export async function localDeploymentAdmin(env: Environment, runner = captureSecretCommand): Promise { if (!['http://127.0.0.1:3210', 'http://localhost:3210'].includes(env.CONVEX_SELF_HOSTED_URL ?? '')) { throw new Error('Local admin derivation is restricted to this host’s Convex deployment'); } // The backend derives its admin credential from its persistent random instance // secret. Docker-host access is already administrative; no second secret file // or write-enabled Infisical identity is required. Only explicit --deployment // commands receive this key; Compose never forwards it to the web container. const key = (await runner(['docker', 'compose', 'exec', '-T', 'backend', './generate_admin_key.sh'], cleanEnvironment(env))).trim(); if (!key || /\s/.test(key) || key.length > 16_384) throw new Error('Unexpected local admin response; output suppressed'); return key; } export function parseExport(text: string): Record { try { const parsed: unknown = JSON.parse(text); const values: Record = Object.create(null); const entries: unknown[] = Array.isArray(parsed) ? parsed : parsed && typeof parsed === 'object' ? Object.entries(parsed).map(([key, value]) => ({ key, value })) : []; if (!parsed || typeof parsed !== 'object') throw new Error(); for (const entry of entries) { if (!entry || typeof entry !== 'object') throw new Error(); const row = entry as Record; const key = row.key ?? row.secretKey; const value = row.value ?? row.secretValue; if (typeof key !== 'string' || !/^[A-Z][A-Z0-9_]*$/.test(key) || typeof value !== 'string' || Object.hasOwn(values, key)) throw new Error(); values[key] = value; } return values; } catch { throw new Error('Invalid Infisical export; values suppressed'); } } export async function fetchSecrets(project: VaultProject, credentials: Record, runner: CliRunner = runCli, base: Environment = process.env): Promise> { validateProject(project); for (const name of BOOTSTRAP_KEYS) if (!credentials[name]) throw new Error(`Missing ${name}`); const env = cleanEnvironment(base); const common = ['--domain', project.domain, '--silent', '--telemetry=false']; const bootstrap = Object.fromEntries(BOOTSTRAP_KEYS.map(key => [key, credentials[key]])); const token = (await runner(['login', '--method=universal-auth', '--plain', ...common], { ...env, ...bootstrap })).trim(); if (!token || /\s/.test(token) || token.length > 16_384) throw new Error('Unexpected Infisical login response; values suppressed'); const output = await runner([ 'export', '--projectId', project.workspaceId, '--env', project.defaultEnvironment, '--path', project.secretPath, '--format=json', '--expand=false', '--include-imports=false', '--secret-overriding=false', ...common, ], { ...env, INFISICAL_TOKEN: token }); return parseExport(output); } export function cleanEnvironment(base: Environment): Environment { const managed = new Set([ADMIN_KEY, ...APPLICATION_KEYS]); return Object.fromEntries(Object.entries(base).filter(([key]) => !key.startsWith('INFISICAL_') && !managed.has(key))); } const PUBLIC_KEYS = new Set([ 'APP_MODE', 'APP_ORIGIN', 'APP_AUTH_ISSUER', 'EVE_CALLBACK_URL', 'PUBLIC_CONVEX_URL', 'CONVEX_URL', 'CONVEX_SELF_HOSTED_URL', 'CONVEX_SITE_ORIGIN', 'REFERENCE_DIR', 'ESI_USER_AGENT', 'WEB_BIND_ADDRESS', 'CONVEX_BIND_ADDRESS', 'WEB_PORT', 'CONVEX_PORT', 'CONVEX_SITE_PORT', 'CONVEX_IMAGE', 'CONVEX_DASHBOARD_IMAGE', ]); export function applicationEnvironment(base: Environment, secrets: Record, publicConfig: Environment, deployment = false): Environment { const env = cleanEnvironment(base); // Public file is the configured deployment, not defaults inherited from Bun's .env.local. for (const [key, value] of Object.entries(publicConfig)) { if (!PUBLIC_KEYS.has(key) || typeof value !== 'string') throw new Error('Unexpected public deployment configuration key'); env[key] = value; } // Managed app keys must come from the selected vault, never a stale ambient .env. for (const key of APPLICATION_KEYS) { delete env[key]; if (secrets[key] !== undefined) env[key] = secrets[key]; } if (deployment && secrets[ADMIN_KEY]) env[ADMIN_KEY] = secrets[ADMIN_KEY]; return env; }