sandboxedjs 0.1.45 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -669,9 +669,10 @@ These are runtime lifecycle rules, not Vite-specific command replacements.
669
669
 
670
670
  **Not done — these things still stand between this and a complete browser IDE.**
671
671
 
672
- - *Preview isolation and WebSockets.* `createPreview()` registers the bundled service worker
673
- and routes HTTP requests into the container. It is an origin-local preview for trusted code;
674
- WebSocket forwarding/HMR is not implemented. Reload the iframe after changes.
672
+ - *Preview isolation.* `createPreview()` registers the bundled service worker and routes HTTP
673
+ and WebSocket traffic into the container. It is an origin-local preview for trusted code:
674
+ scripts in the frame share your origin. Serve it from a separate origin for anything you
675
+ did not write.
675
676
  - *Complete worker isolation.* Worker-capable programs run off the main thread, but host-backed
676
677
  compiler modules can require the local runtime. Container code is not a security boundary
677
678
  from the host page; do not treat it as one.
@@ -846,6 +847,25 @@ URLs — `/src/main.js`, `/@vite/client` — resolve without rewriting anything.
846
847
  > page it serves. Use it for code you trust; for anything else, serve the preview from a separate
847
848
  > origin (a subdomain pointed at the same app) or stay with option 2.
848
849
 
850
+ **WebSockets, and why the HTML is modified.** A service worker can answer `fetch`; it cannot
851
+ answer a WebSocket handshake, because no browser ever shipped that. So a previewed page is handed
852
+ a `WebSocket` that tunnels through the host page into the container, injected as one `<script>` at
853
+ the top of `<head>`. URLs that do not name the container keep the native implementation.
854
+
855
+ ```ts
856
+ const preview = await createPreview(box, { websocket: false }); // serve HTML byte for byte
857
+ ```
858
+
859
+ This is what makes HMR work. Without it Vite loses more than hot reload: it loses the only channel
860
+ it has for telling a page that its dependency hashes are stale, which it needs after re-optimizing
861
+ dependencies — and the frame is then stranded on `504 Outdated Optimize Dep` with every module
862
+ failing and nothing to explain it. If you turn injection off, pass `onStale` to reload the frame
863
+ yourself.
864
+
865
+ The container side is a real socket, not a Vite shim: `http` servers emit `upgrade` with a duplex
866
+ socket, so `ws` and `socket.io` work unmodified. To speak to one from your own code rather than
867
+ from a previewed page, use `box.connect(port, init, peer)`.
868
+
849
869
  Service workers need a secure context, and some embedded browsers disable them entirely — hence
850
870
  the `null` return rather than a throw. Responses are served with both
851
871
  `Cross-Origin-Resource-Policy` and `Cross-Origin-Embedder-Policy`, so a preview still frames
package/dist/agent.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as Container } from './container-CZn9USBQ.cjs';
1
+ import { C as Container } from './container-DyRF-bY0.cjs';
2
2
 
3
3
  /**
4
4
  * Structural copies of the LangChain Deep Agents backend contract.
package/dist/agent.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as Container } from './container-CZn9USBQ.js';
1
+ import { C as Container } from './container-DyRF-bY0.js';
2
2
 
3
3
  /**
4
4
  * Structural copies of the LangChain Deep Agents backend contract.
@@ -146,6 +146,16 @@ interface RuntimePackageInstaller {
146
146
  /** Create a view that installs into another project root. */
147
147
  forCwd?(cwd: string): RuntimePackageInstaller;
148
148
  }
149
+ /** Where bytes written by an upgraded server inside the container come out. */
150
+ interface RuntimeSocketPeer {
151
+ data(bytes: Uint8Array): void;
152
+ close(): void;
153
+ }
154
+ /** The caller's end of a connection opened with {@link RuntimePod.connect}. */
155
+ interface RuntimeConnection {
156
+ send(bytes: Uint8Array): void;
157
+ close(): void;
158
+ }
149
159
  /** The process-manager protocol a container's kernel bridge substitutes for. */
