Repository navigation
Commit f7546a4
fix(plugin-hono-server)!: the CORS default admits no cross-origin caller, and '*' with credentials is refused at boot (#24806)
Fixes #24738
Clause-②: no (narrowing)
## What changes
The CORS middleware that `HonoServerPlugin` (the server `os serve`, `os
dev` and `os start` mount) installs by default now admits only the
origins it is told about. The governing text is `docs/HARDENING.md` §
CORS: "Do **not** use `origin: '*'` with `credentials: true`", and the
go-live checklist's "CORS origin list is explicit, not `*`".
- **Old default.** With no origin list configured (no `cors.origins`
option, no `OS_CORS_ORIGIN`), every origin was admitted for credentialed
cross-origin requests. A configured `'*'` with credentials on was
accepted and answered the same way.
- **New default.** With no list, no cross-origin caller is admitted. A
loopback origin (`localhost`, `127.0.0.1`, `[::1]`, any port) is
admitted on a development boot only: `NODE_ENV` exactly `development`,
read through `isDevelopmentBoot` from `@objectstack/core`, the same
reading the auth secret's development fallback uses.
- **`'*'` with credentials** is refused in `init()`.
`kernel.bootstrap()` rethrows an `init()` throw, so the boot fails, and
the message names both remedies (`OS_CORS_ORIGIN`,
`OS_CORS_CREDENTIALS=false`). This is the precedent of
`@objectstack/plugin-dev`'s production guard (`assertNotProduction`,
ADR-0115 D6), which refuses the same way from `init()`. Triage leaned to
a loud refusal, and the transport has no non-credentialed fallback that
would keep the operator's intent.
Docs now state the default as it is: `docs/HARDENING.md` § CORS only,
the CORS rows of `environment-variables.mdx`, one CORS step in
`self-hosting.mdx` and `production-readiness.mdx`, and the plugin
README. Two doc lines called `OS_TRUSTED_ORIGINS` a CORS list. That was
never what the transport read, and under the new default it would send
an operator to the wrong variable, so both lines are corrected.
## Contract
Every input whose answer changes. "Admitted" means the origin is echoed
in `Access-Control-Allow-Origin`.
| Input | Before | After |
| --- | --- | --- |
| No list, credentials on (the default), not a development boot | every
origin admitted, with credentials | no cross-origin caller admitted |
| No list, credentials on, development boot | every origin admitted,
with credentials | loopback origins only, with credentials |
| No list, `OS_CORS_CREDENTIALS=false`, not a development boot | every
origin admitted, without credentials | no cross-origin caller admitted |
| No list, `OS_CORS_CREDENTIALS=false`, development boot | every origin
admitted, without credentials | loopback origins only, without
credentials |
| A list holding `*` (option or `OS_CORS_ORIGIN`, alone or among other
entries), credentials on | booted; every origin admitted, with
credentials | `init()` throws; the boot fails |
Unchanged:
- A list without `*`, exact origins or patterns, is admitted as before,
with or without credentials. It still admits loopback origins alongside
it: that is the pattern matcher's existing convenience, and this PR does
not touch it.
- `*` with credentials off still boots and admits any origin without
credentials.
- `cors: false`, `cors.enabled: false` and `OS_CORS_ENABLED=false` still
mount nothing.
- Allowed and exposed headers, methods and max-age are unchanged.
Preflights are still answered `204`, admitted or not.
- Same-origin requests are outside CORS, so the Console under
`/_console` and every page the server itself serves are unaffected.
- No export, option, type or spec key moves. `HonoCorsOptions` gains
TSDoc only.
## Mechanism (measured)
- The before-state was measured on `main`. The new pin file ran against
`main`'s source (commit `b1bb0d25c`: `795ff708d` plus the test file
only) and gave `Tests 8 failed | 4 passed (12)`. The eight failures were
a foreign origin echoed back with credentials on, a loopback origin
echoed outside development, and `init()` resolving for `'*'` with
credentials. The four that passed are the preservation pins.
- No `serve.ts` seam was needed. The host's own origin needs no CORS
admission, because a browser applies CORS to cross-origin requests only.
The configured list is the one the transport already reads
(`cors.origins`, else `OS_CORS_ORIGIN`). The transport reads
development-ness through `isDevelopmentBoot`, and `serve` sets
`NODE_ENV=development` under `--dev` before any plugin initialises. No
second copy of auth's trusted-origin rules was added.
- **Consumers in this repo.** Three tests leaned on the old default, and
each now names its configuration (tests only; no example and no shipped
code depended on it):
-
`packages/runtime/src/http-metrics-inbound-coverage.hono.integration.test.ts`
configured `'*'` with credentials, which the boot now refuses. Measured
before its fix: `Plugin init failed: com.objectstack.server.hono` and 1
failed of 27. It now names the origin its preflight carries.
-
`packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts`
and `hono-webhooks-redeliver.conformance.test.ts` are door-parity pins,
and they compared the two doors' CORS headers by accident. Both doors
now run with `cors: false`, as the suite's other parity pins already do.
- Every other test that boots `HonoServerPlugin` with CORS on drives it
from a non-browser client (Node's fetch or Hono's `app.request`), and
neither enforces CORS. The only assertions on
`Access-Control-Allow-Origin` in this repo are `@objectstack/hono`'s
own, for its own default (see Acceptance notes).
`examples/embed-objectql` mounts no HTTP server. The showcase e2e runs
same-origin under `--dev`. objectui's console reaches the API
same-origin: through its Vite `/api` proxy in dev, and with
`VITE_SERVER_URL` empty in both `.env` files (read-only, `../objectui`
at `cd4623d06`).
## Tests
All runs below are on `f7cb52d15`, the head this PR opens with, unless a
line names another commit.
- `pnpm --filter @objectstack/plugin-hono-server exec vitest run
--maxWorkers=2 src/cors-default-admission.test.ts`: 12/12 passed. On
`main`'s source the same file gave 8 failed and 4 passed, as above. The
pins, by what they assert:
1. With no origin list, a foreign origin and (outside development) a
loopback origin get no `Access-Control-Allow-Origin`, on a request and
on a preflight, and the request itself is still answered.
2. A configured origin, by option or by `OS_CORS_ORIGIN`, is admitted
with credentials, and an unlisted one is not.
3. The default admits loopback origins when `NODE_ENV` is `development`,
not when it is unset or `test`.
4. `'*'` with credentials is refused in `init()` from every source, and
the kernel's bootstrap rethrows it. `'*'` without credentials boots.
- `pnpm --filter @objectstack/plugin-hono-server test`: 35 files, 434
tests passed.
- `pnpm --filter @objectstack/plugin-hono-server typecheck`: exit 0. The
test layer compiles, and `tsc -p tsconfig.test.json --listFiles` lists
the new file.
- `pnpm --filter @objectstack/http-conformance exec vitest run
--maxWorkers=2`: the whole suite, 18 files and 228 tests, passed.
- `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2` on
`http-metrics-inbound-coverage.hono.integration.test.ts` and
`dispatcher-plugin.rate-limit.integration.test.ts`: 2 files, 35 tests
passed.
- Consumers of the default through the built `dist/`, all green. They
were measured on the tree committed as `8c038d76d`, before the last
merge of `main`, which touched none of these paths:
- the twelve `@objectstack/runtime` test files that boot
`HonoServerPlugin` with CORS on: 123 tests;
- the six `@objectstack/client` files that do: 30 tests.
## Gates
- `node scripts/pm/dispatch-gates.mjs --commands` derived 98 families
for this diff at `f7cb52d15`. All 98 ran, each exit 0. `--ran`
reconciliation: `98 derived, 98 run, 0 NOT-MEASURED, 0 UNRUN`. Among
them:
- `check:adr-0087-registration --base origin/main`: one
declared-breaking changeset, `not-required (no-migration-prescription)`;
- `check:changeset-no-major`, `check:doc-anchors`,
`check:doc-authoring`, `check:doc-security-posture`, `check:nul-bytes`,
`check:published-files`;
- `check:dual-build-cjs-loads`, `check:type-check-coverage` and
`check:type-check-debt`.
- `pnpm lint` on `f7cb52d15`: exit 0.
- CI check-runs: pending at the time of writing.
## Changeset
`.changeset/24738-cors-default-no-cross-origin-caller.md` bumps
`@objectstack/plugin-hono-server` as `minor` (the package is in the
`fixed` group, pre-release `next`). It is marked **BREAKING**: an
accept-set narrowing shipped as `minor` under the launch-window
convention, the same form the narrowing changesets already on `main` use
(for example `24341-extend-default-row-scope-refused.md`). It carries
the `Clause-②` line, the ADR-0087 disposition and what an operator does.
`check-changeset-no-major` forbids `major`. Whether this enters 18.0.0
is the maintainer's call (triage), and this PR does not decide it.
## Acceptance notes
- **Sibling default, not changed here.** `@objectstack/hono`'s
`createHonoApp` (`packages/adapters/hono/src/index.ts`, its CORS block)
carries the same default. It sits outside this card's file surface, so
it is handed to the seat to file, not widened into this PR.
`HARDENING.md` now tells a host composing it to name its origins
explicitly.
- **Loopback with a configured list.** A configured list admits loopback
origins in every mode (`matchOriginPattern`). The card's "localhost in
dev only" is scoped to the default, so this is reported, not changed.
- **Held-off doc line.** `content/docs/deployment/cli.mdx` (about
`:523`) still calls `OS_TRUSTED_ORIGINS` "(CORS)". PR #24681 holds that
file, so whichever lands second should correct it.
- **Spec default.** `CorsConfigSchema` in spec (`shared/http.zod.ts`)
declares `origins` default `'*'` with `credentials` default `false`. The
transport does not read it (spec lane, held off). The future
`server.cors` admission should carry this default.
- **What the refusal covers.** It covers a literal `*` entry. A
degenerate pattern such as `https://*` is not refused.
- **`HARDENING.md` ordering.** #24740 rewrites other sections of the
same file. Triage asks the two to land serially.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent bd6dab0 commit f7546a4
12 files changed
Lines changed: 394 additions & 50 deletions
File tree
- .changeset
- content/docs/deployment
- docs
- packages
- plugins/plugin-hono-server
- src
- qa/http-conformance/src
- runtime/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 | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
126 | | - | |
127 | | - | |
| 126 | + | |
| 127 | + | |
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
118 | 118 | | |
119 | 119 | | |
120 | 120 | | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
121 | 126 | | |
122 | 127 | | |
123 | 128 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
411 | 411 | | |
412 | 412 | | |
413 | 413 | | |
414 | | - | |
| 414 | + | |
415 | 415 | | |
416 | 416 | | |
417 | 417 | | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
418 | 425 | | |
419 | 426 | | |
420 | 427 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
192 | 192 | | |
193 | 193 | | |
194 | 194 | | |
195 | | - | |
196 | | - | |
197 | | - | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
198 | 223 | | |
199 | 224 | | |
200 | 225 | | |
| |||
209 | 234 | | |
210 | 235 | | |
211 | 236 | | |
212 | | - | |
213 | | - | |
214 | | - | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
215 | 241 | | |
216 | 242 | | |
217 | 243 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
38 | 56 | | |
39 | 57 | | |
40 | 58 | | |
| |||
79 | 97 | | |
80 | 98 | | |
81 | 99 | | |
82 | | - | |
| 100 | + | |
83 | 101 | | |
84 | 102 | | |
85 | | - | |
| 103 | + | |
86 | 104 | | |
87 | 105 | | |
88 | | - | |
| 106 | + | |
89 | 107 | | |
90 | 108 | | |
91 | | - | |
92 | | - | |
| 109 | + | |
| 110 | + | |
93 | 111 | | |
94 | 112 | | |
| 113 | + | |
| 114 | + | |
95 | 115 | | |
96 | 116 | | |
97 | 117 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
128 | 142 | | |
129 | 143 | | |
130 | 144 | | |
| |||
144 | 158 | | |
145 | 159 | | |
146 | 160 | | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
147 | 165 | | |
148 | 166 | | |
149 | 167 | | |
| |||
0 commit comments