deckhearth/.cursor/rules/api-routes.mdc

173 lines
11 KiB
Text
Raw Permalink Normal View History

bootstrap: agent pipeline v0.5.0 + ship-readiness review Installs the three-layer agent-pipeline scaffold (https://github.com/varutasu/agent-pipeline @ v0.5.0): L1 — Context (curated brain) - AGENTS.md: orientation, conventions, 8 explicit gotchas - .cursor/rules/: no-go-zones, api-routes, auth-and-permissions, db-and-schema, ui-and-theming, schema-map - .cursor/skills/: add-api-route, add-page recipes - docs/agent-context/README.md: layer explainer - docs/SCHEMA_MAP.md: hand-curated Neon Postgres reference (replaces Prisma schema map since stack is raw SQL) L2 — Subagent roles (copied verbatim from upstream templates) - 9 .cursor/agents/role-*.md files: Conductor, IA-Architect, UX-Reviewer, Architect, Implementer, Reviewer, Design-System-Auditor, A11y-Auditor, Doc-Writer L3 — Pipeline scaffolding (Vercel variant) - CI: lint + schema-map-drift only (no duplicate build — Vercel handles it). Test job commented out until vitest lands. - preview-smoke + visual-diff via wait-for-vercel-preview - pr-health-rollup sticky comment aggregator - agent-context-drift weekly cron - PULL_REQUEST_TEMPLATE, CODEOWNERS (auth/admin paths tagged) - .convoys/ folder + seed ship-readiness.md review - lib/flags/index.js (JS — converted from TS template) - scripts/wt.sh (Cursor 3.2 deprecation stub), scripts/log-convoy-event.sh - tests/smoke/app.smoke.spec.ts (Playwright skeleton) Manifest - .agent-context-manifest.yml: tracks 31 artifacts by sha256 for future sync-agent-context drift detection Review - .convoys/ship-readiness.md: 16 findings (7 P0 ship-blockers, 5 P1 quality-bar, 4 P2 refactor, P3 UX/IA/a11y/docs) with proposed 13-convoy launch sequence. No production code changed in this commit. All findings in the ship-readiness review will be addressed in follow-up convoys starting with fix-auth-bypass. Structural brain: user-code-review-graph MCP has indexed the codebase (122 files, 628 nodes, 5602 edges, 11 communities, 84 flows). Per-developer; not committed. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 00:16:08 -04:00
---
description: Conventions for Next.js Pages-router API route handlers in tcg-vault
globs: pages/api/**/*.js
---
# API Route Conventions
Pages router handlers; `(req, res)` signature; Vercel serverless functions.
## Authentication & Authorization
Two paths are in use; both go through `lib/permission-middleware.js`.
```js
// 1. Generic auth — every protected route
import { getUserFromRequest } from '../../lib/permission-middleware';
export default async function handler(req, res) {
const user = await getUserFromRequest(req);
if (!user) return res.status(401).json({ error: 'Authentication required' });
// user = { userId, email, role }
// ...
}
```
```js
// 2. Collection-scoped auth — when the route operates on a specific collection
import { withCollectionPermission } from '../../lib/permission-middleware';
async function handler(req, res) {
// req.user and req.permission are populated by the wrapper
const { userId, role } = req.user;
}
export default withCollectionPermission('viewer')(handler);
// 'viewer' | 'editor' | 'owner' — checks owner OR is_public OR explicit collection_permissions row
```
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
`getUserFromRequest` returns `null` for any unauthenticated request (missing header, malformed token, wrong signature, expired token, unknown user id). The early `if (!user) return res.status(401)` pattern in the snippet above is the canonical guard for every authenticated route. (The pre-`fix-auth-bypass` synthetic-admin fallback for missing tokens has been removed — see `AGENTS.md` Gotcha #2 for the audit trail.)
bootstrap: agent pipeline v0.5.0 + ship-readiness review Installs the three-layer agent-pipeline scaffold (https://github.com/varutasu/agent-pipeline @ v0.5.0): L1 — Context (curated brain) - AGENTS.md: orientation, conventions, 8 explicit gotchas - .cursor/rules/: no-go-zones, api-routes, auth-and-permissions, db-and-schema, ui-and-theming, schema-map - .cursor/skills/: add-api-route, add-page recipes - docs/agent-context/README.md: layer explainer - docs/SCHEMA_MAP.md: hand-curated Neon Postgres reference (replaces Prisma schema map since stack is raw SQL) L2 — Subagent roles (copied verbatim from upstream templates) - 9 .cursor/agents/role-*.md files: Conductor, IA-Architect, UX-Reviewer, Architect, Implementer, Reviewer, Design-System-Auditor, A11y-Auditor, Doc-Writer L3 — Pipeline scaffolding (Vercel variant) - CI: lint + schema-map-drift only (no duplicate build — Vercel handles it). Test job commented out until vitest lands. - preview-smoke + visual-diff via wait-for-vercel-preview - pr-health-rollup sticky comment aggregator - agent-context-drift weekly cron - PULL_REQUEST_TEMPLATE, CODEOWNERS (auth/admin paths tagged) - .convoys/ folder + seed ship-readiness.md review - lib/flags/index.js (JS — converted from TS template) - scripts/wt.sh (Cursor 3.2 deprecation stub), scripts/log-convoy-event.sh - tests/smoke/app.smoke.spec.ts (Playwright skeleton) Manifest - .agent-context-manifest.yml: tracks 31 artifacts by sha256 for future sync-agent-context drift detection Review - .convoys/ship-readiness.md: 16 findings (7 P0 ship-blockers, 5 P1 quality-bar, 4 P2 refactor, P3 UX/IA/a11y/docs) with proposed 13-convoy launch sequence. No production code changed in this commit. All findings in the ship-readiness review will be addressed in follow-up convoys starting with fix-auth-bypass. Structural brain: user-code-review-graph MCP has indexed the codebase (122 files, 628 nodes, 5602 edges, 11 communities, 84 flows). Per-developer; not committed. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 00:16:08 -04:00
## Request validation
No schema validator is installed (no zod / yup / valibot). Validate manually:
```js
const { name, game } = req.body || {};
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return res.status(400).json({ error: 'name is required' });
}
if (!['mtg', 'pokemon', 'lorcana'].includes(game)) {
return res.status(400).json({ error: 'invalid game' });
}
```
When adding zod (planned in `.convoys/`), define schemas at the top of the file.
## Method gating
Reject unsupported methods explicitly — Next.js will otherwise call the handler for any method:
```js
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
```
## Error handling
Wrap the handler body in `try/catch`. Never throw unhandled — leaks stack traces in the Vercel response.
```js
export default async function handler(req, res) {
try {
// ...
} catch (err) {
console.error('[POST /api/collections]', err);
return res.status(500).json({ error: 'Internal server error' });
}
}
```
## Database access
- **Prefer:** `import { sql } from '@vercel/postgres'` and tagged templates: `` await sql`SELECT … WHERE id = ${id}` ``.
- **Avoid:** `import { db } from '../../lib/database'`. Its `query(str, params)` API uses `sql.unsafe` after manual interpolation — SQL-injection vector. Slated for removal in a convoy.
- **Always select narrow columns** — don't `SELECT *` from `cards` (large `oracle_text`, `colors` JSONB).
## Response shape
- Success (GET / read): `res.status(200).json({ data: ... })` OR direct payload — codebase is inconsistent; match the surrounding route's existing shape.
- Created (POST): `res.status(201).json({ data: ... })`.
- Errors: `res.status(<4xx|5xx>).json({ error: string, details?: unknown })`.
## Activity logging
Any handler that mutates a collection must call `logCollectionActivity`:
```js
import { logCollectionActivity } from '../../lib/permission-middleware';
await logCollectionActivity(collectionId, userId, 'card_added', { cardId, quantity });
```
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
## Rate limiting
bootstrap: agent pipeline v0.5.0 + ship-readiness review Installs the three-layer agent-pipeline scaffold (https://github.com/varutasu/agent-pipeline @ v0.5.0): L1 — Context (curated brain) - AGENTS.md: orientation, conventions, 8 explicit gotchas - .cursor/rules/: no-go-zones, api-routes, auth-and-permissions, db-and-schema, ui-and-theming, schema-map - .cursor/skills/: add-api-route, add-page recipes - docs/agent-context/README.md: layer explainer - docs/SCHEMA_MAP.md: hand-curated Neon Postgres reference (replaces Prisma schema map since stack is raw SQL) L2 — Subagent roles (copied verbatim from upstream templates) - 9 .cursor/agents/role-*.md files: Conductor, IA-Architect, UX-Reviewer, Architect, Implementer, Reviewer, Design-System-Auditor, A11y-Auditor, Doc-Writer L3 — Pipeline scaffolding (Vercel variant) - CI: lint + schema-map-drift only (no duplicate build — Vercel handles it). Test job commented out until vitest lands. - preview-smoke + visual-diff via wait-for-vercel-preview - pr-health-rollup sticky comment aggregator - agent-context-drift weekly cron - PULL_REQUEST_TEMPLATE, CODEOWNERS (auth/admin paths tagged) - .convoys/ folder + seed ship-readiness.md review - lib/flags/index.js (JS — converted from TS template) - scripts/wt.sh (Cursor 3.2 deprecation stub), scripts/log-convoy-event.sh - tests/smoke/app.smoke.spec.ts (Playwright skeleton) Manifest - .agent-context-manifest.yml: tracks 31 artifacts by sha256 for future sync-agent-context drift detection Review - .convoys/ship-readiness.md: 16 findings (7 P0 ship-blockers, 5 P1 quality-bar, 4 P2 refactor, P3 UX/IA/a11y/docs) with proposed 13-convoy launch sequence. No production code changed in this commit. All findings in the ship-readiness review will be addressed in follow-up convoys starting with fix-auth-bypass. Structural brain: user-code-review-graph MCP has indexed the codebase (122 files, 628 nodes, 5602 edges, 11 communities, 84 flows). Per-developer; not committed. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 00:16:08 -04:00
`lib/rate-limit.js` exposes five named limiters, one per route class. Each named export takes `req` (and `userId` for user-keyed classes) and returns `{ allowed, remaining, reset }`. The auth-only limiter shipped in `fix-auth-bypass` Brief 4 (commit `297afca`, login + register); the four remaining classes — `search`, `upload`, `generate`, `import` — and the seven currently-gated routes shipped in `add-rate-limiting` (squash commit `708ef45`, PR #20, 2026-05-24), the convoy that closed P0 #6 and brought the launch-readiness P0 set to 8/8 RESOLVED. The five Redis key prefixes were renamed `tcgvault:*` → `deckhearth:*` in `pick-a-name` (squash commit `9abbab6`, PR #21, 2026-05-24) — call shape, return shape, and gate-ordering rules below are byte-identical post-rename; only the on-Redis namespace changed (one-time per-window counter reset accepted).
| Class | Limit | Window | Key | Used by | Helper |
| --- | --- | --- | --- | --- | --- |
| `auth` | 5 | 15 min | IP | `/api/auth/login`, `/api/auth/register` | `checkAuthRateLimit(req)` |
| `search` | 60 | 1 min | IP | `/api/users/search`, `/api/cards/search` | `checkSearchRateLimit(req)` |
| `upload` | 10 | 1 hour | user | `/api/user/avatar` | `checkUploadRateLimit(req, userId)` |
| `generate` | 5 | 1 hour | user | `/api/user/avatar/generate` | `checkGenerateRateLimit(req, userId)` |
| `import` | 5 | 1 hour | user | `/api/cards/import-mtg`, `/api/cards/import-pokemon`, `/api/cards/import-lorcana` | `checkImportRateLimit(req, userId)` |
**Verbatim call shape** (identical across all five classes — only the helper name and the optional `userId` argument differ):
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
```js
import { checkSearchRateLimit } from '../../../lib/rate-limit.js';
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
export default async function handler(req, res) {
if (req.method !== 'GET') {
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
return res.status(405).json({ error: 'Method not allowed' });
}
// For user-keyed classes, auth check goes HERE first; see "Gate ordering" below.
const { allowed, reset } = await checkSearchRateLimit(req);
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
if (!allowed) {
res.setHeader('Retry-After', Math.ceil((reset - Date.now()) / 1000));
return res.status(429).json({ error: 'Too many attempts. Try again later.' });
}
try {
// ... handler body ...
} catch (err) {
// ...
}
}
```
**Gate ordering rules:**
1. **Method check first.** Reject the wrong verb with 405 before doing any limiter work.
2. **Auth check before any user-keyed limiter.** `extractUserIdentifier(userId)` THROWS when `userId` is null/undefined/empty (defensive). For `upload`, `generate`, and `import`, the handler MUST call `getUserFromRequest(req)` (or equivalent JWT verification) and confirm a non-null user BEFORE calling the limiter. Wrong order = anonymous user bypasses (the THROW surfaces immediately during dev; do not catch and silently fall back to IP).
3. **For IP-keyed limiters (`auth`, `search`), gate placement is flexible** — either at the top of the handler (after the method check) or after a separate auth check that the route happens to also have (e.g. `users/search` JWT-verifies before rate-limiting, both are correct). The limiter only needs `req` for IP extraction.
4. **Admin-role check, if applicable, goes between auth and rate-limit.** Used by all three `/api/cards/import-*` routes: `if (user.role !== 'admin') return res.status(403).json({ error: 'Admin access required' })` sits between the `if (!user)` 401 and the import rate-limit call.
**Identifier extraction:**
- `extractIpIdentifier(req)` (module-private) — first hop in `x-forwarded-for` (Vercel's edge), falling back to `req.socket.remoteAddress`, falling back to the literal `'anonymous'`. Do NOT key off `req.body.email` (rotates) or `req.headers.authorization` (unauthenticated endpoints don't have one).
- `extractUserIdentifier(userId)` (module-private) — formats as `user:${userId}`. Throws on null/undefined/empty/NaN to surface gate-ordering bugs at dev time rather than silently falling back to IP and creating a per-IP-not-per-user limit.
**Env vars (unchanged from Brief 4):** `KV_REST_API_URL` + `KV_REST_API_TOKEN` (auto-provisioned by Vercel's Upstash Marketplace integration). In prod, missing either var is a **fail-closed throw** on the first call. In dev / test, the module warn-and-no-ops so local work isn't blocked. See `AGENTS.md` Gotcha #12 for the full env-var contract.
**429 response shape is uniform across all five classes.** Same error message (`'Too many attempts. Try again later.'`) and same `Retry-After` header calculation. Per-class variation would fingerprint the limits to an attacker.
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
**Fail-open on Upstash outage.** A network failure inside `ratelimit.limit(...)` returns `{ allowed: true, remaining: Infinity, reset: 0 }` with a single `console.error('[rate-limit]', err)`. Reasoning: a hard Upstash outage should not lock the entire user base out of every gated route. Brute-force / abuse protection lives behind defense-in-depth (Vercel firewall, future fail2ban-style lockout).
docs: post-convoy cleanup for fix-auth-bypass Closes out the fix-auth-bypass convoy (PRs #6–#11, merged through 1629afb) on the docs side. Code already on main; this PR is docs only. Updates: AGENTS.md - §1 auth bullet refreshed (auth-secret SoT, 24h TTL, no synthetic admin, login/register rate limit) - §3 conventions point at lib/auth-secret.js + lib/rate-limit.js - §4 gotchas #2/#3/#5 converted to "Resolved" notes in place (NOT renumbered, to preserve cross-references) - new #12 documents the KV_REST_API_* env-var convention - §5 setup list adds the rate-limit env vars - §6 testing rewritten for Vitest (16 unit tests, blocking CI gate) .cursor/rules/auth-and-permissions.mdc - canonical-surface table gains lib/auth-secret.js + lib/rate-limit.js - token model now 24h (was 7d) with fail-loud explanation - server-side authorization patterns lead with null → 401 contract .cursor/rules/api-routes.mdc - removes the "CRITICAL — known bug" callout (resolved by Brief 2) - adds a "Rate limiting" section with verbatim shape + env-var notes - "Dev/test endpoints" → "Removed" historical note so future agents searching for test-db understand why it's gone .convoys/fix-auth-bypass.md (restored — was on convoy branch only) - frontmatter → status: shipped - new "Convoy outcome" section: briefs + commits + resolved gotchas, R1-R12 risk walk, env-var-rename deviation record, queued follow-up convoys, lessons learned .convoys/fix-auth-bypass/brief-{1..5}-*.md (restored from convoy branch) - audit-trail completeness; convoy plan references them by name - brief 4 additionally updated: UPSTASH_REDIS_REST_* → KV_REST_API_* across init rules, smoke, pre-deploy checklist - brief 4 has a new "Post-merge addendum" explaining the rename .convoys/ship-readiness.md - P0 #1, #2, #4 → RESOLVED with merge-commit citations - P0 #5 (CORS), #6 (rate limit) → PARTIAL with deferral pointers (cors-tighten and add-rate-limiting convoys) - each item gains an "As-shipped" line for self-containment README.md - Next.js 15 → 16, TypeScript claim corrected to JS-with-devDep - auth + rate-limit + testing bullets updated - env-var template extended with KV_REST_API_* - deleted dev-endpoints note added to the API list - "Default Admin Account" section LEFT ALONE — drop-public-setup territory Verified: build exit 0 (with JWT_SECRET set), 16/16 vitest tests pass, lint baseline unchanged (128/81/47). Convoy: fix-auth-bypass / role-doc-writer (closeout) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 12:27:48 -04:00
## Dev/test endpoints (removed)
The four endpoints `pages/api/simple.js`, `pages/api/test-auth.js`, `pages/api/test-db.js`, and `pages/api/setup-database.js` used to exist as unauthenticated dev / diagnostic routes. They were **deleted** by `fix-auth-bypass` Brief 3 (commit `fc0dd73`) and `.github/workflows/ci.yml`'s `forbidden-endpoints` job now fails the build if any of them are re-introduced, or if any new file matching `pages/api/test-*.js` is added. **Do not re-create these files.** If a future agent searches for `test-db` or `setup-database` and finds them missing, this section is the explanation — diagnostics belong outside the public API surface (a CLI script, an admin-gated route, or `npm run` task).
docs: post-convoy cleanup for cors-tighten Reflects the merged cors-tighten convoy (PR #19, squash commit da50d78) in repo documentation. Closes P0 #5 (Wildcard CORS on API surface) from PARTIAL -> RESOLVED, leaving only P0 #6 (full add-rate-limiting) open of the original P0 ship-blocker set. One brief in the convoy: Brief 1 shipped as planned with no scope expansions and no implementer deviations from the verbatim spec. .convoys/cors-tighten.md: - frontmatter status: in-progress -> shipped (added shipped: 2026-05-24) - new ## As-shipped section: all 5 architect-self-ratifiable decisions ratified verbatim (D1 Option B / D2 delete OPTIONS / D3 moot / D4 no new tests / D5 add CI lock); Pattern split (16 Pattern A + 8 Pattern B) per architect's 10-file audit + implementer's per-file diff review; diff size (25 files, +29/-261); empirical CI metrics from post-merge run 26378806555 (forbidden-cors-headers 4s PASS, Playwright smoke 56s 3/3 in 3.3s, Screenshot diff continue-on-error 0 with the documented Decision-4 missing-baseline failure beneath); cross-validation that Playwright smoke continues to pass post-CORS removal (the auth + public surfaces don't depend on the wildcard header); implementer subagent-retry footnote (HEAD already at a843736 when retry woke up - transient retry, work is canonical); operator-action-required-going-forward: none; What did NOT change audit trail. .convoys/ship-readiness.md: - new ## Status summary at the top (right after the code-graph line): P0 set is now 7/8 RESOLVED; only #6 (rate-limiting) remains. Table lists each P0 with its resolving convoy + squash commit for a quick scan of remaining work. - P0 #5 marked RESOLVED 2026-05-24. Added the cors-tighten as-shipped block (24 files swept, new CI job, 16/8 Pattern split, 5 decisions ratified, diff stat, post-merge CI metrics, transient retry footnote, operator-action: none). Brief 4's 2026-05-23 partial is preserved as the prior as-shipped layer above the cors-tighten layer to maintain the audit trail. - Queued convoys: removed the cors-tighten entry (no longer queued). Added a new tighten-visual-diff-path-filter entry (P3 polish) - Screenshot diff workflow triggered on API-only PR #19 because its paths: filter is pages/** which matches pages/api/** too. ~55s of CI waste per API-only PR; one-line YAML tweak; verify GitHub Actions' negated-glob semantics before merging. .cursor/rules/api-routes.mdc: - new ## CORS section near the existing ## Dev/test endpoints (removed) section. Documents the no-CORS-by-default convention, the brief-4 + cors-tighten lineage, the new forbidden-cors-headers CI gate, and three forward-conventions (no setHeader for CORS, no OPTIONS preflight handlers, design a proper middleware layer if a future cross-origin caller is needed - not wildcards in individual handlers). AGENTS.md intentionally untouched. Gotcha #5 (the public setup-database.js endpoint) is already RESOLVED by fix-auth-bypass Brief 3 and unrelated to this convoy. The new convention belongs in .cursor/rules/api-routes.mdc (where API conventions live) rather than AGENTS.md; the convoy file + the new CI gate are sufficient documentation for the audit trail. Per convoy spec, no new gotcha entry needed. No changes to: package.json, package-lock.json, pages/api/**, lib/**, components/**, scripts/**, test/**, tests/**, .github/workflows/**, README.md, TESTING_GUIDE.md, playwright.config.js. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 21:49:30 -04:00
## CORS
No `pages/api/**` route ships CORS headers. The frontend and the API are same-origin on Vercel (same project, same domain), and cross-origin reads serve no legitimate purpose on this API surface — the wildcard `Access-Control-Allow-Origin: *` that 24 handlers used to carry was scaffolding cruft, not a deliberate cross-origin design. `fix-auth-bypass` Brief 4 (commit `297afca`) removed it from `pages/api/auth/login.js` + `pages/api/auth/register.js`; the `cors-tighten` convoy (squash commit `da50d78`, PR #19) swept the remaining 24 handlers and added a new blocking `forbidden-cors-headers` job to `.github/workflows/ci.yml` (modeled on `forbidden-endpoints`) that fails the build if any `Access-Control-Allow-(Origin|Methods|Headers)` reference reappears under `pages/api/`.
Conventions to follow:
- **Do not add `res.setHeader('Access-Control-Allow-*', ...)` to any new route.** The CI gate will fail the build with a file-and-line pointer.
- **Do not add `if (req.method === 'OPTIONS')` preflight handlers.** Same-origin requests don't preflight; cross-origin requests are blocked at the browser CORS layer (the desired end state). If an OPTIONS request ever arrives, the existing method gate (`if (req.method !== '<verb>') return res.status(405)`) returns 405 — strictly safer than the pre-sweep 200-to-everyone.
- **If a future cross-origin caller is legitimately needed** (third-party app, mobile client, public API key program — none exist today), design a proper CORS layer — probably as Next.js middleware reading an allowed-origin list from env — rather than scaffolding wildcards back into individual handlers. That's a separate convoy (`add-cors-layer` or similar); flag it as a new follow-up in `.convoys/ship-readiness.md` § Queued convoys at the time the need surfaces.