Skip to content

fix(server): bundle Bun platform packages so one effect instance serves CORS - #9118

Open
lnieuwenhuis wants to merge 3 commits into
pingdotgg:mainfrom
lnieuwenhuis:fix/bun-single-effect-instance
Open

fix(server): bundle Bun platform packages so one effect instance serves CORS#9118
lnieuwenhuis wants to merge 3 commits into
pingdotgg:mainfrom
lnieuwenhuis:fix/bun-single-effect-instance

Conversation

@lnieuwenhuis

@lnieuwenhuis lnieuwenhuis commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Since the CLI bundling change in #5877, dist/bin.mjs inlines effect but kept @effect/platform-bun and @effect/sql-sqlite-bun external. A Bun-hosted server therefore loaded a second effect from node_modules. Effect attaches CORS, compression, and auth headers to real responses through a pre-response handler stored in a module-level WeakMap, so the bundled copy wrote handlers the platform server's copy never read. OPTIONS preflight still worked because it is answered inline, but every actual response lost Access-Control-Allow-Origin and gzip, which is why the desktop app could not connect to a remote Bun-run environment on a separate origin.

The fix externalizes only Bun's own module namespace (bun, bun:*) and bundles both platform packages, so one effect graph serves the whole server. The two packages move to devDependencies. Because correctness now depends on rolldown keeping bun:sqlite in a chunk reached only through import(), the server build gains a post-build check that walks the emitted chunk graph from both entries along static edges and fails if any eagerly loaded chunk imports a Bun module. Rolldown only warns on that merge and exits 0, so the check reads the artifact instead of trusting the bundler.

