sandboxedjs 0.1.45 → 0.1.47

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
@@ -20318,6 +20318,8 @@ var CleanPackageInstaller = class _CleanPackageInstaller {
20318
20318
  fetcher;
20319
20319
  metadata = /* @__PURE__ */ new Map();
20320
20320
  tarballs = /* @__PURE__ */ new Map();
20321
+ /** Single-version manifests, for the fields the packument omits. */
20322
+ details = /* @__PURE__ */ new Map();
20321
20323
  forCwd(cwd) {
20322
20324
  return new _CleanPackageInstaller(this.volume, { ...this.options, cwd });
20323
20325
  }
@@ -20343,10 +20345,17 @@ var CleanPackageInstaller = class _CleanPackageInstaller {
20343
20345
  const version = resolveVersion(metadata, range);
20344
20346
  const manifest = metadata.versions[version];
20345
20347
  if (!manifest) throw new Error(`No matching version found for ${name}@${range}`);
20346
- if (!supportsPlatform(manifest)) {
20347
- throw new Error(`${name}@${version} is not compatible with linux/x64/glibc`);
20348
- }
20349
20348
  const identity = `${name}@${version}`;
20349
+ if (!supportsPlatform(manifest)) throw new IncompatiblePlatform(identity, "");
20350
+ if (manifest.os?.length || manifest.cpu?.length) {
20351
+ const detail = await this.platformDetail(name, version);
20352
+ if (!platformListAllows(detail.libc, PLATFORM.libc)) {
20353
+ throw new IncompatiblePlatform(identity, ` (needs ${detail.libc?.join(", ")})`);
20354
+ }
20355
+ if (detail.main?.endsWith(".node")) {
20356
+ throw new IncompatiblePlatform(identity, " (native addon; this runtime cannot dlopen)");
20357
+ }
20358
+ }
20350
20359
  const target = join(modulesRoot, name);
20351
20360
  const installed2 = this.tryReadJson(join(target, "package.json"));
20352
20361
  if (installed2?.version === version) return installed2;
@@ -20368,11 +20377,44 @@ var CleanPackageInstaller = class _CleanPackageInstaller {
20368
20377
  try {
20369
20378
  await this.installAt(dependency, dependencyRange, childRoot, options, nextAncestry);
20370
20379
  } catch (error) {
20380
+ if (error instanceof IncompatiblePlatform) continue;
20371
20381
  options.onProgress?.(`Skipped optional ${dependency}: ${error instanceof Error ? error.message : String(error)}`);
20372
20382
  }
20373
20383
  }
20374
20384
  return manifest;
20375
20385
  }
20386
+ /**
20387
+ * The `libc` and `main` fields, which the abbreviated packument leaves out.
20388
+ *
20389
+ * Fetched per version rather than as a full packument: the single-version
20390
+ * document is a few kilobytes, while the full one for a popular package runs
20391
+ * to megabytes. Only packages that already declare `os` or `cpu` ask for it,
20392
+ * so an ordinary install of pure-JavaScript dependencies makes no extra
20393
+ * requests at all — it is the prebuilt binaries, a handful per project, that
20394
+ * need the answer.
20395
+ *
20396
+ * A failure here is not fatal. Being unable to read `libc` puts the check
20397
+ * back where it was before this existed, which is worth strictly less than
20398
+ * being right and strictly more than refusing to install.
20399
+ */
20400
+ async platformDetail(name, version) {
20401
+ const key = `${name}@${version}`;
20402
+ let request = this.details.get(key);
20403
+ if (!request) {
20404
+ request = (async () => {
20405
+ try {
20406
+ const encoded = name.startsWith("@") ? name.replace("/", "%2F") : encodeURIComponent(name);
20407
+ const response = await this.fetcher(`${this.registry}/${encoded}/${version}`);
20408
+ if (!response.ok) return {};
20409
+ return await response.json();
20410
+ } catch {
20411
+ return {};
20412
+ }
20413
+ })();
20414
+ this.details.set(key, request);
20415
+ }
20416
+ return request;
20417
+ }
20376
20418
  async getMetadata(name) {
20377
20419
  let request = this.metadata.get(name);
20378
20420
  if (!request) {
@@ -20440,8 +20482,15 @@ var CleanPackageInstaller = class _CleanPackageInstaller {
20440
20482
  }
20441
20483
  }
20442
20484
  };
20485
+ var PLATFORM = { os: "linux", cpu: "x64", libc: "glibc" };
20486
+ var IncompatiblePlatform = class extends Error {
20487
+ constructor(identity, detail) {
20488
+ super(`${identity} is not compatible with ${PLATFORM.os}/${PLATFORM.cpu}/${PLATFORM.libc}${detail}`);
20489
+ this.name = "IncompatiblePlatform";
20490
+ }
20491
+ };
20443
20492
  function supportsPlatform(manifest) {
20444
- return !manifest.main?.endsWith(".node") && platformListAllows(manifest.os, "linux") && platformListAllows(manifest.cpu, "x64") && platformListAllows(manifest.libc, "glibc");
20493
+ return platformListAllows(manifest.os, PLATFORM.os) && platformListAllows(manifest.cpu, PLATFORM.cpu) && platformListAllows(manifest.libc, PLATFORM.libc);
20445
20494
  }
20446
20495
  function platformListAllows(values, current) {
20447
20496
  if (!values?.length) return true;
@@ -23489,8 +23538,17 @@ var VirtualIncomingMessage = class extends streamModule4__default.default.Readab
23489
23538
  httpVersionMajor = 1;
23490
23539
  httpVersionMinor = 1;
23491
23540
  complete = true;
23541
+ /*
23542
+ * Mutable, and `connection` a getter over it, because an upgraded request
23543
+ * carries a real socket rather than the stub. `emit("upgrade", req, socket)`
23544
+ * hands the same object to both places, and a library that reaches it as
23545
+ * `req.socket` — rather than through the argument — has to find the one it
23546
+ * can actually write to.
23547
+ */
23492
23548
  socket = socketStub();
23493
- connection = this.socket;
23549
+ get connection() {
23550
+ return this.socket;
23551
+ }
23494
23552
  constructor(init) {
23495
23553
  super();
23496
23554
  this.method = (init.method ?? "GET").toUpperCase();
@@ -23596,6 +23654,80 @@ var VirtualServerResponse = class extends streamModule4__default.default.Writabl
23596
23654
  return this;
23597
23655
  }
23598
23656
  };
23657
+ var VirtualSocket = class extends streamModule4__default.default.Duplex {
23658
+ remoteAddress = "127.0.0.1";
23659
+ remotePort = 0;
23660
+ localAddress = "127.0.0.1";
23661
+ localPort = 0;
23662
+ encrypted = false;
23663
+ bufferSize = 0;
23664
+ peer;
23665
+ /** Set once the peer is told, so `end()` then `destroy()` reports once. */
23666
+ hungUp = false;
23667
+ constructor(peer) {
23668
+ super({ allowHalfOpen: false });
23669
+ this.peer = peer;
23670
+ }
23671
+ _read() {
23672
+ }
23673
+ _write(chunk, encoding, callback) {
23674
+ const bytes2 = Buffer2.isBuffer(chunk) ? chunk : Buffer2.from(chunk, encoding);
23675
+ try {
23676
+ this.peer?.data(new Uint8Array(bytes2));
23677
+ callback();
23678
+ } catch (error) {
23679
+ callback(error);
23680
+ }
23681
+ }
23682
+ _final(callback) {
23683
+ this.hangUp();
23684
+ callback();
23685
+ }
23686
+ _destroy(error, callback) {
23687
+ this.hangUp();
23688
+ callback(error);
23689
+ }
23690
+ hangUp() {
23691
+ if (this.hungUp) return;
23692
+ this.hungUp = true;
23693
+ try {
23694
+ this.peer?.close();
23695
+ } catch {
23696
+ }
23697
+ this.peer = null;
23698
+ }
23699
+ /** Bytes arriving from the far end. */
23700
+ deliver(bytes2) {
23701
+ if (this.destroyed || this.hungUp) return;
23702
+ this.push(Buffer2.from(bytes2));
23703
+ }
23704
+ /** The far end went away; let the server's stream end cleanly. */
23705
+ peerClosed() {
23706
+ if (this.destroyed) return;
23707
+ this.push(null);
23708
+ }
23709
+ /* The parts of `net.Socket` a WebSocket library reaches for. None of them
23710
+ * mean anything without a kernel, but every one of them is called. */
23711
+ setNoDelay() {
23712
+ return this;
23713
+ }
23714
+ setKeepAlive() {
23715
+ return this;
23716
+ }
23717
+ setTimeout(_milliseconds, callback) {
23718
+ if (callback) this.once("timeout", callback);
23719
+ return this;
23720
+ }
23721
+ destroySoon() {
23722
+ this.end();
23723
+ }
23724
+ ref() {
23725
+ return this;
23726
+ }
23727
+ unref() {
23728
+ return this;
23729
+ }
23730
+ };
23599
23731
  var VirtualHttpServer = class extends EventEmitter4__default.default {
23600
23732
  constructor(router, owner, listener) {
23601
23733
  super();
@@ -23695,6 +23827,33 @@ var VirtualHttpRouter = class {
23695
23827
  return { statusCode: 500, statusMessage: "Internal Server Error", headers: {}, body: error instanceof Error ? error.message : String(error) };
23696
23828
  }
23697
23829
  }
23830
+ /**
23831
+ * Open a connection that leaves HTTP behind — a WebSocket, in practice.
23832
+ *
23833
+ * The counterpart to {@link request}, and the thing whose absence made a dev
23834
+ * server look broken. Every Node WebSocket library is built the same way: it
23835
+ * hands `http.Server` an `upgrade` listener and waits. Nothing here ever
23836
+ * emitted one, so `ws` sat holding a server that could not receive a single
23837
+ * connection, and Vite lost the channel it uses to tell a page to reload —
23838
+ * which is the only way it can recover after re-optimizing dependencies.
23839
+ *
23840
+ * Null means there is nothing to connect to: no server on the port, or a
23841
+ * server that never asked for upgrades. Both are refusals the caller should
23842
+ * report rather than wait out.
23843
+ */
23844
+ connect(port, init, peer) {
23845
+ const item = this.servers.get(port);
23846
+ if (!item) return null;
23847
+ if (item.server.listenerCount("upgrade") === 0) return null;
23848
+ const request = new VirtualIncomingMessage(init);
23849
+ const socket = new VirtualSocket(peer);
23850
+ request.socket = socket;
23851
+ item.server.emit("upgrade", request, socket, Buffer2.alloc(0));
23852
+ return {
23853
+ send: (bytes2) => socket.deliver(bytes2),
23854
+ close: () => socket.peerClosed()
23855
+ };
23856
+ }
23698
23857
  };
23699
23858
  var VirtualClientResponse = class extends streamModule4__default.default.Readable {
23700
23859
  constructor(statusCode, statusMessage, headers, body) {
@@ -27211,6 +27370,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
27211
27370
  async request(_port, _init = {}) {
27212
27371
  return this.router.request(_port, _init);
27213
27372
  }
27373
+ connect(port, init, peer) {
27374
+ return this.router.connect(port, init, peer);
27375
+ }
27214
27376
  snapshot() {
27215
27377
  this.assertActive();
27216
27378
  return this.volume.snapshot();
@@ -27646,6 +27808,15 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27646
27808
  case "http-response":
27647
27809
  this.settleProxied(Number(message.id), message.response);
27648
27810
  return;
27811
+ case "ws-data":
27812
+ this.upgraded.get(Number(message.id))?.write(new Uint8Array(message.data));
27813
+ return;
27814
+ case "ws-close": {
27815
+ const socket = this.upgraded.get(Number(message.id));
27816
+ this.upgraded.delete(Number(message.id));
27817
+ socket?.end();
27818
+ return;
27819
+ }
27649
27820
  case "child-start":
27650
27821
  this.startChild(worker, children, message, ownedChildren);
27651
27822
  return;
@@ -27734,6 +27905,8 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27734
27905
  // ── HTTP servers living on another thread ─────────────────────────────────
27735
27906
  proxies = /* @__PURE__ */ new Map();
27736
27907
  waiting = /* @__PURE__ */ new Map();
27908
+ /** Upgraded connections, by the id the Worker knows them as. */
27909
+ upgraded = /* @__PURE__ */ new Map();
27737
27910
  nextRequestId = 1;
27738
27911
  /**
27739
27912
  * Register a stand-in for a server that is actually running in the Worker.
@@ -27746,6 +27919,9 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27746
27919
  const server = new VirtualHttpServer(this.router, owner, (request, response) => {
27747
27920
  void this.forward(worker, port, request, response);
27748
27921
  });
27922
+ server.on("upgrade", (request, socket) => {
27923
+ this.upgrade(worker, port, request, socket);
27924
+ });
27749
27925
  try {
27750
27926
  server.listen(port);
27751
27927
  this.proxies.set(port, server);
@@ -27776,6 +27952,33 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27776
27952
  response.writeHead(Number(result.statusCode ?? 200), String(result.statusMessage ?? ""), result.headers ?? {});
27777
27953
  response.end(result.body ?? new Uint8Array());
27778
27954
  }
27955
+ /**
27956
+ * Tunnel one upgraded connection to the server that actually holds the port.
27957
+ *
27958
+ * Unlike {@link forward} there is no reply to wait for: both ends write
27959
+ * whenever they have something, until one of them stops. The id is what ties
27960
+ * the two directions together across the Worker boundary.
27961
+ */
27962
+ upgrade(worker, port, request, socket) {
27963
+ const id = this.nextRequestId++;
27964
+ this.upgraded.set(id, socket);
27965
+ socket.on("data", (chunk) => {
27966
+ const bytes2 = new Uint8Array(chunk);
27967
+ worker.postMessage({ type: "ws-data", id, data: bytes2 });
27968
+ });
27969
+ const drop = () => {
27970
+ if (!this.upgraded.delete(id)) return;
27971
+ worker.postMessage({ type: "ws-close", id });
27972
+ };
27973
+ socket.on("close", drop);
27974
+ socket.on("error", drop);
27975
+ worker.postMessage({
27976
+ type: "ws-open",
27977
+ id,
27978
+ port,
27979
+ init: { method: request.method, path: request.url, headers: request.headers }
27980
+ });
27981
+ }
27779
27982
  settleProxied(id, response) {
27780
27983
  const resolve2 = this.waiting.get(id);
27781
27984
  if (!resolve2) return;
@@ -27788,6 +27991,10 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27788
27991
  server.close();
27789
27992
  this.proxies.delete(port);
27790
27993
  }
27994
+ for (const [id, socket] of [...this.upgraded]) {
27995
+ this.upgraded.delete(id);
27996
+ socket.destroy();
27997
+ }
27791
27998
  }
27792
27999
  teardown() {
27793
28000
  for (const { worker, server } of [...this.live]) {
@@ -28161,6 +28368,29 @@ var Container = class _Container {
28161
28368
  }
28162
28369
  };
28163
28370
  }
28371
+ /**
28372
+ * Open a connection to a server inside the container that upgrades out of
28373
+ * HTTP — in practice, a WebSocket.
28374
+ *
28375
+ * The counterpart to {@link request}, and the thing a dev server needs that
28376
+ * a request cannot provide. Bytes go in with `send`, come back through
28377
+ * `peer.data`, and neither side interprets them: the container runs whatever
28378
+ * WebSocket library the program chose, and the caller is responsible for
28379
+ * speaking the protocol it answers with.
28380
+ *
28381
+ * Null means there is nothing to talk to — no server on the port, or one
28382
+ * that never registered an `upgrade` handler. Both are worth reporting
28383
+ * rather than waiting out, because neither resolves on its own.
28384
+ */
28385
+ connect(port, init = {}, peer) {
28386
+ this.assertActive();
28387
+ if (!this.pod.connect) return null;
28388
+ return this.pod.connect(
28389
+ port,
28390
+ { method: init.method ?? "GET", path: init.path ?? "/", headers: init.headers ?? {} },
28391
+ peer
28392
+ );
28393
+ }
28164
28394
  /**
28165
28395
  * Deliver a request whose body is bytes, without letting them become text.
28166
28396
  *
@@ -28732,6 +28962,12 @@ init_arith();
28732
28962
  init_expand();
28733
28963
  init_builtins();
28734
28964
 
28965
+ // src/preview/ws-client.ts
28966
+ var WS_OPEN = "sandboxedjs:ws-open";
28967
+ var WS_DATA = "sandboxedjs:ws-data";
28968
+ var WS_CLOSE = "sandboxedjs:ws-close";
28969
+ var WS_ERROR = "sandboxedjs:ws-error";
28970
+
28735
28971
  // src/preview/register.ts
28736
28972
  function serveContainerOn(port, box) {
28737
28973
  port.onmessage = async (event) => {
@@ -28817,7 +29053,7 @@ async function createPreview(box, options = {}) {
28817
29053
  channel = new MessageChannel();
28818
29054
  serveContainerOn(channel.port1, box);
28819
29055
  const target = registration.active ?? worker;
28820
- target.postMessage({ type: "sandboxedjs:connect" }, [channel.port2]);
29056
+ target.postMessage({ type: "sandboxedjs:connect", injectSockets: options.websocket !== false }, [channel.port2]);
28821
29057
  };
28822
29058
  const onWorkerMessage = (event) => {
28823
29059
  const type = event.data?.type;
@@ -28826,6 +29062,56 @@ async function createPreview(box, options = {}) {
28826
29062
  };
28827
29063
  navigator.serviceWorker.addEventListener("message", onWorkerMessage);
28828
29064
  connect();
29065
+ const sockets = /* @__PURE__ */ new Map();
29066
+ const onFrameMessage = (event) => {
29067
+ if (event.origin !== location.origin || !event.source) return;
29068
+ const data = event.data;
29069
+ if (!data || typeof data !== "object") return;
29070
+ if (data.type !== WS_OPEN && data.type !== WS_DATA && data.type !== WS_CLOSE) return;
29071
+ const source = event.source;
29072
+ const id = Number(data.id);
29073
+ const reply = (message, transfer = []) => source.postMessage(message, location.origin, transfer);
29074
+ if (data.type === WS_OPEN) {
29075
+ let table2 = sockets.get(source);
29076
+ if (!table2) sockets.set(source, table2 = /* @__PURE__ */ new Map());
29077
+ let opened = null;
29078
+ try {
29079
+ opened = box.connect(
29080
+ Number(data.port),
29081
+ { method: "GET", path: data.path ?? "/", headers: data.headers ?? {} },
29082
+ {
29083
+ data: (bytes2) => {
29084
+ const carrier = bytes2.slice().buffer;
29085
+ reply({ type: WS_DATA, id, data: carrier }, [carrier]);
29086
+ },
29087
+ close: () => {
29088
+ table2.delete(id);
29089
+ reply({ type: WS_CLOSE, id });
29090
+ }
29091
+ }
29092
+ );
29093
+ } catch (error) {
29094
+ opened = null;
29095
+ reply({ type: WS_ERROR, id, message: error instanceof Error ? error.message : String(error) });
29096
+ return;
29097
+ }
29098
+ if (!opened) {
29099
+ reply({ type: WS_ERROR, id, message: `Nothing is accepting WebSocket connections on port ${data.port}.` });
29100
+ return;
29101
+ }
29102
+ table2.set(id, opened);
29103
+ return;
29104
+ }
29105
+ const table = sockets.get(source);
29106
+ const socket = table?.get(id);
29107
+ if (!socket) return;
29108
+ if (data.type === WS_DATA) socket.send(new Uint8Array(data.data));
29109
+ else {
29110
+ socket.close();
29111
+ table.delete(id);
29112
+ }
29113
+ };
29114
+ if (typeof window !== "undefined") window.addEventListener("message", onFrameMessage);
28829
29115
  const base2 = registration.scope.replace(/\/$/, "");
28830
29116
  const urlFor = (port) => `${base2}/__sbx__/${port}/`;
28831
29117
  return {
@@ -28839,6 +29125,9 @@ async function createPreview(box, options = {}) {
28839
29125
  },
28840
29126
  dispose: async () => {
28841
29127
  navigator.serviceWorker.removeEventListener("message", onWorkerMessage);
29128
+ if (typeof window !== "undefined") window.removeEventListener("message", onFrameMessage);
29129
+ for (const table of sockets.values()) for (const socket of table.values()) socket.close();
29130
+ sockets.clear();
28842
29131
  channel?.port1.close();
28843
29132
  channel = null;
28844
29133
  await registration.unregister();