deckhearth/.convoys/drop-public-setup/brief-1-env-var-admin-password.md
Randall Stillwell 4d32659545 architect(drop-public-setup): plan + brief 1 (env-var admin password)
Single-brief convoy. ~25 LOC net across 2 files (scripts/setup-neon-db.js,
README.md). No fan-out; single PR.

Decisions:
  A1 — going-forward only (matches JWT_SECRET pattern from
       fix-auth-bypass Brief 1). Existing weak-hash admin rows in
       deployed envs are NOT rotated; operators rotate manually
       via the app after merge. Queue rotate-default-admin follow-up
       if a real audit finds a deploy still on the weak hash.
  B  — operational change to setup-neon-db.js is allowed; no-go-zones
       rule prohibits SCHEMA edits, not env-var gating.
  C1 — no vitest coverage. Fail-loud path is validated by manual smoke
       (the brief mandates pasting fail-loud + happy-path output into
       the PR description).

Key risks tracked: R1 (existing weak hash), R2 (unhelpful error),
R3 (stdout password leak — delete the console.log lines, do NOT
interpolate the env-var), R4 (silent rotation if ON CONFLICT changed
to DO UPDATE), R5 (README env block omits the new var), R6 (3 sibling
files still have admin123 — out of scope per convoy spec).

Flagged-but-deferred:
  - CommonJS in ESM package (setup-neon-db.js) → convert-setup-db-to-esm
  - admin123 in reset-db.js, create-test-users.js, TESTING_GUIDE.md
    → purge-weak-creds-from-helpers (or fold into launch-polish)
  - admin@tcgvault.com hardcoded email → pick-a-name convoy

addresses: P0 #3 from .convoys/ship-readiness.md
parent: ship-readiness
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 17:02:44 -05:00

15 KiB

convoy brief_number depends_on files
drop-public-setup 1
scripts/setup-neon-db.js
README.md

Brief 1: Require ADMIN_INITIAL_PASSWORD; strip credentials from README

Goal (1 sentence)

Gate admin-user creation in scripts/setup-neon-db.js behind a required ADMIN_INITIAL_PASSWORD env var (fail-loud with an actionable message and exit code 1 if unset), remove the hardcoded 'admin123' literal and the console.log lines that echo credentials to stdout, and replace the README's "Default Admin Account" section with a "First-time admin setup" paragraph that documents the new env var.

Files in scope (do not edit anything else)

  • scripts/setup-neon-db.js — modified
  • README.md — modified

Conventions to follow

  • No-go-zones reading. .cursor/rules/no-go-zones.mdc prohibits editing scripts/setup-neon-db.js for schema changes (DDL, columns, idempotency-of-DDL). This brief's change is operational — env-var gate plus two console.log removals, with zero changes to CREATE TABLE blocks, table shape, or the ON CONFLICT clause. The Architect explicitly recorded this distinction in .convoys/drop-public-setup.md § Decisions B. Do not treat the no-go-zones rule as banning operational hardening of this file.
  • Match existing file style. scripts/setup-neon-db.js uses #!/usr/bin/env node shebang and CommonJS require('dotenv') / require('@neondatabase/serverless') / require('bcryptjs'). The repo's package.json is "type": "module", but converting CJS → ESM is out of scope for this brief (flagged for a separate follow-up convoy in .convoys/drop-public-setup.md § Anything flagged but not acted on). Keep require() everywhere; do not introduce import statements.
  • Fail-loud-message shape, modeled on Brief 1 of fix-auth-bypass. That brief's lib/auth-secret.js::JWT_SECRET throw is the template for the actionable-message shape (names the env var, points at .env.local, suggests a generation tip, mentions CI as an alternative). See .convoys/fix-auth-bypass/brief-1-central-jwt-secret-helper.md § "Acceptance criteria → lib/auth-secret.js (new)" for the verbatim message form. Difference: that brief throws at module load; this brief calls console.error(...) + process.exit(1) at function entry, because the script is a CLI tool, not an imported module — exiting with a non-zero code is the canonical CLI "fail loudly" signal.
  • No new dependencies. bcryptjs and dotenv are already in dependencies. No package.json change.
  • No edits to any file outside files: above. In particular: no lib/, no pages/, no components/, no scripts/reset-db.js, no scripts/create-test-users.js, no TESTING_GUIDE.md. Those three sibling files still reference admin@tcgvault.com / admin123 but are explicitly out of scope per the convoy spec and the architect's flagged follow-ups. If you find yourself wanting to touch them, stop and flag in PR description; do not expand the diff.

