`scripts/create-test-users.js` hardcoded `bcrypt.hash('alice123', 12)`
+ `bcrypt.hash('bob123', 12)` and echoed those literals back to stdout
both per-user and in a final summary block. `TESTING_GUIDE.md`'s Test
Accounts table documented the same `admin123` / `alice123` / `bob123`
trio. These were the last two weak-credential surfaces left in the
helper-script + manual-QA-doc tree after `drop-public-setup` (commits
`ff80753` + `b63b509`) and `fix-reset-db-script` (squash `3ab9bf8`,
PR #25) closed the `setup-neon-db.js` and `reset-db.js` halves of the
umbrella `purge-weak-creds-from-helpers` queued follow-up.
The fix mirrors the post-`drop-public-setup` `setup-neon-db.js`
pattern and the post-PR-#25 `reset-db.js` pattern verbatim, with one
deliberate simplification: a single `TEST_USERS_PASSWORD` env var
covers both alice + bob rather than per-user env vars (risk R2 in the
convoy file argues this — these are fixture users for the
collaboration demo flow, not independent identities, and per-user
sprawl would double the env-var contract for zero security benefit).
`createTestUsers()` now reads `process.env.TEST_USERS_PASSWORD` at the
top of the function body and exits with code 1 BEFORE opening any DB
connection if the var is unset or whitespace-only, with the same
helpful-error wording template the other two scripts use (names the
var, points at `.env.local`, suggests `openssl rand -base64 24`,
references README's "First-time admin setup" section). All four
password-echo `console.log` lines are deleted; the new summary
documents *where* the password comes from without ever printing it.
`TESTING_GUIDE.md`'s Test Accounts table is rewritten to show password
source per user instead of the literal value; the two inline
`Password: alice123` / `Password: bob123` workflow snippets are
replaced with placeholder text. Unlike the previous two convoys, no
CJS→ESM conversion was needed — `create-test-users.js` was already
top-level ESM.
Verification (all static — script is destructive and not live-tested):
`node --check scripts/create-test-users.js` exit 0; `npm run lint` 128
problems (baseline preserved, no regression); `npm run test:run` 21/21
pass; grep `scripts/ TESTING_GUIDE.md` for
`admin123|password123|test123|alice123|bob123` → 0 hits;
`TEST_USERS_PASSWORD` referenced 10 times total (5 script + 5 doc).
Operator caveat: anyone running `node scripts/create-test-users.js`
post-merge must add `TEST_USERS_PASSWORD=<value>` to their
`.env.local` first; existing alice + bob rows in already-seeded
environments are NOT rotated by re-running this script
(`ON CONFLICT (email) DO NOTHING` preserves the old hashes). Same
caveat that applies to the `drop-public-setup` admin row.
Co-authored-by: Cursor <cursoragent@cursor.com>
9.4 KiB
purge-weak-creds-from-helpers (P2 hygiene — final scope close)
Status: IN-FLIGHT 2026-05-26
Priority: P2 hygiene (not a security blocker; the test-fixture
script is dev-only and the documented passwords were never reachable
from a production code path — but the bug pattern is the same as the
P0-grade weak-creds shape that drop-public-setup removed from
setup-neon-db.js, so closing it brings the helper-script surface to
zero weak literals)
Convoy owner: parent (no architect — proven-pattern mirror;
single-script + single-doc fix following two already-shipped
applications of the same pattern)
Opened: 2026-05-26
Classification: hygiene
Background — the multi-convoy history that led here
The original purge-weak-creds-from-helpers convoy was queued in
.convoys/ship-readiness.md as the umbrella for sweeping every
helper-script + manual-QA-doc reference to the legacy admin123 /
alice123 / bob123 literals and the legacy @tcgvault.com email
domain. Its scope has been progressively whittled down by three
already-shipped convoys:
drop-public-setup(squashff80753Brief 1 +b63b509Brief 2): replaced the hardcodedadmin123inscripts/setup-neon-db.jswith the fail-loudADMIN_INITIAL_PASSWORDenv-var gate; converted the script from CJS to ESM sonpm run setup-dbactually runs on Node 22.x. Set the precedent for the env-var + fail-loud + no-echo pattern that the next two convoys (and this one) mirror verbatim.pick-a-nameBrief 2 (squash9abbab6, 2026-05-24): swept every@tcgvault.comliteral in scripts + docs to@deckhearth.comtogether with the one-shot migration script. Email half done.fix-reset-db-script(squash3ab9bf8, PR #25, 2026-05-26): second application of the post-drop-public-setuppattern, this time toscripts/reset-db.js. Removed the secondadmin123literal from the codebase, removed the only remainingAdmin Password:echo, converted the third CJS-in-ESM script.
After those three convoys, the remaining weak-credential surface is exactly two files — the alice/bob test-user fixture script and the manual-QA doc that pairs with it. Both are addressed here.
Remaining scope (this convoy)
scripts/create-test-users.js— alice + bob fixtures still hardcodebcrypt.hash('alice123', 12)+bcrypt.hash('bob123', 12)and echo the literal passwords to stdout (console.log('✅ Created Alice (alice@deckhearth.com / alice123)')).TESTING_GUIDE.md— Test Accounts table still documents the literal passwords for admin + alice + bob.
The fix shape — single env var, no echo, ESM-already
The fix is a verbatim mirror of the post-drop-public-setup
scripts/setup-neon-db.js pattern and the post-fix-reset-db-script
scripts/reset-db.js pattern, with one deliberate simplification:
- Single env var:
TEST_USERS_PASSWORD. Both alice and bob get the same hashed value. Per-user env vars (ALICE_PASSWORD,BOB_PASSWORD) would be unnecessary sprawl for what is a test fixture surface — these aren't independent identities, they're a collaborator-flow demo pair. Risk R2 below argues this explicitly. - Fail-loud at the top of
createTestUsers(). Readsprocess.env.TEST_USERS_PASSWORD; if unset or whitespace-only, prints an actionable error (names the var, points at.env.local, suggestsopenssl rand -base64 24, references README's "First-time admin setup" section) andprocess.exit(1)BEFORE opening any DB connection. Same wording template assetup-neon-db.jslines 20-26 andreset-db.jslines 29-35. - No password echo to stdout. The previous file logged the
literal
alice123/bob123strings in both the per-user creation line and the final summary block. All four echo lines are deleted; the new summary line documents where the password comes from ((passwords from TEST_USERS_PASSWORD)) without ever printing the value. - ESM already. Unlike
setup-neon-db.jsandreset-db.jsat the start of their respective convoys,create-test-users.jswas already top-level ESM (it imports{ config } from 'dotenv',{ sql } from '@vercel/postgres',bcrypt from 'bcryptjs'at the top of the file). No CJS→ESM conversion needed. This convoy is the first of the three to skip that half of the pattern. ON CONFLICT (email) DO NOTHINGis preserved (already in the original file at lines 19, 28) — defensive against double-run.
TESTING_GUIDE.md's Test Accounts table is rewritten to (a) remove
the literal passwords from the table, (b) document the env-var source
for each user, and (c) point at README's "First-time admin setup"
section for the openssl rand -base64 24 generation tip. The two
inline Password: alice123 / Password: bob123 snippets later in the
workflow are replaced with Password: <value of TEST_USERS_PASSWORD from .env.local>.
Verification plan (static-grep only — script is destructive)
This convoy does NOT live-test create-test-users.js. The script
opens a DB connection and inserts rows; running it against a Neon
branch in CI or in the boot-the-brief loop would be a side-effect
cost we don't need to incur. The verification surface is entirely
static:
node --check scripts/create-test-users.js→ exit 0npm run lint→ 128 problems (baseline preserved; no regression)npm run test:run→ 21/21 pass- Grep
scripts/ TESTING_GUIDE.mdforadmin123|password123|test123| alice123|bob123→ 0 hits on literal passwords - Grep
scripts/create-test-users.jsforrequire(→ 0 hits (mirror-the-pattern preserves ESM-only; trivially satisfied here because the file was already ESM) - Grep
scripts/ TESTING_GUIDE.mdforTEST_USERS_PASSWORD→ expect 10 hits (5 in script: docstring + const + error message body; 5 in TESTING_GUIDE.md: table + two inline workflow snippets + explanatory paragraph)
Live verification deferred to operator. Optional post-merge
action: set TEST_USERS_PASSWORD in .env.local, run
node scripts/create-test-users.js against a non-prod Neon branch,
verify alice + bob rows insert; then unset the env var and re-run,
verify the script exits 1 with the helpful error message before
opening the DB connection.
Risks
- R1 — A CI step or doc dep on the literal passwords. If
.github/workflows/**or any other doc (docs/**,TESTING_GUIDE.mdsibling files, the agent-context-pipeline docs) referencesalice123/bob123/admin123as part of an automated test flow, removing the literal would break it. Mitigation: the grep hunt coveredscripts/+TESTING_GUIDE.md. The broader hits in.convoys/**andAGENTS.mdare historical convoy narrative and must NOT be edited (rewriting history). The only live reference outside this convoy's scope ispages/login.js— see "Surfaced out-of-scope follow-up" below. - R2 — Env-var sprawl. Using a single
TEST_USERS_PASSWORDfor both alice and bob is intentional. These are test-fixture users for the collaboration demo flow in TESTING_GUIDE.md; they aren't modeled as independent identities anywhere in the auth surface, and giving them per-user passwords would (a) double the env-var contract for zero security benefit (anyone running this script already has full DB access) and (b) drift from the ADMIN_INITIAL_PASSWORD shape that the operator is already trained on. If future test-user additions need distinct passwords for realistic concurrency testing, that's a separate concern and a separate convoy.
Surfaced out-of-scope follow-up
pages/login.js"Quick Login" buttons still hardcode the legacy literals. Lines 172 + 184 invokehandleQuickLogin('alice@deckhearth.com', 'alice123')andhandleQuickLogin('bob@deckhearth.com', 'bob123'). These are client-side dev convenience buttons that ship to production HTML and reveal the legacy passwords directly to anyone viewing the login page source. NOT in scope for this convoy (the convoy spec is "scripts + docs only; do NOT touchpages/**"). Queue a follow-up convoy:purge-quick-login-from-loginpage(P2 hygiene) to either (a) delete the Quick Login section entirely or (b) gate it behindprocess.env.NODE_ENV === 'development'. The latter still requires a credential source that doesn't ship to prod HTML — likely a.env.local-onlyNEXT_PUBLIC_DEV_*convention or a dev-only proxy endpoint. Architect-worth.
Operator action required
- Pre-merge: none. No schema change. No new infra.
- Post-merge: anyone running
node scripts/create-test-users.js(ornpm run create-test-usersif such a script exists) must addTEST_USERS_PASSWORD=<value>to their.env.localfirst. Existing alice + bob rows in already-seeded environments are not rotated by re-running this script —ON CONFLICT (email) DO NOTHINGpreserves the old hashes. Any environment that rancreate-test-users.jsbefore this convoy still has the weakalice123/bob123hashes in its DB; operators must rotate manually via the app (or drop those rows and re-seed). Same caveat that applies to thedrop-public-setupadmin row —setup-neon-db.js's andcreate-test-users.js's idempotency means they do NOT rotate; they only seed.
Owns
Parent (single-script + single-doc proven-pattern fix; no architect or implementer subagent required).
As-shipped
(stub — fill in post-merge)