Repository navigation
fix(rest)!: the batch door judges each op's API exposure on the object the data door serves (#27899) - #27963
Conversation
…oor's Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwgfUuNAvcUg3JzBiR
…store Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwgfUuNAvcUg3JzBiR
…e data door serves The cross-object batch door's per-op exposure gate now reads each op's object through the same by-name read as the /data/:object gate, moved into one private RestServer method both gates call, instead of keying the object list by declared name. Each distinct object is read once per request. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwgfUuNAvcUg3JzBiR
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 5 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 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 2f98458f404ccb30b57872a9fc28555983230977 && git checkout 2f98458f404ccb30b57872a9fc28555983230977
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ed0e71db94a73acc02512ff2ee0655106d02e585 2019e190f5c40af31fbabccf6fb2d5a3fa612e28 && git checkout -B drift-repro ed0e71db94a73acc02512ff2ee0655106d02e585 && git merge --no-ff 2019e190f5c40af31fbabccf6fb2d5a3fa612e28
node scripts/docs-audit/affected-docs.mjs --json ed0e71db94a73acc02512ff2ee0655106d02e585
|
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head 2019e19 · 2026-10-30T05:16Z |
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL Contract reviewServed-tier: Read at 2026-10-30T05:29Z against the net diff ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentpackages/rest/src/batch-routes.ts — holds against the code it cites and the scenario it is accepted against. The gate block reads each op's object through the host's packages/rest/src/rest-server.ts — holds. packages/rest/src/meta-item-read-gate.ts — docblock-only, and the sentence is true on the head: the list is read by the nav-servability gate and by the by-name read's fallback; every data door's exposure gate reads by name. No must-fix. packages/rest/src/rest.test.ts — the two re-pointed pins still measure a thrown read and a silent empty registry on the replacement member (① item 6). No must-fix. packages/rest/src/objectql-slot-consumer-census.test.ts — one docblock word; the renamed method is the one the path reaches. No must-fix. packages/rest/src/batch-op-exposure-object-read.test.ts — the pins are the right shape: a real engine, the real protocol, the real routes, the metadata store registered so the gate is live rather than failing open; each refusal asserts status and whole body .changeset/27899-batch-op-exposure-object-read.md — frontmatter, both declaration lines, one ADR-0087 marker, BREAKING banner, Before / Now / Who sees it / Remedy, each sentence true against the diff; rule-level throughout, no command or fixture reproduced. No must-fix. No must-fix. The narrowing takes away only an answer that was wrong (a write the data door would have refused under the same name), and makes the spec's own refusal (ADR-0049 Implemented-by: VERDICT: PASS |
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL 维护者速读
席位意见:ACCEPT。 席位核对Head-sha:
|
Requested by Elon · project thread
Fixes #27899
domain:server#4 · os-elon-musk · session_016WV4cwgfUuNAvcUg3JzBiR
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no hand-written docs page or published skill states what the cross-object batch door answers about an object's API exposure, or how a short name resolves there (
git grepofcontent/docsoutsidereferences//releases/and ofskills/forapiEnabled,apiMethodsorOBJECT_API_DISABLEDbeside the batch door: 0 hits;http-protocol.mdxnames the door only as thetransactionalBatchcapability).维护者速读
POST /batch的逐操作 API 暴露闸,改用/data/OBJ暴露闸的同一个按名读法(从enforceApiAccess抽成RestServer的一个私有方法,两处共用)。每个操作判的,就是数据门按这个名字会服务的那个对象;拒绝时回的状态码与正文和数据门一字不差。POST /data/:object/import(synchronous import) on an unregistered object answers 200 with a per-rowOBJECT_NOT_FOUND, wherePOST /data/:objectanswers 404 #27728 / rest: pin the async import job door's disabled-package refusal, and decide whether it should resolve a legacyns__shortname as the data door does #27729 一族的最后一扇门。原先它按声明名在对象清单里找对象,清单不做旧式ns__short短名解析,于是这类对象用短名时被当成未知对象放行,答案与/data门不一致(实测,见 Tests)。__的旧对象、且该对象限制了 API 暴露的批量操作,已发运的对象都不带__。读法由每请求一次清单读变为每个不同对象一次按名读。回滚即还原batch-routes.ts的闸块与rest-server.ts的私有读法两处。packages/rest/src/batch-routes.ts(逐操作闸块);packages/rest/src/rest-server.ts的loadObjectItem(从enforceApiAccess抽出,并接到批量路由的 host 上);新测试packages/rest/src/batch-op-exposure-object-read.test.ts。pnpm --filter @objectstack/rest exec vitest run src/batch-op-exposure-object-read.test.ts,预期 9/9 通过。What changes
/data/:objectgate (enforceApiAccess) made inline —loadObjectIteminmeta-item-read-gate.ts, over the protocol's by-name execution read, or its list read for a protocol with no by-name read — is now one private method,RestServer#loadObjectItem(p, environmentId, name).enforceApiAccesscalls it for the route's:object, and the cross-object batch route calls it for each op's object throughBatchRouteHost.ns__shortobject from its short name as the data door'sregistry.getObjectdoes; the list does not. The gate now judges the object the data door serves for the op's name, so such an op is refused with the data door's answer, status and body (apiAccessDenialFromEnable, unchanged), before the transaction opens. Each distinct object is read once per request; each distinct (object, action) is judged once, in op order; the first refusal answers for the whole batch, as before.RestServer#loadObjectItemshad this gate as its only caller, so it is gone, andBatchRouteHostcarriesloadObjectItemin its place. The two 元数据不可解析时的 fail-open 残余风险评估(api-exposure / rest-server) #3545 observability pins inrest.test.tsthat called it by name now call the method that replaced it, with the same assertions (fail open, a thrown read logged, an empty registry silent). Docblocks that named the batch as a reader of the object list now say otherwise (meta-item-read-gate.ts,metaListSource, one line ofobjectql-slot-consumer-census.test.ts).Why
Triage 6514615922: the
/batchper-op gate reads each op's object through the same read as the/datagate and refuses the same way. PR #27897 gave the/datagate this read; the batch door was the last data door that answered differently about whether an object is exposed.Tests
New
packages/rest/src/batch-op-exposure-object-read.test.ts: a realObjectQLengine, the real protocol and the realRestServerroutes on a sqlite:memory:driver, withobjectQLProviderwired so the batch door is reachable, and the metadata store registered (without it both object reads answerSERVICE_UNAVAILABLEand every exposure gate fails open by design, #3545). Fixtures: legacy-named objects of one package (one unexposed, one exposed for reads only, one exposed), addressed by their short names, and one plain-named unexposed object./data/:objectanswer for the same name, the transaction never opens, no row. A write the object does not expose in bulk: status and body equalPOST /data/:object/createMany's 405. One refused op refuses the whole batch: an exposed op ahead of it is not written.Before and after, through the real
POST /batchdoor (based4064cc1with only the test committed, then head2019e190):OBJECT_API_DISABLED, equal to/dataRECORD_NOT_FOUND(passed the gate, reached the engine)OBJECT_API_DISABLED, equal to/dataOBJECT_API_METHOD_NOT_ALLOWED, equal tocreateManyRed first, ablation, package suite
ec6f33ef..db7513a4(test only, on the base code):Tests 5 failed | 4 passed (9), the five refusal pins red and the four controls green.2019e190:Tests 9 passed (9); withrest-batch-endpoint,rest,import-door-object-existence,objectql-slot-consumer-census,rest-batch-blank-id:Test Files 6 passed (6) · Tests 294 passed (294).scripts/ablation-replace.mjs, anchor hit once, blob0ce906d7to23cf0acd): the gate counted the read item only under the op's declared name, the old list semantics.Tests 5 failed | 4 passed (9), the same five red. Restored: blob equals HEAD0ce906d7,git diff HEADempty.pnpm --filter @objectstack/rest test:Test Files 351 passed (351) · Tests 6775 passed | 332 skipped (7107), lock VERDICT command-exit 0.pnpm --filter @objectstack/rest typecheck: lock VERDICT command-exit 0.Acceptance notes
SERVICE_UNAVAILABLE, so the gate's verdict in an outage is the same as before. The log is now one line per distinct object rather than one per request.loadObjectItem'sOBJECT_PACKAGE_DISABLEDbranch; read from the code, not measured here).objectql-slot-consumer-census.test.tskeeps its measured sentence about where a real op goes on the multi-kernel harness; only the method name changed, the sentence was not re-measured.origin/mainmoved after the branch point (d4064cc1..ed0e71db); itspackages/restchanges touch other files, andgit merge-treeagainst it is clean, so the branch was not merged.Verification
Head
2019e190, based4064cc1, after a whole-tree build (turbo run build --filter=!@objectstack/docs, lock VERDICT command-exit 0).Derived gate families and lint
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 61 families over the 7 changed paths. All 61 exited 0, andpnpm lintexited 0.--ranreconciliation:61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN;pnpm lintrecorded outside the derivation.check-adr-0087-registration:1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.Dogfood battery (narrowing arm), head 2019e19
pnpm --filter @objectstack/dogfood testunder the verify lock, no shard:Test Files 357 passed | 3 skipped (360) · Tests 2875 passed | 20 skipped (2895), lock VERDICT command-exit 0.Human Minimum
packages/specholds no runtime logic. (Not touched.)pnpm lintpasses.main, one pull request.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cwgfUuNAvcUg3JzBiR