Skip to content

Commit 3df8819

Browse files
authored
chore(tooling): enumerate namespace-claiming wildcard mounts and require each to be classified (#4116) (#4122)
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 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 cost four fixes (#2567, #4018, #4088/#4092, cloud#923) and every one was found by hand, after the fact, by someone reading the code for an unrelated reason. The driven tests #4092 and cloud#923 shipped each pin ONE catch-all and cannot cover the next wildcard someone mounts. What never existed is the ENUMERATION. 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), `exempt` with a reason, `ratchet` naming the issue. AST rather than regex because "does this call next()?" is a question about 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. 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 — 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 a new undeclared catch-all fails as NOT DECLARED.
1 parent 821ac7a commit 3df8819

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)