docs: post-convoy cleanup for 7-convoy 2026-05-26 wave #33
9 changed files with 708 additions and 75 deletions
|
|
@ -152,4 +152,77 @@ premise).
|
||||||
|
|
||||||
## As-shipped
|
## As-shipped
|
||||||
|
|
||||||
_To be filled in post-merge._
|
Single squash commit `171f5af` (PR #28, merged 2026-05-27T03:53:29Z
|
||||||
|
UTC / local 2026-05-26). Parent-owned end-to-end per the convoy spec
|
||||||
|
— no architect, no implementer subagent dispatched. Single-prop
|
||||||
|
removal in a leaf component, exactly as planned; no mid-execution
|
||||||
|
surprises.
|
||||||
|
|
||||||
|
**Diff: 3 files, +156 / -2.** `components/MobileNavigation.js` (1
|
||||||
|
line edit — removed `user` from the destructured props) +
|
||||||
|
`components/Layout.js` (1 attribute removal from the JSX call site —
|
||||||
|
removed `user={user}`) + `.convoys/cleanup-mobile-nav-dead-props.md`
|
||||||
|
(the planning document, committed atomically with the fix). The +156
|
||||||
|
addition figure is dominated by the planning doc; actual source diff
|
||||||
|
is 3 lines net.
|
||||||
|
|
||||||
|
**The two surface edits:**
|
||||||
|
|
||||||
|
1. **`components/MobileNavigation.js` line 5:** `export default function
|
||||||
|
MobileNavigation({ user, onMenuOpen }) {` → `export default function
|
||||||
|
MobileNavigation({ onMenuOpen }) {`.
|
||||||
|
2. **`components/Layout.js` lines 598-601:** removed the `user={user}`
|
||||||
|
JSX attribute from the only active `<MobileNavigation>` call site.
|
||||||
|
|
||||||
|
**Audit verdict pre-fix confirmed.** The pre-edit grep
|
||||||
|
(`rg '\buser\b' components/MobileNavigation.js`) returned 1 hit (the
|
||||||
|
destructure on line 5); post-edit grep returned 0 hits. The bottom-bar
|
||||||
|
items (Cards, Decks, Dashboard, Community, More) are static and don't
|
||||||
|
depend on auth state or role — the `user` prop was genuinely dead.
|
||||||
|
|
||||||
|
**`components/Layout.js.backup` left untouched** per the
|
||||||
|
`.cursor/rules/no-go-zones.mdc` § "Append-only / historical" rule
|
||||||
|
("legacy snapshot; delete with a real PR, never edit"). The backup
|
||||||
|
still references the old `MobileNavigation({ user, onMenuOpen })`
|
||||||
|
prop shape; when the `.backup` file is eventually deleted in a
|
||||||
|
separate convoy, that stale call disappears with it. This is the
|
||||||
|
documented out-of-scope handling.
|
||||||
|
|
||||||
|
**Verification (all gates green at merge):**
|
||||||
|
- `rg '\buser\b' components/MobileNavigation.js` → 0 hits post-edit
|
||||||
|
- `rg "MobileNavigation" components/ pages/ --type js` → 1 active
|
||||||
|
call site (`components/Layout.js`) passing only `onMenuOpen`, plus
|
||||||
|
the historical `.backup` reference (intentional)
|
||||||
|
- `npm run lint` → 125 problems (post-PR-#31 baseline preserved; no
|
||||||
|
new dead-code/unused-var warnings created by the change)
|
||||||
|
- `npm run test:run` → 21/21 pass. Specifically,
|
||||||
|
`test/components/Layout.test.js`'s 5 regression-lock assertions for
|
||||||
|
the logged-out Layout branch (Gotcha #8) continue to pass — they
|
||||||
|
don't assert on `MobileNavigation`'s prop shape, so the dead-prop
|
||||||
|
removal is invisible to the suite
|
||||||
|
- CI on PR #28: Lint ✓ | Vitest 21/21 ✓ | Playwright smoke 3/3 ✓ |
|
||||||
|
`forbidden-endpoints` ✓ | `forbidden-cors-headers` ✓ | Vercel
|
||||||
|
preview deploy ✓ | Aggregate gate ✓
|
||||||
|
- `Screenshot diff`: triggered (PR #28 touches `components/**` which
|
||||||
|
matches the visual-diff path filter) — `continue-on-error: true`
|
||||||
|
swallow per the documented Decision-4 end state of
|
||||||
|
`adopt-playwright-smoke` (no baseline committed yet). Not a
|
||||||
|
regression; the dead-prop removal cannot move a single rendered
|
||||||
|
pixel because the prop was never read.
|
||||||
|
|
||||||
|
**Out-of-scope items preserved as documented:**
|
||||||
|
- Pre-existing dead `import { useState } from 'react'` on
|
||||||
|
`components/MobileNavigation.js` line 3 — untouched per the convoy
|
||||||
|
spec's "single-prop removal" boundary. A future hygiene pass can
|
||||||
|
sweep it.
|
||||||
|
- `components/Layout.js.backup` — untouched per no-go-zone rule.
|
||||||
|
|
||||||
|
**Operator action required going forward:** **none.** No env vars, no
|
||||||
|
schema, no infra.
|
||||||
|
|
||||||
|
**Spec deviation:** none.
|
||||||
|
|
||||||
|
**No follow-up surfaced.** The convoy's documented R1 (hypothetical
|
||||||
|
future feature that wants per-user bottom-bar items) is an "accept
|
||||||
|
the cleanup, pay the add-the-prop cost when the feature actually
|
||||||
|
lands" call — not a queued follow-up.
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,91 @@ subagent required — proven-pattern follow-up to PR #25).
|
||||||
|
|
||||||
## As-shipped
|
## As-shipped
|
||||||
|
|
||||||
_(Stub — doc-writer to fill in post-merge with squash commit SHA,
|
Single squash commit `13d6210` (PR #29, merged 2026-05-27T03:53:31Z
|
||||||
PR URL, observed lint baseline before/after, CI gate results, and
|
UTC / local 2026-05-26). Parent-owned end-to-end per the convoy spec
|
||||||
any deviations from the planned shape.)_
|
— no architect, no implementer subagent dispatched. Single-file
|
||||||
|
ESLint config edit following a proven-pattern follow-up shape; no
|
||||||
|
mid-execution surprises.
|
||||||
|
|
||||||
|
**Diff: 2 files, +185 / -0.** `eslint.config.mjs` (the 7-line
|
||||||
|
flat-config block added after the existing `globalIgnores(...)` call,
|
||||||
|
plus a leading comment block referencing the two motivating bugs) +
|
||||||
|
`.convoys/lint-against-cjs-in-esm-scripts.md` (the planning document,
|
||||||
|
committed atomically with the rule).
|
||||||
|
|
||||||
|
**The change shipped exactly as designed.** A new flat-config block
|
||||||
|
appended to `eslint.config.mjs`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
files: ['scripts/**/*.js'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-syntax': ['error', {
|
||||||
|
selector: 'CallExpression[callee.name="require"]',
|
||||||
|
message: 'Use ESM `import` syntax. `package.json` has "type": "module"; require() throws ReferenceError at runtime. See .convoys/fix-reset-db-script.md.',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
The error message points at `.convoys/fix-reset-db-script.md` so a
|
||||||
|
future contributor / agent who trips the rule gets a 1-click path to
|
||||||
|
the exemplar ESM fix shape (top-level `import dotenv from 'dotenv'`,
|
||||||
|
`import { neon } from '@neondatabase/serverless'`, `import bcrypt from
|
||||||
|
'bcryptjs'`) instead of having to re-derive it.
|
||||||
|
|
||||||
|
`scripts/migrations/**` is already in `globalIgnores` (from
|
||||||
|
`pick-a-name` Brief 2's migration script) and stays ignored — the
|
||||||
|
rule does not fire there even though the migration script is ESM and
|
||||||
|
correctly uses `import`. No need to re-lint files already excluded.
|
||||||
|
|
||||||
|
**Verification (all gates green at merge):**
|
||||||
|
- `node --check eslint.config.mjs` → exit 0 (config parses)
|
||||||
|
- `npm run lint` → exit 1 with **125 problems** (post-PR-#31 baseline
|
||||||
|
preserved; **zero new false positives** in the current tree because
|
||||||
|
both motivating bugs were already fixed: `setup-neon-db.js` swept
|
||||||
|
by `drop-public-setup` B2 and `reset-db.js` swept by
|
||||||
|
`fix-reset-db-script` PR #25)
|
||||||
|
- **Negative test verified (apply, run, revert):** prepending
|
||||||
|
`const x = require('fs');` to `scripts/reset-db.js` fired the rule
|
||||||
|
at the expected line/column with the documented message; reverting
|
||||||
|
returned to a clean 125-problem lint. The rule starts with zero
|
||||||
|
positives to silence on day 1, which is the documented success
|
||||||
|
shape — preventative, not retroactive.
|
||||||
|
- `npm run test:run` → 21/21 pass (no test surface touched; verification
|
||||||
|
only)
|
||||||
|
- `rg "require\(" scripts/ --type js` → 0 hits (sanity check
|
||||||
|
confirming the current tree is clean)
|
||||||
|
- CI on PR #29: Lint ✓ | Vitest 21/21 ✓ | Playwright smoke 3/3 ✓ |
|
||||||
|
`forbidden-endpoints` ✓ | `forbidden-cors-headers` ✓ | Vercel
|
||||||
|
preview deploy ✓ | Aggregate gate ✓
|
||||||
|
- `Screenshot diff`: not triggered (PR #29 touches only
|
||||||
|
`eslint.config.mjs` + this convoy file — neither matches the
|
||||||
|
visual-diff `paths:` filter)
|
||||||
|
|
||||||
|
**Both motivating bugs WOULD have been caught at lint time.** Both
|
||||||
|
`drop-public-setup` Brief 2's pre-fix `scripts/setup-neon-db.js`
|
||||||
|
(three `require()` calls at lines 1-3 pre-fix) and `fix-reset-db-script`'s
|
||||||
|
pre-fix `scripts/reset-db.js` (three `require()` calls at lines 10,
|
||||||
|
12, 142 pre-fix) would have triggered the new rule at PR time
|
||||||
|
instead of throwing `ReferenceError: require is not defined` at
|
||||||
|
first execution. This is the exact "would have caught both bugs"
|
||||||
|
shape that motivated the queue entry in
|
||||||
|
`.convoys/ship-readiness.md`.
|
||||||
|
|
||||||
|
**Operator action required going forward:** **none.** The rule is
|
||||||
|
self-defending; no env vars, no secrets, no infra changes. Future
|
||||||
|
helper scripts under `scripts/**` that re-introduce CJS `require()`
|
||||||
|
fail at lint time with the documented message + the exemplar pointer
|
||||||
|
to `.convoys/fix-reset-db-script.md`.
|
||||||
|
|
||||||
|
**Spec deviation:** none. The flat-config block shipped exactly as
|
||||||
|
the convoy file's § The fix described it. Selector, message, scope
|
||||||
|
(`scripts/**/*.js` only — NOT all `.js`), and `globalIgnores`
|
||||||
|
interaction all match the spec verbatim.
|
||||||
|
|
||||||
|
**No follow-up surfaced.** Pairs naturally with the queued
|
||||||
|
`lint-against-lib-database` follow-up from `.convoys/single-sql-client.md`
|
||||||
|
(both are static-source guards added to `eslint.config.mjs`); they
|
||||||
|
could fold into a `harden-eslint-static-guards` convoy if more such
|
||||||
|
guards accumulate.
|
||||||
|
|
|
||||||
|
|
@ -489,7 +489,21 @@ migrations with multi-statement up()s), the operator's options:
|
||||||
|
|
||||||
## As-shipped
|
## As-shipped
|
||||||
|
|
||||||
Shipped 2026-05-26 on `convoy/migration-tool`.
|
Single squash commit `de9f334` (PR #32, merged 2026-05-27T04:01:59Z
|
||||||
|
UTC / local 2026-05-26). Parent-owned end-to-end per the § Subagent / multitask
|
||||||
|
footnote — no architect or implementer subagent dispatched. The
|
||||||
|
convoy spec pre-ratified each Decision's recommended path, and the
|
||||||
|
implementation surface was a small set of well-bounded file edits
|
||||||
|
following the spec's "The change (implementation shape)" checklist
|
||||||
|
verbatim. **AGENTS.md Gotcha #6 flipped from open → RESOLVED in this
|
||||||
|
same wave's post-convoy doc-writer pass.**
|
||||||
|
|
||||||
|
**Diff: 10 files, +1230 / -136.** The 1230-addition figure includes
|
||||||
|
`.convoys/migration-tool.md` (the planning document, ~600 lines,
|
||||||
|
committed atomically), `migrations/1779853647564_initial-schema.js`
|
||||||
|
(~155 lines for the backfill migration), the doc / rule / skill
|
||||||
|
edits, and `package-lock.json` churn for the `node-pg-migrate@^8.0.4`
|
||||||
|
+ `pg@^8.21.0` install (plus transitive deps).
|
||||||
|
|
||||||
### Decisions ratified at gate 1
|
### Decisions ratified at gate 1
|
||||||
|
|
||||||
|
|
@ -524,6 +538,12 @@ back through an architect bounce.
|
||||||
`node -e "import('./migrations/1779853647564_initial-schema.js').then(m => m.down())"` →
|
`node -e "import('./migrations/1779853647564_initial-schema.js').then(m => m.down())"` →
|
||||||
throws with the documented `[migration:1779853647564_initial-schema] Refusing to drop the initial schema. ...` message.
|
throws with the documented `[migration:1779853647564_initial-schema] Refusing to drop the initial schema. ...` message.
|
||||||
- `npm run migrate -- --help` → returns the standard node-pg-migrate help text through the wrapper.
|
- `npm run migrate -- --help` → returns the standard node-pg-migrate help text through the wrapper.
|
||||||
|
- CI on PR #32: Lint ✓ (128 problems at convoy time; the new lint baseline post-PR-#31 is 125 — see § What did NOT change below for the version note) | Vitest 21/21 ✓ | Playwright smoke 3/3 ✓ | `forbidden-endpoints` ✓ | `forbidden-cors-headers` ✓ | Vercel preview deploy ✓ | Aggregate gate ✓
|
||||||
|
- `Screenshot diff`: not triggered (PR #32's diff is `migrations/**` + `scripts/**` + `package.json` + docs / rules / `package-lock.json` — none of which matches the visual-diff `paths:` filter; the post-PR-#26 `!pages/api/**` exclusion is not even relevant here).
|
||||||
|
|
||||||
|
**Spec deviation:** none. All seven decisions landed verbatim from
|
||||||
|
the convoy spec's recommendations at gate 1. The verification gates
|
||||||
|
match § Verification plan exactly.
|
||||||
|
|
||||||
### Live verification status
|
### Live verification status
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -184,4 +184,106 @@ or implementer subagent required).
|
||||||
|
|
||||||
## As-shipped
|
## As-shipped
|
||||||
|
|
||||||
(stub — fill in post-merge)
|
Single squash commit `5f2b234` (PR #27, merged 2026-05-27T03:53:25Z
|
||||||
|
UTC / local 2026-05-26). Parent-owned end-to-end per the convoy spec
|
||||||
|
— no architect, no implementer subagent dispatched. Mirror-the-pattern
|
||||||
|
fix exactly as planned; no mid-execution surprises that would have
|
||||||
|
forced an architect bounce. **The umbrella `purge-weak-creds-from-helpers`
|
||||||
|
is now fully closed** — both remaining files swept, weak-cred surface
|
||||||
|
of the helper-script + manual-QA-doc lane is at zero.
|
||||||
|
|
||||||
|
**Diff: 3 files, +249 / -22.** `scripts/create-test-users.js` (the
|
||||||
|
alice/bob fixture script) + `TESTING_GUIDE.md` (the manual-QA doc
|
||||||
|
that pairs with it) + `.convoys/purge-weak-creds-from-helpers.md`
|
||||||
|
(the planning document, committed atomically with the fix).
|
||||||
|
|
||||||
|
**The two files atomically resolved:**
|
||||||
|
|
||||||
|
1. **`scripts/create-test-users.js`** — alice + bob fixtures previously
|
||||||
|
hardcoded `bcrypt.hash('alice123', 12)` + `bcrypt.hash('bob123', 12)`
|
||||||
|
and echoed both literals to stdout (`✅ Created Alice
|
||||||
|
(alice@deckhearth.com / alice123)` + matching bob line + a final
|
||||||
|
summary block listing both passwords). All four echo lines are
|
||||||
|
gone; the new shape reads a single `TEST_USERS_PASSWORD` env var
|
||||||
|
at the top of `createTestUsers()` with the same fail-loud + actionable
|
||||||
|
error message template as `setup-neon-db.js`'s post-DPS shape (names
|
||||||
|
the var, points at `.env.local`, suggests `openssl rand -base64 24`,
|
||||||
|
references README's "First-time admin setup" section, `process.exit(1)`
|
||||||
|
BEFORE opening any DB connection). Both alice and bob get the same
|
||||||
|
hashed value per Risk R2 (these are collaboration-flow demo
|
||||||
|
fixtures, not independent identities — per-user env vars would be
|
||||||
|
sprawl). `ON CONFLICT (email) DO NOTHING` preserved (already in the
|
||||||
|
original file). The two per-user creation lines are rewritten to
|
||||||
|
not echo the password: `✅ Created Alice (alice@deckhearth.com)`
|
||||||
|
+ `✅ Created Bob (bob@deckhearth.com)`; the final summary block
|
||||||
|
includes `(passwords from TEST_USERS_PASSWORD)` to document where
|
||||||
|
the password came from without printing it.
|
||||||
|
2. **`TESTING_GUIDE.md`** — Test Accounts table rewritten to (a)
|
||||||
|
remove the literal passwords from the table, (b) document the
|
||||||
|
env-var source for each user (`admin` ← `ADMIN_INITIAL_PASSWORD`;
|
||||||
|
`alice` / `bob` ← `TEST_USERS_PASSWORD`), and (c) point at README's
|
||||||
|
"First-time admin setup" section for the `openssl rand -base64 24`
|
||||||
|
generation tip. The two inline `Password: alice123` / `Password:
|
||||||
|
bob123` snippets later in the workflow are replaced with `Password:
|
||||||
|
<value of TEST_USERS_PASSWORD from .env.local>`.
|
||||||
|
|
||||||
|
**ESM-already.** Unlike `setup-neon-db.js` and `reset-db.js` at the
|
||||||
|
start of their respective convoys, `create-test-users.js` was already
|
||||||
|
top-level ESM (it imports `{ config } from 'dotenv'`, `{ sql } from
|
||||||
|
'@vercel/postgres'`, `bcrypt from 'bcryptjs'` at the top of the
|
||||||
|
file). This was the first of the three weak-creds-shape convoys to
|
||||||
|
skip the CJS→ESM conversion half of the pattern.
|
||||||
|
|
||||||
|
**Verification (all gates green at merge):**
|
||||||
|
- `node --check scripts/create-test-users.js` → exit 0
|
||||||
|
- `npm run lint` → 125 problems (post-PR-#31 baseline preserved; no
|
||||||
|
regression introduced)
|
||||||
|
- `npm run test:run` → 21/21 pass
|
||||||
|
- Grep `scripts/ TESTING_GUIDE.md` for `admin123|password123|test123|alice123|bob123`
|
||||||
|
→ 0 hits on literal passwords (the umbrella weak-cred surface is at
|
||||||
|
zero across the helper-script + manual-QA-doc lane)
|
||||||
|
- Grep `scripts/create-test-users.js` for `require(` → 0 hits
|
||||||
|
(ESM-already; trivially satisfied)
|
||||||
|
- Grep `scripts/ TESTING_GUIDE.md` for `TEST_USERS_PASSWORD` →
|
||||||
|
expected ~10 hits (docstring + const + error message body in script;
|
||||||
|
table + two inline workflow snippets + explanatory paragraph in
|
||||||
|
doc)
|
||||||
|
- CI on PR #27: Lint ✓ | Vitest 21/21 ✓ | Playwright smoke 3/3 ✓ |
|
||||||
|
`forbidden-endpoints` ✓ | `forbidden-cors-headers` ✓ | Vercel preview
|
||||||
|
deploy ✓ | Aggregate gate ✓
|
||||||
|
- `Screenshot diff`: not triggered (script + docs only — `paths:`
|
||||||
|
filter excludes `scripts/**` and `TESTING_GUIDE.md`; the
|
||||||
|
post-PR-#26 `!pages/api/**` exclusion is not even relevant here)
|
||||||
|
|
||||||
|
**Live verification deferred per convoy spec.** Optional post-merge
|
||||||
|
operator action: set `TEST_USERS_PASSWORD` in `.env.local`, run `node
|
||||||
|
scripts/create-test-users.js` against a non-prod Neon branch, verify
|
||||||
|
alice + bob rows insert; then unset the env var and re-run, verify
|
||||||
|
the script exits 1 with the helpful error message before opening the
|
||||||
|
DB connection.
|
||||||
|
|
||||||
|
**Operator caveat (going forward).** Existing alice + bob rows in
|
||||||
|
already-seeded environments are **not** rotated by re-running this
|
||||||
|
script — `ON CONFLICT (email) DO NOTHING` preserves the old hashes.
|
||||||
|
Any environment that ran `create-test-users.js` before this convoy
|
||||||
|
still has the weak `alice123` / `bob123` hashes in its DB; operators
|
||||||
|
must rotate manually via the app (or drop those rows and re-seed).
|
||||||
|
Same caveat that applies to the `drop-public-setup` admin row.
|
||||||
|
|
||||||
|
**Surfaced follow-up (newly queued in `.convoys/ship-readiness.md`):**
|
||||||
|
`purge-quick-login-from-loginpage` (P2 hygiene / security). Out-of-scope
|
||||||
|
sibling bug: `pages/login.js` lines ~172 + ~184 still hardcode
|
||||||
|
`alice123` / `bob123` in client-side "Quick Login" button handlers
|
||||||
|
that ship to production HTML. The convoy spec scope was "scripts +
|
||||||
|
docs only; do NOT touch `pages/**`", so this was deliberately left
|
||||||
|
for a follow-up. Architect-worth (the right shape — delete entirely,
|
||||||
|
gate behind `process.env.NODE_ENV === 'development'`, or proxy through
|
||||||
|
a dev-only endpoint — is a design decision).
|
||||||
|
|
||||||
|
**Spec deviation:** none.
|
||||||
|
|
||||||
|
**Cross-validation finding.** The `Playwright smoke` 3/3 PASS on a
|
||||||
|
script + docs-only PR is the seventh consecutive convoy where the
|
||||||
|
same 3-test smoke spec defends the auth surface through a sweeping
|
||||||
|
change (PR #15 → #19 → #20 → #21 → #25 → #32 → this PR). The lineage
|
||||||
|
continues.
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,22 @@ single auth provider, lint baseline cleanup, brand decision, test
|
||||||
coverage expansion) plus the P2 / P3 polish lanes in this file's
|
coverage expansion) plus the P2 / P3 polish lanes in this file's
|
||||||
Queued convoys section. None of those are P0 ship-blockers.
|
Queued convoys section. None of those are P0 ship-blockers.
|
||||||
|
|
||||||
**P1 quality-bar work in progress** (pick-a-name shipped 2026-05-24,
|
**P1 quality-bar milestone reached 2026-05-26 (7-convoy wave):** 5 of 6
|
||||||
PR #21 squash `9abbab6`, closing P1 #12 brand-consistency — the
|
P1 quality items RESOLVED post the 7-convoy multitask wave that merged
|
||||||
inconsistency `AGENTS.md` line 5 had flagged since project setup;
|
2026-05-26 (PRs #26-#32). The three remaining P1 quality launch-sequence
|
||||||
single-auth-provider, single-sql-client, migration-tool queued).
|
items shipped in a single wave:
|
||||||
|
- **P1 #8 single-sql-client** → RESOLVED 2026-05-26 (PR #30, squash `c403ea4`).
|
||||||
|
- **P1 #9 single-auth-provider** → RESOLVED 2026-05-26 (PR #31, squash `0668b0c`).
|
||||||
|
- **P1 #11 migration-tool** → RESOLVED 2026-05-26 (PR #32, squash `de9f334`).
|
||||||
|
|
||||||
|
Combined with prior closures (P1 #10 both steps via `adopt-vitest`
|
||||||
|
brief of `fix-auth-bypass` + `adopt-playwright-smoke` PR #18, and P1
|
||||||
|
#12 brand-consistency via `pick-a-name` PR #21), the P1 lane now
|
||||||
|
stands at **5 of 6 RESOLVED**. Only `fix-lint-baseline` (P1 #11.5)
|
||||||
|
remains in the P1 lane — the CI `|| true` wrapper still cushions the
|
||||||
|
125-problem lint baseline (improved from 128 by `single-auth-provider`,
|
||||||
|
which deleted 3 unused-import / unused-var lints together with
|
||||||
|
`lib/auth-context.js` + `lib/admin-auth.js`).
|
||||||
|
|
||||||
## P0 — ship-blockers (security)
|
## P0 — ship-blockers (security)
|
||||||
|
|
||||||
|
|
@ -203,18 +215,49 @@ These MUST land before any anonymous traffic touches the production URL.
|
||||||
|
|
||||||
## P1 — pre-launch quality bar
|
## P1 — pre-launch quality bar
|
||||||
|
|
||||||
### 8. Two SQL clients in parallel (`@neondatabase/serverless` + `@vercel/postgres`)
|
### 8. Two SQL clients in parallel (`@neondatabase/serverless` + `@vercel/postgres`) — **RESOLVED 2026-05-26**
|
||||||
|
|
||||||
- **Impact:** Two different param-handling APIs, two different transaction stories, two different connection-pool stories. Plus `lib/database.js`'s manual interpolation + `sql.unsafe(query)` is a SQL-injection vector if any caller passes user input through.
|
- **Resolved by:** `single-sql-client` convoy, squash commit `c403ea4` (PR #30). Parent-owned end-to-end (no architect or implementer subagent dispatched per the convoy file's "Owns" decision; single-file proven-pattern surface, mirroring the `fix-reset-db-script` precedent).
|
||||||
- **Fix:** Pick `@vercel/postgres` (tagged-template, no injection vector). Migrate every call site of `lib/database.js::db.query`. Delete `lib/database.js`.
|
- **Impact (pre-fix):** Two different param-handling APIs, two different transaction stories, two different connection-pool stories. Plus `lib/database.js`'s manual interpolation + `sql.unsafe(query)` was a SQL-injection vector if any caller passed user input through. Architect audit found **no current call site** actually exercised the unsafe shape with user input (the 2 callers pass a numeric `users.id` from a verified JWT), so this was foot-gun removal rather than a live security finding — see `.convoys/single-sql-client.md` § D4 for the audit.
|
||||||
- **Reviewer/Architect call:** small enough to fit in one convoy; touches ~3 files based on graph.
|
- **As-shipped:**
|
||||||
|
1. `lib/database.js` (47 lines) **deleted**. The `DatabaseAdapter` abstraction is gone; no replacement.
|
||||||
|
2. **2 callers migrated** (the convoy spec's "~3 files based on graph" estimate was loose; architect grep confirmed exactly 2):
|
||||||
|
- `pages/api/auth-utils.js` — `isAdmin(userId)` + `getUserById(userId)` swapped from `db.query(\`SELECT … WHERE id = $1\`, [userId])` to `` await sql`SELECT … WHERE id = ${userId}` `` (byte-equivalent SQL, identical `result.rows[0]` access pattern). Import swapped from `'../../lib/database.js'` to `'@vercel/postgres'`.
|
||||||
|
- `test/api/auth-utils.test.js` — dropped the now-unused `vi.mock('../../lib/database.js', ...)` call and the unused `vi` import. The 5 tests (2 `generateToken` + 3 `verifyToken`) are unchanged; they never exercised `isAdmin` / `getUserById` in the first place.
|
||||||
|
3. **`@neondatabase/serverless` retained as a runtime dep.** 11 `scripts/*` helpers still import `neon()` directly (`setup-neon-db.js` admin seed, `migrations/2026-05-24-rename-admin-email.js`, `reset-db.js`, plus 8 historical `add-*.js` / `fix-*.js` / `seed-*.js` jobs) — all out of scope per the no-go-zones rule. The dep-purge is tracked as the newly queued `purge-neondatabase-serverless-fully` follow-up (unblocked by PR #32 `migration-tool` — the migration helpers now go through `node-pg-migrate`'s `pg` client, not `@neondatabase/serverless`).
|
||||||
|
- **As-shipped metrics (PR #30, merged 2026-05-27T03:54:01Z UTC / local 2026-05-26):**
|
||||||
|
- Diff: **4 files, +447 / -64.** The 447-addition figure is dominated by `.convoys/single-sql-client.md` (the planning document, committed atomically). Actual source-file diff is small: `pages/api/auth-utils.js` +5 / -7, `test/api/auth-utils.test.js` +1 / -5, `lib/database.js` 0 / -47.
|
||||||
|
- `Lint` — **125 problems** (baseline preserved post-`single-auth-provider`; the `lib/database.js` deletion did not change lint count because the file was already lint-clean).
|
||||||
|
- `Vitest` — **21/21 pass**.
|
||||||
|
- `Playwright smoke` — **3/3 pass** (the smoke spec doesn't exercise `isAdmin` / `getUserById`, but the deployed preview is unaffected by the swap, so the cross-validation lineage continues).
|
||||||
|
- `Screenshot diff` — **not triggered** (PR #30's diff is `pages/api/**` + `lib/**` + `test/**` + `.convoys/**`; the post-PR-#26 `!pages/api/**` exclusion correctly held — see PR #26 below).
|
||||||
|
- `forbidden-endpoints` + `forbidden-cors-headers` — green.
|
||||||
|
- **Operator action required going forward:** **none.** No env-var change; no schema change; no infra change.
|
||||||
|
- **Owns:** `role-architect` (audit + decisions D1-D5 in `.convoys/single-sql-client.md`) — same parent agent that implemented.
|
||||||
|
|
||||||
### 9. Three parallel client-side auth implementations
|
### 9. Three parallel client-side auth implementations — **RESOLVED 2026-05-26**
|
||||||
|
|
||||||
- **Files:** `lib/auth-context.js` (`AuthProvider` / `useAuth`), `lib/admin-auth.js` (`AdminProvider` / `useAdmin` / `useIsAdmin`), `lib/use-auth.js` (`useAuth`).
|
- **Resolved by:** `single-auth-provider` convoy, squash commit `0668b0c` (PR #31). Parent-owned end-to-end (architect + implementer rolled together — mechanical diff once D1's shape-parity decision was made).
|
||||||
- **Impact:** Pages randomly import from one of three places. State is duplicated. Logout in one provider doesn't necessarily clear the others. Token-verify roundtrips happen 3× on initial page load if all three providers mount.
|
- **Files (pre-fix):** `lib/auth-context.js` (`AuthProvider` / `useAuth`), `lib/admin-auth.js` (`AdminProvider` / `useAdmin` / `useIsAdmin`), `lib/use-auth.js` (`useAuth`).
|
||||||
- **Fix:** Collapse to `lib/use-auth.js` as the canonical hook. Migrate every importer. Delete `auth-context.js` and `admin-auth.js`. Roll up `useIsAdmin` semantics into `useAuth().user?.role === 'admin'`.
|
- **Impact (pre-fix):** Pages randomly imported from one of three places. State was duplicated. Logout in one provider didn't necessarily clear the others. Token-verify roundtrips happened up to 3× on `pages/card/[id].js` mount.
|
||||||
- **Owns:** `role-architect` (decision) → `role-implementer` (per-page migration; ~30 importers).
|
- **As-shipped:**
|
||||||
|
1. **`lib/auth-context.js` + `lib/admin-auth.js` deleted.** No replacement; `lib/use-auth.js`'s hook-only `useAuth()` is the sole client auth surface.
|
||||||
|
2. **Importer inventory was 7 source files, not the ~30 estimated in P1 #9.** The estimate was pre-`fix-layout-default-user` (PR #15, `ca302a8`); that convoy had already migrated most of the tree to `lib/use-auth.js`, so the residual surface was much smaller than the estimate. Architect grep (`rg "from ['\"].*lib/auth-context['\"]" --type js`) returned exactly 6 importers of `auth-context.js` (`pages/_app.js`, `pages/index.js`, `pages/scanner.js`, `pages/decks.js`, `pages/deck/[id].js`, `pages/deck-builder.js`) + 1 importer of `admin-auth.js` (`pages/card/[id].js`).
|
||||||
|
3. **`<AuthProvider>` wrapper removed from `pages/_app.js`.** Per D3: `useAuth()` from `lib/use-auth.js` is hook-only, no Provider needed. `<ThemeProvider>` stays. `<AdminProvider>` was never in the tree to begin with (confirmed by reading `_app.js` pre-convoy).
|
||||||
|
4. **`useIsAdmin()`'s lone consumer inlined.** `pages/card/[id].js` was the only consumer; replaced with `const isAdmin = user?.role === 'admin'` from the existing `useAuth()` call (D2). Rendering condition at line 524 unchanged byte-for-byte; `adminLoading` kept as a local alias of `authLoading` to keep the diff minimal.
|
||||||
|
5. **Verify roundtrip count reduced 3 → 1** on `pages/card/[id].js` mount, and 2 → 1 on every other page-load. Single source of truth for `user` state per hook call site.
|
||||||
|
6. **CODEOWNERS sweep:** `.github/CODEOWNERS` lines for the two deleted files removed (per `.convoys/single-auth-provider.md` § Adjacent doc / config edits).
|
||||||
|
7. **Doc surface updated atomically:** `AGENTS.md` § 2 + § 3, `.cursor/rules/auth-and-permissions.mdc`, `.cursor/rules/no-go-zones.mdc`, `.cursor/skills/add-page/SKILL.md` all swept to describe the post-convoy single-surface state. (This entry's parent post-convoy doc-writer pass keeps that work consistent across `ship-readiness.md` and AGENTS.md.)
|
||||||
|
- **As-shipped metrics (PR #31, merged 2026-05-27T03:58:08Z UTC / local 2026-05-26):**
|
||||||
|
- Diff: **15 files, +341 / -263.** 2 file deletions (`lib/auth-context.js`, `lib/admin-auth.js`); 13 modifications (7 source pages + `.github/CODEOWNERS` + 5 docs / rules / skills + the new `.convoys/single-auth-provider.md` planning doc).
|
||||||
|
- `Lint` — **128 → 125 problems** (3 fewer errors; the deleted files contained 3 unused-import / unused-var lints; no new lint surface introduced). **This is the new lint baseline.**
|
||||||
|
- `Vitest` — **21/21 pass**. The 4 test files don't import any of the deleted modules (`grep`-confirmed pre-convoy).
|
||||||
|
- `npm run build` — succeeds end-to-end; 26 pages compile (10 dynamic API routes + 16 `pages/**` views including every file modified by the sweep). No "useAuth must be used within an AuthProvider" runtime error during SSR — confirms `<AuthProvider>` removal is safe.
|
||||||
|
- `Playwright smoke` — **3/3 pass**.
|
||||||
|
- `Screenshot diff` — triggered (PR #31 touches `pages/**` non-API plus `components/**` adjacent surface), `continue-on-error: true` swallow per Decision-4 end state of `adopt-playwright-smoke` (no baseline committed yet).
|
||||||
|
- **Operator action required going forward:** **none.** No new env vars; no schema change.
|
||||||
|
- **Spec deviation:** none of substance. Pre-merge estimate of ~30 importers in P1 #9 was loose; actual was 7 (documented above as the as-shipped reality).
|
||||||
|
- **Owns:** parent (architect + implementer rolled together per the convoy file's "Convoy owner" line).
|
||||||
|
|
||||||
### 10. No tests
|
### 10. No tests
|
||||||
|
|
||||||
|
|
@ -227,12 +270,33 @@ These MUST land before any anonymous traffic touches the production URL.
|
||||||
5. Wire `preview-smoke.yml` to run against the Vercel preview URL. **RESOLVED 2026-05-24** by `fix-vercel-deployment-protection-in-ci` (PR #17, `9a3e077`) + `adopt-playwright-smoke` (PR #18, `7b6f751`).
|
5. Wire `preview-smoke.yml` to run against the Vercel preview URL. **RESOLVED 2026-05-24** by `fix-vercel-deployment-protection-in-ci` (PR #17, `9a3e077`) + `adopt-playwright-smoke` (PR #18, `7b6f751`).
|
||||||
- **Owns:** `role-architect` (test strategy) → `role-implementer` (initial suite).
|
- **Owns:** `role-architect` (test strategy) → `role-implementer` (initial suite).
|
||||||
|
|
||||||
### 11. No migration tool — `scripts/add-*.js` graveyard
|
### 11. No migration tool — `scripts/add-*.js` graveyard — **RESOLVED 2026-05-26**
|
||||||
|
|
||||||
- **Files:** 27 scripts in `scripts/` of the form `add-foo-column.js`, `fix-bar-constraint.js`, `seed-baz.js`. No idempotency tracking, no `schema_migrations` table, no rollback.
|
- **Resolved by:** `migration-tool` convoy, squash commit `de9f334` (PR #32). Parent-owned end-to-end (no architect or implementer subagent dispatched; the convoy spec pre-ratified each Decision's recommended path, and the implementation surface was a small set of well-bounded file edits — see `.convoys/migration-tool.md` § Subagent / multitask footnote).
|
||||||
- **Impact:** Onboarding a new env requires re-running every script in the right order. No way to know what's been run on a given Neon branch. Every new column is at risk of being missed in prod.
|
- **Files (pre-fix):** 27+ scripts in `scripts/` of the form `add-foo-column.js`, `fix-bar-constraint.js`, `seed-baz.js`. No idempotency tracking, no `schema_migrations` table, no rollback.
|
||||||
- **Fix:** Adopt `node-pg-migrate` (lightweight, matches the existing pattern best) OR migrate to `drizzle-kit` if the team wants schema-as-code. Backfill a single "initial" migration matching current prod schema. From there, every new column ships as a migration file.
|
- **Impact (pre-fix):** Onboarding a new env required re-running every script in the right order. No way to know what had been run on a given Neon branch. Every new column was at risk of being missed in prod.
|
||||||
- **Owns:** `role-architect` (tool selection) → `role-implementer` (backfill + first new migration).
|
- **As-shipped (7 architect decisions, all ratified verbatim from the convoy spec at gate 1):**
|
||||||
|
1. **Tool: `node-pg-migrate@^8.0.4`** (D1). JavaScript-native, raw-SQL-friendly via `pgm.sql()`, ESM-clean. Rejected `drizzle-kit` / `prisma migrate` / `kysely` because each would force broader TypeScript surface than `AGENTS.md` Gotcha #9 allows. Brings `pg@^8.21.0` as a peer dep (dev-only).
|
||||||
|
2. **Migrations directory: `migrations/` at the repo root** (D2). Separates the new tool-wrapped artifacts from the legacy `scripts/migrations/` placeholder (which still houses `2026-05-24-rename-admin-email.js` and is preserved per no-go-zones). Matches `node-pg-migrate`'s default `--migrations-dir migrations`.
|
||||||
|
3. **Tracking table: default `pgmigrations`** (D3) — no name collision in the existing schema.
|
||||||
|
4. **Initial backfill: `migrations/1779853647564_initial-schema.js`** (~155 lines). Seven `pgm.sql(\`CREATE TABLE IF NOT EXISTS ...\`)` blocks reproducing `scripts/setup-neon-db.js`'s 7-table DDL verbatim (users / cards / user_cards / collections / collection_cards / decks / deck_cards). Idempotent against fresh AND pre-existing envs (the `CREATE TABLE IF NOT EXISTS` shape is a no-op on existing tables; only the `pgmigrations` row changes).
|
||||||
|
5. **`setup-neon-db.js` split** (D5): now (1) validates `ADMIN_INITIAL_PASSWORD` + `POSTGRES_URL`, (2) spawns `npm run migrate up` via `node:child_process.spawn` with `stdio: 'inherit'` and rejects with a wrapped error on non-zero exit, (3) seeds the admin-row INSERT with `ON CONFLICT (email) DO NOTHING`. The seven `CREATE TABLE IF NOT EXISTS` blocks are removed from `setup-neon-db.js` — they live in the migration now.
|
||||||
|
6. **CI integration: deferred** (D6) to the newly queued `wire-migrate-into-ci` follow-up convoy. Real work (test DB + secret OR Postgres service container) not in scope; documented as known limitation in `.convoys/migration-tool.md` § R3.
|
||||||
|
7. **Down-migration on the initial backfill: hard stub that throws** (D7). Rolling back would drop every user / card / collection / deck row. The stub's error message names the recommended alternative (Neon branch + forward-apply). Future migrations should write their own real `down()`.
|
||||||
|
8. **Doc surface updated atomically:** `README.md` (§ Installation + new § "Schema changes (post-`migration-tool` convoy)"), `AGENTS.md` § 3 (new "Schema changes" bullet) + § 4 Gotcha #6 (flipped → RESOLVED), `.cursor/rules/no-go-zones.mdc` (rewritten "Schema changes" rule), `.cursor/rules/db-and-schema.mdc` (§ "Schema source of truth" rewritten), `docs/SCHEMA_MAP.md` (preamble re-scoped). The 27+ historical `scripts/add-*.js` / `fix-*.js` / `seed-*.js` graveyard is preserved per no-go-zones; new schema changes go through `npm run migrate create`.
|
||||||
|
- **As-shipped metrics (PR #32, merged 2026-05-27T04:01:59Z UTC / local 2026-05-26):**
|
||||||
|
- Diff: **10 files, +1230 / -136.** The 1230-addition figure includes `.convoys/migration-tool.md` (~600 lines), `migrations/1779853647564_initial-schema.js` (~155 lines), the doc edits, and `package-lock.json` churn for the `node-pg-migrate` + `pg` install.
|
||||||
|
- `Lint` — **125 problems** (baseline preserved post-`single-auth-provider`; the new migration file is lint-clean, no new ignore patterns in `eslint.config.mjs`).
|
||||||
|
- `Vitest` — **21/21 pass** in ~1.3s. Vitest doesn't touch the migration surface; the run stayed green.
|
||||||
|
- `node --check migrations/1779853647564_initial-schema.js` → exit 0.
|
||||||
|
- `node --check scripts/setup-neon-db.js` → exit 0.
|
||||||
|
- Module-load + `down()` throw verification: `node -e "import('./migrations/1779853647564_initial-schema.js').then(m => m.down())"` throws the documented `[migration:1779853647564_initial-schema] Refusing to drop the initial schema. ...` message.
|
||||||
|
- `npm run migrate -- --help` → returns standard node-pg-migrate help text through the wrapper.
|
||||||
|
- `Playwright smoke` — **3/3 pass** (sixth consecutive convoy where the same 3-test smoke spec defends the auth surface through a sweeping change — see § Cross-validation in `.convoys/migration-tool.md`).
|
||||||
|
- **Operator action required going forward:** **none for the convoy itself.** The migration is idempotent against existing prod schema. No new env vars beyond the already-required `POSTGRES_URL` + `ADMIN_INITIAL_PASSWORD`. **Optional but recommended:** the next deploy that runs `setup-neon-db.js` silently applies the backfill migration (recording it in `pgmigrations`) — no operator action; this is just-in-time chained.
|
||||||
|
- **Live verification status:** **deferred** per convoy spec — the parent did not have a throwaway Neon branch available. Optional post-merge sequence documented in `.convoys/migration-tool.md` § Operator runbook.
|
||||||
|
- **Spec deviation:** none. All seven decisions landed verbatim from the spec at gate 1.
|
||||||
|
- **Owns:** parent (architect + implementer rolled together per the convoy file's § Subagent / multitask footnote).
|
||||||
|
|
||||||
### 11.5. Codebase has ~100 pre-existing ESLint errors
|
### 11.5. Codebase has ~100 pre-existing ESLint errors
|
||||||
|
|
||||||
|
|
@ -345,9 +409,9 @@ Each phase is one Conductor-created convoy. Don't run more than two in parallel
|
||||||
4. **`add-rate-limiting`** (P0 #6 full). One PR. Adds @upstash/ratelimit + applies to listed routes. **RESOLVED 2026-05-24** — PR #20 squash `708ef45`; closes P0 #6 (last open P0), flipping the ship-blocker set to **8/8 RESOLVED**. 5 named limiters (auth/search/upload/generate/import), 6 routes newly gated + the 3 import routes auth-gated atomically with a `pages/admin/card-import.js` Bearer-header fix. Smoke 3/3 green in 3.8s post-merge — confirms the new 60/min search limiter doesn't 429 the smoke spec. See § Queued convoys and P0 #6 above for the full as-shipped block.
|
4. **`add-rate-limiting`** (P0 #6 full). One PR. Adds @upstash/ratelimit + applies to listed routes. **RESOLVED 2026-05-24** — PR #20 squash `708ef45`; closes P0 #6 (last open P0), flipping the ship-blocker set to **8/8 RESOLVED**. 5 named limiters (auth/search/upload/generate/import), 6 routes newly gated + the 3 import routes auth-gated atomically with a `pages/admin/card-import.js` Bearer-header fix. Smoke 3/3 green in 3.8s post-merge — confirms the new 60/min search limiter doesn't 429 the smoke spec. See § Queued convoys and P0 #6 above for the full as-shipped block.
|
||||||
5. **`pick-a-name`** (P1 #12). Human decision first, then one or two PRs. **RESOLVED 2026-05-24** — PR #21 squash `9abbab6`; closes P1 #12 (brand-consistency, the inconsistency `AGENTS.md` line 5 had flagged since project setup). Two file-disjoint briefs landed serially (B1 commit `ac8c998` display + comment sweep across 7 files; B2 commit `1c18d21` infrastructure + email migration across 10 modified + 1 new migration script). Five canonical-string D-decisions ratified verbatim at gate-1 (Deck Hearth / `deck-hearth` / `deckhearth` / `admin@deckhearth.com` / full `deckhearth` Redis prefix) plus Risk 4 PRESERVE on `test/lib/permission-middleware.test.js` line 87's negative regression-lock literal. Smoke 3/3 green in 1m4s post-merge — fourth convoy in a row (PR #15 → #19 → #20 → #21) where the same 3-test smoke spec defends the auth surface through a sweeping change. **Operator action required:** run `node scripts/migrations/2026-05-24-rename-admin-email.js` against prod Neon BEFORE the next admin login attempt with the new email (idempotent, UNIQUE-collision-safe). See § Queued convoys for the downstream `rename-repo-and-vercel-project` + `point-domain-at-deckhearth` + `regenerate-brand-assets` follow-ups, and `.convoys/pick-a-name.md` § As-shipped for the full record.
|
5. **`pick-a-name`** (P1 #12). Human decision first, then one or two PRs. **RESOLVED 2026-05-24** — PR #21 squash `9abbab6`; closes P1 #12 (brand-consistency, the inconsistency `AGENTS.md` line 5 had flagged since project setup). Two file-disjoint briefs landed serially (B1 commit `ac8c998` display + comment sweep across 7 files; B2 commit `1c18d21` infrastructure + email migration across 10 modified + 1 new migration script). Five canonical-string D-decisions ratified verbatim at gate-1 (Deck Hearth / `deck-hearth` / `deckhearth` / `admin@deckhearth.com` / full `deckhearth` Redis prefix) plus Risk 4 PRESERVE on `test/lib/permission-middleware.test.js` line 87's negative regression-lock literal. Smoke 3/3 green in 1m4s post-merge — fourth convoy in a row (PR #15 → #19 → #20 → #21) where the same 3-test smoke spec defends the auth surface through a sweeping change. **Operator action required:** run `node scripts/migrations/2026-05-24-rename-admin-email.js` against prod Neon BEFORE the next admin login attempt with the new email (idempotent, UNIQUE-collision-safe). See § Queued convoys for the downstream `rename-repo-and-vercel-project` + `point-domain-at-deckhearth` + `regenerate-brand-assets` follow-ups, and `.convoys/pick-a-name.md` § As-shipped for the full record.
|
||||||
6. **`adopt-vitest`** (P1 #10 step 1). One PR. Enables testing every future change.
|
6. **`adopt-vitest`** (P1 #10 step 1). One PR. Enables testing every future change.
|
||||||
7. **`migration-tool`** (P1 #11). One PR. Backfill + first new migration.
|
7. **`migration-tool`** (P1 #11). One PR. **RESOLVED 2026-05-26** — PR #32 squash `de9f334`; closes P1 #11 (no migration tool). `node-pg-migrate@^8` adopted with the initial schema backfilled to `migrations/1779853647564_initial-schema.js`; `setup-neon-db.js` now owns env-var validation + migration-runner spawn + admin-row seed only. The 27+ historical `scripts/add-*.js` / `fix-*.js` / `seed-*.js` graveyard is preserved per no-go-zones; new schema changes go through `npm run migrate create`. All 7 architect decisions ratified verbatim at gate 1. Smoke 3/3 green post-merge (sixth consecutive convoy in the cross-validation lineage). See § P1 #11 above for the full as-shipped block.
|
||||||
8. **`single-sql-client`** (P1 #8). 2-3 PRs, fanned out via multitask once per-file briefs are written.
|
8. **`single-sql-client`** (P1 #8). 2-3 PRs, fanned out via multitask once per-file briefs are written. **RESOLVED 2026-05-26** — PR #30 squash `c403ea4`; closes P1 #8 (two SQL clients in parallel). `lib/database.js` deleted; the 2 callers (`pages/api/auth-utils.js` source + `test/api/auth-utils.test.js` mock cleanup) migrated to `@vercel/postgres` tagged templates. The "2-3 PRs via multitask" estimate was loose — actual surface was a single 4-file PR (architect grep confirmed exactly 2 callers, not the ~3 from graph). `@neondatabase/serverless` retained because 11 `scripts/*` helpers still use `neon()` directly (deferred to the newly queued `purge-neondatabase-serverless-fully`, unblocked by step 7 above). Lint baseline preserved at 125 (the new post-PR-#31 baseline). See § P1 #8 above.
|
||||||
9. **`single-auth-provider`** (P1 #9). 3-5 PRs via multitask.
|
9. **`single-auth-provider`** (P1 #9). 3-5 PRs via multitask. **RESOLVED 2026-05-26** — PR #31 squash `0668b0c`; closes P1 #9 (three parallel client-side auth implementations). `lib/auth-context.js` + `lib/admin-auth.js` deleted; 7 source files swept (`pages/_app.js`, `pages/index.js`, `pages/scanner.js`, `pages/decks.js`, `pages/deck/[id].js`, `pages/deck-builder.js`, `pages/card/[id].js`); `<AuthProvider>` wrapper removed from `_app.js`; `useIsAdmin()`'s lone consumer (`pages/card/[id].js`) inlined as `user?.role === 'admin'`. Verify-roundtrip count reduced 3 → 1 on `card/[id].js` mount, 2 → 1 on every other page-load. Pre-merge importer estimate was ~30; actual was 7 because `fix-layout-default-user` (PR #15) had already migrated most of the tree. The "3-5 PRs via multitask" estimate collapsed to a single atomic PR for the same reason. **Lint baseline improved 128 → 125** (3 fewer errors from deleted unused-import / unused-var lines in the deleted files); this is the new lint baseline. See § P1 #9 above.
|
||||||
10. **`adopt-playwright-smoke`** (P1 #10 step 2). One PR. **RESOLVED 2026-05-24** — PR #18 squash `7b6f751`; smoke 3/3 green in 2.9s, full workflow 59s, zero secret leaks. See § Queued convoys for the full as-shipped block.
|
10. **`adopt-playwright-smoke`** (P1 #10 step 2). One PR. **RESOLVED 2026-05-24** — PR #18 squash `7b6f751`; smoke 3/3 green in 2.9s, full workflow 59s, zero secret leaks. See § Queued convoys for the full as-shipped block.
|
||||||
11. **`schema-cleanup`** (P2 #14). Multi-PR convoy via multitask.
|
11. **`schema-cleanup`** (P2 #14). Multi-PR convoy via multitask.
|
||||||
12. **`god-component-split`** (P2 #13). One convoy per file; fan out via multitask once architect's `slice_dependencies` are written.
|
12. **`god-component-split`** (P2 #13). One convoy per file; fan out via multitask once architect's `slice_dependencies` are written.
|
||||||
|
|
@ -361,15 +425,15 @@ Follow-ups surfaced mid-convoy or mid-PR that didn't fit the original launch seq
|
||||||
|
|
||||||
- **`rotate-default-admin`** (priority: P2 hygiene). Operator-rotation script for envs that ran `setup-neon-db.js` before `drop-public-setup` and still carry the weak `admin123` bcrypt hash. Surfaced in P0 #3 § Operator caveat. Optional: do nothing if no audit finds a deployed env with the weak hash.
|
- **`rotate-default-admin`** (priority: P2 hygiene). Operator-rotation script for envs that ran `setup-neon-db.js` before `drop-public-setup` and still carry the weak `admin123` bcrypt hash. Surfaced in P0 #3 § Operator caveat. Optional: do nothing if no audit finds a deployed env with the weak hash.
|
||||||
- **`delete-dead-lorcana-import`** (priority: P3 polish). Delete `pages/api/cards/import-lorcana.js` (and possibly `scripts/import-lorcana.js`) if Lorcana stays out of the admin UI's `<select>` permanently. Surfaced 2026-05-24 in `add-rate-limiting` Decision 1: the architect ran `rg 'import-lorcana' pages/ components/` and found zero frontend callers — `pages/admin/card-import.js`'s `<select>` only offers `'mtg'` and `'pokemon'`. The route is gated defensively (auth + admin-role + rate-limit) as part of PR #20 so the future Lorcana admin UI path inherits protection automatically, but if Lorcana is never wired in, this is the cleanup convoy. Strictly easier than gating-then-deleting because the gating shape is uniform across all three import routes today (mtg + pokemon + lorcana); a future cleanup only needs to delete the lorcana file + remove the `'lorcana'` enum option from `.cursor/rules/api-routes.mdc`'s import-routes table. Do nothing if Lorcana support gets wired into the admin UI in a feature convoy; cancel the entry then.
|
- **`delete-dead-lorcana-import`** (priority: P3 polish). Delete `pages/api/cards/import-lorcana.js` (and possibly `scripts/import-lorcana.js`) if Lorcana stays out of the admin UI's `<select>` permanently. Surfaced 2026-05-24 in `add-rate-limiting` Decision 1: the architect ran `rg 'import-lorcana' pages/ components/` and found zero frontend callers — `pages/admin/card-import.js`'s `<select>` only offers `'mtg'` and `'pokemon'`. The route is gated defensively (auth + admin-role + rate-limit) as part of PR #20 so the future Lorcana admin UI path inherits protection automatically, but if Lorcana is never wired in, this is the cleanup convoy. Strictly easier than gating-then-deleting because the gating shape is uniform across all three import routes today (mtg + pokemon + lorcana); a future cleanup only needs to delete the lorcana file + remove the `'lorcana'` enum option from `.cursor/rules/api-routes.mdc`'s import-routes table. Do nothing if Lorcana support gets wired into the admin UI in a feature convoy; cancel the entry then.
|
||||||
- **`tighten-visual-diff-path-filter`** (priority: P3 polish). Tighten `.github/workflows/visual-diff.yml`'s `paths:` filter from `pages/**` to something like `pages/!(api)/**` so API-only PRs don't trigger the visual workflow. Surfaced on PR #19 (`cors-tighten`) which was API-only but triggered `Screenshot diff` anyway, wasting ~55s of CI time. PR #20 (`add-rate-limiting`) re-triggered the same false-positive — same shape, same swallow via `continue-on-error: true`. Low priority — works as designed today, just chatty. One-line YAML tweak; verify the negated-glob syntax against GitHub Actions' [path-filter semantics](https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#filter-pattern-cheat-sheet) before merging (the `!()` extglob may need to be expressed differently if Actions uses minimatch without extglob support).
|
- **`tighten-visual-diff-path-filter`** — **RESOLVED 2026-05-26** by `tighten-visual-diff-path-filter` convoy, squash commit `ba95462` (PR #26). Single-edit `paths:` filter change in `.github/workflows/visual-diff.yml`: inserted `'!pages/api/**'` immediately after `'pages/**'` (order-sensitive per GitHub Actions' minimatch path-filter semantics — exclusions only fire after a prior include matches). Verified the YAML deserialization order at gate time (`['pages/**', '!pages/api/**', 'components/**', 'styles/**', 'tailwind.config.js', 'postcss.config.js']`). `preview-smoke.yml` left untouched (no `paths:` filter; intentionally fires on every PR). Diff: 2 files, +279 / -0 (1 YAML entry + inline comment block + the planning convoy file). **Post-merge verification still pending** — the only true verification is that the next API-only PR after this merges does NOT trigger `Screenshot diff`. PR #30 (`single-sql-client`, squash `c403ea4`) was the **first API-only PR post-merge** and its CI Checks tab showed `Screenshot diff: not triggered` — empirical confirmation that the `!pages/api/**` exclusion fires correctly. The next-API-only-PR success line was originally specified in the convoy file's § Verification plan as the deferred-to-post-merge gate; this is that confirmation. Entry kept (not removed) to preserve the audit trail. See `.convoys/tighten-visual-diff-path-filter.md` § As-shipped.
|
||||||
- **`purge-weak-creds-from-helpers`** (priority: P2 hygiene). **Scope reduced 2026-05-26** by `fix-reset-db-script` (squash `3ab9bf8`, PR #25): `scripts/reset-db.js` is fully clean — the `admin123` literal at the old line 143 and the `Admin Password: admin123` echo at the old line 156 are both gone (script now reads `ADMIN_INITIAL_PASSWORD` from env and never echoes the password). **Email-half satisfied 2026-05-24** by `pick-a-name` Brief 2 (squash `9abbab6`) — the `admin@tcgvault.com` / `alice@tcgvault.com` / `bob@tcgvault.com` references in the three helper paths were swept to `@deckhearth.com` together with the migration script. **Remaining scope:** `scripts/create-test-users.js` (alice/bob test fixtures still have weak passwords) + `TESTING_GUIDE.md` (still documents the weak creds). Small enough now that it may fold into the next pass of test-fixture cleanup or a `rename-testing-guide` doc convoy.
|
- **`purge-weak-creds-from-helpers`** — **RESOLVED 2026-05-26** by `purge-weak-creds-from-helpers` convoy, squash commit `5f2b234` (PR #27). The umbrella is now closed; both remaining halves shipped together. **Multi-convoy history:** (1) `drop-public-setup` Brief 1+2 (`ff80753` + `b63b509`) removed the first `admin123` literal from `scripts/setup-neon-db.js` and set the env-var + fail-loud + no-echo precedent. (2) `pick-a-name` Brief 2 (`9abbab6`) swept the `@tcgvault.com` literals in the three helper paths to `@deckhearth.com` together with the migration script. (3) `fix-reset-db-script` (`3ab9bf8`, PR #25) removed the second `admin123` from `scripts/reset-db.js` and the second `Admin Password:` echo. (4) **This convoy (PR #27)** closes the umbrella by sweeping the last two files: `scripts/create-test-users.js` (alice/bob fixtures, previously hardcoding `bcrypt.hash('alice123', 12)` + `bcrypt.hash('bob123', 12)` and echoing both literals to stdout) and `TESTING_GUIDE.md` (Test Accounts table previously documenting the weak literals). The post-convoy contract: single `TEST_USERS_PASSWORD` env var (intentional simplification per Risk R2 — these are collaboration-flow demo fixtures, not independent identities), fail-loud at the top of `createTestUsers()` BEFORE any DB connection, no password echo anywhere (`✅ Created Alice (alice@deckhearth.com / alice123)` → `✅ Created Alice (alice@deckhearth.com)`), `ON CONFLICT (email) DO NOTHING` preserved. Diff: 3 files, +249 / -22. Lint preserved at 125 (post-PR-#31 baseline); vitest 21/21. ESM-already (this was the first of the three weak-creds-shape convoys to skip the CJS→ESM half because `scripts/create-test-users.js` was already top-level ESM). **Operator caveat:** existing alice/bob rows in already-seeded envs are NOT rotated by re-running the script — `ON CONFLICT` preserves the old hashes; operators must rotate manually via the app or drop those rows and re-seed. Same caveat as the `drop-public-setup` admin-row guidance. **Surfaced out-of-scope follow-up:** `purge-quick-login-from-loginpage` — see new queue entry below. Entry kept (not removed) to preserve the audit trail. See `.convoys/purge-weak-creds-from-helpers.md` § As-shipped.
|
||||||
- **`rename-repo-and-vercel-project`** (priority: P2 polish). Rename the GitHub repo + the Vercel project from `tcg-vault` to `deck-hearth` to match the canonical product brand ratified in `pick-a-name` (squash `9abbab6`, 2026-05-24). Auto-redirects on both GitHub and Vercel make this low-urgency; the surface is a one-line update to local git remotes (`git remote set-url origin git@github.com:<owner>/deck-hearth.git`) + a Vercel project-settings rename + the 8 architect-verified literal-repo references documented in `.convoys/pick-a-name.md` § Full surface inventory § Repo / Vercel project name (out-of-scope) — `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` source tags. Also re-evaluate the `.agent-context-manifest.yml` `source: "tcg-vault-local"` tag at that point (Risk 5 of `pick-a-name` — renaming the source tag could break the `sync-agent-context` skill's drift tracking; do this convoy with the sync-skill author's input). Surfaced 2026-05-24 as the explicit downstream of `pick-a-name`.
|
- **`rename-repo-and-vercel-project`** (priority: P2 polish). Rename the GitHub repo + the Vercel project from `tcg-vault` to `deck-hearth` to match the canonical product brand ratified in `pick-a-name` (squash `9abbab6`, 2026-05-24). Auto-redirects on both GitHub and Vercel make this low-urgency; the surface is a one-line update to local git remotes (`git remote set-url origin git@github.com:<owner>/deck-hearth.git`) + a Vercel project-settings rename + the 8 architect-verified literal-repo references documented in `.convoys/pick-a-name.md` § Full surface inventory § Repo / Vercel project name (out-of-scope) — `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` source tags. Also re-evaluate the `.agent-context-manifest.yml` `source: "tcg-vault-local"` tag at that point (Risk 5 of `pick-a-name` — renaming the source tag could break the `sync-agent-context` skill's drift tracking; do this convoy with the sync-skill author's input). Surfaced 2026-05-24 as the explicit downstream of `pick-a-name`.
|
||||||
- **`point-domain-at-deckhearth`** (priority: P2 polish; **blocked on domain acquisition**). Once the operator buys `deckhearth.com` (or `.app` / `.gg` / other), wire DNS to the Vercel deployment + claim the domain in Vercel's project settings + update the seed admin email's TLD if the purchased TLD is anything other than `.com` (a one-line REPLACE migration mirroring `scripts/migrations/2026-05-24-rename-admin-email.js`). Surfaced 2026-05-24 as the explicit downstream of `pick-a-name` (the convoy seed § *"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)"*). Until this lands, the seed admin email `admin@deckhearth.com` is a placeholder STRING used as a unique identifier — auth uses email as identity, not as a mail target, so no working mailbox is required for login to function.
|
- **`point-domain-at-deckhearth`** (priority: P2 polish; **blocked on domain acquisition**). Once the operator buys `deckhearth.com` (or `.app` / `.gg` / other), wire DNS to the Vercel deployment + claim the domain in Vercel's project settings + update the seed admin email's TLD if the purchased TLD is anything other than `.com` (a one-line REPLACE migration mirroring `scripts/migrations/2026-05-24-rename-admin-email.js`). Surfaced 2026-05-24 as the explicit downstream of `pick-a-name` (the convoy seed § *"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)"*). Until this lands, the seed admin email `admin@deckhearth.com` is a placeholder STRING used as a unique identifier — auth uses email as identity, not as a mail target, so no working mailbox is required for login to function.
|
||||||
- **`regenerate-brand-assets`** (priority: P2 polish). Regenerate the favicon (`public/favicon.ico`), Open Graph images, and social share cards with the new Deck Hearth identity. Requires a design pass — out-of-scope for any single agent-driven convoy; queue when an asset-design pass is scheduled. Surfaced 2026-05-22 originally in `.convoys/ship-readiness.md` § Role-design-system-auditor findings ("Then once the name is settled, the 'DH' logo + AnimatedFireLogo need to be unified into one brand mark"); reaffirmed 2026-05-24 in `pick-a-name` Out-of-scope queued follow-ups.
|
- **`regenerate-brand-assets`** (priority: P2 polish). Regenerate the favicon (`public/favicon.ico`), Open Graph images, and social share cards with the new Deck Hearth identity. Requires a design pass — out-of-scope for any single agent-driven convoy; queue when an asset-design pass is scheduled. Surfaced 2026-05-22 originally in `.convoys/ship-readiness.md` § Role-design-system-auditor findings ("Then once the name is settled, the 'DH' logo + AnimatedFireLogo need to be unified into one brand mark"); reaffirmed 2026-05-24 in `pick-a-name` Out-of-scope queued follow-ups.
|
||||||
- **`convert-reset-db-to-esm`** — **RESOLVED 2026-05-26** by `fix-reset-db-script` (squash `3ab9bf8`, PR #25). `scripts/reset-db.js` now uses ESM top-level imports (`import dotenv`, `import { neon }`, `import bcrypt`) and executes cleanly on Node 22.x. Same fix shape as `setup-neon-db.js` post-`drop-public-setup` B2. As predicted in this entry's prior note, the fold-with-`purge-weak-creds-from-helpers` shape was the right call — both ailments in `scripts/reset-db.js` were fixed atomically with a single 55-line diff. Entry kept (not removed) to preserve the audit trail. See `.convoys/fix-reset-db-script.md` § As-shipped.
|
- **`convert-reset-db-to-esm`** — **RESOLVED 2026-05-26** by `fix-reset-db-script` (squash `3ab9bf8`, PR #25). `scripts/reset-db.js` now uses ESM top-level imports (`import dotenv`, `import { neon }`, `import bcrypt`) and executes cleanly on Node 22.x. Same fix shape as `setup-neon-db.js` post-`drop-public-setup` B2. As predicted in this entry's prior note, the fold-with-`purge-weak-creds-from-helpers` shape was the right call — both ailments in `scripts/reset-db.js` were fixed atomically with a single 55-line diff. Entry kept (not removed) to preserve the audit trail. See `.convoys/fix-reset-db-script.md` § As-shipped.
|
||||||
- **`lint-against-cjs-in-esm-scripts`** (priority: P3 polish). Add an ESLint `no-restricted-syntax` rule against `CallExpression[callee.name="require"]` (or equivalent) in `scripts/**` so any future helper script that re-introduces CJS `require()` calls in this ESM-since-`bump-next-js` repo fails at lint time instead of at first execution. Would have caught both `drop-public-setup` Brief 2's pre-fix `setup-neon-db.js` AND `fix-reset-db-script`'s pre-fix `scripts/reset-db.js` before the bug shipped. Surfaced 2026-05-26 as the natural follow-up to PR #25. Small surface (one ESLint rule entry in `eslint.config.mjs`); the only design call is whether to scope to `scripts/**` only or to all `.js` files at the repo root (the former matches the actual blast radius; the latter is broader-than-necessary since `pages/api/**` is already correctly using ESM imports throughout).
|
- **`lint-against-cjs-in-esm-scripts`** — **RESOLVED 2026-05-26** by `lint-against-cjs-in-esm-scripts` convoy, squash commit `13d6210` (PR #29). Single 7-line flat-config block added to `eslint.config.mjs` after the existing `globalIgnores(...)` call: `{ files: ['scripts/**/*.js'], rules: { 'no-restricted-syntax': ['error', { selector: 'CallExpression[callee.name="require"]', message: '...' }] } }`. The error message points at `.convoys/fix-reset-db-script.md` so a future contributor who trips the rule gets a 1-click path to the exemplar ESM fix shape. Scope decision: `scripts/**` only, NOT all `.js` (matches actual blast radius — every observed bug instance has been in a helper script; the config files `postcss.config.js` / `tailwind.config.js` legitimately use CJS-style exports that the next-config base rules already handle correctly). **Would have caught both `drop-public-setup` Brief 2's pre-fix `scripts/setup-neon-db.js` AND `fix-reset-db-script`'s pre-fix `scripts/reset-db.js` at lint time instead of at first execution** — the exact two motivating bugs from the multi-convoy history. Diff: 2 files, +185 / -0. Lint baseline preserved at 125 (post-PR-#31; zero new false positives in the current tree because both motivating bugs were already fixed). Negative test verified: prepending `const x = require('fs');` to `scripts/reset-db.js` fires the rule at the expected line/column with the documented message; reverting returns to a clean lint. `scripts/migrations/**` was already in `globalIgnores` (from `pick-a-name` Brief 2's migration script); the rule does not fire there. Entry kept (not removed) to preserve the audit trail. See `.convoys/lint-against-cjs-in-esm-scripts.md` § As-shipped.
|
||||||
- **`single-auth-provider`** (priority: P1 quality, also listed as launch sequence step 9). Collapse `lib/auth-context.js` + `lib/admin-auth.js` into `lib/use-auth.js`. Surfaced again as a follow-up in P0 #7 § Flagged-but-deferred (4 pages still import the legacy `useAuth`).
|
- **`single-auth-provider`** — **RESOLVED 2026-05-26** by `single-auth-provider` convoy, squash commit `0668b0c` (PR #31; also listed as launch sequence step 9 and § P1 #9 above — both flipped to RESOLVED in the same wave). `lib/auth-context.js` + `lib/admin-auth.js` deleted; 7 source files swept; `<AuthProvider>` wrapper removed from `_app.js`; `useIsAdmin()`'s lone consumer inlined as `user?.role === 'admin'`. Importer inventory was 7, not the ~30 estimated in P1 #9 (most of the tree was already on `lib/use-auth.js` post-`fix-layout-default-user`). Lint improved 128 → 125. Entry kept (not removed) to preserve audit trail. See § P1 #9 + launch sequence step 9 above for the full as-shipped block.
|
||||||
- **`cleanup-mobile-nav-dead-props`** (priority: P3 polish). `components/MobileNavigation.js` accepts a dead `user` prop; remove it. Surfaced in P0 #7 § Flagged-but-deferred. May fold into `god-component-split` (P2 #13) if that lands first.
|
- **`cleanup-mobile-nav-dead-props`** — **RESOLVED 2026-05-26** by `cleanup-mobile-nav-dead-props` convoy, squash commit `171f5af` (PR #28). Two-file, three-line diff: (1) `components/MobileNavigation.js` line 5 — `{ user, onMenuOpen }` → `{ onMenuOpen }`; (2) `components/Layout.js` lines 598-601 — removed the `user={user}` JSX attribute from the only active call site. Audit confirmed `user` was genuinely dead pre-fix (the bottom-bar items are static and don't depend on auth state). `components/Layout.js.backup` left untouched per the `.cursor/rules/no-go-zones.mdc` § "Append-only / historical" rule (its stale `user={user}` call disappears when the `.backup` file is eventually deleted in a separate convoy). Diff: 3 files, +156 / -2 (3 lines source + the planning convoy file). Lint preserved at 125; vitest 21/21 (the `test/components/Layout.test.js` regression-lock assertions for the logged-out Layout branch do not assert on `MobileNavigation`'s prop shape, so the dead-prop removal is invisible to the suite). Pre-existing dead `import { useState } from 'react'` at `MobileNavigation.js` line 3 left untouched per the convoy spec's "single-prop removal" boundary. Did NOT fold into `god-component-split` (P2 #13) — that hasn't landed yet, so this small hygiene convoy shipped first. Entry kept (not removed) to preserve audit trail. See `.convoys/cleanup-mobile-nav-dead-props.md` § As-shipped.
|
||||||
- **`bump-eslint-10`** (priority: P2 hygiene; upstream-blocked). Bump ESLint from v9 to v10 once `typescript-eslint` ships a v10-tested release and `eslint-config-next` bundles it. Surfaced in `.convoys/bump-next-js.md` § Decisions D.
|
- **`bump-eslint-10`** (priority: P2 hygiene; upstream-blocked). Bump ESLint from v9 to v10 once `typescript-eslint` ships a v10-tested release and `eslint-config-next` bundles it. Surfaced in `.convoys/bump-next-js.md` § Decisions D.
|
||||||
- **`harden-multipart-parser`** (priority: P2 quality). Surfaced 2026-05-24 in `add-rate-limiting` § Risk list. `pages/api/user/avatar.js`'s `parseMultipartFormData` consumes the 5MB multipart body via `req.on('data')` before any response is sent, so an attacker can still exhaust the 5MB body even on a 429 path from the new `checkUploadRateLimit` gate. Real defense requires moving the parse into a separate edge function or using `read-up-to` semantics. Not a release-blocker — the gate-ordering in PR #20 places the limiter BEFORE the method branches that call `parseMultipartFormData`, so when this hardening lands, the gate ordering is already correct. Surface as P1 only if a real abuse incident occurs.
|
- **`harden-multipart-parser`** (priority: P2 quality). Surfaced 2026-05-24 in `add-rate-limiting` § Risk list. `pages/api/user/avatar.js`'s `parseMultipartFormData` consumes the 5MB multipart body via `req.on('data')` before any response is sent, so an attacker can still exhaust the 5MB body even on a 429 path from the new `checkUploadRateLimit` gate. Real defense requires moving the parse into a separate edge function or using `read-up-to` semantics. Not a release-blocker — the gate-ordering in PR #20 places the limiter BEFORE the method branches that call `parseMultipartFormData`, so when this hardening lands, the gate ordering is already correct. Surface as P1 only if a real abuse incident occurs.
|
||||||
- **`god-function-split` / `refactor-cards-search-sql`** (priority: P2 refactor). Surfaced 2026-05-24 in `add-rate-limiting` § Files explicitly out of scope. `pages/api/cards/search.js` has a 240-line god-function shape with 7+ conditional `SELECT * FROM cards WHERE …` branches; the PR #20 rate-limit gate sits at the top of the handler and leaves the SQL byte-identical. Splitting is its own scope (probably one convoy per branch group with `slice_dependencies:` for safe multitask fan-out). Not security-critical; deferred to the P2 lane.
|
- **`god-function-split` / `refactor-cards-search-sql`** (priority: P2 refactor). Surfaced 2026-05-24 in `add-rate-limiting` § Files explicitly out of scope. `pages/api/cards/search.js` has a 240-line god-function shape with 7+ conditional `SELECT * FROM cards WHERE …` branches; the PR #20 rate-limit gate sits at the top of the handler and leaves the SQL byte-identical. Splitting is its own scope (probably one convoy per branch group with `slice_dependencies:` for safe multitask fan-out). Not security-critical; deferred to the P2 lane.
|
||||||
|
|
@ -412,6 +476,14 @@ Follow-ups surfaced mid-convoy or mid-PR that didn't fit the original launch seq
|
||||||
3. `Screenshot diff` baseline authoring — orthogonal scope; the visual-diff workflow has nothing to compare against on its first real run.
|
3. `Screenshot diff` baseline authoring — orthogonal scope; the visual-diff workflow has nothing to compare against on its first real run.
|
||||||
- **Owns:** `role-architect` (3 Decisions ratified — A query-param, B 120s timeout, D fork-PR skip) → `role-implementer` (Brief 1) + two scope-expansion commits.
|
- **Owns:** `role-architect` (3 Decisions ratified — A query-param, B 120s timeout, D fork-PR skip) → `role-implementer` (Brief 1) + two scope-expansion commits.
|
||||||
|
|
||||||
|
- **`purge-quick-login-from-loginpage`** (priority: P2 hygiene / security). Surfaced 2026-05-26 by `purge-weak-creds-from-helpers` (PR #27) as an out-of-scope sibling bug. `pages/login.js` lines ~172 + ~184 still hardcode `alice123` / `bob123` in client-side "Quick Login" button handlers (`handleQuickLogin('alice@deckhearth.com', 'alice123')` / `handleQuickLogin('bob@deckhearth.com', 'bob123')`). These ship to production HTML and reveal the legacy passwords directly to anyone viewing the login page source. The PR #27 convoy spec was "scripts + docs only; do NOT touch `pages/**`", so this was deliberately left for a follow-up. Small surface (one file, two button handlers). Two reasonable shapes: (a) delete the Quick Login section entirely, or (b) gate it behind `process.env.NODE_ENV === 'development'` with a credential source that doesn't ship to prod HTML (likely a `.env.local`-only `NEXT_PUBLIC_DEV_*` convention or a dev-only proxy endpoint). The latter is architect-worth. Should fold into a UI hygiene pass or a `pre-launch-checklist` convoy that strips dev affordances from prod builds.
|
||||||
|
- **`purge-neondatabase-serverless-fully`** (priority: P3 polish; **unblocked 2026-05-26** by `migration-tool` PR #32). Surfaced 2026-05-26 by `single-sql-client` (PR #30). The `@neondatabase/serverless` dep was retained in `package.json` because 11 `scripts/*` helpers still imported `neon()` directly (`setup-neon-db.js`, `migrations/2026-05-24-rename-admin-email.js`, `reset-db.js`, plus 8 historical `add-*` / `fix-*` / `seed-*`). Post-`migration-tool` (PR #32), the migration helpers all use `node-pg-migrate`'s `pg` client — not `@neondatabase/serverless` — so the only remaining direct `neon()` consumers are `setup-neon-db.js` (admin seed), `reset-db.js`, and the historical graveyard. The graveyard is no-go-zone; `setup-neon-db.js` and `reset-db.js` could be migrated to `@vercel/postgres` in a single small convoy to fully purge the dep. **Estimate:** 2 file edits + 1 `npm uninstall @neondatabase/serverless` + verify scripts still run against a Neon branch. Low priority — dual deps aren't actively harmful, just untidy. Caveat preserved from `.convoys/single-sql-client.md` § Follow-ups: `@vercel/postgres` is tuned for Vercel's edge / serverless runtime; the right answer may be "keep the dep but route all scripts through a single thin helper" rather than "delete the dep entirely". This is its own scope.
|
||||||
|
- **`wire-migrate-into-ci`** (priority: P2 CI infra). Surfaced 2026-05-26 by `migration-tool` (PR #32) — D6 deferral. Add a CI job that runs `npm run migrate up` against a test DB (either a dedicated Neon branch + `MIGRATE_TEST_DATABASE_URL` secret with branch-reset logic, or a Postgres service container with a ~30s container-start tax). Catches syntactically-invalid migrations + most logical errors at PR time. Currently, the first signal that a new migration is broken is the developer's local `npm run migrate up` against their dev branch (or post-deploy on Vercel). Documented in `.convoys/migration-tool.md` § R3.
|
||||||
|
- **`reconcile-historical-add-scripts`** (priority: P1 quality — needed for fresh-env onboarding). Surfaced 2026-05-26 by `migration-tool` (PR #32). Fold the effects of the 27 historical `scripts/add-*.js` / `fix-*.js` / `seed-*.js` jobs into the migration history so a brand-new Neon branch can be onboarded by `npm install` → `npm run setup-db` alone (without manually replaying the historical scripts). Multi-PR; ideally one migration per logical change, generated by reading the scripts' SQL and re-shaping into idempotent `pgm.sql(...)` blocks (with `IF NOT EXISTS` / `IF EXISTS` guards so re-application is safe). Documented in `.convoys/migration-tool.md` § R1.
|
||||||
|
- **`retire-graveyard-scripts-after-audit`** (priority: P3 polish; **blocked on `reconcile-historical-add-scripts`**). Surfaced 2026-05-26 by `migration-tool` (PR #32). Once the migration history captures all historical effects, the legacy `scripts/add-*.js` / `fix-*.js` / `seed-*.js` files can be deleted (or moved to `scripts/historical/`). They remain no-go-zones until that cleanup convoy lands. Documented in `.convoys/migration-tool.md` § Follow-ups.
|
||||||
|
- **`audit-node-pg-migrate-transitive-deps`** (priority: P3 hygiene). Surfaced 2026-05-26 by `migration-tool` (PR #32) — R5 in the convoy file. `npm audit` reports 11 vulnerabilities (6 moderate, 5 high) coming from `node-pg-migrate@8.0.4`'s `glob@~11.1.0` + `yargs@~17.7.0` transitive deps (older `brace-expansion`, `minimatch`, `picomatch` versions with known advisories). All in dev-only paths; the migration tool runs in scripts/CI, never in the deployed Next.js bundle, and the affected APIs (glob's shell-injection CLI; brace-expansion's ReDoS) are not exercised by node-pg-migrate's call sites. Surface only if a security audit specifically flags this surface, or if `node-pg-migrate` ships a v9 that updates the transitive tree.
|
||||||
|
- **`add-migration-template`** (priority: P3 DX). Surfaced 2026-05-26 by `migration-tool` (PR #32). Add a custom template via `--template-file-name` so generated migrations include the project's preferred docstring shape + a reminder about `docs/SCHEMA_MAP.md` updates. Surface if migration authoring proves inconsistent.
|
||||||
|
|
||||||
## Self-analytics
|
## Self-analytics
|
||||||
|
|
||||||
After each convoy, `scripts/log-convoy-event.sh` emits a record to `.convoys/.metrics.jsonl` (gitignored). After 3-5 convoys, run the upstream `agent-pipeline/analytics/` aggregator to see where token spend goes — that data feeds whether to add or remove rules.
|
After each convoy, `scripts/log-convoy-event.sh` emits a record to `.convoys/.metrics.jsonl` (gitignored). After 3-5 convoys, run the upstream `agent-pipeline/analytics/` aggregator to see where token spend goes — that data feeds whether to add or remove rules.
|
||||||
|
|
|
||||||
|
|
@ -278,19 +278,134 @@ No call sites. No work to do; these symbols disappear when the file is deleted.
|
||||||
|
|
||||||
## As-shipped
|
## As-shipped
|
||||||
|
|
||||||
_Stub for doc-writer post-merge:_
|
Single squash commit `0668b0c` (PR #31, merged 2026-05-27T03:58:08Z
|
||||||
|
UTC / local 2026-05-26). Parent-owned end-to-end per the "Convoy owner"
|
||||||
|
line — no architect, no implementer subagent dispatched. Mirror-the-pattern
|
||||||
|
fix exactly as planned; no mid-execution surprises. **AGENTS.md § 2
|
||||||
|
Architecture quick reference + § 3 Conventions ("Auth (client)") +
|
||||||
|
`.cursor/rules/auth-and-permissions.mdc` swept to describe the
|
||||||
|
post-convoy single-surface state in the same wave.**
|
||||||
|
|
||||||
- Squash commit: `<TBD>`
|
**Diff: 15 files, +341 / -263.** 2 file deletions
|
||||||
- PR: #`<TBD>`
|
(`lib/auth-context.js`, `lib/admin-auth.js`); 12 file modifications
|
||||||
- Files changed: 13 (2 deletions: `lib/auth-context.js`, `lib/admin-auth.js`; 11 modifications:
|
(7 source pages + `.github/CODEOWNERS` + 4 docs / rules / skills);
|
||||||
`pages/_app.js`, `pages/index.js`, `pages/scanner.js`, `pages/decks.js`, `pages/deck/[id].js`,
|
1 new convoy planning file (`.convoys/single-auth-provider.md`).
|
||||||
`pages/deck-builder.js`, `pages/card/[id].js`, `.github/CODEOWNERS`, `AGENTS.md`,
|
|
||||||
`.cursor/rules/auth-and-permissions.mdc`, `.cursor/rules/no-go-zones.mdc`,
|
**The collapse shipped exactly as designed:**
|
||||||
`.cursor/skills/add-page/SKILL.md`).
|
|
||||||
- Verify roundtrip count: documented 3 → 1 on `card/[id].js`, 2 → 1 on every other page-load.
|
1. **`lib/auth-context.js` + `lib/admin-auth.js` deleted.** No
|
||||||
- `.convoys/ship-readiness.md` § P1 entry 9 to be marked RESOLVED with this convoy's squash SHA.
|
replacement; `lib/use-auth.js`'s hook-only `useAuth()` is the sole
|
||||||
- Lint baseline updated 128 → 125 (no regression; 3 fewer errors from deleted unused-import
|
client auth surface.
|
||||||
lines).
|
2. **6 importers of `lib/auth-context.js` swept** per the inventory
|
||||||
|
table — path swap `'../lib/auth-context'` → `'../lib/use-auth'`
|
||||||
|
(each file's relative depth preserved). Destructure pattern
|
||||||
|
(`const { user } = useAuth()` / `const { user, loading } = useAuth()`)
|
||||||
|
stays byte-identical. Affected files: `pages/_app.js`,
|
||||||
|
`pages/index.js`, `pages/scanner.js`, `pages/decks.js`,
|
||||||
|
`pages/deck/[id].js`, `pages/deck-builder.js`.
|
||||||
|
3. **`<AuthProvider>` wrapper removed from `pages/_app.js`.** Per D3:
|
||||||
|
`useAuth()` from `lib/use-auth.js` is hook-only, no Provider
|
||||||
|
needed. `<ThemeProvider>` stays. `<AdminProvider>` was never in
|
||||||
|
the tree to begin with (confirmed by reading `_app.js` pre-convoy
|
||||||
|
— only `<ThemeProvider>` + `<AuthProvider>`).
|
||||||
|
4. **`useIsAdmin()`'s lone consumer inlined.** `pages/card/[id].js`
|
||||||
|
was the only consumer; replaced `const { isAdmin, loading:
|
||||||
|
adminLoading } = useIsAdmin()` with `const isAdmin = user?.role
|
||||||
|
=== 'admin'; const adminLoading = authLoading;` from the existing
|
||||||
|
`useAuth()` call. Rendering condition at line 524 (`{isAdmin &&
|
||||||
|
!adminLoading && (...)}`) unchanged byte-for-byte; the
|
||||||
|
`adminLoading` alias is kept rather than substituting
|
||||||
|
`authLoading` directly to keep the diff minimal.
|
||||||
|
5. **`AdminProvider` and `useAdmin()` had ZERO importers** in the
|
||||||
|
source tree — confirming they were dead exports (only `useIsAdmin`
|
||||||
|
had a live consumer). Deleted together with `lib/admin-auth.js`;
|
||||||
|
no per-file sweep needed for them.
|
||||||
|
6. **Verify roundtrip count reduced 3 → 1** on `pages/card/[id].js`
|
||||||
|
mount, and 2 → 1 on every other page-load. Pre-convoy worst case
|
||||||
|
was `<AuthProvider>` verify + `useAuth()` verify + `useIsAdmin()`
|
||||||
|
verify (3 identical `GET /api/auth/verify` requests, serial cost
|
||||||
|
on cold connection). Post-convoy: single `useAuth()` verify per
|
||||||
|
page; admin status computed synchronously from the same `user`.
|
||||||
|
7. **`.github/CODEOWNERS`** lines for the two deleted files removed.
|
||||||
|
8. **Doc surface updated atomically** (the 4 docs / rules / skills
|
||||||
|
modifications in the diff stat): `AGENTS.md` § 2 (Auth + DB libs
|
||||||
|
row reframed to the post-convoy single-surface state) + § 3
|
||||||
|
("Auth (client)" convention bullet rewritten); `.cursor/rules/auth-and-permissions.mdc`
|
||||||
|
(§ "Legacy" reframed to "deleted by this convoy"; § "Authentication
|
||||||
|
state on the client" rewritten to the post-convoy `useAuth()` shape
|
||||||
|
+ the direct-fetch login flow from `pages/login.js` /
|
||||||
|
`pages/signup.js`); `.cursor/rules/no-go-zones.mdc` (auth-refactors
|
||||||
|
bullet trimmed of the deleted files); `.cursor/skills/add-page/SKILL.md`
|
||||||
|
(checklist + anti-pattern row updated to refer to the deletion).
|
||||||
|
|
||||||
|
**Verification (all gates green at merge):**
|
||||||
|
- `rg "lib/auth-context|lib/admin-auth" --type js` → 0 hits in
|
||||||
|
`pages/`, `lib/`, `components/` post-edit (R3 mitigation —
|
||||||
|
confirms no missed importer; any missed importer would also have
|
||||||
|
surfaced at module-load time during `npm run build` as "Cannot
|
||||||
|
find module", which did not happen).
|
||||||
|
- `npm run lint` → **128 → 125 problems** (3 fewer errors; the
|
||||||
|
deleted files contained 3 unused-import / unused-var lints; no new
|
||||||
|
lint surface introduced). **This is the new lint baseline** for
|
||||||
|
subsequent convoys.
|
||||||
|
- `npm run test:run` → 21/21 pass pre- and post-convoy. The 4 test
|
||||||
|
files don't import any of the deleted modules (`grep`-confirmed
|
||||||
|
pre-convoy); Layout test confirmed unaffected.
|
||||||
|
- `npm run build` → succeeds end-to-end. All 26 pages compile (10
|
||||||
|
dynamic API routes + 16 `pages/**` views including every file
|
||||||
|
modified by the sweep). **No "useAuth must be used within an
|
||||||
|
AuthProvider" runtime error during static generation**, which
|
||||||
|
confirms `<AuthProvider>` removal is safe — no page tried to use
|
||||||
|
the legacy context hook unwrapped.
|
||||||
|
- CI on PR #31: Lint ✓ (125 problems baseline) | Vitest 21/21 ✓ |
|
||||||
|
Playwright smoke 3/3 ✓ | `forbidden-endpoints` ✓ |
|
||||||
|
`forbidden-cors-headers` ✓ | Vercel preview deploy ✓ | Aggregate
|
||||||
|
gate ✓
|
||||||
|
- `Screenshot diff`: triggered (PR #31 touches `pages/**` non-API +
|
||||||
|
some adjacent surface that the visual-diff path filter matches) —
|
||||||
|
`continue-on-error: true` swallow per the documented Decision-4
|
||||||
|
end state of `adopt-playwright-smoke` (no baseline committed yet).
|
||||||
|
|
||||||
|
**Manual smoke deferred** per § Verification plan step 5 — the build
|
||||||
|
pass + vitest pass + zero-hit grep is the gate for merging; the
|
||||||
|
parent did not have a logged-in admin browser session ready in this
|
||||||
|
conversation. Optional post-merge operator sequence: `npm run dev` +
|
||||||
|
exercise dashboard / profile / settings / collections / cards /
|
||||||
|
admin/card-editor to confirm no runtime regression.
|
||||||
|
|
||||||
|
**Cross-validation finding (continues the lineage).** `Playwright
|
||||||
|
smoke` 3/3 PASS on a 15-file sweep confirms the deployed preview is
|
||||||
|
unaffected by the 2-deletion auth-surface collapse. Specifically,
|
||||||
|
smoke test 2 (`'sign-in page renders'`) continues to assert against
|
||||||
|
the `<Link href="/login">Sign in</Link>` CTA introduced by
|
||||||
|
`fix-layout-default-user` (PR #15) — that CTA is unchanged here, and
|
||||||
|
its render path doesn't depend on `<AuthProvider>`. **Ninth
|
||||||
|
consecutive convoy** where the same 3-test smoke spec defends the
|
||||||
|
auth surface (PR #15 → #19 → #20 → #21 → #25 → #32 → #27 → #30 →
|
||||||
|
this PR).
|
||||||
|
|
||||||
|
**Operator action required going forward:** **none.** No env vars,
|
||||||
|
no schema, no infra changes. The single client auth surface is
|
||||||
|
`lib/use-auth.js::useAuth()`; the direct-fetch login flow in
|
||||||
|
`pages/login.js` / `pages/signup.js` is preserved verbatim (no
|
||||||
|
client-side credential handling moved).
|
||||||
|
|
||||||
|
**Spec deviation (documented as the as-shipped reality):** the
|
||||||
|
pre-merge importer estimate was ~30 in `.convoys/ship-readiness.md`
|
||||||
|
P1 #9; actual was 7. This is not a real spec deviation — it's a
|
||||||
|
loose estimate that was correct at the time the estimate was made
|
||||||
|
but became stale once `fix-layout-default-user` (PR #15) migrated
|
||||||
|
most of the tree to `lib/use-auth.js`. The post-flip ship-readiness
|
||||||
|
P1 #9 entry records the actual count.
|
||||||
|
|
||||||
|
**Cross-convoy follow-up (R5 still open).** § Risks R5 — two
|
||||||
|
components on the same page that both call `useAuth()` will issue
|
||||||
|
two verify roundtrips and hold two independent `user` references —
|
||||||
|
is intentionally out of scope. This was true pre-convoy too (the
|
||||||
|
legacy `useIsAdmin` was already a separate verify). Hoisting state
|
||||||
|
into a shared module-level cache or reintroducing a thin
|
||||||
|
`<AuthProvider>` that only hoists state is a separate decision; see
|
||||||
|
§ Follow-ups in this convoy file.
|
||||||
|
|
||||||
## Follow-ups (out of scope here)
|
## Follow-ups (out of scope here)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -376,13 +376,139 @@ action, the rollback is a single-commit revert of this convoy.
|
||||||
|
|
||||||
## As-shipped
|
## As-shipped
|
||||||
|
|
||||||
*Stub for doc-writer.* Populate after merge with:
|
Single squash commit `c403ea4` (PR #30, merged 2026-05-27T03:54:01Z
|
||||||
- Squash commit SHA + PR number
|
UTC / local 2026-05-26). Parent-owned end-to-end per the "Owns" line
|
||||||
- Diff stats (file count, line counts)
|
— no architect, no implementer subagent dispatched. Mirror-the-pattern
|
||||||
- CI gate outcomes (lint, vitest, smoke, forbidden-* gates)
|
fix exactly as planned; no mid-execution surprises that would have
|
||||||
- Any operator-action follow-ups
|
forced an architect bounce. **AGENTS.md Gotcha #1 flipped from open
|
||||||
- Cross-validation findings (e.g. smoke continuing to pass)
|
→ RESOLVED in the post-convoy doc-writer pass** (see
|
||||||
- Updates needed to AGENTS.md Gotcha #1 + `.cursor/rules/db-and-schema.mdc` § Clients + ship-readiness P1 #8 entry
|
`.convoys/ship-readiness.md` § P1 #8 entry post-flip and the AGENTS.md
|
||||||
|
sweep in this same wave).
|
||||||
|
|
||||||
|
**Diff: 4 files, +447 / -64.** The 447-addition figure is dominated
|
||||||
|
by `.convoys/single-sql-client.md` (~330 lines for the planning
|
||||||
|
document, committed atomically with the fix). Actual source-file diff
|
||||||
|
is small: `pages/api/auth-utils.js` +5 / -7 (import swap + 2 query
|
||||||
|
shape conversions); `test/api/auth-utils.test.js` +1 / -5 (drop the
|
||||||
|
now-unused `vi.mock` + unused `vi` import); `lib/database.js` 0 / -47
|
||||||
|
(deletion).
|
||||||
|
|
||||||
|
**The change shipped exactly as designed:**
|
||||||
|
|
||||||
|
1. **`lib/database.js` deleted.** 47-line file; no replacement. The
|
||||||
|
`DatabaseAdapter` abstraction is gone. The two callers go straight
|
||||||
|
to `@vercel/postgres` tagged-template SQL, matching the canonical
|
||||||
|
pattern used by ~30 other `pages/api/**` handlers already in the
|
||||||
|
tree.
|
||||||
|
2. **`pages/api/auth-utils.js`** swept (the only source caller).
|
||||||
|
- Import: `import { db } from '../../lib/database.js'` → `import
|
||||||
|
{ sql } from '@vercel/postgres'`.
|
||||||
|
- `isAdmin(userId)`: `await db.query(\`SELECT role FROM users
|
||||||
|
WHERE id = $1\`, [userId])` → `` await sql`SELECT role FROM
|
||||||
|
users WHERE id = ${userId}` ``.
|
||||||
|
- `getUserById(userId)`: `await db.query(\`SELECT id, email, role,
|
||||||
|
created_at FROM users WHERE id = $1\`, [userId])` → `` await
|
||||||
|
sql`SELECT id, email, role, created_at FROM users WHERE id =
|
||||||
|
${userId}` ``.
|
||||||
|
- The `try/catch` shape, the `result.rows[0]` access, the
|
||||||
|
`?.role === 'admin'` check, and the `console.error` + `return
|
||||||
|
false` / `return null` error paths are all preserved verbatim.
|
||||||
|
- SQL is byte-equivalent (single-parameter numeric `userId`);
|
||||||
|
result shape is identical (`{ rows, rowCount }` from
|
||||||
|
`@vercel/postgres` matches what `DatabaseAdapter.query` was
|
||||||
|
returning).
|
||||||
|
3. **`test/api/auth-utils.test.js`** mock cleanup. Dropped
|
||||||
|
`vi.mock('../../lib/database.js', () => ({ db: { query: vi.fn() } }))`
|
||||||
|
(no longer needed because `auth-utils.js` no longer imports from
|
||||||
|
`lib/database.js`) and the unused `vi` import. The 5 test bodies
|
||||||
|
(2 `generateToken` + 3 `verifyToken`) are unchanged. Test count
|
||||||
|
stays at 21/21.
|
||||||
|
|
||||||
|
**`@neondatabase/serverless` retained as a runtime dep**, per D3 of
|
||||||
|
this convoy file. 11 `scripts/*` helpers still import `neon()`
|
||||||
|
directly (`scripts/setup-neon-db.js`, `scripts/migrations/2026-05-24-rename-admin-email.js`,
|
||||||
|
`scripts/reset-db.js`, plus 8 historical `add-*` / `fix-*` / `seed-*`
|
||||||
|
jobs); all out of scope per the no-go-zones rule. The dep-purge is
|
||||||
|
tracked as the newly queued `purge-neondatabase-serverless-fully`
|
||||||
|
follow-up (now **unblocked** by PR #32 `migration-tool` — the
|
||||||
|
migration helpers go through `node-pg-migrate`'s `pg` client, not
|
||||||
|
`@neondatabase/serverless`, so the only remaining direct `neon()`
|
||||||
|
consumers post-PR-#32 are `setup-neon-db.js`, `reset-db.js`, and the
|
||||||
|
historical graveyard).
|
||||||
|
|
||||||
|
**Verification (all gates green at merge):**
|
||||||
|
- `node --check pages/api/auth-utils.js` → exit 0
|
||||||
|
- `npm run lint` → 125 problems (post-PR-#31 baseline preserved;
|
||||||
|
zero regression). The `lib/database.js` deletion did not change
|
||||||
|
lint count because the file was already lint-clean.
|
||||||
|
- `npm run test:run` → 21/21 pass
|
||||||
|
- `rg "lib/database" --type js -l` → 0 hits anywhere (no `pages/**`,
|
||||||
|
no `lib/**`, no `scripts/**`, no `test/**`) — confirms no missed
|
||||||
|
importer (R2 mitigation)
|
||||||
|
- `rg "@neondatabase/serverless" --type js -l` → still matches
|
||||||
|
`scripts/setup-neon-db.js`,
|
||||||
|
`scripts/migrations/2026-05-24-rename-admin-email.js`,
|
||||||
|
`scripts/reset-db.js`, and the 8 other `scripts/add-*` / `fix-*` /
|
||||||
|
`seed-*` historical helpers (all out of scope per D3)
|
||||||
|
- CI on PR #30: Lint ✓ | Vitest 21/21 ✓ | Playwright smoke 3/3 ✓ |
|
||||||
|
`forbidden-endpoints` ✓ | `forbidden-cors-headers` ✓ | Vercel
|
||||||
|
preview deploy ✓ | Aggregate gate ✓
|
||||||
|
- **`Screenshot diff`: NOT triggered.** PR #30's diff is
|
||||||
|
`pages/api/**` + `lib/**` + `test/**` + `.convoys/**` — the
|
||||||
|
post-PR-#26 `!pages/api/**` exclusion correctly held. **This PR is
|
||||||
|
the first empirical confirmation that the
|
||||||
|
`tighten-visual-diff-path-filter` (PR #26, `ba95462`) exclusion
|
||||||
|
fires as documented** — the post-merge success criterion that
|
||||||
|
PR #26's § Verification plan deferred until "the next API-only PR
|
||||||
|
after this merges". `Screenshot diff` did NOT appear in PR #30's
|
||||||
|
Checks tab; the convoy-file as-shipped block of PR #26 now records
|
||||||
|
this confirmation.
|
||||||
|
|
||||||
|
**Live runtime smoke deferred** per § Verification plan — the two
|
||||||
|
migrated functions (`isAdmin`, `getUserById`) are only reachable via
|
||||||
|
`pages/api/admin/index.js` which requires an admin Bearer token + a
|
||||||
|
populated `users` table. Running a live curl smoke would require
|
||||||
|
seeding the admin user with a known password (operator-only flow per
|
||||||
|
`drop-public-setup`'s contract) and minting a token. The
|
||||||
|
byte-equivalent SQL semantics (D2) plus identical `try/catch` +
|
||||||
|
`result.rows[0]` access shape gives high confidence the migration is
|
||||||
|
correct; if a post-merge Vercel preview admin action 500s, rollback
|
||||||
|
is a single-commit revert.
|
||||||
|
|
||||||
|
**Cross-validation finding (continues the lineage).** The `Playwright
|
||||||
|
smoke` 3/3 PASS confirms the deployed preview is unaffected by the
|
||||||
|
lib-database deletion — the smoke spec doesn't exercise `isAdmin` /
|
||||||
|
`getUserById`, but the auth surface that smoke does exercise
|
||||||
|
(`/api/health`, sign-in render) is correctly insensitive to the
|
||||||
|
DatabaseAdapter removal. **Eighth consecutive convoy** where the
|
||||||
|
same 3-test smoke spec defends the auth surface through a sweeping
|
||||||
|
change (PR #15 → #19 → #20 → #21 → #25 → #32 → #27 → this PR).
|
||||||
|
|
||||||
|
**Operator action required going forward:** **none.** No env-var
|
||||||
|
change; no schema change; no infra change. The migration is purely a
|
||||||
|
client swap; the existing `POSTGRES_URL` contract is preserved
|
||||||
|
verbatim (both `lib/database.js` and `@vercel/postgres` read the same
|
||||||
|
`POSTGRES_URL`).
|
||||||
|
|
||||||
|
**Spec deviation:** none of substance. The convoy spec's "~3 files
|
||||||
|
based on graph" estimate was loose; architect grep (D1) confirmed
|
||||||
|
exactly 2 source-tree callers (1 source + 1 test mock). No deviation
|
||||||
|
flag needed; the convoy is even smaller than estimated. The post-flip
|
||||||
|
ship-readiness P1 #8 entry records this fact explicitly.
|
||||||
|
|
||||||
|
**Doc surface flipped atomically (in the post-convoy doc-writer pass):**
|
||||||
|
`AGENTS.md` Gotcha #1 → RESOLVED with the as-shipped paragraph
|
||||||
|
(tool deletion + retained-dep caveat + the 11-script `neon()` direct
|
||||||
|
consumers list); `AGENTS.md` § 3 "DB access" bullet trimmed (the
|
||||||
|
warning about `lib/database.js` is gone — the file doesn't exist);
|
||||||
|
`.cursor/rules/db-and-schema.mdc` § Clients section refreshed to
|
||||||
|
match (handled in the doc-writer commit alongside this convoy in
|
||||||
|
the 7-wave cleanup).
|
||||||
|
|
||||||
|
**Surfaced follow-ups (newly queued in `.convoys/ship-readiness.md`):**
|
||||||
|
- `purge-neondatabase-serverless-fully` (P3 polish; unblocked by PR #32).
|
||||||
|
- `lint-against-lib-database` (P3 polish; from this convoy's
|
||||||
|
pre-existing § Follow-ups list — would prevent re-introduction).
|
||||||
|
|
||||||
## Follow-ups
|
## Follow-ups
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -243,26 +243,66 @@ architect mid-execution.
|
||||||
|
|
||||||
## As-shipped
|
## As-shipped
|
||||||
|
|
||||||
*(stub — populated by post-merge doc-writer pass)*
|
Single squash commit `ba95462` (PR #26, merged 2026-05-27T03:51:22Z
|
||||||
|
UTC / local 2026-05-26). Parent-owned end-to-end per the convoy spec
|
||||||
|
— no architect, no implementer subagent dispatched. Mirror-the-pattern
|
||||||
|
fix exactly as planned; no mid-execution surprises that would have
|
||||||
|
forced an architect bounce.
|
||||||
|
|
||||||
- Squash commit: `<TBD>`
|
**Diff: 2 files, +279 / -0.** `.github/workflows/visual-diff.yml` +N
|
||||||
- PR: `<TBD>`
|
for the one `!pages/api/**` entry + the inline comment block
|
||||||
- Diff stat: `<TBD>` (expected: 2 files, +N / -0 — `visual-diff.yml`
|
explaining the ordering rule and naming the empirical PRs that
|
||||||
+N for the one entry + comment block; this convoy file +M for the
|
motivated the fix (#19 `cors-tighten`, #20 `add-rate-limiting`);
|
||||||
full planning doc)
|
`.convoys/tighten-visual-diff-path-filter.md` +M for the full planning
|
||||||
- Verification at merge:
|
document, committed atomically with the YAML edit.
|
||||||
- YAML parse: paths list includes `!pages/api/**` immediately after
|
|
||||||
`pages/**`
|
**The change shipped exactly as designed.** The `paths:` list now
|
||||||
- Lint: 128 problems (baseline preserved)
|
deserializes to `['pages/**', '!pages/api/**', 'components/**',
|
||||||
- Vitest: 21/21
|
'styles/**', 'tailwind.config.js', 'postcss.config.js']` (order-sensitive
|
||||||
- All pre-existing CI gates green at merge
|
— GitHub Actions evaluates `paths:` with minimatch and `!`-prefixed
|
||||||
- **Post-merge success criterion** (the deferred verification from
|
exclusions only fire after a prior include matches; keeping
|
||||||
§ Verification plan): the next API-only PR after this merges does
|
`!pages/api/**` immediately after `pages/**` is what makes the
|
||||||
NOT show `Screenshot diff` in its Checks tab. Doc-writer to record
|
exclusion work). All five existing entries preserved verbatim;
|
||||||
that PR's number + the absence of `Screenshot diff` as the
|
`preview-smoke.yml` intentionally untouched (no `paths:` filter; fires
|
||||||
as-shipped success line, mirroring `.convoys/fix-reset-db-script.md`'s
|
on every PR by design — see § The fix § preview-smoke.yml left
|
||||||
*"Screenshot diff: not triggered (script-only PR — `paths:` filter
|
untouched).
|
||||||
excludes `scripts/**`…)"* line.
|
|
||||||
|
**Verification at merge (all gates green):**
|
||||||
|
- YAML parse: `paths:` deserializes to the expected 6-entry list with
|
||||||
|
`'!pages/api/**'` at index 1 (immediately after `'pages/**'`)
|
||||||
|
- `npm run lint` → 128 problems (baseline at convoy time; later
|
||||||
|
improved to 125 by PR #31 `single-auth-provider`)
|
||||||
|
- `npm run test:run` → 21/21 pass
|
||||||
|
- All pre-existing CI gates green at merge
|
||||||
|
|
||||||
|
**Post-merge success criterion — CONFIRMED.** The deferred verification
|
||||||
|
from § Verification plan: the next API-only PR after this merges
|
||||||
|
should NOT show `Screenshot diff` in its Checks tab. **PR #30
|
||||||
|
(`single-sql-client`, squash `c403ea4`, merged 2026-05-27T03:54:01Z
|
||||||
|
UTC) was the first API-only PR post-merge.** Its diff touched
|
||||||
|
`pages/api/auth-utils.js` + `test/api/auth-utils.test.js` +
|
||||||
|
`lib/database.js` (deletion) + `.convoys/single-sql-client.md` — the
|
||||||
|
`pages/api/**` portion matched `pages/**` but was correctly subtracted
|
||||||
|
by the new `!pages/api/**` exclusion. **`Screenshot diff` did NOT
|
||||||
|
appear in PR #30's Checks tab** — the empirical confirmation that the
|
||||||
|
exclusion fires as documented. This is the as-shipped success line
|
||||||
|
the convoy file's § Verification plan asked the doc-writer pass to
|
||||||
|
record, mirroring `.convoys/fix-reset-db-script.md`'s *"Screenshot
|
||||||
|
diff: not triggered (script-only PR — `paths:` filter excludes
|
||||||
|
`scripts/**`…)"* line.
|
||||||
|
|
||||||
|
**Operator action required going forward:** **none.** Path-filter
|
||||||
|
behavior is self-defending; no env vars, no secrets, no infra
|
||||||
|
changes.
|
||||||
|
|
||||||
|
**Spec deviation:** none.
|
||||||
|
|
||||||
|
**No follow-up surfaced.** The orthogonal `seed-visual-baselines-on-linux`
|
||||||
|
follow-up was already queued by `adopt-playwright-smoke`; this convoy
|
||||||
|
didn't add or change anything around it. The `R1 fallback if exclusion
|
||||||
|
didn't fire → restructure to per-feature path globs` contingency from
|
||||||
|
the convoy file was NOT exercised because the post-merge verification
|
||||||
|
on PR #30 confirmed the negated-glob fired correctly.
|
||||||
|
|
||||||
## Owns
|
## Owns
|
||||||
|
|
||||||
|
|
|
||||||
10
AGENTS.md
10
AGENTS.md
|
|
@ -17,7 +17,7 @@ Guidance for agents and humans working in this repo. Prefer existing patterns ov
|
||||||
A web app for managing trading-card-game collections (Magic, Pokémon, Lorcana). Users authenticate, build collections + decks, scan physical cards via a camera+AI-OCR flow, and share publicly. Admin users curate the card database.
|
A web app for managing trading-card-game collections (Magic, Pokémon, Lorcana). Users authenticate, build collections + decks, scan physical cards via a camera+AI-OCR flow, and share publicly. Admin users curate the card database.
|
||||||
|
|
||||||
- **Framework:** Next.js 16 (Pages router) + React 18, JavaScript (not TypeScript — see Gotcha #9)
|
- **Framework:** Next.js 16 (Pages router) + React 18, JavaScript (not TypeScript — see Gotcha #9)
|
||||||
- **Data:** Neon Postgres, accessed two different ways — `@neondatabase/serverless` (`lib/database.js`) AND raw `@vercel/postgres` (`pages/api/**`). Pick ONE; see Gotcha #1.
|
- **Data:** Neon Postgres. The runtime auth surface uses `@vercel/postgres` tagged-template SQL exclusively post-`single-sql-client` (PR #30, `c403ea4`; `lib/database.js` deleted). 11 `scripts/**` helpers (`setup-neon-db.js`, `reset-db.js`, `migrations/2026-05-24-rename-admin-email.js`, plus 8 historical add-*/fix-*/seed-* jobs) still use `@neondatabase/serverless`'s `neon()` directly — out-of-scope per the no-go-zones rule and tracked as the queued `purge-neondatabase-serverless-fully` follow-up. Schema changes ship as `node-pg-migrate` migrations under `migrations/` at the repo root post-`migration-tool` (PR #32, `de9f334`) — see § 3 Conventions § "Schema changes" and Gotcha #6.
|
||||||
- **Auth:** Custom JWT (jsonwebtoken + bcryptjs), token stored in `localStorage`, sent as `Authorization: Bearer …`. No NextAuth. The secret + canonical 24h TTL come from `lib/auth-secret.js` (single source of truth; throws at module load if `JWT_SECRET` is unset). `getUserFromRequest` returns `null` for unauthenticated requests — no synthetic admin fallback — and login + register are rate-limited (5 attempts / 15 min via `@upstash/ratelimit`). The seed admin row is created at `admin@deckhearth.com` with a password supplied via the required `ADMIN_INITIAL_PASSWORD` env var (`scripts/setup-neon-db.js` exits with code 1 before touching the DB if the var is unset); no credential ships in the source tree. Operators of envs that pre-date the `drop-public-setup` convoy still have the old `admin123` hash in their DB — rotate manually via the app (see Gotcha #4).
|
- **Auth:** Custom JWT (jsonwebtoken + bcryptjs), token stored in `localStorage`, sent as `Authorization: Bearer …`. No NextAuth. The secret + canonical 24h TTL come from `lib/auth-secret.js` (single source of truth; throws at module load if `JWT_SECRET` is unset). `getUserFromRequest` returns `null` for unauthenticated requests — no synthetic admin fallback — and login + register are rate-limited (5 attempts / 15 min via `@upstash/ratelimit`). The seed admin row is created at `admin@deckhearth.com` with a password supplied via the required `ADMIN_INITIAL_PASSWORD` env var (`scripts/setup-neon-db.js` exits with code 1 before touching the DB if the var is unset); no credential ships in the source tree. Operators of envs that pre-date the `drop-public-setup` convoy still have the old `admin123` hash in their DB — rotate manually via the app (see Gotcha #4).
|
||||||
- **UI:** Tailwind CSS + custom CSS variables for theming (light/dark via `lib/theme-context.js`)
|
- **UI:** Tailwind CSS + custom CSS variables for theming (light/dark via `lib/theme-context.js`)
|
||||||
- **Hosting:** Vercel (`vercel.json`, `.vercel/` present)
|
- **Hosting:** Vercel (`vercel.json`, `.vercel/` present)
|
||||||
|
|
@ -29,7 +29,7 @@ A web app for managing trading-card-game collections (Magic, Pokémon, Lorcana).
|
||||||
| Pages router views | `pages/*.js` | Public + auth views; uses `components/Layout.js` |
|
| Pages router views | `pages/*.js` | Public + auth views; uses `components/Layout.js` |
|
||||||
| API routes | `pages/api/**/*.js` | Express-style `handler(req, res)`. **30+ handlers depend on `lib/permission-middleware.js::getUserFromRequest`** |
|
| API routes | `pages/api/**/*.js` | Express-style `handler(req, res)`. **30+ handlers depend on `lib/permission-middleware.js::getUserFromRequest`** |
|
||||||
| Shared UI | `components/*.js` | `Layout`, `CardItem`, `CameraScanner`, modal family |
|
| Shared UI | `components/*.js` | `Layout`, `CardItem`, `CameraScanner`, modal family |
|
||||||
| Auth + DB libs | `lib/*.js` | `use-auth` (canonical client hook — sole surface post-`single-auth-provider`), `database`, `permission-middleware` |
|
| Auth + DB libs | `lib/*.js` | `use-auth` (canonical client hook — sole surface post-`single-auth-provider`, PR #31, `0668b0c`), `auth-secret` (single JWT_SECRET + TTL source), `permission-middleware` (server-side `getUserFromRequest` + `withCollectionPermission`), `rate-limit` (5 named limiters — see Gotcha #12). The legacy `lib/database.js` was deleted by `single-sql-client` (PR #30, `c403ea4`); DB access now goes through `@vercel/postgres` tagged templates directly. |
|
||||||
| Migration scripts | `scripts/*.js` | 27+ one-off "add column" / "seed" scripts. No formal migration tool |
|
| Migration scripts | `scripts/*.js` | 27+ one-off "add column" / "seed" scripts. No formal migration tool |
|
||||||
| Card-import jobs | `pages/api/cards/import-*.js`, `scripts/import-*.js` | Scryfall / Lorcana / Pokémon TCG APIs |
|
| Card-import jobs | `pages/api/cards/import-*.js`, `scripts/import-*.js` | Scryfall / Lorcana / Pokémon TCG APIs |
|
||||||
| Database schema | `scripts/setup-neon-db.js` | Bootstrap SQL DDL — the source of truth until a real migration tool lands |
|
| Database schema | `scripts/setup-neon-db.js` | Bootstrap SQL DDL — the source of truth until a real migration tool lands |
|
||||||
|
|
@ -40,13 +40,13 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560
|
||||||
## 3. Key conventions
|
## 3. Key conventions
|
||||||
|
|
||||||
- **Auth (server):** `import { getUserFromRequest } from '../../lib/permission-middleware'` → returns `{ userId, email, role }` or `null`. `null` means "send 401" — always early-return when the user is null before doing any work that depends on their identity.
|
- **Auth (server):** `import { getUserFromRequest } from '../../lib/permission-middleware'` → returns `{ userId, email, role }` or `null`. `null` means "send 401" — always early-return when the user is null before doing any work that depends on their identity.
|
||||||
- **Auth (client):** `import { useAuth } from '../lib/use-auth'`. Returns `{ user, loading, logout, refreshAuth }`; `user === null` means logged out, `loading === true` means token verification in flight. There is no client-side admin hook — compute `const isAdmin = user?.role === 'admin'` from the same `useAuth()` call. The legacy `lib/auth-context.js` and `lib/admin-auth.js` were deleted by the `single-auth-provider` convoy; do not reintroduce a `<AuthProvider>` / `<AdminProvider>` wrapper in `pages/_app.js`.
|
- **Auth (client):** `import { useAuth } from '../lib/use-auth'` is the only client auth surface. Returns `{ user, loading, logout, refreshAuth }`; `user === null` means logged out, `loading === true` means token verification in flight. There is no client-side admin hook — compute `const isAdmin = user?.role === 'admin'` from the same `useAuth()` call. The legacy `lib/auth-context.js` + `lib/admin-auth.js` were deleted by `single-auth-provider` (PR #31, `0668b0c`); do not reintroduce a `<AuthProvider>` / `<AdminProvider>` wrapper in `pages/_app.js`. The login + signup flow uses direct `fetch('/api/auth/{login,register}')` from `pages/login.js` / `pages/signup.js` — there is no `useAuth().login(...)` / `useAuth().register(...)` method; do not add one.
|
||||||
- **Layout `user` prop:** pages should pass `user` from `useAuth()` to `<Layout>`. Layout's default is `null` and renders a logged-out "Sign in" CTA when no user is supplied — both paths are valid (some surfaces like `pages/invite/{accept,decline}.js` legitimately render Layout for anonymous visitors). Do not reintroduce a hardcoded user object as a default prop.
|
- **Layout `user` prop:** pages should pass `user` from `useAuth()` to `<Layout>`. Layout's default is `null` and renders a logged-out "Sign in" CTA when no user is supplied — both paths are valid (some surfaces like `pages/invite/{accept,decline}.js` legitimately render Layout for anonymous visitors). Do not reintroduce a hardcoded user object as a default prop.
|
||||||
- **JWT secret + TTL:** `import { JWT_SECRET, JWT_TOKEN_TTL } from '../../lib/auth-secret.js'`. This is the only place either value is defined; do not reintroduce literal fallbacks. `JWT_TOKEN_TTL = '24h'` is canonical.
|
- **JWT secret + TTL:** `import { JWT_SECRET, JWT_TOKEN_TTL } from '../../lib/auth-secret.js'`. This is the only place either value is defined; do not reintroduce literal fallbacks. `JWT_TOKEN_TTL = '24h'` is canonical.
|
||||||
- **Auth helper (token mint / verify / password hash):** `import { ... } from '../../pages/api/auth-utils'` (`generateToken`, `verifyToken`, `hashPassword`, `verifyPassword`). Reads the secret + TTL from `lib/auth-secret.js` under the hood.
|
- **Auth helper (token mint / verify / password hash):** `import { ... } from '../../pages/api/auth-utils'` (`generateToken`, `verifyToken`, `hashPassword`, `verifyPassword`). Reads the secret + TTL from `lib/auth-secret.js` under the hood.
|
||||||
- **Rate limiting:** `import { checkAuthRateLimit } from '../../lib/rate-limit.js'` for any new auth-surface endpoint (`/api/auth/login` + `/api/auth/register` already wired). Returns `{ allowed, remaining, reset }`; on `!allowed` return 429 with a `Retry-After` header. See `.cursor/rules/api-routes.mdc` § "Rate limiting" for the verbatim shape.
|
- **Rate limiting:** `import { checkAuthRateLimit } from '../../lib/rate-limit.js'` for any new auth-surface endpoint (`/api/auth/login` + `/api/auth/register` already wired). Returns `{ allowed, remaining, reset }`; on `!allowed` return 429 with a `Retry-After` header. See `.cursor/rules/api-routes.mdc` § "Rate limiting" for the verbatim shape.
|
||||||
- **Permission gate for collection routes:** wrap handlers with `withCollectionPermission('viewer' | 'editor' | 'owner')` from `lib/permission-middleware.js`.
|
- **Permission gate for collection routes:** wrap handlers with `withCollectionPermission('viewer' | 'editor' | 'owner')` from `lib/permission-middleware.js`.
|
||||||
- **DB access:** Use **tagged-template** style — `import { sql } from '@vercel/postgres'`. Avoid the legacy `lib/database.js` `db.query(string, params)` API; its parameter interpolation uses `sql.unsafe` and is a SQL-injection vector.
|
- **DB access:** Use **tagged-template** style — `import { sql } from '@vercel/postgres'`. The legacy `lib/database.js` (`db.query(string, params)` wrapper around `@neondatabase/serverless`, which interpolated params into a string and called `sql.unsafe`) was deleted by `single-sql-client` (PR #30, `c403ea4`); do NOT reintroduce that shape. For `scripts/**` helpers that legitimately need the Neon HTTP driver (e.g. `setup-neon-db.js`, `reset-db.js`), import `{ neon } from '@neondatabase/serverless'` directly and use tagged-template SQL (`await sql\`...\``) — the safe shape, not the wrapper's unsafe shape.
|
||||||
- **Activity logging:** `logCollectionActivity(collectionId, userId, action, details)` — call it from any handler that mutates a collection.
|
- **Activity logging:** `logCollectionActivity(collectionId, userId, action, details)` — call it from any handler that mutates a collection.
|
||||||
- **File names:** `kebab-case.js` for libs/scripts; `PascalCase.js` for React components.
|
- **File names:** `kebab-case.js` for libs/scripts; `PascalCase.js` for React components.
|
||||||
- **Imports:** No path aliases configured; use relative imports.
|
- **Imports:** No path aliases configured; use relative imports.
|
||||||
|
|
@ -56,7 +56,7 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560
|
||||||
|
|
||||||
## 4. Common gotchas
|
## 4. Common gotchas
|
||||||
|
|
||||||
- **#1 — Two SQL clients live in parallel.** `@neondatabase/serverless` (used by `lib/database.js`) and `@vercel/postgres` (used by most `pages/api/**` handlers). New code: prefer `@vercel/postgres` tagged templates. Migration to a single client is tracked in `.convoys/`.
|
- **#1 — Two SQL clients live in parallel. RESOLVED** by `single-sql-client` convoy (PR #30, squash commit `c403ea4`, 2026-05-26). `lib/database.js` is deleted; the 2 callers (`pages/api/auth-utils.js` source + `test/api/auth-utils.test.js` mock) migrated to `@vercel/postgres` tagged templates (byte-equivalent SQL semantics for the two single-parameter SELECT queries). The convoy's architect audit (D4) confirmed no current call site actually exercised the `sql.unsafe` injection vector — the `userId` callers passed a numeric SERIAL from a verified JWT — so this was foot-gun removal rather than a live security finding. **`@neondatabase/serverless` is still in `package.json` as a runtime dep** because 11 `scripts/*` helpers continue to use `neon()` directly (`setup-neon-db.js`, `migrations/2026-05-24-rename-admin-email.js`, `reset-db.js`, plus 8 historical `add-*` / `fix-*` / `seed-*` jobs). Those scripts use the safe tagged-template shape (`await sql\`...\``), not the deleted wrapper's unsafe `db.query(string, params)` shape. Full dep purge is tracked as the queued `purge-neondatabase-serverless-fully` follow-up (now unblocked by `migration-tool` PR #32 — the migration helpers all use `node-pg-migrate`'s `pg` client, not `@neondatabase/serverless`, so the only remaining direct `neon()` consumers are `setup-neon-db.js` (admin seed), `reset-db.js`, and the historical graveyard). Entry kept (not renumbered) to preserve cross-references.
|
||||||
- **#2 — `getUserFromRequest` synthetic-admin fallback. RESOLVED** by `fix-auth-bypass` Brief 2 (commit `258e479`). The helper now returns `null` for unauthenticated requests; `pages/api/auth/verify.js` returns 401 on the no-token branch. The 16 unit tests in `test/lib/permission-middleware.test.js` lock in the contract, including a negative regression against the old synthetic-admin shape. Entry kept (not renumbered) to preserve the audit trail and stable cross-references.
|
- **#2 — `getUserFromRequest` synthetic-admin fallback. RESOLVED** by `fix-auth-bypass` Brief 2 (commit `258e479`). The helper now returns `null` for unauthenticated requests; `pages/api/auth/verify.js` returns 401 on the no-token branch. The 16 unit tests in `test/lib/permission-middleware.test.js` lock in the contract, including a negative regression against the old synthetic-admin shape. Entry kept (not renumbered) to preserve the audit trail and stable cross-references.
|
||||||
- **#3 — JWT_SECRET hardcoded across 7 files. RESOLVED** by `fix-auth-bypass` Brief 1 (commit `4a10dce`). `lib/auth-secret.js` is now the single source of truth and throws at module load when `JWT_SECRET` is unset. Canonical TTL is `JWT_TOKEN_TTL = '24h'`. The `'your-secret-key-change-in-production'` literal is gone from all 7 sites; CI lint passes against the post-fix tree. Entry kept (not renumbered) to preserve cross-references.
|
- **#3 — JWT_SECRET hardcoded across 7 files. RESOLVED** by `fix-auth-bypass` Brief 1 (commit `4a10dce`). `lib/auth-secret.js` is now the single source of truth and throws at module load when `JWT_SECRET` is unset. Canonical TTL is `JWT_TOKEN_TTL = '24h'`. The `'your-secret-key-change-in-production'` literal is gone from all 7 sites; CI lint passes against the post-fix tree. Entry kept (not renumbered) to preserve cross-references.
|
||||||
- **#4 — Default admin credentials in the seed. RESOLVED** by `drop-public-setup` Brief 1 (commit `ff80753`) + Brief 2 (commit `b63b509`). `scripts/setup-neon-db.js` no longer hardcodes `admin123`; it reads `ADMIN_INITIAL_PASSWORD` from the environment and exits with code 1 before opening a DB connection if the var is unset. README's "Default Admin Account" section is replaced with "First-time admin setup" copy that documents the env var, `openssl rand -base64 24` generation tip, and CI-secret alternative. Brief 2 converted the script from CJS to ESM so `npm run setup-db` actually runs on Node 22.x (the `bump-next-js` convoy's `"type": "module"` flag had silently broken it). **Operator caveat:** the seed is idempotent (`ON CONFLICT (email) DO NOTHING`); re-running setup-db on an env that already has the admin row does NOT rotate the password. Any deployed env that ran setup before this convoy still has the weak `admin123` hash — operators must rotate manually via the app, or wait for the queued `rotate-default-admin` follow-up convoy. Entry kept (not renumbered) to preserve cross-references.
|
- **#4 — Default admin credentials in the seed. RESOLVED** by `drop-public-setup` Brief 1 (commit `ff80753`) + Brief 2 (commit `b63b509`). `scripts/setup-neon-db.js` no longer hardcodes `admin123`; it reads `ADMIN_INITIAL_PASSWORD` from the environment and exits with code 1 before opening a DB connection if the var is unset. README's "Default Admin Account" section is replaced with "First-time admin setup" copy that documents the env var, `openssl rand -base64 24` generation tip, and CI-secret alternative. Brief 2 converted the script from CJS to ESM so `npm run setup-db` actually runs on Node 22.x (the `bump-next-js` convoy's `"type": "module"` flag had silently broken it). **Operator caveat:** the seed is idempotent (`ON CONFLICT (email) DO NOTHING`); re-running setup-db on an env that already has the admin row does NOT rotate the password. Any deployed env that ran setup before this convoy still has the weak `admin123` hash — operators must rotate manually via the app, or wait for the queued `rotate-default-admin` follow-up convoy. Entry kept (not renumbered) to preserve cross-references.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue