97 Commits
Author SHA1 Message Date
Toshit Chawda f0e682f30f update epoxy 2026-10-22 18:18:42 -07:00
Toshit Chawda 6b537d06b2 centralize token handling and support token renewal 2026-10-19 16:04:25 -07:00
Toshit Chawda 08edff48a3 fix: update Epoxy and preserve Firefox request bodies 2026-10-16 15:36:01 -07:00
Toshit Chawda 7867f1fb1e allow layering whole vfses, give the vmodule its own vfs overlay on top of provided vfs 2026-10-03 21:03:10 -07:00
Toshit Chawda 8d0ab87f39 rewrite node apis to be prototype pollution safe, move puter api out of worker 2026-09-28 20:11:37 -07:00
Toshit ChawdaandClaude Opus 5 f1b3d40945 node:repl, which is node's own
`require("node:repl")` was ENOENT: the module was never registered, and
registering it was not one line because `lib/repl.js` reaches for machinery
this runtime had no reason to have.

`internalBinding('contextify')` is the piece it genuinely needs, and only for
three things — `makeContextifyScript` and the two SIGINT watchdogs. It is the
same indirect eval `node/vm.ts` already was, so that file now builds its
`Script` on this one rather than keeping a second copy: `repl.js:106` lifts
`vm.Script.prototype.runInThisContext` off the public vm and applies it to a
script the internal binding built, so the two have to be one function over one
set of fields. The constructor compiles eagerly, which it must — an input that
throws there is what makes the REPL offer its `...` continuation instead of
reporting an error.

Everything else is weight. Pulling `repl.js` in unmodified drags node's whole
module system behind it, and the build stopped finishing rather than failing:
`commonjs({ dynamicRequireTargets: ["node_core/**/*.js"] })` means one dynamic
`require()` anywhere in the graph bundles every JavaScript file in the node
checkout — `benchmark/`, `deps/v8/test/`, the lot — and
`internal/modules/helpers.js:404` has one. `internal/util/inspector` was the
other half, reaching `internal/process/execution` and so
`internal/modules/typescript` and a 3.3 MB TypeScript stripper. Seven overrides
cut that back; the worker bundle is 4.6s again, against 4.6s before repl existed
and never-finishing with it.

`lib/vm.js` is an override for a different reason: without it a `require('vm')`
from inside the node tree got upstream's while a script's `require("node:vm")`
got ours. Two different `vm`s, and the REPL is the first module in the tree to
ask for one.

The rest are things only running it could find: `process.addUncaughtException-
CaptureCallback` (node 25's replacement for repl's domain-based error routing —
absent, the constructor throws and there is no prompt at all), `overrideStack-
Trace`, `isErrorStackTraceLimitWritable`, `decorateErrorStack`,
`BuiltinModule.getSchemeOnlyModuleNames`, and `SKIP_SYMBOLS`.

Two known gaps, both from the same absence. There is no V8 inspector in a
Worker, so completing a `const` declared at the prompt — which node does through
`Runtime.globalLexicalScopeNames` — does not work, though completing a property
does. And an evaluation cannot be interrupted: indirect `eval` is not something
V8 can be broken into, so `breakEvalOnSigint` is off and a runaway loop needs
killing from outside.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-16 18:17:34 -07:00
Toshit ChawdaandClaude Opus 5 a234d12f4c localhost:port is an address the client half can dial
A server here registers with the signaller under `(credential, port)` and reaches
another Puter app that way -- cross-origin messaging between apps is effectively
impossible, so WebRTC is the transport and the port is the address. The server
side has reflected that since `<code>.peer.puter.com` was phased out. The client
side had not: `connect` handed the string "localhost" to the Wisp relay, which
resolved it on the *relay host*, so the connection did not arrive at all.

So `connect` now resolves a loopback host itself, in the order that costs least:
a server in this same worker, then another app's over WebRTC by port, then
ECONNREFUSED. Never the relay -- a loopback address reaching the public internet
is the bug being fixed.

The same-worker case is two `TransformStream`s wired crosswise into the server's
existing `_onAccept`, so there is no new accept path and backpressure comes free
from the streams. It is a fast path only, and it does not change who is
reachable: `listen` still publishes to the signaller.

`peer.connect` gains `port` beside `code`, and exactly one goes on the wire. A
code exists only for an authenticated server; a port is the address that always
does, and is what browser.js already dials with.

Three smaller things that were wrong in the same area:

  - `listen(0)` now allocates a port instead of binding zero, because `address()`
    has to report what it picked.
  - `address()` reports `127.0.0.1`. Nothing here binds an interface, and the
    address that works is the one a client can pass to `connect`.
  - failing to *publish* is no longer failing to listen. Without a token the
    signaller is simply unavailable, which is an ordinary configuration rather
    than an error in the call; the port is still bound as far as this machine is
    concerned. Emitting `error` there took down a server that worked.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-12 17:56:14 -07:00
Toshit Chawda 9c18007250 fix net bug 2026-09-11 21:11:52 -07:00
Toshit ChawdaandClaude Opus 5 ef320e58fd fix: a raw-mode stdin read blocked instead of saying EAGAIN
From a stack in the deployed app, on the second ^C:

  handleKeyDown -> requestExit -> shutdown -> drainStdin
    -> readSync -> readStdinSync -> stdioSync -> rawSync -> transportError