Acceptance criteria

scripts/setup-neon-db.js

  • Add the env-var check at the top of setupNeonDatabase(), before the const sql = neon(process.env.POSTGRES_URL); line. The check must:
    • Read process.env.ADMIN_INITIAL_PASSWORD into a local const adminPassword.
    • If adminPassword is undefined, null, empty string, or only whitespace, write a clear error message to console.error and call process.exit(1). Do not call process.exit(1) directly without first logging.
    • The error message body MUST name the env var, point at .env.local, give a generation suggestion, and mention the CI-secret alternative. Verbatim shape (the message body MAY be reworded for tone, but every claim MUST be present):
const adminPassword = process.env.ADMIN_INITIAL_PASSWORD;
if (!adminPassword || !adminPassword.trim()) {
  console.error(
    '❌ ADMIN_INITIAL_PASSWORD environment variable is not set.\n' +
      '\n' +
      '   Set it in .env.local for local dev, or as a CI secret if you run setup from CI.\n' +
      '   Generate a strong password with: openssl rand -base64 24\n' +
      '   See README.md → "First-time admin setup" for the full flow.\n'
  );
  process.exit(1);
}
  • The check sits BEFORE the neon(...) call so the script does not open a DB connection (and does not run any DDL) when the env var is missing. This is the documented contract — operators see the error and fix their env BEFORE touching the database.
  • Replace bcrypt.hash('admin123', 12) (currently line 131) with bcrypt.hash(adminPassword, 12). The 12 cost factor is unchanged.
  • Do not change ON CONFLICT (email) DO NOTHING on the INSERT INTO users statement (line 136). This is intentional — re-running setup-db on an env that already has the admin row is a no-op for the password. Rotation is out of scope (Decision A — see convoy file).
  • Delete the two credential-echo console.log lines in the success summary (currently lines 144-145):
// before:
console.log('   Admin User: admin@tcgvault.com');
console.log('   Admin Password: admin123');

// after: replace BOTH lines with a single line that does not echo credentials:
console.log('   Admin user ready (email: admin@tcgvault.com)');

Do not interpolate adminPassword into the log — that would put the chosen password into stdout (and into CI logs if setup ever runs there). The chosen password lives in the operator's env-var source only.

  • The script's existing CommonJS require() calls, transaction structure (none — each sql\` is its own connection per Neon HTTP semantics), and error handling (try/catchwithprocess.exit(1)` on failure) are unchanged.
  • No new require() imports. bcryptjs is already required inline at line 130 (move it to the top of the file ONLY if the linter complains; otherwise leave inline to keep the diff minimal).

README.md

  • Update the env-example block in step 3 (currently lines 44-52) of the Installation section. Add ADMIN_INITIAL_PASSWORD after JWT_SECRET with a comment. Final shape of that block:
POSTGRES_URL="postgresql://your-username:your-password@your-host/your-database"
JWT_SECRET="<generate with: openssl rand -hex 32>"
# Required for `npm run setup-db` — used once to hash the initial admin password.
# Set in .env.local for local dev, or as a CI secret if you run setup from CI.
ADMIN_INITIAL_PASSWORD="<generate with: openssl rand -base64 24>"
# Optional — exercise the rate limiter locally. Without them, `lib/rate-limit.js`
# warn-and-no-ops in dev. In production these are auto-provisioned by the
# Vercel Upstash Marketplace integration.
KV_REST_API_URL="https://<your-upstash-host>.upstash.io"
KV_REST_API_TOKEN="<your-upstash-rest-token>"

