sandboxedjs 0.1.93 → 0.1.94

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
@@ -19821,6 +19821,7 @@ var Op = {
19821
19821
  setsockopt: 1802,
19822
19822
  getsockopt: 1803,
19823
19823
  socket_close: 1804,
19824
+ resolve: 1805,
19824
19825
  spawn: 2048,
19825
19826
  waitpid: 2049,
19826
19827
  kill: 2050
@@ -20057,6 +20058,28 @@ var wheels_default = {
20057
20058
  patches: []
20058
20059
  }
20059
20060
  },
20061
+ {
20062
+ name: "asyncpg",
20063
+ version: "0.31.0",
20064
+ filename: "asyncpg-0.31.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20065
+ sha256: "3eaf965cbd78d2314e8f12b9384e5642725323063aaff2708a8a4b3c01ef10ba",
20066
+ abiId: "sbxabi1-c2637d04695ad927",
20067
+ requires: [
20068
+ 'async_timeout>=4.0.3; python_version < "3.11.0"',
20069
+ 'gssapi; platform_system != "Windows" and extra == "gssauth"',
20070
+ 'sspilib; platform_system == "Windows" and extra == "gssauth"'
20071
+ ],
20072
+ provenance: {
20073
+ sourceTreeDigest: "a9ab0177da5c0c2d0ba635d1f5568687efbf8aefc08cd51cd086277184eba76f",
20074
+ recipeSha256: "fb9cf1a9fd35d8d63ac69dcf6a1671ceeebcb23f381a100b412058d75ff2eedc",
20075
+ buildTools: [
20076
+ "setuptools==84.0.0",
20077
+ "Cython==3.1.4"
20078
+ ],
20079
+ nativeDependencies: [],
20080
+ patches: []
20081
+ }
20082
+ },
20060
20083
  {
20061
20084
  name: "bcrypt",
20062
20085
  version: "5.0.0",
@@ -20078,18 +20101,20 @@ var wheels_default = {
20078
20101
  name: "brotli",
20079
20102
  version: "1.2.0",
20080
20103
  filename: "brotli-1.2.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20081
- sha256: "b67d1b9169ba3e0790d04b8020adb1a58f0b89f8961aca360dca5f020de07a21",
20104
+ sha256: "d8b06cc04770a048b41956c9cc767e7332c49f0229e69744f4b1c2892b888b27",
20082
20105
  abiId: "sbxabi1-c2637d04695ad927",
20083
20106
  requires: [],
20084
20107
  provenance: {
20085
20108
  sourceTreeDigest: "7b563fa2e7f20edac204cc80004cae35c844b36a83758e5957184383332d1209",
20086
- recipeSha256: "09bf4f6072d637866a6b3c3deaec419dc3ff8a32a6f3b0813f57e06316bfcf39",
20109
+ recipeSha256: "1326c898e999feba536ff15f080fed9ac4b9cd14e0dbfee39a950decf19607ef",
20087
20110
  buildTools: [
20088
20111
  "setuptools==84.0.0",
20089
20112
  "pkgconfig==1.6.0"
20090
20113
  ],
20091
20114
  nativeDependencies: [],
20092
- patches: []
20115
+ patches: [
20116
+ "0001-meth-noargs-signatures.patch"
20117
+ ]
20093
20118
  }
20094
20119
  },
20095
20120
  {
@@ -20150,6 +20175,25 @@ var wheels_default = {
20150
20175
  patches: []
20151
20176
  }
20152
20177
  },
20178
+ {
20179
+ name: "google-crc32c",
20180
+ version: "1.8.0",
20181
+ filename: "google_crc32c-1.8.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20182
+ sha256: "e64c21841598ea21121d7b7bb203e124d58c035db49ffce03631c55216c942cd",
20183
+ abiId: "sbxabi1-c2637d04695ad927",
20184
+ requires: [],
20185
+ provenance: {
20186
+ sourceTreeDigest: "fa277032a78adfccf6adcda32cadc4f6958a324979040c231e01f0963821cb75",
20187
+ recipeSha256: "843dea57727061bdfc7bcd27ac380d16a767effa538cbab6257e106939689f43",
20188
+ buildTools: [
20189
+ "setuptools==84.0.0"
20190
+ ],
20191
+ nativeDependencies: [
20192
+ "crc32c==1.1.2"
20193
+ ],
20194
+ patches: []
20195
+ }
20196
+ },
20153
20197
  {
20154
20198
  name: "httptools",
20155
20199
  version: "0.8.0",
@@ -20186,6 +20230,32 @@ var wheels_default = {
20186
20230
  patches: []
20187
20231
  }
20188
20232
  },
20233
+ {
20234
+ name: "lxml",
20235
+ version: "6.1.3",
20236
+ filename: "lxml-6.1.3-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20237
+ sha256: "dc76d45314b806934bdaf35f9e4ae61e86e741e07bac53fae7e94f9d3dab9d4e",
20238
+ abiId: "sbxabi1-c2637d04695ad927",
20239
+ requires: [
20240
+ 'cssselect>=0.7; extra == "cssselect"',
20241
+ 'html5lib; extra == "html5"',
20242
+ 'BeautifulSoup4; extra == "htmlsoup"',
20243
+ 'lxml_html_clean; extra == "html-clean"'
20244
+ ],
20245
+ provenance: {
20246
+ sourceTreeDigest: "aedea680c1fe58ae9a124d81ea28f2316765e444e3809691854ddbf173c22bf5",
20247
+ recipeSha256: "aa62da97bb13eb5fa0a047f7c61ed93554f60142be9fcae64e3053e2a5673e9f",
20248
+ buildTools: [
20249
+ "setuptools==84.0.0",
20250
+ "Cython==3.1.4"
20251
+ ],
20252
+ nativeDependencies: [
20253
+ "xml2==2.13.8",
20254
+ "xslt==1.1.43"
20255
+ ],
20256
+ patches: []
20257
+ }
20258
+ },
20189
20259
  {
20190
20260
  name: "lz4",
20191
20261
  version: "4.4.5",
@@ -20287,6 +20357,113 @@ var wheels_default = {
20287
20357
  ]
20288
20358
  }
20289
20359
  },
20360
+ {
20361
+ name: "pandas",
20362
+ version: "3.0.5",
20363
+ filename: "pandas-3.0.5-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20364
+ sha256: "ae36ad9b403b6fc28cb2bb79f54a38e1897ab43635c596fff2bce87f8148ac48",
20365
+ abiId: "sbxabi1-c2637d04695ad927",
20366
+ requires: [
20367
+ 'numpy>=1.26.0; python_version < "3.14"',
20368
+ 'numpy>=2.3.3; python_version >= "3.14"',
20369
+ "python-dateutil>=2.8.2",
20370
+ 'tzdata; sys_platform == "win32"',
20371
+ 'tzdata; sys_platform == "emscripten"',
20372
+ 'hypothesis>=6.116.0; extra == "test"',
20373
+ 'pytest<9.1,>=8.3.4; extra == "test"',
20374
+ 'pytest-xdist>=3.6.1; extra == "test"',
20375
+ 'pyarrow>=13.0.0; extra == "pyarrow"',
20376
+ 'bottleneck>=1.4.2; extra == "performance"',
20377
+ 'numba>=0.60.0; extra == "performance"',
20378
+ 'numexpr>=2.10.2; extra == "performance"',
20379
+ 'scipy>=1.14.1; extra == "computation"',
20380
+ 'xarray>=2024.10.0; extra == "computation"',
20381
+ 'fsspec>=2024.10.0; extra == "fss"',
20382
+ 's3fs>=2024.10.0; extra == "aws"',
20383
+ 'gcsfs>=2024.10.0; extra == "gcp"',
20384
+ 'odfpy>=1.4.1; extra == "excel"',
20385
+ 'openpyxl>=3.1.5; extra == "excel"',
20386
+ 'python-calamine>=0.3.0; extra == "excel"',
20387
+ 'pyxlsb>=1.0.10; extra == "excel"',
20388
+ 'xlrd>=2.0.1; extra == "excel"',
20389
+ 'xlsxwriter>=3.2.0; extra == "excel"',
20390
+ 'pyarrow>=13.0.0; extra == "parquet"',
20391
+ 'pyarrow>=13.0.0; extra == "feather"',
20392
+ 'pyiceberg>=0.8.1; extra == "iceberg"',
20393
+ 'tables>=3.10.1; extra == "hdf5"',
20394
+ 'pyreadstat>=1.2.8; extra == "spss"',
20395
+ 'SQLAlchemy>=2.0.36; extra == "postgresql"',
20396
+ 'psycopg2>=2.9.10; extra == "postgresql"',
20397
+ 'adbc-driver-postgresql>=1.2.0; extra == "postgresql"',
20398
+ 'SQLAlchemy>=2.0.36; extra == "mysql"',
20399
+ 'pymysql>=1.1.1; extra == "mysql"',
20400
+ 'SQLAlchemy>=2.0.36; extra == "sql-other"',
20401
+ 'adbc-driver-postgresql>=1.2.0; extra == "sql-other"',
20402
+ 'adbc-driver-sqlite>=1.2.0; extra == "sql-other"',
20403
+ 'beautifulsoup4>=4.12.3; extra == "html"',
20404
+ 'html5lib>=1.1; extra == "html"',
20405
+ 'lxml>=5.3.0; extra == "html"',
20406
+ 'lxml>=5.3.0; extra == "xml"',
20407
+ 'matplotlib>=3.9.3; extra == "plot"',
20408
+ 'jinja2>=3.1.5; extra == "output-formatting"',
20409
+ 'tabulate>=0.9.0; extra == "output-formatting"',
20410
+ 'PyQt5>=5.15.9; extra == "clipboard"',
20411
+ 'qtpy>=2.4.2; extra == "clipboard"',
20412
+ 'zstandard>=0.23.0; extra == "compression"',
20413
+ 'pytz>=2020.1; extra == "timezone"',
20414
+ 'adbc-driver-postgresql>=1.2.0; extra == "all"',
20415
+ 'adbc-driver-sqlite>=1.2.0; extra == "all"',
20416
+ 'beautifulsoup4>=4.12.3; extra == "all"',
20417
+ 'bottleneck>=1.4.2; extra == "all"',
20418
+ 'fastparquet>=2024.11.0; extra == "all"',
20419
+ 'fsspec>=2024.10.0; extra == "all"',
20420
+ 'gcsfs>=2024.10.0; extra == "all"',
20421
+ 'html5lib>=1.1; extra == "all"',
20422
+ 'hypothesis>=6.116.0; extra == "all"',
20423
+ 'jinja2>=3.1.5; extra == "all"',
20424
+ 'lxml>=5.3.0; extra == "all"',
20425
+ 'matplotlib>=3.9.3; extra == "all"',
20426
+ 'numba>=0.60.0; extra == "all"',
20427
+ 'numexpr>=2.10.2; extra == "all"',
20428
+ 'odfpy>=1.4.1; extra == "all"',
20429
+ 'openpyxl>=3.1.5; extra == "all"',
20430
+ 'psycopg2>=2.9.10; extra == "all"',
20431
+ 'pyarrow>=13.0.0; extra == "all"',
20432
+ 'pyiceberg>=0.8.1; extra == "all"',
20433
+ 'pymysql>=1.1.1; extra == "all"',
20434
+ 'PyQt5>=5.15.9; extra == "all"',
20435
+ 'pyreadstat>=1.2.8; extra == "all"',
20436
+ 'pytest>=8.3.4; extra == "all"',
20437
+ 'pytest-xdist>=3.6.1; extra == "all"',
20438
+ 'python-calamine>=0.3.0; extra == "all"',
20439
+ 'pytz>=2020.1; extra == "all"',
20440
+ 'pyxlsb>=1.0.10; extra == "all"',
20441
+ 'qtpy>=2.4.2; extra == "all"',
20442
+ 'scipy>=1.14.1; extra == "all"',
20443
+ 's3fs>=2024.10.0; extra == "all"',
20444
+ 'SQLAlchemy>=2.0.36; extra == "all"',
20445
+ 'tables>=3.10.1; extra == "all"',
20446
+ 'tabulate>=0.9.0; extra == "all"',
20447
+ 'xarray>=2024.10.0; extra == "all"',
20448
+ 'xlrd>=2.0.1; extra == "all"',
20449
+ 'xlsxwriter>=3.2.0; extra == "all"',
20450
+ 'zstandard>=0.23.0; extra == "all"'
20451
+ ],
20452
+ provenance: {
20453
+ sourceTreeDigest: "373f08151b5a209a84dd35b086316ef11a0d4030150457c2bfdab38305903d54",
20454
+ recipeSha256: "56976ed58b17053971334a62b503d5397b8ff739911fb596a116df23aeccabb0",
20455
+ buildTools: [
20456
+ "setuptools==84.0.0",
20457
+ "Cython==3.1.4",
20458
+ "meson-python==0.21.0",
20459
+ "meson==1.12.0",
20460
+ "ninja==1.13.2",
20461
+ "versioneer==0.29"
20462
+ ],
20463
+ nativeDependencies: [],
20464
+ patches: []
20465
+ }
20466
+ },
20290
20467
  {
20291
20468
  name: "pillow",
20292
20469
  version: "12.3.0",
@@ -20391,6 +20568,31 @@ var wheels_default = {
20391
20568
  patches: []
20392
20569
  }
20393
20570
  },
