Five decisions routed back for operator gate-1 ratification — none architect-self-ratifiable, since all five are naming choices rather than architectural ones. Gate-0 brand winner (Deck Hearth) is captured; architect's job was to scope and minimize the cost of the rename, not to re-litigate the brand. No blocking findings surfaced: no npm-package collision (we don't publish), domain ownership is already a known queued follow-up, Redis counter reset is the explicitly-accepted trade. Architecture: 2 file-disjoint briefs that can run in parallel via /multitask once gate-1 lands. ~75-110 lines net diff across 16 source files + 1 new migration script (excluding the opaque package-lock.json regen). Brief 1 is the mechanical display/comment sweep (7 files, ~7 lines) — branding notes, rule descriptions, three User-Agent product tokens. Brief 2 owns the infrastructure + email-rename blast (10 files + 1 new migration script, ~30 edits) — Redis prefix rename in lib/rate-limit.js (5 lines), package.json + lockfile regen, admin/alice/ bob email rename across seed/reset/test-user scripts + login.js fixtures + README + TESTING_GUIDE + the test-file regression-lock, plus the new scripts/migrations/2026-05-24-rename-admin-email.js (idempotent REPLACE() UPDATE with UNIQUE-constraint fail-loud semantics). D1-D5 recommendations all biased toward existing-string consistency: D1 "Deck Hearth" (matches all 7 already-correct user-facing surfaces; choosing "Deckhearth" would re-sweep them — net-negative cost), D2 `deck-hearth` (matches the existing `deck-hearth-logo-container` CSS class), D3 `deckhearth` (single token for ID use), D4 `admin@deckhearth.com` (placeholder .com pending point-domain convoy), D5 full `deckhearth` Redis prefix (the 8-byte/key savings of `dh` are negligible vs. self-documenting debuggability). Boot-the-brief findings preempted: lockfile regen is architect-verified to touch only the 2 `name` field lines (lines 2 + 8 of package-lock.json); the test-file negative regression assertion's email literal recommendation is PRESERVE the historical `admin@tcgvault.com` (the literal is a documented pre-fix-auth-bypass bug shape, not an arbitrary email value); scripts/reset-db.js line 142's CJS-in-ESM bug is OUT OF SCOPE and queued as convert-reset-db-to-esm; the in-DB migration's UNIQUE-constraint fail- loud is the intentional safety behavior. AGENTS.md Gotcha #4 / #12 updates are reserved for the doc-writer pass at convoy close (not preempted by Brief 1). Two NEW out-of-scope follow-ups surfaced beyond the convoy seed's four: convert-reset-db-to-esm (CJS-in-ESM bug in reset-db.js, may fold into purge-weak-creds-from-helpers) and update-seed-visual-baselines-on-linux- ordering (the queued seed-visual-baselines convoy MUST run AFTER pick-a-name so the first Linux baseline captures Deck Hearth strings, not TCG Vault). Co-authored-by: Cursor <cursoragent@cursor.com>
49 KiB
| convoy | brief_number | depends_on | files | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pick-a-name | 2 |
|
Brief 2: Infrastructure + email migration (Redis prefix rename, package.json + lockfile regen, admin/alice/bob email rename in seed scripts + login fixtures + migration script + regression test)
Goal (1 sentence)
Sweep the eleven files that own brand-bound INFRASTRUCTURE identity — five Redis key prefixes in lib/rate-limit.js (operator accepts the one-time counter reset), the package.json name field + package-lock.json regen, the admin@tcgvault.com / alice@tcgvault.com / bob@tcgvault.com literals across the three seed/reset/test-user scripts + the README admin-email mention + the TESTING_GUIDE account table + the pages/login.js quick-login fixtures + the test/lib/permission-middleware.test.js negative regression assertion — AND ship a new dated migration script scripts/migrations/2026-05-24-rename-admin-email.js that UPDATEs every existing @tcgvault.com user row to @deckhearth.com (idempotent), in a single atomic PR per the operator-ratified D3 + D4 + D5 decisions.
Files in scope (do not edit anything else)
The 10 modified files + 1 new migration script listed in files: above.
Files explicitly out of scope (do not touch even if it seems related):
- All 7 files owned by Brief 1 —
AGENTS.md,.cursor/rules/ui-and-theming.mdc,.cursor/rules/auth-and-permissions.mdc,scripts/README.md,pages/api/cards/import-lorcana.js,pages/api/cards/import-pokemon.js,scripts/import-lorcana.js. Disjoint file sets enable B1 + B2 to run in parallel via/multitask. components/Layout.js,components/AnimatedFireLogo.js,components/ShareModal.js,pages/index.js,pages/login.jswelcome copy (lines 76 + 158),pages/signup.js,styles/globals.css— already renderDeck Hearthcorrectly. Forpages/login.js, ONLY lines 172 + 184 (the quick-login fixture emails) are in scope; lines 76, 158, and every other line stay byte-identical.scripts/seed-collections-with-cards.js,scripts/seed-collections-alice-bob.js,scripts/add-*.js,scripts/fix-*.js— historical seed / migration scripts per.cursor/rules/no-go-zones.mdc§ Append-only / historical. Do NOT touch. The migration script in B2 handles the in-DB email rename for existing rows; if any of these scripts are re-run post-migration, theirWHERE email IN ('alice@tcgvault.com', ...)clauses match zero rows (harmless no-op).components/Layout.js.backup— legacy snapshot per no-go-zones..convoys/**— append-only history per no-go-zones. Doc-writer pass at convoy close ADDS an "As-shipped" section to.convoys/pick-a-name.mdonly.AGENTS.mdGotcha #4 (line 53) + Gotcha #12 (lines 61-77 — rate-limit gotcha): the doc-writer pass at convoy close updates Gotcha #4 to add the post-migration admin-email caveat and updates Gotcha #12 to reflect the Redis prefix rename. Do NOT preempt those edits in B2.AGENTS.mdline 1 + line 5 + line 13 — B1 + doc-writer scope (line 5 = B1; lines 1 + 13 = doc-writer).scripts/reset-db.jsline 143'sbcrypt.hash('admin123', 12)literal — out of scope per the queuedconvert-reset-db-to-esm+purge-weak-creds-from-helpersfollow-ups. Only edit lines 147 (INSERT email) and 155 (console-log). Do NOT touch line 142 (const bcrypt = require('bcryptjs')CJS-in-ESM bug) or line 143 (weak password literal) or line 156 (Admin Password: admin123console log) — those are separate convoys. (Note: line 156 IS a related concern but its fix collides with the password-rotation convoy, not the email rename; leaving it preserves the existing scope boundary.).env.local— secrets file per no-go-zones; the operator independently updates their local env after the migration runs.tests/smoke/**,tests/visual/**— smoke spec is brand-agnostic (architect-verified); visual baseline doesn't exist yet. Do NOT touch.- Any new vitest spec — deferred to queued
fill-vitest-handler-coverageper theadd-rate-limitingDecision 6 +cors-tightenDecision D4 precedent.
Conventions to follow
Decisions from the convoy file (cite when implementing)
- D1 (operator-ratified at gate-1): Display string is "Deck Hearth" — used in README + TESTING_GUIDE titles + the migration script's stdout messages.
- D2 (operator-ratified at gate-1): Kebab is
deck-hearth— used forpackage.jsonname. - D3 (operator-ratified at gate-1): Camel/lower for IDs is
deckhearth— used in the Redis prefix. - D4 (operator-ratified at gate-1): Admin email is
admin@deckhearth.com(placeholder TLD pending the queuedpoint-domain-at-deckhearthconvoy). Test users follow the same TLD:alice@deckhearth.com,bob@deckhearth.com. - D5 (operator-ratified at gate-1): Redis prefix is the full
deckhearth(NOT abbreviateddh). Pattern:deckhearth:auth,deckhearth:search,deckhearth:upload,deckhearth:generate,deckhearth:import.
Repo conventions (cite + match)
.cursor/rules/no-go-zones.mdc§ "Editing rules of thumb / Schema changes" — INSERT-semantics changes (changing what gets inserted intousers.email) require a NEW dated migration script underscripts/migrations/YYYY-MM-DD-<slug>.js. That folder doesn't exist yet; B2 creates it with the first migration file. The seed scripts' in-placeINSERTliteral updates ARE allowed per the operational-precedent line in the same rule (thesetup-neon-db.jsprecedent fromdrop-public-setupBrief 1 — the rule says "if the change touches DDL strings orINSERTsemantics, file a migration" which we do AND "if it only touches Node-module behavior or pre-flight validation, edit in place" — INSERT-literal updates fall in the migration-required category, and the migration is shipped to satisfy this).@upstash/ratelimitper-class prefix isolation (architect-cited fromadd-rate-limitingBrief 1 Finding 11) — each limiter class needs a distinct Redis prefix. After the rename, the five distinct prefixes aredeckhearth:auth,deckhearth:search,deckhearth:upload,deckhearth:generate,deckhearth:import. The five-prefixes-must-be-distinct invariant is preserved.- JWT auth contract from
fix-auth-bypassBrief 4 +add-rate-limitingBrief 1 —checkAuthRateLimit(req)returns{ allowed, remaining, reset }and that contract is byte-identical post-B2 (only the Redis key prefix changes; the public API surface oflib/rate-limit.jsis unchanged). @vercel/postgrestagged-templates only — the migration script uses@neondatabase/serverless(matchingscripts/setup-neon-db.js's shape, since this is a one-off script, not an API route). Tagged-template SQL is the rule for API handlers; setup / migration scripts use the existingneon(POSTGRES_URL)adapter for parity with the codebase's other scripts.- README
First-time admin setup§ "Operators of envs that pre-date this change" callout (lines 135-141) — the same pattern is reused for the post-pick-a-nameoperator caveat: "if your environment had@tcgvault.comrows, runnode scripts/migrations/2026-05-24-rename-admin-email.jsonce after deploy." B2 adds this caveat alongside the email literal update on line 132.
Acceptance criteria
lib/rate-limit.js (modified — Redis prefix rename ONLY)
Replace the five prefix: values in LIMITER_CONFIG (lines 10-14) from tcgvault:* to deckhearth:*. EVERY OTHER LINE in the 131-line module stays byte-identical, including the comment block above LIMITER_CONFIG (which uses tcgvault as an example of WHY distinct prefixes matter — that comment is conceptually about prefix isolation, NOT brand identity; rewrite the comment example along with the values).
Current lines 9-15:
const LIMITER_CONFIG = {
auth: { limit: 5, window: '15 m', prefix: 'tcgvault:auth' },
search: { limit: 60, window: '1 m', prefix: 'tcgvault:search' },
upload: { limit: 10, window: '1 h', prefix: 'tcgvault:upload' },
generate: { limit: 5, window: '1 h', prefix: 'tcgvault:generate' },
import: { limit: 5, window: '1 h', prefix: 'tcgvault:import' },
};
Replace with:
const LIMITER_CONFIG = {
auth: { limit: 5, window: '15 m', prefix: 'deckhearth:auth' },
search: { limit: 60, window: '1 m', prefix: 'deckhearth:search' },
upload: { limit: 10, window: '1 h', prefix: 'deckhearth:upload' },
generate: { limit: 5, window: '1 h', prefix: 'deckhearth:generate' },
import: { limit: 5, window: '1 h', prefix: 'deckhearth:import' },
};
Acceptance:
-
Lines 10-14 each replace
tcgvault:withdeckhearth:— five edits, one per line, preserving the alignment / spacing inside the{ limit: N, window: '…', prefix: '…' }shape. -
checkAuthRateLimit(req)return shape is byte-identical —{ allowed: boolean, remaining: number, reset: number }. The five exported functions (checkAuthRateLimit,checkSearchRateLimit,checkUploadRateLimit,checkGenerateRateLimit,checkImportRateLimit) keep their byte-identical signatures and return shapes from theadd-rate-limitingBrief 1 contract. No call site needs editing. -
Lines 1-9, 15-131 stay byte-identical (imports,
cached = nulldeclaration,init()body,extractIpIdentifier,extractUserIdentifier,check()helper, the fiveexport async functionblocks, comments). -
In particular: the comment block lines 4-8 (which uses
tcgvault:authas the example for why distinct prefixes matter — "a search-class hit would consume the auth-class budget for the same identifier") is architecturally about prefix isolation, not brand identity. The comment stays byte-identical — it cites the pre-rename example, which still correctly explains the invariant. (Updating the comment to referencedeckhearth:authwould be defensible but creates extra diff; architect recommendation: leave comment as-is for minimum-diff.) If the implementer prefers to update the comment for self-consistency, that's allowed — but it's not required. -
Five distinct prefixes post-rename, no duplicates:
rg "deckhearth:" lib/rate-limit.js | sort -u | wc -lExpected: 5.
-
Zero remaining
tcgvault:references in the lib body:rg "tcgvault:" lib/rate-limit.jsExpected: zero matches if the comment is also updated; OR exactly the comment lines if the implementer left the comment as-is. Both outcomes are acceptable.
package.json (modified — name field rename)
Replace line 2 — the "name" field — from tcg-vault to deck-hearth per D2.
Current line 2:
"name": "tcg-vault",
Replace with:
"name": "deck-hearth",
Acceptance:
- Line 2 matches the verbatim replacement above (preserves leading 2-space indent + trailing comma).
- Every other line in
package.jsonstays byte-identical (version, scripts, dependencies, devDependencies,"type": "module", etc.). - Net diff: 1 line + / 1 line -.
package-lock.json (regenerated — opaque, npm-managed)
Run npm install AFTER the package.json name change to regenerate the lockfile. The diff SHOULD only touch the top-level name field (line 2) + the packages."" entry's nested name (line 8) — both should become deck-hearth.
Expected diff (architect-verified pre-edit grep — exactly two matches for tcg-vault in package-lock.json):
- "name": "tcg-vault", // line 2 — top-level lockfileVersion 3 root
+ "name": "deck-hearth",
...
- "name": "tcg-vault", // line 8 — packages[""] entry
+ "name": "deck-hearth",
Acceptance:
-
After
npm install,package-lock.jsonshows ONLY the twonamefield updates — NO opportunistic dependency churn (noversionbumps, no newintegrityhashes for packages that didn't change, no shifted ordering of unrelated entries). -
If
npm installproduces unrelated churn (e.g., npm pulls fresh metadata for one or more deps), STOP and flag for a separatebump-deps-cleanupconvoy. Do NOT commit accidental dependency updates inside thepick-a-namePR. -
Verification command (run BEFORE commit):
git diff package-lock.json | grep -E '^[+-]' | grep -v -E '^[+-]{3}|^[+-]\s*"name":' | head -20Expected: zero output (every diff line is either the file header
+++/---or a"name":change). If output appears, investigate before commit. -
npm ci(NOTnpm install) should succeed cleanly with the regenerated lockfile — verifies the lockfile is internally consistent and matchespackage.json.
README.md (modified — title + admin-email mention + operator caveat addition)
Two edits in two distinct locations + an additive operator caveat alongside the existing one.
Edit 1 — Line 1 (title):
Current:
# TCG Vault
Replace with:
# Deck Hearth
Edit 2 — Line 132 (admin email):
Current:
- **Admin email:** the seed creates `admin@tcgvault.com`. Change the password
Replace with:
- **Admin email:** the seed creates `admin@deckhearth.com`. Change the password
Edit 3 — Append a new operator caveat as a SIBLING blockquote to the existing one (the existing blockquote is lines 135-141, the > **Operators of envs that pre-date this change:** paragraph). Insert the new blockquote IMMEDIATELY AFTER the existing one (a blank line, then the new blockquote, then a blank line, then the existing ## 🤝 Contributing section heading).
Insert (between current lines 141 and 143):
> **Operators of envs that pre-date the `pick-a-name` convoy (2026-05-24):**
> the admin row was renamed from `admin@tcgvault.com` to
> `admin@deckhearth.com`. Run
> `node scripts/migrations/2026-05-24-rename-admin-email.js` once after
> deploy to UPDATE any existing `@tcgvault.com` user rows (the admin row,
> plus alice/bob if `npm run create-test-users` was ever run). Re-running
> the migration after the first run is idempotent and prints "Nothing to
> migrate." Verify post-migration with
> `psql $POSTGRES_URL -c "SELECT email FROM users WHERE email LIKE '%@tcgvault.com'"`
> — expect zero rows.
Acceptance:
- Line 1 matches the verbatim replacement above.
- Line 132 matches the verbatim replacement above (preserves leading
- **Admin email:**markdown structure + trailingChange the password). - Lines 30 (
cd tcg-vault) and 105 (tcg-vault/directory tree) stay byte-identical — repo-name references, deferred torename-repo-and-vercel-projectconvoy. - The new operator-caveat blockquote is inserted after the existing 2026-05-22-era
> **Operators of envs that pre-date this change:**blockquote (which talks about theadmin123weak hash), formatted as another>blockquote with the verbatim 9-line content above. - No other line in
README.mdchanges. - Net diff: 2 lines changed (lines 1, 132) + ~10 lines added (new caveat blockquote with surrounding whitespace).
TESTING_GUIDE.md (modified — title + 6 email references)
Update the title and every @tcgvault.com email reference to @deckhearth.com.
Edit 1 — Line 1 (title):
Current:
# 🎯 TCG Vault Collaboration Testing Guide
Replace with:
# 🎯 Deck Hearth Collaboration Testing Guide
Edit 2 — Lines 7-9 (account table):
Current:
| Admin | `admin@tcgvault.com` | `admin123` | Admin |
| Alice | `alice@tcgvault.com` | `alice123` | User |
| Bob | `bob@tcgvault.com` | `bob123` | User |
Replace with:
| Admin | `admin@deckhearth.com` | `admin123` | Admin |
| Alice | `alice@deckhearth.com` | `alice123` | User |
| Bob | `bob@deckhearth.com` | `bob123` | User |
Edit 3 — Line 24:
Current:
Email: alice@tcgvault.com
Replace with:
Email: alice@deckhearth.com
Edit 4 — Line 46:
Current:
- Enter: `bob@tcgvault.com`
Replace with:
- Enter: `bob@deckhearth.com`
Edit 5 — Line 53:
Current:
- Email: `bob@tcgvault.com`
Replace with:
- Email: `bob@deckhearth.com`
Acceptance:
- Six lines updated (1, 7, 8, 9, 24, 46, 53 — counting line 1 as the title).
- The
admin123/alice123/bob123password literals stay — that's the queuedpurge-weak-creds-from-helpersconvoy's scope, notpick-a-name. The TESTING_GUIDE is a manual testing reference; the dev test users having weak passwords is the documented state pre-purge-weak-creds-from-helpers. - The instructional flow (sections 1-6) text is byte-identical apart from the email-literal edits above. Don't rewrite copy.
- After the edits,
rg "tcgvault" TESTING_GUIDE.mdreturns zero matches andrg "@tcgvault\.com" TESTING_GUIDE.mdreturns zero matches.
scripts/setup-neon-db.js (modified — admin email INSERT + console log)
Two edits, both on the admin email literal.
Edit 1 — Line 148 (admin INSERT):
Current:
VALUES (${'admin@tcgvault.com'}, ${hashedPassword}, ${'admin'})
Replace with:
VALUES (${'admin@deckhearth.com'}, ${hashedPassword}, ${'admin'})
Edit 2 — Line 157 (console log):
Current:
console.log(' Admin user ready (email: admin@tcgvault.com)');
Replace with:
console.log(' Admin user ready (email: admin@deckhearth.com)');
Acceptance:
- Lines 148 + 157 match the verbatim replacements above.
- Every other line stays byte-identical — including the
ADMIN_INITIAL_PASSWORDenv-var gate (lines 18-28, locked in bydrop-public-setupBrief 1), the ESM imports (lines 11, 14, 15, locked in bydrop-public-setupBrief 2), the sevenCREATE TABLE IF NOT EXISTSblocks, theON CONFLICT (email) DO NOTHINGclause, and the troubleshooting copy. - Re-running
npm run setup-dbafter this convoy lands seeds new deploys withadmin@deckhearth.com. For existing deploys withadmin@tcgvault.com, the seed'sON CONFLICT (email) DO NOTHINGdoesn't match (the existing email is different) so a NEW admin row would be created withadmin@deckhearth.com— creating a TWO-ADMIN row collision. The operator MUST run the migration script FIRST, thensetup-db(the migration sweeps the row to@deckhearth.com, and a subsequentsetup-dbsees the row exists with the new email and no-ops). This ordering is documented in the README operator caveat and in the migration script's stdout. - Net diff: 2 lines + / 2 lines -.
scripts/reset-db.js (modified — admin email INSERT + console log ONLY)
Two edits, both on the admin email literal. Do NOT touch other lines — reset-db.js has the CJS require('bcryptjs') bug (line 142) and weak password literal (line 143 + the password-echo on line 156); those are queued for separate convoys (convert-reset-db-to-esm + purge-weak-creds-from-helpers). pick-a-name is scoped to the email rename only.
Edit 1 — Line 147 (admin INSERT):
Current:
VALUES (${'admin@tcgvault.com'}, ${hashedPassword}, ${'admin'})
Replace with:
VALUES (${'admin@deckhearth.com'}, ${hashedPassword}, ${'admin'})
Edit 2 — Line 155 (console log):
Current:
console.log(' Admin User: admin@tcgvault.com');
Replace with:
console.log(' Admin User: admin@deckhearth.com');
Acceptance:
- Lines 147 + 155 match the verbatim replacements above.
- Line 142 (
const bcrypt = require('bcryptjs');) stays byte-identical — out of scope perconvert-reset-db-to-esmfollow-up. (Note: this line is broken on Node 22.x with"type": "module"in package.json. The bug meansnpm run reset-dbdoesn't actually work today. That's out ofpick-a-name's scope —reset-db.jsis a developer utility that nobody runs in CI; fixing the ESM-conversion bug is its own convoy. The email rename here is preparatory: whenconvert-reset-db-to-esmships, the email will already be correct.) - Line 143 (
const hashedPassword = await bcrypt.hash('admin123', 12);) stays byte-identical — weak password literal ispurge-weak-creds-from-helpersscope. - Line 156 (
console.log(' Admin Password: admin123');) stays byte-identical — same. - Net diff: 2 lines + / 2 lines -.
scripts/create-test-users.js (modified — alice/bob/admin emails across 6 lines)
Six edits across the alice INSERT, bob INSERT, and the three-account summary console log block.
Edit 1 — Line 18 (alice INSERT):
VALUES ('alice@tcgvault.com', ${alicePassword}, 'user')
→
VALUES ('alice@deckhearth.com', ${alicePassword}, 'user')
Edit 2 — Line 21 (alice console log):
console.log('✅ Created Alice (alice@tcgvault.com / alice123)');
→
console.log('✅ Created Alice (alice@deckhearth.com / alice123)');
Edit 3 — Line 27 (bob INSERT):
VALUES ('bob@tcgvault.com', ${bobPassword}, 'user')
→
VALUES ('bob@deckhearth.com', ${bobPassword}, 'user')
Edit 4 — Line 30 (bob console log):
console.log('✅ Created Bob (bob@tcgvault.com / bob123)');
→
console.log('✅ Created Bob (bob@deckhearth.com / bob123)');
Edit 5 — Line 34 (summary admin line):
console.log(' 1. admin@tcgvault.com / admin123 (Admin)');
→
console.log(' 1. admin@deckhearth.com / admin123 (Admin)');
Edit 6 — Line 35 (summary alice line):
console.log(' 2. alice@tcgvault.com / alice123 (User)');
→
console.log(' 2. alice@deckhearth.com / alice123 (User)');
Edit 7 — Line 36 (summary bob line):
console.log(' 3. bob@tcgvault.com / bob123 (User)');
→
console.log(' 3. bob@deckhearth.com / bob123 (User)');
Acceptance:
- Lines 18, 21, 27, 30, 34, 35, 36 each match the verbatim replacements above (preserves leading indent, surrounding emoji/punctuation, password / role literals).
- Password literals (
alice123,bob123,admin123) stay byte-identical —purge-weak-creds-from-helpersscope. ON CONFLICT (email) DO NOTHINGclauses stay byte-identical — preserves the script's idempotency.- The ESM imports (lines 3-5,
dotenv/@vercel/postgres/bcryptjs) stay byte-identical —create-test-users.jsis already ESM-correct. - After this edit, re-running
node scripts/create-test-users.jsagainst a fresh DB creates alice/bob with the new emails. Against an existing DB with the OLD emails, theON CONFLICTclause keys off the email (UNIQUE on email column), so a re-run would INSERT NEW rows under the new emails alongside the old ones (two alices, two bobs). Operator should run the migration script FIRST to rename existing rows. - Net diff: 7 lines + / 7 lines -.
pages/login.js (modified — quick-login fixture emails ONLY, lines 172 + 184)
The "Quick Login for Testing" section has two button onClick handlers that pre-fill the email field with alice / bob fixture emails. These MUST match the post-migration emails in the DB for the quick-login to work.
Edit 1 — Line 172:
Current:
onClick={() => handleQuickLogin('alice@tcgvault.com', 'alice123')}
Replace with:
onClick={() => handleQuickLogin('alice@deckhearth.com', 'alice123')}
Edit 2 — Line 184:
Current:
onClick={() => handleQuickLogin('bob@tcgvault.com', 'bob123')}
Replace with:
onClick={() => handleQuickLogin('bob@deckhearth.com', 'bob123')}
Acceptance:
- Lines 172 + 184 match the verbatim replacements above (preserves the surrounding 22-space indent + the closing parenthesis structure).
- EVERY OTHER LINE in
pages/login.jsstays byte-identical — specifically:- Lines 76, 158 (
Welcome to Deck Hearth,Sign in to Deck Hearth) — already correct, DO NOT touch. - Lines 1-15 (imports, component declaration, state) — DO NOT touch.
- Lines 200-205 (signup link copy) — DO NOT touch.
- The 30-line
<AuthLayout>shell, the form fields, thehandleSubmitbody — DO NOT touch.
- Lines 76, 158 (
- After this edit, clicking the "Alice" / "Bob" quick-login buttons pre-fills the email field with
<name>@deckhearth.com. The button's effect is purely UI state (setsformData); the actual login fires when the user clicks "Sign in to Deck Hearth", which POSTs to/api/auth/login. If the DB has been migrated, login succeeds; if not, login 401s. - Warning to implementer: if the DB migration has NOT been run when these fixtures are tested locally, login will 401. That's the expected pre-migration failure mode, NOT a bug in the edit. Document the testing order in the PR description.
- Net diff: 2 lines + / 2 lines -.
test/lib/permission-middleware.test.js (modified — line 87 negative regression assertion; architect-recommended PRESERVE the historical literal)
This is the negative regression assertion locking in that getUserFromRequest does NOT return the OLD synthetic-admin shape — { userId: 1, email: 'admin@tcgvault.com', role: 'admin' }. Architect-recommended approach: PRESERVE the historical literal admin@tcgvault.com because:
- The
.toBeNull()assertion on the next line (line 90) is the strong contract — it asserts the helper returns null, which is what matters. - The
.not.toEqual({...})assertion is a soft historical-shape check — it specifically locks in that the helper does NOT return the EXACT pre-fix-auth-bypasssynthetic shape. Updating the email literal weakens this — it would only catch a future synthetic-admin path that used the NEW email, not one that resurrected the OLD shape. - Git history shows the bug was committed with
admin@tcgvault.com; the assertion is a historical artifact that documents the bug shape.
Edit — Line 83-91 region (preserve email literal; add explanatory comment ABOVE the .not.toEqual({...}) call):
Current:
it('does NOT return the synthetic admin shape when no Authorization header is present (Brief 2 regression lock)', async () => {
const user = await getUserFromRequest({ headers: {} });
expect(user).not.toEqual({
userId: 1,
email: 'admin@tcgvault.com',
role: 'admin',
});
expect(user).toBeNull();
});
Replace with:
it('does NOT return the synthetic admin shape when no Authorization header is present (Brief 2 regression lock)', async () => {
const user = await getUserFromRequest({ headers: {} });
// Email literal is the OLD `admin@tcgvault.com` (pre-`pick-a-name`
// convoy, 2026-05-24) — preserved as the exact pre-fix-auth-bypass
// synthetic-admin shape this assertion locks against. The
// `.toBeNull()` check below is the strong contract; this soft check
// documents the historical bug. Do NOT update to
// `admin@deckhearth.com` — that would weaken the regression-lock to
// a shape that never actually existed.
expect(user).not.toEqual({
userId: 1,
email: 'admin@tcgvault.com',
role: 'admin',
});
expect(user).toBeNull();
});
Acceptance:
- The 7-line comment block is inserted ABOVE the
expect(user).not.toEqual({...})call (between theconst user = await getUserFromRequest({ headers: {} });line and theexpect(user).not.toEqual({...})line). Preserves the existing 4-space indent. - The
email: 'admin@tcgvault.com'literal stays UNCHANGED (line 87 in the pre-edit file). - The
expect(user).toBeNull();assertion (line 90 pre-edit) stays byte-identical. - Every other line in the file stays byte-identical.
- After the edit,
npm run test:runstill passes 21/21. - Implementer alternative (if preferred): if you'd rather update the email literal to
admin@deckhearth.comfor self-consistency, do so AND change the comment to explain "this assertion's literal email tracks the current admin email; the historical pre-fix-auth-bypassbug returnedadmin@tcgvault.com, see commit258e479for the deleted shape." Either path is acceptable; the architect recommendation is PRESERVE for stronger historical regression coverage.
scripts/migrations/2026-05-24-rename-admin-email.js (NEW — the email migration script)
Create the new scripts/migrations/ folder + the first dated migration script. Idempotent. Operator-invoked once after deploy.
Verbatim new file:
#!/usr/bin/env node
/**
* Migration: 2026-05-24 — Rename @tcgvault.com user emails to @deckhearth.com
*
* Part of the `pick-a-name` convoy. Renames every `users.email` row matching
* `%@tcgvault.com` to the `@deckhearth.com` equivalent (admin + alice + bob,
* plus any other accidentally-`@tcgvault.com` users if they exist).
*
* Idempotent: re-running after the first run prints "Nothing to migrate."
*
* Usage:
* node scripts/migrations/2026-05-24-rename-admin-email.js
*
* Required env: POSTGRES_URL (read from .env.local).
*
* Safety: the UPDATE uses REPLACE() so emails like `admin@tcgvault.com`
* become `admin@deckhearth.com`. The `users.email` UNIQUE constraint will
* fail loudly if a row with the target email already exists — which is the
* correct behavior (do NOT silently overwrite). If you see the constraint
* violation, inspect the DB manually before retrying.
*/
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
import { neon } from '@neondatabase/serverless';
async function main() {
if (!process.env.POSTGRES_URL) {
console.error('❌ POSTGRES_URL is not set. Set it in .env.local before running this migration.');
process.exit(1);
}
const sql = neon(process.env.POSTGRES_URL);
const { rows: before } = await sql`
SELECT id, email, role
FROM users
WHERE email LIKE '%@tcgvault.com'
ORDER BY id
`;
if (before.length === 0) {
console.log('✅ Nothing to migrate. No users with @tcgvault.com emails found.');
return;
}
console.log(`Found ${before.length} user(s) with @tcgvault.com emails:`);
for (const r of before) {
console.log(` id=${r.id} role=${r.role} email=${r.email}`);
}
await sql`
UPDATE users
SET email = REPLACE(email, '@tcgvault.com', '@deckhearth.com'),
updated_at = CURRENT_TIMESTAMP
WHERE email LIKE '%@tcgvault.com'
`;
const { rows: after } = await sql`
SELECT id, email, role
FROM users
WHERE email LIKE '%@deckhearth.com'
ORDER BY id
`;
console.log(`✅ Migrated ${before.length} user(s). Post-migration @deckhearth.com rows:`);
for (const r of after) {
console.log(` id=${r.id} role=${r.role} email=${r.email}`);
}
const { rows: stragglers } = await sql`
SELECT COUNT(*)::int AS count FROM users WHERE email LIKE '%@tcgvault.com'
`;
if (stragglers[0].count !== 0) {
console.warn(`⚠️ ${stragglers[0].count} @tcgvault.com row(s) still present after migration — investigate.`);
process.exit(1);
}
}
main().catch((err) => {
console.error('❌ Migration failed:', err);
process.exit(1);
});
Acceptance:
- File ends up at
scripts/migrations/2026-05-24-rename-admin-email.js(folder created automatically by the file write). - File content is the verbatim shape above. 2-space indent. ESM (matches
scripts/setup-neon-db.js's post-drop-public-setup-Brief-2 shape). - Idempotency: running the script twice should: (a) first run prints the before-count + UPDATEs + post-count; (b) second run prints
Nothing to migrate.and exits 0. Verified by re-running locally against a staging DB. - Safety: UNIQUE-constraint violation handling. If the target email already exists (e.g. operator ran the migration, then created a duplicate admin via
setup-neon-db.js, then tried to re-migrate a regressed row), the UPDATE fails with the Postgres UNIQUE constraint error and the script exits non-zero — that's the correct fail-loud behavior. Do NOT add aON CONFLICTclause to the UPDATE (which doesn't exist in standard SQL for UPDATE statements anyway — it's an INSERT-only clause). - Straggler check: after the UPDATE, the script SELECTs to confirm zero remaining
@tcgvault.comrows. If any remain (unexpected), it warns + exits 1. - No new dependencies. Uses
dotenv+@neondatabase/serverless— both already inpackage.jsondependencies. POSTGRES_URLenv guard at startup — same defensive pattern assetup-neon-db.jsforADMIN_INITIAL_PASSWORD(drop-public-setup Brief 1 precedent).- The script does NOT touch any other column. It only updates
users.email(andusers.updated_atto reflect the change). Nopassword,role, or any other field changes. - NOT added to
package.jsonscripts. It's a one-off migration; the operator invokes it directly vianode scripts/migrations/2026-05-24-rename-admin-email.js. No"migrate-admin-email"npm script is added. (Adding a migration npm script per migration is wrong — when a real migration tool lands per AGENTS.md Gotcha #6, that tool's CLI handles invocation.)
Cross-file checks
-
npm run lintexit code unchanged. Perlib/rate-limit.jsBrief 1's precedent: lib refactors that change a config-literal don't add ESLint surface. New migration script: pure ESM Node, should pass without warnings (matchesscripts/setup-neon-db.js's shape). -
npm run test:run(vitest) passes 21/21. Only test edit is the comment-addition intest/lib/permission-middleware.test.js; assertion semantics are byte-identical, so all 21 tests still pass. -
npm run buildexit 0. Turbopack should compile cleanly. Thelib/rate-limit.jsedits are string literals;pages/login.jsedits are string literals. No new imports. -
npm run test:smokeagainst the Vercel preview passes 3/3 (smoke spec is brand-agnostic; verified at architect time). -
Per-class Redis prefix uniqueness check (the post-rename equivalent of
add-rate-limitingBrief 1's check):rg "deckhearth:" lib/rate-limit.js | sort -uExpected: 5 distinct lines (
deckhearth:auth,deckhearth:search,deckhearth:upload,deckhearth:generate,deckhearth:import).And:
rg "tcgvault:" lib/rate-limit.jsExpected: zero matches in active code (the architect-recommended preserve-comment path may leave the comment block on lines 4-8 referencing
tcgvault:authas the example; if so, that's the only acceptable match). -
Repo-wide grep (post-B2, with B1 also landed):
rg "@tcgvault\.com" --type-not mdExpected: at most 1 match —
test/lib/permission-middleware.test.jsline 87's preserved historical regression literal (if the implementer took the recommended preserve path). Zero matches if the implementer chose the update-to-new-email path.rg "@tcgvault\.com" .convoys/Expected: same count as pre-B2 (historical convoy artifacts).
rg "@tcgvault\.com" --type-not md --type-not jsExpected: zero matches (no
@tcgvault.comleft outside markdown + js source). -
In-DB migration verification (against staging Neon, post-deploy):
node scripts/migrations/2026-05-24-rename-admin-email.jsExpected first-run output (against an env that had the old emails):
Found 3 user(s) with @tcgvault.com emails: id=1 role=admin email=admin@tcgvault.com id=2 role=user email=alice@tcgvault.com id=3 role=user email=bob@tcgvault.com ✅ Migrated 3 user(s). Post-migration @deckhearth.com rows: id=1 role=admin email=admin@deckhearth.com id=2 role=user email=alice@deckhearth.com id=3 role=user email=bob@deckhearth.comExpected second-run output (idempotency check):
✅ Nothing to migrate. No users with @tcgvault.com emails found. -
Diff hygiene.
git diff main..HEAD --stat(after B2 commit only) should show:lib/rate-limit.js: 5 lines + / 5 lines -.package.json: 1 line + / 1 line -.package-lock.json: 2 lines + / 2 lines - (assuming clean regen — see acceptance criteria above).README.md: ~12 lines + / 2 lines - (2 edits + new caveat blockquote).TESTING_GUIDE.md: 6 lines + / 6 lines -.scripts/setup-neon-db.js: 2 lines + / 2 lines -.scripts/reset-db.js: 2 lines + / 2 lines -.scripts/create-test-users.js: 7 lines + / 7 lines -.pages/login.js: 2 lines + / 2 lines -.test/lib/permission-middleware.test.js: 7 lines + / 0 lines - (comment block addition).scripts/migrations/2026-05-24-rename-admin-email.js: NEW, ~85 lines.- No whitespace-only changes elsewhere.
Manual verification (in addition to CI on push)
Run these in order against a staging environment with Upstash + Neon configured. DO NOT run against production.
-
Lockfile regen sanity. Before commit:
npm install # regenerate package-lock.json git diff package-lock.json | head -40Expected: a 4-line diff (two
-namelines, two+namelines). If ANYTHING else changes, STOP and flag. -
Local dev boot.
npm run dev. Expected: clean boot, no[rate-limit]warn-spam at startup (lazy-init; warn only on first request if Upstash isn't configured). The Redis prefix change is transparent at boot time. -
Auth limiter regression check (Brief 4 + add-rate-limiting contract preserved):
for i in 1 2 3 4 5 6; do curl -sS -o /dev/null -w "POST /api/auth/login attempt $i: %{http_code}\n" \ -X POST -H "Content-Type: application/json" \ -d '{"email":"nobody@example.com","password":"wrong"}' \ http://localhost:3000/api/auth/login doneExpected (only meaningful with
KV_REST_API_*set against the staging Upstash):- Attempts 1-5:
401 - Attempt 6:
429withRetry-Afterheader
The behavior is byte-identical to pre-B2 — only the Redis key namespace changes from
tcgvault:auth:*todeckhearth:auth:*. The counter fornobody@example.com-from-this-IP starts fresh at zero post-deploy because the oldtcgvault:auth:*keys are orphaned. This is the acknowledged Redis counter reset. - Attempts 1-5:
-
Migration script smoke (against staging only):
node scripts/migrations/2026-05-24-rename-admin-email.jsRun twice. First run reports the count + UPDATEs; second run reports "Nothing to migrate."
-
Post-migration login smoke:
curl -sX POST http://localhost:3000/api/auth/login \ -H 'content-type: application/json' \ -d '{"email":"admin@deckhearth.com","password":"<your-admin-password>"}'Expected:
200+ JWT in response body. -
Quick-login button smoke: in the browser, visit
/login, click "Alice", confirm the email field showsalice@deckhearth.com, click "Sign in to Deck Hearth", confirm redirect to/dashboard. Repeat for "Bob". Both require the migration to have run; if migration skipped, both 401 (expected pre-migration failure mode). -
Visit
/admin/card-importas admin to confirm the post-add-rate-limitingadmin-UI Bearer-token wiring still works against the renamed admin row. Type a small set code (e.g.neo) and click Import. Expected: 200 from the API (or whatever Scryfall returns). -
Vitest pass count unchanged.
npm run test:run 2>&1 | tail -5→Tests 21 passed (21).
Boot-the-brief findings (preempted by the architect; do not re-investigate)
Finding 1 — Lockfile regen is mostly opaque but architect-verified to be minimal
Architect ran rg "tcg-vault" package-lock.json at brief time → exactly 2 matches (lines 2 + 8 — top-level name field and the packages[""] entry's nested name). Standard npm behavior: npm install after a package.json name change updates ONLY those two fields. However, npm sometimes opportunistically refreshes stale dependency metadata (integrity hashes, peer-dep resolutions). The brief includes a verification command to confirm zero unrelated churn before commit.
If unrelated churn appears, the implementer must STOP — the convoy is a brand rename, not a dependency update. Quarantine the unrelated diff for a separate bump-deps-cleanup convoy.
Finding 2 — Redis counter reset is per-window, not permanent
Architect cross-checked against @upstash/ratelimit@2.0.8's slidingWindow semantics + the five window durations in LIMITER_CONFIG. Maximum window: 1 hour (upload / generate / import). Minimum: 1 minute (search). After the prefix rename ships, all five windows reset to zero — within an hour, the system is back to nominal counter state for all five classes. No persistent damage. The legitimate user who was at 4/5 auth-attempts pre-deploy now has 5/5 fresh — same as the attacker. Net defensively neutral.
The convoy seed explicitly accepts this. No additional mitigation needed.
Finding 3 — scripts/reset-db.js has a CJS-in-ESM bug that is OUT OF SCOPE here
Line 142 (const bcrypt = require('bcryptjs');) and line 5 (#!/usr/bin/env node shebang without any import statement) indicate reset-db.js was never converted to ESM after package.json got "type": "module" (the bump-next-js convoy's flag). The script is broken on Node 22.x. Out of scope for pick-a-name — track in queued convert-reset-db-to-esm (which can also fold the weak-password literal cleanup from purge-weak-creds-from-helpers). B2 only changes the email-literal strings on lines 147 + 155; do NOT attempt to fix the CJS bug.
Finding 4 — The migration script's UPDATE will fail loudly on UNIQUE-constraint violation, which is correct
If the operator runs setup-neon-db.js BEFORE the migration (after B2 deploys), there'd be TWO admin rows: one with admin@tcgvault.com (pre-existing) + one with admin@deckhearth.com (newly seeded). The migration's UPDATE … SET email = REPLACE(...) would try to update the old row to admin@deckhearth.com, hitting the UNIQUE constraint on users.email. Postgres throws duplicate key value violates unique constraint "users_email_key". The script exits non-zero. The operator sees the error, manually deletes one of the two duplicate admin rows, then re-runs the migration. This is the correct fail-loud behavior. Do NOT add silent conflict handling.
The README operator caveat documents the recommended order: migration FIRST, then setup-db (which then no-ops because the row exists with the new email).
Finding 5 — pages/login.js quick-login buttons are pre-fill, NOT direct-login
Architect read lines 168-194 of pages/login.js. The handleQuickLogin(email, password) function sets formData state — it pre-fills the email + password fields. The user still has to click "Sign in to Deck Hearth" to actually submit. So the quick-login emails are tested AGAINST the database when the user clicks Sign In, not when they click the Quick-Login button. Pre-migration, clicking Alice + then Sign-In will 401. Post-migration, it succeeds.
This is the correct UX — the implementer doesn't need to add any pre-submit validation.
Finding 6 — The test/lib/permission-middleware.test.js regression-lock email is architecturally a HISTORICAL marker
Architect read lines 83-91 of the test file. The assertion expect(user).not.toEqual({ userId: 1, email: 'admin@tcgvault.com', role: 'admin' }) is checking against the EXACT pre-fix-auth-bypass synthetic-admin return shape. The .toBeNull() immediately after is the strong contract — that's the assertion that fails the test if the bug regresses. The .not.toEqual() is a soft historical-shape check that documents the bug's exact return value at the time it was a bug.
Architect recommendation: PRESERVE the historical literal admin@tcgvault.com + add a comment explaining why. The alternative (updating to admin@deckhearth.com) is defensible but creates a weaker assertion (only catches a future synthetic-admin path that uses the NEW email). The brief documents both options; implementer chooses.
Finding 7 — .cursor/rules/no-go-zones.mdc allows the migration script + permits the in-place email edits
Re-read the rule's "Editing rules of thumb" subsection:
Schema changes: until a proper migration tool lands, document the change in a new dated script under
scripts/migrations/YYYY-MM-DD-<slug>.js(folder TBD). Do NOT editscripts/setup-neon-db.jsin place for any DDL change ... Operational changes are allowed (env-var gating, error-message hardening, module-system fixes) —drop-public-setupset this precedent ... The distinction: if the change touches DDL strings orINSERTsemantics, file a migration; if it only touches Node-module behavior or pre-flight validation, edit in place and document why in the convoy.
The admin-email rename:
- IS an INSERT-semantics change (different literal value going into
INSERT INTO users (email, …)). - Therefore requires a NEW dated migration script. B2 ships one.
- AND requires the in-place edits to
setup-neon-db.js+reset-db.js+create-test-users.jsto keep new-deploy seeding correct. The rule explicitly allows operational changes in-place when accompanied by a migration; B2 satisfies both halves.
The folder scripts/migrations/ is created automatically with the first file write.
Finding 8 — No existing convoy has touched scripts/reset-db.js (architect cross-check)
Architect ran rg "scripts/reset-db.js" .convoys/ at brief time. The only matches are passing references (e.g. ship-readiness.md line 360's purge-weak-creds-from-helpers queued entry mentioning the file). No prior convoy has edited reset-db.js. B2's edit (email literals only) is the file's first convoy-driven change. Coordination risk: zero.
Finding 9 — The pages/login.js quick-login section is bounded; no other email references
Architect ran rg "@tcgvault" pages/login.js → exactly 2 matches (lines 172, 184). Outside the quick-login <button> onClick handlers, there are no other email literals in pages/login.js. The handleSubmit body: JSON.stringify(formData) sends whatever the user typed — no other email-bound shapes to update.
Finding 10 — Brief 2 has the larger blast but lower decision-density than B1
B1 has 7 trivial edits + 6 boot-the-brief findings. B2 has ~30 edits + 1 new file + 10+ boot-the-brief findings. The complexity is in the cross-file ordering and the migration script's idempotency contract — NOT in any single edit, all of which are mechanical literal replacements. The implementer should land B2 against a fresh checkout, sequence the edits in the order listed in Acceptance criteria, and run the boot-the-brief verification commands before commit.
Out of scope (do not do these)
- Do NOT edit any file in B1's
files:list. - Do NOT touch
.convoys/**historical artifacts. - Do NOT touch
components/Layout.js.backup,scripts/seed-collections-*.js,scripts/add-*.js, orscripts/fix-*.js(historical per no-go-zones). - Do NOT touch repo-name references (README cd / tree, AGENTS.md line 1, workflow YAML comments,
.agent-context-manifest.ymlsource tags) — that'srename-repo-and-vercel-project. - Do NOT touch AGENTS.md (any line) — B1 owns line 5; doc-writer owns lines 1, 13, 53, and Gotcha #12.
- Do NOT preempt AGENTS.md Gotcha #4 / #12 updates — doc-writer pass at convoy close.
- Do NOT fix
scripts/reset-db.js's CJS-in-ESM bug (line 142) or weak-password literals (lines 143, 156) —convert-reset-db-to-esm+purge-weak-creds-from-helpersscope. - Do NOT touch
TESTING_GUIDE.md's weak password literals (admin123,alice123,bob123) —purge-weak-creds-from-helpersscope. - Do NOT add
KV_REST_API_*env vars totest/setup.js. The warn-and-noop branch is the correct test behavior. The Redis prefix rename doesn't change this. - Do NOT bump
@upstash/ratelimitor@upstash/redisversions. Pins stay at^2.0.8and^1.38.0. - Do NOT add Lorcana to the
<select>inpages/admin/card-import.js(that's theadd-rate-limitingBrief 1's explicit out-of-scope; this convoy doesn't touch admin/card-import.js at all). - Do NOT add a "migrate-admin-email" npm script to
package.json. The migration is invoked directly vianode scripts/migrations/.... When a real migration tool lands (per AGENTS.md Gotcha #6), its CLI handles invocation. - Do NOT add visual baselines or update the smoke spec — the smoke spec is brand-agnostic; baselines are seeded by the queued
seed-visual-baselines-on-linuxconvoy AFTERpick-a-namelands. - Do NOT preempt the doc-writer convoy-close work. The "As-shipped" section of
.convoys/pick-a-name.md, the AGENTS.md gotcha edits, and the ship-readiness.md milestone entry are all post-merge doc-writer scope.
Rationale (≤3 sentences)
B2 is the higher-blast slice of the convoy: 11 files + 1 new migration script, ~30 net edits, with one externally-visible behavior change (Redis counter reset) and one operator-action-required step (run the migration script post-deploy). Splitting the email migration from B1's mechanical sweep keeps both briefs file-disjoint so the conductor can dispatch them in parallel via /multitask, AND co-locates every email + infrastructure ID change in one PR so the migration script + the seed-script email updates + the login.js quick-login fixtures ship atomically (preventing a deploy window where any of them is wrong). The migration script's idempotent UNIQUE-constraint-aware design + the README operator caveat together make the post-deploy operator action survivable even if executed in the wrong order.