Keep the existing trailing line `JWT_SECRET` is **required** — `lib/auth-secret.js` throws at import time if it's unset.. Add a second sentence immediately after it: `ADMIN_INITIAL_PASSWORD` is **required** for `npm run setup-db` — the script exits with code 1 if it's unset.

  • Replace the "🔐 Default Admin Account" section (currently lines 115-119) with a "🔐 First-time admin setup" section. New copy (markdown body is suggestive, not verbatim — adjust prose to match repo voice, but every claim below MUST be present):
## 🔐 First-time admin setup

`npm run setup-db` creates a single admin user the first time it runs. The
password is read from the `ADMIN_INITIAL_PASSWORD` environment variable; the
script exits with code 1 (and does not open a database connection) if the
variable is unset or empty.

- **Local dev:** set `ADMIN_INITIAL_PASSWORD` in `.env.local` before running
  `npm run setup-db`. Use `openssl rand -base64 24` (or any other strong
  source) to generate the value.
- **CI / Vercel:** set `ADMIN_INITIAL_PASSWORD` as a project secret if setup
  ever runs from CI. The env var is **only** read by the seed script; runtime
  auth uses the per-user password stored in the database.
- **Admin email:** the seed creates `admin@tcgvault.com`. Change the password
  immediately after first login via the app's profile settings.

> **Operators of envs that pre-date this change:** `npm run setup-db` is
> idempotent (`ON CONFLICT (email) DO NOTHING`) — re-running it with
> `ADMIN_INITIAL_PASSWORD` set will **not** rotate an existing admin row's
> password. If your environment was set up before this change and still has
> the weak default (`admin123`), rotate the password manually via the app
> after logging in, or wait for the queued `rotate-default-admin` follow-up
> convoy.
  • Remove every other occurrence of admin123 in README.md. Grep verification: rg 'admin123' README.md returns zero hits after this brief.
  • Do NOT remove the line **Admin Panel**: Manage cards and users under "## 🚀 Features" — that's a feature description, not a credential.
  • Do NOT touch the "Database Schema" / "API Endpoints" / "Deployment" sections. Out of scope.

Repo-wide grep verification (run before opening PR)

  • rg "'admin123'" --type js scripts/setup-neon-db.js returns zero hits.
  • rg 'admin123' README.md returns zero hits.
  • rg 'process\.env\.ADMIN_INITIAL_PASSWORD' --type js returns exactly one hit — in scripts/setup-neon-db.js.
  • rg 'admin123' --type js scripts/ returns hits in scripts/reset-db.js and scripts/create-test-users.js. Leave those alone — they are explicitly out of scope per the convoy spec (see § "Out of scope" in .convoys/drop-public-setup.md and § "Anything flagged but not acted on" in the architecture section). Document them in the PR description as known-but-deferred references.
  • rg 'admin123' TESTING_GUIDE.md still has one hit (line 7). Leave it; same follow-up bucket.

Smoke (manual — no test runner for this brief; Decision C — C1)

Run these in order and paste the relevant terminal output into the PR description:

  • Fail-loud path. In your shell, unset ADMIN_INITIAL_PASSWORD (or comment it out in .env.local), then run npm run setup-db. Expect:
    • Exit code is 1 (echo $? immediately after returns 1).
    • Stderr contains the error message body (env var name, .env.local reference, openssl rand -base64 24 suggestion, README pointer).
    • Stdout does NOT contain ✅ Connecting to Neon database... — the check fires before the neon(...) call.
  • Happy path. Set ADMIN_INITIAL_PASSWORD=temporary-strong-pw-for-smoke in .env.local (or export it in the shell), then run npm run setup-db. Expect:
    • Exit code is 0.
    • Stdout includes ✅ Created admin user (or the existing equivalent line if the row already exists — both are acceptable; ON CONFLICT DO NOTHING keeps the script idempotent).
    • Stdout does NOT contain the literal string admin123 anywhere.
    • Stdout does NOT contain temporary-strong-pw-for-smoke (the chosen password) anywhere.
  • End-to-end login. With the env var still set, run npm run dev. From a second terminal: curl -sX POST http://localhost:3000/api/auth/login -H 'content-type: application/json' -d '{"email":"admin@tcgvault.com","password":"temporary-strong-pw-for-smoke"}'. Expect HTTP 200 + a JWT in the body only if the admin row was created by this run (i.e. the DB was empty for admin@tcgvault.com before step 2). If the row pre-existed with a different password (e.g. the weak admin123 from before this convoy), expect 401 — that's the documented R1 / Decision A behavior (re-running setup-db does not rotate; that's manual rotation territory). Paste whichever outcome you got and note which case applies.
  • Lint baseline. npm run lint exits 0 (or matches the existing pre-PR baseline — pre-existing lint errors are fine; do not introduce new ones).
  • Tests. npm run test:run is green (no new tests added per Decision C; existing 16 auth tests should still pass — this brief does not touch any file they cover).

