Repository navigation
fix(rest)!: an empty ?id= on the app list or ?object= on the view list is refused 400 naming the parameter instead of read as absent - #28602
Conversation
… parameter instead of read as absent `GET /meta/app?id=` with an empty value skipped the app filter and served the caller's whole visible list. The shared list chain now refuses it after the per-caller gate with 400 VALIDATION_ERROR naming `id`, the answer the #27484 fix gives a blank `$searchFields`. A non-empty id, an absent id and the repeated-id 400 are unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 ab5df89dcd4bb42f0ec36b2ce71bf92ed269f94a && git checkout ab5df89dcd4bb42f0ec36b2ce71bf92ed269f94a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 882fcdbe79dbd299a66217da5221a113f851d639 70a3f972988a14f0d6e878f9404820af06e4886e && git checkout -B drift-repro 882fcdbe79dbd299a66217da5221a113f851d639 && git merge --no-ff 70a3f972988a14f0d6e878f9404820af06e4886e
node scripts/docs-audit/affected-docs.mjs --json 882fcdbe79dbd299a66217da5221a113f851d639
|
…g the parameter, as the empty app `id` is `GET /meta/view?object=` with an empty value failed the truthiness test and served every view, unsorted and including views bound to no object. The list chain now refuses it after the per-caller gate with 400 VALIDATION_ERROR naming `object`, through the same helper the empty app `id` uses. Co-Authored-By: Claude <noreply@anthropic.com>
…message only, not in a param member Co-Authored-By: Claude <noreply@anthropic.com>
?id= on the app list is refused 400 naming the parameter instead of read as absent?id= on the app list or ?object= on the view list is refused 400 naming the parameter instead of read as absent
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: ① Derived judgmentsAccept-set changes the diff implies, each judged against the cited code on
One wrong claim about the surface, in prose only (see ② for the finding): the changeset's "Now" bullet says ② Semver level
Should-fix (prose, no code change): the changeset's "Now" bullet " ③ Boundary flags
④ Design judgmentThe narrowing, read against the published spec on
Must-fix: none. Should-fix: the one in ②. Implemented-by: VERDICT: PASS |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: Delta only: the seat's record |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract review (delta)Served-tier: Delta judgment
Findings: none owed on this head. Implemented-by: VERDICT: PASS (delta) |
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #28602 · 2026-11-01T08:55ZHead-sha: 维护者速读(席位终稿)
核对六项
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Requested by Leehom · project thread
Fixes #28548
Clause-②: no (narrowing) —
GET /meta/app/GET /meta/appswith an empty?id=, andGET /meta/view/GET /meta/viewswith an empty?object=(onRestServerand on the runtime dispatcher, which hand their store answer to the same list chain), are refused with400 VALIDATION_ERRORnaming the parameter; they answered 200 with the caller's whole visible list before. Nothing is widened: no export, answer member, error code or status is added.Metadata-protocol: none
维护者速读
GET /api/v1/meta/app?id=(id 为空)和GET /api/v1/meta/view?object=(object 为空)现在回 400VALIDATION_ERROR,消息点名参数,不再返回调用者能看到的全部应用或全部视图。object是同一个毛病,席位要求在本 PR 一并修。和 data: a blank or comma-only$searchFieldsis read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484(空$searchFields被当成没写)同一类,按同样方式拒收。id或空object的 API 调用方;@objectstack/client和 Console 读这两个列表都不带这两个参数。非空值、不带参数、重复参数的回答都不变。回滚 = revert 本 PR。packages/rest/src/meta-item-read-gate.ts(createMetaListAnswer第 3、4 步和refuseEmptyListFilter)、packages/rest/src/meta-app-publish-gate.test.ts(有意翻转的钉子)。pnpm --filter @objectstack/rest exec vitest run src/meta-app-publish-gate.test.ts,预期 27 个用例全过(空 id 三种调用者都回 400,空格 id 仍回空列表,空 object 回 400)。What changes
createMetaListAnswerstep 3 (packages/rest/src/meta-item-read-gate.ts): the app filter used to skip an emptyid(typeof appIdFilter === 'string' && appIdFilter !== ''), so the request was served as if no filter had been sent. An empty value now goes to the newrefuseEmptyListFilter(), which throws400 VALIDATION_ERRORwith the messageQuery parameter 'id' names no app; got "". …. The throw reaches each transport's own error door (RestServer'shandleRouteError, the dispatcher'serrorFromThrown), the wayrefuseUnknownMetaListTypealready answers on this route, so neither transport hand-builds a body.createMetaListAnswerstep 4 (seat-directed fold, same class):?object=onviewwas read with a truthiness test (query?.object), soGET /meta/view?object=listed every view, unsorted and including views bound to no object, as if no filter had been sent. An empty value now goes to the same helper:400 VALIDATION_ERROR, messageQuery parameter 'object' names no object; got "". …. A named object still switches;?object=on other types is still ignored.idnarrows as before (one app, or an empty list when it names none,' 'included). Noidlists every app the caller may see. A repeatedidis still the arity 400 at the door entry.?id=on any other type is still ignored.createMetaListAnswersteps 3 and 4 updated to say an empty value is refused.meta-app-publish-gate.test.ts"an ABSENT id still returns the whole published set" asserted that?id=(empty) returns the full list, citing an unsetselectas its producer. No producer sends it (see Acceptance notes), and the card's expected result rules the other way, so that half moves to the new refusal case..changeset/28548-empty-app-id-refused.md:patchon@objectstack/rest, BREAKING banner, ADR-0087not-required (no-migration-prescription).@objectstack/runtimechanges only a test, which itsfiles[]does not ship.Which precedent, and why
The card offers two outcomes: refuse with a 400 naming the parameter, or match nothing as
' 'does. This follows the precedent PR #27670 set for #27484: that change refused a blank$searchFieldswith400 VALIDATION_ERRORand a message naming the parameter, rather than reading it as a value that matches nothing. Along the four axes: there is no real caller that sends an emptyid(measured below), so the loud answer costs nobody; a refusal is the contract-first answer and the one that stops an AI-written client from mistaking a blank name for a real filter; and it adds no gate or surface, only a refusal on the existing door.Tests
Red first, then green (head 9c45ac5)
id(first commit): red on the base code with the new tests —meta-app-publish-gate.test.ts"an EMPTY id is refused…" (expected 200 to be 400) andmeta-list-projection-parity.test.tsrow A "the reference moves" on the new?id=assertion. The row A matrix cells for?id= (empty)passed on the base, because both transports agreed on the wrong 200.object(second commit): red on 9246901 with the new tests — "an EMPTY viewobjectis refused…" (view: expected 200 to be 400, 1 failed / 26 passed) and row A on the new?object=assertion (1 failed / 934 passed).restrebuild:meta-app-publish-gate.test.ts+meta-view-dashboard-audience-gate.test.ts46/46;meta-list-projection-parity.test.ts+dispatcher-query-arity.test.ts968/968.idas a builder (studio.access), asmanage_usersand as a caller holding nothing, on/meta/appand/meta/apps; an emptyobjecton/meta/viewand/meta/views. Each assertsstatus400,codeVALIDATION_ERROR, the message's first clause, and no items. The dispatcher's twin is pinned by the new?id= (empty)and?object= (empty)probes in@objectstack/runtime's list parity census: every type × caller cell must answer the samestatus+code+ items on both transports, and row A asserts400 VALIDATION_ERRORwith no items for each.?id=crmreturns that one app, noidreturns the visible list,?id=with a space still returns200 [], repeatedidstays 400,?id=onviewis still ignored;?object=leadstill switches to that object's views and noobjectstill lists them all (asserted inside the new object case).Ablation (each leg red on both suites, then restored to the HEAD blob)
idleg (on 9246901):--anchor " const appIdFilter = metaType === 'app' ? query?.id : undefined;" --replacement " const appIdFilter = metaType === 'app' && query?.id !== '' ? query?.id : ((globalThis as any).ABLATE_28548 = undefined);", which puts back the old skip. Anchor 1 → 0, blob 24ba18a4 → acc770c9; rebuilt; preflight hitdist/index.js+dist/index.cjs. Red: the rest case (caller holding [studio.access]: expected 200 to be 400) and runtime row A. Restored: blob == HEAD 24ba18a4,git diff HEADempty; rebuilt;--absentclean.objectleg (on 9c45ac5):--anchor " if (metaType === 'view' && query?.object === '') refuseEmptyListFilter('object', 'object', 'view');" --replacement " if ((globalThis as any).ABLATE_28548B === 'on' && metaType === 'view' && query?.object === '') refuseEmptyListFilter('object', 'object', 'view');", which lets the empty value fall back to the old truthiness skip. Anchor 1 → 0, blob 35301758 → 15af187f; rebuilt; preflight hitdist/index.js+dist/index.cjs. Red: the rest object case (view: expected 200 to be 400, 1 failed / 26 passed) and runtime row A (1 failed / 934 passed). Restored: blob == HEAD 35301758,git diff HEADempty; rebuilt;--absentclean over 6 built files, tree clean.Package suites, typecheck, lint (head 9c45ac5)
pnpm --filter @objectstack/rest test:: exit 0 · Test Files 362 passed (362) · Tests 6907 passed, 332 skippedpnpm --filter @objectstack/runtime exec vitest run src/domains/meta-list-projection-parity.test.ts src/domains/dispatcher-query-arity.test.ts:: exit 0 · 968 passed (the parity file is the only runtime file changed; it is in therepoproject, which CI's Test Core runs)pnpm --filter @objectstack/rest typecheck:: exit 0pnpm --filter @objectstack/runtime typecheck:: exit 0 (@objectstack/driver-tursobuilt first, a test-only import outside the closure)pnpm exec eslint --no-inline-config packages/rest packages/runtime:: exit 0Dogfood battery (9c45ac5)
pnpm exec turbo run build --concurrency=2 --filter='@objectstack/dogfood^...'thenpnpm --filter @objectstack/dogfood testunderos-verify-lock.sh, no shard, no path:VERDICT command-exit 0· Test Files 364 passed, 3 skipped (367) · Tests 2934 passed, 28 skipped (2962) · no failures. (Also green on 9246901 with the same counts.)Acceptance notes
idorobject:@objectstack/client'smeta.getItems(type, { packageId })sends noid; no file underui/packages,ui/apps,packages/client/srcorpackages/cli/srcbuilds ameta/app?id=request, and the console readsmeta.getItems('view')with no query. The QA ledger rows underdocs/qa/platform-checklistthat read/meta/app?id=all name an app; none asserts the empty answer.RestServeris the flat{ error, code }its route error door writes for a thrown refusal (the same doorrefuseUnknownMetaListTypeuses on this route); the repeated-id400 at the door entry writes the nested{ error: { code, message } }. Both carryVALIDATION_ERROR. The dispatcher writes its own envelope with the sameerror.code. Not changed here.?id=with a space keeps its answer (200 []): the ruling keeps every non-emptyidas it was. data: a blank or comma-only$searchFieldsis read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484 refused a whitespace-only$searchFields, but there the value is split into names and trimmed; an app name is compared whole.?object=refusal was folded in on the seat's direction (four adjacency conditions: same class, same file, mechanical, same gate family); the claim's file surface already covered identical empty-as-absent filters in this gate.claude/project-thread-cq2t0tis the card thread's harness-pinned branch, named in claim 6554674218; it carries no issue number. Signature:domain:server#3 · os-project-manager · session_01FEkQ5pQoUJCqKk6SJQb6Qx.packages/rest/src/meta-item-read-gate.tsat the claim's read.Verification
Local subset, head
9c45ac50:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 60 families; every one run with its exit code captured before any pipe, all 60exit 0(check:type-check-debtunderos-verify-lock.sh).--ranreconcile: "60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN". Three first-pass readings were not verdicts and were re-run once the parallel dogfood closure build had settled:check:dts-closure(exit 1 while that build was rewriting dists; 239/239 declaration files present on re-run),check:dual-build-cjs-loads(exit 3, missing dists; passed after the remaining seven were built) andcheck:type-check-debt(exit 99, lock held by the dogfood battery).check-plugin-teardown-shape.mjs --self-testran after fetching its pinned fixture commit. The same 60 were allexit 0on 9246901. CI-run families: see this PR's check-runs.Human Minimum
packages/specholds no runtime logic. (not touched)pnpm lintpasses on the touched packages.main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code