Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
122 lines
4.6 KiB
JavaScript
122 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* ADR-0040 P3 — Seed `_secrets` from current `process.env` values.
|
|
*
|
|
* Idempotent one-shot: for each Tier-1 entry in `services/secrets/registry.js`
|
|
* — if `process.env[ENV]` is set AND the vault has no row yet for `vaultKey`
|
|
* — encrypt + insert. Skips when vault already holds the key (never
|
|
* overwrites an existing row; rotation goes through the Settings UI).
|
|
*
|
|
* Run during deploy on PROD/.205 + .72:
|
|
*
|
|
* $ node backend/scripts/seed-secrets-from-env.js
|
|
* $ node backend/scripts/seed-secrets-from-env.js --actor=1
|
|
* $ node backend/scripts/seed-secrets-from-env.js --dry-run
|
|
*
|
|
* Exit codes:
|
|
* 0 — all reachable keys seeded (or already present)
|
|
* 2 — vault not configured (SECRETS_MASTER_KEY missing) — refused
|
|
* 1 — at least one putSecret failed
|
|
*/
|
|
|
|
import dotenv from 'dotenv';
|
|
dotenv.config();
|
|
|
|
import vault from '../services/secrets/SecretsVault.js';
|
|
import { TIER_1_SECRETS } from '../services/secrets/registry.js';
|
|
import { getAdapter as getDbAdapter } from '../database/connection.js';
|
|
|
|
const ACTOR_FLAG = process.argv.find((a) => a.startsWith('--actor='));
|
|
const DEFAULT_ACTOR = ACTOR_FLAG ? Number(ACTOR_FLAG.split('=')[1]) : 1; // space 11 owner
|
|
const DRY_RUN = process.argv.includes('--dry-run');
|
|
|
|
function pickEnv(envFallback) {
|
|
const names = Array.isArray(envFallback) ? envFallback : [envFallback];
|
|
for (const n of names) if (process.env[n]) return { name: n, value: process.env[n] };
|
|
return null;
|
|
}
|
|
|
|
async function main() {
|
|
if (!process.env.SECRETS_MASTER_KEY) {
|
|
console.error('❌ SECRETS_MASTER_KEY not set — refusing to seed.');
|
|
console.error(' Generate one with: openssl rand -base64 32');
|
|
console.error(' Then set it in .env on this host and re-run.');
|
|
process.exit(2);
|
|
}
|
|
|
|
const adapter = await getDbAdapter();
|
|
await vault.init({ adapter, allowEnvFallback: false });
|
|
|
|
const report = { seeded: [], skipped: [], missing: [], failed: [] };
|
|
|
|
for (const entry of TIER_1_SECRETS) {
|
|
const env = pickEnv(entry.envFallback);
|
|
if (!env) {
|
|
report.missing.push(entry.vaultKey);
|
|
continue;
|
|
}
|
|
|
|
// Idempotency check via direct adapter query (avoids decrypting just to
|
|
// know existence + sidesteps cache).
|
|
const existing = await adapter.query(
|
|
`SELECT id FROM _secrets WHERE key = $1 LIMIT 1`,
|
|
[entry.vaultKey]
|
|
);
|
|
if (existing.rowCount > 0) {
|
|
report.skipped.push({ key: entry.vaultKey, reason: 'already in vault' });
|
|
continue;
|
|
}
|
|
|
|
if (DRY_RUN) {
|
|
report.seeded.push({ key: entry.vaultKey, env: env.name, dryRun: true });
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await vault.putSecret(entry.vaultKey, env.value, {
|
|
actor: DEFAULT_ACTOR,
|
|
description: `[${entry.category}] ${entry.description}`,
|
|
});
|
|
report.seeded.push({ key: entry.vaultKey, env: env.name });
|
|
} catch (err) {
|
|
report.failed.push({ key: entry.vaultKey, error: err.message });
|
|
}
|
|
}
|
|
|
|
// ── Report ────────────────────────────────────────────────────────────
|
|
console.log('');
|
|
console.log(`ADR-0040 seed report (host: ${process.env.HOSTNAME || 'unknown'}, dryRun=${DRY_RUN})`);
|
|
console.log('─────────────────────────────────────────────────────────────');
|
|
for (const s of report.seeded) {
|
|
const tag = s.dryRun ? '[DRY]' : '[SEED]';
|
|
console.log(`${tag} ${s.key.padEnd(32)} ← process.env.${s.env}`);
|
|
}
|
|
for (const s of report.skipped) {
|
|
console.log(`[SKIP] ${s.key.padEnd(32)} — ${s.reason}`);
|
|
}
|
|
for (const s of report.missing) {
|
|
console.log(`[GAP ] ${s.padEnd(32)} — env not set on this host`);
|
|
}
|
|
for (const s of report.failed) {
|
|
console.log(`[FAIL] ${s.key.padEnd(32)} — ${s.error}`);
|
|
}
|
|
console.log('─────────────────────────────────────────────────────────────');
|
|
console.log(
|
|
`seeded=${report.seeded.length} ` +
|
|
`skipped=${report.skipped.length} ` +
|
|
`missing=${report.missing.length} ` +
|
|
`failed=${report.failed.length}`
|
|
);
|
|
|
|
await vault.shutdown();
|
|
if (adapter && typeof adapter.close === 'function') {
|
|
try { await adapter.close(); } catch { /* ignore */ }
|
|
}
|
|
|
|
process.exit(report.failed.length > 0 ? 1 : 0);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('seed-secrets-from-env: fatal:', err);
|
|
process.exit(1);
|
|
});
|