Repository navigation
fix(rest,runtime): ?package=all names no package on every /meta door of both hosts (#22188) - #22265
Conversation
…of both hosts The layered read, the list, the book tree and the diagnostics sweep on RestServer, and the dispatcher's book tree, layered read, item read and save, and list, forwarded ?package=all to the store as a literal package id. The layered read answered 404, the list [], the book tree an empty implicit book, and the dispatcher's PUT bound the row to a package called all. Each now reads ?package= through metaItemPackageBinding, which @objectstack/rest exports from its entry for the runtime to take. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-Authored-By: Claude <noreply@anthropic.com>
The Studio editor reads a pending draft back with its raw router ?package=, so under the list's "show everything" scope it sends ?state=draft&package=all. Both hosts now serve the draft ?state=draft serves. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 34 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 558f54f896a21b36856ba8bc89225e6dc43c8474 && git checkout 558f54f896a21b36856ba8bc89225e6dc43c8474
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9f0de32a03629a021836d9216dbca86ccdc645f3 db727d0d1b23908aef12d2d622b223c244b8646a && git checkout -B drift-repro 9f0de32a03629a021836d9216dbca86ccdc645f3 && git merge --no-ff db727d0d1b23908aef12d2d622b223c244b8646a
node scripts/docs-audit/affected-docs.mjs --json 9f0de32a03629a021836d9216dbca86ccdc645f3
|
Contract reviewServed-tier: ① Derived judgmentsInputs read. Card #22188 (body and all 6 comments: the cli seat's pointer Public surface, one change. Answer changes, each judged. The contract is the one #22128 / PR #22185 stated and pinned:
The cache bypass left raw ( Nothing left unfolded. At head, every Truth of the changesets and PR body against the diff. Rest changeset: the export sentence, the four-door list, the function's contract ( ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…ld permission for every caller class (objectstack-ai#22250) (objectstack-ai#22308) Fixes objectstack-ai#22250 Clause-②: yes (widening) ## What this does For one caller, the object metadata read and the data route disagreed about a field that caller's field-level permission marks unreadable. The `sortability` projection served beside the schema on `GET /meta/object/NAME` marked that field `sortable: true` for a platform-admin or organization-admin caller, while the data route refuses a sort or filter on it with `403 PERMISSION_DENIED`. After this PR, the `sortability` projection is derived from the caller's own field permission for every caller class. The object definition itself (`item.fields`) is unchanged: whole for the exempt classes, because Studio and Setup authoring need it, and masked for everyone else. The data route and the permission read are unchanged. The spec's description of the projection is amended to match. ## Measured first: the exemption and its stated reason - **Where it is.** `isObjectSchemaMaskExempt` in `packages/metadata-core/src/object-schema-fls.ts` (ADR-0106 D4). It exempts `isSystem` and any caller holding `manage_metadata`, `studio.access` or `setup.access`. The platform-admin set holds all three. An organization admin holds `setup.access`. `resolveObjectSchemaMaskPosture` answers such a caller `passthrough` / `exempt` before it asks the security service. - **Its stated reason.** ADR-0106 D4: "Studio/Setup authoring requires the full schema." The code adds the write-gate derivation: a projected read sent back by a `PUT` would delete the fields it withheld. So this is the triage ruling's first branch: keep the definition read for authoring, and make the runtime projections agree with the caller's field permission. - **Dispatch assumption M2 was partly wrong.** The sortability projection (`translateMetaEnvelope` in `packages/rest/src/meta-item-read-gate.ts`, which both transports call) already read the post-mask document. For an exempt caller, though, that document is the unmasked one, so the projection inherited the exemption. - **Picker-facing field lists on the server.** `sortability` is the only runtime projection the object read serves beside the definition. The list read and the layered view serve definitions only, with no projection. `item.fields` is itself the definition the ruling keeps for authoring. objectui handles its pickers against the permission read on its side (objectui#11925, objectui#11943). ## Door table: `examples/app-showcase`, real boot Setup: `showcase_client_liaison` (which marks `showcase_project.budget`, `spent` and `budget_remaining` as `readable: false`) is bound to the `everyone` position. Callers: the seeded platform admin; `phone.demo` promoted to organization `admin`; `auditor.demo` as a member. The table was measured at base `c8bb3c8d9c` and at head `8b412a83e2`, then re-measured on the final head `a337d0cc53`, which carries `805f1ba09a`'s change to the exemption predicate's source and the spec text. Every head cell below reads the same on `8b412a83e2` and on `a337d0cc53`. | Door | Platform admin, base → head | Organization admin, base → head | Member (control), base → head | |---|---|---|---| | `GET /auth/me/permissions`, `budget` | `readable: false` → same | `readable: false` → same | `readable: false` → same | | `GET /data/showcase_project` | field absent from rows → same | field absent → same | field absent → same | | `GET /data/showcase_project?sort=budget` (and `?budget=100`) | `403 PERMISSION_DENIED` → same | `403` → same | `403` → same | | `GET /meta/object/showcase_project`, `item.fields.budget` | present (22 fields) → present (22) | present (22) → present (22) | absent (19) → absent (19) | | same read, `sortability.fields.budget` | `{ sortable: true }` (23 entries) → **absent** (20 entries) | `{ sortable: true }` (23) → **absent** (20) | absent (20) → absent (20) | | same read, `ETag` | `"39ce56dc"` → `"39ce56dc~3c4b8092"` | `"39ce56dc"` → `"39ce56dc~3c4b8092"` | `"39ce56dc~32c59ec2"` → same | | `GET /meta/object/showcase_project/layers` (authoring) | `effective` 22 fields with `budget` → same | same → same | 19 without `budget` → same | At head, all three classes are served byte-equal `sortability`. Conditional reads, the same on both heads: - The platform admin's own ETag answers `304`, and so does the organization admin's own ETag (on `a337d0cc53`). - The pre-change ETag `"39ce56dc"` answers `200`. - The member sending the admin's ETag gets `200`. After unbinding the set, the unrestricted admin's ETag is `"39ce56dc"` again (byte-identical, ADR-0106 D3), and `budget` is `{ sortable: true }` with 23 entries. ## How - **`@objectstack/metadata-core` (`object-schema-fls.ts`).** The `exempt` posture of a caller exempt by capability, in a deployment with a `security` service, carries a lazy `runtime` question. That question asks the same `getMetadataReadableFields` the mask asks a non-exempt caller, at most once. It never rejects: a throw or an `undefined` answer settles `undetermined`, with one `warn`, so D4's "a sick security service cannot fault an exempt caller" holds. `resolveObjectSchemaRuntimeView(document, posture)` returns the view the runtime projections are derived from: the served document for every other caller, and for an exempt caller the `fields` their permission reads, plus a fingerprint of what it withholds. Each withheld name in that fingerprint carries a qualifier (`@` occurs in no machine name), so an exempt caller and a projected caller denied the same field never share a validator. `isObjectSchemaMaskExempt` now delegates its capability half to a private helper, which the resolver asks alone. That adds no second elevation read (`check:system-context-census`). - **`@objectstack/rest`.** - `translateMetaEnvelope` takes an optional `runtimeDocument` (default: the served document) and derives `sortability` from it. - `createMetaItemAnswer` step 5 computes the runtime view and passes it through the `translateEnvelope` port, which gains an optional third parameter. - In `rest-server.ts`, the cached arm of `GET /meta/:type/:name` treats a posture carrying `runtime` like a projecting posture: the protocol is not handed `If-None-Match`, the runtime view's fingerprint is folded into the ETag, and the `304` is judged against that folded value. The private `translateMetaEnvelope` and `metaItemAnswerSources` forward the view. - **`@objectstack/runtime` (`domains/meta.ts`).** Only `answerMetaItem`'s `translateEnvelope` port, which forwards `runtimeDocument`. Every dispatcher item read already goes through `createMetaItemAnswer`. ## Spec text (cross-lane, `domain:spec`) Declared by the seat on `domain:spec`'s seat post. The change is to text only: no shape, key, enum or accept-set change. - **`packages/spec/src/api/sortability.zod.ts`.** - The `ObjectSortabilitySchema.fields` `.describe()` now says: "The domain is the served field map plus the always-provisioned `id`, less any field the caller's own field permission withholds: a caller exempt from the object-schema mask (ADR-0106 D4) is served such fields in `item` and gets no entry for them; for every other caller the served map already excludes them." The list of absent names gains "a field the caller may not read". - The module docblock's category list gains case 5, "Withheld by the caller's field permission". Case 1's domain sentence carries the same exception, which removes nothing for every caller but a D4-exempt one. - `resolveObjectSortability`'s docblock says that, for a D4-exempt caller, it runs on the document narrowed to the fields the caller's own permission reads. - **Generated.** `content/docs/references/api/sortability.mdx` and `content/docs/references/api/protocol.mdx` were regenerated by `pnpm --filter @objectstack/spec check:generated --fix`. That forced a spec build, found only `check:docs` stale (14 of 15 green), and regenerated it. A second `check:generated` run then reported all 15 artifacts current. Nothing generated was hand-edited. - **Changeset.** No `@objectstack/spec` entry was added, because no changeset gate requires one for a description-only change: - `Check Changeset` (`pr-automation.yml`) counts the changesets a PR adds; - `check-changeset-no-major.mjs`, driven offline with this body as its event, reads `Clause-②: yes (widening)` and is satisfied by `metadata-core` and `rest` graded `minor`, and it lists no ungraded spec package; - `check-empty-changeset.mjs` is green. ## Tests - **`packages/metadata-core/src/object-schema-fls.test.ts`** (new describe block): - per class (platform admin, organization admin, member): the runtime view holds only the readable fields, the exempt definition is served whole, and the member's view is the served document by reference; - one permission gives one view whatever the class; no validator is shared with the definition mask; - an unrestricted exempt caller is byte-identical; the question is lazy and asked at most once, and never for a document without `fields`; - `undefined` and a throw both settle undetermined with a `warn`; `isSystem` and a deployment with no service carry no question. One existing assertion went from `toEqual` to `toMatchObject` plus a `typeof runtime === 'function'` check, because the exempt posture now carries the function. Its "asks nothing" pin (`asked === 0`) is kept. - **`packages/rest/src/meta-object-sortability-caller-fls.test.ts`** (new), per class, on the cached (default) and uncached branches: - the definition is unchanged, `sortability` never names the unreadable field, and it is equal across classes; - the member control still agrees with `resolveObjectSortability(item)`; - the cached arm's ETag is fingerprinted, the protocol is not handed the conditional, the admin's own ETag answers `304`, the member's ETag does not cross over, and an unrestricted admin keeps `"v1"` and its `304`; - a throwing service answers `200` with the definition and the unprojected `sortability`. - **`packages/runtime/src/domains/meta-object-sortability-caller-fls.test.ts`** (new): the same per-class pins on the dispatcher's protocol-backed and registry-backed item reads. **Reverse verification (from committed state, on-disk proof by `scripts/ablation-replace.mjs`):** - **A.** `translateMetaEnvelope` was mutated to derive `sortability` from the served document (the behaviour before this PR). The REST suite went 5 failed / 8 passed. After `pnpm --filter @objectstack/rest build`, `ablation-dist-preflight` found the marker in `dist`, and the runtime suite went 4 failed / 4 passed. Restore: blob equal to `HEAD`, `git diff HEAD` empty; rebuild; preflight `--absent` with a clean tree; 13/13 and 8/8 green. - **B.** The exempt posture was mutated to never carry `runtime`. The metadata-core file went 7 failed / 29 passed, all assertion failures. Restored, blob equal to `HEAD`. The first B attempt used an undefined identifier and went red on a `ReferenceError`, so that reading is void. The second was refused by the tool because the replacement contained the anchor, so nothing was mutated. ## Local verification (final head `a337d0cc53`) All suites ran through the shared verify lock. Exit codes were read from its `VERDICT` line. - **This round (spec text and regenerated docs).** - `@objectstack/spec` test: 625 files, 18711 passed and 1 todo. `typecheck` exit 0, which includes `check:scripts-typecheck` and `check:test-typecheck`. - `dispatch-gates --commands` derives 111 commands for the full touched path set (11 paths). All 111 ran and exited 0. - `--ran` reconciliation: 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero: every entry records its exit code). - The derivation warns that one file it reads, `scripts/platform-object-tenancy-census.json`, changed on `main` since this branch's merge base, so that family read this branch's copy. - Narrowed lint on `sortability.zod.ts`: - `eslint --no-inline-config --format json` reports 1 file, 0 errors and 0 warnings. - The effective config (`--print-config`) enables 5 syntax rules and `parserOptions.project` is `null`. - With no type-aware linting, the change cannot move a verdict on a file it does not touch. - **Earlier round (unchanged since).** - `@objectstack/metadata-core`: 18 files, 425 passed; `typecheck` exit 0. - `@objectstack/rest` at `3768ffad20`: 268 files, 5142 passed and 327 skipped; `typecheck` exit 0. - `@objectstack/runtime`: 340 files, 5508 passed and 19 skipped at `805f1ba09a`; `typecheck` exit 0 at `3768ffad20`. - Narrowed lint over those 7 files: 0 errors and 0 warnings. - Gates: 64 of 64 exited 0 at `3768ffad20`. - **Merge.** `main` was merged once, at `9865012548` (objectstack-ai#22265 touched `rest-server.ts` and `runtime/src/domains/meta.ts` in a different region). Against the current `origin/main`, `git merge-tree --write-tree` exits 0 and no file overlaps, so no second merge was made. ## Answered by the seat - **Clause-②.** The measurement decides: `yes (widening)`, with `minor` for `metadata-core` and `rest` and `patch` for `runtime`. The claim's line was revised to match. Two transport-local copies of the posture logic were refused. - **The spec text.** It rides this PR, not a separate card, with `packages/spec` lifted for exactly this text. It is declared cross-lane on `domain:spec`'s seat post. ## Acceptance notes These are observations only, read from source and not measured. No card is filed for them. - **objectui's docblock.** objectui's `packages/core/src/utils/column-sortability.ts` repeats the sentence this PR amends ("the served field map plus the always-provisioned `id`"). That is another repo and is not edited here. Carrier: objectui `domain:ui` seat 1 (objectui#11925's filer). - **Partially masked fields.** For any caller, `sortability` keys on readability, while the security service's query guard also refuses a predicate or sort on a field served partially masked (`maskingRule`). Per `getQueryableFields`'s docblock, a masked field is readable but not queryable. No in-repo app declares a `maskingRule` today (showcase `permission-sets.ts`). Whoever touches `resolveObjectSortability` next should weigh it. - **External objects.** On an ADR-0015 `external` object, an author-declared platform-injected column that the caller cannot read is re-added by `unprovisionedInjectedColumns` as `sortable: true` plus a caveat once the mask removes the declaration. This is the same for every class. --- _Generated by [Claude Code](https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #22188
Clause-②: yes
What this changes
?package=allis the metadata list's "show everything" scope. #22128 (PR #22185) gave the item read, the save and the publish one reading of?package=:metaItemPackageBinding, under whichall, the empty value and a non-string name no package. The other/metadoors forwarded the raw value, soallreached the store as a package id that no item is bound to. Every door that reads?package=, on both hosts, now reads it through that one function.RestServer(packages/rest/src/rest-server.ts): the layered read (serveMetaItemLayered), the list (GET /meta/:type), the book tree (GET /meta/book/:name/tree) and the diagnostics sweep (GET /meta/diagnostics, see H3).packages/runtime/src/domains/meta.ts, the/metadomain behind the@objectstack/hono${prefix}/*catch-all): its book tree, layered read, item branch (the read, the?state=draftread and thePUT) and list.Clause-②: yes).@objectstack/rest's package entry now exportsmetaItemPackageBinding, and the runtime imports it besidemetaSaveRequestOptions. That is the only new export. Changesets:@objectstack/restminorwithClause-②: yes (widening), and@objectstack/runtimepatchwithClause-②: no.packageScoped, H3) still reads the raw parameter.Readings (H1 to H5, on this branch's base
ffb31fca)H1, reproduced on the real stack, each door. The pin file below was run against the base sources before the fix. Each door has its own better-sqlite3
:memory:store with the realsys_metadata*objects and a realObjectStackProtocolImplementation. The store is seeded through the protocol:case_gridincom.probe.pkg,other_gridincom.other.pkg, the env-locallocal_grid, and the bookprobe_bookwith the pageprobe_pageincom.probe.pkg.?package=)RestServerGET /meta/view/case_grid/layers?package=allRESOURCE_NOT_FOUNDliveRestServerGET /meta/view?package=all[]case_grid,local_grid,other_gridRestServerGET /meta/book/probe_book/tree?package=allprobe_book, no entriesProbe Bookwithprobe_pageRestServerGET /meta/diagnostics?type=view&package=allscannedItems: 0scannedItems: 3, both packages instatsRestServer?state=draftread,PUT ?package=allGET /meta/view/case_grid/layers?package=allRESOURCE_NOT_FOUNDliveGET /meta/view?package=all[]GET /meta/book/probe_book/tree?package=allGET /meta/view/case_grid?package=all_provenance: 'org'with no_packageId_packageId: 'com.probe.pkg'GET /meta/view/case_grid?state=draft&package=allNO_DRAFTPUT /meta/view/fresh_grid?package=allpackage_id: 'all'(the seat's pointer re-measured)RestServer:package_idnullpackage_idnullPOST /meta/view/case_grid/publish?package=allROUTE_NOT_FOUND, zero protocol calls?package=to readH2, what "no package" means at the list. Measured on both hosts:
GET /meta/viewwithout?package=listscase_grid(incom.probe.pkg),other_grid(incom.other.pkg) andlocal_grid(env-local). No package already means every package, sometaItemPackageBindingis the right fold for the list. The list needs no reading ofallof its own.H3,
:5807and:6582(line numbers on31d24a52).:5807isGET /meta/diagnostics, the cross-type spec-validation sweep. It hands?package=togetMetaDiagnostics, which runsgetMetaItems({ type, packageId })for every type it sweeps. That is a list read keyed by package, so it is in the family.allreached it as a literal (scannedItems: 0, above), so it is folded and pinned. objectui at the.objectui-shapin (a58626c8) sends that door's?package=only after validating it against the project's packages (DirectoryPage.tsx,StudioHomePage.tsx), so no first-party caller sendsallthere.:6582is the item read's cache bypass,packageScoped. It is not a read keyed by package: it asks whether the raw parameter is present, to pick the uncached arm, and since fix(metadata-protocol): one head row per /meta write address — a second unpinned package-less draft save is accepted (#22128) #22185 that arm reads through the fold. Soallreaches it as a literal by design, and?package=allkeeps the uncached arm and its servedversion. Folding it would move?package=allinto the cached arm, which serves noversion. It is not changed here, and the fold's docblock now says why.H4, the export. Kept in
rest-server.tsand exported from the package entry:export { metaItemPackageBinding } from './rest-server.js'. Not moved besidemetaSaveRequestOptions, for two reasons.meta-save-request.ts's header scopes that module to the save's precondition and lifecycle, so the fold does not belong there.rest-server.jsforRestServer, so the export adds no module to the entry's closure.H5, Studio reach (triage's p1 condition): the save half does not hold. Read at objectui
a58626c8,packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx:allthemselves (readActivePackageBinding, lines 248 to 255, used bydoSaveat 1493 to 1498 anddoPublishat 1685 to 1688), so Studio's save omits?package=under the "show everything" scope.?package=(ownerPackageId, lines 433, 1009 to 1015 and 1502 to 1505), so they send?package=all. Through the catch-all they reach the dispatcher's layered and item branches, which this PR folds./metathrough that catch-all: NOT MEASURED from this session (objectstack-ai/cloud is not readable here).Pins (
packages/runtime/src/domains/meta-package-all-fold.test.ts)One file drives both hosts over real stores, as
meta-save-preconditions-parity.test.tsdoes. TheRestServerleg calls its registered handlers. The catch-all leg repeats the catch-all's four statements over a real FetchRequestinto the realHttpDispatcher.dispatch(). Each row compares a door's answer to?package=allwith the same door's answer without?package=, and keeps a scoping control beside it.?package=com.probe.pkgserves the same layers,?package=com.other.pkganswers 404), the list (control: each real package id lists only its own view), the book tree (control: the owning package resolves the declared book, another package falls to its implicit book), the item read, the?state=draftread of a draft that inherited the item's package, and the save (control:PUT ?package=com.probe.pkgbinds the row).RestServeralone: the diagnostics sweep (control: a real package id sweeps fewer items).Reverse verification and ablations
The fix was committed first. Every leg was restored from
HEADand proven by blob hash and an emptygit diff HEAD. The subject resolves by relative source import (meta.ts) and@objectstack/restthrough the runtime vitest alias to source, so nodist/leg applies.rest-server.ts,index.tsandmeta.tsin the treeRestServer's item read, draft read and save, which #22185 already foldedRestServer's layered read back toreq.query?.package or undefinedRestServer's layered readThe ablations went through
node scripts/ablation-replace.mjs. A: anchor x1 to x0, blob4f3ef5968792to905c48c396c2, restored to4f3ef5968792== HEAD. B: anchor x1 to x0, blobb21893a63ef8to88f1ec5f41a7, restored tob21893a63ef8== HEAD.Verification
All at this branch's head
db727d0d(git rev-parse --short HEAD), baseffb31fca, except where a line namesa13568b4(the fix commit;db727d0dadds only the draft-read row to the pin file). Heavy runs went throughscripts/pm/os-verify-lock.sh.pnpm --filter '@objectstack/runtime^...' build(the dependency closure), then@objectstack/restand@objectstack/runtimerebuilt at head: exit 0. The built entries load:require('packages/rest/dist/index.cjs')gives 35 exports,metaItemPackageBindingamong them (it answersundefinedforall, the empty value and an array, andcom.probe.pkgforcom.probe.pkg); the ESM entry exports it too;require('packages/runtime/dist/index.cjs')gives 312 exports.pnpm --filter @objectstack/rest typecheck: exit 0.pnpm --filter @objectstack/runtime typecheck: exit 0 after the rest rebuild (before it, the staledist/index.d.tsanswered TS2305 for the new export, as expected). The test layer printedcheck:test-typecheck: OK — 27 file(s) / 190 error(s) / 68 pinned signature(s) held, ledger unchanged. The new pin file is in that program (tsc -p tsconfig.test.json --listFileslists it) with no error of its own.a13568b4.pnpm --filter @objectstack/rest exec vitest run --project local: 262 files passed, 4938 passed, 326 skipped.pnpm --filter @objectstack/runtime exec vitest run --project local: 336 files passed, 4747 passed, 19 skipped. Therepoprojects are CI's, exceptmeta-list-projection-parity.test.ts, which readsrest-server.ts's handler source and was run by name.db727d0d.meta-package-all-fold.test.ts13 passed; withmeta-save-preconditions-parity.test.ts, 26 passed.meta-draft-head-package-inheritance.test.tsandmeta-draft-read-door-census.test.ts: 13 passed.meta-list-projection-parity.test.ts(--project repo): 682 passed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsatdb727d0dderives 64 commands (6 paths, 420 changed lines, under the 5000 threshold), the same 64 the order named. Each was run with its exit code recorded.--ranreconciliation:64 derived famil(ies) accounted for — 63 run, 1 NOT-MEASURED, 0 unrun. 63 exit 0. The fullpnpm lint(eslint . --no-inline-config, whole repository) exits 0 with no findings.pnpm check:dual-build-cjs-loads, reason: exit 3PREREQUISITE NOT MET(its self-test passed, 93 cases). It reads every package'sdist/, and 36 packages are unbuilt in this worktree. The declared narrowing in its place is the CJS and ESM entry loads of the two packages whose shipped code changed (above). CI runs the full gate.origin/mainis 5 commits past the base (9f0de32a). None touchespackages/rest,packages/runtimeorpackages/metadata-protocol, so no merge ofmainwas taken.Acceptance notes
?packageon the dispatcher. The dispatcher's/metadomain does not judge query multiplicity, as finding(runtime): through the @objectstack/hono catch-all, PUT /meta/:type/:name ignores If-Match, If-None-Match and ?mode=draft — a stale token writes (200, not 409) and a draft save lands ACTIVE #22141 recorded. The catch-all flattens its query to one string per name, so this changes nothing there. A Node host that hands an array used to forward the array to the store as the package id; it now reads it as naming no package, asRestServer's fold reads a non-string.GET /meta/_drafts?packageId=(RestServerand the dispatcher) reads a differently named parameter,packageId, and forwards it raw. It is not a?package=reader, and no caller sendingallthere was measured, so it is not in this family and is not changed here.POST /meta/:type/:name/publish(404ROUTE_NOT_FOUND, zero protocol calls), so it has no publish door to fold.Written by session
session_01RWZbGvPFcRKvUqASZtunCU.Generated by Claude Code