Repository navigation
fix(plugin-hono-server)!: the CORS default admits no cross-origin caller, and '*' with credentials is refused at boot - #24806
Conversation
…lass Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…ler; '*' with credentials is refused at init With no origin list configured (no cors.origins, no OS_CORS_ORIGIN), the transport's CORS answer now admits no cross-origin caller; a loopback origin is admitted on a development boot only. A list holding '*' while credentials are on is refused at init, so the boot fails naming both remedies. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…n operator sets HARDENING.md § CORS, the deployment pages and the plugin README now state what the CORS answer admits with and without an origin list, that '*' with credentials is refused at boot, and that OS_TRUSTED_ORIGINS is auth's list, not CORS's. Changeset for @objectstack/plugin-hono-server. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…rs-default-no-foreign-credentialed-origin
…guration The runtime metrics pin configured '*' with credentials, which the transport now refuses at boot; it names the origin its preflight carries. Two http-conformance parity pins compared the doors' CORS headers by accident; both doors now run with CORS off, as the suite's other parity pins do. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…cisely Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…rs-default-no-foreign-credentialed-origin
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 9 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 31d0919f6748707c7a2ff6940c5f2a1d79c64d5a && git checkout 31d0919f6748707c7a2ff6940c5f2a1d79c64d5a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 99e4eaa227e0375b59c7bbdf6e36ad7f026bb22d dbdd7fe6a89302192e82d4b484e0d5a1e65a9db6 && git checkout -B drift-repro 99e4eaa227e0375b59c7bbdf6e36ad7f026bb22d && git merge --no-ff dbdd7fe6a89302192e82d4b484e0d5a1e65a9db6
node scripts/docs-audit/affected-docs.mjs --json 99e4eaa227e0375b59c7bbdf6e36ad7f026bb22d
|
Contract reviewServed-tier: Net diff read as merge-base ① Derived judgmentsDisclosure (security card, ruling on #9387, comment 5323310017).
Check-runs on the head (latest run per name, 40 names).
The accept-set changes the diff implies: the PR body's
The "Unchanged" list.
Public-surface change outside the table. The diff adds a new boot refusal (the thrown message) and changes a debug log line ( One precision note on the development-boot claim. Its direction is narrow, so it is not an accept-set defect.
Prose faces, each against the shipped behaviour.
Consumer tests in the diff. All three changes are RIGHT and minimal.
② Semver level
③ Boundary flagsThe dev's flags (report
Open question: should
Landing. The 18.0.0 ruling is recorded ( Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT
Accepted deviations:
Out-of-scope dispositions (one line each):
Open question ( Landing: through the merge queue by this seat once:
Generated by Claude Code |
OS_TRUSTED_ORIGINS stays auth's own list and admits nothing to CORS. Auth also trusts every origin listed in OS_CORS_ORIGIN, so that one variable covers a cross-origin front-end that signs in; a list given through the plugin's cors.origins option is not seen by auth, so such a front-end is then also named in OS_TRUSTED_ORIGINS. Changeset and HARDENING.md § CORS. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Net diff read as merge-base ① Derived judgmentsDisclosure (security card, ruling on #9387, comment 5323310017). The one new commit changes two prose sentences, and its message restates them. It adds no request sequence, no description of what a foreign page could obtain, and no unauthenticated-surface detail. The PR keeps to the ruling. This record keeps to it as well: the conditions named below are stated as configurations and file locations only. Check-runs on the head (latest run per name, 41 names, read 2026-10-20T09:58Z).
Blob equality, path by path (
The two new sentences, clause by clause, against the code. The same paragraph lands twice: changeset :17 and
The ruling's wording, across every face. No "belongs in both" survives anywhere under
② Semver level
③ Boundary flagsThe dev's open question is ruled: option A (
Flags carried from the previous record (its ③ items 1-10). Nothing in the delta touches them, and they stand as answered there. Items 3, 4 and 7 are now filed as #24809 (ACCEPT New flags from this round.
Landing. The 18.0.0 ruling ( Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT addendum (head
|
Fixes #24738
Clause-②: no (narrowing)
What changes
The CORS middleware that
HonoServerPlugin(the serveros serve,os devandos startmount) installs by default now admits only the origins it is told about. The governing text isdocs/HARDENING.md§ CORS: "Do not useorigin: '*'withcredentials: true", and the go-live checklist's "CORS origin list is explicit, not*".cors.originsoption, noOS_CORS_ORIGIN), every origin was admitted for credentialed cross-origin requests. A configured'*'with credentials on was accepted and answered the same way.localhost,127.0.0.1,[::1], any port) is admitted on a development boot only:NODE_ENVexactlydevelopment, read throughisDevelopmentBootfrom@objectstack/core, the same reading the auth secret's development fallback uses.'*'with credentials is refused ininit().kernel.bootstrap()rethrows aninit()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 frominit(). 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 ofenvironment-variables.mdx, one CORS step inself-hosting.mdxandproduction-readiness.mdx, and the plugin README. Two doc lines calledOS_TRUSTED_ORIGINSa 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.OS_CORS_CREDENTIALS=false, not a development bootOS_CORS_CREDENTIALS=false, development boot*(option orOS_CORS_ORIGIN, alone or among other entries), credentials oninit()throws; the boot failsUnchanged:
*, 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: falseandOS_CORS_ENABLED=falsestill mount nothing.204, admitted or not./_consoleand every page the server itself serves are unaffected.HonoCorsOptionsgains TSDoc only.Mechanism (measured)
main. The new pin file ran againstmain's source (commitb1bb0d25c:795ff708dplus the test file only) and gaveTests 8 failed | 4 passed (12). The eight failures were a foreign origin echoed back with credentials on, a loopback origin echoed outside development, andinit()resolving for'*'with credentials. The four that passed are the preservation pins.serve.tsseam 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, elseOS_CORS_ORIGIN). The transport reads development-ness throughisDevelopmentBoot, andservesetsNODE_ENV=developmentunder--devbefore any plugin initialises. No second copy of auth's trusted-origin rules was added.packages/runtime/src/http-metrics-inbound-coverage.hono.integration.test.tsconfigured'*'with credentials, which the boot now refuses. Measured before its fix:Plugin init failed: com.objectstack.server.honoand 1 failed of 27. It now names the origin its preflight carries.packages/qa/http-conformance/src/hono-approvals-act.conformance.test.tsandhono-webhooks-redeliver.conformance.test.tsare door-parity pins, and they compared the two doors' CORS headers by accident. Both doors now run withcors: false, as the suite's other parity pins already do.HonoServerPluginwith CORS on drives it from a non-browser client (Node's fetch or Hono'sapp.request), and neither enforces CORS. The only assertions onAccess-Control-Allow-Originin this repo are@objectstack/hono's own, for its own default (see Acceptance notes).examples/embed-objectqlmounts no HTTP server. The showcase e2e runs same-origin under--dev. objectui's console reaches the API same-origin: through its Vite/apiproxy in dev, and withVITE_SERVER_URLempty in both.envfiles (read-only,../objectuiatcd4623d06).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. Onmain's source the same file gave 8 failed and 4 passed, as above. The pins, by what they assert:Access-Control-Allow-Origin, on a request and on a preflight, and the request itself is still answered.OS_CORS_ORIGIN, is admitted with credentials, and an unlisted one is not.NODE_ENVisdevelopment, not when it is unset ortest.'*'with credentials is refused ininit()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, andtsc -p tsconfig.test.json --listFileslists 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=2onhttp-metrics-inbound-coverage.hono.integration.test.tsanddispatcher-plugin.rate-limit.integration.test.ts: 2 files, 35 tests passed.dist/, all green. They were measured on the tree committed as8c038d76d, before the last merge ofmain, which touched none of these paths:@objectstack/runtimetest files that bootHonoServerPluginwith CORS on: 123 tests;@objectstack/clientfiles that do: 30 tests.Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 98 families for this diff atf7cb52d15. All 98 ran, each exit 0.--ranreconciliation: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-coverageandcheck:type-check-debt.pnpm lintonf7cb52d15: exit 0.Changeset
.changeset/24738-cors-default-no-cross-origin-caller.mdbumps@objectstack/plugin-hono-serverasminor(the package is in thefixedgroup, pre-releasenext). It is marked BREAKING: an accept-set narrowing shipped asminorunder the launch-window convention, the same form the narrowing changesets already onmainuse (for example24341-extend-default-row-scope-refused.md). It carries theClause-②line, the ADR-0087 disposition and what an operator does.check-changeset-no-majorforbidsmajor. Whether this enters 18.0.0 is the maintainer's call (triage), and this PR does not decide it.Acceptance notes
@objectstack/hono'screateHonoApp(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.mdnow tells a host composing it to name its origins explicitly.matchOriginPattern). The card's "localhost in dev only" is scoped to the default, so this is reported, not changed.content/docs/deployment/cli.mdx(about:523) still callsOS_TRUSTED_ORIGINS"(CORS)". PR feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681 holds that file, so whichever lands second should correct it.CorsConfigSchemain spec (shared/http.zod.ts) declaresoriginsdefault'*'withcredentialsdefaultfalse. The transport does not read it (spec lane, held off). The futureserver.corsadmission should carry this default.*entry. A degenerate pattern such ashttps://*is not refused.HARDENING.mdordering. docs: HARDENING.md and production-readiness.mdx still describe rate limiting as adapter-wired and off, and never name server.security.rateLimit or trustProxy #24740 rewrites other sections of the same file. Triage asks the two to land serially.Generated by Claude Code