sandboxedjs 0.2.4 → 0.2.6

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/dist/index.cjs CHANGED
@@ -8374,6 +8374,60 @@ function createSysProvider(kernel) {
8374
8374
  };
8375
8375
  }
8376
8376
 
8377
+ // src/net/proxy-fetch.ts
8378
+ function toBase64(bytes2) {
8379
+ let text2 = "";
8380
+ for (let at = 0; at < bytes2.length; at += 32768) {
8381
+ text2 += String.fromCharCode(...bytes2.subarray(at, at + 32768));
8382
+ }
8383
+ return btoa(text2);
8384
+ }
8385
+ function fromBase64(text2) {
8386
+ const binary = atob(text2);
8387
+ const bytes2 = new Uint8Array(binary.length);
8388
+ for (let at = 0; at < binary.length; at += 1) bytes2[at] = binary.charCodeAt(at);
8389
+ return bytes2;
8390
+ }
8391
+ async function proxyExchange(proxy, request, fetchImpl = fetch) {
8392
+ let answer;
8393
+ try {
8394
+ answer = await fetchImpl(proxy, {
8395
+ method: "POST",
8396
+ headers: { "content-type": "application/json" },
8397
+ body: JSON.stringify({
8398
+ url: request.url,
8399
+ method: request.method,
8400
+ headers: request.headers,
8401
+ ...request.body?.length ? { body: toBase64(request.body) } : {}
8402
+ })
8403
+ });
8404
+ } catch (error) {
8405
+ throw new Error(
8406
+ `the egress proxy at ${proxy} could not be reached (${error instanceof Error ? error.message : String(error)}). It performs this container's outbound requests, so nothing leaves until it answers.`
8407
+ );
8408
+ }
8409
+ if (!answer.ok) {
8410
+ throw new Error(
8411
+ `the egress proxy at ${proxy} refused ${request.method} ${request.url} with ${answer.status} ${answer.statusText}: ${(await answer.text()).trim().slice(0, 300)}`
8412
+ );
8413
+ }
8414
+ const payload = await answer.json();
8415
+ return {
8416
+ status: payload.status ?? 200,
8417
+ statusText: payload.statusText ?? "",
8418
+ headers: payload.headers ?? {},
8419
+ body: payload.body ? fromBase64(payload.body) : new Uint8Array(0)
8420
+ };
8421
+ }
8422
+ function isBrowser() {
8423
+ return typeof process === "undefined" || process.versions?.node == null;
8424
+ }
8425
+ function browserBlocked(hostname, message) {
8426
+ return new Error(
8427
+ `${message} \u2014 the request to ${hostname} was blocked by the browser. A page can only read a response from a host that sends CORS headers, and most APIs send none. Give the container a proxy that makes the request for it \u2014 network: { proxy }, and \`npx sandboxedjs-egress\` is one \u2014 or run the container on a server.`
8428
+ );
8429
+ }
8430
+
8377
8431
  // src/net/policy.ts
8378
8432
  function isLoopbackHostname(hostname) {
8379
8433
  const host2 = hostname.replace(/^\[|\]$/g, "").toLowerCase();
@@ -8416,7 +8470,40 @@ function policedFetch(policy, fetchImpl) {
8416
8470
  if (!outboundAllowed(policy, url)) {
8417
8471
  throw Object.assign(new TypeError("fetch failed"), { cause: outboundBlockedError(url) });
8418
8472
  }
8419
- return fetchImpl(input, init);
8473
+ if (policy.proxy) return await fetchThroughProxy(policy.proxy, fetchImpl, input, init);
8474
+ try {
8475
+ return await fetchImpl(input, init);
8476
+ } catch (error) {
8477
+ if (!isBrowser()) throw error;
8478
+ throw Object.assign(new TypeError("fetch failed"), {
8479
+ cause: browserBlocked(new URL(url).hostname, error instanceof Error ? error.message : String(error))
8480
+ });
8481
+ }
8482
+ });
8483
+ }
8484
+ async function fetchThroughProxy(proxy, fetchImpl, input, init) {
8485
+ const request = new Request(input, init);
8486
+ const headers = {};
8487
+ request.headers.forEach((value, name) => {
8488
+ headers[name] = value;
8489
+ });
8490
+ const bodyless = request.method === "GET" || request.method === "HEAD";
8491
+ const result = await proxyExchange(
8492
+ proxy,
8493
+ {
8494
+ url: request.url,
8495
+ method: request.method,
8496
+ headers,
8497
+ ...bodyless ? {} : { body: new Uint8Array(await request.arrayBuffer()) }
8498
+ },
8499
+ fetchImpl
8500
+ );
8501
+ const nullBody = result.status === 204 || result.status === 205 || result.status === 304 || request.method === "HEAD";
8502
+ const body = nullBody ? null : result.body.slice().buffer;
8503
+ return new Response(body, {
8504
+ status: result.status,
8505
+ statusText: result.statusText,
8506
+ headers: result.headers
8420
8507
  });
8421
8508
  }
8422
8509
  function policedWebSocket(Native, policy) {
@@ -17474,9 +17561,6 @@ function parseUrl(raw) {
17474
17561
  return null;
17475
17562
  }
17476
17563
  }
