Repository navigation
Commit d659c1d
fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app (#29046)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X7czLawmpLmMTMhtzDM1pmZ)_
Fixes #28930
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or doc page teaches the app list's `?id=`
filter (`git grep` over `skills/` and `content/docs/` finds none), so
nothing there names a spelling this refuses.
## 维护者速读
- **改了什么**:应用列表 `GET /meta/app` 收到括号写法的
`id`(`?id[]=NAME`、`?id[0]=NAME`,以及任何以 `id[` 开头的键)时,答 400
`VALIDATION_ERROR` 并点名该键,不再忽略过滤、把调用者能看的整份应用列表当作结果返回。
- **为什么改**:按 qs 风格序列化数组的客户端只想取一个应用,却拿回全部应用,还会把第一个当作它要的那个来渲染;这和 #28548
修好的空 `?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[]=setup` and `?id[0]=setup` reach the `/meta` list chain under the
key as spelled: every adapter hands query keys through unparsed
(`readQuery` in `plugin-hono-server`, `readDispatchQuery` in
`@objectstack/hono`). Step 3 of `createMetaListAnswer`, the one-app
filter, reads `query.id` only, 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 asks
`refuseBracketedAppIdFilter(query)` first on the app list. Any query key
that opens `id[` is refused with `400 VALIDATION_ERROR` and no list. The
refusal uses the empty `?id=` refusal's sentence frame (#28548). Both
now go through one builder, `throwListFilterRefusal`, and the empty
`?id=` / `?object=` messages stay byte for byte the same:
- empty (unchanged): `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.`
- bracketed (new): `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.`
- The refusal sits after the per-caller gate (step 2), where #28548
placed the empty refusal, and it throws to each transport's own catch.
So `RestServer` and the runtime dispatcher answer it identically,
through the one seam both list doors share.
- A bracketed key next to a plain `?id=` is refused too, because the
request carries two spellings of one filter.
**Measured: does the seam serve other `/meta/:type` list doors?** Only
the app list reads `id`. Step 3 is keyed on the folded `metaType ===
'app'`, so `/meta/apps` is covered and no other type reads an `id`
spelling. On every other type a bracketed `id` key 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/app` only, and the
siblings are reported under Acceptance notes.
**On the order's words "the same 400 the repeated `id` gets (#28548's
words)".** The repeated `id` answer is `repeatedQueryParamMessage`,
which says `supplied 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 repeated `id`'s 400.
## Tests
Measured at HEAD `2055ce23` (worktree on `claude/project-thread-vh6ns8`,
base `135054eb`).
- `packages/rest/src/meta-app-publish-gate.test.ts`: `it.each(['id[]',
'id[0]', 'id[name]'])` asserts `400` + `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 plural `apps`, and with a bracketed
key next to a plain `id`. A control shows a bracketed `id` key on the
view list is not read.
- `packages/runtime/src/domains/meta-list-projection-parity.test.ts`:
two `PARAM_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 the
`RestServer` answer is a 400.
- `packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts`
(booted showcase over the Hono stack): `?id[]=setup` and `?id[0]=setup`
as the admin and as a signed-up member (the `client_liaison` holder)
answer `400 VALIDATION_ERROR` with `Query parameter 'id[]' names no app`
and no items. Controls: `?id=showcase_app` returns one app, `?id=setup`
returns `[]`, `?id=` returns 400, a repeated `id` returns 400, and no
`id` returns the list, where a member never sees `setup`.
<details><summary>Red first / ablation: dropping the arm reds only the
new cases</summary>
The mutation replaced the call `if (metaType === 'app')
refuseBracketedAppIdFilter(query);` with `void
refuseBracketedAppIdFilter;` through `scripts/ablation-replace.mjs`,
which is main's behaviour. The tool reported anchor 1 → 0, blob
`9c8674a8` → `b9866803`. Then `pnpm --filter @objectstack/rest build`
(exit 0), and `ablation-dist-preflight.mjs @objectstack/rest
"refuseBracketedAppIdFilter(query);" --absent` (exit 0, marker absent
from all 6 built files).
- rest unit: `Tests 3 failed | 28 passed (31)`. The three failures are
exactly the `BRACKETED id[]` / `id[0]` / `id[name]` cases.
- runtime parity: `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.
- dogfood: `Tests 2 failed | 12 passed (14)`. The failures are
`?id[]=setup` and `?id[0]=setup`, which got `status: 200` with `items:
[showcase_app, …]`, the card's reported answer.
Restore: `ablation-replace --restore` reported blob == HEAD `9c8674a8`
and an empty `git 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.
</details>
<details><summary>Package suites and typecheck (HEAD 2055ce2)</summary>
- `pnpm --filter @objectstack/rest test`: exit 0, `Test Files 366 passed
(366)`, `Tests 6940 passed | 332 skipped (7272)`
- `pnpm --filter @objectstack/rest typecheck`: exit 0
- `pnpm --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 0
- `pnpm --filter @objectstack/dogfood exec vitest run
test/meta-read-doors-live.dogfood.test.ts`: exit 0, `Tests 14 passed
(14)`
</details>
## Acceptance notes
- **Premise.** The card and the order place the filter in
`packages/rest/src/rest-server.ts`. It lives in
`packages/rest/src/meta-item-read-gate.ts` (`createMetaListAnswer`, step
3), the chain both transports call. The defect reproduced as stated (see
the ablation leg).
- **The dogfood stack has no `setup` app.** `bootShowcase` serves only
`showcase_app` to both callers. The card's `setup` comes from a full `os
dev` boot. The bracketed probe still uses `setup` as the order asks, and
on main it answered the whole list (`[showcase_app]`). The narrowing
control uses `?id=showcase_app`, and `?id=setup` → `[]`.
- **Sibling, same class (finding, not fixed here).** `GET
/meta/view?object[]=lead` answers every view with 200, where
`?object=lead` answers one. This was measured through `RestServer`'s
`GET /meta/:type` route handler with a three-view fixture: `object[]`
gave `200 ["all_leads","acct_view","loose_view"]` and `object=` gave
`200 ["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").
- **Overlap.** Open PR #28850 also edits
`meta-read-doors-live.dogfood.test.ts`, in its header comment and the
acceptance[2] setup. This PR only appends a `describe` block at the end
of the file, so the hunks do not meet. No other open PR touches
`meta-item-read-gate.ts`, `rest-server.ts`, the rest unit file or the
runtime parity file. Every open PR was checked by diff names; #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.
- **Derived gate set** (`node scripts/pm/dispatch-gates.mjs --commands`,
from the merge-base): 64 commands run, all exit 0. `--ran`
reconciliation answers `64 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-test` needed its pinned commit
on this shallow clone (`git fetch --depth=1 origin 621a487…`).
- `check:dual-build-cjs-loads` read 7 packages with no `dist/`. After
they were built, it reported `115 published require entry point(s)
across 70 package(s) load`.
The record is `pm-orders/server/28930-gates-ran.txt` in the project
files.
- **Full dogfood battery** (the `(narrowing)` arm owes it): `pnpm
--filter @objectstack/dogfood test` under 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)`.
- **Not run locally:** `test:repo` (the vitest `repo` projects, which
belong to CI Test Core) and `pnpm lint` (a repo-wide scan owned by CI).
The one `repo`-project file this diff edits,
`packages/runtime/src/domains/meta-list-projection-parity.test.ts`
(listed in `packages/runtime/vitest.repo-tests.json`), was run directly
(991/991). The rest of that project is left to CI.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5o6bowfKzFDhW76JZ3)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6ada509 commit d659c1d
5 files changed
Lines changed: 153 additions & 5 deletions
File tree
- .changeset
- packages
- qa/dogfood/test
- rest/src
- runtime/src/domains
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
164 | 164 | | |
165 | 165 | | |
166 | 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 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
167 | 214 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
338 | 338 | | |
339 | 339 | | |
340 | 340 | | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
341 | 377 | | |
342 | 378 | | |
343 | 379 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3418 | 3418 | | |
3419 | 3419 | | |
3420 | 3420 | | |
| 3421 | + | |
| 3422 | + | |
| 3423 | + | |
| 3424 | + | |
| 3425 | + | |
| 3426 | + | |
| 3427 | + | |
| 3428 | + | |
| 3429 | + | |
| 3430 | + | |
| 3431 | + | |
| 3432 | + | |
| 3433 | + | |
| 3434 | + | |
| 3435 | + | |
| 3436 | + | |
| 3437 | + | |
| 3438 | + | |
| 3439 | + | |
| 3440 | + | |
| 3441 | + | |
| 3442 | + | |
| 3443 | + | |
| 3444 | + | |
| 3445 | + | |
| 3446 | + | |
| 3447 | + | |
| 3448 | + | |
| 3449 | + | |
| 3450 | + | |
| 3451 | + | |
| 3452 | + | |
| 3453 | + | |
| 3454 | + | |
| 3455 | + | |
| 3456 | + | |
| 3457 | + | |
3421 | 3458 | | |
3422 | | - | |
3423 | | - | |
| 3459 | + | |
| 3460 | + | |
3424 | 3461 | | |
3425 | 3462 | | |
3426 | 3463 | | |
| |||
3516 | 3553 | | |
3517 | 3554 | | |
3518 | 3555 | | |
3519 | | - | |
3520 | | - | |
| 3556 | + | |
| 3557 | + | |
| 3558 | + | |
3521 | 3559 | | |
3522 | 3560 | | |
3523 | 3561 | | |
| |||
3575 | 3613 | | |
3576 | 3614 | | |
3577 | 3615 | | |
3578 | | - | |
| 3616 | + | |
| 3617 | + | |
| 3618 | + | |
3579 | 3619 | | |
3580 | 3620 | | |
3581 | 3621 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
525 | 525 | | |
526 | 526 | | |
527 | 527 | | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
528 | 532 | | |
529 | 533 | | |
530 | 534 | | |
| |||
687 | 691 | | |
688 | 692 | | |
689 | 693 | | |
| 694 | + | |
| 695 | + | |
690 | 696 | | |
691 | 697 | | |
692 | 698 | | |
| |||
0 commit comments