150
160
  interface RuntimeProcessManager {
151
161
  spawn(config: ChildSpawnConfig): ChildHandle;
@@ -160,6 +170,14 @@ interface RuntimePod {
160
170
  };
161
171
  spawn(command: string, args?: string[], options?: Record<string, unknown>): Promise<RuntimeProcess>;
162
172
  request(port: number, init?: Record<string, unknown>): Promise<RuntimeHttpResponse>;
173
+ /**
174
+ * Open a connection that upgrades out of HTTP, or null if nothing takes one.
175
+ *
176
+ * Optional because a pod that only ever answers requests is still a usable
177
+ * pod — a caller treats the absence as "no WebSocket here" rather than as a
178
+ * broken implementation.
179
+ */
180
+ connect?(port: number, init: Record<string, unknown>, peer: RuntimeSocketPeer): RuntimeConnection | null;
163
181
  snapshot(options?: Record<string, unknown>): unknown;
164
182
  restore(snapshot: unknown, options?: Record<string, unknown>): Promise<void>;
165
183
  teardown(): void;
@@ -1705,6 +1723,20 @@ interface HttpResponse {
1705
1723
  bytes: Uint8Array;
1706
1724
  json<T = unknown>(): T;
1707
1725
  }
1726
+ /** Where bytes written by an upgraded server inside the container come out. */
1727
+ interface SocketPeer {
1728
+ /** The server sent these. */
1729
+ data(bytes: Uint8Array): void;
1730
+ /** The server hung up. */
1731
+ close(): void;
1732
+ }
1733
+ /** The caller's end of a connection opened with {@link Container.connect}. */
1734
+ interface SocketConnection {
1735
+ /** Send bytes to the server. */
1736
+ send(bytes: Uint8Array): void;
1737
+ /** Hang up. */
1738
+ close(): void;
1739
+ }
1708
1740
  declare class Container {
1709
1741
  readonly kernel: Kernel;
1710
1742
  readonly pod: RuntimePod;
@@ -1758,6 +1790,25 @@ declare class Container {
1758
1790
  headers?: Record<string, string>;
1759
1791
  body?: string | Uint8Array;
1760
1792
  }): Promise<HttpResponse>;
1793
+ /**
1794
+ * Open a connection to a server inside the container that upgrades out of
1795
+ * HTTP — in practice, a WebSocket.
1796
+ *
1797
+ * The counterpart to {@link request}, and the thing a dev server needs that
1798
+ * a request cannot provide. Bytes go in with `send`, come back through
1799
+ * `peer.data`, and neither side interprets them: the container runs whatever
1800
+ * WebSocket library the program chose, and the caller is responsible for
1801
+ * speaking the protocol it answers with.
1802
+ *
1803
+ * Null means there is nothing to talk to — no server on the port, or one
1804
+ * that never registered an `upgrade` handler. Both are worth reporting
1805
+ * rather than waiting out, because neither resolves on its own.
1806
+ */
1807
+ connect(port: number, init: {
1808
+ method?: string;
1809
+ path?: string;
1810
+ headers?: Record<string, string>;
1811
+ } | undefined, peer: SocketPeer): SocketConnection | null;
1761
1812
  /**
1762
1813
  * Deliver a request whose body is bytes, without letting them become text.
1763
1814
  *
@@ -1808,4 +1859,4 @@ declare class Container {
1808
1859
  /** Boot a container. The one function most callers need. */
1809
1860
  declare function createContainer(opts?: ContainerOptions): Promise<Container>;
1810
1861
 
1811
- export { type ProcessKind as $, type KernelOptions as A, BufferSink as B, Container as C, type DirEntry as D, type ExecContext as E, type FileData as F, type GroupEntry as G, type HttpResponse as H, type InputStream as I, type Job as J, Kernel as K, type ListeningPort as L, type MountEntry as M, type Node as N, type OutputStream as O, type NetInterface as P, type NetworkOptions as Q, type RuntimeVolume as R, Shell as S, NetworkStack as T, NullInput as U, Vfs as V, NullOutput as W, PYTHON_VERSION as X, type PasswdEntry as Y, Pipe as Z, Process as _, type ShellIO as a, type ProcessOptions as a0, type ProcessState as a1, ProcessTable as a2, type PythonOptions as a3, ROOT_CRED as a4, type ResolvedExecutable as a5, type RunOptions as a6, type RunResult as a7, type RuntimeProcessManager as a8, type RuntimeProcessResult as a9, isCPythonAvailable as aA, isPythonAvailable as aB, makeCred as aC, octalMode as aD, parseUmask as aE, resetPidCounter as aF, shellQuote as aG, type SessionInit as aa, type SessionResult as ab, type SessionRunOptions as ac, ShellExit as ad, type ShellInit as ae, type ShellOptions as af, type SpawnHandle as ag, Stats as ah, type Stdio as ai, TeeOutput as aj, UserDatabase as ak, Variables as al, type VirtualNode as am, type VirtualProvider as an, type WriteOptions as ao, applyChmod as ap, braceExpand as aq, captureStdio as ar, configureCPython as as, configurePython as at, createChildProcessModule as au, createContext as av, defineCommand as aw, expandWord as ax, expandWords as ay, formatMode as az, Session as b, type Cred as c, type Command as d, type VolumeStat as e, type VolumeStats as f, type RuntimeHttpResponse as g, type SpawnChild as h, type SyncSpawn as i, type RuntimePod as j, type RuntimePackageInstaller as k, type ChildSpawnConfig as l, type ChildHandle as m, type RuntimeProcess as n, createContainer as o, type CPythonOptions as p, CallbackSink as q, CommandRegistry as r, ContainerFs as s, type ContainerOptions as t, type ContextInit as u, type Env as v, type ExecOptions as w, type ExecResult as x, FileInput as y, FileOutput as z };
1862
+ export { Pipe as $, FileInput as A, BufferSink as B, Container as C, type DirEntry as D, type ExecContext as E, type FileData as F, FileOutput as G, type GroupEntry as H, type HttpResponse as I, type InputStream as J, Kernel as K, type Job as L, type KernelOptions as M, type Node as N, type OutputStream as O, type ListeningPort as P, type MountEntry as Q, type RuntimeVolume as R, Shell as S, type NetInterface as T, type NetworkOptions as U, Vfs as V, NetworkStack as W, NullInput as X, NullOutput as Y, PYTHON_VERSION as Z, type PasswdEntry as _, type ShellIO as a, Process as a0, type ProcessKind as a1, type ProcessOptions as a2, type ProcessState as a3, ProcessTable as a4, type PythonOptions as a5, ROOT_CRED as a6, type ResolvedExecutable as a7, type RunOptions as a8, type RunResult as a9, expandWords as aA, formatMode as aB, isCPythonAvailable as aC, isPythonAvailable as aD, makeCred as aE, octalMode as aF, parseUmask as aG, resetPidCounter as aH, shellQuote as aI, type RuntimeProcessManager as aa, type RuntimeProcessResult as ab, type SessionInit as ac, type SessionResult as ad, type SessionRunOptions as ae, ShellExit as af, type ShellInit as ag, type ShellOptions as ah, type SpawnHandle as ai, Stats as aj, type Stdio as ak, TeeOutput as al, UserDatabase as am, Variables as an, type VirtualNode as ao, type VirtualProvider as ap, type WriteOptions as aq, applyChmod as ar, braceExpand as as, captureStdio as at, configureCPython as au, configurePython as av, createChildProcessModule as aw, createContext as ax, defineCommand as ay, expandWord as az, Session as b, type Cred as c, type Command as d, type VolumeStat as e, type VolumeStats as f, type RuntimeHttpResponse as g, type SpawnChild as h, type SyncSpawn as i, type RuntimePod as j, type RuntimePackageInstaller as k, type ChildSpawnConfig as l, type ChildHandle as m, type RuntimeProcess as n, type RuntimeSocketPeer as o, type RuntimeConnection as p, createContainer as q, type CPythonOptions as r, CallbackSink as s, CommandRegistry as t, ContainerFs as u, type ContainerOptions as v, type ContextInit as w, type Env as x, type ExecOptions as y, type ExecResult as z };
@@ -146,6 +146,16 @@ interface RuntimePackageInstaller {
146
146
  /** Create a view that installs into another project root. */
147
147
  forCwd?(cwd: string): RuntimePackageInstaller;
148
148
  }
149
+ /** Where bytes written by an upgraded server inside the container come out. */
150
+ interface RuntimeSocketPeer {
151
+ data(bytes: Uint8Array): void;
152
+ close(): void;
153
+ }
154
+ /** The caller's end of a connection opened with {@link RuntimePod.connect}. */
155
+ interface RuntimeConnection {
156
+ send(bytes: Uint8Array): void;
157
+ close(): void;
158
+ }
149
159
  /** The process-manager protocol a container's kernel bridge substitutes for. */
150
160
  interface RuntimeProcessManager {
151
161
  spawn(config: ChildSpawnConfig): ChildHandle;
@@ -160,6 +170,14 @@ interface RuntimePod {
160
170
  };
161
171
  spawn(command: string, args?: string[], options?: Record<string, unknown>): Promise<RuntimeProcess>;
162
172
  request(port: number, init?: Record<string, unknown>): Promise<RuntimeHttpResponse>;
173
+ /**
174
+ * Open a connection that upgrades out of HTTP, or null if nothing takes one.
175
+ *
176
+ * Optional because a pod that only ever answers requests is still a usable
177
+ * pod — a caller treats the absence as "no WebSocket here" rather than as a
178
+ * broken implementation.
179
+ */
180
+ connect?(port: number, init: Record<string, unknown>, peer: RuntimeSocketPeer): RuntimeConnection | null;
163
181
  snapshot(options?: Record<string, unknown>): unknown;
164
182
  restore(snapshot: unknown, options?: Record<string, unknown>): Promise<void>;
165
183
  teardown(): void;
@@ -1705,6 +1723,20 @@ interface HttpResponse {
1705
1723
  bytes: Uint8Array;
1706
1724
  json<T = unknown>(): T;
1707
1725
  }
1726
+ /** Where bytes written by an upgraded server inside the container come out. */
1727
+ interface SocketPeer {
1728
+ /** The server sent these. */
1729
+ data(bytes: Uint8Array): void;
1730
+ /** The server hung up. */
1731
+ close(): void;
1732
+ }
1733
+ /** The caller's end of a connection opened with {@link Container.connect}. */
1734
+ interface SocketConnection {
1735
+ /** Send bytes to the server. */
1736
+ send(bytes: Uint8Array): void;
1737
+ /** Hang up. */
1738
+ close(): void;
1739
+ }
1708
1740
  declare class Container {
1709
1741
  readonly kernel: Kernel;
1710
1742
  readonly pod: RuntimePod;
@@ -1758,6 +1790,25 @@ declare class Container {
1758
1790
  headers?: Record<string, string>;
1759
1791
  body?: string | Uint8Array;
1760
1792
  }): Promise<HttpResponse>;
1793
+ /**
1794
+ * Open a connection to a server inside the container that upgrades out of
1795
+ * HTTP — in practice, a WebSocket.
1796
+ *
1797
+ * The counterpart to {@link request}, and the thing a dev server needs that
1798
+ * a request cannot provide. Bytes go in with `send`, come back through
1799
+ * `peer.data`, and neither side interprets them: the container runs whatever
1800
+ * WebSocket library the program chose, and the caller is responsible for
1801
+ * speaking the protocol it answers with.
1802
+ *
1803
+ * Null means there is nothing to talk to — no server on the port, or one
1804
+ * that never registered an `upgrade` handler. Both are worth reporting
1805
+ * rather than waiting out, because neither resolves on its own.
1806
+ */
1807
+ connect(port: number, init: {
1808
+ method?: string;
1809
+ path?: string;
1810
+ headers?: Record<string, string>;
1811
+ } | undefined, peer: SocketPeer): SocketConnection | null;
1761
1812
  /**
1762
1813
  * Deliver a request whose body is bytes, without letting them become text.
1763
1814
  *
@@ -1808,4 +1859,4 @@ declare class Container {
1808
1859
  /** Boot a container. The one function most callers need. */
1809
1860
  declare function createContainer(opts?: ContainerOptions): Promise<Container>;
1810
1861
 
1811
- export { type ProcessKind as $, type KernelOptions as A, BufferSink as B, Container as C, type DirEntry as D, type ExecContext as E, type FileData as F, type GroupEntry as G, type HttpResponse as H, type InputStream as I, type Job as J, Kernel as K, type ListeningPort as L, type MountEntry as M, type Node as N, type OutputStream as O, type NetInterface as P, type NetworkOptions as Q, type RuntimeVolume as R, Shell as S, NetworkStack as T, NullInput as U, Vfs as V, NullOutput as W, PYTHON_VERSION as X, type PasswdEntry as Y, Pipe as Z, Process as _, type ShellIO as a, type ProcessOptions as a0, type ProcessState as a1, ProcessTable as a2, type PythonOptions as a3, ROOT_CRED as a4, type ResolvedExecutable as a5, type RunOptions as a6, type RunResult as a7, type RuntimeProcessManager as a8, type RuntimeProcessResult as a9, isCPythonAvailable as aA, isPythonAvailable as aB, makeCred as aC, octalMode as aD, parseUmask as aE, resetPidCounter as aF, shellQuote as aG, type SessionInit as aa, type SessionResult as ab, type SessionRunOptions as ac, ShellExit as ad, type ShellInit as ae, type ShellOptions as af, type SpawnHandle as ag, Stats as ah, type Stdio as ai, TeeOutput as aj, UserDatabase as ak, Variables as al, type VirtualNode as am, type VirtualProvider as an, type WriteOptions as ao, applyChmod as ap, braceExpand as aq, captureStdio as ar, configureCPython as as, configurePython as at, createChildProcessModule as au, createContext as av, defineCommand as aw, expandWord as ax, expandWords as ay, formatMode as az, Session as b, type Cred as c, type Command as d, type VolumeStat as e, type VolumeStats as f, type RuntimeHttpResponse as g, type SpawnChild as h, type SyncSpawn as i, type RuntimePod as j, type RuntimePackageInstaller as k, type ChildSpawnConfig as l, type ChildHandle as m, type RuntimeProcess as n, createContainer as o, type CPythonOptions as p, CallbackSink as q, CommandRegistry as r, ContainerFs as s, type ContainerOptions as t, type ContextInit as u, type Env as v, type ExecOptions as w, type ExecResult as x, FileInput as y, FileOutput as z };
1862
+ export { Pipe as $, FileInput as A, BufferSink as B, Container as C, type DirEntry as D, type ExecContext as E, type FileData as F, FileOutput as G, type GroupEntry as H, type HttpResponse as I, type InputStream as J, Kernel as K, type Job as L, type KernelOptions as M, type Node as N, type OutputStream as O, type ListeningPort as P, type MountEntry as Q, type RuntimeVolume as R, Shell as S, type NetInterface as T, type NetworkOptions as U, Vfs as V, NetworkStack as W, NullInput as X, NullOutput as Y, PYTHON_VERSION as Z, type PasswdEntry as _, type ShellIO as a, Process as a0, type ProcessKind as a1, type ProcessOptions as a2, type ProcessState as a3, ProcessTable as a4, type PythonOptions as a5, ROOT_CRED as a6, type ResolvedExecutable as a7, type RunOptions as a8, type RunResult as a9, expandWords as aA, formatMode as aB, isCPythonAvailable as aC, isPythonAvailable as aD, makeCred as aE, octalMode as aF, parseUmask as aG, resetPidCounter as aH, shellQuote as aI, type RuntimeProcessManager as aa, type RuntimeProcessResult as ab, type SessionInit as ac, type SessionResult as ad, type SessionRunOptions as ae, ShellExit as af, type ShellInit as ag, type ShellOptions as ah, type SpawnHandle as ai, Stats as aj, type Stdio as ak, TeeOutput as al, UserDatabase as am, Variables as an, type VirtualNode as ao, type VirtualProvider as ap, type WriteOptions as aq, applyChmod as ar, braceExpand as as, captureStdio as at, configureCPython as au, configurePython as av, createChildProcessModule as aw, createContext as ax, defineCommand as ay, expandWord as az, Session as b, type Cred as c, type Command as d, type VolumeStat as e, type VolumeStats as f, type RuntimeHttpResponse as g, type SpawnChild as h, type SyncSpawn as i, type RuntimePod as j, type RuntimePackageInstaller as k, type ChildSpawnConfig as l, type ChildHandle as m, type RuntimeProcess as n, type RuntimeSocketPeer as o, type RuntimeConnection as p, createContainer as q, type CPythonOptions as r, CallbackSink as s, CommandRegistry as t, ContainerFs as u, type ContainerOptions as v, type ContextInit as w, type Env as x, type ExecOptions as y, type ExecResult as z };
package/dist/index.cjs CHANGED
@@ -23489,8 +23489,17 @@ var VirtualIncomingMessage = class extends streamModule4__default.default.Readab
23489
23489
  httpVersionMajor = 1;
23490
23490
  httpVersionMinor = 1;
23491
23491
  complete = true;
23492
+ /*
23493
+ * Mutable, and `connection` a getter over it, because an upgraded request
23494
+ * carries a real socket rather than the stub. `emit("upgrade", req, socket)`
23495
+ * hands the same object to both places, and a library that reaches it as
23496
+ * `req.socket` — rather than through the argument — has to find the one it
23497
+ * can actually write to.
23498
+ */
23492
23499
  socket = socketStub();
23493
- connection = this.socket;
23500
+ get connection() {
23501
+ return this.socket;
23502
+ }
23494
23503
  constructor(init) {
23495
23504
  super();
23496
23505
  this.method = (init.method ?? "GET").toUpperCase();
@@ -23596,6 +23605,80 @@ var VirtualServerResponse = class extends streamModule4__default.default.Writabl
23596
23605
  return this;
23597
23606
  }
23598
23607
  };
23608
+ var VirtualSocket = class extends streamModule4__default.default.Duplex {
23609
+ remoteAddress = "127.0.0.1";
23610
+ remotePort = 0;
23611
+ localAddress = "127.0.0.1";
23612
+ localPort = 0;
23613
+ encrypted = false;
23614
+ bufferSize = 0;
23615
+ peer;
23616
+ /** Set once the peer is told, so `end()` then `destroy()` reports once. */
23617
+ hungUp = false;
23618
+ constructor(peer) {
23619
+ super({ allowHalfOpen: false });
23620
+ this.peer = peer;
23621
+ }
23622
+ _read() {
23623
+ }
23624
+ _write(chunk, encoding, callback) {
23625
+ const bytes2 = Buffer2.isBuffer(chunk) ? chunk : Buffer2.from(chunk, encoding);
23626
+ try {
23627
+ this.peer?.data(new Uint8Array(bytes2));
23628
+ callback();
23629
+ } catch (error) {
23630
+ callback(error);
23631
+ }
23632
+ }
23633
+ _final(callback) {
23634
+ this.hangUp();
23635
+ callback();
23636
+ }
23637
+ _destroy(error, callback) {
23638
+ this.hangUp();
23639
+ callback(error);
23640
+ }
23641
+ hangUp() {
23642
+ if (this.hungUp) return;
23643
+ this.hungUp = true;
23644
+ try {
23645
+ this.peer?.close();
23646
+ } catch {
23647
+ }
23648
+ this.peer = null;
23649
+ }
23650
+ /** Bytes arriving from the far end. */
23651
+ deliver(bytes2) {
23652
+ if (this.destroyed || this.hungUp) return;
23653
+ this.push(Buffer2.from(bytes2));
23654
+ }
23655
+ /** The far end went away; let the server's stream end cleanly. */
23656
+ peerClosed() {
23657
+ if (this.destroyed) return;
23658
+ this.push(null);
23659
+ }
23660
+ /* The parts of `net.Socket` a WebSocket library reaches for. None of them
23661
+ * mean anything without a kernel, but every one of them is called. */
23662
+ setNoDelay() {
23663
+ return this;
23664
+ }
23665
+ setKeepAlive() {
23666
+ return this;
23667
+ }
23668
+ setTimeout(_milliseconds, callback) {
23669
+ if (callback) this.once("timeout", callback);
23670
+ return this;
23671
+ }
23672
+ destroySoon() {
23673
+ this.end();
23674
+ }
23675
+ ref() {
23676
+ return this;
23677
+ }
23678
+ unref() {
23679
+ return this;
23680
+ }
23681
+ };
23599
23682
  var VirtualHttpServer = class extends EventEmitter4__default.default {
23600
23683
  constructor(router, owner, listener) {
23601
23684
  super();
@@ -23695,6 +23778,33 @@ var VirtualHttpRouter = class {
23695
23778
  return { statusCode: 500, statusMessage: "Internal Server Error", headers: {}, body: error instanceof Error ? error.message : String(error) };
23696
23779
  }
23697
23780
  }
23781
+ /**
23782
+ * Open a connection that leaves HTTP behind — a WebSocket, in practice.
23783
+ *
23784
+ * The counterpart to {@link request}, and the thing whose absence made a dev
23785
+ * server look broken. Every Node WebSocket library is built the same way: it
23786
+ * hands `http.Server` an `upgrade` listener and waits. Nothing here ever
23787
+ * emitted one, so `ws` sat holding a server that could not receive a single
23788
+ * connection, and Vite lost the channel it uses to tell a page to reload —
23789
+ * which is the only way it can recover after re-optimizing dependencies.
23790
+ *
23791
+ * Null means there is nothing to connect to: no server on the port, or a
23792
+ * server that never asked for upgrades. Both are refusals the caller should
23793
+ * report rather than wait out.
23794
+ */
23795
+ connect(port, init, peer) {
23796
+ const item = this.servers.get(port);
23797
+ if (!item) return null;
23798
+ if (item.server.listenerCount("upgrade") === 0) return null;
23799
+ const request = new VirtualIncomingMessage(init);
23800
+ const socket = new VirtualSocket(peer);
23801
+ request.socket = socket;
23802
+ item.server.emit("upgrade", request, socket, Buffer2.alloc(0));
23803
+ return {
23804
+ send: (bytes2) => socket.deliver(bytes2),
23805
+ close: () => socket.peerClosed()
23806
+ };
23807
+ }
23698
23808
  };
23699
23809
  var VirtualClientResponse = class extends streamModule4__default.default.Readable {
23700
23810
  constructor(statusCode, statusMessage, headers, body) {
@@ -27211,6 +27321,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
27211
27321
  async request(_port, _init = {}) {
27212
27322
  return this.router.request(_port, _init);
27213
27323
  }
27324
+ connect(port, init, peer) {
27325
+ return this.router.connect(port, init, peer);
27326
+ }
27214
27327
  snapshot() {
27215
27328
  this.assertActive();
27216
27329
  return this.volume.snapshot();
@@ -27646,6 +27759,15 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27646
27759
  case "http-response":
27647
27760
  this.settleProxied(Number(message.id), message.response);
27648
27761
  return;
27762
+ case "ws-data":
27763
+ this.upgraded.get(Number(message.id))?.write(new Uint8Array(message.data));
27764
+ return;
27765
+ case "ws-close": {
27766
+ const socket = this.upgraded.get(Number(message.id));
27767
+ this.upgraded.delete(Number(message.id));
27768
+ socket?.end();
27769
+ return;
27770
+ }
27649
27771
  case "child-start":
27650
27772
  this.startChild(worker, children, message, ownedChildren);
27651
27773
  return;
@@ -27734,6 +27856,8 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27734
27856
  // ── HTTP servers living on another thread ─────────────────────────────────
27735
27857
  proxies = /* @__PURE__ */ new Map();
27736
27858
  waiting = /* @__PURE__ */ new Map();
27859
+ /** Upgraded connections, by the id the Worker knows them as. */
27860
+ upgraded = /* @__PURE__ */ new Map();
27737
27861
  nextRequestId = 1;
27738
27862
  /**
27739
27863
  * Register a stand-in for a server that is actually running in the Worker.
@@ -27746,6 +27870,9 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27746
27870
  const server = new VirtualHttpServer(this.router, owner, (request, response) => {
27747
27871
  void this.forward(worker, port, request, response);
27748
27872
  });
27873
+ server.on("upgrade", (request, socket) => {
27874
+ this.upgrade(worker, port, request, socket);
27875
+ });
27749
27876
  try {
27750
27877
  server.listen(port);
27751
27878
  this.proxies.set(port, server);
@@ -27776,6 +27903,33 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27776
27903
  response.writeHead(Number(result.statusCode ?? 200), String(result.statusMessage ?? ""), result.headers ?? {});
27777
27904
  response.end(result.body ?? new Uint8Array());
27778
27905
  }
27906
+ /**
27907
+ * Tunnel one upgraded connection to the server that actually holds the port.
27908
+ *
27909
+ * Unlike {@link forward} there is no reply to wait for: both ends write
27910
+ * whenever they have something, until one of them stops. The id is what ties
27911
+ * the two directions together across the Worker boundary.
27912
+ */
27913
+ upgrade(worker, port, request, socket) {
27914
+ const id = this.nextRequestId++;
27915
+ this.upgraded.set(id, socket);
27916
+ socket.on("data", (chunk) => {
27917
+ const bytes2 = new Uint8Array(chunk);
27918
+ worker.postMessage({ type: "ws-data", id, data: bytes2 });
27919
+ });
27920
+ const drop = () => {
27921
+ if (!this.upgraded.delete(id)) return;
27922
+ worker.postMessage({ type: "ws-close", id });
27923
+ };
27924
+ socket.on("close", drop);
27925
+ socket.on("error", drop);
27926
+ worker.postMessage({
27927
+ type: "ws-open",
27928
+ id,
27929
+ port,
27930
+ init: { method: request.method, path: request.url, headers: request.headers }
27931
+ });
27932
+ }
27779
27933
  settleProxied(id, response) {
27780
27934
  const resolve2 = this.waiting.get(id);
27781
27935
  if (!resolve2) return;
@@ -27788,6 +27942,10 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27788
27942
  server.close();
27789
27943
  this.proxies.delete(port);
27790
27944
  }
27945
+ for (const [id, socket] of [...this.upgraded]) {
27946
+ this.upgraded.delete(id);
27947
+ socket.destroy();
27948
+ }
27791
27949
  }
27792
27950
  teardown() {
27793
27951
  for (const { worker, server } of [...this.live]) {
@@ -28161,6 +28319,29 @@ var Container = class _Container {
28161
28319
  }
28162
28320
  };
28163
28321
  }
28322
+ /**
28323
+ * Open a connection to a server inside the container that upgrades out of
28324
+ * HTTP — in practice, a WebSocket.
28325
+ *
28326
+ * The counterpart to {@link request}, and the thing a dev server needs that
28327
+ * a request cannot provide. Bytes go in with `send`, come back through
28328
+ * `peer.data`, and neither side interprets them: the container runs whatever
28329
+ * WebSocket library the program chose, and the caller is responsible for
28330
+ * speaking the protocol it answers with.
28331
+ *
28332
+ * Null means there is nothing to talk to — no server on the port, or one
28333
+ * that never registered an `upgrade` handler. Both are worth reporting
28334
+ * rather than waiting out, because neither resolves on its own.
28335
+ */
28336
+ connect(port, init = {}, peer) {
28337
+ this.assertActive();
28338
+ if (!this.pod.connect) return null;
28339
+ return this.pod.connect(
28340
+ port,
28341
+ { method: init.method ?? "GET", path: init.path ?? "/", headers: init.headers ?? {} },
28342
+ peer
28343
+ );
28344
+ }
28164
28345
  /**
28165
28346
  * Deliver a request whose body is bytes, without letting them become text.
28166
28347
  *
@@ -28732,6 +28913,12 @@ init_arith();
28732
28913
  init_expand();
28733
28914
  init_builtins();
28734
28915
 
28916
+ // src/preview/ws-client.ts
28917
+ var WS_OPEN = "sandboxedjs:ws-open";
28918
+ var WS_DATA = "sandboxedjs:ws-data";
28919
+ var WS_CLOSE = "sandboxedjs:ws-close";
28920
+ var WS_ERROR = "sandboxedjs:ws-error";
28921
+
28735
28922
  // src/preview/register.ts
28736
28923
  function serveContainerOn(port, box) {
28737
28924
  port.onmessage = async (event) => {
@@ -28817,7 +29004,7 @@ async function createPreview(box, options = {}) {
28817
29004
  channel = new MessageChannel();
28818
29005
  serveContainerOn(channel.port1, box);
28819
29006
  const target = registration.active ?? worker;
28820
- target.postMessage({ type: "sandboxedjs:connect" }, [channel.port2]);
29007
+ target.postMessage({ type: "sandboxedjs:connect", injectSockets: options.websocket !== false }, [channel.port2]);
28821
29008
  };
28822
29009
  const onWorkerMessage = (event) => {
28823
29010
  const type = event.data?.type;
@@ -28826,6 +29013,56 @@ async function createPreview(box, options = {}) {
28826
29013
  };
28827
29014
  navigator.serviceWorker.addEventListener("message", onWorkerMessage);
28828
29015
  connect();
29016
+ const sockets = /* @__PURE__ */ new Map();
29017
+ const onFrameMessage = (event) => {
29018
+ if (event.origin !== location.origin || !event.source) return;
29019
+ const data = event.data;
29020
+ if (!data || typeof data !== "object") return;
29021
+ if (data.type !== WS_OPEN && data.type !== WS_DATA && data.type !== WS_CLOSE) return;
29022
+ const source = event.source;
29023
+ const id = Number(data.id);
29024
+ const reply = (message, transfer = []) => source.postMessage(message, location.origin, transfer);
29025
+ if (data.type === WS_OPEN) {
29026
+ let table2 = sockets.get(source);
29027
+ if (!table2) sockets.set(source, table2 = /* @__PURE__ */ new Map());
29028
+ let opened = null;
29029
+ try {
29030
+ opened = box.connect(
29031
+ Number(data.port),
29032
+ { method: "GET", path: data.path ?? "/", headers: data.headers ?? {} },
29033
+ {
29034
+ data: (bytes2) => {
29035
+ const carrier = bytes2.slice().buffer;
29036
+ reply({ type: WS_DATA, id, data: carrier }, [carrier]);
29037
+ },
29038
+ close: () => {
29039
+ table2.delete(id);
29040
+ reply({ type: WS_CLOSE, id });
29041
+ }
29042
+ }
29043
+ );
29044
+ } catch (error) {
29045
+ opened = null;
29046
+ reply({ type: WS_ERROR, id, message: error instanceof Error ? error.message : String(error) });
29047
+ return;
29048
+ }
29049
+ if (!opened) {
29050
+ reply({ type: WS_ERROR, id, message: `Nothing is accepting WebSocket connections on port ${data.port}.` });
29051
+ return;
29052
+ }
29053
+ table2.set(id, opened);
29054
+ return;
29055
+ }
29056
+ const table = sockets.get(source);
29057
+ const socket = table?.get(id);
29058
+ if (!socket) return;
29059
+ if (data.type === WS_DATA) socket.send(new Uint8Array(data.data));
29060
+ else {
29061
+ socket.close();
29062
+ table.delete(id);
29063
+ }
29064
+ };
29065
+ if (typeof window !== "undefined") window.addEventListener("message", onFrameMessage);
28829
29066
  const base2 = registration.scope.replace(/\/$/, "");
28830
29067
  const urlFor = (port) => `${base2}/__sbx__/${port}/`;
28831
29068
  return {
@@ -28839,6 +29076,9 @@ async function createPreview(box, options = {}) {
28839
29076
  },
28840
29077
  dispose: async () => {
28841
29078
  navigator.serviceWorker.removeEventListener("message", onWorkerMessage);
29079
+ if (typeof window !== "undefined") window.removeEventListener("message", onFrameMessage);
29080
+ for (const table of sockets.values()) for (const socket of table.values()) socket.close();
29081
+ sockets.clear();
28842
29082
  channel?.port1.close();
28843
29083
  channel = null;
28844
29084
  await registration.unregister();