Pre-merge operator checklist (paste into PR description)

This is the human-side handoff. Reviewer confirms each item is acknowledged before merging:

  • R1 callout: "Existing deploys with the weak admin123 hash are NOT rotated by this PR. If any deployed environment (production, staging, dev branches) currently has admin@tcgvault.com / admin123 in its database, the operator must rotate the password manually after this merges — log in with the weak password, change it via profile settings, then verify the new hash. The queued rotate-default-admin follow-up convoy will land an idempotent rotation script if any real deploy still has the weak hash after manual triage."
  • CI / Vercel env var: "Before merging, confirm ADMIN_INITIAL_PASSWORD is set as a Vercel project secret on any branch that runs npm run setup-db from CI (today: none — this is a package.json script run manually). If/when a setup CI step is added, the secret MUST be in place first or the CI job will exit 1."
  • Reviewer ran the fail-loud and happy-path smoke locally OR has confirmed the PR description includes terminal output proving both paths.

Out of scope (do not do these)

  • No edit to scripts/reset-db.js, scripts/create-test-users.js, TESTING_GUIDE.md. Sibling weak-credential references — convoy out-of-scope, architect-flagged follow-up.
  • No edit to lib/auth-secret.js. JWT secret model is fix-auth-bypass Brief 1's surface, already shipped.
  • No edit to pages/api/auth/login.js or pages/api/auth/register.js. CORS / rate-limit are fix-auth-bypass Brief 4's surface, already shipped (partial — verify.js deferred to cors-tighten).
  • No CommonJS → ESM conversion of scripts/setup-neon-db.js. Flagged for a separate convoy.
  • No idempotent rotation script under scripts/migrations/. Decision A defers this to the queued rotate-default-admin follow-up convoy.
  • No new vitest tests for the env-var path. Decision C — C1.
  • No package.json change. bcryptjs and dotenv are already installed.
  • No AGENTS.md or .cursor/rules/*.mdc updates. Doc-writer pass updates these AFTER the convoy lands. (Specifically: Gotcha #4 in AGENTS.md is the next doc-writer change; do not touch it in this PR.)
  • No .github/workflows/*.yml change. CI gates for forbidden endpoints are already in place from fix-auth-bypass Brief 3.

Rationale (≤3 sentences)

Gating the seed script behind a required env var converts the one remaining hardcoded credential in the source tree (the 'admin123' bcrypt input) into operator-supplied input, while the README rewrite removes the same credential from the documentation surface — together they close P0 #3 from .convoys/ship-readiness.md and resolve AGENTS.md Gotcha #4. Keeping ON CONFLICT (email) DO NOTHING and deleting (rather than reformatting) the credential-echo console.log lines ensures the brief does not introduce a silent-rotation surprise (R4) or a stdout-leak surprise (R3) on top of the intended fix. Holding the no-go-zones reading at "operational change is allowed" — and documenting it explicitly in this brief — gives the next agent (likely doc-writer or whoever opens rotate-default-admin) a clear precedent for distinguishing operational hardening of setup-neon-db.js from the prohibited schema-edit case.