MOTIR-2366Done
The unconfirmed gates (2/2) — the four public reads, the three ready-set routes and the two upload endpoints
The other half of the guard's CLAIMED_BUT_UNVERIFIED bucket. Same contract as its sibling — one read each, ending in a named hop or a gate — but these nine are harder, because two of the three groups are supposed to be reachable without a project membership and the question is what the right check is, not whether there is one.
| Operation | Gate today (per the inventory) | Claimed permission |
|---|---|---|
/api/public/categories · /api/public/explore | session only | public_request:submit |
/api/public/p/[identifier]/roadmap · /tree | session only | public_request:submit |
/api/ready · /api/ready/next | workspace only | project:browse |
/api/ready/nudge | session only | project:browse |
/api/upload/avatar · /api/upload/issue-attachment | session only | attachment:create |
What each group actually needs
- The public reads. Two siblings in the same directory already do it properly —
/api/public/p/[identifier]/itemsgates onassertCanBrowsePublicand/api/public/projects/[projectId]/requests/duplicatesonassertCanSubmitToTriage. These four take a session and nothing else. The claimed key is wrong as well: reading a public project's roadmap or tree is browsing, not submitting, so the rows wantproject:browseresolved through the public-access path, and the mapping is corrected as part of the read. - The ready set.
/api/readyand/api/ready/nextanswer what should I work on, which is a project-scoped read;nudgeis session-only today. Gate onproject:browsefor the project each result belongs to — and check whether the LIST leaks across projects the actor cannot browse, which is the failure mode a per-request gate would not catch. - The uploads. A session-only upload endpoint accepts bytes from anyone signed in.
attachment:createis the claimed key and it is project-scoped, soissue-attachmentneeds the project it is attaching to;avataris genuinely user-scoped and its row is probably mis-mapped — if so, correct it touser-scopedrather than inventing a project for it.
Acceptance criteria
- All nine are read, and each row records either the named hop that reaches its gate or the gate this card added.
- The four public rows end gated through the same public-access predicate their two working siblings use, with their claimed permission corrected where the read shows it wrong.
/api/upload/issue-attachmentasserts a project-scoped key resolved from the work item it attaches to;/api/upload/avataris either gated or re-decided asuser-scoped, and the PR body says which and why.- The ready-set routes cannot return an item from a project the caller may not browse, proved by a test with two projects and one membership — not by a single-project happy path.
- Where a gate was real but invisible, the walk is extended to see it, with both halves of the control the file's earlier repairs use.
- The guard's
CLAIMED_BUT_UNVERIFIEDpin is re-derived by running it on this branch and falls by exactly these nine, reaching zero once its sibling card has also landed. pnpm test tests/permissions/ tests/services/is green.
Context refs
app/api/public/p/[identifier]/items/route.ts— the sibling that gates correctly, and the predicate to copy.app/api/public/p/[identifier]/roadmap/route.ts,/tree/route.ts,app/api/public/categories/route.ts,app/api/public/explore/route.ts— the four to read.app/api/ready/route.ts,/next/route.ts,/nudge/route.tsand the service behind them.app/api/upload/avatar/route.ts,app/api/upload/issue-attachment/route.ts.lib/projects/access.ts—canBrowsePublic/canSubmitToTriageand the level-gated grants.docs/decisions/public-projects.md— the Story 6.12 decision that defines what an anonymous or non-member actor may read.- The unconfirmed gates (1/2) — the sibling card; the two together empty the bucket.