Skip to content

Commit d527925

Browse files
committed
chore(tooling): enumerate namespace-claiming wildcard mounts and require each to be classified (#4116)
A handler mounted on `<prefix>/*` claims an entire namespace, and Hono runs every handler matching a path in registration order with the first Response winning. So a TERMINAL wildcard — one that always answers, never yields — makes every other route under that prefix reachable only when it happens to register first, and plugin registration order is not a contract anyone declares or tests. That shape has now cost four fixes, and every one was found by hand, after the fact, by someone reading the code for an unrelated reason: - #2567 — anonymous-deny on raw `/data` held only while REST registered first, so a load-order change silently reopened anonymous data access. - #4018 — the standalone `/discovery` sat behind the dispatcher's, so a third publisher drifted unnoticed. - #4088 / #4092 — AuthPlugin's `/api/v1/auth/*` swallowed plugin-hono-server's `/auth/me/permissions`, the console's entire permission layer. - cloud#923 — the same shape in AuthProxyPlugin, which cloud had ALREADY hit in staging (its apps/*/server/index.ts record the Console 404ing on that path). Nothing in CI noticed any of them, and nothing would notice the fifth. The driven tests #4092 and cloud#923 shipped each pin ONE catch-all and cannot cover the next wildcard someone mounts — a test only drives the routes that existed when it was written. What never existed is the ENUMERATION. This adds it, following check-route-envelope.mjs (#3843) including the part that matters most: a site the scan finds but the ledger does not declare is an ERROR, not a default. Three states — `yields` (verified from the AST, so an entry cannot rot: delete the next() and it fails), `exempt` with a reason for a wildcard that is supposed to own its namespace, `ratchet` naming the issue for one that is terminal and should not be. No fourth state where nobody looked. AST rather than regex because "does this call next()?" is a question about the handler's parameter binding: a comment or string mentioning next(), or an inner closure's own next, all fool a textual match in the direction that PASSES while the defect ships, and the parameter is not always named `next`. Handlers passed by identifier are resolved too — cloud#923 mounts its handler that way, and a scan that only understood inline arrows would have called it terminal. Found 13 namespace-claiming mounts where a manual grep had found 3, and with them two real, previously untracked instances of the #4088 defect in `packages/adapters/hono` (`${prefix}/auth/*`, `${prefix}/storage/*`) — ratcheted under #4117. It also separates that file's deliberate `${prefix}/*` ownership (ADR-0076 OQ#9) from the two that merely forgot to yield; in source they look identical. Verified both directions by hand: reverting #4092's fix fails with the terminal diagnostic, and adding a new undeclared `${prefix}/reports/*` catch-all fails as NOT DECLARED. Self-test covers 15 cases, including the three textual-match failure modes and identifier handler resolution. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VZLsKypjrGhiLpio2uWKu
1 parent c20b875 commit d527925

4 files changed

Lines changed: 538 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
---
3+
4+
chore(tooling): enumerate namespace-claiming wildcard mounts and require each to be classified (#4116)
5+
6+
Release-nothing: adds `scripts/check-wildcard-fallthrough.mjs` plus its CI step.
7+
No package code changes.
8+
9+
A handler mounted on `<prefix>/*` claims an entire namespace, and Hono runs
10+
handlers matching a path in registration order with the first Response winning.
11+
So a TERMINAL wildcard — one that always answers and never yields — makes every
12+
other route under that prefix reachable only when it happens to register first.
13+
That shape has cost four fixes (#2567, #4018, #4088/#4092, cloud#923) and every
14+
one was found by hand, after the fact, by someone reading the code for an
15+
unrelated reason.
16+
17+
The two driven tests #4092 and cloud#923 shipped each pin one catch-all, and are
18+
structurally unable to cover the next wildcard someone mounts. What never existed
19+
is the enumeration. This scan is it: three states (`yields`, verified from the AST
20+
so the entry cannot rot; `exempt` with a reason; `ratchet` naming the issue), and
21+
a mount the scan finds but the ledger does not declare is an error, never a
22+
default — the same shape as `check-route-envelope.mjs` (#3843).
23+
24+
It found 13 namespace-claiming mounts where a manual grep had found 3, including
25+
two real, previously untracked instances of the #4088 defect in
26+
`packages/adapters/hono` (`${prefix}/auth/*` and `${prefix}/storage/*`), now
27+
ratcheted under #4117.

.github/workflows/lint.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,19 @@ jobs:
123123
- name: Error-code casing guard
124124
run: pnpm check:error-code-casing
125125

126+
# Namespace-wildcard fall-through guard (#4116). A handler mounted on
127+
# `<prefix>/*` claims the whole namespace, and Hono's first-registered
128+
# handler that answers wins — so a TERMINAL wildcard makes every other
129+
# route under that prefix reachable only by registration luck. That shape
130+
# has now cost four fixes (#2567, #4018, #4088/#4092, cloud#923) and every
131+
# one was found by hand, never by CI. Per-plugin tests cannot cover the
132+
# next one; what was missing is the ENUMERATION. Three states — yields
133+
# (verified from the AST, so it cannot rot) / exempt-with-reason / ratchet
134+
# — and an undeclared mount is an error, never a default. Runs its own
135+
# --self-test first.
136+
- name: Wildcard fall-through guard
137+
run: pnpm check:wildcard-fallthrough
138+
126139
# Release-notes drift guard: the platform is one version-locked train, so
127140
# every released @objectstack/spec major must have a curated, navigable
128141
# release page at content/docs/releases/v<major>.mdx. Catches the gap that

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"check:authz-resolver": "node scripts/check-single-authz-resolver.mjs",
3737
"check:route-envelope": "node scripts/check-route-envelope.mjs --self-test && node scripts/check-route-envelope.mjs",
3838
"check:error-code-casing": "node scripts/check-error-code-casing.mjs --self-test && node scripts/check-error-code-casing.mjs",
39+
"check:wildcard-fallthrough": "node scripts/check-wildcard-fallthrough.mjs --self-test && node scripts/check-wildcard-fallthrough.mjs",
3940
"check:console-sha": "node scripts/check-console-sha.mjs",
4041
"check:release-notes": "node scripts/check-release-notes.mjs",
4142
"check:node-version": "node scripts/check-node-version.mjs"

0 commit comments

Comments
 (0)