Repository navigation
fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app - #29046
Conversation
…ing every app `?id[]=NAME` and `?id[0]=NAME` reach the list chain under the key as spelled, which the one-app filter never read, so the caller's whole visible list came back. The chain now refuses any key that opens `id[` on the app list with the empty `?id=` refusal's frame: 400 VALIDATION_ERROR naming the key, no list. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5o6bowfKzFDhW76JZ3
…se_app Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5o6bowfKzFDhW76JZ3
📓 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 — 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 4d36e6586f65ae2d11a6f14a506615020eb64bab && git checkout 4d36e6586f65ae2d11a6f14a506615020eb64bab
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab085a048241b0bf1fde06696fc6cbf659a6707f 2055ce2324552f66a7654791d750177a9859837d && git checkout -B drift-repro ab085a048241b0bf1fde06696fc6cbf659a6707f && git merge --no-ff 2055ce2324552f66a7654791d750177a9859837d
node scripts/docs-audit/affected-docs.mjs --json ab085a048241b0bf1fde06696fc6cbf659a6707f |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo Contract reviewServed-tier: ① Derived judgmentsAccept-set and public-surface changes the diff implies, each read against the cited code on the merge-base and on the head:
Claims checked for wrong statements about the surface: the changeset's "Now" message is the literal the builder produces; the PR body's "every adapter hands query keys through unparsed" holds for both Hono adapters read here (the node reference adapter reads ② Semver level
The declaration deviation, judged. The order expected ③ Boundary flags
④ Design judgmentThe narrowing, read against the published contract: Must-fix: none. Should-fix: none that blocks; the vacuous control sentence in ③ may be reworded in the next edit of that file. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt Landing record · PR #29046
Generated by Claude Code |
Requested by Leehom · project thread
Fixes #28930
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or doc page teaches the app list's
?id=filter (git grepoverskills/andcontent/docs/finds none), so nothing there names a spelling this refuses.维护者速读
GET /meta/app收到括号写法的id(?id[]=NAME、?id[0]=NAME,以及任何以id[开头的键)时,答 400VALIDATION_ERROR并点名该键,不再忽略过滤、把调用者能看的整份应用列表当作结果返回。?id=是同一类静默放宽。packages/rest列表链里的一步(两条传输共用),patch带 BREAKING 横幅;仅影响发送括号键的 API 调用方,@objectstack/client与控制台都不发id。回滚即还原本 PR。packages/rest/src/meta-item-read-gate.ts(refuseBracketedAppIdFilter与第 3 步的调用);packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts末尾新块;.changeset/28930-bracketed-app-id-refused.md。pnpm --filter @objectstack/rest exec vitest run src/meta-app-publish-gate.test.ts,预期 31 个全绿,其中三条BRACKETED用例断言 400。What changes
?id[]=setupand?id[0]=setupreach the/metalist chain under the key as spelled: every adapter hands query keys through unparsed (readQueryinplugin-hono-server,readDispatchQueryin@objectstack/hono). Step 3 ofcreateMetaListAnswer, the one-app filter, readsquery.idonly, so it never saw them, and the caller's whole visible list came back with 200.packages/rest/src/meta-item-read-gate.ts: step 3 now asksrefuseBracketedAppIdFilter(query)first on the app list. Any query key that opensid[is refused with400 VALIDATION_ERRORand no list. The refusal uses the empty?id=refusal's sentence frame (Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548). Both now go through one builder,throwListFilterRefusal, and the empty?id=/?object=messages stay byte for byte the same:Query parameter 'id' names no app; got "". The list was not served: an empty app name is not a filter. Name one app, or leave the parameter out to list every app you can see.Query parameter 'id[]' names no app; the 'id' filter takes exactly one app name, spelled ?id=NAME. The list was not served: a bracketed 'id' key is not a filter. Name one app, or leave the parameter out to list every app you can see.RestServerand the runtime dispatcher answer it identically, through the one seam both list doors share.?id=is refused too, because the request carries two spellings of one filter.Measured: does the seam serve other
/meta/:typelist doors? Only the app list readsid. Step 3 is keyed on the foldedmetaType === 'app', so/meta/appsis covered and no other type reads anidspelling. On every other type a bracketedidkey stays unread, the way?id=is (pinned by a control). The query parse is the adapters' and is not this chain's to change. So the fix is/meta/apponly, and the siblings are reported under Acceptance notes.On the order's words "the same 400 the repeated
idgets (#28548's words)". The repeatedidanswer isrepeatedQueryParamMessage, which sayssupplied N times. That would be false for a key supplied once. So the bracketed key gets #28548's own refusal frame, as the claim's "extends #28548's refusal" reads. Status and code are the same as the repeatedid's 400.Tests
Measured at HEAD
2055ce23(worktree onclaude/project-thread-vh6ns8, base135054eb).packages/rest/src/meta-app-publish-gate.test.ts:it.each(['id[]', 'id[0]', 'id[name]'])asserts400+code: VALIDATION_ERROR+ the message's first clause naming the key, with no list in the body. It runs for three caller postures, on the pluralapps, and with a bracketed key next to a plainid. A control shows a bracketedidkey on the view list is not read.packages/runtime/src/domains/meta-list-projection-parity.test.ts: twoPARAM_PROBES(?id[]=crm,?id[0]=crm) drive both transports over every type and caller. The census of row A grows by 2 probes × 26 type cells (991 tests). The "reference moves" test asserts theRestServeranswer is a 400.packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts(booted showcase over the Hono stack):?id[]=setupand?id[0]=setupas the admin and as a signed-up member (theclient_liaisonholder) answer400 VALIDATION_ERRORwithQuery parameter 'id[]' names no appand no items. Controls:?id=showcase_appreturns one app,?id=setupreturns[],?id=returns 400, a repeatedidreturns 400, and noidreturns the list, where a member never seessetup.Red first / ablation: dropping the arm reds only the new cases
The mutation replaced the call
if (metaType === 'app') refuseBracketedAppIdFilter(query);withvoid refuseBracketedAppIdFilter;throughscripts/ablation-replace.mjs, which is main's behaviour. The tool reported anchor 1 → 0, blob9c8674a8→b9866803. Thenpnpm --filter @objectstack/rest build(exit 0), andablation-dist-preflight.mjs @objectstack/rest "refuseBracketedAppIdFilter(query);" --absent(exit 0, marker absent from all 6 built files).Tests 3 failed | 28 passed (31). The three failures are exactly theBRACKETED id[]/id[0]/id[name]cases.Tests 1 failed | 990 passed (991). The one failure is the "reference moves" test. Row A stays green because both transports gave the same unfiltered list.Tests 2 failed | 12 passed (14). The failures are?id[]=setupand?id[0]=setup, which gotstatus: 200withitems: [showcase_app, …], the card's reported answer.Restore:
ablation-replace --restorereported blob == HEAD9c8674a8and an emptygit diff HEAD. The rebuild exited 0, and the preflight showed the marker present in 2 built files with a clean tree. A first ablation attempt deleted the line outright. The now-unused function then failed the rest DTS build (exit 1), and that run was discarded.Package suites and typecheck (HEAD 2055ce2)
pnpm --filter @objectstack/rest test: exit 0,Test Files 366 passed (366),Tests 6940 passed | 332 skipped (7272)pnpm --filter @objectstack/rest typecheck: exit 0pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/meta-list-projection-parity.test.ts: exit 0,Tests 991 passed (991)pnpm --filter @objectstack/runtime typecheck: exit 0pnpm --filter @objectstack/dogfood exec vitest run test/meta-read-doors-live.dogfood.test.ts: exit 0,Tests 14 passed (14)Acceptance notes
packages/rest/src/rest-server.ts. It lives inpackages/rest/src/meta-item-read-gate.ts(createMetaListAnswer, step 3), the chain both transports call. The defect reproduced as stated (see the ablation leg).setupapp.bootShowcaseserves onlyshowcase_appto both callers. The card'ssetupcomes from a fullos devboot. The bracketed probe still usessetupas the order asks, and on main it answered the whole list ([showcase_app]). The narrowing control uses?id=showcase_app, and?id=setup→[].GET /meta/view?object[]=leadanswers every view with 200, where?object=leadanswers one. This was measured throughRestServer'sGET /meta/:typeroute handler with a three-view fixture:object[]gave200 ["all_leads","acct_view","loose_view"]andobject=gave200 ["all_leads"]. The adapters pass the key through the same way, so the view switcher's filter is dropped the same way. Not measured:package[]on the list doors (?package=scopes the read, and an unread key would read as "every package").meta-read-doors-live.dogfood.test.ts, in its header comment and the acceptance[2] setup. This PR only appends adescribeblock at the end of the file, so the hunks do not meet. No other open PR touchesmeta-item-read-gate.ts,rest-server.ts, the rest unit file or the runtime parity file. Every open PR was checked by diff names; fix(metadata-protocol): the by-id OCC comparison answers only for a row the caller reads #28731 was checked through its file list.Verification
All local readings are at HEAD
2055ce23. Every family CI runs is cited from this PR's check-runs in the report, and none of them was re-run here.node scripts/pm/dispatch-gates.mjs --commands, from the merge-base): 64 commands run, all exit 0.--ranreconciliation answers64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED. Two first-pass exit 3s were prerequisites, not reds, and each was re-run green:check-plugin-teardown-shape.mjs --self-testneeded its pinned commit on this shallow clone (git fetch --depth=1 origin 621a4876…).check:dual-build-cjs-loadsread 7 packages with nodist/. After they were built, it reported115 published require entry point(s) across 70 package(s) load.The record is
pm-orders/server/28930-gates-ran.txtin the project files.(narrowing)arm owes it):pnpm --filter @objectstack/dogfood testunder the verify lock, after the@objectstack/dogfood^...closure build.VERDICT command-exit 0·Test Files 383 passed | 3 skipped (386)·Tests 3166 passed | 28 skipped (3194).test:repo(the vitestrepoprojects, which belong to CI Test Core) andpnpm lint(a repo-wide scan owned by CI). The onerepo-project file this diff edits,packages/runtime/src/domains/meta-list-projection-parity.test.ts(listed inpackages/runtime/vitest.repo-tests.json), was run directly (991/991). The rest of that project is left to CI.Generated by Claude Code