Files
Nexus/scripts/export-dev-seed.mjs
Hartmut 01f8974314
CI / Architecture Guardrails (pull_request) Successful in 2m59s
CI / Typecheck (pull_request) Successful in 6m41s
CI / Lint (pull_request) Successful in 4m18s
CI / Assistant Split Regression (pull_request) Successful in 5m6s
CI / Unit Tests (pull_request) Successful in 7m21s
CI / Build (pull_request) Successful in 5m21s
CI / Fresh-Linux Docker Deploy (pull_request) Failing after 38s
CI / E2E Tests (pull_request) Successful in 3m28s
CI / Release Images (pull_request) Has been skipped
rename(phase 3): compose/DB/infra names + stray code refs capakraken → nexus
- docker-compose.yml / .prod.yml / .ci.yml: project names, POSTGRES_DB/USER,
  pg_isready, DATABASE_URL, volume names (nexus_pgdata, nexus_prod_*)
- .github/workflows/ci.yml: POSTGRES_PASSWORD, pg_isready, psql credentials,
  GRANT statements, POSTGRES_PASSWORD=nexus_dev for Docker Deploy job
- scripts/db-target-guard.mjs: expectedDatabase default, NEXUS_EXPECTED_DB_NAME
- scripts/prisma-with-env.mjs, e2e/test-server.mjs: env-var rename
- packages/db/src/safe-destructive-env.ts + reset-dispo-import.ts: DB name set
- packages/db/src/destructive-db-guard.ts: PROTECTED_DATABASE_NAMES → "nexus"
- packages/db/src/destructive-db-guard.test.ts: all fixture DB names + comments
- .env.example, tooling/deploy/deploy.env.example: DATABASE_URL, image refs
- packages/api: Redis channel/key prefixes (rbac-invalidate, sse, ratelimit),
  logger service name, app-base-url log prefix
- E2E: DB container names, localStorage/sessionStorage keys, email domains
- scripts: architecture-guardrails filter, export/import-dev-seed defaults,
  harden-postgres defaults, start.sh pg_isready, worktree-hygiene fixture
- tooling/migrate/rename-to-nexus.sh: new maintenance-window cutover script

Only intentional capakraken survivor: anonymization.ts DEFAULT_ANONYMIZATION_SEED
(functional cryptographic constant — changing it would invalidate stored aliases).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:35:39 +02:00

163 lines
6.0 KiB
JavaScript

