Repository navigation
Commit cc40033
Fixes #20237
Clause-②: yes
The runtime dispatcher's `/meta/:type` LIST now prunes what
`RestServer`'s `GET /meta/:type` prunes, for the same caller. It is one
function with two callers. `RestServer`'s per-caller list filters moved
unchanged into `createMetaListReadGate`, beside the item gate in
`packages/rest/src/meta-item-read-gate.ts`. `RestServer`'s list route
and the dispatcher's one-segment list branch both call it. There is no
second audience resolver in `packages/runtime` (ruling `5793362670` item
1). This is the route #20193's item half took in PR #20236.
#20139 remains open.
## The mechanism assumptions, measured first
### 1. `RestServer`'s LIST filters, classified
In handler order (`GET ${metaPath}/:type`, after
`getMetaItems(listRequest)`):
| filter | class | dispatcher before | now |
|:--|:--|:--|:--|
| `api` served-set face (`selectServedEndpoints`) | per-deployment
(projection of what the endpoint matcher serves) | not run | not run
(out of scope; see Acceptance notes) |
| `app` nav filter (`filterAppForUser`: unpublished,
`requiredPermissions`, docs-audience entry arm, plus the ADR-0057 D10
and #7912 servability arms) | **per-caller** (with per-deployment arms
riding along) | not run | **the shared list gate** |
| `app` `?id=` filter | projection (query narrowing, not a gate) |
ignored | ignored (out of scope; see Acceptance notes) |
| `dashboard` widget gate (ADR-0057 D10) | per-deployment | not run |
**the shared list gate** (the card's direction names it) |
| `view` `?object=` filter | projection | ignored | ignored (out of
scope) |
| `book` audience prune (ADR-0046 §6.7) | **per-caller** | not run |
**the shared list gate** |
| `doc` effective-audience prune (ADR-0046 §6.7) | **per-caller** | not
run | **the shared list gate** |
| `doc` locale collapse, translation | projection | not run | not run
(out of scope) |
| `doc` content slim (`?include=content`) | projection | run
(`slimDocList`), singular spelling only | unchanged |
| ADR-0106 object mask | **per-caller** | run on the protocol exit only
| run on **every** list exit |
The object mask stays on each transport's own exit, as the item gate
documents: both already call the one mask in
`@objectstack/metadata-core`. The projections withhold nothing from any
caller, so they are not this card.
### 2. The dispatcher's list branch
Confirmed at source on `2dccb7d4`: `handleMetadataRequest`,
`parts.length === 1`, had no per-caller filter anywhere on its path. It
has **three** list exits, not one:
- the protocol's `getMetaItems`, masked for objects and slimmed for
docs;
- a fallback to the runtime metadata service's `list`, reached when the
protocol does not answer the type. It was slimmed only: no gate, and
**no object mask**;
- a fallback to the ObjectQL registry's `listItems`. It had no gate, no
mask and no slim; for `object` it answers `getAllObjects()`, unmasked.
The mask ran inside the protocol exit's swallowing `try`. A mask fault
other than the D6 tier-3 one therefore fell through to the unmasked
metadata-service exit.
### 3. The rows, re-measured before the fix
Driven through `dispatch()` (the `createHonoApp` catch-all's delegate)
at base `2dccb7d4`, against the same fixtures and caller as
`RestServer`'s list route. The caller is a member without `crm_admin`:
| request | dispatcher before | `RestServer`, same caller | dispatcher
now |
|:--|:--|:--|:--|
| `GET /meta/doc?include=content` | `crm_intro`, **`crm_admin_runbook`
with its body** | `crm_intro` | `crm_intro` |
| `GET /meta/book` | **`admin_guide`** (with its description),
`help_center` | `help_center` | `help_center` |
| `GET /meta/app` | **`crm` with 3 entries, `payroll`, `launchpad`** |
`crm`, pruned to `nav_leads` | `crm`, pruned to `nav_leads` |
| `GET /meta/dashboard` (any caller) | `ops`, **both widgets** | `ops`,
`w_open_cases` | `ops`, `w_open_cases` |
| `GET /meta/object` | `invoice`, masked to `amount` | the same | the
same |
| `GET /meta/docs?include=content`, `/meta/books`, `/meta/apps` | as
their singular rows | as their singular rows | as their singular rows |
- **Holder:** every row equals `RestServer` before and after.
- **Anonymous:** the dispatcher answers `401 UNAUTHENTICATED` on every
list, before any read, both before and after. For `doc` and `book` lists
`RestServer` instead serves the `public` entries (none in this fixture).
The dispatcher is stricter there; see Acceptance notes.
### 4. The #20193 lessons
- **Liveness:** no gate symbol moved out of its file. The list filters
were inline blocks, not named symbols, so no pointer broke. `pnpm
--filter @objectstack/spec run check:liveness`: exit 0, 760 of 760
`path#symbol` pointers resolve, and no ledger file was edited.
- **Pin location:** the composed-host pin sits beside #20193's, in
`packages/qa/http-conformance/src/`.
- **Exports:** one new value export, `createMetaListReadGate`. It takes
the item gate's existing `MetaItemReadGateSources`, so no new type is
exported. That keeps `Clause-②: yes` and makes `@objectstack/rest`
`minor`; `@objectstack/runtime` is `patch`.
### 5. The exec-ctx census
68 → 66 sites and 92 → 90 mentions. The list route's app and dashboard
blocks each resolved the context inline (two same-line CAUGHT sites).
Both moved into the gate, which reads the caller through the port the
item gate already uses, so nothing replaced them. The census now reads:
CAUGHT 23 → 21, same-line 15 → 13, bare 45 unchanged, and the inline
floor 14 → 12, keeping its one site of slack. The arithmetic is in the
same hunks.
## How
- **`createMetaListReadGate(sources, metaType)`**
(`meta-item-read-gate.ts`) answers a judge from a list's items to the
items this caller may be served. It is `RestServer`'s former inline list
filters, in the same order and with the same inputs, and it is built
from the functions already in the module (`filterAppForUser`,
`filterDashboardForUser`, `resolveRegisteredServices`,
`resolveNavServability`, `resolveNavDocAudience`, `resolveDocsAudience`,
`fetchAudienceBooks`, `docCorpusOf`). It answers the same array when
nothing applies, and a gate input that cannot be read rejects.
- **`RestServer`'s list route** calls it once, at the position where the
app filter always ran: before `?id=` narrows, and before the doc locale
collapse. It rewraps the result only when the gate pruned something. Its
list answers are unchanged: the package's 3,662 local tests pass, the
list-route tests among them.
- **The dispatcher's list branch** sends every list exit through
`gateMetaListAnswer`, which runs the shared gate and then the ADR-0106
mask. The lookups keep their own `try`s, which classify a type the store
does not know. The gate and the mask run outside them, so a fault is
answered as itself (its own status, or `500` for a shapeless one) and
never falls through to the next store. Each exit keeps its own
projection (the doc slim) exactly as before.
- **`RestServer` delegates.** Seven private delegates lost their only
caller with the move (`filterAppForUser`, `filterDashboardForUser`,
`resolveRegisteredServices`, `resolveNavServability`,
`resolveNavDocAudience`, `fetchAudienceBooks`, `docCorpusOf`). They are
deleted: `noUnusedLocals` reports each one as TS6133, and a delegate
with no caller is a second place to read a rule that nothing runs.
- 83 unit-test call sites in `rest.test.ts`,
`meta-app-nav-servability-gate.test.ts` and
`meta-app-nav-doc-audience.test.ts` reached those delegates through
`any`. They now call the one implementation directly. Assertions are
unchanged, and the `it()` counts are unchanged at 215, 16 and 18.
- **`filterAppForUser` stays resolvable at `rest-server.ts`** as a named
re-export, because row 18 of
`docs/adr/0056-permission-model-landing-verification.md` cites
`packages/rest/src/rest-server.ts#filterAppForUser`. Measured: deleting
the declaration turns `check:adr-symbol-anchors` `unresolved-symbol` on
that row, and that ADR is a governed surface this PR does not touch. The
re-export is not on the package barrel; see Acceptance notes.
## Tests
- **`packages/runtime/src/domains/meta-list-read-gate-parity.test.ts`**,
40 cases. It drives the same fixtures through `dispatch()` and through
`RestServer`'s `GET /meta/:type`:
- the reference answers `RestServer` gives the non-holder (11);
- dispatcher equals `RestServer`, for holder and non-holder, on every
row (22): the same status and code, the same items, and the same nav
entries, widgets, fields and served bodies per item;
- controls (4): the holder served in full; anonymous `401` before any
read; `RestServer`'s anonymous doc and book lists serving no gated
content; and a books-read fault answered as that fault;
- the fallback exits (3): the metadata-service exit prunes doc, book and
app lists and masks objects; the registry exit masks objects; the holder
is served in full.
-
**`packages/qa/http-conformance/src/hono-meta-list-read-gate.conformance.test.ts`**,
6 cases, through a real `LiteKernel` behind the real `createHonoApp`
(`app.request(...)`). For the non-holder, the card's three rows: the doc
list with bodies lists `crm_intro` only, and the gated doc's name and
body are nowhere on the wire; the book list lists `help_center` only;
the app list lists `crm` pruned to `nav_leads`, with `payroll` absent.
The holder control lists everything in full. Only identity is stubbed,
and the runtime resolves through `dist/`.
**Ablation** (the fix committed first at `34f2588dd`;
`scripts/ablation-replace.mjs` in WRAP mode, plus a shell trap restoring
from `HEAD`):
- **Mutate leg.** `gateMetaListAnswer`'s gate call `const judged = await
createMetaListReadGate(...)(list);` was replaced by a pass-through
carrying the code marker `ablated20237`. The anchor count went 1 → 0 and
the blob `e5cbe6ba` → `74221a11`. After a runtime rebuild,
`ablation-dist-preflight` found the marker in `dist/index.js` and
`dist/index.cjs`.
- Composed-host pin: **3 failed, 3 passed**. These are exactly the three
non-holder rows; the holder rows stay green.
- Parity pin: **11 failed, 29 passed**. Red: every non-holder doc, book
and app row (both spellings), the dashboard rows for both callers (the
gate is per-deployment), the books-fault control and the
metadata-service exit. The object rows stay green, because the mask is
not the gate.
- **Restore leg.** The blob equals `HEAD` (`e5cbe6ba`) and `git diff
HEAD` is empty. After a rebuild, `--absent` found no marker in any of
the 6 built files, and the whole-tree `git status --porcelain` was
empty. Result: **6 of 6** and **40 of 40**.
- The direction was the predicted one: red.
**Verification at head `a48f373a2`** (after merging `origin/main` at
`6a6a17b62`, merge commit `8c972a5c4`):
- `pnpm --filter @objectstack/rest exec vitest run --project local`: 202
files, 3662 passed, 1 skipped. `--project repo`: 1 file, 8 passed.
- `pnpm --filter @objectstack/runtime exec vitest run --project local`:
282 files, 4057 passed, 1 skipped. `--project repo`: 2 files, 69 passed.
Run at `8c972a5c4`; the only later commit, `a48f373a2`, touches two rest
test files that no runtime suite reads.
- `pnpm --filter @objectstack/http-conformance test`: 8 files, 102
passed. Run at `8c972a5c4`, for the same reason.
- `typecheck` for rest (at `a48f373a2`), runtime and http-conformance
(at `8c972a5c4`): exit 0 each. The test layers are OK: rest at 0 errors,
and the runtime and http-conformance debt ledgers unchanged.
- `pnpm --filter @objectstack/spec run check:liveness`: exit 0 (760 of
760).
- `pnpm lint` (full, `eslint . --no-inline-config`): exit 0, 114 s on a
shared box.
- `node scripts/check-issue-citations.mjs --base origin/main`: exit 0,
board probed, 17 citations judged, all resolve.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 61 commands for these 11 paths. All 61 exited 0, and
`--ran` reconciled them: 61 derived, 61 run, 0 NOT-MEASURED (a derived
zero from recorded exit codes).
- Near-diff gates, run beside the derived set, all exit 0:
`check:liveness`, `check:adr-symbol-anchors`, `check:adr-anchors`,
`check:scripts-symbol-anchors`, `check-changeset-fixed`,
`check-published-list-mirrors`, `check:error-code-casing`,
`check:route-ledger-census`, `check:published-readme-exports`,
`check:meta-type-normalized`, `check:init-service-contract`,
`check:startup-registry-verdict`, `check:wildcard-fallthrough`,
`check:optional-error-sink`, `check:filter-alias-parity`.
## Changeset
`.changeset/20237-dispatcher-meta-list-gate.md`:
- **`@objectstack/rest`: `minor`.** One new public export,
`createMetaListReadGate`, consumed by the runtime dispatcher's `/meta`
list. `RestServer`'s answers are unchanged.
- **`@objectstack/runtime`: `patch`.** The dispatcher's list is pulled
back to the per-caller contract the platform already declares (ADR-0046
§6.7, ADR-0045 §3, `apps.mdx`'s `requiredPermissions` row: 「absent from
the `/meta` body」). A permission pull-back is not a Clause-② widening.
Who could read what before: on a host that mounts only the catch-all,
any authenticated member could list a `{ permissionSet }`-gated doc with
its body, a set-gated book's name and description, an app whose
`requiredPermissions` they lack, and an ungated app's gated nav entries.
On a host whose protocol does not answer a type, the fallback exits also
served object schemas unmasked.
## Acceptance notes
- **Out of scope, same family (the dispatcher's `/meta` list is a second
implementation of `RestServer`'s), measured through `dispatch()` after
the fix.** None of these serves anything a caller may not read, because
the gate prunes first. They are reported for the seat, not fixed here:
- `GET /meta/app?id=crm` ignores `?id=`: the holder is listed all three
apps, where `RestServer` answers `[crm]`;
- `GET /meta/view?object=nope` ignores `?object=` and lists every view,
where `RestServer` answers `[]`;
- `GET /meta/docs`, the plural spelling without `?include=content`,
serves the doc bodies, because `slimDocList` compares the raw segment
with `'doc'`. `RestServer` slims both spellings;
- the doc locale collapse, translation and the `api` served-set face are
not run.
- **Anonymous `public` docs and books are unreachable on a
catch-all-only host.** The dispatcher answers `401` to every anonymous
`/meta` read. With a `public` book claiming `crm_intro`, `RestServer`
lists `public_guide` and `crm_intro` to an anonymous caller, and the
dispatcher answers `401 UNAUTHENTICATED`. That is fail-closed, so it is
not this card; it is reported for the seat.
- **ADR-0056 row 18** still cites `rest-server.ts#filterAppForUser`. The
rule lives in `meta-item-read-gate.ts#filterAppForUser`, and
`rest-server.ts` re-exports it by name so the pointer keeps landing one
hop from it. Re-anchoring that row is a governed-surface edit for the
maintainer; the re-export goes when the row moves.
- **Fault paths, fail closed.** A gate or mask fault on the dispatcher's
list is now answered as that fault, not by falling through to the next
store. A host whose protocol has no `getMetaItems` answers a doc list
with the fault rather than the unfiltered list, the same requirement the
item gate and `RestServer` already have.
- **Deviations from the suggested route**, each measured:
- the object mask stays per transport, not inside the seam (the item
gate's documented split);
- seven `RestServer` delegates were deleted and their unit tests
retargeted (the `noUnusedLocals` consequence of the move);
- no liveness or ADR-anchor file moved.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7338efe commit cc40033
11 files changed
Lines changed: 1086 additions & 352 deletions
File tree
- .changeset
- packages
- qa/http-conformance/src
- rest/src
- runtime/src/domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
310 | 310 | | |
311 | 311 | | |
312 | 312 | | |
313 | | - | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
314 | 326 | | |
315 | 327 | | |
316 | 328 | | |
| |||
436 | 448 | | |
437 | 449 | | |
438 | 450 | | |
439 | | - | |
440 | | - | |
| 451 | + | |
| 452 | + | |
441 | 453 | | |
442 | 454 | | |
443 | | - | |
444 | | - | |
445 | | - | |
446 | | - | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
447 | 460 | | |
448 | 461 | | |
449 | 462 | | |
450 | 463 | | |
451 | 464 | | |
452 | | - | |
453 | | - | |
| 465 | + | |
| 466 | + | |
454 | 467 | | |
455 | 468 | | |
456 | 469 | | |
457 | 470 | | |
458 | | - | |
| 471 | + | |
459 | 472 | | |
460 | 473 | | |
461 | 474 | | |
| |||
727 | 740 | | |
728 | 741 | | |
729 | 742 | | |
730 | | - | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
731 | 749 | | |
732 | 750 | | |
733 | 751 | | |
| |||
0 commit comments