fix(metadata-protocol): getMetaItemLayered gates the org read, bound after the canonical fold - #16034
Conversation
…he canonical fold `getMetaItemLayered` — the third `/meta` read verb — applied no registry read gate of its own, so a caller could spend a raw active organization on a type with no per-org read channel and a pre-#6190 phantom org-scoped row became the `overlay` layer of the three-layer diagnostic. The binding moved BELOW `canonicalizeMetaRequestType` rather than being replaced in place: it sat above the fold, so the sibling verbs' one-liner would have gated on the RAW type, which #10340 measured splits one item across two partitions for the URL-only spellings (`translations` / `email_templates`). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…old ordering Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
The layered read now gates its organization, so an allowOrgOverride:false type issues no org-scope read at all and the case would pass vacuously. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
📓 Docs Drift CheckThis PR changes 1 package(s): 24 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 006a4127460c14f43265af4dcc7506828fa80f4c && git checkout 006a4127460c14f43265af4dcc7506828fa80f4c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 784595113caeb45bf3e0776a7550c5dbb240a9c5 f4ca1bceb48862eaa1492db8a46559fc23af4765 && git checkout -B drift-repro 784595113caeb45bf3e0776a7550c5dbb240a9c5 && git merge --no-ff f4ca1bceb48862eaa1492db8a46559fc23af4765
node scripts/docs-audit/affected-docs.mjs --json 784595113caeb45bf3e0776a7550c5dbb240a9c5
|
Fixes #14907
getMetaItemLayeredis the third/metaread verb, and the third instalment of the ungated-org-read series (#9454 → #14683 plural → #14770 singular). It applied no registry read gate of its own, so a caller could spend a raw active organization on a type with no per-org read channel and a pre-#6190 phantom org-scoped row became theoverlaylayer of the three-layer diagnostic — a false positive customization claim, rendered by the Studio "Code default vs Overlay vs Effective" tab as evidence.The repair is a REORDER, not the siblings' one-liner
In both twins the
orgIdbinding already sat aftercanonicalizeMetaRequestType, so the fix there was a substitution. Here it sat before the fold, and dropping the same expression in place would have gated on the RAW type — the #10340 defect:declaresOrgOverridetolerates the manifest plurals but not the URL-only spellings (translations/email_templateshave no manifest key), so a raw segment splits one item across two partitions, addressed by spelling.So the binding moved below the fold:
Site re-derived rather than taken from the card, which warned its own numbers drift: the ungated binding was at
:7812, not the:7838recorded. Both of the card's commands fired, each serving as the other's control —const orgId = request.organizationId;returned 1 hit before the change and 0 after; the gated-binding control returned 2 before (the landed twins) and 3 after.The card asked for this to be verified per site rather than inherited. It was, and it does not hold as written. Both errors are in the direction that ADDS callers to the gate, so the remedy shape is unchanged — but the idempotence proof had to be discharged over the real population.
runtime/src/domains/meta.ts:346rest-server.ts/layers:3410, not:3172)rest-server.tssecond doorplugin-securitysitesThe second REST door.
GET /meta/:type/:name/publishedpassespublishedCtx.tenantIdraw and thenres.json(layered.overlay)— structurally the same shape as the runtime dispatcher branch the card identified, so it is a second reachable serving site, not a gated one. Its own comment argues the raw tenant "is right for a READ ... nothing that resolves today stops resolving", which is exactly the fail-open reasoningorganizationIdForMetaRead's docblock exists to refute. That comment is why two seats recorded this door as gating; filed as #16032 (behaviour is repaired by this PR — only the comment is left, and correcting it would pullpackages/restinto the diff).The
plugin-securitysite.permission-set-projection.ts:770forwardsevt.organizationIdon a hard-codedtype: 'permission', which isallowOrgOverride: false. It is fed bypermission-set-overlay-discard.ts:244(callerCtx?.tenantId, raw) and by the mutation-projector subscription. It moves, and moving it makes the projection read the partition boot hydration actually serves.⇒ Three callers move, each one the defect. Every other caller is a no-op by
f(t, undefined) === undefinedorf(t, f(t, o)) === f(t, o); the proof is written out at the gate and pinned in §4/§5.Tests —
packages/metadata-protocol/src/get-meta-item-layered-org-read-gate.test.ts18 cases. §0 pins the tier-A five and that
object/permissionare outside it; §1 is the phantom case; §2 is the must-not-deny-everyone direction; §3 is the section neither twin has — the after-fold ordering; §4/§5 are the idempotence legs over the complete accepted-spelling population; §6 pins the predicate both serving doors read.Two ablations, disjoint failure sets — mutation proven on disk before measuring, restore proven byte-exact after, in one shell each:
Ablation B is the one that matters: it demonstrates the sibling verbs' one-liner would have shipped the #10340 partition split, and that this PR's pin catches it. Restores verified by blob hash (
f5f22093…both sides) and an emptygit diff HEAD.exit 99(verify-lock queue timeout) — no reading, not a green; it was re-run to a real result, narrowed off the lock and declared below.Fixture triage
One existing pin needed re-triage rather than a spelling tweak:
protocol.metadata-store-outage.test.ts's "a failed ORG-scope read does not silently demote" usedtype: 'object'with an organization. Gated, that type issues no org-scope read at all, so the case would have passed vacuously against a method that had stopped doing the thing the case is about. Moved toview(tier-A), where the org arm is really taken. Scanned by the rule's consumption radius, not by package: the only other real-verb fixture carrying an organization isobjectql'sprotocol-layered-get.test.ts, which already usesview— unaffected, and re-run green against the freshly builtdist.Verification (branch head
f4ca1bceb)pnpm --filter @objectstack/metadata-protocol typecheck— exit 0. Itstsconfigisinclude: src/**/*with no test exclusion, so this covers the new test file rather than passing over it.objectqlprotocol-layered-get+protocol-meta— 2 files, 102 tests, exit 0, against adistrebuilt at this head.check:nul-bytes,check:where-matcher,check:objectql-double-limit,check:engine-double-contract,check:test-source-alias,check:cross-package-test-inputs,check:filter-alias-parity,check:durability-log-level,check:type-source-resolution,check:published-files. The two ratchets reportbaseline key set verified against f7db8f4: no files added— the new test double needed no baseline entry.Declared narrowing. The full 154-file package suite is not run here: the shared verify lock returned
exit 99on five separate acquisitions (box under heavy contention; holders and queue read from--status). Per the queue discipline the check was narrowed to the derived affected set above and the narrowing is declared rather than left implicit. CI runs the farm.Tier — Clause-②: no, re-measured, not inherited
The claim's
nowas not taken on trust.api-surface/covers onlypackages/spec, so the published surface here isfiles[]+types: built at head, swappedprotocol.tsback to the merge base, rebuilt, diffed the builtdist/index.d.ts, then restored byte-exact and rebuilt again.Byte-identical. The diff changes a local
constinitializer and adds comments; no exported symbol, signature or type moves, and no request is newly accepted or rejected.Notes
/meta/:type/:name/publisheddoor argues in a comment that a RAW tenant "is right for a READ" — the argumentorganizationIdForMetaReadexists to refute, and the door the #14907 card recorded as already gating #16032 (the/publisheddoor's comment). A correction was also posted to spec:GetMetaItemsRequestSchema.organizationId's describe() promises the org partition is consulted whenever it is supplied — PR #14767 makes that false for registry-non-overridable types, and three sibling schemas share the wording #14772, whose table records this verb'sorganizationIddescribe() as "arguably untouched" — this PR moves that row to affected, and its premise was already false because of the second door.Lint & Repo Gatesmay red oncheck:merge-driverwithself-test failed -- 1 failure(s) (cases and floor). That ischeck-regen-pending --self-testruns its gate stubs through ambientpnpmin a temp dir with nopackageManager, so a launcher that cannot resolve there reds an innocent PR — reproduced 2/2 onorigin/mainand on an unrelated branch #15990 / main red:Lint & Repo Gates› Merge-driver wiring gate —check-regen-pending.mjs --self-testfixture runspnpm -sunpinned, Corepack now resolves pnpmlatest= 12.3.4 which rejects-s; merge queue blocked since 17:05Z #15992, fix queued as PR fix(devx): pin the regen self-test fixture to the root's packageManager so Corepack never resolves pnpm latest (#15992) #16002 — unrelated to this diff.Generated by Claude Code