17477
- function isBrowser() {
17478
- return typeof process === "undefined" || process.versions?.node == null;
17479
- }
17480
17564
  async function performRequest(ctx, url, init) {
17481
17565
  const isLocal = ctx.kernel.net.isLocal(url.hostname);
17482
17566
  if (isLocal) {
@@ -17504,7 +17588,16 @@ async function performRequest(ctx, url, init) {
17504
17588
  );
17505
17589
  }
17506
17590
  const proxy = ctx.kernel.net.proxy;
17507
- if (proxy) return await proxyRequest(ctx, proxy, url, init);
17591
+ if (proxy) {
17592
+ const result = await proxyExchange(proxy, {
17593
+ url: url.toString(),
17594
+ method: init.method,
17595
+ headers: init.headers,
17596
+ ...init.body?.length ? { body: init.body } : {}
17597
+ });
17598
+ ctx.kernel.net.countRx(result.body.length);
17599
+ return { ...result, url: url.toString() };
17600
+ }
17508
17601
  let response;
17509
17602
  try {
17510
17603
  response = await fetch(url.toString(), {
@@ -17513,9 +17606,7 @@ async function performRequest(ctx, url, init) {
17513
17606
  ...init.body ? { body: init.body.slice().buffer } : {}
17514
17607
  });
17515
17608
  } catch (error) {
17516
- throw isBrowser() ? new Error(
17517
- `${error instanceof Error ? error.message : String(error)} \u2014 the request to ${url.hostname} was blocked by the browser. A page can only read a response from a host that sends CORS headers, and most APIs send none. Give the container a proxy that makes the request for it \u2014 network: { proxy }, and \`npx sandboxedjs-egress\` is one \u2014 or run the container on a server.`
17518
- ) : error;
17609
+ throw isBrowser() ? browserBlocked(url.hostname, error instanceof Error ? error.message : String(error)) : error;
17519
17610
  }
17520
17611
  const body = new Uint8Array(await response.arrayBuffer());
17521
17612
  ctx.kernel.net.countRx(body.length);
@@ -17525,53 +17616,6 @@ async function performRequest(ctx, url, init) {
17525
17616
  });
17526
17617
  return { status: response.status, statusText: response.statusText, headers, body, url: response.url || url.toString() };
17527
17618
  }
