Files
sim/apps/docs
Waleed 81108d5a4e fix(v2): serve HEAD, advertise PATCH, and document the reachable 403 (#6623)
* fix(v2): serve HEAD, advertise PATCH, and document the reachable 403

Three HTTP-semantics defects on the v2 surface, all found by probing the
published contract rather than the happy path.

**HEAD answered 500 on every v2 endpoint.** Next implements a missing `HEAD`
export by aliasing it onto `GET` and dropping the body when it sends, so a
route's `GET` legitimately runs with `request.method === 'HEAD'`. The builders'
method guard compared that against the contract's declared method and threw, so
`HEAD /api/v2/workflows` and every sibling replied 500 — which is what health
checkers, uptime monitors, link checkers, and some CDNs send, all of them
reading the API as hard-down. RFC 9110 §9.3.2 makes HEAD identical to GET but
for the body, which is exactly what running the GET path produces. Fixed once in
`methodMatchesContract`, shared by all five route builders; every other mismatch
stays a hard error so a handler exported under the wrong verb still fails loudly.

**CORS advertised `GET,POST,OPTIONS,PUT,DELETE`** while the v2 spec has 17
`PATCH` operations, so a browser preflight for any of them was rejected. It also
advertised `PUT`, which two operations use — the shape of a hand-maintained list
outgrown by its surface. The list stays hand-written because middleware cannot
import the contract tree without pulling Zod into the edge bundle, but it is now
pinned by a test that sweeps the real contracts and fails on any method it omits.

**Six operations omitted a 403 their siblings documented** — three knowledge
reads and three file-upload operations. Traced from the code rather than the
spec: `requirePermission` throws `NoWorkspaceAccessError` for no access at all
(concealed as 404) but `InsufficientWorkspacePermissionsError` for access below
`minimumRole` (a real 403), and `PersonalApiKeysDisabledError` reaches every
operation a personal API key can call. So 403 was reachable on all six and the
omission was an accident of hand-assembled error lists, not a policy. They now
use the shared `RESOURCE_ERRORS` / `RESOURCE_CONFLICT_ERRORS` sets, and two
operations spelling those same sets by hand were normalized onto them.

All 128 documented operations now declare 403. The rules for HEAD, for the
403/404 split, and for using the shared error sets are recorded in
`.agents/skills/v2-api-conventions/SKILL.md`.

* test(proxy): update the CORS policy assertion to the served method list

`proxy.test.ts` pinned the previous hand-written method string, so widening
`resolveApiCorsPolicy` to advertise PATCH and HEAD left it asserting a list the
middleware no longer returns. The literal is kept rather than imported from
`proxy.ts` so the test still pins the exact wire value independently of the
implementation.

* refactor(v2): retire the error sets that could omit Forbidden

The three knowledge reads and three upload operations lost their `403` by
assembling `[...VALIDATED_ERRORS, ...]` by hand, and `VALIDATED_ERRORS` /
`STANDARD_ERRORS` were the only exported sets that omit `Forbidden`. Migrating
the last consumers to the shared `RESOURCE_*` sets left both unreferenced, so
deleting them turns the fix from a one-time cleanup into an invariant: there is
no longer a building block from which a workspace-scoped operation can assemble
an error list without `Forbidden`. Regenerating the specs produces no diff, so
the migration is output-neutral.

Also folds `method-match.test.ts` into `definition.test.ts` to match the
repo's `feature.ts` -> `feature.test.ts` convention, types `contractMethod`
as `HttpMethod` so a contract declaring `HEAD` is unrepresentable, and drops
the duplicated Next-aliasing rationale so `methodMatchesContract`'s TSDoc is
its single home.

* fix(cors): expose the API response headers a browser client needs

Without `Access-Control-Expose-Headers` a browser can read only the six
CORS-safelisted response headers, so the rate-limit budget, the `Retry-After`
a 429 or 503 asks the caller to observe, and the request/run correlation ids
were all on the wire but invisible to `fetch()`. Server-to-server callers were
unaffected, which is why it went unnoticed.

Exposed on the default `/api` policy only. The per-route `CORS_RULES` entries
are wildcard-origin public endpoints and opt in individually if they ever need
it, so this does not widen what an anonymous cross-origin caller can read from
them.
2026-08-12 11:09:33 -07:00
..
2026-02-16 00:00:12 -08:00

docs

This is a Next.js application generated with Create Fumadocs.

Run development server:

bun run dev

Open http://localhost:3000 with your browser to see the result.

Learn More

To learn more about Next.js and Fumadocs, take a look at the following resources: