Uh oh!
There was an error while loading. Please reload this page.
test(cli): a test seam for os serve's unknown-hostname guard — refusal literal stays inline (#9442) - #9583
Conversation
The OS_ROOT_DOMAIN guard was a plugin object literal built inside
Serve.run(), closing over its locals and installing itself on a
`http.server` service resolved from the plugin context. Nothing about it
was exported or constructible, so reaching any of it meant booting a real
`os serve` — and every branch went untested, including the health/
readiness bypass whose own comment says a 404 there "would kill the
container".
Extract the SEAM, not the refusal: `createUnknownHostnameGuardPlugin()`
is exported from serve.ts the way its sibling helpers are, with run()
calling it. The middleware is byte-identical modulo indentation and five
declared rebinds (`__rootDomain` → `rootDomain`, the reserved set and the
health-path list to exported constants, and `process.env.OS_CLOUD_URL` to
an injectable reader that is still called PER REQUEST). The
`c.json({ ... }, 404)` refusal body stays an INLINE OBJECT LITERAL:
check-route-envelope.mjs judges the literal passed to `c.json(...)`, so
hoisting it would leave the file discovered while every counter read
zero — the #9364 conformance pin would go vacuous with the gate green.
The new suite mounts the real middleware on a real Hono app and pins both
directions: every bypass as an explicit pass-through (a sentinel 200,
never "not a 404" — Hono's own unmatched answer is a 404 too), and the
refusal by error.code AND HTTP status together.
The slot-lookup ratchet moves DOWN 10 → 9 for serve.ts: the
`const guardPlugin: any = { … }` declarator no longer contains a
service-lookup call, so one erasure site is gone. Hand-edited by one
number rather than regenerated.
Co-Authored-By: Claude <noreply@anthropic.com>`rawApp.fetch()` is typed `Response | Promise<Response>`, so the harness returned something the declared `Harness` interface did not accept. `pnpm --filter @objectstack/cli typecheck` now exits 0. Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9a0a4409ec041d4210042f4bedf57730bdf07bfd && git checkout 9a0a4409ec041d4210042f4bedf57730bdf07bfd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a0d24bf8813b38e2bd17bc626186296c3bf255bc 89b8cfd4a59f8234191c6d16393df8db4ef872c2 && git checkout -B drift-repro a0d24bf8813b38e2bd17bc626186296c3bf255bc && git merge --no-ff 89b8cfd4a59f8234191c6d16393df8db4ef872c2
node scripts/docs-audit/affected-docs.mjs --json a0d24bf8813b38e2bd17bc626186296c3bf255bc
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9442
What was missing
os serve'sOS_ROOT_DOMAINunknown-hostname guard was a plugin object literalbuilt inside
Serve.run(), closing over__rootDomain,RESERVED,getEnvRegistryandprocess.env.OS_CLOUD_URL, installing itself on ahttp.serverservice resolved from the plugin context. Nothing about it wasexported or constructible, so reaching any of it meant booting a real
os serve— and nothing did. Every branch was untested, including the one whoseown comment says a 404 there "would kill the container".
What changed
createUnknownHostnameGuardPlugin()is now exported fromserve.ts, the way itssibling helpers (
resolveTenancyPostureOrRefusal,resolveStorageCapabilityArg,…) already are, with
run()calling it. That is the established extractionpattern in this file, and it is applicable here because it keeps the guard — and
its refusal literal — inside
serve.ts.Pure extraction, proved rather than asserted. Normalizing indentation and
applying the five declared rebinds to the pre-change source makes the two
middleware bodies identical over all 177 significant lines, and the embedded
404 HTML page is byte-identical (2089 bytes both sides). The five rebinds:
__rootDomainrootDomaintrim().toLowerCase(), which is idempotent on the call site's already-normalized inputconst RESERVED = new Set([…])UNKNOWN_HOSTNAME_GUARD_RESERVED_SUBDOMAINSp === '/api/v1/health' || …HEALTH_PATHS.includes(p)process.env.OS_CLOUD_URLreadCloudUrl()const guardPlugin: any = { … }return { … }from the factoryZone 1, condition 1 — the refusal literal stayed inline, and here is how I know the pin still bites
"CI is green" is necessary and not sufficient, so I measured the pin in both
directions on the post-change tree:
Vacuity probe (what condition 1 forbids). I hoisted the refusal body into a
local
const refusalBodyand leftc.json(refusalBody, 404)in place, then rannode scripts/check-route-envelope.mjs: exit 0, still "11 module(s) audited".The file stays discovered —
bodiescounts thec.json(call — while all sixcounters read nothing, so the
{}entry keeps passing and stops asserting.Exactly the silent vacuity the card describes. Reverted.
Non-vacuity probe (the one that proves the pin sees this file). With the
literal inline as shipped, I injected the pre-#9364 dialect into it
(
{ error: 'environment_not_found', message, hostname }) and re-ran the gate:Red, naming this file, with the counters moving. Reverted; the tree is back to
the committed state. A defect injected into the shipped literal turns the gate
red, which is what "the #9364 pin still pins" means.
The tests, and why they pin both directions
packages/cli/src/commands/serve-unknown-hostname-guard.test.ts— 34 tests. Theymount the real middleware on a real Hono app (
HonoHttpServer, already adependency of this package and the same class production resolves as
http.server), driving it withapp.fetch(new Request(…)). No mock of themiddleware anywhere.
A suite that only asserted the refusal would stay green if the guard started
refusing everything — the container-killer the card warns about. So:
error.codeand HTTP status together, plussuccess: false, the message,error.details.hostname, and that the body hasno top-level key besides
successanderror; thetext/htmlvariant too;200 PASSED_THROUGHfrom a sentinel route mounted after the guard, never "not a 404" (Hono's own
unmatched answer is a 404 and would read as a refusal): a mapped hostname,
every reserved subdomain and the apex, every health/readiness path,
/_adminand
/.well-known/*, non-platform hostnames, and all four env-registryfailure modes;
constants the middleware itself branches on, so a path added to the guard is
covered the moment it is added rather than the day someone copies it here.
Break it and watch it go red — predicted vs actual
Both ablations were applied on top of the committed implementation and reverted
with
git checkoutafterwards (git statusclean, back at751ee771c).expected 404 to be 200; the reserved-host/api/v1/healthcase stays green because it returns at the reserved branch firstAssertionError: expected 404 to be 200expected 200 to be 404Ratchet moved DOWN, by one number, by hand
scripts/slot-lookup-baseline.json:packages/cli/src/commands/serve.ts10 → 9.The old
const guardPlugin: any = { … }declarator containedctx.getService?.('http.server')inside its initializer, which is one of theerasure sites the rule counts; the declarator now initializes from a factory call
and contains no lookup.
pnpm check:slot-lookupdemanded the ratchet-downitself ("erasure count fell 10 → 9 — ratchet DOWN"). Edited as a single number
rather than regenerated with
--update, so nothing else in the baseline couldmove; the diff is one line.
Verification
Union re-run at
89b8cfd4a(the final commit):pnpm --filter @objectstack/cli typecheck— exit 0vitest run src/commands/serve-— 14 files, 152 tests passed (the new 34plus every sibling that imports
serve.ts)check:route-envelope·check:slot-lookup·check:query-options-erasure·check:engine-double-contract·check:where-matcher·check:type-check-coverage·check:cross-package-test-inputs·check:nul-bytes·check:objectui-changeset·check:changeset-gate-self-tests— all passcheck-empty-changeset·check-changeset-no-major·check-adr-0087-registration— pass on the committed diffThe whole
packages/clisuite andcheck:type-check-debt --re-measure(whichneeds the entire workspace closure built) are left to CI's
Test CoreandLint & Repo Gates.Generated by Claude Code