A terminal program shutting down drains stdin. In raw mode node answers
`readSync` with EAGAIN when there is nothing to read, and a drain loop is
written against exactly that. `readStdinSync` asked for a *blocking* read
unconditionally, so with nobody typing it waited — and waiting here is a
synchronous transport request held open, which does not stall so much as
eventually fail: the request timed out and threw a TransportError out of
a teardown path that was never written to handle one. The program then
sat neither running nor exited, which is worse than either.

So a raw-mode read is non-blocking and raises EAGAIN when nothing is
available, which is what node does and what the wire already described:
"eof: false with no bytes when that is nothing, which is what a
non-blocking fd does". Only raw mode — a cooked read at a prompt still
blocks, because there the program is waiting for a line and means it.

Measured in a browser with a TTY sibling: before, the drain never
returns and the run is still unsettled at 25s; after, EAGAIN in 2ms and
the program exits cleanly in 14ms.

Also here, from the same investigation but not the same bug: `process.exit`
tells the page *before* running the program's own beforeExit/exit
handlers. Those are synchronous and can block the thread outright, and
then `ctl.exit` is never posted at all — the program is gone and nothing
can say so. The page arms a deadline on that declaration, which is armed
only by a program announcing an exit, so it stays clear of a worker
merely parked in a long blocking call.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-11 18:29:35 -07:00
Toshit ChawdaandClaude Opus 5 130161d9e8 fix: an exit could be prevented from ever being reported
A program that exits while its module is still running throws
`ProcessExit` out of the run, and `execute` catches it and reports the
code. A program that exits from a callback — a timer, a keypress, which
is every interactive one — threw it into a handler nobody is watching, so
the message the worker posts to the page is the *only* remaining evidence
the process ended. Three unbounded awaits sat in front of that message,
and any one of them could hold it forever:

- `requestExit` flushed output first, deliberately, so a program's last
  line is not lost. But a chunk settles only when whoever is reading
  stdout accepts it, so a reader that stalls holds the flush — and with
  it the exit — open for good.
- The page awaited every exit listener before terminating. Their one
  window to read worker-side state, and it must not become a veto.
- The worker process provider flushed again after the run settled,
  before pushing the exit event. Same stall, same result: the child is
  gone and nothing says so.

All three are bounded now, and `flushStdio` takes a deadline so the
callers on the teardown path can say what they can afford. Waiting is a
courtesy to the last line; the ending is not optional.

Measured with the page-side flush wedged permanently: before, a
`node -e 'setTimeout(() => process.exit(3), 50)'` never reports and the
run never settles; after, the shell gets rc=3.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-11 17:40:10 -07:00
Toshit ChawdaandClaude Opus 5 c07de55860 fix: one unanswered attach ended the session for good
"service worker did not acknowledge the session" was fatal on a single
5s miss, and a miss is not evidence of anything wrong: the worker can be
stopped, replaced, or still starting at the moment the message is posted,
and the page cannot see which. The `rescue` and `controllerchange` paths
already treat exactly these as routine and simply re-attach — only the
first attach had no second chance, which is the difference between a
shell that hiccups and a shell that will not start.

It now tries three times, re-reading `registration.active` each attempt
so a worker replaced between tries is picked up rather than messaged
again in its grave, and a port that timed out is closed so a late
`attached` cannot leave the service worker holding a session pointing at
a channel this page has forgotten. Measured: with every first handshake
dropped, boot recovers in 7.5s instead of failing.

A version mismatch still fails immediately. Retrying a settled
disagreement only spends another ten seconds.

If all three miss, the error now carries the registration's state —
scope, active, waiting, installing, controller — because "did not
acknowledge" names a symptom shared by every cause, and these tell them
apart in a report from someone else's browser.

Separately, `registerOnce` cached a registration for the life of the
page. A registration is not forever: another app on the origin can claim
the scope and clearing site data takes it away, neither of which is
visible from the object — it goes on reporting an `active` worker that
still answers postMessage. So the handshake succeeded and every
synchronous request 404'd, reported as a filesystem fault rather than as
the registration being gone. Reproduced by unregistering mid-session:
before, every later spawn failed with probe-failed; now it re-registers.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-09 17:41:30 -07:00
Toshit ChawdaandClaude Opus 5 01d2b837c6 add worker_threads + fork, and a worker-backed process provider
worker_threads.Worker and child_process.fork now work with no setup: a thread
is a second NodeWorker the page creates, joined to the first by a real
MessageChannel, so structured clone, transferables and ordering are the
platform's own and the page is not in the data path. That replaces the
node-core JS shim, which had isMainThread hardcoded false. receiveMessageOnPort
and moveMessagePortToContext still throw — both want a synchronous port drain,
which needs SharedArrayBuffer, and not needing that is what lets this run
without cross-origin isolation.

