docs: post-convoy cleanup for fix-layout-default-user (+ queue CI-protection follow-up)

Reflects the merged fix-layout-default-user convoy (PR #15) and the
companion CI permissions fix (PR #16) in repo documentation. Also
queues the new fix-vercel-deployment-protection-in-ci convoy that
PR #16 exposed.

.convoys/ship-readiness.md:
  - P0 #7: mark RESOLVED 2026-05-24 with squash commit ca302a8.
    Document the as-shipped Layout default-null change, the 7-page
    sweep, the 5 new regression-lock vitest assertions, and the
    queued follow-ups (single-auth-provider, MobileNavigation
    cleanup) that stayed explicitly out of scope.
  - Queued convoys: add fix-vercel-deployment-protection-in-ci
    (P2, CI infra) — PR #16's permissions fix exposed that Vercel
    Deployment Protection 401s anonymous CI requests; needs a
    bypass-secret plumb to land cleanly. New section also captures
    other in-flight follow-ups (rotate-default-admin, cors-tighten,
    add-rate-limiting, purge-weak-creds-from-helpers,
    single-auth-provider, cleanup-mobile-nav-dead-props,
    bump-eslint-10) so the audit trail is centralized.

AGENTS.md:
  - § 4 Gotcha #8: mark RESOLVED with commit ref ca302a8. Mirror
    the convention used by the prior cleanup commits for #2, #3,
    #4, #5 (entry kept, not renumbered).
  - § 3 Key conventions: add a new "Layout user prop" bullet
    documenting the new default-null + logged-out-CTA contract so
    the convention is discoverable from the conventions list, not
    just the resolved-gotcha entry.

.convoys/fix-vercel-deployment-protection-in-ci.md (new):
  - Queued scaffold. Operator must seed
    VERCEL_AUTOMATION_BYPASS_SECRET as a repo secret before the
    implementer can run. Decisions to ratify (query param vs.
    header), known constraints, acceptance criteria, and
    out-of-scope all enumerated.

No changes to: package.json, lib/**, pages/**, components/**,
scripts/**, .github/**, README.md, .cursor/rules/**.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-05-24 14:37:39 -05:00
parent ca302a89c1
commit b7ddd0895e
3 changed files with 268 additions and 2 deletions

View file

@ -0,0 +1,239 @@
---
name: fix-vercel-deployment-protection-in-ci
classification: convoy
success_metric: |
`Playwright smoke` and `Screenshot diff` workflows reach their actual
smoke / capture step on every PR (no more 401-from-Vercel-SSO 10-min
timeouts). Both workflows complete in < 5 minutes. Failures, when they
occur, are real assertion failures — not auth failures against the
preview URL.
skip:
- role-design-system-auditor # CI infra only
- role-a11y-auditor # no a11y surface
- role-ux-reviewer # no UX surface
- role-ia-architect # no IA surface
- browser-smoke # the convoy IS the smoke pipeline
status: queued
created: 2026-05-24
parent: ship-readiness
addresses: P0 #7 (CI infrastructure side-effect)
depends_on:
- fix-layout-default-user (shipped — PR #15, ca302a8)
- PR #16 fix(ci) scoped permissions (shipped — 7e97254)
---
# Fix Vercel Deployment Protection in CI
Plumb `VERCEL_AUTOMATION_BYPASS_SECRET` into the `Playwright smoke` and
`Screenshot diff` workflows so anonymous GitHub Actions runners can
actually GET the preview URL without hitting Vercel's SSO 401 challenge.
Without this, both workflows permanently red on every PR — just slower
red than before PR #16.
## Why now
PR #16 (`fix(ci): scoped permissions`, squash commit `7e97254`) added
minimal scoped `permissions:` blocks to `.github/workflows/preview-smoke.yml`
and `.github/workflows/visual-diff.yml`. That fixed the 5-second 403
"Resource not accessible by integration" failure both workflows hit when
trying to call the GitHub deployments API. **However**, with permissions
correct, both workflows now reach the actual deployment check and fail
with a different error: a 10-minute timeout from
`patrickedqvist/wait-for-vercel-preview@v1.3.2`'s subsequent HTTP GET
against the preview URL, which Vercel returns 401 for because Deployment
Protection is on (anonymous GitHub-runner request → Vercel SSO challenge).
Cost: ~10 minutes of runner time per workflow per PR — and zero signal,
since neither workflow ever reaches its smoke step. This blocks PR #15's
recurring follow-up convoys (visual-regression baselines, Playwright smoke
for `adopt-playwright-smoke`) from getting any CI feedback.
The bypass token already exists locally as `VERCEL_AUTOMATION_BYPASS_SECRET`
in `.env.local` (Protection Bypass for Automation, configured in the
Vercel project). Documented in `AGENTS.md` § 7 — Deployment. The work here
is plumbing it from operator-supplied repo secret → workflow env →
`wait-for-vercel-preview`'s `path:` input + the eventual Playwright
`BASE_URL` so anonymous runner requests bypass the SSO challenge.
## Operator action required (BEFORE this convoy can run)
This convoy CANNOT proceed without the operator first seeding the secret
into GitHub Actions. The implementer has nothing to wire up if the
secret isn't visible to the workflows.
1. **Seed the secret:**
```bash
gh secret set VERCEL_AUTOMATION_BYPASS_SECRET --body "<value from local .env.local>"
```
(The value is whatever `VERCEL_AUTOMATION_BYPASS_SECRET=…` says in
`.env.local`. Do not paste it anywhere logged. Do not echo it from a
workflow step.)
2. **Confirm visibility:**
```bash
gh secret list
```
Expect to see `VERCEL_AUTOMATION_BYPASS_SECRET` listed alongside the
existing repo secrets. Note: `gh secret list` shows names only — never
values — by design.
3. **Notify the next agent** that steps 1 + 2 are done. The convoy file's
frontmatter `status:` should flip from `queued` to `in-progress` only
after this notification.
This is the same pattern `npm run setup-db`'s `ADMIN_INITIAL_PASSWORD`
established (`drop-public-setup` Brief 1, commit `ff80753`): CI / scripts
that need a secret get an actionable fail-loud error when the secret is
missing, and the operator seeds it once per environment.
## Decisions to ratify with operator
Queued; do not pre-decide.
1. **Bypass via query param vs. request header.**
- **Option A — query param.** Append `?x-vercel-protection-bypass=...&x-vercel-set-bypass-cookie=true`
to the wait-action's `path:` input AND to the Playwright `BASE_URL`.
The first request sets a `_vercel_jwt` cookie on the runner's
ephemeral browser context; subsequent same-origin requests reuse it.
Pro: works with any HTTP client, no custom config in Playwright.
Con: the bypass token shows up in workflow run logs if any step
echoes the URL (mitigation: never `echo` or `cat` a URL containing
the token; log `${{ steps.wait.outputs.url }}` only after stripping
the query string).
- **Option B — request header (`x-vercel-protection-bypass: <secret>`).**
Cleaner — the token never appears in any URL. But requires custom
HTTP-client config in `playwright.config.js` (`extraHTTPHeaders`)
AND in `wait-for-vercel-preview` (the action's docs need confirming —
header support may not be exposed via inputs).
2. **CI assertion that bypass actually works.** Should we add a step
that explicitly asserts `200` on the preview URL during the wait-
action's healthcheck phase, before handing off to Playwright /
screenshot capture? This would surface bypass-misconfiguration as a
fast-fail step instead of letting Playwright time out 8 minutes
later on a different error. Cost: ~5 lines of YAML; benefit: clearer
failure signal for the next operator-touch event.
3. **Workflow concurrency cancellation.** The workflows already use
`concurrency:` keyed on `github.ref`. Confirm that the bypass-token
wiring doesn't inadvertently break the cancel-stale behavior (e.g.
if the `secrets.VERCEL_AUTOMATION_BYPASS_SECRET` reference is in a
`concurrency:` expression, that's a syntax error and the implementer
should pull it into a job-level `env:` instead).
## Scope
**In scope:**
- `.github/workflows/preview-smoke.yml` — wire the bypass into the
`wait-for-vercel-preview` step's `path:` input (Option A) OR add the
bypass header via the action's input shape (Option B, pending
confirmation that the action exposes header inputs).
- `.github/workflows/visual-diff.yml` — same treatment as preview-smoke
(the two workflows have similar shapes; whatever pattern works for one
should land in both).
- `playwright.config.js` (when it exists — the `adopt-playwright-smoke`
convoy ships it) — add `use: { extraHTTPHeaders: { 'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET } }`
if Decision #1 picks Option B; OR build the BASE_URL with the query
param (Option A).
- Any test-setup file or helper that constructs the preview URL for
`screenshot-diff`-style workflows.
**Out of scope:**
- Writing new Playwright tests. Test authoring lives in
`adopt-playwright-smoke`. This convoy only makes the existing smoke
pipeline reachable.
- Broadening workflow `permissions:` blocks. PR #16 already landed the
minimal scope; this convoy should not need to touch them again.
- Replacing `patrickedqvist/wait-for-vercel-preview` with a different
action. The action retrieves the URL successfully (confirmed in PR #16's
run logs); the failure is the subsequent HTTP GET, which is a
configuration issue, not an action choice. A wholesale action swap is
a deeper rewrite — separate convoy if/when it's needed.
- Authoring new visual-regression baselines. The screenshot diff workflow
has nothing meaningful to compare against today; baseline authoring is
its own convoy.
- Disabling Vercel Deployment Protection on the project. Operator may
prefer to keep protected previews (cheap defense-in-depth against
preview-URL leakage); this fix lets CI work _around_ the protection
without weakening it.
## Known constraints
- **`wait-for-vercel-preview@v1.3.2` `path:` input is supported.** PR
#16's run logs confirm the action retrieves the URL successfully — the
subsequent HTTP GET is what fails. The action's `path:` input accepts a
full path including query string, so Option A (`?x-vercel-protection-bypass=...`)
is mechanically straightforward. Whether the action exposes a way to
inject custom request headers (Option B) needs to be confirmed by
reading the action's source / README before the implementer commits to
it.
- **The same secret will need to be plumbed into Playwright's `BASE_URL`
or into a request header in `playwright.config.js`** when the
`adopt-playwright-smoke` convoy ships. Coordinating shape now (this
convoy) vs. shape later (when Playwright lands) saves churn — the
implementer should pick whichever option keeps both call sites
consistent.
- **`npm run setup-db`'s `ADMIN_INITIAL_PASSWORD` is a parallel
precedent** for "CI needs a secret the operator must seed." Same
pattern applies: secret is repo-scoped, fail-loud (or fail-noisy) when
unset, never echoed to logs. See `drop-public-setup` Brief 1
(commit `ff80753`).
- **Token rotation.** The Vercel bypass token can be rotated from the
Vercel dashboard. If/when that happens, the operator must re-seed the
GitHub secret (`gh secret set ...`). No automation here — this is a
human responsibility per the same pattern as `JWT_SECRET` rotation.
## Acceptance criteria
The convoy is shippable when ALL of the following hold:
1. `Playwright smoke` workflow reaches its actual smoke step on a fresh
PR. It either passes (smoke green) OR fails on a real assertion
(Playwright reports a test failure or a runtime error from the
smoke spec). It does NOT fail with a 10-min timeout from the
`wait-for-vercel-preview` step or with a 401 from the preview URL.
2. `Screenshot diff` workflow reaches its screenshot capture step and
posts the "Visual Diff" comment to the PR (even if the diff itself
is empty / first-run / null-baseline). Same constraint: no 10-min
timeout, no 401.
3. Both workflows complete in < 5 minutes on a typical PR (the
pre-PR-16 baseline was ~30 seconds for the workflow body; adding a
bypass query string or header shouldn't materially affect runtime).
4. The bypass token does not appear in any workflow run log. Verify by
downloading the raw log of a passing run and grepping for the token's
first 8 chars.
5. Workflow YAML still passes basic actionlint review (`actionlint .github/workflows/*.yml`
exits 0). PR #16's permissions blocks remain unchanged.
6. `AGENTS.md` § 7 deployment paragraph (the "Preview protection bypass
for automation" line) still reflects reality after the change. May
need a one-sentence update if the implementer picks Option B
(`x-vercel-protection-bypass` header) vs. Option A (query string).
## Anything flagged but not acted on (in advance)
These are real findings that the architect / implementer should NOT try
to solve in this convoy. Each is queued separately if it warrants a fix.
- **The `wait-for-vercel-preview` action is no longer maintained** (last
release Mar 2024; no v2). Could be replaced with a few lines of
`gh api` + `curl`-loop in the workflow itself. Not in scope here —
this convoy needs to fix the immediate auth failure, not rewrite the
wait logic. Queue as `replace-wait-for-vercel-preview` if the action
ages out further or has a security advisory.
- **Playwright config doesn't exist yet.** `playwright.config.js`,
`tests/smoke/`, and `@playwright/test` all land in
`adopt-playwright-smoke` (P1 #10 step 2 / launch sequence step 10).
Until that convoy ships, the only `Playwright smoke` workflow body is
a no-op. This convoy can pre-wire the bypass infrastructure (env var,
workflow secrets) so `adopt-playwright-smoke` only needs to add the
test files and the Playwright config — but it can't ship a real
smoke-pass without that follow-up.
- **`Screenshot diff` baseline authoring.** Even after this convoy lands,
the visual-diff workflow has nothing to compare against on its first
run. That's expected and orthogonal — baseline authoring is a separate
scope.
- **Operator-rotation hygiene for `VERCEL_AUTOMATION_BYPASS_SECRET`.**
Vercel's bypass tokens don't auto-expire. If the team wants a periodic
rotation policy, that's an ops-runbook concern outside this convoy.
- **`AGENTS.md` § 7 wording.** The current "Smoke/visual-diff workflows
pass this header (`x-vercel-protection-bypass`)" line in § 7 is
aspirational — it describes intent, not what was actually wired. After
this convoy ships, that line becomes accurate. The doc-writer pass at
convoy close should reword to past-tense reality.

View file

@ -88,11 +88,24 @@ These MUST land before any anonymous traffic touches the production URL.
- **As-shipped:** `lib/rate-limit.js` (new) provides `checkAuthRateLimit(req)` via `@upstash/ratelimit@^2.0.8` + `@upstash/redis@^1.38.0` (5 attempts / 15-min sliding window per IP). Wired into login + register. Env vars are `KV_REST_API_URL` / `KV_REST_API_TOKEN` (auto-provisioned by Vercel's Upstash Marketplace integration — note this is a rename from the brief's original `UPSTASH_REDIS_REST_*` spec; see `.convoys/fix-auth-bypass/brief-4-tighten-auth-surface.md` § Post-merge addendum). Fails closed in prod when env vars are unset; warn-and-no-ops in dev. Search / import / avatar endpoints are unchanged.
- **Owns:** `role-architect` (pattern) → `role-implementer` (per-route).
### 7. Layout default-prop leaks maintainer email
### 7. Layout default-prop leaks maintainer email — **RESOLVED 2026-05-24**
- **Resolved by:** `fix-layout-default-user` convoy (PR #15, squash commit `ca302a8`). Brief 1 (pre-squash `ddf8fd2`) shipped the Layout default-null + logged-out branch + vitest lock-in; Brief 2 (pre-squash `8c7d127`, rebased to `0f6bfbb` pre-merge) swept the 7 pages that needed page-level fixes.
- **File:** `components/Layout.js` line 562: `function Layout({ children, user = { email: 'me@randallstillwell.com', role: 'user' }, ... })`.
- **Impact:** Any page that renders Layout without passing a `user` prop displays your real email and impersonates you as the logged-in user.
- **Fix:** Default `user = null` and render a logged-out state branch. Verify every page passes `user` explicitly (the graph shows ~13 pages call `Layout`; audit each).
- **As-shipped:**
1. `components/Layout.js` default prop changed from hardcoded `{ email: 'me@randallstillwell.com', role: 'user' }` to `null`. `UserProfileDropdown` now branches on `user === null` and renders a `<Link href="/login">Sign in</Link>` CTA in place of the avatar + email + dropdown menu (`NavigationContent`'s `authenticatedNavigation` / `myCollectionNavigation` / `adminNavigation` were already null-safe via existing optional chains; no change there).
2. **7 pages swept** (Brief 2, 11 `<Layout>` call sites total). `pages/scanner.js` (×1), `pages/decks.js` (×3), `pages/deck-builder.js` (×4), `pages/deck/[id].js` (×3) now pass `user={user}` explicitly. `pages/profile.js` and `pages/settings.js` replaced their leaky `useState({ email: 'me@randallstillwell.com', role: 'admin' })` initializer with `useState(null)` (15 sync `user.*` reads in profile + 1 in settings got null-guards). `pages/card/[id].js` replaced its hardcoded `const user = { email: 'me@…', role: 'user' }` with `const { user } = useAuth()` from `lib/use-auth.js`.
3. **10 pages already correct** (architect's per-page audit, Decision B in `.convoys/fix-layout-default-user.md`): `dashboard`, `my-cards`, `cards`, `collections`, `collection/[identifier]`, `community/collections`, `admin/card-import`, `admin/card-editor`, `invite/accept`, `invite/decline`. No changes there.
4. **Test coverage:** `test/components/Layout.test.js` (new) adds 5 regression-lock assertions — no maintainer email when `user` is `null`/omitted; "Sign in" link present when logged out; supplied email renders when supplied; no accidental `Guest` placeholder. Vitest 21/21 green at merge (16 pre-existing auth tests still green).
5. **New devDeps:** `jsdom@^29` + `@testing-library/react@^16` (test-only). `vitest.config.js` got a 3-line `esbuild` block to parse JSX in `.js` files (per-file `// @vitest-environment jsdom` directive — no global env change).
6. **Verification at merge:** `rg 'me@randallstillwell.com' pages/` → 0 hits; anonymous `curl /cards` returned HTTP 200 with no maintainer email; lint baseline match (128 problems, unchanged); CI Aggregate gate / Lint / Vitest / Vercel preview / forbidden-endpoints all green. `Playwright smoke` + `Screenshot diff` red but for an unrelated CI-infra reason — see CI infrastructure side-effect note below.
- **Flagged-but-deferred** (deliberately out of scope per the convoy spec):
1. 4 pages still import `useAuth` from `lib/auth-context.js` (`pages/scanner.js`, `pages/decks.js`, `pages/deck-builder.js`, `pages/deck/[id].js`) — collapsing the three parallel client-side auth surfaces is the queued `single-auth-provider` convoy (P1 #9 in this file), not this one.
2. `components/MobileNavigation.js` still receives a dead `user` prop (it accepts `{ user, onMenuOpen }` but never reads `user.*` — the bottom-bar items are static). Queued as `cleanup-mobile-nav-dead-props` (or fold into `god-component-split` if that lands first).
3. `pages/card/[id].js` still imports `useIsAdmin` from `lib/admin-auth.js` — third parallel auth surface; same `single-auth-provider` convoy will collapse it.
- **CI infrastructure side-effect (not part of this convoy).** PR #16 (squash commit `7e97254`) landed alongside as a CI permissions fix, adding scoped `permissions:` blocks to `.github/workflows/preview-smoke.yml` + `.github/workflows/visual-diff.yml`. That fixed the 5-second 403 "Resource not accessible by integration" failure on both workflows but exposed a second issue: with permissions correct, both now reach the actual deployment check and 10-min-timeout against Vercel Deployment Protection's 401 SSO challenge (anonymous GitHub runner GETs the preview URL). New queued convoy `fix-vercel-deployment-protection-in-ci` (`.convoys/fix-vercel-deployment-protection-in-ci.md`) tracks that follow-up.
- **Owns:** `role-implementer`.
### 8. Next.js 15.4.3 — Vercel platform blocks deploys (vulnerable version)
@ -259,6 +272,19 @@ Each phase is one Conductor-created convoy. Don't run more than two in parallel
Total: ~14 convoys to get from current state to public-launch-ready. Estimate 4-8 weeks at one human-in-the-loop reviewer per convoy. Multitask + Cursor 3.2 worktrees compress steps 8-12 substantially.
## Queued convoys
Follow-ups surfaced mid-convoy or mid-PR that didn't fit the original launch sequence but need to land before public traffic. Listed in priority order; not all will be P0/P1 — most are CI / DX / hygiene polish.
- **`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.
- **`cors-tighten`** (priority: P1 quality). Drop the wildcard `Access-Control-Allow-Origin` header from `pages/api/auth/verify.js`. Surfaced in P0 #5 (deferred from `fix-auth-bypass` Brief 4).
- **`add-rate-limiting`** (priority: P1 quality, also listed in launch sequence step 4). Extend `lib/rate-limit.js` to `/api/users/search`, `/api/cards/search`, all `/api/cards/import-*`, and `/api/user/avatar*`. Login + register already wired in `fix-auth-bypass` Brief 4.
- **`purge-weak-creds-from-helpers`** (priority: P2 hygiene). Sweep `scripts/reset-db.js`, `scripts/create-test-users.js`, and `TESTING_GUIDE.md` for the literal `admin@tcgvault.com` / `admin123` references. May fold into `pick-a-name` since the email itself is changing.
- **`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`).
- **`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.
- **`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.
- **`fix-vercel-deployment-protection-in-ci`** (priority: P2 CI infra; **operator action required**). PR #16's permissions fix exposed that Vercel Deployment Protection 401s anonymous CI requests, so `Playwright smoke` and `Screenshot diff` now 10-min-timeout instead of 5-second-403. Plumb `VERCEL_AUTOMATION_BYPASS_SECRET` into both workflows + (eventually) Playwright config. Operator must seed the secret into GitHub Actions before the implementer can run. See `.convoys/fix-vercel-deployment-protection-in-ci.md`. Created 2026-05-24.
## 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.

View file

@ -33,6 +33,7 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560
- **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'`. Avoid `lib/auth-context.js` and `lib/admin-auth.js` for new code — they are legacy parallel implementations.
- **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.
- **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.
@ -53,7 +54,7 @@ Code graph is indexed by `user-code-review-graph` MCP (122 files, 628 nodes, 560
- **#5`pages/api/setup-database.js` public endpoint. RESOLVED** by `fix-auth-bypass` Brief 3 (commit `fc0dd73`). The file is deleted along with the other three dev endpoints (`/api/simple`, `/api/test-auth`, `/api/test-db`), and `.github/workflows/ci.yml`'s new `forbidden-endpoints` job fails the build if any of them are re-introduced (or if a new `pages/api/test-*.js` file appears). Entry kept (not renumbered) to preserve cross-references.
- **#6 — Migrations are bare scripts.** `scripts/add-*.js` and `scripts/fix-*.js` are run-once jobs with no idempotency tracking. Adopt `node-pg-migrate`, `kysely`, or `drizzle-kit` before more schema changes.
- **#7 — Dual `is_public` semantics.** Collections and decks both have `is_public` columns; check which controls discovery vs. anonymous read in the relevant route.
- **#8 — Layout has hardcoded default user.** `Layout({ user = { email: 'me@randallstillwell.com', role: 'user' } })`. Anything rendering Layout without passing `user` will impersonate the maintainer. Pass `user` explicitly from every page.
- **#8 — Layout has hardcoded default user. RESOLVED** by `fix-layout-default-user` convoy (PR #15, squash commit `ca302a8`). `components/Layout.js`'s default prop is now `null`; `UserProfileDropdown` renders a `<Link href="/login">Sign in</Link>` CTA when `user === null`. Brief 2 also swept the 7 pages that needed page-level fixes (`scanner` / `decks` / `deck-builder` / `deck/[id]` now pass `user={user}` to Layout; `profile` / `settings` replaced leaky `useState({email:'me@…'})` with `useState(null)` + null-guards on every sync `user.*` read; `card/[id]` swapped a hardcoded `const user = {...}` for `useAuth()` from `lib/use-auth.js`). `test/components/Layout.test.js` adds 5 regression-lock assertions (no maintainer email when user is null/omitted; "Sign in" link present; supplied email renders; no "Guest" placeholder); vitest 21/21 green at merge. New devDeps: `jsdom@^29` + `@testing-library/react@^16`. See `.convoys/fix-layout-default-user.md` and `.convoys/ship-readiness.md` P0 #7. Entry kept (not renumbered) to preserve cross-references.
- **#9`typescript` is a devDep, but the source is still JavaScript-only.** `package.json` lists `typescript@^5.9.3` purely so `eslint-config-next@16`'s bundled `typescript-eslint` chain can satisfy its hard `require('typescript')` at module load (the `peerDependenciesMeta.typescript.optional: true` flag in `eslint-config-next` only suppresses npm's install-time warning, not the runtime require). There is no `tsconfig.json`, no `.ts`/`.tsx` files, and no `// @ts-check` directives. Do not rename `.js` files to `.ts` or add a `tsconfig.json` without an explicit convoy decision — TypeScript adoption is its own scope. See `.convoys/bump-next-js.md` § Decisions C.
- **#10 — ESLint pinned to v9 (maintenance), not v10 (latest).** `devDependencies.eslint` is `^9.39.4` even though `latest` is `10.4.0`. We tried v10 and `npm run lint` crashed with `TypeError: scopeManager.addGlobals is not a function` because `eslint-config-next@16`'s bundled `typescript-eslint@8.x` predates ESLint v10's redesigned global-ingestion path. Reverted to v9 under Decision D. **Do NOT bump ESLint independently** — wait for the queued `bump-eslint-10` follow-up convoy, which is upstream-blocked until `typescript-eslint` ships a v10-tested release that `eslint-config-next` bundles. See `.convoys/bump-next-js.md` § Decisions D + "Follow-up convoys queued".
- **#11 — Turbopack is now the default bundler.** `next dev` and `next build` use Turbopack by default in Next.js 16. The fallback per command is `--webpack` (e.g. `next build --webpack`). We have no custom `webpack:` block in `next.config.js`, no custom loaders/aliases, and no Sass tilde imports, so Turbopack should "just work" — but if a build/runtime regression appears, reproduce on both bundlers before deciding whether to revert or pin a script to webpack. Do not pre-emptively switch to `--webpack`.