Repository navigation
Commit c63028e
Fixes #22640
Clause-②: no
The cross-object search now asks the spec's one exposure decision for
the `search` operation on each object. Before,
`ObjectStackProtocolImplementation.searchAll`
(`packages/metadata-protocol/src/protocol.ts`) used its own skip
predicate, which read `enable.apiEnabled === false` and
`enable.searchable === false` and never read `apiMethods`. So a
whitelist that withholds `list`, and with it `search`, kept nothing out
of the sweep.
## What changed
- **`searchAll`'s per-object skip** asks
`canServeApiOperation(obj.enable, 'search')` from
`@objectstack/spec/data`. This is the decision the REST data routes, the
dispatcher, MCP and the analytics door already judge. It runs first in
the loop, so a refused object is never queried, named or counted. An
explicit `objects=` naming it answers like one naming no object at all.
- **`searchable: false` keeps its own skip** beside the decision. The
decision does not cover it everywhere, as measured below.
- No other product code changed. `getQueryableFields` and the data
routes in `protocol.ts` are untouched.
## The premise, checked on `origin/main` `cb3bb933`
- **The predicate.** Found inside `searchAll`, as the card describes.
The line numbers had moved since the card was filed.
- **What the decision covers for `search`.** Measured against the built
`@objectstack/spec/data`:
| `enable` | `apiExposureDenialReason(enable, 'search')` |
|:--|:--|
| absent, or `{}` | `null` (served) |
| `{ searchable: false }` (no whitelist) | `null` (served) |
| `{ searchable: false, apiMethods: ['get', 'list'] }` |
`method-not-allowed` |
| `{ apiMethods: ['get'] }` | `method-not-allowed` |
| `{ apiMethods: [] }` | `method-not-allowed` |
| `{ apiEnabled: false }`, with or without a whitelist | `api-disabled`
|
| `{ apiMethods: ['list'] }` or `['get', 'list']` | `null` (served) |
The decision covers `apiEnabled: false` and the whitelist. It covers
`searchable: false` only under a whitelist: for an object with none
(`unrestricted`) it answers without reading the flag. That is why the
`searchable` line stays. The PM's mechanism assumption that the decision
covers both flags is therefore only half right: it holds for
`apiEnabled`, and for `searchable` only under a whitelist.
- **Dormant today, confirmed.** Every object in this repository whose
`apiMethods` withholds `list` also declares `searchable: false` or
`apiEnabled: false`. That is three `['get']` objects
(`sys_verification`, `sys_device_code`, `sys_two_factor`), each with
`searchable: false`, and nine deny-all `[]` objects, each with
`apiEnabled: false` and all but `sys_approval_token` also with
`searchable: false`. The control: the same scan reads all 70 inline
whitelist declarations in the repository, and a second scan for any
`apiMethods:` not written inline finds only the registry's rewrite and
the Zod schema, so no object hides a multi-line whitelist. No example
app declares `apiMethods`. So no shipped object's search results change.
Deployed and cloud-held definitions were NOT MEASURED.
## Pins
1.
**`packages/metadata-protocol/src/protocol.search-api-exposure.test.ts`**
(13 cases) holds the card's three pins:
- An object whose whitelist withholds `list`, without `searchable:
false`, is not swept. This is checked unscoped, with `objects=`, as
deny-all `[]`, and context-less.
- The existing skips still hold: `apiEnabled: false`, and `searchable:
false` both with and without a whitelist.
- An ordinary object, and one whose whitelist grants `list`, is swept as
before.
2. **The enumeration pin, homed outside `metadata-protocol`:
`packages/core/src/security/row-serving-door-exposure.pin.test.ts`.**
This is triage's escalation: every row-serving door that is not a data
route must ask the same decision.
- **Discriminator.** A door that sweeps or joins objects it chose itself
has to decide read admission per object, so it calls the security
service's `canReadObject` itself. A data route gets that from the engine
middleware instead. Every non-test `.ts` source under `packages/` that
calls it must be classified in the pin's table.
- **Current classification.** On this tree that is two doors and one
provider:
- search: `metadata-protocol` `searchAll`, operation `search`;
- analytics: `service-analytics`, operation `aggregate`, decision asked
in `api-exposure-door.ts`;
- the provider: `plugin-security`, which is not a door.
- **What it asserts.** Each door's decision site calls the decision, and
the door's behaviour pin exists. A new unclassified caller, or a stale
classified file, turns it red.
- **Why it lives in `core`.** `core` already declares the
`packages/**/*.ts` cross-package radius, so the pin needs no new
declaration in `scripts/cross-package-test-inputs.mjs` or `turbo.json`.
Like its siblings, it is listed in
`packages/core/vitest.repo-tests.json`, so it runs in core's `repo`
project, whose turbo task hashes that radius. Its scan surface and
prefilter copy `operation-private-keys.pin.test.ts` in the same
directory.
- **Blind spot, stated in its header.** It cannot see a door that reads
rows without asking `canReadObject` at all. Two such doors are named
there and not held: the share-link door, and knowledge retrieval.
## Ablations (all from committed state, via
`scripts/ablation-replace.mjs`, restore proven by blob == HEAD and an
empty `git diff HEAD`)
- **The fix.** The decision line was replaced with the old `apiEnabled`
check (anchor 1 to 0). The exposure pin went red on exactly the 4
negative cases and stayed green on the 9 others. The first failure read
`expected [ 'plain', 'get_only' ] to deeply equal [ 'plain' ]`.
- **Census A, the same mutation.** The census pin's "each door asks the
decision" case went red with "the cross-object search ... does not ask
the exposure decision in packages/metadata-protocol/src/protocol.ts".
The other two cases stayed green. This also shows that the docblock
mention of the call is not counted as a call.
- **Census B, a planted caller.** A `.canReadObject(` call was appended
in a non-door core source. The "every caller is classified" case went
red, naming that file, and the other two stayed green. The first attempt
was a no-op, refused by the tool: the replacement contained the anchor,
so the anchor count did not drop. It was redone with a different anchor
spelling.
## Tests and gates
All on HEAD `7e7d0eefe2`, which is this branch after merging
`origin/main` `d8830c2805`, unless a line names another commit.
- `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2 src/protocol.search-api-exposure.test.ts
src/protocol.search-skip-unreadable.test.ts`: 2 files, 25 tests passed.
- `pnpm --filter @objectstack/core exec vitest run --project repo
--maxWorkers=2`: 4 files, 51 tests passed, the census included.
- `pnpm --filter @objectstack/plugin-approvals exec vitest run
src/sys-approval-token-generic-door.integration.test.ts`: 6 tests
passed. This suite runs on a real engine and resolves
`metadata-protocol` through its built `dist/`, which carries the fix
(`canServeApiOperation(obj.enable, "search")` is present in
`dist/index.js`).
- Typecheck: `pnpm --filter @objectstack/metadata-protocol run
typecheck` exited 0. `pnpm --filter @objectstack/core run typecheck`
also exited 0, with the test layer OK.
- Full suites on `f3b4714a`, before the merge and the last test-only
commit; the `metadata-protocol` source is unchanged since:
- `metadata-protocol`: 222 files passed and 3 skipped; 28060 tests
passed.
- `core --project local`: 89 files, 2296 tests passed.
- **Gates.** `dispatch-gates --commands` on this HEAD derives 71
families, the 51 named in the dispatch plus 20 more. All 71 were run on
this HEAD and every one exited 0. `dispatch-gates --ran` reports: 71
derived, 71 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero.
- A first battery on `f3b4714a` had 7 non-zero exits:
- `check:engine-double-contract` flagged a new `findOne` double. The
double was unneeded and was dropped, which leaves the ledger untouched.
- `check:cross-package-test-inputs` asked for the census in core's
`repo` project, and for one quoted path mention to be reworded.
- Four were prerequisites, not findings: the teardown-shape self-test
needed its pinned fixture commit fetched (the clone is shallow), and
`check:dual-build-cjs-loads`, `check:i18n` and
`check:lean-entry-closure` needed built dists.
- `check:type-check-debt` was stopped by the runner's own 900-second cap
during its full re-measure.
- All 7 are green in the second battery, after a full `./packages/**`
build.
- **Lint, a declared narrowing.** `eslint --no-inline-config --format
json` over the 4 changed `.ts` files: 4 files, 0 errors, 0 warnings. The
checked population was read from the config: `--print-config` matches
every one, and the `.md` and `.json` files are outside the config's
`files`. The invariance holds because `eslint.config.mjs` enables no
type-aware linting (no `parserOptions.project` and no `projectService`
in code), so this diff cannot move a verdict on an untouched file. The
repository-wide `pnpm lint` is CI's.
## Also in this diff
-
`packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts`:
one docblock sentence said `searchAll` "never consults `apiMethods` at
all". This change made that false, so the sentence was rewritten. It is
a comment-only edit in a test file, outside the claim's file surface.
- `packages/core/vitest.repo-tests.json`: one entry, the census, which
`check:cross-package-test-inputs` requires. It is a test-infrastructure
list, outside the claim's file surface.
- `.changeset/22640-search-api-exposure.md`: `minor`, BREAKING (an
accept-set narrowing), FROM to TO, and ADR-0087 `not-required
(no-migration-prescription)`.
## Acceptance notes
- **The Clause-② line.** The `Clause-②: no` line is copied verbatim from
the claim. The claim's own text calls this change a narrowing, but the
line carries no `(narrowing)` arm. The changeset declares breaking
through its `**BREAKING**` banner and its `!` summary, and
`check:adr-0087-registration` reads it as `[BREAKING+bang]`.
- **Doors the enumeration cannot see** (observation only, not filed):
- The share-link door (`plugin-sharing` `share-link-routes.ts`) serves a
record as the system behind its token. Its gate is its own opt-in
public-sharing policy, not the exposure decision.
- Knowledge retrieval (`service-knowledge` `KnowledgeService.search` /
`reindexSource`) serves object-source content without asking the
decision. It has no HTTP door in this repository.
- Neither was measured through a public door.
- **A comment now incomplete.** `service-analytics`
`api-exposure-door.ts` says the cross-object search "reads the same off
switch". It now asks the whole decision. The sentence is still true but
incomplete, and it was not edited because it is product code outside
this claim.
- **No checklist item yet.** The search area of the platform test
checklist (`docs/qa/platform-checklist/areas/search.json`) has no item
for this exposure skip.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9f15939 commit c63028e
6 files changed
Lines changed: 423 additions & 8 deletions
File tree
- .changeset
- packages
- core
- src/security
- metadata-protocol/src
- plugins/plugin-approvals/src
| 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 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
| 5 | + | |
5 | 6 | | |
0 commit comments