17528
- async function proxyRequest(ctx, proxy, url, init) {
17529
- let answer;
17530
- try {
17531
- answer = await fetch(proxy, {
17532
- method: "POST",
17533
- headers: { "content-type": "application/json" },
17534
- body: JSON.stringify({
17535
- url: url.toString(),
17536
- method: init.method,
17537
- headers: init.headers,
17538
- ...init.body?.length ? { body: toBase64(init.body) } : {}
17539
- })
17540
- });
17541
- } catch (error) {
17542
- throw new Error(
17543
- `the egress proxy at ${proxy} could not be reached (${error instanceof Error ? error.message : String(error)}). It performs this container's outbound requests, so nothing leaves until it answers.`
17544
- );
17545
- }
17546
- if (!answer.ok) {
17547
- throw new Error(
17548
- `the egress proxy at ${proxy} refused ${init.method} ${url.toString()} with ${answer.status} ${answer.statusText}: ${(await answer.text()).trim().slice(0, 300)}`
17549
- );
17550
- }
17551
- const payload = await answer.json();
17552
- const body = payload.body ? fromBase64(payload.body) : new Uint8Array(0);
17553
- ctx.kernel.net.countRx(body.length);
17554
- return {
17555
- status: payload.status ?? 200,
17556
- statusText: payload.statusText ?? "",
17557
- headers: payload.headers ?? {},
17558
- body,
17559
- url: url.toString()
17560
- };
17561
- }
17562
- function toBase64(bytes2) {
17563
- let text2 = "";
17564
- for (let at = 0; at < bytes2.length; at += 32768) {
17565
- text2 += String.fromCharCode(...bytes2.subarray(at, at + 32768));
17566
- }
17567
- return btoa(text2);
17568
- }
17569
- function fromBase64(text2) {
17570
- const binary = atob(text2);
17571
- const bytes2 = new Uint8Array(binary.length);
17572
- for (let at = 0; at < binary.length; at += 1) bytes2[at] = binary.charCodeAt(at);
17573
- return bytes2;
17574
- }
17575
17619
  var curl = defineCommand({
17576
17620
  name: "curl",
17577
17621
  path: "/usr/bin/curl",
@@ -21013,7 +21057,7 @@ var wheels_default = {
21013
21057
 
21014
21058
  // package.json
21015
21059
  var package_default = {
21016
- version: "0.2.4"};
21060
+ version: "0.2.6"};
21017
21061
 
21018
21062
  // src/python/extension-abi.ts
21019
21063
  var EXTENSION_ABI = {
@@ -22678,8 +22722,12 @@ function createHostAbiServer(proc) {
22678
22722
  }
22679
22723
  case Op.resolve: {
22680
22724
  const sockets = proc.sockets;
22725
+ const name = r.string();
22726
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(name)) {
22727
+ return { status: 0, payload: new Writer().string(name).finish() };
22728
+ }
22681
22729
  if (!sockets?.outbound) throw new PosixError(Errno.ENOSYS);
22682
- const address = sockets.outbound.resolve(r.string());
22730
+ const address = sockets.outbound.resolve(name);
22683
22731
  return { status: 0, payload: new Writer().string(address).finish() };
22684
22732
  }
22685
22733
  case Op.send: {
@@ -24806,6 +24854,77 @@ if os.environ.get("SBX_OUTBOUND_SOCKETS") == "1":
24806
24854
  sys.meta_path.insert(0, _SbxTruststoreHook())
24807
24855
 
24808
24856
 
24857
+ # Literal addresses are answered here rather than through the host.
24858
+ #
24859
+ # The container's resolver is a host call, and a container with no outbound
24860
+ # sockets configured has no resolver to call: it answers ENOSYS. That is the
24861
+ # right answer for a name, but the egress hop below dials 127.0.0.1, and
24862
+ # http.client calls getaddrinfo even for an address that needs no resolving --
24863
+ # so every request through the egress died with "Function not implemented"
24864
+ # before it reached the endpoint. A numeric address (and loopback by its usual
24865
+ # names) is now formed here, which is what a resolver would have returned.
24866
+ def _sbx_local_addresses():
24867
+ import socket as _socket_module
24868
+ import _socket as _socket_native
24869
+
24870
+ # The native entry point, not socket.getaddrinfo: the stdlib wrapper
24871
+ # delegates to _socket, so wrapping the wrapper and replacing _socket with
24872
+ # the wrapper makes the two call each other forever.
24873
+ _real_getaddrinfo = _socket_native.getaddrinfo
24874
+
24875
+ _literals = {"localhost": "127.0.0.1", "localhost.localdomain": "127.0.0.1",
24876
+ "ip6-localhost": "127.0.0.1"}
24877
+
24878
+ def _literal(host):
24879
+ """The address host already is, or None when it needs a resolver."""
24880
+ if isinstance(host, (bytes, bytearray)):
24881
+ host = bytes(host).decode("ascii", "replace")
24882
+ if host is None:
24883
+ return None
24884
+ host = _literals.get(host.lower(), host)
24885
+ parts = host.split(".")
24886
+ if len(parts) != 4:
24887
+ return None
24888
+ for part in parts:
24889
+ if not part.isdigit() or len(part) > 3 or int(part) > 255:
24890
+ return None
24891
+ return host
24892
+
24893
+ def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
24894
+ address = _literal(host)
24895
+ if address is None:
24896
+ return _real_getaddrinfo(host, port, family, type, proto, flags)
24897
+ if isinstance(port, (bytes, bytearray)):
24898
+ port = bytes(port).decode("ascii", "replace")
24899
+ if isinstance(port, str):
24900
+ port = int(port) if port.isdigit() else _socket_module.getservbyname(port)
24901
+ if port is None:
24902
+ port = 0
24903
+ if family not in (0, _socket_module.AF_UNSPEC, _socket_module.AF_INET):
24904
+ raise _socket_module.gaierror(
24905
+ _socket_module.EAI_FAMILY, "only AF_INET is supported in this container"
24906
+ )
24907
+ kind = type or _socket_module.SOCK_STREAM
24908
+ protocol = proto or (_socket_module.IPPROTO_TCP
24909
+ if kind == _socket_module.SOCK_STREAM else 0)
24910
+ return [(_socket_module.AF_INET, kind, protocol, "", (address, int(port)))]
24911
+
24912
+ def gethostbyname(host):
24913
+ return getaddrinfo(host, 0)[0][4][0]
24914
+
24915
+ def gethostbyname_ex(host):
24916
+ return (host, [], [gethostbyname(host)])
24917
+
24918
+ _socket_module.getaddrinfo = getaddrinfo
24919
+ _socket_module.gethostbyname = gethostbyname
24920
+ _socket_module.gethostbyname_ex = gethostbyname_ex
24921
+ _socket_native.getaddrinfo = getaddrinfo
24922
+ _socket_native.gethostbyname = gethostbyname
24923
+ _socket_native.gethostbyname_ex = gethostbyname_ex
24924
+
24925
+
24926
+ _sbx_local_addresses()
24927
+
24809
24928
  _egress = None if os.environ.get("SBX_OUTBOUND_SOCKETS") == "1" else os.environ.get("SBX_HTTP_EGRESS")
24810
24929
  if _egress:
24811
24930
  import http.client
@@ -37825,7 +37944,8 @@ var Container = class _Container {
37825
37944
  if (opts.python) configurePython(opts.python);
37826
37945
  const network = {
37827
37946
  allowOutbound: opts.network?.allowOutbound ?? false,
37828
- allowedHosts: opts.network?.allowedHosts ?? null
37947
+ allowedHosts: opts.network?.allowedHosts ?? null,
37948
+ ...opts.network?.proxy ? { proxy: opts.network.proxy } : {}
37829
37949
  };
37830
37950
  const podOptions = {
37831
37951
  workdir: opts.cwd ?? "/",
package/dist/index.d.cts CHANGED
@@ -1,8 +1,8 @@
1
- import { S as Shell, a as ShellIO, b as Session, N as Node, c as Command, K as Kernel, E as ExecContext, O as OutputStream, C as Container, B as BinaryBackend, d as createContainer } from './container-DJfFjito.cjs';
2
- export { e as BinaryExecutionRegistry, f as BinaryInfo, g as BinaryRequest, h as BufferSink, i as CallbackSink, j as CommandRegistry, k as ContainerFs, l as ContainerOptions, m as ContextInit, n as Env, o as ExecOptions, p as ExecResult, q as ExecutionDecision, r as ExecutionTier, F as FileData, s as FileInput, t as FileOutput, G as GroupEntry, H as HttpResponse, I as InputStream, J as Job, u as KernelOptions, L as ListeningPort, M as MANIFEST_FORMAT, v as MANIFEST_SCHEMA_VERSION, w as MountEntry, x as NetInterface, y as NetworkOptions, z as NetworkStack, A as NullInput, D as NullOutput, P as PYTHON_VERSION, Q as PasswdEntry, R as Pipe, T as Preparation, U as PreparedBinary, V as Process, W as ProcessKind, X as ProcessOptions, Y as ProcessState, Z as ProcessTable, _ as PythonCapabilities, $ as PythonOptions, a0 as PythonProfile, a1 as PythonRuntimeManifest, a2 as ResolvedExecutable, a3 as RunOptions, a4 as RunResult, a5 as SessionInit, a6 as SessionResult, a7 as SessionRunOptions, a8 as ShellExit, a9 as ShellInit, aa as ShellOptions, ab as SpawnHandle, ac as Stdio, ad as TeeOutput, ae as UserDatabase, af as Variables, ag as WasmCommandArtifact, ah as WasmTranslator, ai as binaryDigest, aj as braceExpand, ak as captureStdio, al as configurePython, am as createContext, an as createTranslationBackend, ao as createWasmCompatibilityBackend, ap as defineCommand, aq as expandWord, ar as expandWords, as as inspectElf, at as installWasmCommands, au as isElfBinary, av as isPythonAvailable, aw as resetPidCounter, ax as shellQuote, ay as validateManifest } from './container-DJfFjito.cjs';
3
- import { V as Vfs, C as Cred, R as RuntimeVolume, a as VirtualTcpNetwork, b as RuntimeHttpResponse, O as OutboundPolicy, S as SpawnChild, c as SyncSpawn, I as IpcTransport, d as VolumeStat, e as VolumeStats, f as RuntimePod, g as RuntimePackageInstaller, h as ChildSpawnConfig, i as ChildHandle, j as RuntimeProcess, k as RuntimeSocketPeer, l as RuntimeConnection } from './contracts-CXZ_25-O.cjs';
4
- export { D as DirEntry, m as ROOT_CRED, n as RuntimeProcessManager, o as RuntimeProcessResult, p as Stats, q as VirtualNode, r as VirtualProvider, W as WriteOptions, s as applyChmod, t as createChildProcessModule, u as formatMode, v as makeCred, w as octalMode, x as parseUmask } from './contracts-CXZ_25-O.cjs';
5
- import { M as MemoryVolume, a as MemoryVolumeSnapshotEntry } from './memory-volume-Bp71nqvc.cjs';
1
+ import { S as Shell, a as ShellIO, b as Session, N as Node, c as Command, K as Kernel, E as ExecContext, O as OutputStream, C as Container, B as BinaryBackend, d as createContainer } from './container-BQx27_d-.cjs';
2
+ export { e as BinaryExecutionRegistry, f as BinaryInfo, g as BinaryRequest, h as BufferSink, i as CallbackSink, j as CommandRegistry, k as ContainerFs, l as ContainerOptions, m as ContextInit, n as Env, o as ExecOptions, p as ExecResult, q as ExecutionDecision, r as ExecutionTier, F as FileData, s as FileInput, t as FileOutput, G as GroupEntry, H as HttpResponse, I as InputStream, J as Job, u as KernelOptions, L as ListeningPort, M as MANIFEST_FORMAT, v as MANIFEST_SCHEMA_VERSION, w as MountEntry, x as NetInterface, y as NetworkOptions, z as NetworkStack, A as NullInput, D as NullOutput, P as PYTHON_VERSION, Q as PasswdEntry, R as Pipe, T as Preparation, U as PreparedBinary, V as Process, W as ProcessKind, X as ProcessOptions, Y as ProcessState, Z as ProcessTable, _ as PythonCapabilities, $ as PythonOptions, a0 as PythonProfile, a1 as PythonRuntimeManifest, a2 as ResolvedExecutable, a3 as RunOptions, a4 as RunResult, a5 as SessionInit, a6 as SessionResult, a7 as SessionRunOptions, a8 as ShellExit, a9 as ShellInit, aa as ShellOptions, ab as SpawnHandle, ac as Stdio, ad as TeeOutput, ae as UserDatabase, af as Variables, ag as WasmCommandArtifact, ah as WasmTranslator, ai as binaryDigest, aj as braceExpand, ak as captureStdio, al as configurePython, am as createContext, an as createTranslationBackend, ao as createWasmCompatibilityBackend, ap as defineCommand, aq as expandWord, ar as expandWords, as as inspectElf, at as installWasmCommands, au as isElfBinary, av as isPythonAvailable, aw as resetPidCounter, ax as shellQuote, ay as validateManifest } from './container-BQx27_d-.cjs';
3
+ import { V as Vfs, C as Cred, R as RuntimeVolume, a as VirtualTcpNetwork, b as RuntimeHttpResponse, O as OutboundPolicy, S as SpawnChild, c as SyncSpawn, I as IpcTransport, d as VolumeStat, e as VolumeStats, f as RuntimePod, g as RuntimePackageInstaller, h as ChildSpawnConfig, i as ChildHandle, j as RuntimeProcess, k as RuntimeSocketPeer, l as RuntimeConnection } from './contracts-BHo4LdY2.cjs';
4
+ export { D as DirEntry, m as ROOT_CRED, n as RuntimeProcessManager, o as RuntimeProcessResult, p as Stats, q as VirtualNode, r as VirtualProvider, W as WriteOptions, s as applyChmod, t as createChildProcessModule, u as formatMode, v as makeCred, w as octalMode, x as parseUmask } from './contracts-BHo4LdY2.cjs';
5
+ import { M as MemoryVolume, a as MemoryVolumeSnapshotEntry } from './memory-volume-e-uJFRnG.cjs';
6
6
  import EventEmitter from 'events/events.js';
7
7
  import streamModule from 'stream-browserify';
8
8
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { S as Shell, a as ShellIO, b as Session, N as Node, c as Command, K as Kernel, E as ExecContext, O as OutputStream, C as Container, B as BinaryBackend, d as createContainer } from './container-C16pCOWC.js';
2
- export { e as BinaryExecutionRegistry, f as BinaryInfo, g as BinaryRequest, h as BufferSink, i as CallbackSink, j as CommandRegistry, k as ContainerFs, l as ContainerOptions, m as ContextInit, n as Env, o as ExecOptions, p as ExecResult, q as ExecutionDecision, r as ExecutionTier, F as FileData, s as FileInput, t as FileOutput, G as GroupEntry, H as HttpResponse, I as InputStream, J as Job, u as KernelOptions, L as ListeningPort, M as MANIFEST_FORMAT, v as MANIFEST_SCHEMA_VERSION, w as MountEntry, x as NetInterface, y as NetworkOptions, z as NetworkStack, A as NullInput, D as NullOutput, P as PYTHON_VERSION, Q as PasswdEntry, R as Pipe, T as Preparation, U as PreparedBinary, V as Process, W as ProcessKind, X as ProcessOptions, Y as ProcessState, Z as ProcessTable, _ as PythonCapabilities, $ as PythonOptions, a0 as PythonProfile, a1 as PythonRuntimeManifest, a2 as ResolvedExecutable, a3 as RunOptions, a4 as RunResult, a5 as SessionInit, a6 as SessionResult, a7 as SessionRunOptions, a8 as ShellExit, a9 as ShellInit, aa as ShellOptions, ab as SpawnHandle, ac as Stdio, ad as TeeOutput, ae as UserDatabase, af as Variables, ag as WasmCommandArtifact, ah as WasmTranslator, ai as binaryDigest, aj as braceExpand, ak as captureStdio, al as configurePython, am as createContext, an as createTranslationBackend, ao as createWasmCompatibilityBackend, ap as defineCommand, aq as expandWord, ar as expandWords, as as inspectElf, at as installWasmCommands, au as isElfBinary, av as isPythonAvailable, aw as resetPidCounter, ax as shellQuote, ay as validateManifest } from './container-C16pCOWC.js';
3
- import { V as Vfs, C as Cred, R as RuntimeVolume, a as VirtualTcpNetwork, b as RuntimeHttpResponse, O as OutboundPolicy, S as SpawnChild, c as SyncSpawn, I as IpcTransport, d as VolumeStat, e as VolumeStats, f as RuntimePod, g as RuntimePackageInstaller, h as ChildSpawnConfig, i as ChildHandle, j as RuntimeProcess, k as RuntimeSocketPeer, l as RuntimeConnection } from './contracts-CXZ_25-O.js';
4
- export { D as DirEntry, m as ROOT_CRED, n as RuntimeProcessManager, o as RuntimeProcessResult, p as Stats, q as VirtualNode, r as VirtualProvider, W as WriteOptions, s as applyChmod, t as createChildProcessModule, u as formatMode, v as makeCred, w as octalMode, x as parseUmask } from './contracts-CXZ_25-O.js';
5
- import { M as MemoryVolume, a as MemoryVolumeSnapshotEntry } from './memory-volume-C4xwtIRZ.js';
1
+ import { S as Shell, a as ShellIO, b as Session, N as Node, c as Command, K as Kernel, E as ExecContext, O as OutputStream, C as Container, B as BinaryBackend, d as createContainer } from './container-D-e5SMXQ.js';
2
+ export { e as BinaryExecutionRegistry, f as BinaryInfo, g as BinaryRequest, h as BufferSink, i as CallbackSink, j as CommandRegistry, k as ContainerFs, l as ContainerOptions, m as ContextInit, n as Env, o as ExecOptions, p as ExecResult, q as ExecutionDecision, r as ExecutionTier, F as FileData, s as FileInput, t as FileOutput, G as GroupEntry, H as HttpResponse, I as InputStream, J as Job, u as KernelOptions, L as ListeningPort, M as MANIFEST_FORMAT, v as MANIFEST_SCHEMA_VERSION, w as MountEntry, x as NetInterface, y as NetworkOptions, z as NetworkStack, A as NullInput, D as NullOutput, P as PYTHON_VERSION, Q as PasswdEntry, R as Pipe, T as Preparation, U as PreparedBinary, V as Process, W as ProcessKind, X as ProcessOptions, Y as ProcessState, Z as ProcessTable, _ as PythonCapabilities, $ as PythonOptions, a0 as PythonProfile, a1 as PythonRuntimeManifest, a2 as ResolvedExecutable, a3 as RunOptions, a4 as RunResult, a5 as SessionInit, a6 as SessionResult, a7 as SessionRunOptions, a8 as ShellExit, a9 as ShellInit, aa as ShellOptions, ab as SpawnHandle, ac as Stdio, ad as TeeOutput, ae as UserDatabase, af as Variables, ag as WasmCommandArtifact, ah as WasmTranslator, ai as binaryDigest, aj as braceExpand, ak as captureStdio, al as configurePython, am as createContext, an as createTranslationBackend, ao as createWasmCompatibilityBackend, ap as defineCommand, aq as expandWord, ar as expandWords, as as inspectElf, at as installWasmCommands, au as isElfBinary, av as isPythonAvailable, aw as resetPidCounter, ax as shellQuote, ay as validateManifest } from './container-D-e5SMXQ.js';
3
+ import { V as Vfs, C as Cred, R as RuntimeVolume, a as VirtualTcpNetwork, b as RuntimeHttpResponse, O as OutboundPolicy, S as SpawnChild, c as SyncSpawn, I as IpcTransport, d as VolumeStat, e as VolumeStats, f as RuntimePod, g as RuntimePackageInstaller, h as ChildSpawnConfig, i as ChildHandle, j as RuntimeProcess, k as RuntimeSocketPeer, l as RuntimeConnection } from './contracts-BHo4LdY2.js';
4
+ export { D as DirEntry, m as ROOT_CRED, n as RuntimeProcessManager, o as RuntimeProcessResult, p as Stats, q as VirtualNode, r as VirtualProvider, W as WriteOptions, s as applyChmod, t as createChildProcessModule, u as formatMode, v as makeCred, w as octalMode, x as parseUmask } from './contracts-BHo4LdY2.js';
5
+ import { M as MemoryVolume, a as MemoryVolumeSnapshotEntry } from './memory-volume-meoRW8ST.js';
6
6
  import EventEmitter from 'events/events.js';
7
7
  import streamModule from 'stream-browserify';
8
8
 
package/dist/index.js CHANGED
@@ -8357,6 +8357,60 @@ function createSysProvider(kernel) {
8357
8357
  };
8358
8358
  }
8359
8359
 
8360
+ // src/net/proxy-fetch.ts
8361
+ function toBase64(bytes2) {
8362
+ let text2 = "";
8363
+ for (let at = 0; at < bytes2.length; at += 32768) {
8364
+ text2 += String.fromCharCode(...bytes2.subarray(at, at + 32768));
8365
+ }
8366
+ return btoa(text2);
8367
+ }
8368
+ function fromBase64(text2) {
8369
+ const binary = atob(text2);
8370
+ const bytes2 = new Uint8Array(binary.length);
8371
+ for (let at = 0; at < binary.length; at += 1) bytes2[at] = binary.charCodeAt(at);
8372
+ return bytes2;
8373
+ }
8374
+ async function proxyExchange(proxy, request, fetchImpl = fetch) {
8375
+ let answer;
8376
+ try {
8377
+ answer = await fetchImpl(proxy, {
8378
+ method: "POST",
8379
+ headers: { "content-type": "application/json" },
8380
+ body: JSON.stringify({
8381
+ url: request.url,
8382
+ method: request.method,
8383
+ headers: request.headers,
8384
+ ...request.body?.length ? { body: toBase64(request.body) } : {}
8385
+ })
8386
+ });
8387
+ } catch (error) {
8388
+ throw new Error(
8389
+ `the egress proxy at ${proxy} could not be reached (${error instanceof Error ? error.message : String(error)}). It performs this container's outbound requests, so nothing leaves until it answers.`
8390
+ );
8391
+ }
8392
+ if (!answer.ok) {
8393
+ throw new Error(
8394
+ `the egress proxy at ${proxy} refused ${request.method} ${request.url} with ${answer.status} ${answer.statusText}: ${(await answer.text()).trim().slice(0, 300)}`
8395
+ );
8396
+ }
8397
+ const payload = await answer.json();
8398
+ return {
8399
+ status: payload.status ?? 200,
8400
+ statusText: payload.statusText ?? "",
8401
+ headers: payload.headers ?? {},
8402
+ body: payload.body ? fromBase64(payload.body) : new Uint8Array(0)
8403
+ };
8404
+ }
8405
+ function isBrowser() {
8406
+ return typeof process === "undefined" || process.versions?.node == null;
8407
+ }
8408
+ function browserBlocked(hostname, message) {
8409
+ return new Error(
8410
+ `${message} \u2014 the request to ${hostname} was blocked by the browser. A page can only read a response from a host that sends CORS headers, and most APIs send none. Give the container a proxy that makes the request for it \u2014 network: { proxy }, and \`npx sandboxedjs-egress\` is one \u2014 or run the container on a server.`
8411
+ );
8412
+ }
8413
+
8360
8414
  // src/net/policy.ts
8361
8415
  function isLoopbackHostname(hostname) {
8362
8416
  const host2 = hostname.replace(/^\[|\]$/g, "").toLowerCase();
@@ -8399,7 +8453,40 @@ function policedFetch(policy, fetchImpl) {
8399
8453
  if (!outboundAllowed(policy, url)) {
8400
8454
  throw Object.assign(new TypeError("fetch failed"), { cause: outboundBlockedError(url) });
8401
8455
  }
8402
- return fetchImpl(input, init);
8456
+ if (policy.proxy) return await fetchThroughProxy(policy.proxy, fetchImpl, input, init);
8457
+ try {
8458
+ return await fetchImpl(input, init);
8459
+ } catch (error) {
8460
+ if (!isBrowser()) throw error;
8461
+ throw Object.assign(new TypeError("fetch failed"), {
8462
+ cause: browserBlocked(new URL(url).hostname, error instanceof Error ? error.message : String(error))
8463
+ });
8464
+ }
8465
+ });
8466
+ }
8467
+ async function fetchThroughProxy(proxy, fetchImpl, input, init) {
8468
+ const request = new Request(input, init);
8469
+ const headers = {};
8470
+ request.headers.forEach((value, name) => {
8471
+ headers[name] = value;
8472
+ });
8473
+ const bodyless = request.method === "GET" || request.method === "HEAD";
8474
+ const result = await proxyExchange(
8475
+ proxy,
8476
+ {
8477
+ url: request.url,
8478
+ method: request.method,
8479
+ headers,
8480
+ ...bodyless ? {} : { body: new Uint8Array(await request.arrayBuffer()) }
8481
+ },
8482
+ fetchImpl
8483
+ );
8484
+ const nullBody = result.status === 204 || result.status === 205 || result.status === 304 || request.method === "HEAD";
8485
+ const body = nullBody ? null : result.body.slice().buffer;
8486
+ return new Response(body, {
8487
+ status: result.status,
8488
+ statusText: result.statusText,
8489
+ headers: result.headers
8403
8490
  });
8404
8491
  }
8405
8492
  function policedWebSocket(Native, policy) {
@@ -17457,9 +17544,6 @@ function parseUrl(raw) {
17457
17544
  return null;
17458
17545
  }
17459
17546
  }
17460
- function isBrowser() {
17461
- return typeof process === "undefined" || process.versions?.node == null;
17462
- }
17463
17547
  async function performRequest(ctx, url, init) {
17464
17548
  const isLocal = ctx.kernel.net.isLocal(url.hostname);
17465
17549
  if (isLocal) {
@@ -17487,7 +17571,16 @@ async function performRequest(ctx, url, init) {
17487
17571
  );
17488
17572
  }
17489
17573
  const proxy = ctx.kernel.net.proxy;
17490
- if (proxy) return await proxyRequest(ctx, proxy, url, init);
17574
+ if (proxy) {
17575
+ const result = await proxyExchange(proxy, {
17576
+ url: url.toString(),
17577
+ method: init.method,
17578
+ headers: init.headers,
17579
+ ...init.body?.length ? { body: init.body } : {}
17580
+ });
17581
+ ctx.kernel.net.countRx(result.body.length);
17582
+ return { ...result, url: url.toString() };
17583
+ }
17491
17584
  let response;
17492
17585
  try {
17493
17586
  response = await fetch(url.toString(), {
@@ -17496,9 +17589,7 @@ async function performRequest(ctx, url, init) {
17496
17589
  ...init.body ? { body: init.body.slice().buffer } : {}
17497
17590
  });
17498
17591
  } catch (error) {
17499
- throw isBrowser() ? new Error(
17500
- `${error instanceof Error ? error.message : String(error)} \u2014 the request to ${url.hostname} was blocked by the browser. A page can only read a response from a host that sends CORS headers, and most APIs send none. Give the container a proxy that makes the request for it \u2014 network: { proxy }, and \`npx sandboxedjs-egress\` is one \u2014 or run the container on a server.`
17501
- ) : error;
17592
+ throw isBrowser() ? browserBlocked(url.hostname, error instanceof Error ? error.message : String(error)) : error;
17502
17593
  }
17503
17594
  const body = new Uint8Array(await response.arrayBuffer());
17504
17595
  ctx.kernel.net.countRx(body.length);
@@ -17508,53 +17599,6 @@ async function performRequest(ctx, url, init) {
17508
17599
  });
17509
17600
  return { status: response.status, statusText: response.statusText, headers, body, url: response.url || url.toString() };
17510
17601
  }
17511
- async function proxyRequest(ctx, proxy, url, init) {
17512
- let answer;
17513
- try {
17514
- answer = await fetch(proxy, {
17515
- method: "POST",
17516
- headers: { "content-type": "application/json" },
17517
- body: JSON.stringify({
17518
- url: url.toString(),
17519
- method: init.method,
17520
- headers: init.headers,
17521
- ...init.body?.length ? { body: toBase64(init.body) } : {}
17522
- })
17523
- });
17524
- } catch (error) {
17525
- throw new Error(
17526
- `the egress proxy at ${proxy} could not be reached (${error instanceof Error ? error.message : String(error)}). It performs this container's outbound requests, so nothing leaves until it answers.`
17527
- );
17528
- }
17529
- if (!answer.ok) {
17530
- throw new Error(
17531
- `the egress proxy at ${proxy} refused ${init.method} ${url.toString()} with ${answer.status} ${answer.statusText}: ${(await answer.text()).trim().slice(0, 300)}`
17532
- );
17533
- }
17534
- const payload = await answer.json();
17535
- const body = payload.body ? fromBase64(payload.body) : new Uint8Array(0);
17536
- ctx.kernel.net.countRx(body.length);
17537
- return {
17538
- status: payload.status ?? 200,
17539
- statusText: payload.statusText ?? "",
17540
- headers: payload.headers ?? {},
17541
- body,
17542
- url: url.toString()
17543
- };
17544
- }
17545
- function toBase64(bytes2) {
17546
- let text2 = "";
17547
- for (let at = 0; at < bytes2.length; at += 32768) {
17548
- text2 += String.fromCharCode(...bytes2.subarray(at, at + 32768));
17549
- }
17550
- return btoa(text2);
17551
- }
17552
- function fromBase64(text2) {
17553
- const binary = atob(text2);
17554
- const bytes2 = new Uint8Array(binary.length);
17555
- for (let at = 0; at < binary.length; at += 1) bytes2[at] = binary.charCodeAt(at);
17556
- return bytes2;
17557
- }
17558
17602
  var curl = defineCommand({
17559
17603
  name: "curl",
17560
17604
  path: "/usr/bin/curl",
@@ -20996,7 +21040,7 @@ var wheels_default = {
20996
21040
 
20997
21041
  // package.json
20998
21042
  var package_default = {
20999
- version: "0.2.4"};
21043
+ version: "0.2.6"};
21000
21044
 
21001
21045
  // src/python/extension-abi.ts
21002
21046
  var EXTENSION_ABI = {
@@ -22661,8 +22705,12 @@ function createHostAbiServer(proc) {
22661
22705
  }
22662
22706
  case Op.resolve: {
22663
22707
  const sockets = proc.sockets;
22708
+ const name = r.string();
22709
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(name)) {
22710
+ return { status: 0, payload: new Writer().string(name).finish() };
22711
+ }
22664
22712
  if (!sockets?.outbound) throw new PosixError(Errno.ENOSYS);
22665
- const address = sockets.outbound.resolve(r.string());
22713
+ const address = sockets.outbound.resolve(name);
22666
22714
  return { status: 0, payload: new Writer().string(address).finish() };
22667
22715
  }
22668
22716
  case Op.send: {
@@ -24789,6 +24837,77 @@ if os.environ.get("SBX_OUTBOUND_SOCKETS") == "1":
24789
24837
  sys.meta_path.insert(0, _SbxTruststoreHook())
24790
24838
 
24791
24839
 
24840
+ # Literal addresses are answered here rather than through the host.
24841
+ #
24842
+ # The container's resolver is a host call, and a container with no outbound
24843
+ # sockets configured has no resolver to call: it answers ENOSYS. That is the
24844
+ # right answer for a name, but the egress hop below dials 127.0.0.1, and
24845
+ # http.client calls getaddrinfo even for an address that needs no resolving --
24846
+ # so every request through the egress died with "Function not implemented"
24847
+ # before it reached the endpoint. A numeric address (and loopback by its usual
24848
+ # names) is now formed here, which is what a resolver would have returned.
24849
+ def _sbx_local_addresses():
24850
+ import socket as _socket_module
24851
+ import _socket as _socket_native
24852
+
24853
+ # The native entry point, not socket.getaddrinfo: the stdlib wrapper
24854
+ # delegates to _socket, so wrapping the wrapper and replacing _socket with
24855
+ # the wrapper makes the two call each other forever.
24856
+ _real_getaddrinfo = _socket_native.getaddrinfo
24857
+
24858
+ _literals = {"localhost": "127.0.0.1", "localhost.localdomain": "127.0.0.1",
24859
+ "ip6-localhost": "127.0.0.1"}
24860
+
24861
+ def _literal(host):
24862
+ """The address host already is, or None when it needs a resolver."""
24863
+ if isinstance(host, (bytes, bytearray)):
24864
+ host = bytes(host).decode("ascii", "replace")
24865
+ if host is None:
24866
+ return None
24867
+ host = _literals.get(host.lower(), host)
24868
+ parts = host.split(".")
24869
+ if len(parts) != 4:
24870
+ return None
24871
+ for part in parts:
24872
+ if not part.isdigit() or len(part) > 3 or int(part) > 255:
24873
+ return None
24874
+ return host
24875
+
24876
+ def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
24877
+ address = _literal(host)
24878
+ if address is None:
24879
+ return _real_getaddrinfo(host, port, family, type, proto, flags)
24880
+ if isinstance(port, (bytes, bytearray)):
24881
+ port = bytes(port).decode("ascii", "replace")
24882
+ if isinstance(port, str):
24883
+ port = int(port) if port.isdigit() else _socket_module.getservbyname(port)
24884
+ if port is None:
24885
+ port = 0
24886
+ if family not in (0, _socket_module.AF_UNSPEC, _socket_module.AF_INET):
24887
+ raise _socket_module.gaierror(
24888
+ _socket_module.EAI_FAMILY, "only AF_INET is supported in this container"
24889
+ )
24890
+ kind = type or _socket_module.SOCK_STREAM
24891
+ protocol = proto or (_socket_module.IPPROTO_TCP
24892
+ if kind == _socket_module.SOCK_STREAM else 0)
24893
+ return [(_socket_module.AF_INET, kind, protocol, "", (address, int(port)))]
24894
+
24895
+ def gethostbyname(host):
24896
+ return getaddrinfo(host, 0)[0][4][0]
24897
+
24898
+ def gethostbyname_ex(host):
24899
+ return (host, [], [gethostbyname(host)])
24900
+
24901
+ _socket_module.getaddrinfo = getaddrinfo
24902
+ _socket_module.gethostbyname = gethostbyname
24903
+ _socket_module.gethostbyname_ex = gethostbyname_ex
24904
+ _socket_native.getaddrinfo = getaddrinfo
24905
+ _socket_native.gethostbyname = gethostbyname
24906
+ _socket_native.gethostbyname_ex = gethostbyname_ex
24907
+
24908
+
24909
+ _sbx_local_addresses()
24910
+
24792
24911
  _egress = None if os.environ.get("SBX_OUTBOUND_SOCKETS") == "1" else os.environ.get("SBX_HTTP_EGRESS")
24793
24912
  if _egress:
24794
24913
  import http.client
@@ -37808,7 +37927,8 @@ var Container = class _Container {
37808
37927
  if (opts.python) configurePython(opts.python);
37809
37928
  const network = {
37810
37929
  allowOutbound: opts.network?.allowOutbound ?? false,
37811
- allowedHosts: opts.network?.allowedHosts ?? null
37930
+ allowedHosts: opts.network?.allowedHosts ?? null,
37931
+ ...opts.network?.proxy ? { proxy: opts.network.proxy } : {}
37812
37932
  };
37813
37933
  const podOptions = {
37814
37934
  workdir: opts.cwd ?? "/",
@@ -1,4 +1,4 @@
1
- import { R as RuntimeVolume, d as VolumeStat, e as VolumeStats } from './contracts-CXZ_25-O.cjs';
1
+ import { R as RuntimeVolume, d as VolumeStat, e as VolumeStats } from './contracts-BHo4LdY2.cjs';
2
2
 
3
3
  type NodeKind = "file" | "directory" | "symlink";
4
4
  interface MemoryVolumeSnapshotEntry {
@@ -1,4 +1,4 @@
1
- import { R as RuntimeVolume, d as VolumeStat, e as VolumeStats } from './contracts-CXZ_25-O.js';
1
+ import { R as RuntimeVolume, d as VolumeStat, e as VolumeStats } from './contracts-BHo4LdY2.js';
2
2
 
3
3
  type NodeKind = "file" | "directory" | "symlink";
4
4
  interface MemoryVolumeSnapshotEntry {