#!/usr/bin/env node
/**
* export-dev-seed.mjs
*
* Dumps the current dev database into packages/db/prisma/dev-seed.sql.
* The dump is safe to commit: passwords, TOTP secrets, SMTP credentials,
* and webhook secrets are all sanitized before writing.
*
* Usage:
* node scripts/export-dev-seed.mjs
*
* Requirements:
* - The nexus-postgres-1 Docker container must be running
* - DATABASE_URL must point to a local nexus database
*/
import { execSync, spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { loadWorkspaceEnv, resolveRealWorkspaceRoot } from "./load-env.mjs";
loadWorkspaceEnv();
const workspaceRoot = resolveRealWorkspaceRoot();
// ── Safety check ─────────────────────────────────────────────────────────────
const rawUrl = process.env["DATABASE_URL"];
if (!rawUrl) {
console.error("❌ DATABASE_URL is not set.");
process.exit(1);
}
let parsedUrl;
try {
parsedUrl = new URL(rawUrl);
} catch {
console.error("❌ DATABASE_URL is not a valid URL.");
process.exit(1);
}
const host = parsedUrl.hostname;
if (!["localhost", "127.0.0.1", "::1"].includes(host)) {
console.error(`❌ Refusing to export from non-local host: ${host}`);
console.error(" export-dev-seed is only for local development databases.");
process.exit(1);
}
// ── Docker container check ────────────────────────────────────────────────────
const CONTAINER = "nexus-postgres-1";
const containerCheck = spawnSync("docker", ["inspect", "--format={{.State.Running}}", CONTAINER], {
encoding: "utf8",
});
if (containerCheck.stdout.trim() !== "true") {
console.error(`❌ Container ${CONTAINER} is not running.`);
console.error(" Start it with: docker compose up -d postgres");
process.exit(1);
}
// ── Tables to exclude entirely ────────────────────────────────────────────────
const EXCLUDE_TABLES = [
"_prisma_migrations",
"audit_logs",
"active_sessions",
"sessions",
"accounts",
"verification_tokens",
"invite_tokens",
"notifications",
"import_batches",
"staged_assignments",
"staged_availability_rules",
"staged_clients",
"staged_projects",
"staged_resources",
"staged_unresolved_records",
"staged_vacations",
];
const excludeFlags = EXCLUDE_TABLES.flatMap((t) => ["--exclude-table-data", `public.${t}`]);
// ── Run pg_dump inside the Docker container ───────────────────────────────────
const DB_USER = decodeURIComponent(parsedUrl.username) || "nexus";
const DB_NAME = parsedUrl.pathname.replace(/^\/+/, "") || "nexus";
const DB_PORT = parsedUrl.port || "5432";
console.log(`🔍 Exporting ${DB_USER}@${host}:${DB_PORT}/${DB_NAME}`);
const pgDumpArgs = [
"exec",
CONTAINER,
"pg_dump",
"-U", DB_USER,
"-d", DB_NAME,
"--data-only",
"--no-owner",
"--no-acl",
"--disable-triggers",
...excludeFlags,
];
const dump = spawnSync("docker", pgDumpArgs, { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
if (dump.status !== 0) {
console.error("❌ pg_dump failed:");
console.error(dump.stderr);
process.exit(1);
}
// ── Sanitize sensitive values ─────────────────────────────────────────────────
let sql = dump.stdout;
// Replace argon2id password hashes with a clearly invalid placeholder.
// The import script will update these with a real dev hash.
sql = sql.replace(/\$argon2id\$[^\t\n\\]*/g, "__DEV_PASSWORD_HASH__");
// Append sanitizing statements (TOTP, SMTP password, webhook secrets).
// These run after the COPY blocks so they don't require line-level parsing.
sql += `
-- ─── Sanitize secrets (applied after data load) ──────────────────────────────
UPDATE users SET "totpSecret" = NULL, "totpEnabled" = false;
UPDATE system_settings SET "smtpPassword" = NULL;
UPDATE webhooks SET secret = NULL;
`;
// ── Add header ────────────────────────────────────────────────────────────────
const header = `-- Nexus dev seed — exported ${new Date().toISOString()}
-- Source: ${DB_USER}@${host}:${DB_PORT}/${DB_NAME}
--
-- Excluded tables:
${EXCLUDE_TABLES.map((t) => `-- ${t}`).join("\n")}
--
-- Sanitized fields:
-- users.passwordHash → placeholder (import-dev-seed sets "Dev123456!")
-- users.totpSecret → NULL
-- users.totpEnabled → false
-- system_settings.smtpPassword → NULL
-- webhooks.secret → NULL
--
-- Import with:
-- node scripts/import-dev-seed.mjs
-- ─────────────────────────────────────────────────────────────────────────────
`;
// ── Write output ──────────────────────────────────────────────────────────────
const outPath = resolve(workspaceRoot, "packages/db/prisma/dev-seed.sql");
writeFileSync(outPath, header + sql, "utf8");
const lines = (header + sql).split("\n").length;
const sizeKb = Math.round(Buffer.byteLength(header + sql, "utf8") / 1024);
console.log(`✅ Written to packages/db/prisma/dev-seed.sql`);
console.log(` ${lines.toLocaleString()} lines · ${sizeKb.toLocaleString()} KB`);
console.log();
console.log("Next step: commit dev-seed.sql or share it with your team.");
console.log("Import it with: node scripts/import-dev-seed.mjs");