Verification: in the built artifact the single pre-response-handler WeakMap lives in the shared chunk that both bin.mjs and the BunHttpServer chunk import, and no bare @effect/*-bun runtime import remains. A probe bundled with the repo's own build predicates and run under Bun showed Access-Control-Allow-Origin: * and Content-Encoding: gzip restored on GET, and the real CLI under Node shows no regression. Bundle size grows by about 0.5%. The full server cannot run under Bun on Windows (the Bun PTY adapter is unsupported there), and the freshly built CLI has now passed the Linux Bun runtime smoke described below. A negative control confirmed the new build check rejects a bundle where bun:sqlite is hoisted into bin.mjs, which otherwise fails at load under Node.

Closes #8878

Claude Fable 5.1 via Claude Code

Fresh verification (2026-09-06): built the server/web artifacts and passed the isolated Linux Bun 1.4.0 runtime smoke for CORS, gzip body equivalence, pairing Set-Cookie, and authenticated cookie replay. The smoke is now wired into CI. 20 focused packaging tests passed. Published-package installation was not exercised; Windows Bun startup still has the existing unsupported PTY limitation.


Note

Medium Risk
Changes core server CLI bundling and build-time validation; a mistaken static import of Bun adapters would fail builds or break Node entrypoints, but the new eager-import scan is meant to catch that before release.

Overview
Fixes missing CORS, gzip, and auth on Bun-hosted servers when the desktop app talks to a remote environment on another origin. External @effect/platform-bun and @effect/sql-sqlite-bun had been loading a second effect beside the bundle, so pre-response handlers (WeakMap-keyed) never applied to real responses.

Bundling policy in cli-external-packages.ts now inlines those two packages and only keeps bun / bun:* external. They move from dependencies to devDependencies in apps/server/package.json. Inlining pulls bun:sqlite into the artifact, which must stay behind dynamic import() so node bin.mjs still works.

A post-bundle guard runs in the server CLI build step: findEagerBunRuntimeImports walks static edges from bin.mjs and service-launcher.mjs and fails with ServerCliEagerBunImportError if any eagerly loaded chunk references a Bun runtime module (rolldown only warns). Tests cover the scanner and updated bundle expectations; desktop self-containment comments point at this check for the Bun adapters.

Reviewed by Cursor Bugbot for commit bff7b06. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Bundle @effect/platform-bun and @effect/sql-sqlite-bun instead of externalizing them

  • Removes @effect/platform-bun and @effect/sql-sqlite-bun from CLI external prefixes in cli-external-packages.ts so they are bundled rather than left external. Bun runtime modules (bun, bun:*) remain external.
  • Adds findEagerBunRuntimeImports to walk the emitted chunks' static import graph from entry chunks and report any reachable Bun runtime module imports.
  • Adds assertNoEagerBunImports to the build command in cli.ts, which fails the build with ServerCliEagerBunImportError if any eagerly loaded chunk imports a Bun runtime module.
  • Updates tests in cli-external-packages.test.ts to expect the two adapter packages bundled and to cover the new eager-import detection logic.
  • Risk: isExternalCliDependency now bundles @effect/platform-bun and @effect/sql-sqlite-bun; if any code statically imports Bun runtime modules through these packages, the build will fail via assertNoEagerBunImports rather than silently producing a broken bundle.
📊 Macroscope summarized bff7b06. 4 files reviewed, 1 issue evaluated, 1 issue filtered, 0 comments posted

🗂️ Filtered Issues

apps/server/scripts/cliErrors.ts — 0 comments posted, 1 evaluated, 1 filtered
  • line 80: ServerCliEagerBunImportError also reports violations for the bare specifier bun (as exercised by the new scanner test), but its message always says the failure is ERR_UNSUPPORTED_ESM_URL_SCHEME for a bun: URL. A static bare bun import is package resolution and Node reports ERR_MODULE_NOT_FOUND, so this build failure gives an incorrect diagnosis for one of the explicitly detected violation types. [ Out of scope (post-validation triage) ]

…es CORS

Since 0.0.34 a Bun-hosted server omits `Access-Control-Allow-Origin` from
real GET/POST responses while answering OPTIONS preflight correctly, so a
desktop app cannot reach a remote environment on a separate HTTPS origin.
Node-hosted servers are fine. Compression and the auth refresh / DPoP /
cloud credential headers are broken the same way and for the same reason.

The CORS code did not change. The CLI bundling change did. `dist/bin.mjs`
inlines `effect`, but `@effect/platform-bun` and `@effect/sql-sqlite-bun`
stayed external so the bundler would never have to resolve `bun:sqlite`.
Under Bun that external `BunHttpServer` loads a second `effect` from
node_modules beside the bundle. Effect keys each request's pre-response
handler off a module-level WeakMap in
`effect/unstable/http/internal/preResponseHandler`: `HttpMiddleware.cors`
and `compression` write to it, and the platform server's `toHandled` reads
it when sending the response. With two `effect` instances the bundled copy
writes handlers the node_modules copy never reads. Preflight survives
because `cors` answers OPTIONS inline without touching the WeakMap.
`@effect/platform-node` is bundled into the same graph, which is why Node
never saw this.

Externalize Bun's own module namespace (`bun`, `bun:*`) instead of the
packages that import it. That is all the bundler could not resolve, and
nothing has to resolve it under Node either: every Bun import sits behind a
`typeof Bun !== "undefined"` dynamic import, so it lands in a chunk only a
Bun-hosted server loads. The two packages also leave `dependencies`, since
nothing resolves them at runtime any more.

That trades one silent failure for another, so it is now checked. Inlining
moves `bun:sqlite` into the bundle, and it is only harmless while its chunk
stays reachable solely through `import()`; statically reachable, every
`node bin.mjs` dies with ERR_UNSUPPORTED_ESM_URL_SCHEME. Rolldown merges a
dynamic import into its importer with an INEFFECTIVE_DYNAMIC_IMPORT warning
and exit 0, and removing these packages from `dependencies` is not a
backstop either — the desktop self-containment probe runs
`node bin.mjs --version` and never takes the Bun branch. So
`assertNoEagerBunImports` in `apps/server/scripts/cli.ts` walks the emitted
chunk graph from `bin.mjs` and `service-launcher.mjs` following static
edges only and fails the build on any Bun specifier in that set. It runs on
every PR through `vp run build:desktop`, unlike the desktop probe.

Verified by bundling one probe that mirrors `server.ts`'s conditional
`BunHttpServer` import plus `http.ts`'s cors and compression middleware,
built both ways and run under Bun 1.2.15. Packages external: no
`access-control-allow-origin`, no `content-encoding`, 9728 bytes. Packages
bundled: `access-control-allow-origin: *`, `content-encoding: gzip`, 81
bytes. Preflight identical in both. The shipped bundle has no bare
`@effect/*-bun` import left, keeps one `bun:sqlite` external in the chunk
Node never loads, and defines the pre-response WeakMap exactly once. The
real CLI still serves CORS and sets `vary: Accept-Encoding` under Node.
Bundle JS grows 8,662,684 -> 8,705,534 bytes while `bin.mjs` itself drops
180,952; the npm install loses 537 KB of now-unused packages.

The new check passes on a real build (11 eagerly loaded chunks, no Bun
modules). Adding a static `@effect/sql-sqlite-bun/SqliteClient` import to
`bin.ts` made it report `bin.mjs -> bun:sqlite` and exit 1, and the bundle
it rejected does fail under Node with ERR_UNSUPPORTED_ESM_URL_SCHEME.

`vp test run scripts/lib/cli-external-packages.test.ts` (20) passes and
`apps/server` typecheck is clean. `scripts/build-desktop-artifact.test.ts`
has 7 failures on Windows (symlink EPERM, WSL archive, macOS entitlements)
that reproduce identically on an unmodified tree.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Sep 1, 2026
@macroscopeapp

macroscopeapp Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — The change alters the production Bun bundle and restores CORS, compression, and authentication-cookie behavior while adding build-time graph validation and a cross-runtime smoke test. The new test also adds file-level static-analysis diagnostic suppressions, making the overall change require closer review.

Not approved because:

  • Monthly spending limit reached (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings. You can add or adjust custom eligibility rules. Learn more.

@derektrimm

Copy link
Copy Markdown
Contributor

@juliusmarminge re your note on #8878 about browser-session cookie emission: here is a manual Linux check of this branch that includes it.

Repo-built apps/server/dist, comparing upstream main at 04efa7907 with the same main plus this PR merged on top (635625d42), so the only source difference is this change. Bun 1.4.0 and Node v22.23.2, fresh --base-dir per run, server bound to 0.0.0.0 so the auth policy reports remote-reachable, and a fresh credential from node bin.mjs auth pairing create for each pairing run.

Build Runtime GET /.well-known/t3/environment with Origin GET / with accept-encoding: gzip POST /api/auth/browser-session
main Node Access-Control-Allow-Origin: * Content-Encoding: gzip, Vary: Accept-Encoding 200, authenticated: true, Set-Cookie present
main Bun header absent both headers absent 200, authenticated: true, Set-Cookie absent
main + PR Node Access-Control-Allow-Origin: * Content-Encoding: gzip, Vary: Accept-Encoding 200, authenticated: true, Set-Cookie present
main + PR Bun Access-Control-Allow-Origin: * Content-Encoding: gzip, Vary: Accept-Encoding 200, authenticated: true, Set-Cookie present

The main/Bun pairing response is the #7756 shape exactly: 200 with authenticated: true and no Set-Cookie (Cache-Control and Pragma are missing from that response too). Replaying the issued cookie against GET /api/auth/session authenticated in every run that had one. The Bun cells were repeated three times each with identical headers.

This is symptom-level evidence from the repo build with workspace node_modules, not an npm pack install of the published package, and not in-repo regression coverage.

…-instance

# Conflicts:
#	apps/server/package.json
#	pnpm-lock.yaml
@derektrimm

Copy link
Copy Markdown
Contributor

A Bun runtime smoke for the browser-session cookie case discussed in #8878 is available here: derektrimm@a90d0ef. It runs the built CLI under Bun 1.4.0 in the Check job and asserts CORS, gzip, and the pairing Set-Cookie. The smoke fails on main at bfef973d9 at the CORS assertion and passes with bff7b06 plus this commit. The commit is based on bff7b06 and can be cherry-picked if useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Access-Control-Allow-Origin missing from responses since 0.0.34 — desktop app cannot connect to a remote environment on a separate HTTPS origin

2 participants