Repository navigation
Commit 96e7a43
fix(runtime)!: the package draft doors refuse a query parameter they do not read (#26853)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XAR1TDS8qyw6DaeA7t9jepP)_
Fixes #26713
Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or doc teaches a query parameter on either
draft door: `skills/**` and `content/docs` name `publish-drafts` /
`discard-drafts` only in `plugin-hooks.md`, `metadata-api.mdx` (a route
list) and two reference tables, none with a query string, and no caller
in `ui/**`, `packages/client`, the CLI or `skills/**` sends one.
## 维护者速读
- 改了什么:包的两个草稿门 `POST /packages/:id/publish-drafts` 与 `discard-drafts`
现在拒收任何查询参数,答 `400 VALIDATION_ERROR`,文案用仓里唯一的那句「不认识的查询参数」。
- 为什么改:`publish-drafts?dryRun=true` 以前忽略参数、真的发布草稿并答 200
`published`;想预览的工具与 AI 拿到的是不可逆的发布。
- 风险与回滚:只拒带查询参数的请求;控制台、`MetadataClient`、SDK 都不带参数,行为不变。回滚即还原
`packages.ts` 一处 helper 与两行调用。
- 先看:`packages/runtime/src/domains/packages.ts` 的 `refuseDraftDoorQuery`
与两处调用(在权限与 wall 闸之后、协议之前);其次新测试
`packages-draft-doors-closed-query.test.ts`。
- 两分钟验证:`cd packages/runtime && pnpm exec vitest run
src/domains/packages-draft-doors-closed-query.test.ts`,预期 10 passed;带
`?dryRun=true` 的两例答 400 且协议未被调用。
- 需要你做的:无,席位落地。
## What changes
- `packages/runtime/src/domains/packages.ts`: the draft doors' closed
query set is declared (`DRAFT_DOOR_QUERY_PARAMS`, measured EMPTY: both
doors read the path id, the session and the body, never the query), and
`refuseDraftDoorQuery` answers any name on the query string `400
VALIDATION_ERROR` with `unknownQueryParamRefusalMessage` from
`@objectstack/rest`, the recognition sentence every closed-query door
answers, written through `deps.error` with the code the way the `/meta`
doors write it (#23072). No new sentence, no new code, no helper copied.
- Placement: after `requireManageMetadata` and
`refuseWalledCatalogPackageWrite`, before the protocol is resolved, the
#23214 placement on this domain's other write doors. A caller without
`manage_metadata` keeps its `403`.
- `discard-drafts` is closed in the same change: it is the twin door the
order names, reads no query either, and `?dryRun=true` there dropped
every pending draft. Its hunk is the same one call line.
- ⛔ No `dryRun`. A dry run would be a declared parameter and a protocol
addition, which goes back to triage.
- Transports: only the runtime dispatcher serves these two doors
(`dispatcher-plugin.ts` `mountPackagesRoute`, and the
`@objectstack/hono` catch-all into the same `dispatch()`); `RestServer`
mounts neither (`packages/rest/src` has no route for them), so there is
one place to close.
- Changeset `@objectstack/runtime` patch, with the BREAKING banner and
the ADR-0087 `not-required (no-migration-prescription)` disposition.
## Why
The QA run (S3) measured `POST
/api/v1/packages/:id/publish-drafts?dryRun=true` with `{}` answering
`200 outcome: "published"` with the draft live. A caller asking for a
preview got the irreversible act, shaped like a success. Triage
`6446824931` ruled: close the query set and refuse an unknown parameter
with 400.
## Tests
`packages/runtime/src/domains/packages-draft-doors-closed-query.test.ts`,
10 cases, driving `dispatch()` over a real `SchemaRegistry` and a
recording protocol, for each door:
- refusal: `{dryRun: 'true'}`, a repeated `dryRun`, and two unknown
names each answer 400 `VALIDATION_ERROR` with exactly
`unknownQueryParamRefusalMessage(query, [])`, every name quoted, and the
protocol's `publishPackageDrafts` / `discardPackageDrafts` never called;
- preservation: no query, 200, the protocol called once;
- order: a caller without `manage_metadata` sending `?dryRun=true` gets
403, protocol not called.
There is no declared flag on either door, so the order's "repeated
declared flag still refused the #23214 way" has nothing to pin here; a
repeated unknown name is answered by recognition, which runs first (the
`query-allowlist.ts` rule).
<details><summary>Red first on base c8583f8, green on head,
ablation</summary>
- Red, base `c8583f80` (implementation absent): the new file 6 failed |
4 passed (`expected 400, got {"status":200,..."outcome":"published"...}`
and the discard twin `discardedCount: 1`).
- Real-door probe (one-off, not committed; real
`ObjectStackProtocolImplementation` over SQLite, one pending view draft,
`handlePackages('/com.example.repairs/publish-drafts', 'POST', {},
{dryRun:'true'})`):
- base:
`{"status":200,"code":null,"outcome":"published","draftsBefore":["view/repairs_board"],"draftsAfter":[],"servedLabel":"Board
v1"}`: the draft went live.
- head: `{"status":400,"code":"VALIDATION_ERROR","message":"The
\"dryRun\" query parameter is not supported by this endpoint. ... This
endpoint takes no query
parameters.","draftsBefore":["view/repairs_board"],"draftsAfter":["view/repairs_board"],"servedLabel":null}`:
nothing published, the draft still pending.
- Ablation on head `e542b231` through `scripts/ablation-replace.mjs`
(anchor `if (message === undefined) return null;` → `... || true ...`,
anchor 1 → 0, blob `8dcfe74db65c` → `f592f4389363`): 6 failed | 4
passed, exactly the six refusal cases; restored blob == HEAD, `git diff
HEAD` empty. The test imports the dispatcher from `src`, so no build
sits between the mutation and the run.
</details>
## Acceptance notes
Sibling package write doors, measured on `c8583f80` (none of them is
changed here):
| door | closed query set? |
|---|---|
| `POST /meta/:type/:name/publish` (per-item publish, `RestServer`) |
yes, `META_ITEM_PUBLISH_PARAMS` (#22877) |
| `POST /packages` (install), reads `overwrite` | no; repeats refused
(#23214), unknown names ignored |
| `DELETE /packages/:id` (uninstall), reads `keepData` | no; repeats
refused (#23214), unknown names ignored |
| `PATCH /packages/:id`, `PATCH /:id/enable`, `PATCH /:id/disable`,
`POST /:id/publish`, `/:id/revert`, `/:id/rollback`,
`/:id/commits/:c/revert`, `/:id/adopt-orphans`, `/:id/duplicate` | no;
none reads the query |
| `POST /packages/publish` (`RestServer` package route) | no; reads no
query |
Measured at the dispatcher door with the `packages-write-flag-arity`
harness: `DELETE /packages/:id?keepdata=true` (and `?keep_data=true`)
answers 200 and calls `deletePackage({ packageId })` with no `keepData`,
so the protocol drops the object tables the caller asked to keep. That
is reported to the seat as a proposed card, not widened into this PR.
## Verification
All on head `e542b231` (base `c8583f80`).
- `pnpm --filter @objectstack/runtime test`: Test Files 460 passed
(460), Tests 7458 passed | 19 skipped; lock VERDICT command-exit 0.
- `pnpm --filter @objectstack/runtime typecheck`: exit 0.
- Lint scoped to the touched package: `eslint packages/runtime
--no-inline-config` exit 0 (the repo-wide `pnpm lint` is CI's).
- Derived gates: `node scripts/pm/dispatch-gates.mjs --commands` on this
diff named 60 families; all 60 ran and exited 0; `--ran` reconciliation:
`60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED`. Two
first-pass exit 3s were container prerequisites, re-run after fixing
them: `check-plugin-teardown-shape.mjs --self-test` (positive-control
commit fetched) and `check:dual-build-cjs-loads` (the seven packages
without `dist/` built: 112 entries across 69 packages load).
- Hand gates: `check-commit-card-trailers.mjs --range origin/main..HEAD`
exit 0, `check-nul-bytes.mjs` exit 0.
- Full dogfood battery (the `(narrowing)` arm), `pnpm --filter
@objectstack/dogfood test` under the lock after its `^...` closure
build: Test Files 314 passed | 3 skipped (317), Tests 2609 passed | 20
skipped (2629); VERDICT command-exit 0.
- `test:repo` (vitest `repo` project): not run locally; the diff touches
no repo-project test and the derived list does not name it; CI Test Core
runs it.
- Merge check against `origin/main` `8eef9a8d`: `git merge-tree` clean;
no open PR for #26708 when this was opened.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No spec change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (scoped to
`packages/runtime`, exit 0; the full sweep is CI's).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
dispatch contract's Chinese summary).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
Written by an os-dev card thread in session
`session_01FEkQ5nX7RrMithjQTtAtVb`, dispatched by the PM loop (server
seat 2).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5nX7RrMithjQTtAtVb
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5nX7RrMithjQTtAtVb)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent a20824f commit 96e7a43
3 files changed
Lines changed: 172 additions & 1 deletion
File tree
- .changeset
- packages/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 | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
119 | 119 | | |
120 | 120 | | |
121 | 121 | | |
122 | | - | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
123 | 126 | | |
124 | 127 | | |
125 | 128 | | |
| |||
1062 | 1065 | | |
1063 | 1066 | | |
1064 | 1067 | | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
| 1076 | + | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
1065 | 1096 | | |
1066 | 1097 | | |
1067 | 1098 | | |
| |||
1699 | 1730 | | |
1700 | 1731 | | |
1701 | 1732 | | |
| 1733 | + | |
1702 | 1734 | | |
1703 | 1735 | | |
1704 | 1736 | | |
| |||
2059 | 2091 | | |
2060 | 2092 | | |
2061 | 2093 | | |
| 2094 | + | |
2062 | 2095 | | |
2063 | 2096 | | |
2064 | 2097 | | |
| |||
0 commit comments