Repository navigation
docs(api): write down the API conventions - #676
Merged
Merged
Conversation
API.md covers how an /api/v1 endpoint is built on the operations layer, how it documents itself, what each method, list and error looks like, and which consumers it has to stay compatible with. CLAUDE.md points agents at it, and CONTRIBUTING.md's description of routes and server actions now matches the operations layer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
Claude finished @alukach's task in 16s —— View job ✅ No blocking issues — safe to merge. This is a documentation-only PR (
Simplify (ponytail)
💰 Estimated review cost: $0.11 · 0m16s · 4 turns |
…ntions Code review of #676 found the guide stating rules the code doesn't yet follow. Name the exceptions instead of implying they're closed: the data-connections routes and older actions that still call isAuthorized, the revocations route, the trust exchange's 403 body, the 500 `internal` error, the coverage test's allowlist and regex, HEAD /accounts answering anyone, operations that validate before authorizing, and accountsTable.update writing name/type and metadata whole. Drop the unverified `vercel metrics --group-by route` command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alukach
marked this pull request as ready for review
October 16, 2026 03:19
alukach
added a commit
that referenced
this pull request
Oct 16, 2026
…ody (#678) `updateProduct`, `deleteProduct` and `updateMembership` parsed their input before checking whether the caller could act. A signed-in stranger who sent `PATCH /products/{a}/{p}` with a bad body got `field_errors` back instead of a 403, which tells them the product exists and what its schema accepts. Found in review of #676, whose guide says to authorize before validating wherever the permission doesn't depend on the input. ## What - **`updateProduct` and `deleteProduct`** fetch the product and authorize first, then parse the body or query. An unknown product is now `not_found` whatever the body, and a caller who may not act is `forbidden` whatever the body. - **`deleteProduct`** answers an empty account or product ID as `not_found` without asking DynamoDB, as `updateProduct` already does. - **`updateMembership`** refuses, before reading the body, a caller who may make no change at all to the membership: one holding none of `UpdateMembershipRole`, `AcceptMembership` or `RevokeMembership` over it. Since #679 the body is `{ role }` or `{ state }`, and each needs a different right, so the right that change needs is checked once the body is parsed: the manager's for a role (against the current role and the requested one), the invitee's to accept, the member's or a manager's to revoke. A caller who may change a membership can still be told its body is invalid. - **`changeMembership`** takes one `decide(m)` callback that answers `ok(change)` or a failure, in place of a `refuse` callback plus a fixed `change`. That lets `updateMembership` build its change from input it parses after authorization. `acceptInvitation`, `rejectInvitation` and `revokeMembership` move onto it with the same behavior; the accept and revoke decisions are shared with `updateMembership`. `createProduct` and `inviteMember` stay as they are: their permission depends on the parsed input. `withApiSession` still answers a body that isn't JSON at all with 400 before any operation runs; that says nothing about the resource, and moving it would change every route, so it is out of scope here. ## Docs, ADRs and `llms.txt` No user-facing flow or endpoint changes, so docs.source.coop and `llms.txt` still hold. The data-proxy ADRs describe who may act, not the order of checks, so none change. `API.md` in #676 already states this convention; once this merges, the matching line in #676's "Where the code doesn't follow the guide yet" list can go. ## Testing - The refusal and not-found tests for all three operations send an invalid body, so they fail if parsing moves back above the lookup or the authorization. - A new `updateMembership` test gives the caller one right at a time. It fails if the pre-parse check goes back to the role right alone, which would refuse an invitee accepting. - On the head after merging `main` (with #679): `jest src/lib/operations src/app/api src/lib/actions/memberships.test.ts`, 16 suites, 201 tests pass. - `npm run type-check` passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows #667, now merged. Writes down the API conventions this round of PRs settled on, so the next endpoint follows them without anyone reconstructing them from the code.
What
API.mdat the repository root, besideCONTRIBUTING.md:registerPathbeside the handler, the coverage test and itsUNREGISTEREDallowlist, published prose, explicitHEAD,bearerandoptionalBearer,internal, anddeprecated.PATCH { "disabled": true }, neverDELETE, and one resource has one edit endpoint.{items, next_cursor}, never a bare array.internalerror and the trust exchange's{trusted: false}403, authorizing before validating where the permission doesn't depend on the input, and answering 404 for things the caller may not know of.accountsTable.updatealways writes.CLAUDE.md(andAGENTS.md, which links to it) gets a short API section telling an agent to readAPI.mdbefore touching/api/v1.CONTRIBUTING.md's "Server Actions & API Routes" paragraph said both callisAuthorized()themselves. It now describes them as adapters over one operation and points atAPI.md.Where it lives
A root
API.mdrather than more ofCLAUDE.md: these are conventions for whoever writes an endpoint, person or agent, and much of it is visible in the code once you know where to look, which is whatCLAUDE.mdsays it isn't for.CLAUDE.mdcarries the pointer, so agents still load it at the right moment.CONTRIBUTING.mdlinks to it.Merge order
The guide describes the API as it stands once the open PRs from this round merge, and names things they add:
optionalBearer,internalanddeprecatedendpoints (feat(api): accounts, whoami and the internal routes on the ops layer #667)data-proxy-contract.test.ts(test(api): pin the endpoints the data proxy calls #671)HEAD(feat(api): document HEAD /products/{account_id}/{product_id} #672)pageSchemaandonePage(feat(api): answer the membership lists a page at a time #673)PATCHper account (feat(api): edit every account with PATCH /accounts/{account_id} #674)DELETEthat only deletes (feat(api)!: one endpoint per account: DELETE only deletes; profile and flags fold into /accounts/{account_id} #675)Merge it after them, or trim the line that names whichever of them doesn't land.
Where the code doesn't follow the guide yet
The guide states the conventions, not the code's progress toward them. Known gaps, tracked outside the docs:
/data-connectionsroutes and the data-connection actions still read tables and callisAuthorized()directly: API: data connections, including account-owned ones #593.createAccountactions still callisAuthorized()directly: API: accounts, organizations and profiles #592 and API: service accounts and their trusts #594.updateProduct,deleteProductandupdateMembershipvalidate the body before authorizing, so a caller who may not act sees field errors instead of a 403.accountsTable.updatealways writesname,typeand whole metadata maps, soupdateAccountcan undo a concurrent edit.Docs, ADRs and
llms.txtRepository-internal guidance only: no user-facing flow changes, so docs.source.coop and
llms.txtare unaffected, and it records no decision shared with the data proxy beyond the existing contract test, so no data.source.coop ADR changes.Testing
Documentation only; no code changed. Each claim in
API.mdwas checked against the code after a review pass found several the code didn't hold to (route-coverage.test.ts,accountsTable.update,withApiSession's error handling, the revocations and trust-exchange routes). Checked thatAGENTS.mdstill resolves toCLAUDE.mdand that the prose inAPI.mdwraps at 80, as the repository's Markdown does.🤖 Generated with Claude Code