Repository navigation
Commit cb9841a
fix(hono,runtime)!: createHonoApp keeps a repeated query name as an array, so the dispatcher refuses it; /packages refuses a repeated keepData or overwrite (#23466)
Fixes #23214
Clause-②: yes (narrowing)
Slice 2 of this card, built under seat ruling `6165288468` (Q1 B, Q2 A,
Q3 A) and claim `6165302054`. Slice 1 (PR #23263, landed as
`d45d0e938a`) guarded the dispatcher's read points. This PR guards the
two `/packages` write flags that slice 1's census did not name, then
flips the adapter: the order "guard first, then flip" is met inside one
PR.
## What changes
**`@objectstack/hono`: the catch-all keeps a repeated query name.**
`createHonoApp`'s `${prefix}/*` catch-all built the dispatcher's query
with `URLSearchParams.forEach` into an object, so `?package=a&package=b`
reached `HttpDispatcher.dispatch()` as `'b'`. Every arity rule the
dispatcher declares was unreachable on a `createHonoApp` host. It now
reads the query through `readDispatchQuery`, a module-private mirror of
`@objectstack/plugin-hono-server`'s `readQuery` (that one is
module-private, and that package's `exports` has only `.`). It uses
`c.req.queries()` with the length normalisation (one value stays a
string, a repeat is the array) and `Object.fromEntries`, never a dynamic
write, so `?__proto__=x` is an own key and no prototype moves. The
docblock names the twin and the #6878 ruling. The catch arms are
untouched; #23365 is not addressed here.
**`@objectstack/runtime`: `/packages` refuses a repeated `keepData` or
`overwrite`.** Both were read as `query?.NAME === 'true'`, so a repeat
read as absent:
- `DELETE /packages/:id?keepData=true&keepData=true` answered `200` and
`deletePackage` received no `keepData`. The protocol keeps object
storage only for `keepData === true`, so the tables the caller asked to
keep were dropped. This was measured on a `plugin-hono-server` host
before this PR, through the explicit `/packages` mount. Today's
`createHonoApp` keeps them (last value), so the flip alone would have
carried the drop to a second door.
- `POST /packages?overwrite=true&overwrite=true` answered `409` "already
exists" for a package one `overwrite=true` replaces.
Both now refuse a repeat with `400 VALIDATION_ERROR` and
`repeatedQueryParamMessage`, as the domain already refuses `enabled` and
`version`. Each refusal sits after the two authorization gates (the
capability gate and the wall) and before any registry or protocol call.
A one-element array is one occurrence and is unwrapped in place, as the
`status`/`type` rule does (`refuseRepeatedWriteFlag`, module-private).
## The order lesson, re-measured
- On `origin/main` `823da2f21c`, a static census of every non-comment
query read in the 22 domain modules and the `@objectstack/rest` helpers
they call, plus a host map and empirical `dispatch()` probes with the
post-flip array shape (os-dev-report `6165233131`), found:
- no read point added since slice 1;
- two unguarded `/packages` write flags, the ones this PR guards.
- Re-checked at `61b12584f0`: the 5 commits since `823da2f21c` touch
none of `packages/runtime`, `packages/rest`, `packages/adapters`,
`plugin-hono-server` or `http-conformance` (diff stat: 0 lines).
- #23265 (in flight in `domains/data.ts`, a different file) adds a
`DELETE /data/:object/:id ?expectedVersion` read point that refuses a
repeat. It is not on `main` and is not part of this PR.
## Pins
- `packages/adapters/hono/src/hono-query-multiplicity.test.ts` (15
tests, over the package's recording stub):
- a repeated name reaches `dispatch()` as the array of its values;
- nine single-valued inputs (plain, `+`, `%20`, a UTF-8 escape, a
percent-encoded key, an empty value, a bare flag, a comma list, none)
arrive exactly as the OLD line produced them, compared against a
reference Hono app running that line;
- `__proto__`, once and repeated, is an own key with `Object.prototype`
untouched.
-
`packages/qa/http-conformance/src/hono-query-multiplicity.conformance.test.ts`
(7 tests): a REAL `createHonoApp` over the REAL `HttpDispatcher`,
composed as its six `hono-*` siblings are. It lives here because
`packages/adapters/hono/vitest.config.ts` aliases `@objectstack/runtime`
to a stub for that whole suite. It pins:
- `GET /meta/app/crm?package=a&package=b` refused `400` before
`getMetaItem`;
- `GET /packages` with a repeated `status` or `enabled` refused `400`;
- `DELETE /packages/:id?keepData=true&keepData=true` refused `400` with
`deletePackage` never called;
- a single-value control for each, asserting what the service received.
Refusals assert status, nested `error.code` `VALIDATION_ERROR`, and the
named parameter with its count ("supplied 2 times"). This is not
`query-multiplicity.conformance.test.ts`, which is untouched (ruling
item 3).
- `packages/runtime/src/domains/packages-write-flag-arity.test.ts` (11
tests, through `dispatch()` over a real `SchemaRegistry`):
- three `keepData` repeat shapes refused, with neither `deletePackage`
nor `registry.getPackage` called;
- one `keepData` (`'true'`, `'1'`, `['true']`, absent) reaches
`deletePackage` as `{ packageId, keepData: true }` or `{ packageId }`;
- a repeated `overwrite` refused, with neither `installPackage` nor
`registry.getPackage` called;
- one `overwrite` (`'true'`, `['true']`) overwrites with `201`;
- a control: no `overwrite` is still `409`.
## Ablations (one-time proofs; no permanent test files)
1. **The flip.** `node scripts/ablation-replace.mjs` (WRAP mode, restore
on exit) put the old `forEach` construction back in
`packages/adapters/hono/src/index.ts`.
- Anchor x1 → x0, blob `4089af0a973d` → `6948cd6354ba`.
- No build leg: the hono pin file imports `./index` from source, and
http-conformance aliases `@objectstack/hono` (`/^@objectstack\/hono$/`)
to `src/index.ts`.
- Hono pin file: **6 failed, 9 passed**. The 4 array-shape and 2
`__proto__` cases went red; all 9 single-value controls stayed green.
- Conformance file: **4 failed, 3 passed**. Every refusal went red;
every control stayed green.
- Restore: blob == HEAD (`4089af0a973d`), `git diff HEAD` empty.
2. **The `keepData` guard.** The guard call in `domains/packages.ts` was
disabled behind a marker property, in HOLD mode inside a script with an
EXIT/INT/TERM trap.
- Anchor x1 → x0, blob `20fc9b7cd2c2` → `ecba3c0a4be4`, then `pnpm
--filter @objectstack/runtime build`.
- `ablation-dist-preflight` found the marker in 2 built files. The
conformance file reads runtime through `dist/`.
- Runtime pin file: **4 failed, 7 passed**. The three `keepData`
refusals went red, plus the one-element-array preservation (the unwrap
is the guard's own); every `overwrite` case stayed green.
- Conformance file: **1 failed, 6 passed** (the `keepData` refusal).
- Restore: blob == HEAD (`20fc9b7cd2c2`), rebuild, `--absent` preflight
(marker absent from all 6 built files; whole tree clean).
## Tests and gates (head `99c7c050b2`, after merging `origin/main`
`7b3d5585ca`)
All runs went through `os-verify-lock` (slot `issue-23214-s2`), each
with `VERDICT command-exit 0`.
- **Post-merge, at `99c7c050b2`:**
- closure rebuild `turbo run build
--filter='@objectstack/http-conformance^...' --concurrency=2`: 36/36
tasks;
- runtime `packages-write-flag-arity.test.ts` plus slice 1's
`dispatcher-query-arity.test.ts`: 44 passed;
- `@objectstack/hono` test: 7 files / 154 passed, and typecheck exit 0
(both configs; the test files are under `src/**`);
- `@objectstack/http-conformance` test: 12 files / 124 passed, and
typecheck exit 0 (test-layer ledger held at 3 files / 27 errors / 10
signatures);
- `@objectstack/runtime` typecheck: exit 0, ledger held at 27 / 190 /
68;
- `pnpm --filter @objectstack/spec check:generated`: all 14 artifacts up
to date.
- **Runtime full local suite** at `e5da6a04aa` (runtime source identical
to this head): 374 files, 5466 passed / 19 skipped.
- **Gate union:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths) derives 63 commands at
`99c7c050b2`. All 63 were run, each exit captured before any pipe: **63
exit 0**. `--ran`: "63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN". On a
pre-merge head the union reddened `check-registry-log-declared`, because
the conformance file constructed a `SchemaRegistry` in a suite that
declares no registry log level; the rows now use the three-method
double.
- **Lint**, as the proven narrowing over the 5 changed `.ts` files:
1. Population: `eslint.config.mjs:971`'s block covers them all, and none
is ignored.
2. Count: `--format json` with `--no-inline-config` gives 0 errors, 0
warnings and 0 fatal (eslint exit 0).
3. Invariance: no type-aware linting (`:328`), so no untouched file's
verdict can move.
## Acceptance notes
- **Excluded by ruling Q3 → A:** each of these already answers a repeat
exactly as its canonical, already array-fed twin does, so the flip opens
no divergence.
- P3 `GET /analytics/meta ?cube` (typeof: a repeat reads as absent, an
unscoped list). Twin: the `dispatcher-plugin` `/analytics/meta` mount.
- P4 the share-link list (`?object`, `?recordId`, `?includeRevoked`) and
resolve (`?email`, `?password`) names (typeof or `=== 'true'`: a repeat
reads as absent). Twin: `plugin-sharing`'s share-link routes.
- **One malformed input moves the other way.** A query pair with an
EMPTY name (`?=x`) is dropped by hono's parser, so it no longer reaches
the dispatcher. Measured through a real `createHonoApp`: `GET
/api/v1/meta/app/crm?=x` was `400 VALIDATION_ERROR` ('The "" query
parameter is not supported…') with the old line, and is `200` now.
`plugin-hono-server`'s `readQuery` already drops it the same way, so
this converges the two Hono doors. It is stated in the changeset. The
ruled `Clause-②: no (narrowing)` line is kept; whether this input
changes that line is the seat's to say.
- **Multi-valued names.** Names such as `select` and `expand` now reach
the dispatcher as arrays on a `createHonoApp` host, exactly as
`RestServer` hands them to `findData` / `getData` through
`plugin-hono-server`.
- **Not changed in this PR:**
- The domain's inline `status`/`type` loop (slice 1) could adopt
`refuseRepeatedWriteFlag`; it is outside this claim's surface.
- Slice 1's changeset (`.changeset/23214-dispatcher-query-arity.md`,
unreleased) still says the refusals are reachable only through
`dispatch()` until this adapter keeps repeats. It is outside this
claim's surface; this PR's changeset states the new reach.
- **hono version.** `c.req.queries()`'s null-prototype record is what
keeps `__proto__` as an own key on the installed hono 4.13.12. On the
peer-range floor 4.12.8 (read statically), hono's own parser drops a
`__proto__` name, so the pin is a reading of the installed version. No
prototype is polluted on either.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 584e737 commit cb9841a
6 files changed
Lines changed: 516 additions & 3 deletions
File tree
- .changeset
- packages
- adapters/hono/src
- qa/http-conformance/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 | + | |
| 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 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
328 | 328 | | |
329 | 329 | | |
330 | 330 | | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 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 | + | |
331 | 369 | | |
332 | 370 | | |
333 | 371 | | |
| |||
784 | 822 | | |
785 | 823 | | |
786 | 824 | | |
787 | | - | |
788 | | - | |
789 | | - | |
| 825 | + | |
| 826 | + | |
790 | 827 | | |
791 | 828 | | |
792 | 829 | | |
| |||
0 commit comments