deckhearth/.convoys/fix-auth-bypass.md
Randall Stillwell a8bab93270 convoy: scope fix-auth-bypass (P0 #1, #2, #4, #5, #6 partial)
Conductor output for the first real convoy after the agent-pipeline
bootstrap. Closes P0 ship-blockers #1, #2, #4, #5 and the auth-route
slice of #6 from .convoys/ship-readiness.md.

Classification: server-only
Skip: ia, ux, visual, a11y, design
Next role: role-architect

The convoy is unflagged (auth fixes don't ship behind a feature flag).
Architect produces a slice plan with explicit slice_dependencies so
implementers can /multitask the disjoint briefs (dev-endpoint delete +
CORS tighten + rate-limit wiring) once the central JWT secret helper
lands.

Out of scope here (own convoys):
- P0 #3 default admin creds + README   → drop-public-setup
- P0 #7 Layout default-prop email leak → fix-layout-default-user
- P0 #6 full (search/import/upload)    → add-rate-limiting

Convoy file: .convoys/fix-auth-bypass.md
Analytics: emitted via scripts/log-convoy-event.sh

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 23:27:25 -05:00

8.2 KiB
Raw Blame History

name classification success_metric skip status created
fix-auth-bypass server-only getUserFromRequest returns null for missing tokens; no API route accepts unauthenticated requests; CI green.
ia
ux
visual
a11y
design
open 2026-05-22

Convoy: fix-auth-bypass

Closes P0 ship-blockers #1, #2, #4, #5, and #6 (partial) from .convoys/ship-readiness.md. This is the very first real convoy after the bootstrap and gates the rest of the launch sequence — until it lands, every other production-bound PR is paused.

Why

The current lib/permission-middleware.js::getUserFromRequest returns a hardcoded admin user ({ id: 1, role: 'admin', email: 'admin@tcgvault.com' }) when no Authorization header is present. Every API route that calls it (30+ handlers per user-code-review-graph) therefore accepts unauthenticated requests as admin. Combined with:

  • A weak fallback JWT_SECRET ('your-secret-key-change-in-production') duplicated across 7 files,
  • Four dev-only endpoints (/api/simple, /api/test-auth, /api/test-db, /api/setup-database) shipped in pages/api/,
  • Access-Control-Allow-Origin: * on auth endpoints,
  • Zero rate limiting on login,

…the production URL is effectively wide-open. No anonymous traffic can touch the live site until this convoy ships.

Success looks like:

  1. getUserFromRequest returns null when there is no Bearer token. Period. No callers receive a synthetic admin.
  2. There is exactly one source of truth for the JWT secret. If process.env.JWT_SECRET is unset, the server fails to boot with a clear error — not a silent fallback.
  3. The four dev endpoints are gone, and CI fails the build if they reappear.
  4. The login + register endpoints respond only to the production frontend origin (or no CORS header at all on same-origin Vercel deploy).
  5. Login + register are rate-limited (the bare minimum of P0 #6; the rest is add-rate-limiting).
  6. CI is green (lint + the new auth tests).

Scope

In:

  • lib/permission-middleware.js — remove hardcoded admin fallback; return null on missing/invalid token.
  • New lib/auth-secret.js (or named equivalent — Architect to confirm) — single export of JWT_SECRET, throws at module load if unset.
  • Refactor pages/api/auth-utils.js, pages/api/auth/login.js, pages/api/auth/register.js, pages/api/auth/verify.js, pages/api/favorites.js, pages/api/users/search.js, and lib/permission-middleware.js to import from the new secret helper. Remove all process.env.JWT_SECRET || '…' literals.
  • Reconcile token expiry inconsistency (login = 24h, auth-utils = 7d). Pick one — Architect's call; record in .cursor/rules/auth-and-permissions.mdc.
  • Delete pages/api/simple.js, pages/api/test-auth.js, pages/api/test-db.js, pages/api/setup-database.js.
  • Add a CI grep step to .github/workflows/ci.yml that fails the build if pages/api/test-*, pages/api/simple.js, or pages/api/setup-database.js ever re-appear.
  • Tighten Access-Control-Allow-Origin on pages/api/auth/login.js and pages/api/auth/register.js. Default: drop the header entirely (same-origin on Vercel). Fallback: pin to a process.env.PUBLIC_FRONTEND_ORIGIN env var.
  • Adopt @upstash/ratelimit (or equivalent — Architect's pick) and apply to /api/auth/login and /api/auth/register only. Other endpoints listed in P0 #6 (search, imports, avatar upload) are deferred to the add-rate-limiting convoy.
  • Add unit tests for getUserFromRequest: missing header → null, malformed token → null, valid token → user object, expired token → null. Architect to decide whether to land this with vitest now or defer to the adopt-vitest convoy. Default recommendation: install vitest in this convoy. The blast radius of an auth refactor justifies bringing the test runner forward by one slot in the launch sequence.

Out (deferred to their own convoys):

  • P0 #3 (default admin creds + README) → drop-public-setup.
  • P0 #7 (Layout default-prop leaks maintainer email) → fix-layout-default-user.
  • P0 #6 (full) — rate limit on search / import / upload routes → add-rate-limiting.
  • Any auth-context client-side cleanup (lib/auth-context.js vs lib/admin-auth.js vs lib/use-auth.js) → single-auth-provider.
  • The lib/database.js vs @vercel/postgres reconciliation → single-sql-client.

Hard "do not touch" in this convoy:

  • No UI files. No components/, no pages/*.js that aren't under pages/api/. If a UI file appears in a brief, kick it back to Architect.
  • No schema changes. No SQL migrations. (scripts/setup-neon-db.js is read-only here.)
  • No new feature flags. The flag wrapper exists (lib/flags/index.js) but this convoy is unflagged — auth fixes don't ship behind a flag.

Roles invoked

Per server-only classification (skip: ia, ux, visual, a11y, design):

  1. role-architect — produces a slice plan with explicit slice_dependencies:. Expect 46 briefs (auth-secret helper, getUserFromRequest fix + caller audit, dev-endpoint removal + CI guard, CORS tighten, rate-limit pattern + login/register wiring, tests). Architect must declare which briefs are parallel-safe.
  2. role-implementer — runs one brief at a time, except where Architect marks depends_on: [] and files: are disjoint. Then /multitask can fan out (see dispatch below).
  3. role-reviewer — single-pass after the PR drafts. Design-system-auditor and a11y-auditor are skipped for this convoy — there is no UI surface to audit. Reviewer covers correctness, security regressions, and test coverage.
  4. role-doc-writer — last. Updates .cursor/rules/auth-and-permissions.mdc (canonical secret helper, chosen expiry, rate-limit pattern), AGENTS.md "Common gotchas" section (remove items that are no longer gotchas), and docs/SCHEMA_MAP.md only if any DB read pattern changed (it shouldn't).

Multitask dispatch recommendation (Cursor 3.2 /multitask): after Architect publishes briefs with depends_on: [] and disjoint files:, the user may dispatch implementers in parallel. Typical safe fan-out for this convoy:

  • Group audit-fix-auth-bypass-<pr>: role-reviewer only (no design / a11y).
  • Implementer fan-out: only if Architect explicitly marks briefs as parallel-safe. The auth-secret helper brief must complete first; everything else depends on it. So realistic fan-out is post-secret-helper: dev-endpoint deletion + CORS tighten + rate-limit wiring in parallel; getUserFromRequest fix runs alongside but its tests block on the secret helper landing first.

Todos

High-level checklist for the next role to refine. Each becomes a brief under .convoys/fix-auth-bypass/brief-N-*.md.

  • Brief 1 — Central JWT secret helper. Create lib/auth-secret.js, fail-loud on missing env. Decide canonical token TTL.
  • Brief 2 — Remove the admin bypass. Fix getUserFromRequest; audit every caller (user-code-review-graph query: incoming edges to lib-admin::getUserFromRequest). Add unit tests covering missing/invalid/expired/valid token paths.
  • Brief 3 — Delete dev-only endpoints. Remove four files; add CI guard.
  • Brief 4 — Tighten auth CORS. Drop Access-Control-Allow-Origin: * on login + register. Add same-origin fallback via env var.
  • Brief 5 — Rate-limit login + register. Install @upstash/ratelimit (or Architect-chosen alternative). Wire to login + register only. Defer the full sweep to add-rate-limiting.
  • Brief 6 — Test harness (provisional). Install vitest, write the getUserFromRequest suite, re-enable the test: job in .github/workflows/ci.yml. Architect to confirm whether this is in-scope here or split to adopt-vitest.
  • Doc-writer pass. Update auth rules + AGENTS.md gotchas.

Hand-off

Next role: role-architect.

To run it in a new chat, paste:

"Run role-architect on convoy fix-auth-bypass. Read .convoys/fix-auth-bypass.md for scope and todos, then produce a slice plan with explicit slice_dependencies:. Output briefs to .convoys/fix-auth-bypass/brief-N-*.md. Flag which briefs are parallel-safe so the user can /multitask implementers."

Conductor exits here. Human-in-the-loop gate: review the convoy file, confirm the scope split, then start the Architect.