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>
15 KiB
| convoy | brief_number | depends_on | files | ||
|---|---|---|---|---|---|
| drop-public-setup | 1 |
|
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— modifiedREADME.md— modified
Conventions to follow
- No-go-zones reading.
.cursor/rules/no-go-zones.mdcprohibits editingscripts/setup-neon-db.jsfor schema changes (DDL, columns, idempotency-of-DDL). This brief's change is operational — env-var gate plus twoconsole.logremovals, with zero changes toCREATE TABLEblocks, table shape, or theON CONFLICTclause. 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.jsuses#!/usr/bin/env nodeshebang and CommonJSrequire('dotenv')/require('@neondatabase/serverless')/require('bcryptjs'). The repo'spackage.jsonis"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). Keeprequire()everywhere; do not introduceimportstatements. - Fail-loud-message shape, modeled on Brief 1 of
fix-auth-bypass. That brief'slib/auth-secret.js::JWT_SECRETthrow 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 callsconsole.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.
bcryptjsanddotenvare already independencies. Nopackage.jsonchange. - No edits to any file outside
files:above. In particular: nolib/, nopages/, nocomponents/, noscripts/reset-db.js, noscripts/create-test-users.js, noTESTING_GUIDE.md. Those three sibling files still referenceadmin@tcgvault.com/admin123but 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 theconst sql = neon(process.env.POSTGRES_URL);line. The check must:- Read
process.env.ADMIN_INITIAL_PASSWORDinto a localconst adminPassword. - If
adminPasswordisundefined,null, empty string, or only whitespace, write a clear error message toconsole.errorand callprocess.exit(1). Do not callprocess.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):
- Read
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) withbcrypt.hash(adminPassword, 12). The12cost factor is unchanged. - Do not change
ON CONFLICT (email) DO NOTHINGon theINSERT INTO usersstatement (line 136). This is intentional — re-runningsetup-dbon 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.loglines 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 — eachsql\`is its own connection per Neon HTTP semantics), and error handling (try/catchwithprocess.exit(1)` on failure) are unchanged. - No new
require()imports.bcryptjsis 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_PASSWORDafterJWT_SECRETwith 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
admin123inREADME.md. Grep verification:rg 'admin123' README.mdreturns zero hits after this brief. - Do NOT remove the line
**Admin Panel**: Manage cards and usersunder "## 🚀 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.jsreturns zero hits.rg 'admin123' README.mdreturns zero hits.rg 'process\.env\.ADMIN_INITIAL_PASSWORD' --type jsreturns exactly one hit — inscripts/setup-neon-db.js.rg 'admin123' --type js scripts/returns hits inscripts/reset-db.jsandscripts/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.mdand § "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.mdstill 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 runnpm run setup-db. Expect:- Exit code is 1 (
echo $?immediately after returns1). - Stderr contains the error message body (env var name,
.env.localreference,openssl rand -base64 24suggestion, README pointer). - Stdout does NOT contain
✅ Connecting to Neon database...— the check fires before theneon(...)call.
- Exit code is 1 (
- Happy path. Set
ADMIN_INITIAL_PASSWORD=temporary-strong-pw-for-smokein.env.local(orexportit in the shell), then runnpm 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 NOTHINGkeeps the script idempotent). - Stdout does NOT contain the literal string
admin123anywhere. - 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 foradmin@tcgvault.combefore step 2). If the row pre-existed with a different password (e.g. the weakadmin123from 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 lintexits 0 (or matches the existing pre-PR baseline — pre-existing lint errors are fine; do not introduce new ones). - Tests.
npm run test:runis 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
admin123hash are NOT rotated by this PR. If any deployed environment (production, staging, dev branches) currently hasadmin@tcgvault.com / admin123in 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 queuedrotate-default-adminfollow-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_PASSWORDis set as a Vercel project secret on any branch that runsnpm run setup-dbfrom CI (today: none — this is apackage.jsonscript 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 isfix-auth-bypassBrief 1's surface, already shipped. - No edit to
pages/api/auth/login.jsorpages/api/auth/register.js. CORS / rate-limit arefix-auth-bypassBrief 4's surface, already shipped (partial —verify.jsdeferred tocors-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 queuedrotate-default-adminfollow-up convoy. - No new
vitesttests for the env-var path. Decision C — C1. - No
package.jsonchange.bcryptjsanddotenvare already installed. - No
AGENTS.mdor.cursor/rules/*.mdcupdates. Doc-writer pass updates these AFTER the convoy lands. (Specifically: Gotcha #4 inAGENTS.mdis the next doc-writer change; do not touch it in this PR.) - No
.github/workflows/*.ymlchange. CI gates for forbidden endpoints are already in place fromfix-auth-bypassBrief 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.