feat(brand): unify on Deck Hearth across in-repo strings + infra (P1 brand decision) #21

Merged
varutasu merged 4 commits from convoy/pick-a-name into main 2026-05-25 03:28:29 -04:00
21 changed files with 1897 additions and 37 deletions

596
.convoys/pick-a-name.md Normal file
View file

@ -0,0 +1,596 @@
# pick-a-name (P1 — pre-launch brand consistency)
**Status:** in-progress
**Priority:** P1 (pre-launch; not a security blocker, but a brand-consistency
blocker that touches every user-facing surface)
**Convoy owner:** parent (orchestration); `role-architect` then
`role-implementer(s)` then `role-doc-writer`
**Opened:** 2026-05-24
**Operator decision (gate-0, already ratified):** **Deck Hearth wins.**
## Problem
`AGENTS.md` line 5 has called this out since project setup: the repo, README,
seed data, package name, infrastructure, and admin email all say **"TCG
Vault"** (and the kebab `tcg-vault` / camel `tcgvault` variants), while the
actual rendered header brand in `components/Layout.js` says **"Deck Hearth"**.
Two distinct names, two distinct visual brands (the `AnimatedFireLogo.js`
component is also affected). Pre-launch this MUST be one name end-to-end.
## Operator's brand decision (gate-0)
**Deck Hearth wins.** Rationale (operator-stated): more distinctive, more
ownable, and the brand the human-facing surface (Layout header) already
renders. The trade is real: this is the higher-cost path because the
already-shipped infrastructure ID is the *losing* name. The architect's job
is to scope and minimize that cost — NOT to re-litigate the brand decision.
## Initial surface inventory (architect to expand)
Pre-architect rough hit-count from `rg`, scoped to non-vendor files:
| Surface | Files (sample) | "TCG Vault" or `tcg(-)?vault` hits |
| --- | --- | --- |
| Marketing copy | `README.md`, `pages/index.js`, `pages/login.js`, `pages/signup.js` | 4 + 5 + 4 + 1 |
| Brand components | `components/AnimatedFireLogo.js`, `components/ShareModal.js` | 4 + 1 |
| Seed scripts (admin email) | `scripts/setup-neon-db.js`, `scripts/create-test-users.js`, `scripts/seed-collections-*.js` | 2 + 7 + 6 |
| Test guides | `TESTING_GUIDE.md` | 7 |
| **Infrastructure-bound (rename has external blast)** | `package.json`, `lib/rate-limit.js` (Redis prefix), `.github/workflows/*.yml` | 1 + 5 + 3 |
| **Already-correct (Deck Hearth)** | `components/Layout.js`, `styles/globals.css`, `components/AnimatedFireLogo.js` partial | n/a (these stay) |
## Hard scoping rules
- **Strictly NO re-litigating the brand decision.** Deck Hearth wins; the
architect ratifies *how* to rename, not *whether* to.
- **Keep the repo `tcg-vault` for now.** GitHub rename is a separate concern
(auto-redirects work, but Vercel-link / `gh` configs / local remotes /
bookmarks all need touching). Track as a queued follow-up convoy
`rename-repo-and-vercel-project`. This convoy renames the **product brand
in-app**; the repo/Vercel rename is a downstream convoy.
- **Redis prefix migration is in scope.** `lib/rate-limit.js` uses
`tcgvault:auth/search/upload/generate/import` as Redis key prefixes. These
must rename to `deckhearth:*` (or similar) atomically with the convoy.
Accept the one-time rate-limit-state reset (counters are per-15-min /
per-1-hour anyway; no user-perceptible impact).
- **Admin email migration is in scope.** `admin@tcgvault.com` rename to
`admin@deckhearth.com` (or similar — architect proposes the canonical
email, operator ratifies). Includes the in-DB UPDATE for any existing
`admin@tcgvault.com` row (write a one-off `scripts/migrations/YYYY-MM-DD-
rename-admin-email.js` per the no-go-zones rule for new migrations).
- **`package.json` name field:** rename to `deck-hearth` in this convoy
(npm package name; we don't publish, so blast is contained to lockfile +
any local script reference).
- **Lockfile regeneration:** the architect ratifies whether to regenerate
`package-lock.json` in this convoy or accept the rename mismatch
short-term. (Recommended: regenerate; cheap; matches package.json shape.)
- **DNS / domain:** out of scope — separate convoy
`point-domain-at-deckhearth` (you don't own a `deckhearth.*` domain yet
per operator's pre-convoy statement; if/when acquired, that triggers DNS).
- **Favicon / OG images / social cards:** in scope IF source files exist;
defer to follow-up convoy `regenerate-brand-assets` if they need new design.
## Architect deliverables
1. **Full surface inventory** — every occurrence of `TCG Vault`, `tcgvault`,
`tcg-vault`, plus every occurrence of `Deck Hearth` / `deck-hearth` /
`deckhearth` (the latter set tells you what's *already* correct and what
shape collisions need handling, e.g., if Layout.js has `Deck Hearth` but
AnimatedFireLogo.js has `DECKHEARTH` — pick canonical casing).
2. **Canonical-string ratification:** propose the canonical product name
("Deck Hearth" with internal-cap; or "Deckhearth" one word; or other);
the canonical kebab (`deck-hearth`); the canonical camel/lower
(`deckhearth`); the canonical admin email (`admin@deckhearth.com` or
variant); the canonical Redis prefix (`deckhearth` or `dh`). Mark each as
D1-D5 (operator gate-1 for all five — they're naming choices, not
architectural ones).
3. **Slice plan:** probably 2-3 briefs. Suggested split (architect can
change): Brief 1 = in-repo string sweep + AnimatedFireLogo brand update
(mechanical); Brief 2 = Redis prefix migration in `lib/rate-limit.js` +
admin-email DB migration script + package.json rename + lockfile
regen (operationally riskier — needs careful testing). Brief 3 (if
needed) = visual baseline reset (because the smoke + visual-diff specs
will all fail with the new brand).
4. **Boot-the-brief verification** for each: confirm specs compile, lint
passes, vitest passes, smoke can still run against a preview.
5. **Out-of-scope queued follow-ups:** explicit list (repo rename, Vercel
project rename, DNS / domain acquisition, brand asset regeneration,
social card refresh).
## Known constraints
- **Cross-cutting smoke/visual diff blast.** PR-#18's Playwright smoke spec
("sign-in page renders") + the visual-diff baseline are both predicated on
the *current* brand strings. Brief 2 (or Brief 3 if split) MUST update
the smoke spec expectations and either regenerate visual baselines or
accept temporary baseline drift (the `adopt-playwright-smoke` Decision 4
`continue-on-error` swallow is still in effect, so visual-diff drift
won't fail CI — but the queued `seed-visual-baselines-on-linux` convoy
becomes harder to ship until *after* this convoy lands).
- **Smoke spec lives at `tests/smoke/app.smoke.spec.ts`.** Likely needs
one-or-two-character edits to text matchers.
- **`AGENTS.md` line 5's branding note** is the canonical historical record
of the inconsistency; the doc-writer pass MUST update it from "Pick one
before launch" to "Deck Hearth (decided 2026-05-24 in `pick-a-name`
convoy; commit X)."
- **Operator-action-required pre-merge:** smoke-test the rebranded preview
manually before merge (every public page should render "Deck Hearth";
login + register should still work; admin import UI should still
function with the renamed admin user).
## Out of scope (queued follow-ups)
- `rename-repo-and-vercel-project` — GitHub repo rename + Vercel project
rename + local git remote update. P2 polish. Auto-redirects make this
low-risk-low-urgency.
- `point-domain-at-deckhearth` — DNS / domain. Blocked on domain
acquisition (operator pre-convoy: doesn't own `deckhearth.*` yet).
- `regenerate-brand-assets` — favicon, OG images, social cards. P2
polish; requires design pass.
- `migrate-existing-user-emails` — if any existing user accounts have
`@tcgvault.com` emails (besides admin), propose a migration. Architect
to surface count via a `rg`/DB query during inventory.
## Owns
`role-architect` (decisions D1-D5, slice plan) → `role-implementer(s)`
(per brief) → `role-doc-writer` (AGENTS.md line 5 + this file's
As-shipped + ship-readiness Status summary footer).
## Architecture (role-architect, 2026-05-24)
**Status:** architect-complete; awaiting operator gate-1 ratification on D1-D5
before implementer dispatch.
**No blocking findings.** No surfaced naming collision with existing npm
packages (we don't publish; the `name` field is a local identifier).
`deckhearth.com` domain is unowned per operator's pre-convoy statement —
queued in `point-domain-at-deckhearth`, downstream of this convoy. Redis
counter reset is an acknowledged one-time cost (per-15-min and per-1-hour
windows; no user-perceptible impact). All other costs are mechanical sweeps.
**Scope summary:** 16 source files modified across two parallel briefs +
1 new migration script. ~75-110 lines of net diff (excluding `package-lock.json`
regen, which is its own opaque artifact). Two briefs touch DISJOINT file
sets and can run in parallel via `/multitask`.
### Operator-ratified decisions (gate-0, captured)
- **Brand winner:** **Deck Hearth.** Operator's rationale: more distinctive,
more ownable, and the brand the human-facing surface (Layout header)
already renders. The architect's job is to scope and minimize the cost of
getting there — NOT to re-litigate the brand decision.
### Architect-proposed decisions (operator gate-1 required, ALL FIVE)
These five naming choices are NOT architect-self-ratifiable. They establish
the canonical strings the entire codebase will be swept to. **All five
require operator gate-1 ratification before Brief 1 OR Brief 2 dispatch.**
#### D1 — Canonical display string (operator gate-1)
**Recommendation:** **"Deck Hearth"** (two words, internal cap, space-separated).
Trade-off considered: "Deckhearth" (one word) is brand-simpler and gives a
cleaner email TLD (`deckhearth.com` reads as one syllable). But every
already-shipped user-facing surface in the repo renders the two-word form:
- `components/Layout.js` lines 623 + 716 → `Deck Hearth`
- `components/AnimatedFireLogo.js` line 13 → `Deck Hearth Logo`
- `pages/index.js` lines 64, 96, 266, 299, 315 → `Deck Hearth`
- `pages/login.js` lines 76, 158 → `Deck Hearth`
- `pages/signup.js` line 215 → `Deck Hearth`
- `components/ShareModal.js` line 116 → `Deck Hearth`
- `styles/globals.css` line 15 → `Deck Hearth Fire-Inspired`
Choosing "Deckhearth" would require RE-sweeping all seven currently-correct
surfaces — net negative-cost choice. Recommendation: **ratify "Deck Hearth"**
to make the in-progress strings the canonical strings.
#### D2 — Canonical kebab string (operator gate-1)
**Recommendation:** **`deck-hearth`** (matches existing CSS class
`deck-hearth-logo-container` in `components/AnimatedFireLogo.js` line 8 +
the conventional hyphenated lowercase used by package.json `name` fields and
file naming).
Trade-off: `deckhearth` (no hyphen) would match D3 below but conflicts with
both Node ecosystem convention (`@scope/kebab-name`) and the existing CSS
class. Recommendation: **ratify `deck-hearth`**.
#### D3 — Canonical camel/lower string for IDs (operator gate-1)
**Recommendation:** **`deckhearth`** (single token, no separators).
Used for: Redis key prefixes, env-var-style identifiers, internal symbol
names. Distinct from D2 because IDs lack convention-bound separators (the
Upstash `prefix:` option is a single string token; convention is colon-
separated namespace, e.g. `deckhearth:auth:1.2.3.4`). Recommendation: **ratify
`deckhearth`**.
#### D4 — Canonical admin email (operator gate-1)
**Recommendation:** **`admin@deckhearth.com`** (placeholder TLD pending the
queued `point-domain-at-deckhearth` convoy).
Trade-off: the operator does not yet own `deckhearth.com` (or any
`deckhearth.*` TLD) per the convoy seed § *"point-domain-at-deckhearth …
blocked on domain acquisition"*. The seed admin row's email is a STRING
literal in the database — it doesn't need to resolve to a real mailbox to
function (auth uses the email as a unique identifier, not as a mail target).
Picking `.com` now matches the most-likely future domain purchase; if the
operator instead buys `.app` or `.gg`, a future one-line migration can sweep
the TLD without touching this convoy's structure.
**Apply to all three current `@tcgvault.com` accounts:** admin, alice, bob
(the test users created by `scripts/create-test-users.js`). The migration
script in Brief 2 sweeps all three uniformly via a `REPLACE(email,
'@tcgvault.com', '@deckhearth.com')` UPDATE.
Recommendation: **ratify `admin@deckhearth.com` + sweep all `@tcgvault.com`
emails to `@deckhearth.com`** in the same migration. If the operator wants
a different TLD, swap `.com` for the chosen TLD in the migration script and
the seed scripts before Brief 2 implementer dispatch — that's a one-line
change confined to B2's file set.
#### D5 — Canonical Redis prefix (operator gate-1)
**Recommendation:** **full `deckhearth`** (NOT abbreviated `dh`).
Trade-off considered: `dh` saves ~8 bytes per key (vs. `deckhearth`). At peak
load with say 100k active rate-limit identifiers, total savings would be
~800KB of Redis memory — negligible against an Upstash plan that bills by
request count, not per-byte storage. The self-documenting `deckhearth:auth:…`
prefix is materially easier to debug when reading Redis console output. (And
the `cors-tighten` Decision-style precedent applies: short-and-cryptic IDs
tend to attract "what does this mean?" comments in code, longer-and-clear IDs
explain themselves.)
Recommendation: **ratify full `deckhearth`**.
### Full surface inventory
Architect-verified `rg` sweep results, grouped by classification. Files
under `.convoys/**` (append-only convoy history per `.cursor/rules/no-go-
zones.mdc`) are listed at the bottom for completeness but are NOT in any
brief's `files:` list — they stay byte-identical.
#### User-facing display strings (must rename)
| File | Line | Current | Brief |
| --- | --- | --- | --- |
| `README.md` | 1 | `# TCG Vault` | 2 (file already in B2 for email) |
| `TESTING_GUIDE.md` | 1 | `# 🎯 TCG Vault Collaboration Testing Guide` | 2 (file already in B2 for emails) |
| `scripts/README.md` | 1 | `# TCG Vault Bulk Import Scripts` | 1 |
#### Brand-bound infrastructure IDs (must rename; has external blast)
| File | Line | Current | Blast | Brief |
| --- | --- | --- | --- | --- |
| `lib/rate-limit.js` | 10-14 | `tcgvault:auth/search/upload/generate/import` | Orphans existing Redis counters at deploy time; new counters start at zero | 2 |
| `package.json` | 2 | `"name": "tcg-vault"` | Lockfile regen required | 2 |
| `package-lock.json` | 2, 8 | `"name": "tcg-vault"` (×2) | Generated; regen via `npm install` after `package.json` change | 2 |
| `scripts/setup-neon-db.js` | 148 | `VALUES (${'admin@tcgvault.com'}, …)` | Seeds NEW deploys' admin email; needs migration for existing deploys | 2 |
| `scripts/setup-neon-db.js` | 157 | `console.log(' Admin user ready (email: admin@tcgvault.com)')` | Stdout copy | 2 |
| `pages/api/cards/import-lorcana.js` | 14 | `'User-Agent': 'TCG-Vault/1.0'` | External HTTP identity sent to Lorcana API | 1 |
| `pages/api/cards/import-pokemon.js` | 14 | `'User-Agent': 'TCG-Vault/1.0'` | External HTTP identity sent to Pokémon TCG API | 1 |
| `scripts/import-lorcana.js` | 20 | `'User-Agent': 'TCG-Vault/1.0'` | External HTTP identity (CLI variant) | 1 |
#### Email-bound infrastructure (must rename; in-DB migration required)
| File | Lines | Current | Brief |
| --- | --- | --- | --- |
| `scripts/setup-neon-db.js` | 148, 157 | `admin@tcgvault.com` | 2 (also infra above) |
| `scripts/reset-db.js` | 147, 155 | `admin@tcgvault.com` | 2 |
| `scripts/create-test-users.js` | 18, 21, 27, 30, 34-36 | `alice@tcgvault.com`, `bob@tcgvault.com`, `admin@tcgvault.com` | 2 |
| `pages/login.js` | 172, 184 | `alice@tcgvault.com`, `bob@tcgvault.com` (quick-login fixtures) | 2 |
| `README.md` | 132 | `Admin email: the seed creates admin@tcgvault.com` | 2 |
| `TESTING_GUIDE.md` | 7-9, 24, 46, 53 | admin/alice/bob `@tcgvault.com` emails in account table + workflow | 2 |
| `test/lib/permission-middleware.test.js` | 87 | `email: 'admin@tcgvault.com'` (NEGATIVE regression assertion) | 2 (see B2 decision on whether to preserve or update — leaning preserve) |
| `scripts/migrations/2026-05-24-rename-admin-email.js` | NEW | (new file) | 2 |
#### Internal references in docs / rules / agent context (rename for consistency)
| File | Line | Current | Brief |
| --- | --- | --- | --- |
| `AGENTS.md` | 5 | `Branding note: the repo, README, and seed data say "TCG Vault" …` | 1 |
| `.cursor/rules/ui-and-theming.mdc` | 61 | `The repo says "TCG Vault" everywhere except …` | 1 |
| `.cursor/rules/auth-and-permissions.mdc` | 2 | `description: Auth model + permission model for tcg-vault (…)` | 1 |
#### Repo / Vercel project name (explicitly OUT OF SCOPE per convoy seed)
These references match the literal repo name `tcg-vault`, which stays for now
per the convoy seed § *"Keep the repo `tcg-vault` for now."* When the
follow-up convoy `rename-repo-and-vercel-project` ships, these update
together with the repo rename. **Do NOT touch in this convoy.**
| File | Line | Reference |
| --- | --- | --- |
| `README.md` | 30 | `cd tcg-vault` (git clone instructions) |
| `README.md` | 105 | `tcg-vault/` (directory tree diagram) |
| `AGENTS.md` | 1 | `# AGENTS.md — AI collaboration (tcg-vault)` |
| `.github/workflows/ci.yml` | 12 | `# NOTE: tcg-vault is JavaScript (not TypeScript). …` |
| `.github/workflows/ci.yml` | 123 | `# The tcg-vault frontend and API are served from the same Vercel …` |
| `.github/workflows/visual-diff.yml` | 5 | `# Paths are tcg-vault-specific (pages router, JS).` |
| `.agent-context-manifest.yml` | 84, 89, 99, 104, 109, 114 | `source: "tcg-vault-local"` (internal source tag, re-evaluated by `sync-agent-context` skill at repo rename time) |
#### Already-correct (Deck Hearth — stays byte-identical)
These render the canonical D1 string and have no `tcgvault` baggage. Do NOT
touch:
- `components/Layout.js` (lines 623, 716 — sidebar headers)
- `components/AnimatedFireLogo.js` (lines 8, 13, 25, 29 — CSS class +
alt text + style scope)
- `components/ShareModal.js` (line 116 — social share title)
- `pages/index.js` (lines 64, 96, 266, 299, 315 — landing page)
- `pages/login.js` (lines 76, 158 — welcome copy; emails at 172/184 are in B2)
- `pages/signup.js` (line 215 — signup heading)
- `styles/globals.css` (line 15 — theme comment)
#### Convoy artifacts + manifest (no-go per `.cursor/rules/no-go-zones.mdc`)
`.convoys/**/*.md` are append-only history — they describe the state at
write-time. Per the no-go zone for append-only artifacts and `scripts/seed-
*.js` historical jobs, these are NOT renamed in this convoy:
- `.convoys/fix-auth-bypass/brief-2-remove-admin-bypass.md` (4 hits — describes the BUG that returned the OLD synthetic-admin email)
- `.convoys/fix-auth-bypass/brief-5-vitest-and-auth-tests.md` (2 hits — same)
- `.convoys/drop-public-setup.md`, `.convoys/drop-public-setup/brief-*.md` (multiple hits — historical decision record)
- `.convoys/add-rate-limiting.md`, `.convoys/add-rate-limiting/brief-*.md` (8 hits — Redis prefix references reflect the *pre-rename* state at convoy-close; this convoy is what changes them; doc-writer pass at THIS convoy's close updates the gotcha / rule, NOT the historical convoy file)
- `.convoys/cors-tighten/brief-1-sweep-wildcard-cors.md` (2 hits — historical)
- `.convoys/fix-layout-default-user.md`, `.convoys/fix-layout-default-user/brief-*.md` (3 hits — historical)
- `.convoys/ship-readiness.md` (4 hits — historical milestone log; doc-writer pass appends a NEW "12. RESOLVED" entry rather than rewriting line 244)
- `.convoys/bump-next-js.md`, `.convoys/fix-vercel-deployment-protection-in-ci.md` (each 1 hit — historical)
- `scripts/seed-collections-with-cards.js`, `scripts/seed-collections-alice-bob.js` (3 hits each — historical seed jobs per no-go-zone rule "`scripts/seed-*.js` — historical migration / seed jobs already executed"; the email migration in B2 covers the in-DB rename, so these scripts' WHERE-clause literals would simply match zero rows post-migration if re-run, which is a harmless no-op)
- `components/Layout.js.backup` (1 hit — legacy snapshot per no-go-zone rule)
### API surface
**No new or modified API routes.** No request / response shape changes. The
Redis prefix rename in `lib/rate-limit.js` is internal to the lib module —
the five exported `check*RateLimit(req[, userId])` functions retain their
byte-identical Brief-4-and-`add-rate-limiting`-Brief-1 contracts: same name,
same args, same `{ allowed, remaining, reset }` return shape. The seven
gated handlers do NOT need editing.
### Schema diff
**No DDL changes.** The only schema-adjacent operation is the in-DB UPDATE
of `users.email` for at most 3 rows (admin + alice + bob), performed by
the new dated migration script `scripts/migrations/2026-05-24-rename-admin-
email.js`. No new tables, no new columns, no new indexes, no new
constraints. `docs/SCHEMA_MAP.md` requires no update (column shapes and
table inventory are unchanged).
The new `scripts/migrations/` folder is created with this convoy's first
migration file. Per `.cursor/rules/no-go-zones.mdc` § *"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 seed scripts' literal email INSERT values
in `scripts/setup-neon-db.js` + `scripts/reset-db.js` + `scripts/create-test-
users.js` ARE INSERT-semantics changes; the migration script is the
required artifact to accompany those edits. The architect ratifies this:
the in-place edits keep new-deploy seeding correct; the migration handles
the existing-deploy update.
### Test plan
**No new vitest unit tests** in this convoy (decoupled from the rebrand).
Per `add-rate-limiting` Decision 6 precedent — new handler / unit tests are
queued under `fill-vitest-handler-coverage`. The only test edit is the
update to `test/lib/permission-middleware.test.js` line 87's NEGATIVE
regression assertion (see B2 acceptance criteria).
**Vitest 21/21 must still pass** after Brief 1 and Brief 2 land. Brief 1
touches zero files referenced by any test. Brief 2 touches three test-
adjacent files (`pages/login.js` quick-login fixtures, `test/lib/permission-
middleware.test.js` line 87, and the test-user creation scripts) — none of
these break the unit-test suite. The negative regression assertion's
`.toBeNull()` check is the canonical assertion; the `.not.toEqual({...})`
soft check is incidentally about the historical literal email value and
will continue to pass against any email.
**Playwright smoke 3/3 must still pass** after both briefs. Verified at
architect time: `tests/smoke/app.smoke.spec.ts` text matchers are `'sign in'`
(case-insensitive regex), `/api/health`, and HTTP status < 500. NONE of
these text matchers reference `TCG Vault` or `Deck Hearth` brand strings;
the smoke spec is brand-agnostic and needs **zero edits**. (This is the
"audit" the prompt requested — confirmed via direct read of the spec file.)
**Playwright visual baseline**: verified at architect time that
`tests/visual/__screenshots__/` does NOT yet exist (no committed baselines).
Per the `adopt-playwright-smoke` Decision 4 `continue-on-error` swallow and
the queued `seed-visual-baselines-on-linux` convoy, the visual-diff
workflow's first-run failure is the documented end state. **No baseline
drift to manage in this convoy.** When `seed-visual-baselines-on-linux`
eventually lands, it MUST run AFTER `pick-a-name` so the seeded baseline
captures the Deck Hearth brand strings; the architect updates the
`seed-visual-baselines-on-linux` queued entry below to record this
ordering constraint.
**Manual verification** (operator pre-merge):
- Boot `npm run dev`. Visit `/`, `/login`, `/signup`, `/cards`. Every brand
string should render `Deck Hearth` (verify in browser DevTools "Find").
- Click the "Alice" / "Bob" quick-login buttons on `/login`. Expected
behavior post-migration: each button fills the email field with
`<name>@deckhearth.com` and the password `<name>123`. With the migration
run, login succeeds and redirects to dashboard. WITHOUT the migration
run (e.g., op forgot), login 401s — that's the expected failure mode and
the operator's signal to run the migration script.
- Run `node scripts/migrations/2026-05-24-rename-admin-email.js` against
a staging DB. Expected: prints the count of `@tcgvault.com` rows found
(0-3), UPDATEs them, prints the success count. Re-run: prints "Nothing
to migrate. Exiting." (idempotency check).
- Visit a Vercel preview. Verify all public surfaces render Deck Hearth.
Quick-login test (admin / alice / bob with the new emails) only works
if the preview's DB is the staging Neon with the migration run.
### Risk list
1. **Operator forgets to run the migration script post-deploy.** The
admin email and Alice/Bob test-user emails would remain `@tcgvault.com`
in the DB while the seed scripts and login.js fixtures reference
`@deckhearth.com`. Symptom: admin login attempts with the new email
401 because the DB still has the old email. **Mitigation:** the
`pick-a-name` PR description MUST include a "Post-merge operator
action" callout naming the migration script and the
`node scripts/migrations/...` command verbatim. (Same defense pattern
as `drop-public-setup`'s "operator caveat" for the weak-hash
rotation.) Doc-writer pass at convoy close adds this to `AGENTS.md`
Gotcha #4's "Operator caveat" paragraph as a sibling caveat.
2. **Redis counter reset surprises a heavy active user.** Rare in
pre-launch. The five sliding windows (15-min auth, 1-min search,
1-hour upload/generate/import) all expire within an hour anyway —
the worst-case "loss" is that an attacker who had burned 4/5 of the
auth budget at the moment of deploy gets reset to 0/5. This is
defensively neutral: the legitimate user's prior counter also resets
(they too get a fresh 5/15-min budget). **Mitigation:** none needed;
the convoy seed explicitly accepts this trade.
3. **Lockfile regen surfaces unrelated dependency churn.** Running
`npm install` after a `package.json` `name` change SHOULD only update
the `name` field at the top of `package-lock.json` plus the
`packages.""` entry inside it. But npm might opportunistically
refresh stale `integrity` hashes or pull in metadata updates.
**Mitigation:** B2 acceptance criteria require the implementer to
inspect the `package-lock.json` diff and confirm it touches only
`name`-related entries; if any unrelated `dependencies` / `version`
churn appears, the brief instructs the implementer to STOP and flag
for a separate `bump-deps-cleanup` convoy rather than landing
accidental dependency updates inside the rebrand PR.
4. **The historical synthetic-admin negative regression assertion in
`test/lib/permission-middleware.test.js` line 87.** The literal email
`admin@tcgvault.com` is the OLD bug's exact shape. Two options: (a)
preserve the historical literal so the test continues to encode "the
helper does NOT return THIS SPECIFIC pre-`fix-auth-bypass` shape"; or
(b) update to `admin@deckhearth.com` so the test's literal matches
the current admin email. Recommendation: **preserve (a)** and add a
comment explaining the historical preservation. The `.toBeNull()`
assertion two lines below is the strong contract; the
`.not.toEqual({...})` is the soft historical-shape check. B2's
acceptance criteria documents the preserve-(a) recommendation but
leaves the final call to the implementer.
5. **The `.agent-context-manifest.yml` `source: "tcg-vault-local"` tag.**
Architect-verified at brief time that 6 lines reference this string
as an internal source identifier for the `sync-agent-context` skill's
drift detection. Renaming to `deck-hearth-local` could break the
sync skill's behavior (it tracks "what shipped from this source").
**Mitigation:** leave as-is in this convoy; flag for re-evaluation
in `rename-repo-and-vercel-project` when the repo's literal name
actually changes.
6. **Email is the unique constraint on `users` — collision is impossible
but admin-row recreation is.** If the operator runs `setup-neon-db.js`
AFTER the migration runs, the seed's `INSERT … ON CONFLICT (email)
DO NOTHING` would NOT recreate the admin row (because the row exists
with the NEW email post-migration). That's the correct behavior. If
the operator runs `setup-neon-db.js` BEFORE the migration runs and
the existing admin row has the OLD email, the seed inserts a NEW row
with the NEW email, creating TWO admin rows (one with each TLD).
**Mitigation:** the migration script's UPDATE is idempotent and
handles both cases (it sweeps any remaining `@tcgvault.com` row to
`@deckhearth.com`, which would collide with the existing
`@deckhearth.com` row's UNIQUE constraint and fail loudly). The
architect recommends the operator run the migration FIRST, then
confirm via `SELECT email FROM users WHERE role = 'admin'` returns
exactly one row before running `setup-neon-db.js`. B2 acceptance
criteria document this ordering.
7. **B1 and B2 are parallelizable BUT both depend on operator gate-1
ratification of D1-D5 BEFORE dispatch.** This is a single gate at the
convoy level, not a per-brief gate. Once the operator says "approved,
run implementers" with the D1-D5 choices ratified, the conductor can
dispatch B1 + B2 in parallel via `/multitask` since their `files:`
sets are disjoint.
### Decomposition
| Brief # | Title | Files | Depends on | Estimated PR size |
| --- | --- | --- | --- | --- |
| 1 | In-repo display + comment sweep (User-Agent strings, branding notes, rule descriptions) | 7 files | none (gate-1 ratification of D1+D2+D3 required) | ~10-15 lines net |
| 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) | 10 files + 1 new migration script | none (gate-1 ratification of D1-D5 required); operator pre-merge action: run migration | ~65-95 lines net + opaque package-lock.json regen |
### Slice dependencies (multitask-ready)
```yaml
slice_dependencies:
- brief: 1
depends_on: []
files:
- 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
- brief: 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
```
**Parallelism:** B1's 7 files and B2's 11 files are DISJOINT. The
conductor can dispatch both in parallel via `/multitask` once the operator
ratifies D1-D5 at gate-1. Neither brief blocks the other.
### Out-of-scope queued follow-ups (reaffirmed + 2 new)
Reaffirms the convoy seed's four:
- **`rename-repo-and-vercel-project`** (P2 polish) — GitHub repo rename
(`tcg-vault` → `deck-hearth`) + Vercel project rename + local git
remote update + the 8 architect-verified `tcg-vault` references that
cite the literal repo (README cd/tree, AGENTS.md line 1, the three
workflow YAML comments, `.agent-context-manifest.yml` source tags).
Auto-redirects make this low-risk-low-urgency.
- **`point-domain-at-deckhearth`** — DNS / domain. Blocked on domain
acquisition (operator pre-convoy: doesn't own `deckhearth.*` yet).
If the operator buys a TLD other than `.com`, this convoy also
sweeps the email TLD via a one-line REPLACE migration.
- **`regenerate-brand-assets`** — favicon, OG images, social cards.
`public/favicon.ico` exists today (unverified what brand it is — out
of scope to inspect binary in this convoy). P2 polish; requires
design pass.
- **`migrate-existing-user-emails`** — for real users with
`@tcgvault.com` emails. Architect-verified at brief time: pre-launch,
the ONLY `@tcgvault.com` accounts are admin + alice + bob (3 fixtures
created by `create-test-users.js`). Brief 2's migration handles all
three. If the operator opened the app to friends-and-family pre-this-
convoy and any signed up with `@tcgvault.com` emails, the same
migration covers them too (it sweeps every row matching
`email LIKE '%@tcgvault.com'`).
Two NEW follow-ups surfaced by architect inventory:
- **`convert-reset-db-to-esm`** (P2 hygiene) — `scripts/reset-db.js` line
142 uses CommonJS `require('bcryptjs')` inside an ESM file. Same bug
pattern that hit `setup-neon-db.js` pre-`drop-public-setup` Brief 2.
Brief 2 of `pick-a-name` does NOT fix this in scope (it touches the
email literal and console-log copy only); a separate convoy converts
the require → import. May fold into `purge-weak-creds-from-helpers`.
- **`update-seed-visual-baselines-on-linux-ordering`** — the queued
`seed-visual-baselines-on-linux` convoy MUST run AFTER `pick-a-name`
so the first committed Linux baseline captures the Deck Hearth brand
strings (not TCG Vault). Architect recommends the operator (or the
conductor of that future convoy) add a `depends_on: pick-a-name`
ordering note to the `seed-visual-baselines-on-linux` queued entry
in `.convoys/ship-readiness.md`.
### As-shipped
(To be appended post-merge by `role-doc-writer`.)

View file

@ -0,0 +1,320 @@
---
convoy: pick-a-name
brief_number: 1
depends_on: []
files:
- 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
---
# Brief 1: In-repo display + comment sweep (User-Agent strings, branding notes, rule descriptions)
## Goal (1 sentence)
Sweep the seven repo files whose ONLY brand references are display / comment / external-HTTP-User-Agent strings (NOT emails, NOT Redis prefixes, NOT package.json identifiers), renaming each occurrence of `TCG Vault`, `TCG-Vault`, and standalone-product-identity `tcg-vault` to the operator-ratified `Deck Hearth` / `Deck-Hearth` / `deck-hearth` per D1 + D2, while leaving every literal-repo-name reference (`cd tcg-vault`, directory-tree diagrams, workflow YAML comments) byte-identical because the repo + Vercel project rename is the queued `rename-repo-and-vercel-project` convoy.
## Files in scope (do not edit anything else)
The 7 files listed in `files:` above (all modified, no new files, no deletions).
**Files explicitly out of scope** (do not touch even if it seems related):
- `README.md` — title + admin-email mentions are in **Brief 2** (file-disjoint per the slice plan; B2 owns README to keep the title + email changes atomic with the migration). Do NOT touch README in B1.
- `TESTING_GUIDE.md` — title + test-user email table are in **Brief 2** (same reason).
- `lib/rate-limit.js`, `package.json`, `package-lock.json`, `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` — all in **Brief 2** (infrastructure + emails).
- `components/Layout.js`, `components/AnimatedFireLogo.js`, `components/ShareModal.js`, `pages/index.js`, `pages/login.js` (welcome copy at lines 76 + 158), `pages/signup.js`, `styles/globals.css` — already render `Deck Hearth` correctly. Do NOT touch.
- **Repo-name references** (literal `tcg-vault` describing the GitHub repo / Vercel project / directory): `README.md` lines 30 + 105, `AGENTS.md` line 1, `.github/workflows/ci.yml` lines 12 + 123, `.github/workflows/visual-diff.yml` line 5, `.agent-context-manifest.yml` lines 84/89/99/104/109/114. These stay byte-identical — the queued `rename-repo-and-vercel-project` convoy will sweep them when the repo + Vercel project are actually renamed. Per `.convoys/pick-a-name.md` § *"Keep the repo `tcg-vault` for now"*. Do NOT rename in this brief.
- **AGENTS.md Gotcha #4** (line 53, `admin@tcgvault.com` reference) and **AGENTS.md Gotcha #12** (rate-limit env-var explanation) — the **doc-writer pass at convoy close** updates these to reflect the post-migration admin email + post-rename Redis prefix. Do NOT preempt that edit here.
- **AGENTS.md line 1** (`# AGENTS.md — AI collaboration (tcg-vault)`) and **AGENTS.md line 13** (Project overview `admin@tcgvault.com` mention) — repo-name reference (line 1) and email reference (line 13). Line 1 stays per the repo-rename scoping rule above; line 13 is updated by the doc-writer pass at convoy close (same as Gotcha #4) because it's email-bound and would otherwise contradict B2's migration outcome. Do NOT preempt those edits here.
- **`.convoys/**` history files** — append-only per `.cursor/rules/no-go-zones.mdc`. The 30+ `tcgvault` / `Deck Hearth` references inside `.convoys/` describe historical state at write-time and stay byte-identical. Doc-writer pass at this convoy's close ADDS a new "As-shipped" section to `.convoys/pick-a-name.md`; it does NOT rewrite history in other convoy files.
- **`scripts/seed-collections-*.js`, `scripts/add-*.js`, `scripts/fix-*.js`, `components/Layout.js.backup`** — historical / legacy per `.cursor/rules/no-go-zones.mdc` § Append-only / historical. Do NOT touch.
- **`pages/login.js` quick-login fixture emails (lines 172, 184)** — these are in **Brief 2** because the email rename must coordinate with the seed scripts + migration. Do NOT touch login.js in B1.
- **`tests/**`, `test/**`** — smoke spec, visual spec, and unit tests are untouched by B1. `test/lib/permission-middleware.test.js` line 87's negative regression assertion is in B2 (email-bound). The Playwright smoke spec's text matchers (`'sign in'` regex, `/api/health`) are brand-agnostic — verified at architect time.
## Conventions to follow
### Decisions from the convoy file (cite when implementing)
- **D1 (operator-ratified at gate-1):** The canonical display string is **"Deck Hearth"** — two words, internal cap, space-separated. Use this for every user-facing-style replacement (titles, branding notes, descriptions).
- **D2 (operator-ratified at gate-1):** The canonical kebab string is **`deck-hearth`** — used in `User-Agent` HTTP headers per the lowercase-hyphenated convention of HTTP user-agent product tokens (e.g. `Mozilla/5.0`, `curl/8.4.0`).
### Repo conventions (cite + match)
- **`.cursor/rules/no-go-zones.mdc`.** None of the 7 source files are listed under no-go zones. The "Card-import jobs" entry warns *"Don't run them ad-hoc against prod data; use staging"* — this brief edits only the User-Agent string sent in the HTTP request; the import behavior itself (Scryfall / Pokémon TCG / Lorcana API calls) is byte-identical post-edit.
- **`.cursor/rules/ui-and-theming.mdc`** § Branding — line 61 currently states *"The repo says 'TCG Vault' everywhere except `components/Layout.js`, which renders 'Deck Hearth' and 'DH' logo. A naming convoy is open. Until resolved, **do not introduce a third name** in new copy."* That advice expires the moment the operator ratifies D1; the verbatim replacement copy is in the per-file Acceptance criteria below.
- **No fail-loud test gate for this kind of edit.** There's no CI grep job that fails on `tcgvault` (we don't want one — convoy history references the literal). Validation is via per-file `git diff` review + the post-edit cross-file `Repo-wide grep clean` check in § Cross-file checks below.
## Acceptance criteria
### `AGENTS.md` (modified)
Replace line 5 — the branding note — with a one-line post-decision callout. Keep every other line byte-identical.
**Current line 5:**
```
> Branding note: the repo, README, and seed data say "TCG Vault" and `admin@tcgvault.com`, but the Layout component renders "Deck Hearth". Pick one before launch — see `.convoys/` for tracking.
```
**Replace with:**
```
> Branding note: **Deck Hearth** is the canonical product brand (ratified 2026-05-24 in the `pick-a-name` convoy). The repo directory name `tcg-vault` stays for now — see queued `rename-repo-and-vercel-project`. The seed admin email migrated from `admin@tcgvault.com` to `admin@deckhearth.com` in the same convoy; see Gotcha #4 + `.convoys/pick-a-name.md` for the rotation flow.
```
Acceptance:
- [ ] Line 5 matches the verbatim replacement above (including the leading `>` blockquote marker, the two `**`-emphasis spans, the four backtick-quoted identifiers, and the trailing period).
- [ ] Lines 1-4 and lines 6-end stay byte-identical. **Do NOT touch line 1** (`# AGENTS.md — AI collaboration (tcg-vault)` — repo-name reference, scoped to `rename-repo-and-vercel-project` convoy).
- [ ] **Do NOT touch line 13** (Project overview Auth bullet's `admin@tcgvault.com` mention) — the doc-writer pass at convoy close updates that line together with Gotcha #4 to reflect the post-migration admin email; preempting here would contradict B2's migration outcome and create merge conflicts.
- [ ] **Do NOT touch Gotcha #4 (line 53)** or **Gotcha #12 (line 61-77)** — those are doc-writer pass at convoy close.
- [ ] Net diff: 1 line changed, 0 lines added, 0 lines deleted.
### `.cursor/rules/ui-and-theming.mdc` (modified)
Replace line 61 — the "Branding" subsection content — with a post-decision sentence. Keep the heading `## Branding` (the line ABOVE it, line 60 in the unedited file) byte-identical.
**Current line 61:**
```
The repo says "TCG Vault" everywhere except `components/Layout.js`, which renders "Deck Hearth" and "DH" logo. A naming convoy is open. Until resolved, **do not introduce a third name** in new copy.
```
**Replace with:**
```
The canonical product brand is **Deck Hearth** (two words, internal cap), ratified 2026-05-24 in the `pick-a-name` convoy. The repo directory + GitHub project name remain `tcg-vault` until the queued `rename-repo-and-vercel-project` convoy ships. New UI copy MUST use `Deck Hearth` verbatim — do not introduce a third name, do not abbreviate to "DH" outside the logo glyph (currently used in `components/Layout.js` lines 621, 714 as the sidebar logo monogram).
```
Acceptance:
- [ ] Line 61 matches the verbatim replacement above.
- [ ] Every other line in the file stays byte-identical.
- [ ] Net diff: 1 line changed, 0 lines added, 0 lines deleted.
- [ ] The two referenced line numbers in the new copy (621, 714) are architect-verified against the current `components/Layout.js` — those are the two `<span className="text-white font-bold text-sm">DH</span>` lines.
### `.cursor/rules/auth-and-permissions.mdc` (modified)
Replace line 2 — the frontmatter `description:` value — with the new product name. Keep lines 1 (`---`), 3 (`globs: ...`), and 4 (`---`) byte-identical.
**Current line 2:**
```
description: Auth model + permission model for tcg-vault (JWT + collection roles)
```
**Replace with:**
```
description: Auth model + permission model for Deck Hearth (JWT + collection roles)
```
Acceptance:
- [ ] Line 2 matches the verbatim replacement above (no trailing whitespace).
- [ ] Frontmatter delimiters and `globs:` line stay byte-identical.
- [ ] Every line below the frontmatter (`# Auth + permissions` heading down) stays byte-identical. **In particular, do NOT touch the body's `tcgvault` references** — there are none in this file outside the frontmatter description.
- [ ] Net diff: 1 line changed.
### `scripts/README.md` (modified)
Replace line 1 — the title. Keep every other line byte-identical (line 3 contains the generic phrase `TCG card data` referring to trading-card-game card data, NOT the product brand "TCG Vault"; preserve unchanged).
**Current line 1:**
```
# TCG Vault Bulk Import Scripts
```
**Replace with:**
```
# Deck Hearth Bulk Import Scripts
```
Acceptance:
- [ ] Line 1 matches the verbatim replacement above.
- [ ] Line 3 (`This directory contains scripts for bulk importing TCG card data into the database.`) stays byte-identical — `TCG card data` is the generic acronym for "trading card game", NOT the product brand.
- [ ] Every other line stays byte-identical.
- [ ] Net diff: 1 line changed.
### `pages/api/cards/import-lorcana.js` (modified)
Replace the User-Agent product token on line 14. This is the external HTTP identity sent to the Lorcana API and is part of our brand contract with the upstream service (Lorcana operators may rate-limit or contact us via this UA — better that they see the current brand name).
**Current line 14:**
```js
'User-Agent': 'TCG-Vault/1.0',
```
**Replace with:**
```js
'User-Agent': 'Deck-Hearth/1.0',
```
Acceptance:
- [ ] Line 14 matches the verbatim replacement above (preserves the surrounding 10-space indent, the trailing comma, the version `1.0`).
- [ ] No other line in the file changes. **Specifically:** the auth check (line 2 `getUserFromRequest` import), the rate-limit check (line 3 `checkImportRateLimit` import), the `delay()` helper, `fetchWithRetry()` retry logic, and the handler body all stay byte-identical. The `add-rate-limiting` Brief 1 contract for this file (auth → admin-role → import rate-limit gate ordering) is preserved.
- [ ] Net diff: 1 line changed.
### `pages/api/cards/import-pokemon.js` (modified)
Same change as `import-lorcana.js` — replace the User-Agent product token on line 14.
**Current line 14:**
```js
'User-Agent': 'TCG-Vault/1.0',
```
**Replace with:**
```js
'User-Agent': 'Deck-Hearth/1.0',
```
Acceptance:
- [ ] Line 14 matches the verbatim replacement above.
- [ ] No other line in the file changes (including the Pokémon TCG fetch + INSERT loop, the `fetchWithRetry` + `delay` helpers, and the `add-rate-limiting`-installed gates).
- [ ] Net diff: 1 line changed.
### `scripts/import-lorcana.js` (modified — standalone CLI variant)
Replace the User-Agent product token on line 20. This is the CLI-script variant of the Lorcana importer (independent of the API route); per `.cursor/rules/no-go-zones.mdc` § "Card-import jobs" the script runs against external APIs with rate limits — same brand-identity argument as the API route.
**Current line 20:**
```js
'User-Agent': 'TCG-Vault/1.0',
```
**Replace with:**
```js
'User-Agent': 'Deck-Hearth/1.0',
```
Acceptance:
- [ ] Line 20 matches the verbatim replacement above.
- [ ] No other line in the file changes.
- [ ] Net diff: 1 line changed.
### Cross-file checks
- [ ] **`npm run lint` exit code unchanged.** The baseline is whatever the convoy/pick-a-name branch tip currently reports — none of B1's edits introduce ESLint surface (no new code, no removed imports, no new syntax). If the count changes by even 1, investigate before commit.
- [ ] **`npm run test:run` (vitest) passes 21/21.** No test file is touched in B1. Re-run after the sweep to confirm.
- [ ] **`npm run build` exit 0.** Turbopack should compile cleanly — all edits are string literals or markdown.
- [ ] **Smoke spec still passes 3/3** (`npm run test:smoke` against a preview, or skip locally — the smoke spec's text matchers are `'sign in'` regex + `/api/health` + HTTP status < 500, brand-agnostic).
- [ ] **Repo-wide grep check (post-B1, expect B2 still pending).** After running B1, the following `rg` invocations should show ONLY the still-pending B2 surfaces (no B1 surfaces should remain):
```bash
# B1 should have eliminated these:
rg "TCG-Vault/1.0" .
```
Expected: **zero matches**. (Architect verified pre-B1: 3 matches — the three User-Agent strings touched by B1.)
```bash
rg '"TCG Vault"' AGENTS.md .cursor/rules/ scripts/README.md
```
Expected: **zero matches** in the listed paths. (Architect verified pre-B1: 1 match in `AGENTS.md` line 5, 1 match in `.cursor/rules/ui-and-theming.mdc` line 61, 0 in `scripts/README.md` — the `# TCG Vault Bulk Import Scripts` title is unquoted.)
```bash
rg "for tcg-vault" .cursor/rules/
```
Expected: **zero matches**. (Architect verified pre-B1: 1 match in `auth-and-permissions.mdc` line 2.)
- [ ] **No collateral damage in `.convoys/`.** The 30+ historical references stay byte-identical:
```bash
rg "TCG Vault|tcgvault|tcg-vault" .convoys/ | wc -l
```
Expected: **same count as pre-B1** (architect-verified: ~25 lines across `.convoys/`).
- [ ] **No collateral damage in already-correct files.** The seven `Deck Hearth`-correct files (`components/Layout.js`, `components/AnimatedFireLogo.js`, `components/ShareModal.js`, `pages/index.js`, `pages/login.js` welcome copy, `pages/signup.js`, `styles/globals.css`) stay byte-identical:
```bash
git diff --stat -- components/Layout.js components/AnimatedFireLogo.js components/ShareModal.js pages/index.js pages/signup.js styles/globals.css
```
Expected: **no diff** for those six. (`pages/login.js` is touched by B2 — that's the only listed file with expected B2-side changes.)
- [ ] **Diff hygiene.** `git diff main..HEAD --stat` (after B1 commit only) should show:
- `AGENTS.md`: 1 line + / 1 line -.
- `.cursor/rules/ui-and-theming.mdc`: 1 line + / 1 line -.
- `.cursor/rules/auth-and-permissions.mdc`: 1 line + / 1 line -.
- `scripts/README.md`: 1 line + / 1 line -.
- `pages/api/cards/import-lorcana.js`: 1 line + / 1 line -.
- `pages/api/cards/import-pokemon.js`: 1 line + / 1 line -.
- `scripts/import-lorcana.js`: 1 line + / 1 line -.
- **No whitespace-only changes elsewhere.**
- Total: ~7 lines + / ~7 lines -.
## Manual verification (in addition to CI on push)
- [ ] **Visual sanity.** Open `AGENTS.md` and `.cursor/rules/ui-and-theming.mdc` in an editor. The branding-note paragraphs should read coherently — no orphaned "see `.convoys/` for tracking" reference left after the rewrite.
- [ ] **Import job smoke (optional, against staging only).** If a staging Postgres is available, run one of the gated import jobs (`POST /api/cards/import-mtg` with an admin token + a small set code like `neo`) and confirm the response succeeds. The User-Agent change is observed by the upstream API, not by the response — but the request should still 200 if our auth + rate-limit + admin-role chain is intact. **Skip if no staging is available.** Per `.cursor/rules/no-go-zones.mdc` § Card-import jobs: do NOT run against prod.
- [ ] **Read the convoy file's § "Boot-the-brief findings" below before commit** to make sure no architect note was missed.
## Boot-the-brief findings (preempted by the architect; do not re-investigate)
### Finding 1 — None of the 7 B1 files have NPM dependency churn
Architect-verified: every edit in B1 is a string-literal replacement inside an already-committed file. Zero new imports, zero new dependencies, zero version pin changes. `package.json` and `package-lock.json` are NOT in B1's `files:` (they're in B2).
### Finding 2 — User-Agent rename is observable to external services (not user-facing)
The three User-Agent strings on line 14 of `import-lorcana.js`, line 14 of `import-pokemon.js`, and line 20 of `scripts/import-lorcana.js` are sent in the `fetch()` headers to the respective upstream APIs. They identify OUR app to the upstream — Scryfall (MTG; note: `import-mtg.js` does NOT set a custom UA, falling back to Node's default), Pokémon TCG, and the Lorcana API. Renaming aligns with the post-D1 brand. Upstream API responses are not affected by the UA string change (they use the UA for logging / contact, not for routing).
**Architect ran `rg "User-Agent" pages/api/cards/ scripts/` at brief time** — exactly three matches (the three lines above). No fourth `import-mtg.js` UA setting was found; Scryfall is fine with Node's default UA.
### Finding 3 — AGENTS.md line 5 rewrite must NOT pre-empt Gotcha #4 / #13 updates
The convoy file's `.convoys/pick-a-name.md` § Known constraints explicitly notes that AGENTS.md line 5 is the "canonical historical record of the inconsistency" and that "the doc-writer pass MUST update it from 'Pick one before launch' to '<post-decision callout>'". This brief does the line-5 callout rewrite; the doc-writer pass at convoy close adds an "As-shipped" milestone entry to `.convoys/pick-a-name.md` AND updates Gotcha #4's admin-email caveat to reflect the migration outcome AND adds a NEW Gotcha #13 (or amends #12) for the Redis prefix rename. **Do NOT preempt those Gotcha edits in B1.** The line-5 callout rewrite is the line that needs to "stop saying the brand is unresolved" — that's what unlocks the rest of the doc-writer's milestone copy.
### Finding 4 — `.cursor/rules/auth-and-permissions.mdc` line 2 is a YAML frontmatter description, not body markdown
Architect-verified by reading lines 1-4 of the file: it's standard Cursor `.mdc` rule frontmatter. The `description:` field shows in the rule-picker UI when a user is browsing rules. Changing it from `tcg-vault` to `Deck Hearth` updates the human-facing tag in the rule picker. **Do NOT add a body-markdown edit** chasing other `tcgvault` strings in the file body — architect ran `rg "tcgvault" .cursor/rules/auth-and-permissions.mdc` and found ZERO matches outside line 2.
### Finding 5 — `scripts/README.md` line 3's "TCG card data" is the GENERIC acronym, not the product
Architect read line 3 in full: `"This directory contains scripts for bulk importing TCG card data into the database."``TCG` here is "trading card game" (the industry generic), NOT "TCG Vault" the product. Architect verified this elsewhere in the file: lines 14, 18, 29, 33 use "TCGs", "Magic", "Pokemon", "Lorcana" all in the trading-card-game generic sense. **Only line 1 (the title) is the product-brand reference.** Replace line 1 only.
### Finding 6 — Workflow YAML comments referring to "tcg-vault" describe the literal repo, not the product
`.github/workflows/ci.yml` line 12 (`# NOTE: tcg-vault is JavaScript (not TypeScript). …`) and line 123 (`# The tcg-vault frontend and API are served from the same Vercel deployment …`), and `.github/workflows/visual-diff.yml` line 5 (`# Paths are tcg-vault-specific …`) all use `tcg-vault` as the literal repo / deployment name. Per the convoy seed § *"Keep the repo `tcg-vault` for now"* and § *"GitHub rename is a separate concern … track as a queued follow-up convoy `rename-repo-and-vercel-project`"*, these stay byte-identical. **Do NOT add `.github/workflows/*.yml` to B1's `files:`** — they're out of scope.
### Finding 7 — `.agent-context-manifest.yml` `source: "tcg-vault-local"` tags stay (internal sync-tracking)
Architect read the manifest: 6 entries reference `source: "tcg-vault-local"` as an internal source identifier for the `sync-agent-context` skill's drift detection. The skill compares an installed artifact's content against the "tcg-vault-local" source to detect drift. Renaming the source tag to `deck-hearth-local` could break the sync skill's behavior (it tracks "what shipped from this source"); the rename belongs to `rename-repo-and-vercel-project` when the actual repo name changes. **Do NOT touch `.agent-context-manifest.yml` in B1.**
## Out of scope (do not do these)
- [ ] Do NOT edit README.md, TESTING_GUIDE.md, lib/rate-limit.js, package.json, package-lock.json, scripts/setup-neon-db.js, scripts/reset-db.js, scripts/create-test-users.js, pages/login.js, or test/lib/permission-middleware.test.js — all in B2.
- [ ] Do NOT touch any file under `.convoys/` (append-only history per no-go-zones).
- [ ] Do NOT touch `components/Layout.js.backup` (legacy snapshot per no-go-zones).
- [ ] Do NOT touch `scripts/seed-collections-*.js`, `scripts/add-*.js`, or `scripts/fix-*.js` (historical migration jobs per no-go-zones).
- [ ] Do NOT touch repo-name references (`README.md` cd / tree, `AGENTS.md` line 1, `.github/workflows/*.yml`, `.agent-context-manifest.yml` source tags) — those are in `rename-repo-and-vercel-project`.
- [ ] Do NOT touch AGENTS.md Gotcha #4 / Gotcha #12 — doc-writer pass at convoy close.
- [ ] Do NOT touch AGENTS.md line 1 (`# AGENTS.md — AI collaboration (tcg-vault)`) or line 13 (project overview Auth bullet's `admin@tcgvault.com` mention).
- [ ] Do NOT preempt visual-baseline regeneration. `tests/visual/__screenshots__/` does not exist; the queued `seed-visual-baselines-on-linux` convoy seeds it AFTER `pick-a-name` lands.
- [ ] Do NOT introduce a new file (no migration scripts, no helper modules). All B1 edits are in-place.
- [ ] Do NOT bump any dependency or run `npm install` (that's a B2 step for the package.json name change).
## Rationale (≤3 sentences)
B1 is the lowest-risk, lowest-blast slice of the convoy: 7 files, ~7 lines net, no infrastructure changes, no test or DB impact — purely the in-repo display / comment / external-HTTP-User-Agent strings that match the operator-ratified D1 ("Deck Hearth") and D2 (`deck-hearth`) decisions. Splitting it from B2 keeps the email-migration + Redis-prefix-rename + lockfile-regen blast radius isolated, AND lets the conductor dispatch both briefs in parallel via `/multitask` because their `files:` sets are disjoint. The only soft scope edge — repo-literal references like `cd tcg-vault` — is explicitly deferred to the queued `rename-repo-and-vercel-project` convoy so this brief stays mechanical.

View file

@ -0,0 +1,841 @@
---
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-<slug>.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 `<AuthLayout>` 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 `<name>@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":"<your-admin-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-<slug>.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 `<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`, or `scripts/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.yml` source tags) — that's `rename-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-helpers` scope.
- [ ] Do NOT touch `TESTING_GUIDE.md`'s weak password literals (`admin123`, `alice123`, `bob123`) — `purge-weak-creds-from-helpers` scope.
- [ ] Do NOT add `KV_REST_API_*` env vars to `test/setup.js`. The warn-and-noop branch is the correct test behavior. The Redis prefix rename doesn't change this.
- [ ] Do NOT bump `@upstash/ratelimit` or `@upstash/redis` versions. Pins stay at `^2.0.8` and `^1.38.0`.
- [ ] Do NOT add Lorcana to the `<select>` in `pages/admin/card-import.js` (that's the `add-rate-limiting` Brief 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 via `node 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-linux` convoy AFTER `pick-a-name` lands.
- [ ] 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.

View file

@ -1,5 +1,5 @@
--- ---
description: Auth model + permission model for tcg-vault (JWT + collection roles) description: Auth model + permission model for Deck Hearth (JWT + collection roles)
globs: pages/api/**/*.js,lib/*.js,components/*.js,pages/*.js globs: pages/api/**/*.js,lib/*.js,components/*.js,pages/*.js
--- ---

View file

@ -58,4 +58,4 @@ Don't duplicate navigation in a page — extend `NavigationContent` inside Layou
## Branding ## Branding
The repo says "TCG Vault" everywhere except `components/Layout.js`, which renders "Deck Hearth" and "DH" logo. A naming convoy is open. Until resolved, **do not introduce a third name** in new copy. The canonical product brand is **Deck Hearth** (two words, internal cap), ratified 2026-05-24 in the `pick-a-name` convoy. The repo directory + GitHub project name remain `tcg-vault` until the queued `rename-repo-and-vercel-project` convoy ships. New UI copy MUST use `Deck Hearth` verbatim — do not introduce a third name, do not abbreviate to "DH" outside the logo glyph (currently used in `components/Layout.js` lines 621, 714 as the sidebar logo monogram).

View file

@ -2,7 +2,7 @@
Guidance for agents and humans working in this repo. Prefer existing patterns over new abstractions. Guidance for agents and humans working in this repo. Prefer existing patterns over new abstractions.
> Branding note: the repo, README, and seed data say "TCG Vault" and `admin@tcgvault.com`, but the Layout component renders "Deck Hearth". Pick one before launch — see `.convoys/` for tracking. > Branding note: **Deck Hearth** is the canonical product brand (ratified 2026-05-24 in the `pick-a-name` convoy). The repo directory name `tcg-vault` stays for now — see queued `rename-repo-and-vercel-project`. The seed admin email migrated from `admin@tcgvault.com` to `admin@deckhearth.com` in the same convoy; see Gotcha #4 + `.convoys/pick-a-name.md` for the rotation flow.
## 1. Project overview ## 1. Project overview

View file

@ -1,4 +1,4 @@
# TCG Vault # Deck Hearth
A modern trading card game collection manager built with Next.js and Neon Database. A modern trading card game collection manager built with Next.js and Neon Database.
@ -129,7 +129,7 @@ variable is unset or empty.
- **CI / Vercel:** set `ADMIN_INITIAL_PASSWORD` as a project secret if setup - **CI / Vercel:** set `ADMIN_INITIAL_PASSWORD` as a project secret if setup
ever runs from CI. The env var is **only** read by the seed script; runtime ever runs from CI. The env var is **only** read by the seed script; runtime
auth uses the per-user password stored in the database. auth uses the per-user password stored in the database.
- **Admin email:** the seed creates `admin@tcgvault.com`. Change the password - **Admin email:** the seed creates `admin@deckhearth.com`. Change the password
immediately after first login via the app's profile settings. immediately after first login via the app's profile settings.
> **Operators of envs that pre-date this change:** `npm run setup-db` is > **Operators of envs that pre-date this change:** `npm run setup-db` is
@ -140,6 +140,17 @@ variable is unset or empty.
> after logging in, or wait for the queued `rotate-default-admin` follow-up > after logging in, or wait for the queued `rotate-default-admin` follow-up
> convoy. > convoy.
> **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.
## 🤝 Contributing ## 🤝 Contributing
1. Fork the repository 1. Fork the repository

View file

@ -1,12 +1,12 @@
# 🎯 TCG Vault Collaboration Testing Guide # 🎯 Deck Hearth Collaboration Testing Guide
## 👥 Test Accounts ## 👥 Test Accounts
| User | Email | Password | Role | | User | Email | Password | Role |
|------|-------|----------|------| |------|-------|----------|------|
| Admin | `admin@tcgvault.com` | `admin123` | Admin | | Admin | `admin@deckhearth.com` | `admin123` | Admin |
| Alice | `alice@tcgvault.com` | `alice123` | User | | Alice | `alice@deckhearth.com` | `alice123` | User |
| Bob | `bob@tcgvault.com` | `bob123` | User | | Bob | `bob@deckhearth.com` | `bob123` | User |
## 🃏 Sample Cards Available ## 🃏 Sample Cards Available
@ -21,7 +21,7 @@
### 1. **Login as Alice** ### 1. **Login as Alice**
``` ```
Email: alice@tcgvault.com Email: alice@deckhearth.com
Password: alice123 Password: alice123
``` ```
@ -43,14 +43,14 @@ Password: alice123
### 4. **Invite Bob as Collaborator** ### 4. **Invite Bob as Collaborator**
- Click "Invite Collaborator" button - Click "Invite Collaborator" button
- Enter: `bob@tcgvault.com` - Enter: `bob@deckhearth.com`
- Role: Collaborator (default) - Role: Collaborator (default)
- Message: "Help me build this Pokemon collection!" - Message: "Help me build this Pokemon collection!"
- Click "Send Invitation" - Click "Send Invitation"
### 5. **Switch to Bob's Account** ### 5. **Switch to Bob's Account**
- Logout and login as Bob - Logout and login as Bob
- Email: `bob@tcgvault.com` - Email: `bob@deckhearth.com`
- Password: `bob123` - Password: `bob123`
### 6. **Accept Invitation (Simulated)** ### 6. **Accept Invitation (Simulated)**

View file

@ -7,11 +7,11 @@ import { Redis } from '@upstash/redis';
// classes to match Brief 4's existing algorithm; switching to // classes to match Brief 4's existing algorithm; switching to
// `tokenBucket` per-class would be its own convoy. // `tokenBucket` per-class would be its own convoy.
const LIMITER_CONFIG = { const LIMITER_CONFIG = {
auth: { limit: 5, window: '15 m', prefix: 'tcgvault:auth' }, auth: { limit: 5, window: '15 m', prefix: 'deckhearth:auth' },
search: { limit: 60, window: '1 m', prefix: 'tcgvault:search' }, search: { limit: 60, window: '1 m', prefix: 'deckhearth:search' },
upload: { limit: 10, window: '1 h', prefix: 'tcgvault:upload' }, upload: { limit: 10, window: '1 h', prefix: 'deckhearth:upload' },
generate: { limit: 5, window: '1 h', prefix: 'tcgvault:generate' }, generate: { limit: 5, window: '1 h', prefix: 'deckhearth:generate' },
import: { limit: 5, window: '1 h', prefix: 'tcgvault:import' }, import: { limit: 5, window: '1 h', prefix: 'deckhearth:import' },
}; };
// Lazy singleton. Module-load init would throw in environments without // Lazy singleton. Module-load init would throw in environments without

4
package-lock.json generated
View file

@ -1,11 +1,11 @@
{ {
"name": "tcg-vault", "name": "deck-hearth",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "tcg-vault", "name": "deck-hearth",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@neondatabase/serverless": "^1.0.1", "@neondatabase/serverless": "^1.0.1",

View file

@ -1,5 +1,5 @@
{ {
"name": "tcg-vault", "name": "deck-hearth",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",

View file

@ -11,7 +11,7 @@ async function fetchWithRetry(url, maxRetries = 3, delayMs = 2000) {
try { try {
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
'User-Agent': 'TCG-Vault/1.0', 'User-Agent': 'Deck-Hearth/1.0',
'Accept': 'application/json' 'Accept': 'application/json'
}, },
timeout: 30000 // 30 second timeout timeout: 30000 // 30 second timeout

View file

@ -11,7 +11,7 @@ async function fetchWithRetry(url, maxRetries = 3, delayMs = 2000) {
try { try {
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
'User-Agent': 'TCG-Vault/1.0', 'User-Agent': 'Deck-Hearth/1.0',
'Accept': 'application/json' 'Accept': 'application/json'
}, },
timeout: 30000 // 30 second timeout timeout: 30000 // 30 second timeout

View file

@ -169,7 +169,7 @@ export default function Login() {
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<button <button
type="button" type="button"
onClick={() => handleQuickLogin('alice@tcgvault.com', 'alice123')} onClick={() => handleQuickLogin('alice@deckhearth.com', 'alice123')}
className="px-4 py-2 rounded-lg text-sm font-medium border border-opacity-20 transition-all duration-200 hover:shadow-md backdrop-blur-sm hover:bg-opacity-80" className="px-4 py-2 rounded-lg text-sm font-medium border border-opacity-20 transition-all duration-200 hover:shadow-md backdrop-blur-sm hover:bg-opacity-80"
style={{ style={{
backgroundColor: 'rgba(var(--bg-primary-rgb), 0.6)', backgroundColor: 'rgba(var(--bg-primary-rgb), 0.6)',
@ -181,7 +181,7 @@ export default function Login() {
</button> </button>
<button <button
type="button" type="button"
onClick={() => handleQuickLogin('bob@tcgvault.com', 'bob123')} onClick={() => handleQuickLogin('bob@deckhearth.com', 'bob123')}
className="px-4 py-2 rounded-lg text-sm font-medium border border-opacity-20 transition-all duration-200 hover:shadow-md backdrop-blur-sm hover:bg-opacity-80" className="px-4 py-2 rounded-lg text-sm font-medium border border-opacity-20 transition-all duration-200 hover:shadow-md backdrop-blur-sm hover:bg-opacity-80"
style={{ style={{
backgroundColor: 'rgba(var(--bg-primary-rgb), 0.6)', backgroundColor: 'rgba(var(--bg-primary-rgb), 0.6)',

View file

@ -1,4 +1,4 @@
# TCG Vault Bulk Import Scripts # Deck Hearth Bulk Import Scripts
This directory contains scripts for bulk importing TCG card data into the database. This directory contains scripts for bulk importing TCG card data into the database.

View file

@ -15,25 +15,25 @@ async function createTestUsers() {
const alicePassword = await bcrypt.hash('alice123', 12); const alicePassword = await bcrypt.hash('alice123', 12);
await sql` await sql`
INSERT INTO users (email, password, role) INSERT INTO users (email, password, role)
VALUES ('alice@tcgvault.com', ${alicePassword}, 'user') VALUES ('alice@deckhearth.com', ${alicePassword}, 'user')
ON CONFLICT (email) DO NOTHING ON CONFLICT (email) DO NOTHING
`; `;
console.log('✅ Created Alice (alice@tcgvault.com / alice123)'); console.log('✅ Created Alice (alice@deckhearth.com / alice123)');
// Create Bob (collaborator) // Create Bob (collaborator)
const bobPassword = await bcrypt.hash('bob123', 12); const bobPassword = await bcrypt.hash('bob123', 12);
await sql` await sql`
INSERT INTO users (email, password, role) INSERT INTO users (email, password, role)
VALUES ('bob@tcgvault.com', ${bobPassword}, 'user') VALUES ('bob@deckhearth.com', ${bobPassword}, 'user')
ON CONFLICT (email) DO NOTHING ON CONFLICT (email) DO NOTHING
`; `;
console.log('✅ Created Bob (bob@tcgvault.com / bob123)'); console.log('✅ Created Bob (bob@deckhearth.com / bob123)');
console.log('\n🎉 Test users created successfully!'); console.log('\n🎉 Test users created successfully!');
console.log('\n👥 Available Test Accounts:'); console.log('\n👥 Available Test Accounts:');
console.log(' 1. admin@tcgvault.com / admin123 (Admin)'); console.log(' 1. admin@deckhearth.com / admin123 (Admin)');
console.log(' 2. alice@tcgvault.com / alice123 (User)'); console.log(' 2. alice@deckhearth.com / alice123 (User)');
console.log(' 3. bob@tcgvault.com / bob123 (User)'); console.log(' 3. bob@deckhearth.com / bob123 (User)');
} catch (error) { } catch (error) {
console.error('❌ Failed to create test users:', error.message); console.error('❌ Failed to create test users:', error.message);

View file

@ -17,7 +17,7 @@ async function fetchWithRetry(url, maxRetries = 3, delayMs = 2000) {
try { try {
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
'User-Agent': 'TCG-Vault/1.0', 'User-Agent': 'Deck-Hearth/1.0',
'Accept': 'application/json' 'Accept': 'application/json'
}, },
timeout: 30000 // 30 second timeout timeout: 30000 // 30 second timeout

View file

@ -0,0 +1,85 @@
#!/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);
});

View file

@ -144,7 +144,7 @@ async function resetDatabase() {
await sql` await sql`
INSERT INTO users (email, password, role) INSERT INTO users (email, password, role)
VALUES (${'admin@tcgvault.com'}, ${hashedPassword}, ${'admin'}) VALUES (${'admin@deckhearth.com'}, ${hashedPassword}, ${'admin'})
`; `;
console.log('✅ Created admin user'); console.log('✅ Created admin user');
@ -152,7 +152,7 @@ async function resetDatabase() {
console.log(''); console.log('');
console.log('📋 Database Details:'); console.log('📋 Database Details:');
console.log(' Database: Neon PostgreSQL'); console.log(' Database: Neon PostgreSQL');
console.log(' Admin User: admin@tcgvault.com'); console.log(' Admin User: admin@deckhearth.com');
console.log(' Admin Password: admin123'); console.log(' Admin Password: admin123');
} catch (error) { } catch (error) {

View file

@ -145,7 +145,7 @@ async function setupNeonDatabase() {
await sql` await sql`
INSERT INTO users (email, password, role) INSERT INTO users (email, password, role)
VALUES (${'admin@tcgvault.com'}, ${hashedPassword}, ${'admin'}) VALUES (${'admin@deckhearth.com'}, ${hashedPassword}, ${'admin'})
ON CONFLICT (email) DO NOTHING ON CONFLICT (email) DO NOTHING
`; `;
console.log('✅ Created admin user'); console.log('✅ Created admin user');
@ -154,7 +154,7 @@ async function setupNeonDatabase() {
console.log(''); console.log('');
console.log('📋 Database Details:'); console.log('📋 Database Details:');
console.log(' Database: Neon PostgreSQL'); console.log(' Database: Neon PostgreSQL');
console.log(' Admin user ready (email: admin@tcgvault.com)'); console.log(' Admin user ready (email: admin@deckhearth.com)');
console.log(''); console.log('');
console.log('🔧 Next Steps:'); console.log('🔧 Next Steps:');
console.log(' 1. Test the API endpoints'); console.log(' 1. Test the API endpoints');

View file

@ -82,6 +82,13 @@ describe('getUserFromRequest', () => {
it('does NOT return the synthetic admin shape when no Authorization header is present (Brief 2 regression lock)', async () => { it('does NOT return the synthetic admin shape when no Authorization header is present (Brief 2 regression lock)', async () => {
const user = await getUserFromRequest({ headers: {} }); 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({ expect(user).not.toEqual({
userId: 1, userId: 1,
email: 'admin@tcgvault.com', email: 'admin@tcgvault.com',