Architect pass for P0 security-critical convoy (closes ship-blockers #1, #2, #4, #5, #6 partial). Produces 5 briefs with explicit slice_dependencies for /multitask fan-out. Decomposition: - Brief 1: Central JWT secret helper + 24h token TTL (8 files, ~120 LOC) - Brief 2: Remove the synthetic-admin bypass (2 files, ~25 LOC net negative) - Brief 3: Delete 4 dev-only endpoints + CI guard (6 files, ~30 LOC) - Brief 4: Tighten auth surface — CORS + rate limit (5 files, ~150 LOC) - Brief 5: Install vitest + auth tests + re-enable CI test job (8 files, ~280 LOC) Total estimate: ~600 LOC across 5 PRs. All under 400-LOC budget. Wave A (parallel from t=0): Briefs 1 + 3 (disjoint files) Wave B (parallel after Brief 1): Briefs 2 + 4 (disjoint subsets of Brief 1's exports) Wave C (after Briefs 2 + 4): Brief 5 alone (lockfile sequencing + functional dep on Brief 2's null contract) Architect's calls (3 decisions documented in convoy file Decisions log): - Token TTL = 24h (matches current login.js UX; security-conservative) - Rate-limit = @upstash/ratelimit@^2.0.8 + @upstash/redis@^1.38.0 (DIY-Postgres needs schema change OOS; DIY-memory broken on Vercel cold starts; next-rate-limit is stale) - Vitest in this convoy (not split to adopt-vitest); pinned to ^3.2.4 to dodge vitest@4's non-optional vite peer dep Boot-the-brief findings (9 verifications, 0 revisions): - 24/24 getUserFromRequest callers already handle null correctly — Brief 2 is safer than the convoy file predicted - 7 JWT_SECRET literal sites match AGENTS.md gotcha #3 exactly - Dev endpoints have zero runtime references (only doc references) — safe to delete - Cross-brief commitments declared in both directions for every Brief-1 -> {2,4,5} pair Risk list: 12 risks documented (R1-R12). Headlines: - R1: JWT_SECRET fail-loud throws may break unexpected import chains - R3: existing tokens stop verifying once literal fallback removed (one-time "log back in" pre-launch is acceptable) - R5-R6: rate-limit IP extraction + Upstash quota; fail-open mitigation - R10: JWT_SECRET rotation now requires a deploy (no silent fallback) Pre-merge env-var checklist (user action required before Brief 4 ships): - UPSTASH_REDIS_REST_URL (new — Vercel project settings) - UPSTASH_REDIS_REST_TOKEN (new — Vercel project settings) - JWT_SECRET (verify already set — no fallback any more) Awaiting human gate 1 (plan approval) before implementers run. Co-authored-by: Cursor <cursoragent@cursor.com>
5.7 KiB
| convoy | brief_number | depends_on | files | deletes | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| fix-auth-bypass | 3 |
|
|
Brief 3: Delete dev-only API endpoints + add CI guard
Goal (1 sentence)
Delete the four unauthenticated dev endpoints currently shipped to prod (/api/simple, /api/test-auth, /api/test-db, /api/setup-database) and add a CI grep step that fails the build if anyone re-introduces them.
Files in scope (do not edit anything else)
pages/api/simple.js— deletedpages/api/test-auth.js— deletedpages/api/test-db.js— deletedpages/api/setup-database.js— deleted.github/workflows/ci.yml— modified (new job)README.md— modified (one-line removal)
Conventions to follow
.cursor/rules/api-routes.mdc§ "Dev/test endpoints" — these files are explicitly called out as dev-only and slated for deletion. This brief executes that..cursor/rules/no-go-zones.mdc— none of these four files appear in the no-go list (they are not inscripts/add-*or any "append-only / historical" set). They are explicitly listed in the api-routes rule as "should be deleted.".github/workflows/ci.ymlformatting: 2-space indent, jobs go under the existingjobs:map, match the style oflint:andschema-map-fresh:.
Acceptance criteria
Deletions
pages/api/simple.jsremoved viagit rm.pages/api/test-auth.jsremoved viagit rm.pages/api/test-db.jsremoved viagit rm.pages/api/setup-database.jsremoved viagit rm.- No grep hits for any of these paths anywhere in
pages/,components/,lib/, orscripts/. Run before the PR:
rg "/api/(simple|test-auth|test-db|setup-database)" --type js
rg "(setup-database|test-auth|test-db|api/simple)" pages components lib scripts
Expected: zero hits in source. Doc references in .cursor/rules/api-routes.mdc, AGENTS.md, .convoys/, docs/ are out of scope (doc-writer cleans them up later).
README.md
- Remove the line
- \GET /api/test-db` - Database connection test` (currently line 79). If the surrounding API list is short and now incomplete, leave it as-is — the doc-writer pass will rewrite that section.
.github/workflows/ci.yml
- Add a new job
forbidden-endpointsafterschema-map-fresh:. Verbatim shape:
forbidden-endpoints:
name: No dev endpoints in pages/api
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fail if dev endpoints re-appear under pages/api/
run: |
BAD_PATHS=(
"pages/api/simple.js"
"pages/api/test-auth.js"
"pages/api/test-db.js"
"pages/api/setup-database.js"
)
FOUND=()
for path in "${BAD_PATHS[@]}"; do
if [ -f "$path" ]; then
FOUND+=("$path")
fi
done
# Also flag any new pages/api/test-*.js the explicit list missed.
while IFS= read -r path; do
FOUND+=("$path")
done < <(find pages/api -maxdepth 4 -type f -name 'test-*.js' 2>/dev/null || true)
if [ ${#FOUND[@]} -gt 0 ]; then
echo "::error::Forbidden dev endpoints present in pages/api/. Delete them or move to scripts/."
for path in "${FOUND[@]}"; do
echo "::error file=${path}::Forbidden dev endpoint."
done
exit 1
fi
echo "OK: no forbidden dev endpoints under pages/api/."
- The job runs on
pull_requestandpush(it inherits the workflow-levelon:triggers — no per-jobon:block needed). - No new
concurrency:block (the workflow-levelconcurrency:is already set). - No
if:conditional that lets this job skip on docs-only PRs. The check is fast (afind+ 4[ -f ]calls) and skipping it would defeat the purpose. - The job is blocking — no
|| truewrapper, no::warningfallback. (Lint has the wrapper because of the documentedfix-lint-baselinedebt; this job is not subject to that.)
Smoke
- After deleting the files,
npm run buildsucceeds (no broken imports — these endpoints are unreferenced, verified in the architect's audit). git grep -l 'api/simple\|test-auth\|test-db\|setup-database' pages components libreturns no source files (only docs).- Locally, simulate the CI guard:
bash -c '
BAD_PATHS=("pages/api/simple.js" "pages/api/test-auth.js" "pages/api/test-db.js" "pages/api/setup-database.js")
FOUND=(); for p in "${BAD_PATHS[@]}"; do [ -f "$p" ] && FOUND+=("$p"); done
[ ${#FOUND[@]} -eq 0 ] && echo OK || { echo "FAIL: ${FOUND[@]}"; exit 1; }
'
Expect OK. Then create a temporary pages/api/test-fake.js (matches test-*.js glob) and re-run — expect FAIL. Delete the temp file before opening the PR.
Out of scope
- No
pages/api/cards/import-*.jsdeletion or gating. Those are admin-imports with rate-limit concerns;add-rate-limitingconvoy. - No
pages/api/auth/*changes — Brief 1 + Brief 2 + Brief 4 cover those. - No README rewrite of the API list — doc-writer pass.
- No new test files — Brief 5.
Rationale (≤3 sentences)
These four files are the highest-impact deletions in the convoy: pages/api/setup-database.js is a public unauthenticated POST that triggers DDL, and the other three leak DB / auth internals to anyone who hits them. The CI guard is cheap insurance — without it, a future agent following an outdated tutorial could re-introduce pages/api/test-db.js in good faith. Keeping this brief tiny (deletions + one CI job + one README line) means it can ship in parallel with Briefs 1, 2, and 4 with no merge-conflict risk.