Repository navigation
Commit 186a83a
fix(rest)!: /meta/_drafts refuses a bracketed packageId or type key instead of listing every draft (#29440)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi96RirUSzHMtPHuFBWpUKa7o)_
Fixes #29319
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or doc page teaches a bracketed `packageId`
or `type` key on `_drafts` (`git grep` over `skills/`, `content/docs/`
and `apps/docs/` finds none); the pages that name `_drafts` teach no
query spelling at all.
## 维护者速读
- **改了什么**:`GET /meta/_drafts` 收到括号写法的
`packageId`(`?packageId[]=ID`、`?packageId[0]=ID`)或
`type`(`?type[]=TYPE`、`?type[0]=TYPE`)时答 400 `VALIDATION_ERROR`
并点名该键;以前这些键被忽略,返回全部包、全部类型的草稿。
- **为什么改**:按 qs 风格序列化数组的客户端只想看一个包或一种类型的草稿,却拿回全部草稿并当成过滤结果;这是
#29046、#29298 修过的同一类静默放宽在 `/meta` 上的最后一处。
- **风险与回滚**:只动 `packages/rest`(#29298 的括号判定改成按门查表的一个函数,`_drafts`
门入口加同一判定,两条传输共用),`patch` 带 BREAKING 横幅;只影响发括号键的 API
调用方,`@objectstack/client` 与控制台都不发。回滚即还原本 PR。
-
**先看**:`packages/rest/src/meta-item-read-gate.ts`(`META_BRACKETED_FILTERS`
与
`metaBracketedFilterRefusal`);`packages/rest/src/meta-collection-routes.ts`
的 `_drafts`
门入口新增几行;`.changeset/29319-bracketed-drafts-filter-refused.md`。
- **两分钟验证**:`pnpm --filter @objectstack/rest exec vitest run
src/meta-query-arity-doors.test.ts`,预期全绿,其中 `a BRACKETED packageId or
type key on _drafts` 用例断言 400。
- **需要你做的**:无,席位落地。
## What changes
`?packageId[]=ID`, `?packageId[0]=ID`, `?type[]=TYPE` and
`?type[0]=TYPE` reach `GET /meta/_drafts` under the key as spelled: both
Hono adapters hand query keys through unparsed (`readQuery` in
`plugin-hono-server`, `readDispatchQuery` in `@objectstack/hono`). Both
transports read only `query.packageId` and `query.type`, so `listDrafts`
was handed no package and no type, and an author who named one package's
(or one type's) drafts got every draft with 200.
- `packages/rest/src/meta-item-read-gate.ts`: #29298's
`metaBracketedPackageRefusal` becomes `metaBracketedFilterRefusal(door,
query)`, driven by one per-door table, `META_BRACKETED_FILTERS`: the
list and the book tree judge `package` (as before), `_drafts` judges
`packageId` and then `type`. It is the same function, generalised and
renamed, not a second copy; it was module-level and never on the package
index, so no export moves. The two `package` sentences are
byte-identical (#29298's pins pass unchanged). `metaQueryArityRefusal`
already asks it after the multiplicity rule, so the runtime dispatcher's
`_drafts` gets the refusal with no runtime source change.
- `packageId`: `Query parameter 'packageId[]' names no package; the
'packageId' filter takes exactly one package id, spelled ?packageId=ID.
The list was not served: a bracketed 'packageId' key is not a filter.
Name one package, or leave the parameter out to list the drafts of every
package.`
- `type`: `Query parameter 'type[]' names no metadata type; the 'type'
filter takes exactly one metadata type, spelled ?type=TYPE. The list was
not served: a bracketed 'type' key is not a filter. Name one metadata
type, or leave the parameter out to list the drafts of every type.`
- `packages/rest/src/meta-collection-routes.ts`: `RestServer`'s
`_drafts` door asks the same decision right after
`refuseRepeatedQueryParams`, before `listDrafts`, and writes it in the
nested envelope its arity gate writes (as the list door does).
- `packages/rest/src/meta-item-read-routes.ts`: the book tree's call
follows the rename (one line).
- The order is unchanged on both transports: capability gate (403),
protocol probe (501), multiplicity, then the bracketed key. A bracketed
key beside the plain one is refused (two spellings of one filter).
`MetaItemQueryRefusal.rule` gains no member: the refusal reuses
`bracketed-parameter`, so `Clause-②: no (narrowing)`.
**Measured on main before any edit (27c1c55).** Real Hono request
parsing on both transports (`RestServer` on a `HonoHttpServer`, driven
with `app.fetch`; the dispatcher on a raw Hono app with
`readDispatchQuery`'s exact query shape), an author caller, four seeded
drafts (two packages × two types), brackets sent raw and
percent-encoded:
| Request | Both transports on main |
|:--|:--|
| no query | 200, 4 drafts (control) |
| `?packageId=crm` / `?type=view` / both | 200, 2 / 2 / 1 drafts
(controls) |
| `?packageId[]=crm`, `?packageId[0]=crm` | 200, 4 drafts,
`listDrafts({})` |
| `?type[]=view`, `?type[0]=view` | 200, 4 drafts, `listDrafts({})` |
| `?packageId[]=crm&type[]=view` | 200, 4 drafts |
| `?packageId=crm&packageId[]=hr` | 200, 2 drafts: the bracketed key
silently dropped |
**Producer census.** `@objectstack/client`'s `listDrafts` builds both
keys with `URLSearchParams.set`; the console builds
`?packageId=${encodeURIComponent(…)}` (`usePendingDrafts.ts`,
`PackagesPage.tsx`); neither has a qs library; no docs or skill example
spells either key bracketed. No real producer sends them.
## Tests
- `packages/rest/src/meta-query-arity-doors.test.ts` (appended
`describe`): the decision as data for `packageId[]`, `packageId[0]`,
`packageId[id]`, `type[]`, `type[0]`, `type[name]` (and beside the plain
key); a repeated plain `packageId` still answers the multiplicity
sentence first; not judged on the list or the tree, nor for `types[]`,
`packageIds[]` or `package[]` on `_drafts`; `RestServer`'s `_drafts`
answers the nested 400 and `listDrafts` is never called; controls: the
plain spellings still narrow, none reads every draft.
- `packages/runtime/src/domains/dispatcher-query-arity.test.ts`
(appended `describe`): the dispatcher's `_drafts` refuses `packageId[]`,
`packageId[0]`, `type[]`, `type[0]` with the same code and sentence
before `listDrafts`; beside the plain key too; control: the plain
spellings reach `listDrafts`.
- `packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts`
(booted showcase over Hono, appended `describe`):
`/meta/_drafts?packageId[]=` / `?packageId[0]=com.example.showcase` and
`?type[]=` / `?type[0]=view` answer 400 `VALIDATION_ERROR` naming the
key, no list, as the admin; controls: `?packageId=` and `?type=` answer
200 with only matching rows, and a member is still refused 403 at the
capability gate first.
No existing test was edited; every hunk in a test file is an addition.
<details><summary>Red first on main (a2fcede, pins at
b748eaa)</summary>
The pins were committed before the fix and run against main's source and
dist:
- rest: `Tests 10 failed | 39 passed (49)`: the six as-data cases and
the four `RestServer` door cases. Every control and every #29298 pin
passed.
- runtime: `Tests 5 failed | 38 passed (43)`: the four dispatcher cases
and the beside-the-plain-key case.
- dogfood: the reds are read off the ablation legs below.
</details>
<details><summary>Ablation: each refused key, and the RestServer
hand-off, put back, reds only its own pins</summary>
Through `scripts/ablation-replace.mjs` (wrap mode), `pnpm --filter
@objectstack/rest build`, then `ablation-dist-preflight.mjs
packages/rest MARKER` (marker present in 2 built files) before reading
any run. Baseline on the fixed head first: rest `49 passed`, runtime `43
passed`, dogfood file `24 passed`.
1. **packageId.** The `_drafts` table row's `param: 'packageId'` renamed
to a marker, so the key is not judged. Anchor 1 → 0, blob `977baa3c` →
`69581359`. rest `5 failed | 44 passed (49)` (the three `packageId`
as-data cases, the two `RestServer` `packageId` cases); runtime `3
failed | 40 passed (43)` (`packageId[]`, `packageId[0]`,
beside-the-plain-key); dogfood `2 failed | 22 passed (24)`
(`packageId[]`, `packageId[0]`).
2. **type.** The row's `param: 'type'` renamed to a marker. Anchor 1 →
0, blob `977baa3c` → `36e7b636`. rest `5 failed | 44 passed (49)` (the
three `type` as-data cases, the two `RestServer` `type` cases); runtime
`2 failed | 41 passed (43)` (`type[]`, `type[0]`); dogfood `2 failed |
22 passed (24)` (`type[]`, `type[0]`).
3. **RestServer hand-off.** The `_drafts` door's call asks an unknown
door instead of `'drafts'`. Anchor 1 → 0, blob `824a9f54` → `2d929242`.
rest `4 failed | 45 passed (49)` (the four `RestServer` door cases);
runtime `43 passed (43)` (the dispatcher asks the decision through
`metaQueryArityRefusal`, untouched); dogfood `4 failed | 20 passed (24)`
(the showcase is served by `RestServer`).
Each restore: blob back to HEAD, `git diff HEAD` empty; then a rebuild,
all three markers absent from all 6 built files, whole tree clean (`git
status --porcelain` empty).
</details>
## Acceptance notes
- **Premise.** Holds as the card states it, on both transports, raw and
percent-encoded; and beside a plain `?packageId=` the bracketed key was
silently dropped.
- **File surface vs the claim.** As claimed, plus the one-line rename at
the book tree's call in `meta-item-read-routes.ts`, the other caller of
the function this PR generalises. `packages/runtime/src/domains/meta.ts`
needed no edit: its `_drafts` already asks
`metaQueryArityRefusal('drafts', …)`. No `ui/**`, no `packages/spec/**`.
- **Main-red seen on the final head, not this change.**
`flow-runas.dogfood.test.ts` (3) and
`declarative-endpoint-anonymous-guest.dogfood.test.ts` (1) red with this
PR's source set back to main's (see Verification); reported to the seat.
- **Same class, still not fixed here.** `?preview[]=draft` on the list
reads as no preview and `?include[]=` as no inclusion (#29298's note).
Not measured here; outside this card's keys.
## Verification
Every local reading below is at HEAD `ee0e30c9` (the fix `359c7496` plus
two merges of `origin/main`, the last taken after `dfbe34d8`), in a
worktree on `claude/project-thread-ta4g7t`. Families CI runs are cited
from this PR's check-runs in the report, not re-run here.
<details><summary>Package suites and typecheck (HEAD ee0e30c)</summary>
- `pnpm --filter @objectstack/rest test`: `VERDICT command-exit 0` ·
`Test Files 370 passed (370)` · `Tests 7043 passed | 332 skipped (7375)`
- `pnpm --filter @objectstack/rest typecheck && … runtime typecheck && …
dogfood typecheck`: `VERDICT command-exit 0`
- `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2
src/domains/dispatcher-query-arity.test.ts
src/domains/meta-list-projection-parity.test.ts`: `VERDICT command-exit
0` · `Tests 1185 passed (1185)`
- lint: root eslint with `--no-inline-config` over `packages/rest`,
`packages/runtime`, `packages/qa/dogfood`: exit 0, no findings
- `node scripts/check-platform-checklist.mjs --changed-from origin/main`
(a module export was renamed): OK, 12 anchors judged, 0 lost
</details>
<details><summary>Full dogfood battery (the narrowing owes it)</summary>
`pnpm --filter @objectstack/dogfood test` under the verify lock, after
the `@objectstack/dogfood^...` closure build:
- HEAD `ee0e30c9`: `Test Files 2 failed | 391 passed | 3 skipped (396)`
· `Tests 4 failed | 3244 passed | 28 skipped (3276)`. The four reds are
`flow-runas.dogfood.test.ts` (three: the member's flow trigger answers
`404 RECORD_NOT_FOUND`) and
`declarative-endpoint-anonymous-guest.dogfood.test.ts` (one). They are
not this change: with this PR's three `packages/rest/src` files set back
to `origin/main`'s (blob-identical to the merged main), `rest` rebuilt
and the fix's sentence absent from `dist/`, the same two files answer
the same `Tests 4 failed | 8 passed (12)`; restored, `git status
--porcelain` empty. Neither file touches `/meta`; between the two heads
main took `dfbe34d8`, which changed the flow start doors (not bisected).
Reported to the seat as a main-red, not fixed here.
- HEAD `88dc5779` (the same diff on the previous main): `VERDICT
command-exit 0` · `Test Files 393 passed | 3 skipped (396)` · `Tests
3248 passed | 28 skipped (3276)`.
</details>
<details><summary>Derived gate set</summary>
`node scripts/pm/dispatch-gates.mjs --commands` (from the merge-base, at
`ee0e30c9`): 64 commands, all exit 0. `--ran` reconciliation with each
exit code recorded: `64 derived famil(ies) accounted for — 64 run, 0
NOT-MEASURED (a DERIVED zero)`. On the previous head two first-pass exit
3s were prerequisites, each re-run green:
`check-plugin-teardown-shape.mjs --self-test` (its pinned fixture commit
fetched on this shallow clone) and `check:dual-build-cjs-loads` (7
packages outside the dogfood closure built).
</details>
- **Not run locally:** `test:repo` (the vitest `repo` projects, CI `Test
Core`). The one `repo`-project file this diff could reach,
`meta-list-projection-parity.test.ts`, was run directly (above).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmSq83zsshBxiLpFJdE)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 29c70ca commit 186a83a
7 files changed
Lines changed: 232 additions & 32 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 | |
|---|---|---|---|
| |||
264 | 264 | | |
265 | 265 | | |
266 | 266 | | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
267 | 304 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
465 | 465 | | |
466 | 466 | | |
467 | 467 | | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
468 | 478 | | |
469 | 479 | | |
470 | 480 | | |
| |||
651 | 661 | | |
652 | 662 | | |
653 | 663 | | |
654 | | - | |
| 664 | + | |
655 | 665 | | |
656 | 666 | | |
657 | 667 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2417 | 2417 | | |
2418 | 2418 | | |
2419 | 2419 | | |
2420 | | - | |
| 2420 | + | |
2421 | 2421 | | |
2422 | 2422 | | |
2423 | 2423 | | |
| |||
2558 | 2558 | | |
2559 | 2559 | | |
2560 | 2560 | | |
2561 | | - | |
| 2561 | + | |
2562 | 2562 | | |
2563 | 2563 | | |
2564 | 2564 | | |
2565 | 2565 | | |
2566 | 2566 | | |
2567 | 2567 | | |
2568 | 2568 | | |
2569 | | - | |
| 2569 | + | |
2570 | 2570 | | |
2571 | 2571 | | |
2572 | 2572 | | |
2573 | | - | |
2574 | | - | |
2575 | | - | |
2576 | | - | |
2577 | | - | |
2578 | | - | |
2579 | | - | |
2580 | | - | |
2581 | | - | |
2582 | | - | |
| 2573 | + | |
| 2574 | + | |
| 2575 | + | |
| 2576 | + | |
| 2577 | + | |
| 2578 | + | |
| 2579 | + | |
| 2580 | + | |
| 2581 | + | |
| 2582 | + | |
| 2583 | + | |
| 2584 | + | |
| 2585 | + | |
| 2586 | + | |
| 2587 | + | |
| 2588 | + | |
| 2589 | + | |
| 2590 | + | |
| 2591 | + | |
| 2592 | + | |
| 2593 | + | |
| 2594 | + | |
| 2595 | + | |
| 2596 | + | |
| 2597 | + | |
| 2598 | + | |
| 2599 | + | |
| 2600 | + | |
| 2601 | + | |
| 2602 | + | |
| 2603 | + | |
| 2604 | + | |
| 2605 | + | |
| 2606 | + | |
| 2607 | + | |
| 2608 | + | |
| 2609 | + | |
| 2610 | + | |
| 2611 | + | |
| 2612 | + | |
| 2613 | + | |
| 2614 | + | |
| 2615 | + | |
| 2616 | + | |
| 2617 | + | |
| 2618 | + | |
| 2619 | + | |
| 2620 | + | |
2583 | 2621 | | |
2584 | 2622 | | |
2585 | 2623 | | |
2586 | 2624 | | |
2587 | | - | |
2588 | | - | |
| 2625 | + | |
| 2626 | + | |
2589 | 2627 | | |
2590 | | - | |
2591 | | - | |
2592 | | - | |
2593 | | - | |
2594 | | - | |
2595 | | - | |
2596 | | - | |
2597 | | - | |
2598 | | - | |
2599 | | - | |
2600 | | - | |
2601 | | - | |
2602 | | - | |
2603 | | - | |
2604 | | - | |
| 2628 | + | |
| 2629 | + | |
| 2630 | + | |
| 2631 | + | |
| 2632 | + | |
| 2633 | + | |
| 2634 | + | |
| 2635 | + | |
| 2636 | + | |
| 2637 | + | |
| 2638 | + | |
| 2639 | + | |
| 2640 | + | |
| 2641 | + | |
| 2642 | + | |
2605 | 2643 | | |
2606 | 2644 | | |
2607 | 2645 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
386 | 386 | | |
387 | 387 | | |
388 | 388 | | |
389 | | - | |
| 389 | + | |
390 | 390 | | |
391 | 391 | | |
392 | 392 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
198 | 198 | | |
199 | 199 | | |
200 | 200 | | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
0 commit comments