spawn still needs a ProcessProvider, because "run an arbitrary program" is the
embedder's question. "Run another copy of this runtime" is not, so
createWorkerProcessProvider + nodeCommandLine ship the common case, and
spawnSibling makes the child by inheriting the worker script, service worker,
filesystem, network and TTY of the worker it is called on.

Verified against the phoenix kernel, which drives both halves: 122 passing in
the browser, including five cases that exist only to exercise a node process
spawning bash back into the kernel over this provider.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-04 18:00:45 -07:00
Toshit Chawda 8ac98f5997 add child_process api + refactor messaging 2026-09-02 16:20:23 -07:00
Toshit Chawda b0bfa88677 add vfs cache, fix some multi worker bugs 2026-08-18 20:16:21 -07:00
Toshit Chawda dedb528c76 add /dev, fix statfs & puter rename 2026-08-14 14:12:18 -07:00
Toshit Chawda e642a9289a fix node incompats 2026-08-10 15:43:59 -07:00
Toshit Chawda 0d60f1bc1a Add Node compatibility fixes and configurable epoxy builds
- Add epoxyBase worker option and update the default epoxy pin
  - Fix async rewriter ordering around try blocks and await
  - Add Node built-ins for stream, path, and net.BlockList
  - Correct socket close/error and terminal resize behavior
  - Hide browser-only XMLHttpRequest
2026-08-10 15:04:16 -07:00
Toshit Chawda b4fd4d3801 fix anonymous fs events 2026-08-06 09:38:22 -07:00
Toshit Chawda 92e71f20a2 docs, change wispUrl 2026-08-05 15:34:41 -07:00
Toshit Chawda 333272a4cc proper teardown path 2026-08-05 13:09:12 -07:00
Toshit Chawda e4581671fd fix rtc to stream max message size bug 2026-08-05 12:48:56 -07:00
Toshit Chawda 3cdeb8ba5d disable puter fs events watching on anon 2026-08-05 12:30:43 -07:00
Toshit Chawda edfb2f33b0 anonymous stuff 2026-08-05 12:15:31 -07:00
Toshit Chawda b0924f92ee fix resolver cache 2026-08-05 11:39:08 -07:00
Toshit Chawda 02251c0022 fix numeric open 2026-08-05 11:25:58 -07:00
Toshit Chawda 080ce10c5c rewrite vfs to be on the host side 2026-08-05 10:59:23 -07:00
Toshit Chawda 7e3b841b38 fix console 2026-08-04 16:36:12 -07:00
Toshit Chawda a0768720a6 fix remaining vfs issues and add cwd/args + proper tty handling 2026-08-03 12:32:26 -07:00
Toshit Chawda 5591abf2c6 clean up fs duplication + add vfs 2026-07-31 16:01:12 -07:00
Toshit Chawda 9068df0cd0 quick cache busting 2026-07-31 14:18:06 -07:00
Toshit Chawda 8eddd490d8 revamp the node:fs implementation again 2026-07-31 14:05:24 -07:00
Toshit Chawda 057e0b939e add esbuild shim + proper error stack locations 2026-07-29 16:08:52 -07:00
Toshit Chawda 5613c2e65e better keepalive 2026-07-29 15:10:01 -07:00
Toshit Chawda 9ed250a2a8 initial asynclocalstorage transform 2026-07-27 12:04:54 -07:00
Toshit Chawda e778b8847d rewrite peer server, use upstream timers 2026-07-22 15:32:59 -07:00
Toshit Chawda 4ab3fd65f5 clean up some ts errors 2026-07-20 14:40:40 -07:00
Toshit Chawda 0648a59ebe nghttp2, some resolve fixes, better implementations 2026-07-18 12:59:44 -07:00
Toshit Chawda 74d5eddaa5 swap to systemjs, polyfill more stuff 2026-07-16 15:18:25 -07:00
Toshit Chawda 632ffcd34b reduce nested code in esm paths 2026-07-10 16:58:05 -07:00
Toshit Chawda ce32371546 bunch of fs fixes, buffer fixes, vite dev server loads 2026-06-20 14:56:09 +05:30
Toshit Chawda 71ab716df6 more fixes for vite. add module silent rewriting 2026-06-20 13:39:31 +05:30
Toshit Chawda 85ec75e61c implement most of node:tls/https client 2026-06-07 09:43:56 -07:00
Toshit Chawda 4f315655ee README, some more stubs for vite 2026-06-04 18:51:25 -07:00
Toshit Chawda eafc696786 implement real node:crypto via openssl emscripten 2026-06-01 18:54:18 -07:00
Toshit Chawda 78e7863e49 fix resolver to use correct import target & esm->cjs export fixes 2026-05-29 16:57:13 -07:00
Toshit Chawda 2ef2d7b8fd fix ws load and cache in the resolver 2026-05-29 16:31:56 -07:00
Toshit Chawda 297d96602d work towards vite support. dns/child_process/os stub. timers/promises implementation 2026-05-28 22:19:41 -07:00
Toshit Chawda 7de500432b make process more compliant 2026-05-20 16:45:18 -07:00
Toshit Chawda 3d31e17f88 add real node zlib backing 2026-05-18 15:55:18 -07:00
Toshit Chawda c522730a1a eventloop tracking 2026-05-14 16:03:18 -07:00