godcrm/backend/scripts/seed-secrets-from-env.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

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);
});