Skip to content

safe-bash: documented @poe-platform/safe-bash/browser Workers entry fails to start without nodejs_compat — contracts/io.ts imports node:stream/web, contracts/path.ts imports node:path #671

Description

@kamilio

Where: packages/safe-bash/src/contracts/io.ts:5 (import { TransformStream } from "node:stream/web") and packages/safe-bash/src/contracts/path.ts:2 (export { posix as posixPath } from "node:path"). Both are in the contracts/ core layer, which is in the import graph of the @poe-platform/safe-bash/browser entry. The ./browser export has no browser/workerd condition (package.json: "./browser": { "import": "./dist/browser.js" }), so these node: specifiers ship verbatim in dist/contracts/io.js and dist/contracts/path.js.

Contradicts the README (README.md, "Browsers and Workers"): "Use @poe-platform/safe-bash/browser … No nodejs_compat flag or Node globals are required."

PoC: bundle the documented browser entry for workerd and deploy with compatibility_flags = []:

esbuild worker.ts --bundle --format=esm --platform=browser --conditions=workerd,worker,browser
# worker.ts imports only from @poe-platform/safe-bash/browser (createBrowserCommands, portableSearchCommands, Shell, createMemoryFileSystem)
wrangler dev --local   # compatibility_flags = []

Measured (workerd via wrangler dev --local, wrangler 4.129.1): the runtime fails to start:

service core:user:safebash-browser: Uncaught Error: No such module "node:stream/web".
  imported from "worker-browser.js"
The Workers runtime failed to start.

esbuild import trace confirms the source:

node:stream/web <= packages/safe-bash/src/contracts/io.ts
node:path       <= packages/safe-bash/src/contracts/path.ts

The same imports are present in the built artifact: dist/contracts/io.js:3 and dist/contracts/path.js:2. Adding compatibility_flags = ["nodejs_compat"] makes the identical bundle start and run (grep, sed, pipelines work) — so the only blocker is the two node: imports the docs say are not needed.

Impact: (d) availability, with a (a) secondary — the documented, secret-safe Workers deployment path does not run at all. The available workaround is to enable nodejs_compat, which is exactly what maps a Worker's secret bindings into process.env (the risk the same README warns about in the env row). So the broken no-compat path pushes operators onto the higher-secret-exposure configuration.

Fix: in contracts/io.ts use the global TransformStream (a standard global in workerd and browsers) instead of importing it from node:stream/web; in contracts/path.ts use a portable posix-path join/normalize (safe-fs already ships one) instead of node:path. Alternatively add a browser/workerd condition on the ./browser export (and the contracts subpaths) that resolves to node-free implementations, and add a CI check that bundles the browser entry with --conditions=workerd and asserts zero node: specifiers.

Found in security audit v3, Cloudflare Workers pass (2026-09-08), verified on real workerd (wrangler dev --local).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions