Repository navigation
fix(rest): the API-exposure gate reads the one object it checks, by name, instead of every object's metadata on each /data request - #24093
Conversation
enforceApiAccess asked for the whole object list and searched it for one name. The list read decorates every object with a full schema diagnostic, so every /data request paid a diagnostic pass over the whole registry to read one object's enable flags. The gate now asks the protocol's by-name read (getMetaItem) for that object. A thrown read still fails open and logs; no object behind the name is still no policy; a protocol with no by-name read is asked the list as before. Nothing is cached. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
…er /data request Over the real protocol and a real SchemaRegistry, unscoped and environment-scoped: list, get, create, update and delete each make zero getMetaItems calls; the gate's 404/405 answers are unchanged for a code-registered, an extended and a stored object; an unknown object is answered by the data path's own 404 with nothing logged; a throwing by-name read fails open and logs; a protocol with no by-name read is asked the list. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
…limit, and its findOne double is ledgered check:objectql-double-limit graded the new file's find double limit-blind; it now applies a numeric limit after the filter. check:engine-double-contract asked for the new file's findOne double in the pinned ledger (check-engine-double-contract --write added that one entry and nothing else). Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
…st does; pin the disabled-package class; changeset Three exposure suites' protocol doubles answered getMetaItem with a body their own getMetaItems contradicted. The real protocol's two reads agree, so each double's by-name read now answers the object its list holds; no assertion changes. The gate pin gains the one class whose answer moves: on a disabled package's object a denied operation answers the exposure verdict (405), and an allowed one still reaches the data plane's 404 OBJECT_PACKAGE_DISABLED. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 18 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 f2739775189f3f0f7caf49b74775fce4102c28c5 && git checkout f2739775189f3f0f7caf49b74775fce4102c28c5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6e180163def1d31004905e4d6aac2d413571fb8e 70eca6c17eb6dbe65bd6b4ecf4d78a3e89032b39 && git checkout -B drift-repro 6e180163def1d31004905e4d6aac2d413571fb8e && git merge --no-ff 70eca6c17eb6dbe65bd6b4ecf4d78a3e89032b39
node scripts/docs-audit/affected-docs.mjs --json 6e180163def1d31004905e4d6aac2d413571fb8e |
|
Generated by Claude Code |
… read from the registry, beside the list The rig's real protocol has no metadata store, so it already answered getMetaItems from the registry; the REST exposure gate now reads its one object by name, and that read threw here, so the gate failed open and twelve exposure rows read as allowed objects. getMetaItem for type object now answers the same registry definition the list holds; other types keep the real read. No assertion changes. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
席位核对ACCEPT · #24031 ·
Filed from it: #24097 ( After it lands: #23977 ( Landing: Generated by Claude Code |
Fixes #24031
Clause-②: no
Basis for the Clause-② line: the gate answers every request as before, with the same 405s, the same fail-open and the same answer for an unknown object; only the read behind the verdict changes, and no schema, published text or accept set moves. For a denied operation on an object whose package is disabled, the refusal's
statusandcodechange: FROM404 OBJECT_PACKAGE_DISABLEDTO the exposure verdict (405 OBJECT_API_METHOD_NOT_ALLOWED, or404 OBJECT_API_DISABLEDforapiEnabled: false). No accepted request becomes refused and no refused request becomes accepted (seat ruling6226615729, Q2 → B).What changed
packages/rest/src/rest-server.ts,enforceApiAccessonly. The gate asks the protocol's by-name read,getMetaItem({ type: 'object', name }), for the one object in the path. It sends the same env-scoped request shape the list read sends, and it no longer callsloadObjectItems. A protocol with no by-name read is asked the list, as before.packages/rest/src/meta-item-read-gate.ts:loadObjectItem, besideloadObjectItems.api-exposure gatewarning (元数据不可解析时的 fail-open 残余风险评估(api-exposure / rest-server) #3545).MetaReadGateListSourcegains one optional by-name method,getObjectItem.loadObjectItemsis unchanged for the batch route and the nav-servability gate. One docblock sentence no longer names this gate as a caller.packages/rest/src/rest-exposure-gate-one-object-read.test.ts(37 tests). It runs the real protocol over a realSchemaRegistry, both unscoped and environment-scoped. It pins:/datalist, get, create, update and delete each make zerogetMetaItemscalls, and therefore run no list-wide diagnostic pass;enable, becauseObjectExtensionSchemais strict);404 OBJECT_PACKAGE_DISABLED).rest.test.ts,rest-bulk-path-object.test.ts,rest-export-permission-gate.test.ts. Each double'sgetMetaItemnow answers the object its owngetMetaItemsholds. On the real protocol the two reads agree for every one of 104 showcase objects. The diff touches double setup only: no changed line containsexpect(,it(,describe(,toBeortoEqual.packages/runtime/src/domains/data-write-request-gate.test.ts, its rig only (Q1 A extended,6227635439). The rig's real protocol has no metadata store, so its by-name read threw and the gate failed open on 12 exposure rows. ItsgetMetaItemnow answers the same registry definition itsgetMetaItemsoverride holds; every other type keeps the real read. No assertion changed. The earlier rounds' "existing exposure tests" grep named onlypackages/rest, and this rig sat outside it. A census of every non-rest test that constructsRestServerfound it the only member of the class.scripts/engine-double-contract.pinned.json. One entry, generated bycheck-engine-double-contract --writefor the new pin file. The seat declared it on [PM seat] domain:spec — ⏳ vacant #6017 (comment6226622031)..changeset/24031-exposure-gate-one-object-read.md(@objectstack/restpatch): the latency gain, the one status/code change (FROM/TO), andClause-②: no.Measurements (showcase,
bootShowcase, shared box)/datarequest, base gate → head: 1 → 0. Measured on list, get, create, update, delete, a refused request, a stored object and an unknown object.GET /api/v1/data/showcase_account?limit=5: 2421 ms → 647 ms in total, median 71.7 → 19.5 ms (-73 %).getMetaItems,readFlattenedMetaItemsandregistry.listItems('object').e10990366; the merge brought no hunk insidegetMetaItems,readFlattenedMetaItemsorgetMetaItem):enableis deep-equal for every one;getMetaItemdoes not throw, and its envelope has noitem;com.objectstack.plugin-authdisabled), base treee246f0611→ heade238774dd:POST /api/v1/data/sys_api_key:404 OBJECT_PACKAGE_DISABLED→405 OBJECT_API_METHOD_NOT_ALLOWED;GET /api/v1/data/sys_api_key:404 OBJECT_PACKAGE_DISABLEDat both.Re-measuring the Rerun Safety file of #23977 is the seat's step once this lands; #23977 stays open here.
Tests (at
70eca6c17; the@objectstack/restreadings are ate238774dd, whose rest sources are the same)70eca6c17:@objectstack/runtimelocal project: Test Files 405 passed (405); Tests 6175 passed | 19 skipped;@objectstack/http-conformance: 14 files, 150 passed;@objectstack/runtimetypecheck exit 0, with the debt ledger unchanged.@objectstack/restlocal suite: Test Files 290 passed (290); Tests 5990 passed | 326 skipped;VERDICT command-exit 0.pnpm --filter @objectstack/rest typecheck: exit 0, includingcheck:test-typecheckwith 0 debt.e238774dd:enforceApiAccessput back onloadObjectItemsthroughscripts/ablation-replace.mjs(anchor 1 → 0, blobffc716c6a290→b429a90b19d3). The dist preflight found the marker, exit 0.404 OBJECT_PACKAGE_DISABLED). Green: every answer row, and all 228 tests of the 3 ruled files, so their doubles keep their verdicts under both reads.git diff HEADis empty; the preflight with--absentexits 0; 265 passed.3f612b8d5): the 9 tests in those 3 files went red, and the rest of the suite was green.test:repowas not run locally, because the diff touches none of its files. CI'sTest Coreruns it.Gates (at
70eca6c17)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 75 families, the same set as ate238774dd. All 75 were re-run at70eca6c17and every one exited 0;check:dual-build-cjs-loadswas included and ran after a full build. The--ranreconciliation exited 0.pnpm exec eslint --no-inline-configover the 6 changed TypeScript files exited 0. The repo-wide lint is theLint & Repo Gatescheck-run's.Acceptance notes
Three other paths still read the whole object list on every request (not measured):
POST /api/v1/data/batch, which reads it once for all its operations and is held off by the ruling;readFormObjectDefinitions, and the form schema read, which lists every object to find one.Registry hydration. On an UNSCOPED kernel, the list read hydrates stored rows into the registry, and the gate no longer triggers that on every request. The designed paths still do it:
/dataat head;metadata.mutated.Standalone serve boots are env-scoped (
env_local), where that hydration never ran.Two docblocks still name this gate as a list caller: the private
loadObjectItemsdocblock inrest-server.ts, which is outside this surface, and themeta-overlay-cache.tsheader inpackages/metadata-protocol, which is held off.The disagreement the Q2 ruling leaves to another lane. The by-name read serves a disabled package's object while the list omits it; the READERS table of Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphaned
sys_metadatarows #7557 classifies neither. Measured withcom.objectstack.plugin-authdisabled, identical at basee246f0611and heade238774dd:GET /api/v1/meta/object/sys_api_keyanswers 200 and serves the object;GET /api/v1/meta/objectsandGET /api/v1/meta/objectanswer 200 with 73 items, andsys_api_keyis absent (103 items while the package is enabled).The seat files this as its own card.
Generated by Claude Code