**Total: 25 files modified. No new files. No deletions. No schema changes. No new dependencies.**
### API surface
No API surface changes (same routes, same methods, same auth requirements, same response shapes, same rate-limit considerations as today). The only externally-observable behavior change is:
- Cross-origin browser requests no longer succeed (browser blocks them at the CORS layer post-sweep — the desired success-metric end state).
- Same-origin requests (the existing frontend) continue to work unchanged.
- A direct `OPTIONS` request that bypasses the same-origin policy (e.g. `curl -X OPTIONS`) returns 405 instead of 200. Strictly safer.
### Schema diff
None. No DDL, no migration, no `docs/SCHEMA_MAP.md` update needed.
### Pattern-drift audit results
Architect spot-checked 7 files (parent spot-checked 3 additional, listed in the convoy's "Known constraints" section). All 10 confirmed identical scaffolded pattern with two structural sub-shapes:
| `pages/api/cards/[id]/ownership.js` (architect) | Pattern A | None — identical; the `req.query.id` parse happens INSIDE the post-OPTIONS-removal `try` block, no pre-OPTIONS dependency on `[id]` |
| `pages/api/public/collections.js` (architect) | Pattern A | None — identical; the only "intentionally public" route, but no documented cross-origin consumer (see § Public-routes finding below) |
**Conclusion: zero drift across the 10-file audit. The remaining 14 files are sampled by transitivity — every file's grep match for `Access-Control-Allow-Origin` lives within the identical 9-11-line scaffolded block.** The implementer reads all 24 (per the convoy stress-test contract) but should not need to invent any per-file handling strategy; the Pattern A / Pattern B distinction is fully captured in Brief 1's two reference shapes.
### Public-routes finding
`pages/api/public/collections.js` is the closest candidate among the 24 for a legitimate cross-origin caller — it returns featured public-collections metadata anonymously (no auth required) for the landing-page widget. The architect's conservative call (D1 + D2): still sweep. Reasoning:
1.**Same-origin frontend.** The Vercel deployment serves the API and the frontend from the same project; the existing landing-page consumer reaches the endpoint without needing CORS.
2.**No documented external consumer.** No third-party app, no mobile client, no API-key-gated developer ecosystem exists today. YAGNI.
3.**Sweep-and-revisit is cheap.** If a third-party consumer ever lands, a proper CORS layer (Next.js middleware OR explicit `Access-Control-*` headers gated on `process.env.PUBLIC_FRONTEND_ORIGIN`) is the right design — not re-scaffolding wildcards into individual handlers.
Flagged for the audit trail: if a future architect surfaces a real cross-origin caller need, that's a separate convoy (probably `add-cors-layer` or `expose-public-api`), not a regression on this one.
### Risk list
- **R1 — Method-check ordering on Pattern B files.** Two of 24 (`collections.js`, `collections/[identifier].js`, `collections/[identifier]/permissions.js`, `user/avatar.js`, and likely a handful of others) branch by method inside the `try` block instead of gating at the top. Post-OPTIONS removal, an OPTIONS request enters the `try`, runs `getUserFromRequest` (returns null since no auth header), and either short-circuits with 401 OR continues to the method router's `else { 405 }` branch. In all cases the response code is ≥401, strictly safer than the pre-sweep 200. **Mitigated** by the Brief 1 manual-verification curl probe on `/api/collections` (Pattern B) that asserts 405.
- **R2 — Smoke spec regression.** Smoke hits `/`, `/login`, `/api/health` — none in scope. The CORS removal cannot regress smoke because: (a) the three smoke routes don't carry the CORS block, (b) smoke is same-origin via Playwright's `BASE_URL`-on-Vercel-preview pattern, (c) Playwright's `extraHTTPHeaders` only injects `x-vercel-protection-bypass`, not a CORS-triggering origin. **Mitigated** by the smoke spec's existing CI run on the PR.
- **R3 — Lint baseline regression.** The current baseline is 128 problems (per `bump-next-js` Decision D). The sweep is pure deletion; it cannot introduce new findings. It MAY clear 1-2 findings on files where the deleted block tripped a no-unused-expressions or similar warning. **Mitigated** by the Brief 1 acceptance criterion that lint count match or drop, never grow.
- **R4 — Mid-edit syntax errors.** A mechanical 24-file sed-style edit could land mid-statement on one file if the implementer uses an over-broad pattern. **Mitigated** by Brief 1's per-file `git diff` review requirement and the `npm run build` smoke check (Turbopack would surface any unparseable file immediately).
- **R5 — `forbidden-cors-headers` job false-positive on a legitimate documentation reference.** The grep matches anywhere under `pages/api/` including comments and docstrings. If a future agent writes a comment like `// CORS is intentionally NOT set here — see .convoys/cors-tighten.md`, the grep would catch it. **Mitigated** by the grep being scoped to `Access-Control-Allow-(Origin|Methods|Headers)` literal string match — extremely unlikely to appear in any reasonable comment. If it ever does, the comment can use different wording (e.g. "wildcard origin").
- **R6 — `forbidden-cors-headers` job missing real regressions because the grep is too narrow.** If a future agent reintroduces CORS via `res.setHeader('access-control-allow-origin', '*')` (lowercase) or via `res.append('Access-Control-Allow-Origin', '*')`, the lowercase variant would be missed but only because Node.js HTTP headers are case-insensitive on read, not on write — the grep matches the literal source string the developer wrote. The conventional capitalization used by every existing site (and the original scaffolded template) is `Access-Control-Allow-Origin`. **Mitigated** by the grep's case-sensitive default; if false-negatives become a real risk in the future, `grep -iE` is a one-character change.
- **R7 — Implementer sweeps `login.js` / `register.js` by accident.** Brief 1 explicitly lists them as out-of-scope. The pre-sweep grep baseline (24 files) and the post-sweep grep baseline (0 files) make a sweep of these two visible — the `git diff` would show them as changed, but the diff would be no-op (they have nothing to delete). **Mitigated** by the Brief's explicit out-of-scope list and the diff-hygiene acceptance criterion (deletion count per file ≈ 9-11 lines; a no-op file would show 0).
### Test plan
No new tests this convoy (Decision D4). Existing coverage continues to defend:
- **Vitest (21/21):** unchanged. Verifies on push via the `test` job in `.github/workflows/ci.yml` (blocking).
- **Playwright smoke (3/3):** unchanged. Verifies on push via `.github/workflows/preview-smoke.yml`.
- **Visual diff:** unchanged behavior (still fails on missing baseline until `seed-visual-baselines-on-linux` lands; that's the documented Decision-4 end state of `adopt-playwright-smoke`).
- **Lint (`|| true` wrapped):** baseline must match (128 problems) or drop, never grow.
- **New `forbidden-cors-headers` CI job:** locks in the sweep against future regressions. Blocking on the PR.
If `fill-vitest-handler-coverage` ever lands, the per-route handler tests should explicitly assert (a) response headers do NOT include any `Access-Control-Allow-*` and (b) `OPTIONS` returns 405. That's a separate convoy's scope.
## Decomposition
| Brief # | Title | Files | Depends on | Estimated PR size |
| --- | --- | --- | --- | --- |
| 1 | Sweep wildcard `Access-Control-Allow-Origin` from all 24 remaining API handlers + add CI regression-lock | 24 source files + `.github/workflows/ci.yml` | none | ~260 LOC (240 deletions across 24 files + ~20-25 lines added to ci.yml) |
Single brief is the right decomposition because:
1.**Mechanical sweep, no per-file decisions.** Every file's diff is structurally identical (Pattern A or Pattern B, both documented verbatim in Brief 1). Splitting into N briefs would multiply doc-writer overhead with zero architectural benefit.
2.**CI regression-lock belongs in the same PR.** Landing the grep gate in a separate brief creates a window where a re-scaffolded handler could slip in undetected (and forces the regression-lock to grep-check against an empty cleanup, which would be a no-op).
3.**Under-400-LOC threshold honored.** ~260 LOC of diff fits comfortably under the architect-contract's brief-size budget.
4.**Reviewable as a single diff.** Reviewers can grep-spot-check the 24 files in seconds (every diff should be a pure deletion of the same 9-11 lines); the new CI job is a single self-contained block.