You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[finding] GET /meta/:type/:name/diff serves PENDING draft content to a member with no authoring capability: its history versions include draft saves, and it is the one draft-serving door the #20338 gate leaves open #20378
Filing gate: ① a defect with a named landing site, packages/rest/src/rest-server.ts, the GET /meta/:type/:name/diff handler (registerPerItemRoute → diffMetaItem). Finding class (b), with reach: measured at a public HTTP door; security exception: it leaks unpublished content.
Found by the os-dev round on #20338 (PR #20373). The order for that round forbade gating this door, because it is one of ruling B's stored-version doors (#20156). Verified at source and filed by the domain:cli execution seat (#6024, session_01UYBdGBzWSrAMzpW8ah3GbP). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
What happens (measured)
The dev drove the real REST stack at 8bc02e48b (the PR #20373 branch; this door is unchanged from origin/main there). The caller was a member with no authoring capability (none of system, studio.access, setup.access or manage_metadata) who may open the app.
door
answer
GET /meta/app/NAME/diff?from=0&to=2 on a published app with a pending draft
200, carrying the draft's label and a draft-only navigation entry
the same on a draft-only app
404
GET /meta/app/NAME/history
events only, no bodies
The seat's reading at source (origin/main)
sys_metadata_history stores a full body for every draft save: the repository's single put appends a history row for state: 'draft' saves too (sys-metadata-repository.ts). diffMetaItem (packages/metadata-protocol/src/protocol.ts) reads versions with where { organization_id, type, name } and no state filter.
For app, doc and book (gatesPerCaller), the REST handler judges every side with metaItemReadGate(..., RestServer.STORED_VERSION_DOOR_POLICY). That gate decides who may open the item and prunes by audience. It never asks whether the caller may read drafts.
Triage grades and routes it. If it lands in the domain:cli lane, the landing site is the /diff handler in rest-server.ts, a hot file on that lane's serial queue.
Ruled: 5865708652 · letter B · 2026-09-28T07:49Z
Filing gate: ① a defect with a named landing site,
packages/rest/src/rest-server.ts, theGET /meta/:type/:name/diffhandler (registerPerItemRoute→diffMetaItem). Finding class (b), withreach:measured at a public HTTP door; security exception: it leaks unpublished content.Found by the
os-devround on #20338 (PR #20373). The order for that round forbade gating this door, because it is one of ruling B's stored-version doors (#20156). Verified at source and filed by thedomain:cliexecution seat (#6024,session_01UYBdGBzWSrAMzpW8ah3GbP). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens (measured)
The dev drove the real REST stack at
8bc02e48b(the PR #20373 branch; this door is unchanged fromorigin/mainthere). The caller was a member with no authoring capability (none of system,studio.access,setup.accessormanage_metadata) who may open the app.GET /meta/app/NAME/diff?from=0&to=2on a published app with a pending draftGET /meta/app/NAME/historyThe seat's reading at source (
origin/main)sys_metadata_historystores a full body for every draft save: the repository's singleputappends a history row forstate: 'draft'saves too (sys-metadata-repository.ts).diffMetaItem(packages/metadata-protocol/src/protocol.ts) reads versions withwhere { organization_id, type, name }and no state filter.For
app,docandbook(gatesPerCaller), the REST handler judges every side withmetaItemReadGate(..., RestServer.STORED_VERSION_DOOR_POLICY). That gate decides who may open the item and prunes by audience. It never asks whether the caller may read drafts.For every other type (for example
view),/difffetches no current document and applies no per-caller gate. So the draft-only 404 above holds only for those three types. This is a reading at source, confirmed by the contract review of PR fix(rest, runtime): serve pending metadata drafts only to a caller with an authoring capability #20373 (5863212700); it was not measured.The runtime dispatcher (
packages/runtime/src/domains/meta.ts) has no/diffroute. This door is REST-only.Once PR fix(rest, runtime): serve pending metadata drafts only to a caller with an authoring capability #20373 lands, every door that takes a draft switch asks
isObjectSchemaMaskExemptfirst:?state=draftand?preview=draft;?preview=draft;previewDrafts;?preview=draft./diffstill serves draft bodies to the same member.Governing text
2a29caachangeset: 「declaration ≠ authorization … draft access stays admin-gated upstream」.5856774816, confirmed5856866273) sets this door's pruning policy (STORED_VERSION_DOOR_POLICY,app: 'author-exempt'). A fix must keep that policy for what the door still serves. Whether a member keeps seeing published history on/diffwhile draft versions are withheld is the shape triage has to set.Who acts on it
Triage grades and routes it. If it lands in the
domain:clilane, the landing site is the/diffhandler inrest-server.ts, a hot file on that lane's serial queue.Duplicate check
Searched the board, open and closed:
?state=draftand?preview=draft: no builder gate, though ADR-0037's Risks row and ADR-0106 D4 assume one #20338 (the parent, gating the draft switches) and [finding] an app author's draft baseline is read through the pruned plain read (GET /meta/app/:name?state=draft), and the designers merge it over the whole stored app, so a draft save drops the navigation entries withheld from that author #20290 (the author's draft baseline) are relatives. The rest are unrelated (SqlDriver org auto-scope makes SysMetadataRepository.listDrafts blind to env-wide drafts (organization_id IS NULL ∧ injected org = always empty); mode=draft saves drop package_id #11087, pm-dispatch: the director seat presents the governed drafts awaiting a human merge as one decision batch, and checks each carries its four-piece (maintainer, 2026-09-13) #17951, [finding] diffMetaItem awaits a full historyMetaItem read and discards it — a dead round trip on every live diff request #8798, draft-publish-lifecycle: the metadata audit trail records onlysave— publish, rollback and the 409 conflict denial never write a row #7748, [metadata-protocol] SysMetadataRepository.publishDraft() 把 draft 清理的全部失败都当「并发发布者已抽走」,静默留下一条永远 pending 的 draft 行 #4981, [P2] Write-only / disconnected metadata (tool, email, sharing, spec-bridge) #1892).?state=draftand?preview=draft: no builder gate, though ADR-0037's Risks row and ADR-0106 D4 assume one #20338 is the parent and [finding] class closure: the alternate read doors of /meta/:type/:name (/layers, ?layers=true, /published) serve a set-gated doc's BODY to a non-holder; they skip every per-caller read gate the plain read applies #20156 closed the alternate doors' per-caller gates. Neither covers draft versions on/diff.Generated by Claude Code