86 lines
4.6 KiB
TypeScript
86 lines
4.6 KiB
TypeScript
import { homedir } from 'node:os';
|
|
import { join, resolve } from 'node:path';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { parseEnv } from 'node:util';
|
|
import { ADMIN_KEY, applicationEnvironment, fetchSecrets, localDeploymentAdmin, readCredentials, REQUIRED_KEYS, validateProject } from './lib/infisical';
|
|
|
|
const HELP = `Infisical-backed development deployment (secret values are never printed by this helper)
|
|
|
|
bun run secrets:status
|
|
bun run secrets:run -- <trusted command> [arguments...]
|
|
bun run secrets:run --deployment -- <trusted deployment command> [arguments...]
|
|
|
|
Optional: --credentials-file PATH (default ~/.config/evebot/infisical.env), --worker to inject only worker-safe application secrets, or --deployment --configure for Convex function configuration.
|
|
Project metadata: .infisical.json. Public deployment configuration: config/dev.env.
|
|
Only allowlisted app secrets are injected. --deployment additionally permits the Convex admin key
|
|
and, when absent from the vault, derives it from the local persistent backend without printing it.
|
|
Never use this wrapper with printenv, env, shell tracing, or docker compose config without --quiet.
|
|
`;
|
|
|
|
export function parseArguments(args: string[]) {
|
|
const [action, ...rest] = args;
|
|
if (action !== 'status' && action !== 'run') throw new Error('Use status or run');
|
|
let command: string[] = [];
|
|
let credentialsFile = join(homedir(), '.config/evebot/infisical.env');
|
|
let deployment = false;
|
|
let worker = false;
|
|
let configure = false;
|
|
for (let i = 0; i < rest.length; i++) {
|
|
if (rest[i] === '--') { command = rest.slice(i + 1); break; }
|
|
if (rest[i] === '--credentials-file' && rest[i + 1] && !rest[i + 1].startsWith('--')) credentialsFile = resolve(rest[++i]);
|
|
else if (rest[i] === '--deployment') deployment = true;
|
|
else if (rest[i] === '--worker') worker = true;
|
|
else if (rest[i] === '--configure') configure = true;
|
|
// `bun run` consumes its own -- separator before forwarding script arguments.
|
|
else if (action === 'run' && !rest[i].startsWith('-')) { command = rest.slice(i); break; }
|
|
else throw new Error('Unknown or incomplete secrets helper option');
|
|
}
|
|
if (action === 'run' && !command.length) throw new Error('Supply a trusted command after --');
|
|
if (action === 'status' && command.length) throw new Error('status does not execute a command');
|
|
if (worker && deployment) throw new Error('Worker secret mode cannot request deployment admin credentials');
|
|
if (worker && configure) throw new Error('Worker and configure secret modes are mutually exclusive');
|
|
if (configure && !deployment) throw new Error('--configure requires --deployment so the Convex admin credential is available');
|
|
return { action, command, credentialsFile, deployment, worker, configure };
|
|
}
|
|
|
|
async function main(args: string[]): Promise<void> {
|
|
if (!args.length || args[0] === '--help') { console.log(HELP); return; }
|
|
const { action, command, credentialsFile, deployment, worker, configure } = parseArguments(args);
|
|
const project = validateProject(JSON.parse(await readFile(new URL('../.infisical.json', import.meta.url), 'utf8')));
|
|
const credentials = await readCredentials(credentialsFile);
|
|
const secrets = await fetchSecrets(project, credentials);
|
|
if (action === 'status') {
|
|
console.log(JSON.stringify({
|
|
authenticated: true,
|
|
domain: project.domain, projectId: project.workspaceId,
|
|
environment: project.defaultEnvironment, secretPath: project.secretPath,
|
|
secretNames: Object.keys(secrets).sort(),
|
|
missingRequiredKeys: REQUIRED_KEYS.filter((key) => !secrets[key]?.trim()),
|
|
values: 'not displayed',
|
|
}, null, 2));
|
|
return;
|
|
}
|
|
const publicConfig = parseEnv(await readFile(new URL('../config/dev.env', import.meta.url), 'utf8'));
|
|
const env = applicationEnvironment(process.env, secrets, publicConfig, deployment, worker ? 'worker' : configure ? 'configure' : 'web');
|
|
if (deployment && !env[ADMIN_KEY]) env[ADMIN_KEY] = await localDeploymentAdmin(env);
|
|
const child = Bun.spawn(command, { env, stdin: 'inherit', stdout: 'inherit', stderr: 'inherit' });
|
|
const interrupt = () => child.kill('SIGINT');
|
|
const terminate = () => child.kill('SIGTERM');
|
|
process.on('SIGINT', interrupt);
|
|
process.on('SIGTERM', terminate);
|
|
try { process.exitCode = await child.exited; }
|
|
finally {
|
|
process.off('SIGINT', interrupt);
|
|
process.off('SIGTERM', terminate);
|
|
}
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
try { await main(process.argv.slice(2)); }
|
|
catch (error) {
|
|
// Never print stack/source excerpts or raw CLI errors around the secret boundary.
|
|
console.error(error instanceof Error ? error.message : 'Secret configuration failed');
|
|
process.exitCode = 1;
|
|
}
|
|
}
|