--- convoy: pick-a-name brief_number: 2 depends_on: [] files: - lib/rate-limit.js - package.json - package-lock.json - README.md - TESTING_GUIDE.md - scripts/setup-neon-db.js - scripts/reset-db.js - scripts/create-test-users.js - pages/login.js - test/lib/permission-middleware.test.js - scripts/migrations/2026-05-24-rename-admin-email.js --- # 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.js` welcome copy (lines 76 + 158), `pages/signup.js`, `styles/globals.css` — already render `Deck Hearth` correctly. **For `pages/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, their `WHERE 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.md` only. - `AGENTS.md` Gotcha #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.md` line 1 + line 5 + line 13 — B1 + doc-writer scope (line 5 = B1; lines 1 + 13 = doc-writer). - `scripts/reset-db.js` line 143's `bcrypt.hash('admin123', 12)` literal — out of scope per the queued `convert-reset-db-to-esm` + `purge-weak-creds-from-helpers` follow-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: admin123` console 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-coverage` per the `add-rate-limiting` Decision 6 + `cors-tighten` Decision 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 for `package.json` `name`. - **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 queued `point-domain-at-deckhearth` convoy). 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 abbreviated `dh`). 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 into `users.email`) require a NEW dated migration script under `scripts/migrations/YYYY-MM-DD-.js`. That folder doesn't exist yet; B2 creates it with the first migration file. The seed scripts' in-place `INSERT` literal updates ARE allowed per the operational-precedent line in the same rule (the `setup-neon-db.js` precedent from `drop-public-setup` Brief 1 — the rule says "if the change touches DDL strings or `INSERT` semantics, 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/ratelimit` per-class prefix isolation** (architect-cited from `add-rate-limiting` Brief 1 Finding 11) — each limiter class needs a distinct Redis prefix. After the rename, the five distinct prefixes are `deckhearth:auth`, `deckhearth:search`, `deckhearth:upload`, `deckhearth:generate`, `deckhearth:import`. The five-prefixes-must-be-distinct invariant is preserved. - **JWT auth contract from `fix-auth-bypass` Brief 4 + `add-rate-limiting` Brief 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 of `lib/rate-limit.js` is unchanged). - **`@vercel/postgres` tagged-templates only** — the migration script uses `@neondatabase/serverless` (matching `scripts/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 existing `neon(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-name` operator caveat: "if your environment had `@tcgvault.com` rows, run `node scripts/migrations/2026-05-24-rename-admin-email.js` once 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:** ```js 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:** ```js 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:` with `deckhearth:` — 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 the `add-rate-limiting` Brief 1 contract. **No call site needs editing.** - [ ] Lines 1-9, 15-131 stay byte-identical (imports, `cached = null` declaration, `init()` body, `extractIpIdentifier`, `extractUserIdentifier`, `check()` helper, the five `export async function` blocks, comments). - [ ] **In particular:** the comment block lines 4-8 (which uses `tcgvault:auth` as 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 reference `deckhearth:auth` would 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: ```bash rg "deckhearth:" lib/rate-limit.js | sort -u | wc -l ``` Expected: **5**. - [ ] Zero remaining `tcgvault:` references in the lib body: ```bash rg "tcgvault:" lib/rate-limit.js ``` Expected: **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:** ```json "name": "tcg-vault", ``` **Replace with:** ```json "name": "deck-hearth", ``` Acceptance: - [ ] Line 2 matches the verbatim replacement above (preserves leading 2-space indent + trailing comma). - [ ] Every other line in `package.json` stays 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.json` shows ONLY the two `name` field updates — NO opportunistic dependency churn (no `version` bumps, no new `integrity` hashes for packages that didn't change, no shifted ordering of unrelated entries). - [ ] If `npm install` produces unrelated churn (e.g., npm pulls fresh metadata for one or more deps), **STOP** and flag for a separate `bump-deps-cleanup` convoy. Do NOT commit accidental dependency updates inside the `pick-a-name` PR. - [ ] **Verification command** (run BEFORE commit): ```bash git diff package-lock.json | grep -E '^[+-]' | grep -v -E '^[+-]{3}|^[+-]\s*"name":' | head -20 ``` Expected: **zero output** (every diff line is either the file header `+++` / `---` or a `"name":` change). If output appears, investigate before commit. - [ ] `npm ci` (NOT `npm install`) should succeed cleanly with the regenerated lockfile — verifies the lockfile is internally consistent and matches `package.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 + trailing `Change the password`). - [ ] **Lines 30 (`cd tcg-vault`) and 105 (`tcg-vault/` directory tree) stay byte-identical** — repo-name references, deferred to `rename-repo-and-vercel-project` convoy. - [ ] 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 the `admin123` weak hash), formatted as another `>` blockquote with the verbatim 9-line content above. - [ ] No other line in `README.md` changes. - [ ] 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` / `bob123` password literals stay** — that's the queued `purge-weak-creds-from-helpers` convoy's scope, not `pick-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.md` returns **zero matches** and `rg "@tcgvault\.com" TESTING_GUIDE.md` returns **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: ```js VALUES (${'admin@tcgvault.com'}, ${hashedPassword}, ${'admin'}) ``` Replace with: ```js VALUES (${'admin@deckhearth.com'}, ${hashedPassword}, ${'admin'}) ``` **Edit 2 — Line 157 (console log):** Current: ```js console.log(' Admin user ready (email: admin@tcgvault.com)'); ``` Replace with: ```js 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_PASSWORD` env-var gate (lines 18-28, locked in by `drop-public-setup` Brief 1), the ESM imports (lines 11, 14, 15, locked in by `drop-public-setup` Brief 2), the seven `CREATE TABLE IF NOT EXISTS` blocks, the `ON CONFLICT (email) DO NOTHING` clause, and the troubleshooting copy. - [ ] **Re-running `npm run setup-db` after this convoy lands** seeds new deploys with `admin@deckhearth.com`. For existing deploys with `admin@tcgvault.com`, the seed's `ON CONFLICT (email) DO NOTHING` doesn't match (the existing email is different) so a NEW admin row would be created with `admin@deckhearth.com` — creating a TWO-ADMIN row collision. **The operator MUST run the migration script FIRST**, then `setup-db` (the migration sweeps the row to `@deckhearth.com`, and a subsequent `setup-db` sees 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: ```js VALUES (${'admin@tcgvault.com'}, ${hashedPassword}, ${'admin'}) ``` Replace with: ```js VALUES (${'admin@deckhearth.com'}, ${hashedPassword}, ${'admin'}) ``` **Edit 2 — Line 155 (console log):** Current: ```js console.log(' Admin User: admin@tcgvault.com'); ``` Replace with: ```js 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 per `convert-reset-db-to-esm` follow-up. (Note: this line is broken on Node 22.x with `"type": "module"` in package.json. The bug means `npm run reset-db` doesn't actually work today. That's out of `pick-a-name`'s scope — `reset-db.js` is a developer utility that nobody runs in CI; fixing the ESM-conversion bug is its own convoy. The email rename here is preparatory: when `convert-reset-db-to-esm` ships, the email will already be correct.) - [ ] **Line 143 (`const hashedPassword = await bcrypt.hash('admin123', 12);`) stays byte-identical** — weak password literal is `purge-weak-creds-from-helpers` scope. - [ ] **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):** ```js VALUES ('alice@tcgvault.com', ${alicePassword}, 'user') ``` → ```js VALUES ('alice@deckhearth.com', ${alicePassword}, 'user') ``` **Edit 2 — Line 21 (alice console log):** ```js console.log('✅ Created Alice (alice@tcgvault.com / alice123)'); ``` → ```js console.log('✅ Created Alice (alice@deckhearth.com / alice123)'); ``` **Edit 3 — Line 27 (bob INSERT):** ```js VALUES ('bob@tcgvault.com', ${bobPassword}, 'user') ``` → ```js VALUES ('bob@deckhearth.com', ${bobPassword}, 'user') ``` **Edit 4 — Line 30 (bob console log):** ```js console.log('✅ Created Bob (bob@tcgvault.com / bob123)'); ``` → ```js console.log('✅ Created Bob (bob@deckhearth.com / bob123)'); ``` **Edit 5 — Line 34 (summary admin line):** ```js console.log(' 1. admin@tcgvault.com / admin123 (Admin)'); ``` → ```js console.log(' 1. admin@deckhearth.com / admin123 (Admin)'); ``` **Edit 6 — Line 35 (summary alice line):** ```js console.log(' 2. alice@tcgvault.com / alice123 (User)'); ``` → ```js console.log(' 2. alice@deckhearth.com / alice123 (User)'); ``` **Edit 7 — Line 36 (summary bob line):** ```js console.log(' 3. bob@tcgvault.com / bob123 (User)'); ``` → ```js 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-helpers` scope. - [ ] **`ON CONFLICT (email) DO NOTHING` clauses stay byte-identical** — preserves the script's idempotency. - [ ] **The ESM imports (lines 3-5, `dotenv` / `@vercel/postgres` / `bcryptjs`) stay byte-identical** — `create-test-users.js` is already ESM-correct. - [ ] After this edit, **re-running `node scripts/create-test-users.js` against a fresh DB creates alice/bob with the new emails**. Against an existing DB with the OLD emails, the `ON CONFLICT` clause 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: ```js onClick={() => handleQuickLogin('alice@tcgvault.com', 'alice123')} ``` Replace with: ```js onClick={() => handleQuickLogin('alice@deckhearth.com', 'alice123')} ``` **Edit 2 — Line 184:** Current: ```js onClick={() => handleQuickLogin('bob@tcgvault.com', 'bob123')} ``` Replace with: ```js 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.js` stays 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 `` shell, the form fields, the `handleSubmit` body — DO NOT touch. - [ ] After this edit, clicking the "Alice" / "Bob" quick-login buttons pre-fills the email field with `@deckhearth.com`. The button's effect is purely UI state (sets `formData`); 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: 1. The `.toBeNull()` assertion on the next line (line 90) is the strong contract — it asserts the helper returns null, which is what matters. 2. 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-bypass` synthetic 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. 3. 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: ```js 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: ```js 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 the `const user = await getUserFromRequest({ headers: {} });` line and the `expect(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:run` still passes 21/21. - [ ] **Implementer alternative (if preferred):** if you'd rather update the email literal to `admin@deckhearth.com` for 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-bypass` bug returned `admin@tcgvault.com`, see commit `258e479` for 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:** ```js #!/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 a `ON CONFLICT` clause 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.com` rows. If any remain (unexpected), it warns + exits 1. - [ ] **No new dependencies.** Uses `dotenv` + `@neondatabase/serverless` — both already in `package.json` `dependencies`. - [ ] **`POSTGRES_URL` env guard at startup** — same defensive pattern as `setup-neon-db.js` for `ADMIN_INITIAL_PASSWORD` (drop-public-setup Brief 1 precedent). - [ ] **The script does NOT touch any other column.** It only updates `users.email` (and `users.updated_at` to reflect the change). No `password`, `role`, or any other field changes. - [ ] **NOT added to `package.json` scripts.** It's a one-off migration; the operator invokes it directly via `node 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 lint` exit code unchanged.** Per `lib/rate-limit.js` Brief 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 (matches `scripts/setup-neon-db.js`'s shape). - [ ] **`npm run test:run` (vitest) passes 21/21.** Only test edit is the comment-addition in `test/lib/permission-middleware.test.js`; assertion semantics are byte-identical, so all 21 tests still pass. - [ ] **`npm run build` exit 0.** Turbopack should compile cleanly. The `lib/rate-limit.js` edits are string literals; `pages/login.js` edits are string literals. No new imports. - [ ] **`npm run test:smoke` against 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-limiting` Brief 1's check): ```bash rg "deckhearth:" lib/rate-limit.js | sort -u ``` Expected: **5 distinct lines** (`deckhearth:auth`, `deckhearth:search`, `deckhearth:upload`, `deckhearth:generate`, `deckhearth:import`). And: ```bash rg "tcgvault:" lib/rate-limit.js ``` Expected: **zero matches in active code** (the architect-recommended preserve-comment path may leave the comment block on lines 4-8 referencing `tcgvault:auth` as the example; if so, that's the only acceptable match). - [ ] **Repo-wide grep (post-B2, with B1 also landed):** ```bash rg "@tcgvault\.com" --type-not md ``` Expected: **at most 1 match** — `test/lib/permission-middleware.test.js` line 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. ```bash rg "@tcgvault\.com" .convoys/ ``` Expected: **same count as pre-B2** (historical convoy artifacts). ```bash rg "@tcgvault\.com" --type-not md --type-not js ``` Expected: **zero matches** (no `@tcgvault.com` left outside markdown + js source). - [ ] **In-DB migration verification (against staging Neon, post-deploy):** ```bash node scripts/migrations/2026-05-24-rename-admin-email.js ``` Expected 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.com ``` Expected 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: ```bash npm install # regenerate package-lock.json git diff package-lock.json | head -40 ``` Expected: a 4-line diff (two `-name` lines, two `+name` lines). 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):** ```bash 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 done ``` Expected (only meaningful with `KV_REST_API_*` set against the staging Upstash): - Attempts 1-5: `401` - Attempt 6: `429` with `Retry-After` header The behavior is byte-identical to pre-B2 — only the Redis key namespace changes from `tcgvault:auth:*` to `deckhearth:auth:*`. The counter for `nobody@example.com`-from-this-IP starts fresh at zero post-deploy because the old `tcgvault:auth:*` keys are orphaned. This is the acknowledged Redis counter reset. - [ ] **Migration script smoke (against staging only):** ```bash node scripts/migrations/2026-05-24-rename-admin-email.js ``` Run twice. First run reports the count + UPDATEs; second run reports "Nothing to migrate." - [ ] **Post-migration login smoke:** ```bash curl -sX POST http://localhost:3000/api/auth/login \ -H 'content-type: application/json' \ -d '{"email":"admin@deckhearth.com","password":""}' ``` Expected: `200` + JWT in response body. - [ ] **Quick-login button smoke:** in the browser, visit `/login`, click "Alice", confirm the email field shows `alice@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-import` as admin** to confirm the post-`add-rate-limiting` admin-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-.js` (folder TBD). Do NOT edit `scripts/setup-neon-db.js` in place for any **DDL change** ... **Operational changes are allowed** (env-var gating, error-message hardening, module-system fixes) — `drop-public-setup` set this precedent ... The distinction: if the change touches DDL strings or `INSERT` semantics, 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.js` to 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 `