From 218e2be7bcfbe69ca6df685c3394b08dd26382c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 01:45:48 +0000 Subject: [PATCH] docs(cli): correct the unknown-hostname-guard's install-order rationale (#9745) The guard is installed correctly; the stated rationale was not. Name Phase 1 init() as the sufficient condition and drop the "order-independent for matching" clause, and pin the ordering property locally. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WeN7F6jQFpcqW2BN56RdPa --- .../serve-unknown-hostname-guard.test.ts | 78 +++++++++++++++++++ packages/cli/src/commands/serve.ts | 25 +++++- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/serve-unknown-hostname-guard.test.ts b/packages/cli/src/commands/serve-unknown-hostname-guard.test.ts index 88e9872bf4..cb2e250503 100644 --- a/packages/cli/src/commands/serve-unknown-hostname-guard.test.ts +++ b/packages/cli/src/commands/serve-unknown-hostname-guard.test.ts @@ -391,3 +391,81 @@ describe('createUnknownHostnameGuardPlugin — install is soft, never fatal', () expect(h.infos.join('\n')).toContain('installed'); }); }); + +/** + * The install-order property the guard's rationale in `serve.ts` names. + * + * That rationale used to say the middleware intercepts everything "as long as + * it is added before kernel:listening fires". Measured false (#9745): Hono + * composes the handlers a request matched in REGISTRATION order, and + * `kernel:ready` / `kernel:bootstrapped` / `kernel:listening` all fire after + * Phase 2 `start()` — i.e. after every route on the platform is registered — so + * a guard installed from any of those hooks sits behind all of them and refuses + * nothing, with no error and no log. + * + * Nothing else in this file can fail when that claim comes back: every other + * test mounts the guard FIRST via `mountGuard`, which is precisely the case + * that works. So this pins the ordering itself, both directions on ONE app — + * neither direction can be satisfied by the middleware simply never installing: + * + * - `/before` is registered BEFORE the guard's `init()` → guard never runs + * - `/after` is registered AFTER the guard's `init()` → guard refuses + */ +describe('createUnknownHostnameGuardPlugin — install order is the contract', () => { + it('gates only what is registered after it, which is why init() is the requirement', async () => { + const server = new HonoHttpServer(0); + const rawApp = server.getRawApp(); + const asked: string[] = []; + + // A route that already exists when the guard installs. This is the shape a + // guard installed from a lifecycle hook would face for EVERY platform + // route, not an exotic one. + rawApp.get('/before', (c: { text: (body: string, status: number) => Response }) => + c.text('BEFORE', 200)); + + const ctx = { + getService: (name: string) => + name === 'http.server' + ? { getRawApp: () => rawApp } + : name === 'env-registry' + ? { + resolveByHostname: async (host: string) => { asked.push(host); return null; }, + } + : undefined, + logger: { warn: () => {}, info: () => {} }, + }; + + await createUnknownHostnameGuardPlugin({ + rootDomain: ROOT_DOMAIN, + readCloudUrl: () => '', + }).init(ctx); + + // Production's order: route-registering plugins run in Phase 2 `start()`, + // after every plugin's `init()`. + rawApp.get('/after', (c: { text: (body: string, status: number) => Response }) => + c.text('AFTER', 200)); + + const call = (path: string) => + rawApp.fetch(new Request(`http://placeholder${path}`, { + headers: { host: 'nobody.objectos.ai', accept: 'application/json' }, + })); + + // Registered first: it answers and never calls `next()`, so the guard is + // never reached — an unmapped hostname sails straight through, and the + // registry is not even consulted. + const before = await call('/before'); + expect(before.status).toBe(200); + expect(await before.text()).toBe('BEFORE'); + + // Registered after: the guard is ahead of it in the composed chain and + // refuses. Same app, same host, same unmapped environment — registration + // order is the only difference between these two requests. + const after = await call('/after'); + expect(after.status).toBe(404); + expect((await after.json() as RefusalBody).error?.code).toBe('ENVIRONMENT_NOT_FOUND'); + + // The registry was asked exactly once, for the request the guard actually + // saw — the direct evidence that the `/before` hit never reached it. + expect(asked).toEqual(['nobody.objectos.ai']); + }); +}); diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index 9466ffdadb..213930832c 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -3427,10 +3427,27 @@ export const UNKNOWN_HOSTNAME_GUARD_HEALTH_PATHS: readonly string[] = [ * * Implemented as a Plugin so the middleware is wired during init (when * `http.server` is available) and BEFORE start() runs on the Console static - * plugin / route-registering plugins. Hono's `app.use('*')` is order-independent - * for matching, so as long as the middleware is added before kernel:listening - * fires, it intercepts every request regardless of which plugin registered its - * handler. The env-registry is resolved lazily per request on purpose: it is + * plugin / route-registering plugins. Phase 1 `init()` is the REQUIREMENT, not + * a convenience: Hono composes the handlers a request matched in REGISTRATION + * order, so a route registered ahead of this middleware answers and never calls + * `next()`, and the guard never runs for that path. Route registration starts + * in Phase 2 `start()` (createConsoleStaticPlugin mounts the Console there) and + * continues in the `kernel:ready` hooks registered from it (HonoServerPlugin's + * current-user endpoints, plugin-auth's terminal `/api/v1/auth/*`), while both + * kernels run every plugin's `init()` before the first `start()` + * (`LiteKernel.bootstrap`, `ObjectKernel.bootstrap`) — so an install from + * `init()` is ahead of all of it. + * + * ⛔ "Added before kernel:listening" is NOT the condition, however natural it + * reads. `kernel:ready`, `kernel:bootstrapped` and `kernel:listening` all fire + * strictly AFTER Phase 2, so a guard installed from one of those hooks is + * registered behind every route and observes NOTHING — no error, no log, no + * refusal (#9745, measured across three install points). A refusal surface that + * gates nothing is the failure this guard exists to prevent, so anything + * modelled on it installs during `init()`. Both directions are pinned in + * `serve-unknown-hostname-guard.test.ts`. + * + * The env-registry is resolved lazily per request on purpose: it is * registered by ObjectOSEnvironmentPlugin's init, and the guard must not depend * on plugin ordering to work. *