Repository navigation
Commit fa6a608
Fixes #23532
Clause-②: no
## What this does
`createHonoApp` (`@objectstack/hono`) has two catch arms: the auth
mount's and the `${prefix}/*` catch-all's around
`HttpDispatcher.dispatch()`. Both end in `thrownJson`. A throw that
`declaredEnvelopeForThrow` does not accept falls to the arm's own
answer, `errorJson(c, err.message || 'Internal Server Error',
err.statusCode || 500)`, and that line shipped the thrown message
without asking the disclosure filter. A driver diagnostic therefore
reached the client.
The fallback now asks `looksLikeInternalErrorLeak`
(`@objectstack/types`) when the status it resolves is 5xx. A flagged
message is replaced by `INTERNAL_ERROR_MESSAGE`, the constant the other
doors ship (`HttpDispatcher.error`, `packages/rest`'s registrars,
`declaredEnvelopeForThrow`'s 5xx limb, and this file's `/auth/config`
arm). No new literal is written. Everything else is unchanged: the
status, the `code`, a message the predicate does not flag, and every 4xx
message. The edit is in `thrownJson` alone, so both arms get it.
`declaredEnvelopeForThrow` (`plugin-hono-server/src/adapter.ts`) is
read, not edited.
## Measured through a real `createHonoApp` (one-shot, not committed)
The rig is the card's own: `packages/qa/http-conformance`'s real
`LiteKernel`, `ObjectQLPlugin` over the in-memory SQLite WASM driver,
the real `HttpDispatcher`, and `@objectstack/hono` aliased to this
checkout's source. Object `ghost` is registered and its table is never
synced. The request is `POST /api/v1/data/ghost`. The probe test file
was written into the worktree, run, and deleted by the script's trap.
Readings were taken at `5407a49e2`, and for "before" the fix line was
reverted through `scripts/ablation-replace.mjs`.
| | Answer |
|---|---|
| Before (filter removed) | `500
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"insert into
[statement and bound values redacted] - no such table: ghost"}}` |
| After (`5407a49e2`) | `500
{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Internal
server error"}}` |
The "before" row is byte-for-byte the answer the card quotes. The throw
reached the fallback undeclared: a declared envelope would have been
answered by `declaredEnvelopeForThrow`, whose own 5xx limb already
filters.
## Mechanism hypotheses, as measured
- **H1, the only unfiltered exit:** confirmed. At `origin/main`
`086fa4640`, `index.ts` has two exits that carry a thrown message. One
is `thrownJson`'s fallback (`:536`, reached from the auth arm's catch at
`:814` and the catch-all's at `:865`). The other is the `/auth/config`
arm (`:746`), which already filters. `errorJson`'s third caller passes
the literal `'Not Found'`.
- **H2, already importable:** confirmed. `looksLikeInternalErrorLeak`
and `INTERNAL_ERROR_MESSAGE` are imported at `index.ts:13-14`. No
dependency was added.
- **H3, the predicate flags the probe:** confirmed on the real string,
through `packages/types/src/error-leak.ts`.
`looksLikeInternalErrorLeak('insert into [statement and bound values
redacted] - no such table: ghost')` is `true`. The bare tail `no such
table: ghost` alone is `true` as well. `boom` is `false`.
- **H4, scoped to 5xx:** adopted, with the same expression as
`HttpDispatcher.error` and `declaredEnvelopeForThrow` (status 500 or
above, and the predicate). A 4xx `statusCode` keeps its message. This is
pinned, and Ablation leg B shows the pin can fail.
## Tests
New file:
`packages/adapters/hono/src/hono-thrown-message-disclosure.test.ts`, 7
cases. `createHonoApp`, both of its arms and `thrownJson` are the real
source. The dispatcher is the package's stubbed runtime, so each case
can hand the arm the throw it needs.
- **Catch-all arm.** The probe answers `500` with body `{ success:
false, error: { code: 'INTERNAL_ERROR', message: INTERNAL_ERROR_MESSAGE
} }`, and `ghost` and `no such table` appear nowhere in it. A plain `new
Error('boom')` keeps `boom`. A probe that declares `statusCode: 503` is
withheld at `503 SERVICE_UNAVAILABLE`. A probe that declares
`statusCode: 400` keeps its message.
- **Auth-mount arm.** An auth service whose `handleRequest` throws the
probe gets the withheld message, and one that throws `boom` keeps it.
The legacy `dispatcher.handleAuth` fallback (no auth service) throwing
the probe is withheld too.
Results:
- `pnpm --filter @objectstack/hono test`: 8 files, 161 tests passed (the
7 new ones included). `pnpm --filter @objectstack/hono typecheck` exited
0 under both programs. `tsc --noEmit -p tsconfig.test.json --listFiles`
lists the new test file.
- The existing pins still pass unchanged. `hono.test.ts` (`returns error
on dispatch exception` for `Service unavailable`, the default-message
case, and the `statusCode: 403` case) and `http-conformance`'s
`hono-declared-envelope.conformance.test.ts` control rows all use
messages the predicate does not flag.
## Ablation (`scripts/ablation-replace.mjs` in wrap mode, plus an outer
`trap … EXIT INT TERM` restore)
The test imports the subject as `./index` (source). No build sits on its
resolution path, so no `dist/` preflight applies.
- **Leg A: the filter removed** (the fallback ships `message` unread).
The anchor hit once, x1 to x0. The blob went `446655b1e9b5` to a mutated
one. Result: **4 failed, 3 passed**. The four withheld pins (catch-all
probe, `statusCode: 503`, auth-service probe, legacy `handleAuth` probe)
went red. The two plain controls and the 4xx pin stayed green. Received
body: `"message": "insert into [statement and bound values redacted] -
no such table: ghost"`.
- **Leg B: the 5xx bound removed** (the filter runs at every status).
Result: **1 failed, 6 passed**. Only the 4xx pin went red: it expected
the probe and received `Internal server error`.
- Restore was proven per leg: the blob equals HEAD (`446655b1e9b5`) and
`git diff HEAD` is empty.
- The first attempt was a no-op and was discarded. Its replacement text
(`message,`) already occurred in the anchor, so `ablation-replace`
refused on "replacement count rose 0, not 1" and restored the file
before the child ran. The legs above re-ran with replacements carrying a
unique marker.
## Gates (all at `5407a49e2`, the head of this PR)
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 63 commands. Each exit code was
captured to a file before any pipe. All 63 ended at exit 0. Three first
returned exit 3 (PREREQUISITE NOT MET) and were re-run once their
prerequisite was met:
- `node scripts/check-plugin-teardown-shape.mjs --self-test` needed its
pinned control commit (`621a487607`) in this shallow clone. After `git
fetch --depth=1` of that commit, it exited 0 (48 cases).
- `pnpm check:type-check-debt` first timed out in the shared verify
lock's queue (a sibling's run held it for 17 minutes). On re-run: "1
ledger entr(ies) re-measured … none above its recorded number".
- `pnpm check:dual-build-cjs-loads` needs the whole tree's `dist/`. The
re-measure above built `./packages/*`, and the re-run passed: "110
published require entry point(s) across 68 package(s) load".
- `node scripts/pm/dispatch-gates.mjs --ran ran.list`: "63 derived, 63
run, 0 NOT-MEASURED, 0 UNRUN".
- Lint, narrowed to the diff's two `.ts` files: `eslint
--no-inline-config` linted 2 files with 0 errors and 0 warnings.
Positive control: a stdin file in `packages/adapters/hono/src/`
importing a root namespace from `@objectstack/spec` exits 1
(`no-restricted-imports`). Repo-wide `pnpm lint` is CI's run.
- The hono `dist/` built at this head loads as CJS and as ESM
(`createHonoApp` is a function in both) and carries the new expression.
## Changeset
`.changeset/23532-hono-undeclared-500-leak-filter.md`: `patch` for
`@objectstack/hono`. It is a bug fix in a published package, adds no
export, and changes no status or code.
## Acceptance notes
- Out of scope, untouched: the standing tiering of an undeclared 5xx's
legibility. A bare `Error` whose message does not look like a dump still
names itself, as the card's acceptance requires. Also untouched: the
declared-5xx disagreement between the Hono doors and the dispatcher's
own exit. It stays where PR #23530's acceptance notes put it.
- Observation, not filed: in the rig, the unsynced-table insert left the
engine as an undeclared bare `Error`. That is why it reached this
fallback rather than the declared limb. The predicate's own header
prefers that a producer declare its fault (`code` plus a 5xx `status`)
over phrasing heuristics. Whether the insert path should declare is the
producer's question, not this door's. Carrier: none.
- `main` gained `b57228913` and `7ffc57846` after the base. They touch
`packages/spec` tests and `packages/metadata-protocol`, nothing this
diff touches, so the branch was not merged. CI runs on the merge ref,
and the merge queue rebuilds on `main`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG)_
Co-authored-by: Claude <noreply@anthropic.com>
1 parent cdc42eb commit fa6a608
3 files changed
Lines changed: 198 additions & 1 deletion
File tree
- .changeset
- packages/adapters/hono/src
| 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 | + | |
| 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 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
529 | 529 | | |
530 | 530 | | |
531 | 531 | | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
532 | 552 | | |
533 | 553 | | |
534 | 554 | | |
535 | 555 | | |
536 | | - | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
537 | 563 | | |
538 | 564 | | |
539 | 565 | | |
| |||
0 commit comments