20571
+ {
20572
+ name: "rapidfuzz",
20573
+ version: "3.14.6",
20574
+ filename: "rapidfuzz-3.14.6-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20575
+ sha256: "b9ed2ea058350925beda765364c0f382bf9a7c08b3bec33840795178de021d32",
20576
+ abiId: "sbxabi1-c2637d04695ad927",
20577
+ requires: [
20578
+ 'numpy; extra == "all"'
20579
+ ],
20580
+ provenance: {
20581
+ sourceTreeDigest: "ae2b2bb95f7aba2babbbe668b68d48b5347ca923e6508b76d24228cd13ac7cd3",
20582
+ recipeSha256: "46954814226fff070aa6771d2f09d32a96b718ce8ef7a9f9404375850f7d1f77",
20583
+ buildTools: [
20584
+ "setuptools==84.0.0",
20585
+ "scikit-build-core==1.0.3",
20586
+ "cmake==4.4.3",
20587
+ "ninja==1.13.2",
20588
+ "Cython==3.1.4",
20589
+ "pathspec==1.1.1",
20590
+ "packaging==26.3"
20591
+ ],
20592
+ nativeDependencies: [],
20593
+ patches: []
20594
+ }
20595
+ },
20394
20596
  {
20395
20597
  name: "regex",
20396
20598
  version: "2026.9.10",
@@ -20756,7 +20958,7 @@ var wheels_default = {
20756
20958
 
20757
20959
  // package.json
20758
20960
  var package_default = {
20759
- version: "0.1.93"};
20961
+ version: "0.1.94"};
20760
20962
 
20761
20963
  // src/python/extension-abi.ts
20762
20964
  var EXTENSION_ABI = {
@@ -21794,6 +21996,8 @@ var VirtualTcpSocket = class _VirtualTcpSocket {
21794
21996
  network;
21795
21997
  listenerValue = null;
21796
21998
  connectionValue = null;
21999
+ /** Held so closing the guest's socket closes the host's, not just the queues. */
22000
+ hostConnection = null;
21797
22001
  boundPort = 0;
21798
22002
  boundAddress = "0.0.0.0";
21799
22003
  bind(address, port) {
@@ -21811,13 +22015,35 @@ var VirtualTcpSocket = class _VirtualTcpSocket {
21811
22015
  }
21812
22016
  this.listenerValue = this.network.listen(this.boundPort, this.boundAddress, backlog);
21813
22017
  }
21814
- connect(port, address = "127.0.0.1") {
22018
+ /**
22019
+ * Connect, in the container or out of it.
22020
+ *
22021
+ * The destination used to be discarded, which quietly turned every connect
22022
+ * into a loopback connect: a guest dialling example.com:443 would reach a
22023
+ * container server listening on 443 and believe it had reached the internet.
22024
+ * Loopback is still routed inside the container; anything else goes to the
22025
+ * host's outbound stack, which owns the names and the real sockets, and
22026
+ * which refuses with a reason when the host has no way out.
22027
+ */
22028
+ async connect(port, address = "127.0.0.1") {
21815
22029
  if (this.listenerValue || this.connectionValue) throw new PosixError(Errno.EISCONN);
21816
22030
  if (!isLoopback(address)) {
21817
- throw new PosixError(
21818
- Errno.ENETUNREACH,
21819
- `cannot reach ${address}:${port}: outbound network access is not available through raw sockets in this container; only loopback is routed`
21820
- );
22031
+ const outbound = this.network.outbound;
22032
+ if (!outbound) {
22033
+ throw new PosixError(
22034
+ Errno.ENETUNREACH,
22035
+ `cannot reach ${address}:${port}: outbound network access is not available through raw sockets in this container; only loopback is routed`
22036
+ );
22037
+ }
22038
+ const dialled = await outbound.connect(address, port, {
22039
+ address: this.boundAddress,
22040
+ port: this.boundPort
22041
+ });
22042
+ this.connectionValue = dialled.connection;
22043
+ this.hostConnection = dialled.host;
22044
+ this.boundPort = dialled.connection.local.port;
22045
+ this.boundAddress = dialled.connection.local.address;
22046
+ return;
21821
22047
  }
21822
22048
  this.connectionValue = this.network.connect(port, this.boundPort, this.boundAddress);
21823
22049
  this.boundPort = this.connectionValue.local.port;
@@ -21868,6 +22094,8 @@ var VirtualTcpSocket = class _VirtualTcpSocket {
21868
22094
  close() {
21869
22095
  if (this.listenerValue) this.network.close(this.listenerValue);
21870
22096
  this.connectionValue?.close();
22097
+ this.hostConnection?.close();
22098
+ this.hostConnection = null;
21871
22099
  this.listenerValue = null;
21872
22100
  this.connectionValue = null;
21873
22101
  }
@@ -21932,7 +22160,7 @@ var SocketDescription = class _SocketDescription {
21932
22160
  this.resource.listen(backlog);
21933
22161
  }
21934
22162
  connect(address, port) {
21935
- this.resource.connect(port, address);
22163
+ return this.resource.connect(port, address);
21936
22164
  }
21937
22165
  accept() {
21938
22166
  return new _SocketDescription(this.resource.accept(), this.flags);
@@ -21944,7 +22172,11 @@ var SocketDescription = class _SocketDescription {
21944
22172
  this.options.set(`${level}:${option}`, value.slice());
21945
22173
  }
21946
22174
  getOption(level, option) {
21947
- return this.options.get(`${level}:${option}`)?.slice() ?? new Uint8Array(4);
22175
+ const stored = this.options.get(`${level}:${option}`);
22176
+ if (stored) return stored.slice();
22177
+ if (level === SOL_SOCKET && option === SO_TYPE) return int32(SOCK_STREAM);
22178
+ if (level === SOL_SOCKET && option === SO_ERROR) return int32(0);
22179
+ return new Uint8Array(4);
21948
22180
  }
21949
22181
  localAddress() {
21950
22182
  return this.resource.local;
@@ -21955,6 +22187,15 @@ var SocketDescription = class _SocketDescription {
21955
22187
  return peer;
21956
22188
  }
21957
22189
  };
22190
+ var SOL_SOCKET = 1;
22191
+ var SO_ERROR = 4;
22192
+ var SO_TYPE = 3;
22193
+ var SOCK_STREAM = 1;
22194
+ function int32(value) {
22195
+ const bytes2 = new Uint8Array(4);
22196
+ new DataView(bytes2.buffer).setInt32(0, value, true);
22197
+ return bytes2;
22198
+ }
21958
22199
  function isLoopback(address) {
21959
22200
  if (address === "" || address === "localhost" || address === "0.0.0.0") return true;
21960
22201
  if (address === "::1" || address === "::" || address === "[::1]") return true;
@@ -21966,6 +22207,18 @@ var VirtualTcpNetwork = class {
21966
22207
  }
21967
22208
  occupied;
21968
22209
  listeners = /* @__PURE__ */ new Map();
22210
+ /** The host's outbound stack, when this host can open sockets at all. */
22211
+ outbound = null;
22212
+ /**
22213
+ * Ports held by in-container servers that are not sockets.
22214
+ *
22215
+ * A JavaScript HTTP server in the container is registered with the request
22216
+ * router and never binds one of these sockets, so a guest connecting to its
22217
+ * port found nothing listening. Before sockets could leave the container
22218
+ * this did not arise -- every guest client went out through the egress --
22219
+ * and afterwards `urllib` dialling a container server got ECONNREFUSED.
22220
+ */
22221
+ loopbackHttp = null;
21969
22222
  listen(port, address = "0.0.0.0", backlog = 128) {
21970
22223
  if (this.listeners.has(port) || this.occupied?.(port)) throw new PosixError(Errno.EADDRINUSE, `port ${port} is already in use`);
21971
22224
  const listener = new VirtualTcpListener(port, address, backlog);
@@ -21979,7 +22232,11 @@ var VirtualTcpNetwork = class {
21979
22232
  }
21980
22233
  connect(port, localPort = 0, localAddress = "127.0.0.1") {
21981
22234
  const listener = this.listeners.get(port);
21982
- if (!listener || listener.isClosed) throw new PosixError(Errno.ECONNREFUSED, `port ${port} is not listening`);
22235
+ if (!listener || listener.isClosed) {
22236
+ const bridged = this.connectToNonSocketServer(port, localPort, localAddress);
22237
+ if (bridged) return bridged;
22238
+ throw new PosixError(Errno.ECONNREFUSED, `port ${port} is not listening`);
22239
+ }
21983
22240
  const client = new VirtualTcpConnection(
21984
22241
  { address: localAddress, port: localPort },
21985
22242
  { address: listener.address, port }
@@ -21993,6 +22250,26 @@ var VirtualTcpNetwork = class {
21993
22250
  listener.enqueue(server);
21994
22251
  return client;
21995
22252
  }
22253
+ /** A connection to a port an in-container server holds without a socket. */
22254
+ connectToNonSocketServer(port, localPort, localAddress) {
22255
+ if (!this.loopbackHttp) return null;
22256
+ const client = new VirtualTcpConnection(
22257
+ { address: localAddress, port: localPort },
22258
+ { address: "127.0.0.1", port }
22259
+ );
22260
+ const server = new VirtualTcpConnection(
22261
+ { address: "127.0.0.1", port },
22262
+ { address: localAddress, port: localPort }
22263
+ );
22264
+ client.pairWith(server);
22265
+ server.pairWith(client);
22266
+ if (!this.loopbackHttp(port, server)) {
22267
+ client.close();
22268
+ server.close();
22269
+ return null;
22270
+ }
22271
+ return client;
22272
+ }
21996
22273
  hasListener(port) {
21997
22274
  return this.listeners.has(port);
21998
22275
  }
@@ -22010,7 +22287,7 @@ var POLLIN = 1;
22010
22287
  var POLLOUT = 4;
22011
22288
  var POLLHUP = 16;
22012
22289
  var AF_INET = 2;
22013
- var SOCK_STREAM = 1;
22290
+ var SOCK_STREAM2 = 1;
22014
22291
  var IPPROTO_TCP = 6;
22015
22292
  function createHostAbiServer(proc) {
22016
22293
  const files = new FileService(proc.vfs, proc.cred);
@@ -22301,7 +22578,7 @@ function createHostAbiServer(proc) {
22301
22578
  const family = r.u32();
22302
22579
  const type = r.u32();
22303
22580
  const protocol = r.i32();
22304
- if (family !== AF_INET || (type & 15) !== SOCK_STREAM) throw new PosixError(Errno.EAFNOSUPPORT);
22581
+ if (family !== AF_INET || (type & 15) !== SOCK_STREAM2) throw new PosixError(Errno.EAFNOSUPPORT);
22305
22582
  if (protocol !== 0 && protocol !== IPPROTO_TCP) throw new PosixError(Errno.EPROTONOSUPPORT);
22306
22583
  if (!proc.sockets) throw new PosixError(Errno.ENOSYS);
22307
22584
  return { status: proc.table.add(new SocketDescription(new VirtualTcpSocket(proc.sockets))) };
@@ -22325,9 +22602,15 @@ function createHostAbiServer(proc) {
22325
22602
  }
22326
22603
  case Op.connect: {
22327
22604
  const description = socketDescription(proc.table.get(r.i32()));
22328
- description.connect(r.string(), r.u32());
22605
+ await description.connect(r.string(), r.u32());
22329
22606
  return { status: 0 };
22330
22607
  }
22608
+ case Op.resolve: {
22609
+ const sockets = proc.sockets;
22610
+ if (!sockets?.outbound) throw new PosixError(Errno.ENOSYS);
22611
+ const address = sockets.outbound.resolve(r.string());
22612
+ return { status: 0, payload: new Writer().string(address).finish() };
22613
+ }
22331
22614
  case Op.send: {
22332
22615
  const description = socketDescription(proc.table.get(r.i32()));
22333
22616
  return { status: await writeBlocking(description, r.bytes32()) };
@@ -22885,6 +23168,32 @@ async function serve(ctx, connection) {
22885
23168
  connection.close();
22886
23169
  }
22887
23170
  }
23171
+ async function serveHttpOverConnection(connection, handle) {
23172
+ try {
23173
+ const request = await readRequest(connection);
23174
+ if (!request) {
23175
+ writeResponse(connection, 400, "Bad Request", {}, new TextEncoder().encode("malformed request\n"));
23176
+ return;
23177
+ }
23178
+ const result = await handle(request);
23179
+ writeResponse(connection, result.status, result.statusText, result.headers, result.body);
23180
+ } catch (error) {
23181
+ const message = error instanceof Error ? error.message : String(error);
23182
+ try {
23183
+ writeResponse(
23184
+ connection,
23185
+ 502,
23186
+ "Bad Gateway",
23187
+ { "x-sbx-error": "1" },
23188
+ new TextEncoder().encode(`${message}
23189
+ `)
23190
+ );
23191
+ } catch {
23192
+ }
23193
+ } finally {
23194
+ connection.close();
23195
+ }
23196
+ }
22888
23197
  var listeners = /* @__PURE__ */ new WeakMap();
22889
23198
  function ensureHttpEgress(ctx) {
22890
23199
  const sockets = ctx.kernel.pod.sockets;
@@ -23415,7 +23724,7 @@ async function resolve2(options) {
23415
23724
  if (viable.length === 0) {
23416
23725
  const wanted = [...unusable.get(name)?.versions ?? []].filter((version) => clauses.every((s) => compareVersions(version, s.version, s.op)));
23417
23726
  const nothingUsable = all.length === 0 || wanted.length > 0;
23418
- const error2 = new ResolutionError(
23727
+ const error = new ResolutionError(
23419
23728
  nothingUsable ? `no distribution of ${name}${all.length > 0 ? ` ${newest(wanted)}` : ""} is usable by this runtime (it needs a pure-Python wheel or one tagged ${WHEEL_TAG})` : `no version of ${name} satisfies every requirement`,
23420
23729
  explain(name, next.constraints, null, all),
23421
23730
  nothingUsable ? {
@@ -23427,8 +23736,8 @@ async function resolve2(options) {
23427
23736
  sourceAvailable: unusable.get(name)?.sourceAvailable ?? false
23428
23737
  } : { reason: "conflicting-requirements", package: name }
23429
23738
  );
23430
- nogoods.set(signature, error2);
23431
- throw error2;
23739
+ nogoods.set(signature, error);
23740
+ throw error;
23432
23741
  }
23433
23742
  let failure2 = null;
23434
23743
  const seenVersions = /* @__PURE__ */ new Set();
@@ -23440,9 +23749,9 @@ async function resolve2(options) {
23440
23749
  let requires;
23441
23750
  try {
23442
23751
  requires = await requiresFor(candidate);
23443
- } catch (error2) {
23752
+ } catch (error) {
23444
23753
  failure2 = new ResolutionError(
23445
- `could not read the dependencies of ${name} ${candidate.version}: ${error2.message}`
23754
+ `could not read the dependencies of ${name} ${candidate.version}: ${error.message}`
23446
23755
  );
23447
23756
  continue;
23448
23757
  }
@@ -23466,15 +23775,13 @@ async function resolve2(options) {
23466
23775
  chosen.set(name, { ...candidate, requires });
23467
23776
  try {
23468
23777
  return await search([...edges, ...rest], { chosen, constraints: next.constraints });
23469
- } catch (error2) {
23470
- if (!(error2 instanceof ResolutionError)) throw error2;
23471
- if (/cancelled|budget/.test(error2.message)) throw error2;
23472
- failure2 = error2;
23778
+ } catch (error) {
23779
+ if (!(error instanceof ResolutionError)) throw error;
23780
+ if (/cancelled|budget/.test(error.message)) throw error;
23781
+ failure2 = error;
23473
23782
  }
23474
23783
  }
23475
- const error = failure2 ?? new ResolutionError(`could not resolve ${name}`);
23476
- nogoods.set(signature, error);
23477
- throw error;
23784
+ throw failure2 ?? new ResolutionError(`could not resolve ${name}`);
23478
23785
  };
23479
23786
  const seeded = /* @__PURE__ */ new Map();
23480
23787
  for (const root of roots) {
@@ -23980,8 +24287,18 @@ async function runOwnedPython(ctx, argv) {
23980
24287
  if (!manifest) throw new Error("no Python runtime manifest is configured");
23981
24288
  const mounts = containerMounts(ctx);
23982
24289
  const env2 = withSitePackages(ctx.env);
24290
+ await ensureTrustStore(ctx);
23983
24291
  const egressPort = ensureHttpEgress(ctx);
23984
24292
  if (egressPort > 0) env2[EGRESS_ENV] = `127.0.0.1:${egressPort}`;
24293
+ try {
24294
+ if (ctx.vfs.stat(CA_BUNDLE_PATH, { cred: ctx.cred }).size > 0) {
24295
+ env2.SSL_CERT_FILE = CA_BUNDLE_PATH;
24296
+ env2.SSL_CERT_DIR = "/etc/ssl/certs";
24297
+ env2.REQUESTS_CA_BUNDLE = CA_BUNDLE_PATH;
24298
+ }
24299
+ } catch {
24300
+ }
24301
+ if (ctx.kernel.pod.sockets?.outbound) env2.SBX_OUTBOUND_SOCKETS = "1";
23985
24302
  env2.SBX_SERIAL_HOST_CALLS = "1";
23986
24303
  const inherit = claimInheritance(env2);
23987
24304
  const process2 = await startPythonProcess({
@@ -24022,6 +24339,200 @@ function isTerminal(ctx) {
24022
24339
  const stdin = ctx.stdin;
24023
24340
  return Boolean(stdin.isTTY || stdin.interactive);
24024
24341
  }
24342
+ var TLS_MODULE_SOURCE = `"""TLS over a SandboxedJs socket, using this interpreter's own OpenSSL.
24343
+
24344
+ CPython's \`ssl\` cannot wrap one of these sockets directly: \`_ssl\` wants a real
24345
+ \`_socket.socket\` whose descriptor OpenSSL can read and write, and a socket here
24346
+ is a host resource reached through the SandboxedJs ABI. But OpenSSL does not
24347
+ need a descriptor -- \`SSLContext.wrap_bio\` drives the same protocol over a pair
24348
+ of memory buffers, which is how asyncio has always done TLS. So the handshake
24349
+ and every record are performed here, in the container, and only the ciphertext
24350
+ crosses to the host. Certificates are verified by this interpreter against the
24351
+ trust store the host writes into the image, and \`getpeercert\` answers.
24352
+ """
24353
+
24354
+ import io
24355
+ import socket as _socket_module
24356
+ import ssl as _ssl
24357
+
24358
+
24359
+ def wrappable(sock):
24360
+ """Whether this socket is one of the container's own."""
24361
+ target = getattr(sock, "_sock", sock)
24362
+ return hasattr(target, "_fd")
24363
+
24364
+
24365
+ class SbxTlsSocket:
24366
+ """What \`SSLContext.wrap_socket\` returns for a container socket."""
24367
+
24368
+ def __init__(self, context, sock, server_hostname, suppress_ragged_eofs=True):
24369
+ self._sock = sock
24370
+ self.context = context
24371
+ self.server_hostname = server_hostname
24372
+ self.suppress_ragged_eofs = suppress_ragged_eofs
24373
+ self._incoming = _ssl.MemoryBIO()
24374
+ self._outgoing = _ssl.MemoryBIO()
24375
+ self._closed = False
24376
+ self._tls = context.wrap_bio(
24377
+ self._incoming, self._outgoing,
24378
+ server_side=False,
24379
+ server_hostname=server_hostname,
24380
+ )
24381
+
24382
+ # -- moving bytes between OpenSSL and the socket ------------------------
24383
+ def _flush(self):
24384
+ data = self._outgoing.read()
24385
+ if data:
24386
+ self._sock.sendall(data)
24387
+
24388
+ def _feed(self):
24389
+ chunk = self._sock.recv(32768)
24390
+ if not chunk:
24391
+ self._incoming.write_eof()
24392
+ return False
24393
+ self._incoming.write(chunk)
24394
+ return True
24395
+
24396
+ def _run(self, operation):
24397
+ """Drive one OpenSSL operation, pumping the socket as it asks."""
24398
+ while True:
24399
+ try:
24400
+ result = operation()
24401
+ except _ssl.SSLWantReadError:
24402
+ self._flush()
24403
+ if not self._feed():
24404
+ raise
24405
+ continue
24406
+ except _ssl.SSLWantWriteError:
24407
+ self._flush()
24408
+ continue
24409
+ self._flush()
24410
+ return result
24411
+
24412
+ # -- the handshake ------------------------------------------------------
24413
+ def do_handshake(self, block=False):
24414
+ self._run(self._tls.do_handshake)
24415
+
24416
+ # -- the socket ---------------------------------------------------------
24417
+ def recv(self, length=1024, flags=0):
24418
+ if flags:
24419
+ raise OSError("socket flags are not supported")
24420
+ try:
24421
+ return self._run(lambda: self._tls.read(length))
24422
+ except _ssl.SSLZeroReturnError:
24423
+ return b""
24424
+ except _ssl.SSLWantReadError:
24425
+ # The peer closed without close_notify. Ragged, and normal enough
24426
+ # on the public internet that clients ask to tolerate it.
24427
+ if self.suppress_ragged_eofs:
24428
+ return b""
24429
+ raise
24430
+
24431
+ def recv_into(self, buffer, nbytes=0, flags=0):
24432
+ data = self.recv(nbytes or len(buffer), flags)
24433
+ buffer[:len(data)] = data
24434
+ return len(data)
24435
+
24436
+ def read(self, length=1024, buffer=None):
24437
+ if buffer is not None:
24438
+ return self.recv_into(buffer, length)
24439
+ return self.recv(length)
24440
+
24441
+ def send(self, data, flags=0):
24442
+ if flags:
24443
+ raise OSError("socket flags are not supported")
24444
+ return self._run(lambda: self._tls.write(data))
24445
+
24446
+ def sendall(self, data, flags=0):
24447
+ view = memoryview(data)
24448
+ while view:
24449
+ view = view[self.send(view, flags):]
24450
+
24451
+ def write(self, data):
24452
+ return self.send(data)
24453
+
24454
+ _io_refs = 0
24455
+
24456
+ def _decref_socketios(self):
24457
+ # socket.SocketIO calls this when a file object built on this socket is
24458
+ # closed; without it http.client failed with "no attribute
24459
+ # _decref_socketios" the moment it read a response.
24460
+ if self._io_refs > 0:
24461
+ self._io_refs -= 1
24462
+
24463
+ def makefile(self, mode="r", buffering=None, *, encoding=None, errors=None, newline=None):
24464
+ # http.client and everything built on it read the response through a
24465
+ # file object, so this has to work like socket.makefile does.
24466
+ raw = _socket_module.SocketIO(self, "rw" if "w" in mode else "r")
24467
+ self._io_refs += 1
24468
+ buffer = io.BufferedRWPair(raw, raw) if "w" in mode and "r" in mode else (
24469
+ io.BufferedWriter(raw) if "w" in mode else io.BufferedReader(raw)
24470
+ )
24471
+ if "b" in mode:
24472
+ return buffer
24473
+ text = io.TextIOWrapper(buffer, encoding, errors, newline)
24474
+ text.mode = mode
24475
+ return text
24476
+
24477
+ def settimeout(self, value): return self._sock.settimeout(value)
24478
+ def gettimeout(self): return self._sock.gettimeout()
24479
+ def setblocking(self, flag): return self._sock.setblocking(flag)
24480
+ def getblocking(self): return self._sock.getblocking()
24481
+ def setsockopt(self, *args): return self._sock.setsockopt(*args)
24482
+ def getsockopt(self, *args): return self._sock.getsockopt(*args)
24483
+ def fileno(self): return self._sock.fileno()
24484
+ def getpeername(self): return self._sock.getpeername()
24485
+ def getsockname(self): return self._sock.getsockname()
24486
+ def detach(self): return self._sock.detach()
24487
+
24488
+ def shutdown(self, how):
24489
+ return self._sock.shutdown(how)
24490
+
24491
+ def unwrap(self):
24492
+ self._run(self._tls.unwrap)
24493
+ return self._sock
24494
+
24495
+ def close(self):
24496
+ if not self._closed:
24497
+ self._closed = True
24498
+ self._sock.close()
24499
+
24500
+ def __enter__(self): return self
24501
+ def __exit__(self, *args): self.close()
24502
+
24503
+ @property
24504
+ def family(self): return getattr(self._sock, "family", _socket_module.AF_INET)
24505
+ @property
24506
+ def type(self): return getattr(self._sock, "type", _socket_module.SOCK_STREAM)
24507
+ @property
24508
+ def proto(self): return getattr(self._sock, "proto", 0)
24509
+ @property
24510
+ def session(self): return self._tls.session
24511
+ @session.setter
24512
+ def session(self, value): self._tls.session = value
24513
+ @property
24514
+ def session_reused(self): return self._tls.session_reused
24515
+
24516
+ # -- what OpenSSL knows about this connection ---------------------------
24517
+ def getpeercert(self, binary_form=False): return self._tls.getpeercert(binary_form)
24518
+ def get_unverified_chain(self): return self._tls.get_unverified_chain()
24519
+ def get_verified_chain(self): return self._tls.get_verified_chain()
24520
+ def cipher(self): return self._tls.cipher()
24521
+ def shared_ciphers(self): return self._tls.shared_ciphers()
24522
+ def compression(self): return self._tls.compression()
24523
+ def selected_alpn_protocol(self): return self._tls.selected_alpn_protocol()
24524
+ def selected_npn_protocol(self): return None
24525
+ def version(self): return self._tls.version()
24526
+ def pending(self): return self._tls.pending()
24527
+ def get_channel_binding(self, cb_type="tls-unique"):
24528
+ return self._tls.get_channel_binding(cb_type)
24529
+
24530
+
24531
+ def wrap(context, sock, server_hostname, suppress_ragged_eofs=True):
24532
+ wrapped = SbxTlsSocket(context, sock, server_hostname, suppress_ragged_eofs)
24533
+ wrapped.do_handshake()
24534
+ return wrapped
24535
+ `;
24025
24536
  var SHIM_HEADER = "# generated by sandboxedjs - safe to delete or replace\n";
24026
24537
  var SHIM_SOURCE = `"""Rebuild os.system and os.popen on the container's process table."""
24027
24538
  import io, os, subprocess, sys
@@ -24158,7 +24669,51 @@ if sys.platform in ("emscripten", "wasi"):
24158
24669
  sys.meta_path.insert(0, _SbxPyodideFetchHook())
24159
24670
 
24160
24671
 
24161
- _egress = os.environ.get("SBX_HTTP_EGRESS")
24672
+ # The egress shims exist for a host that cannot open sockets. Where the host
24673
+ # can, the guest's own clients are used instead: real streaming, real TLS, and
24674
+ # whatever protocol the library speaks rather than the subset HTTP-over-egress
24675
+ # allows. Both worlds are supported, and which one this is comes from the host
24676
+ # rather than from probing.
24677
+ if os.environ.get("SBX_OUTBOUND_SOCKETS") == "1":
24678
+
24679
+ class _SbxTruststoreHook:
24680
+ """truststore verifies the peer's chain itself, against the operating
24681
+ system's trust store. In this container the handshake happens on the
24682
+ host, which verified the chain against exactly that store, and the
24683
+ chain is not carried back -- so truststore's own check cannot run and
24684
+ httpx2 failed with "no attribute get_unverified_chain". Its context is
24685
+ therefore the standard one: same verification, done once, where the
24686
+ connection is."""
24687
+
24688
+ def find_spec(self, name, path=None, target=None):
24689
+ if name != "truststore":
24690
+ return None
24691
+ import importlib.util
24692
+
24693
+ class _Loader:
24694
+ def create_module(self, spec):
24695
+ return None
24696
+
24697
+ def exec_module(self, module):
24698
+ import ssl as _ssl
24699
+ module.SSLContext = _ssl.SSLContext
24700
+
24701
+ def inject_into_ssl():
24702
+ pass
24703
+
24704
+ def extract_from_ssl():
24705
+ pass
24706
+
24707
+ module.inject_into_ssl = inject_into_ssl
24708
+ module.extract_from_ssl = extract_from_ssl
24709
+ module.__version__ = "sandboxedjs"
24710
+
24711
+ return importlib.util.spec_from_loader(name, _Loader(), is_package=True)
24712
+
24713
+ sys.meta_path.insert(0, _SbxTruststoreHook())
24714
+
24715
+
24716
+ _egress = None if os.environ.get("SBX_OUTBOUND_SOCKETS") == "1" else os.environ.get("SBX_HTTP_EGRESS")
24162
24717
  if _egress:
24163
24718
  import http.client
24164
24719
  import urllib.request
@@ -24556,10 +25111,86 @@ if os.environ.get("SBX_SERIAL_HOST_CALLS") == "1":
24556
25111
  return spec
24557
25112
 
24558
25113
  sys.meta_path.insert(0, _SbxAnyioThreadHook())
25114
+
25115
+ class _SbxLoopResolverHook:
25116
+ """asyncio resolves names on a worker thread, which cannot run here.
25117
+
25118
+ loop.getaddrinfo hands the call to a ThreadPoolExecutor and awaits
25119
+ it -- the deadlock described above -- so every asyncio client that
25120
+ connects by name hung: aiohttp never got past its first request. In
25121
+ this container resolution is a host call that answers immediately, so
25122
+ it is done on the loop thread instead of being handed away."""
25123
+
25124
+ def find_spec(self, name, path=None, target=None):
25125
+ if name != "asyncio.base_events":
25126
+ return None
25127
+ import importlib.machinery
25128
+ spec = importlib.machinery.PathFinder.find_spec(name, path)
25129
+ if spec is None or spec.loader is None:
25130
+ return spec
25131
+ load = spec.loader.exec_module
25132
+
25133
+ def exec_module(module):
25134
+ load(module)
25135
+ import socket as _socket
25136
+
25137
+ async def getaddrinfo(self, host, port, *, family=0, type=0, proto=0, flags=0):
25138
+ return _socket.getaddrinfo(host, port, family, type, proto, flags)
25139
+
25140
+ async def getnameinfo(self, sockaddr, flags=0):
25141
+ return _socket.getnameinfo(sockaddr, flags)
25142
+
25143
+ module.BaseEventLoop.getaddrinfo = getaddrinfo
25144
+ module.BaseEventLoop.getnameinfo = getnameinfo
25145
+
25146
+ spec.loader.exec_module = exec_module
25147
+ return spec
25148
+
25149
+ sys.meta_path.insert(0, _SbxLoopResolverHook())
25150
+
25151
+ def _sbx_patch_loop_resolver(module):
25152
+ """Also patch the module if it is already imported.
25153
+
25154
+ This file imports asyncio itself, a few lines above, so by the time the
25155
+ hook is installed asyncio.base_events is usually already in
25156
+ sys.modules and the hook would never see it -- which is why aiohttp
25157
+ still hung after the hook existed."""
25158
+ import socket as _socket
25159
+
25160
+ async def getaddrinfo(self, host, port, *, family=0, type=0, proto=0, flags=0):
25161
+ return _socket.getaddrinfo(host, port, family, type, proto, flags)
25162
+
25163
+ async def getnameinfo(self, sockaddr, flags=0):
25164
+ return _socket.getnameinfo(sockaddr, flags)
25165
+
25166
+ module.BaseEventLoop.getaddrinfo = getaddrinfo
25167
+ module.BaseEventLoop.getnameinfo = getnameinfo
25168
+
25169
+ _loaded_base_events = sys.modules.get("asyncio.base_events")
25170
+ if _loaded_base_events is not None:
25171
+ _sbx_patch_loop_resolver(_loaded_base_events)
24559
25172
  `;
25173
+ var CA_BUNDLE_PATH = "/etc/ssl/certs/ca-certificates.crt";
25174
+ async function ensureTrustStore(ctx) {
25175
+ try {
25176
+ if (ctx.vfs.stat(CA_BUNDLE_PATH, { cred: ctx.cred }).size > 0) return;
25177
+ } catch {
25178
+ }
25179
+ try {
25180
+ const tls = await nodeBuiltin("tls");
25181
+ const bundle = tls.rootCertificates.join("\n");
25182
+ if (!bundle) return;
25183
+ ctx.vfs.mkdir("/etc/ssl/certs", { cred: ctx.cred, recursive: true });
25184
+ ctx.vfs.writeFile(CA_BUNDLE_PATH, bundle, { privileged: true, mode: 420 });
25185
+ } catch {
25186
+ }
25187
+ }
24560
25188
  function ensureProcessShims(ctx) {
24561
- const hook = `${SITE_PACKAGES}/sitecustomize.py`;
24562
- const source = `${SHIM_HEADER}${SHIM_SOURCE}`;
25189
+ writeShim(ctx, `${SITE_PACKAGES}/sitecustomize.py`, SHIM_SOURCE);
25190
+ writeShim(ctx, `${SITE_PACKAGES}/_sbx_tls.py`, TLS_MODULE_SOURCE);
25191
+ }
25192
+ function writeShim(ctx, hook, body) {
25193
+ const source = `${SHIM_HEADER}${body}`;
24563
25194
  try {
24564
25195
  const existing = ctx.vfs.readText(hook, ctx.cred);
24565
25196
  if (existing === source || !existing.startsWith(SHIM_HEADER)) return;
@@ -24606,6 +25237,7 @@ function containerMounts(ctx) {
24606
25237
 
24607
25238
  // src/python/pip-command.ts
24608
25239
  init_binary();
25240
+ var RESOLVE_DEADLINE_MS = 15 * 6e4;
24609
25241
  async function requestRemoteBuild(service, requirement, line) {
24610
25242
  line(`Asking ${new URL(service).host} to build ${requirement}`);
24611
25243
  try {
@@ -24808,6 +25440,14 @@ environments. A package with no usable wheel is an error, never a silent skip.`,
24808
25440
  vfs: ctx.vfs,
24809
25441
  cred: ctx.cred,
24810
25442
  requirements,
25443
+ /* The resolver's own default is three minutes, which is a fine bound
25444
+ * for a library but far too tight for a real requirements file: a
25445
+ * `langchain` plus `deepagents` graph is hundreds of distributions,
25446
+ * each needing a metadata fetch, and the install died mid-resolution
25447
+ * with "exceeded its 180s budget" while still making progress. `pip`
25448
+ * already has a 20-minute ceiling on the whole command; this leaves
25449
+ * room for the download phase inside it. */
25450
+ deadlineMs: RESOLVE_DEADLINE_MS,
24811
25451
  pythonVersion: pythonBackend().manifest?.pythonVersion ?? "3.13.5",
24812
25452
  progress: {
24813
25453
  collecting: (name) => ctx.line(`Collecting ${name}`),
@@ -24960,7 +25600,7 @@ function mountContainerFs(FS, opts) {
24960
25600
  const throwErrno = (code) => {
24961
25601
  throw new FS.ErrnoError(emErrno(code));
24962
25602
  };
24963
- const translate = (e) => {
25603
+ const translate2 = (e) => {
24964
25604
  if (isSysError(e)) return throwErrno(e.code);
24965
25605
  throw new FS.ErrnoError(emErrno("EIO"));
24966
25606
  };
@@ -25009,7 +25649,7 @@ function mountContainerFs(FS, opts) {
25009
25649
  blocks: Math.ceil(st.size / 512)
25010
25650
  };
25011
25651
  } catch (e) {
25012
- return translate(e);
25652
+ return translate2(e);
25013
25653
  }
25014
25654
  },
25015
25655
  setattr(node2, attr) {
@@ -25025,7 +25665,7 @@ function mountContainerFs(FS, opts) {
25025
25665
  opts.onWrite?.(path);
25026
25666
  }
25027
25667
  } catch (e) {
25028
- translate(e);
25668
+ translate2(e);
25029
25669
  }
25030
25670
  },
25031
25671
  lookup(parent, name) {
@@ -25050,7 +25690,7 @@ function mountContainerFs(FS, opts) {
25050
25690
  }
25051
25691
  opts.onWrite?.(path);
25052
25692
  } catch (e) {
25053
- translate(e);
25693
+ translate2(e);
25054
25694
  }
25055
25695
  return backend.createNode(parent, name, mode, dev);
25056
25696
  },
@@ -25061,7 +25701,7 @@ function mountContainerFs(FS, opts) {
25061
25701
  vfs.rename(from, to, cred);
25062
25702
  opts.onWrite?.(to);
25063
25703
  } catch (e) {
25064
- translate(e);
25704
+ translate2(e);
25065
25705
  }
25066
25706
  oldNode.name = newName;
25067
25707
  oldNode.parent = newDir;
@@ -25072,7 +25712,7 @@ function mountContainerFs(FS, opts) {
25072
25712
  vfs.unlink(path, cred);
25073
25713
  opts.onWrite?.(path);
25074
25714
  } catch (e) {
25075
- translate(e);
25715
+ translate2(e);
25076
25716
  }
25077
25717
  },
25078
25718
  rmdir(parent, name) {
@@ -25081,7 +25721,7 @@ function mountContainerFs(FS, opts) {
25081
25721
  vfs.rmdir(path, cred);
25082
25722
  opts.onWrite?.(path);
25083
25723
  } catch (e) {
25084
- translate(e);
25724
+ translate2(e);
25085
25725
  }
25086
25726
  },
25087
25727
  readdir(node2) {
@@ -25089,7 +25729,7 @@ function mountContainerFs(FS, opts) {
25089
25729
  try {
25090
25730
  return [".", "..", ...vfs.readdir(path, cred)];
25091
25731
  } catch (e) {
25092
- return translate(e);
25732
+ return translate2(e);
25093
25733
  }
25094
25734
  },
25095
25735
  symlink(parent, newName, oldPath) {
@@ -25098,7 +25738,7 @@ function mountContainerFs(FS, opts) {
25098
25738
  vfs.symlink(oldPath, path, cred);
25099
25739
  opts.onWrite?.(path);
25100
25740
  } catch (e) {
25101
- translate(e);
25741
+ translate2(e);
25102
25742
  }
25103
25743
  },
25104
25744
  readlink(node2) {
@@ -25106,7 +25746,7 @@ function mountContainerFs(FS, opts) {
25106
25746
  try {
25107
25747
  return vfs.readlink(path, cred);
25108
25748
  } catch (e) {
25109
- return translate(e);
25749
+ return translate2(e);
25110
25750
  }
25111
25751
  }
25112
25752
  };
@@ -25118,7 +25758,7 @@ function mountContainerFs(FS, opts) {
25118
25758
  stream.sbxBuffer = vfs.lexists(path) ? vfs.readFile(path, cred) : new Uint8Array(0);
25119
25759
  stream.sbxDirty = false;
25120
25760
  } catch (e) {
25121
- translate(e);
25761
+ translate2(e);
25122
25762
  }
25123
25763
  },
25124
25764
  close(stream) {
@@ -25128,7 +25768,7 @@ function mountContainerFs(FS, opts) {
25128
25768
  vfs.writeFile(path, stream.sbxBuffer, { cred });
25129
25769
  opts.onWrite?.(path);
25130
25770
  } catch (e) {
25131
- translate(e);
25771
+ translate2(e);
25132
25772
  }
25133
25773
  stream.sbxDirty = false;
25134
25774
  },
@@ -25154,7 +25794,7 @@ function mountContainerFs(FS, opts) {
25154
25794
  opts.onWrite?.(path);
25155
25795
  stream.sbxDirty = false;
25156
25796
  } catch (e) {
25157
- translate(e);
25797
+ translate2(e);
25158
25798
  }
25159
25799
  return length;
25160
25800
  },
@@ -34835,6 +35475,212 @@ ${BUNDLER_HINT}`),
34835
35475
 
34836
35476
  // src/worker/local-runtime-pod.ts
34837
35477
  init_path();
35478
+
35479
+ // src/net/outbound-tcp.ts
35480
+ var FIRST_SYNTHETIC = 2887581696;
35481
+ var SYNTHETIC_COUNT = 65534;
35482
+ function toDotted(value) {
35483
+ return [value >>> 24, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
35484
+ }
35485
+ var OutboundTcp = class {
35486
+ constructor(policy, dialer) {
35487
+ this.policy = policy;
35488
+ this.dialer = dialer;
35489
+ }
35490
+ policy;
35491
+ dialer;
35492
+ byName = /* @__PURE__ */ new Map();
35493
+ byAddress = /* @__PURE__ */ new Map();
35494
+ next = 0;
35495
+ /** Whether this address was handed out by `resolve`. */
35496
+ knows(address) {
35497
+ return this.byAddress.has(address);
35498
+ }
35499
+ hostnameFor(address) {
35500
+ return this.byAddress.get(address);
35501
+ }
35502
+ /**
35503
+ * The address for `hostname`, allocating one on first use.
35504
+ *
35505
+ * Refusal happens here as well as at connect, because a name that cannot be
35506
+ * reached should fail as a resolution failure -- which is what every client
35507
+ * reports as "unknown host" rather than as a mid-connection error.
35508
+ */
35509
+ resolve(hostname) {
35510
+ const name = hostname.trim().toLowerCase();
35511
+ if (!name) throw new PosixError(Errno.EINVAL, "empty hostname");
35512
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(name)) return name;
35513
+ if (isLoopbackHostname(name)) return "127.0.0.1";
35514
+ const known = this.byName.get(name);
35515
+ if (known) return known;
35516
+ if (!this.allowed(name)) {
35517
+ throw new PosixError(
35518
+ Errno.ENETUNREACH,
35519
+ `cannot resolve ${hostname}: outbound network access is not allowed for this container`
35520
+ );
35521
+ }
35522
+ if (this.next >= SYNTHETIC_COUNT) {
35523
+ throw new PosixError(Errno.ENOSPC, "no addresses left for resolved names");
35524
+ }
35525
+ const address = toDotted(FIRST_SYNTHETIC + this.next + 1);
35526
+ this.next += 1;
35527
+ this.byName.set(name, address);
35528
+ this.byAddress.set(address, name);
35529
+ return address;
35530
+ }
35531
+ /** Open a connection to a resolved address, or to a literal one. */
35532
+ async connect(address, port, local) {
35533
+ const hostname = this.byAddress.get(address) ?? address;
35534
+ if (!this.allowed(hostname)) {
35535
+ throw new PosixError(
35536
+ Errno.ENETUNREACH,
35537
+ `cannot reach ${hostname}:${port}: outbound network access is not allowed for this container`
35538
+ );
35539
+ }
35540
+ if (!this.dialer) {
35541
+ throw new PosixError(
35542
+ Errno.ENETUNREACH,
35543
+ `cannot reach ${hostname}:${port}: this host cannot open sockets (a browser cannot), so outbound traffic has to leave over HTTP through the container's egress`
35544
+ );
35545
+ }
35546
+ const host2 = await this.dialer(hostname, port);
35547
+ const guest = new VirtualTcpConnection(
35548
+ { address: local.address, port: host2.localPort || local.port },
35549
+ { address, port }
35550
+ );
35551
+ const bridge = new VirtualTcpConnection({ address, port }, { address: local.address, port: local.port });
35552
+ guest.pairWith(bridge);
35553
+ bridge.pairWith(guest);
35554
+ host2.onData((bytes2) => {
35555
+ try {
35556
+ bridge.write(bytes2);
35557
+ } catch {
35558
+ host2.close();
35559
+ }
35560
+ });
35561
+ host2.onClose(() => {
35562
+ bridge.shutdown(false, true);
35563
+ bridge.close();
35564
+ });
35565
+ void (async () => {
35566
+ for (; ; ) {
35567
+ if (bridge.readable) {
35568
+ let chunk;
35569
+ try {
35570
+ chunk = bridge.read(64 * 1024);
35571
+ } catch {
35572
+ break;
35573
+ }
35574
+ if (chunk.length === 0) {
35575
+ host2.end();
35576
+ break;
35577
+ }
35578
+ host2.write(chunk);
35579
+ continue;
35580
+ }
35581
+ await bridge.waitForChange();
35582
+ }
35583
+ })();
35584
+ return { connection: guest, host: host2 };
35585
+ }
35586
+ allowed(hostname) {
35587
+ const policy = this.policy();
35588
+ if (isLoopbackHostname(hostname)) return true;
35589
+ if (!policy.allowOutbound) return false;
35590
+ const hosts = policy.allowedHosts;
35591
+ if (!hosts) return true;
35592
+ return hosts.some((allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`));
35593
+ }
35594
+ };
35595
+
35596
+ // src/net/node-tcp-dialer.ts
35597
+ init_binary();
35598
+ function canDialTcp() {
35599
+ return typeof process !== "undefined" && Boolean(process.versions?.node);
35600
+ }
35601
+ function createNodeTcpDialer() {
35602
+ return async (host2, port) => {
35603
+ const net = await nodeBuiltin("net");
35604
+ return await new Promise((resolve3, reject) => {
35605
+ const socket = net.connect({ host: host2, port });
35606
+ const pending = [];
35607
+ let onData = null;
35608
+ let onClose = null;
35609
+ let closed = false;
35610
+ socket.on("data", (chunk) => {
35611
+ const bytes2 = new Uint8Array(chunk);
35612
+ if (onData) onData(bytes2);
35613
+ else pending.push(bytes2);
35614
+ });
35615
+ const finish = () => {
35616
+ if (closed) return;
35617
+ closed = true;
35618
+ onClose?.();
35619
+ };
35620
+ socket.on("close", finish);
35621
+ socket.on("end", finish);
35622
+ socket.on("error", (error) => {
35623
+ if (!closed) {
35624
+ closed = true;
35625
+ onClose?.();
35626
+ }
35627
+ reject(translate(error, host2, port));
35628
+ });
35629
+ socket.on("connect", () => {
35630
+ socket.setNoDelay(true);
35631
+ resolve3({
35632
+ write(bytes2) {
35633
+ socket.write(bytes2);
35634
+ },
35635
+ end() {
35636
+ socket.end();
35637
+ },
35638
+ close() {
35639
+ socket.destroy();
35640
+ },
35641
+ onData(handler) {
35642
+ onData = handler;
35643
+ for (const chunk of pending.splice(0)) handler(chunk);
35644
+ },
35645
+ onClose(handler) {
35646
+ onClose = handler;
35647
+ if (closed) handler();
35648
+ },
35649
+ get remoteAddress() {
35650
+ return socket.remoteAddress ?? host2;
35651
+ },
35652
+ get remotePort() {
35653
+ return socket.remotePort ?? port;
35654
+ },
35655
+ get localPort() {
35656
+ return socket.localPort ?? 0;
35657
+ }
35658
+ });
35659
+ });
35660
+ });
35661
+ };
35662
+ }
35663
+ function translate(error, host2, port) {
35664
+ const where = `${host2}:${port}`;
35665
+ switch (error.code) {
35666
+ case "ECONNREFUSED":
35667
+ return new PosixError(Errno.ECONNREFUSED, `connection refused by ${where}`);
35668
+ case "ENOTFOUND":
35669
+ case "EAI_AGAIN":
35670
+ return new PosixError(Errno.ENETUNREACH, `cannot resolve ${host2}`);
35671
+ case "ETIMEDOUT":
35672
+ return new PosixError(Errno.ETIMEDOUT, `connection to ${where} timed out`);
35673
+ case "ECONNRESET":
35674
+ return new PosixError(Errno.ECONNRESET, `connection to ${where} was reset`);
35675
+ case "EHOSTUNREACH":
35676
+ case "ENETUNREACH":
35677
+ return new PosixError(Errno.ENETUNREACH, `cannot reach ${where}`);
35678
+ default:
35679
+ return new PosixError(Errno.ECONNREFUSED, `connection to ${where} failed: ${error.message}`);
35680
+ }
35681
+ }
35682
+
35683
+ // src/worker/local-runtime-pod.ts
34838
35684
  var WASM_ALIASES = {
34839
35685
  esbuild: "esbuild-wasm",
34840
35686
  rollup: "@rollup/wasm-node"
@@ -35190,6 +36036,32 @@ var LocalRuntimePod = class _LocalRuntimePod {
35190
36036
  constructor(options) {
35191
36037
  ensureProcessGlobal();
35192
36038
  this.sockets = new VirtualTcpNetwork((port) => this.router.activePortsIncludes(port));
36039
+ this.sockets.loopbackHttp = (port, connection) => {
36040
+ if (!this.router.activePortsIncludes(port)) return false;
36041
+ void serveHttpOverConnection(connection, async (request) => {
36042
+ const url = new URL(request.target, `http://127.0.0.1:${port}`);
36043
+ const answer = await this.router.request(port, {
36044
+ method: request.method,
36045
+ path: `${url.pathname}${url.search}`,
36046
+ headers: request.headers,
36047
+ ...request.body.length > 0 ? { body: request.body } : {}
36048
+ });
36049
+ const payload = answer.body ?? new Uint8Array();
36050
+ return {
36051
+ status: Number(answer.statusCode ?? 200),
36052
+ statusText: String(answer.statusMessage ?? ""),
36053
+ headers: answer.headers ?? {},
36054
+ body: typeof payload === "string" ? new TextEncoder().encode(payload) : payload instanceof Uint8Array ? payload : new Uint8Array(payload)
36055
+ };
36056
+ });
36057
+ return true;
36058
+ };
36059
+ if (canDialTcp()) {
36060
+ this.sockets.outbound = new OutboundTcp(
36061
+ () => this.networkPolicy ?? { allowOutbound: false, allowedHosts: null },
36062
+ createNodeTcpDialer()
36063
+ );
36064
+ }
35193
36065
  this.router = new VirtualHttpRouter(this.sockets);
35194
36066
  this.workdir = options.workdir ?? "/";
35195
36067
  this.env = { ...options.env };