sandboxedjs 0.1.92 → 0.1.93

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.
Files changed (3) hide show
  1. package/dist/index.cjs +488 -10
  2. package/dist/index.js +488 -10
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -20074,6 +20074,24 @@ var wheels_default = {
20074
20074
  patches: []
20075
20075
  }
20076
20076
  },
20077
+ {
20078
+ name: "brotli",
20079
+ version: "1.2.0",
20080
+ filename: "brotli-1.2.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20081
+ sha256: "b67d1b9169ba3e0790d04b8020adb1a58f0b89f8961aca360dca5f020de07a21",
20082
+ abiId: "sbxabi1-c2637d04695ad927",
20083
+ requires: [],
20084
+ provenance: {
20085
+ sourceTreeDigest: "7b563fa2e7f20edac204cc80004cae35c844b36a83758e5957184383332d1209",
20086
+ recipeSha256: "09bf4f6072d637866a6b3c3deaec419dc3ff8a32a6f3b0813f57e06316bfcf39",
20087
+ buildTools: [
20088
+ "setuptools==84.0.0",
20089
+ "pkgconfig==1.6.0"
20090
+ ],
20091
+ nativeDependencies: [],
20092
+ patches: []
20093
+ }
20094
+ },
20077
20095
  {
20078
20096
  name: "cffi",
20079
20097
  version: "2.1.1",
@@ -20099,14 +20117,14 @@ var wheels_default = {
20099
20117
  name: "cryptography",
20100
20118
  version: "50.0.1",
20101
20119
  filename: "cryptography-50.0.1-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20102
- sha256: "5c1498a64d987b68623a0e3ce5da36499cd69ad6a7e1309ec62c584ec7cc5f52",
20120
+ sha256: "10bb6568bf4368a593ea3a990ea870e57d4eeb1d3278af89bf24a8713473238e",
20103
20121
  abiId: "sbxabi1-c2637d04695ad927",
20104
20122
  requires: [
20105
20123
  'cffi>=2.0.0; platform_python_implementation != "PyPy"'
20106
20124
  ],
20107
20125
  provenance: {
20108
20126
  sourceTreeDigest: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
20109
- recipeSha256: "c7521d2b03c8bf941fe7d464ba54c8ec9356d823c715d65baab53e3223412dc6",
20127
+ recipeSha256: "082169d1d317ba001e37ccfb7a40a0dafd8d3d34e42034da8590f0207ae73ea5",
20110
20128
  buildTools: [
20111
20129
  "setuptools==84.0.0",
20112
20130
  "cffi==2.1.1"
@@ -20168,6 +20186,32 @@ var wheels_default = {
20168
20186
  patches: []
20169
20187
  }
20170
20188
  },
20189
+ {
20190
+ name: "lz4",
20191
+ version: "4.4.5",
20192
+ filename: "lz4-4.4.5-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20193
+ sha256: "f94c0ef7d84c9bf700cc86f69a28382699262e442e29d9763736afdb61141ffe",
20194
+ abiId: "sbxabi1-c2637d04695ad927",
20195
+ requires: [
20196
+ 'pytest!=3.3.0; extra == "tests"',
20197
+ 'psutil; extra == "tests"',
20198
+ 'pytest-cov; extra == "tests"',
20199
+ 'sphinx>=1.6.0; extra == "docs"',
20200
+ 'sphinx_bootstrap_theme; extra == "docs"',
20201
+ 'flake8; extra == "flake8"'
20202
+ ],
20203
+ provenance: {
20204
+ sourceTreeDigest: "5f7bda9bfb026d0a30bb5e3383a4b25ae6b7f44fd6dc74950653d57f84c9d88e",
20205
+ recipeSha256: "80250de0ae159cd9f977bdfad059de0d192f28555225b084b42b5a180f684e0c",
20206
+ buildTools: [
20207
+ "setuptools==84.0.0",
20208
+ "pkgconfig==1.6.0",
20209
+ "setuptools-scm==10.2.3"
20210
+ ],
20211
+ nativeDependencies: [],
20212
+ patches: []
20213
+ }
20214
+ },
20171
20215
  {
20172
20216
  name: "msgpack",
20173
20217
  version: "1.1.0",
@@ -20228,16 +20272,65 @@ var wheels_default = {
20228
20272
  name: "ormsgpack",
20229
20273
  version: "1.12.2",
20230
20274
  filename: "ormsgpack-1.12.2-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20231
- sha256: "42ca38d1948a36d43294c7843fd9398f1ae88bddd9e56b26ad80a644f9bdec52",
20275
+ sha256: "7fa4463a69aad2fe057c111642cce71c8f72af8465c22769d839415dbc15ac79",
20232
20276
  abiId: "sbxabi1-c2637d04695ad927",
20233
20277
  requires: [],
20234
20278
  provenance: {
20235
- sourceTreeDigest: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
20236
- recipeSha256: "6ab618fe6bc9dee1fa76f57f298c55732fe2241e122240f44007a2ce0025e1b2",
20279
+ sourceTreeDigest: "eff3a7be3bc6de2a15e479c0e3e94905c35203fd83369124073f2df5cce46409",
20280
+ recipeSha256: "bd398762801fce51d35b33439f5e57037e1d4d274f25bd795683e2ad17d34b62",
20237
20281
  buildTools: [
20238
20282
  "setuptools==84.0.0"
20239
20283
  ],
20240
20284
  nativeDependencies: [],
20285
+ patches: [
20286
+ "0001-pylong-asbytearray-313-signature.patch"
20287
+ ]
20288
+ }
20289
+ },
20290
+ {
20291
+ name: "pillow",
20292
+ version: "12.3.0",
20293
+ filename: "pillow-12.3.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20294
+ sha256: "7e585c71bf943029d8201283308a1b43f2be5f166d9beb36697d1f6bfba865e9",
20295
+ abiId: "sbxabi1-c2637d04695ad927",
20296
+ requires: [
20297
+ 'furo; extra == "docs"',
20298
+ 'olefile; extra == "docs"',
20299
+ 'sphinx>=8.2; extra == "docs"',
20300
+ 'sphinx-autobuild; extra == "docs"',
20301
+ 'sphinx-copybutton; extra == "docs"',
20302
+ 'sphinx-inline-tabs; extra == "docs"',
20303
+ 'sphinxext-opengraph; extra == "docs"',
20304
+ 'olefile; extra == "fpx"',
20305
+ 'olefile; extra == "mic"',
20306
+ 'arro3-compute; extra == "test-arrow"',
20307
+ 'arro3-core; extra == "test-arrow"',
20308
+ 'nanoarrow; extra == "test-arrow"',
20309
+ 'pyarrow; extra == "test-arrow"',
20310
+ 'coverage>=7.4.2; extra == "tests"',
20311
+ 'defusedxml; extra == "tests"',
20312
+ 'markdown2; extra == "tests"',
20313
+ 'olefile; extra == "tests"',
20314
+ 'packaging; extra == "tests"',
20315
+ 'pytest; extra == "tests"',
20316
+ 'pytest-cov; extra == "tests"',
20317
+ 'pytest-timeout; extra == "tests"',
20318
+ 'pytest-xdist; extra == "tests"',
20319
+ 'setuptools; extra == "tests"',
20320
+ 'trove-classifiers>=2024.10.12; extra == "tests"',
20321
+ 'defusedxml; extra == "xmp"'
20322
+ ],
20323
+ provenance: {
20324
+ sourceTreeDigest: "5427eff55a6a0e5f667d70527c2c3749f2c0786598903d7bbe7aaf16c4e95e31",
20325
+ recipeSha256: "77418b7e139e4779f2a422a06abbea27c941174b1f86597a2f3c4483bb236b7f",
20326
+ buildTools: [
20327
+ "setuptools==84.0.0",
20328
+ "pybind11==3.1.0"
20329
+ ],
20330
+ nativeDependencies: [
20331
+ "z==1.3.1",
20332
+ "jpeg==9f"
20333
+ ],
20241
20334
  patches: []
20242
20335
  }
20243
20336
  },
@@ -20503,6 +20596,23 @@ var wheels_default = {
20503
20596
  patches: []
20504
20597
  }
20505
20598
  },
20599
+ {
20600
+ name: "simplejson",
20601
+ version: "4.1.2",
20602
+ filename: "simplejson-4.1.2-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20603
+ sha256: "80671c2b02beb0d429108df8cefb6f27857b34e032bb5a5f1e0f957e7bf3cab6",
20604
+ abiId: "sbxabi1-c2637d04695ad927",
20605
+ requires: [],
20606
+ provenance: {
20607
+ sourceTreeDigest: "055af71755ff30d71076fdebc6f605e66f5876e6f01350a7d6075e8679006111",
20608
+ recipeSha256: "e163a1f2e78f32265c3ffdbf743dab27bc9e0829f21c553cabf0bafafd7d69fd",
20609
+ buildTools: [
20610
+ "setuptools==84.0.0"
20611
+ ],
20612
+ nativeDependencies: [],
20613
+ patches: []
20614
+ }
20615
+ },
20506
20616
  {
20507
20617
  name: "siphash24",
20508
20618
  version: "1.9",
@@ -20546,6 +20656,24 @@ var wheels_default = {
20546
20656
  patches: []
20547
20657
  }
20548
20658
  },
20659
+ {
20660
+ name: "ujson",
20661
+ version: "6.0.0",
20662
+ filename: "ujson-6.0.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20663
+ sha256: "3d4fd009acb9fad7a2070b0e2e498f6257e9c0cc28f806950d8c04cbb6849c24",
20664
+ abiId: "sbxabi1-c2637d04695ad927",
20665
+ requires: [],
20666
+ provenance: {
20667
+ sourceTreeDigest: "2a0c67ecb71628cb89b46090372d11dc4cf6e91c2406260a4e544c17331a31da",
20668
+ recipeSha256: "e9f3fe48e14241bd38d1d20dc691413d8c646984d67ae3898f4b27d10af4bf80",
20669
+ buildTools: [
20670
+ "setuptools==84.0.0",
20671
+ "setuptools-scm==10.2.3"
20672
+ ],
20673
+ nativeDependencies: [],
20674
+ patches: []
20675
+ }
20676
+ },
20549
20677
  {
20550
20678
  name: "uuid-utils",
20551
20679
  version: "0.17.1",
@@ -20628,7 +20756,7 @@ var wheels_default = {
20628
20756
 
20629
20757
  // package.json
20630
20758
  var package_default = {
20631
- version: "0.1.92"};
20759
+ version: "0.1.93"};
20632
20760
 
20633
20761
  // src/python/extension-abi.ts
20634
20762
  var EXTENSION_ABI = {
@@ -22733,7 +22861,13 @@ async function serve(ctx, connection) {
22733
22861
  headers: forwarded,
22734
22862
  ...request.body.length > 0 ? { body: request.body } : {}
22735
22863
  });
22736
- writeResponse(connection, result.status, result.statusText, result.headers, result.body);
22864
+ const {
22865
+ "content-encoding": _encoding,
22866
+ "content-length": _length,
22867
+ "transfer-encoding": _chunked,
22868
+ ...responseHeaders
22869
+ } = result.headers;
22870
+ writeResponse(connection, result.status, result.statusText, responseHeaders, result.body);
22737
22871
  } catch (error) {
22738
22872
  const message = error instanceof Error ? error.message : String(error);
22739
22873
  try {
@@ -23032,16 +23166,33 @@ def run(main, *, loop_factory=new_event_loop, debug=None, **run_kwargs):
23032
23166
  with __asyncio.Runner(loop_factory=loop_factory, debug=debug) as runner:
23033
23167
  return runner.run(main)
23034
23168
  `;
23035
- function builtinDistribution(name, version, summary, files) {
23169
+ var HTTPX2_JSFETCH_VERSION = "1.0";
23170
+ var HTTPX2_JSFETCH_SOURCE = `"""SandboxedJs stand-in for httpx2-jsfetch.
23171
+
23172
+ The real package drives the browser fetch API through Pyodide's \`js\` module.
23173
+ This runtime has no \`js\` module but does have sockets, so httpx2's regular
23174
+ httpcore2 transports are used under the names httpx2 imports on Emscripten.
23175
+ """
23176
+
23177
+ from httpx2._transports.default import (
23178
+ AsyncHTTPTransport as AsyncJavascriptFetchTransport,
23179
+ HTTPTransport as JavascriptFetchTransport,
23180
+ )
23181
+
23182
+ __all__ = ["AsyncJavascriptFetchTransport", "JavascriptFetchTransport"]
23183
+ `;
23184
+ function builtinDistribution(name, version, summary, files, requires = []) {
23036
23185
  return {
23037
23186
  version,
23187
+ requires,
23038
23188
  files: {
23039
23189
  ...files,
23040
23190
  [`${name}-${version}.dist-info/METADATA`]: `Metadata-Version: 2.1
23041
23191
  Name: ${name}
23042
23192
  Version: ${version}
23043
23193
  Summary: ${summary}
23044
- `,
23194
+ ` + requires.map((r) => `Requires-Dist: ${r}
23195
+ `).join(""),
23045
23196
  [`${name}-${version}.dist-info/WHEEL`]: "Wheel-Version: 1.0\nGenerator: sandboxedjs\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
23046
23197
  [`${name}-${version}.dist-info/INSTALLER`]: "sandboxedjs\n",
23047
23198
  [`${name}-${version}.dist-info/RECORD`]: ""
@@ -23049,6 +23200,13 @@ Summary: ${summary}
23049
23200
  };
23050
23201
  }
23051
23202
  var BUILTINS3 = {
23203
+ "httpx2-jsfetch": builtinDistribution(
23204
+ "httpx2_jsfetch",
23205
+ HTTPX2_JSFETCH_VERSION,
23206
+ "SandboxedJs stand-in: httpx2's socket transports under the jsfetch names",
23207
+ { "httpx2_jsfetch/__init__.py": HTTPX2_JSFETCH_SOURCE },
23208
+ ["httpcore2[asyncio]", "anyio>=4.10", "truststore>=0.10"]
23209
+ ),
23052
23210
  greenlet: builtinDistribution(
23053
23211
  "greenlet",
23054
23212
  GREENLET_VERSION,
@@ -23073,7 +23231,7 @@ function builtinCandidates(name) {
23073
23231
  filename: `${name}-${builtin.version}-py3-none-any.whl`,
23074
23232
  sha256: "",
23075
23233
  metadataUrl: null,
23076
- indexedRequires: []
23234
+ indexedRequires: builtin.requires
23077
23235
  }];
23078
23236
  }
23079
23237
  function isBuiltinOnly(name) {
@@ -23969,6 +24127,36 @@ if sys.platform in ("emscripten", "wasi"):
23969
24127
 
23970
24128
  sys.meta_path.insert(0, _SbxAsyncioWatcherHook())
23971
24129
 
24130
+ class _SbxPyodideFetchHook:
24131
+ """Libraries read \`sys.platform == "emscripten"\` as "running in Pyodide"
24132
+ and switch to a browser-fetch backend that imports Pyodide's \`js\`
24133
+ module. This runtime has no \`js\` but does have sockets, so those
24134
+ backends are replaced by modules that leave the socket path in place.
24135
+ urllib3 (and so requests) imports its backend at package import, which
24136
+ made anything using requests fail with "No module named 'js'"."""
24137
+
24138
+ _STANDINS = {
24139
+ # urllib3/__init__.py calls inject_into_urllib3() on emscripten.
24140
+ "urllib3.contrib.emscripten": "def inject_into_urllib3():\\n pass\\n",
24141
+ }
24142
+
24143
+ def find_spec(self, name, path=None, target=None):
24144
+ source = self._STANDINS.get(name)
24145
+ if source is None:
24146
+ return None
24147
+ import importlib.util
24148
+
24149
+ class _Loader:
24150
+ def create_module(self, spec):
24151
+ return None
24152
+
24153
+ def exec_module(self, module):
24154
+ exec(compile(source, f"<sandboxedjs {name}>", "exec"), module.__dict__)
24155
+
24156
+ return importlib.util.spec_from_loader(name, _Loader(), is_package=False)
24157
+
24158
+ sys.meta_path.insert(0, _SbxPyodideFetchHook())
24159
+
23972
24160
 
23973
24161
  _egress = os.environ.get("SBX_HTTP_EGRESS")
23974
24162
  if _egress:
@@ -24050,6 +24238,255 @@ if _egress:
24050
24238
  http.client.HTTPConnection = _SbxEgressConnection
24051
24239
  http.client.HTTPSConnection = _SbxEgressTlsConnection
24052
24240
 
24241
+ # Everything above routes code that reaches for the standard library. The
24242
+ # popular HTTP clients do not: urllib3 (so requests), httpcore (so httpx)
24243
+ # and httpcore2 (so httpx2) open sockets themselves, and raw sockets do not
24244
+ # leave this container -- connect() to anything but loopback is
24245
+ # ENETUNREACH, and CPython's ssl cannot wrap these sockets anyway. So each
24246
+ # client's connection layer is replaced by one that speaks plain HTTP to
24247
+ # the egress endpoint with the absolute URL in the request line, which is
24248
+ # what the standard-library shims above already do.
24249
+
24250
+ def _sbx_egress_request(method, url, headers, body, timeout=None):
24251
+ """One request through the egress. Returns (status, reason, headers, body)."""
24252
+ connection = _RealHTTPConnection(_egress_host, _egress_port, timeout=timeout)
24253
+ try:
24254
+ connection.request(method, url, body, {k: v for k, v in headers if k.lower() != "host"})
24255
+ response = connection.getresponse()
24256
+ payload = response.read()
24257
+ if response.getheader("x-sbx-error"):
24258
+ raise OSError(payload.decode("utf-8", "replace").strip())
24259
+ return response.status, response.reason, response.getheaders(), payload
24260
+ finally:
24261
+ connection.close()
24262
+
24263
+ async def _sbx_egress_request_async(method, url, headers, body):
24264
+ """The same hop as _sbx_egress_request, over asyncio streams."""
24265
+ import asyncio
24266
+ reader, writer = await asyncio.open_connection(_egress_host, _egress_port)
24267
+ try:
24268
+ payload = body or b""
24269
+ if isinstance(payload, str):
24270
+ payload = payload.encode()
24271
+ lines = [f"{method} {url} HTTP/1.1", f"Host: {_egress_host}:{_egress_port}"]
24272
+ for key, value in headers:
24273
+ if key.lower() in ("host", "connection", "content-length", "transfer-encoding"):
24274
+ continue
24275
+ lines.append(f"{key}: {value}")
24276
+ lines.append(f"Content-Length: {len(payload)}")
24277
+ # Read to EOF rather than tracking keep-alive framing: one request
24278
+ # per connection, which is what this hop is for.
24279
+ lines.append("Connection: close")
24280
+ writer.write(("\\r\\n".join(lines) + "\\r\\n\\r\\n").encode("latin-1") + payload)
24281
+ await writer.drain()
24282
+
24283
+ head = await reader.readuntil(b"\\r\\n\\r\\n")
24284
+ start, _, rest = head.decode("latin-1").partition("\\r\\n")
24285
+ _, _, status_text = start.partition(" ")
24286
+ code, _, reason = status_text.partition(" ")
24287
+ received = []
24288
+ for line in rest.split("\\r\\n"):
24289
+ key, sep, value = line.partition(":")
24290
+ if sep:
24291
+ received.append((key.strip(), value.strip()))
24292
+ content = dict((k.lower(), v) for k, v in received)
24293
+ if "content-length" in content:
24294
+ data = await reader.readexactly(int(content["content-length"]))
24295
+ else:
24296
+ data = await reader.read()
24297
+ if content.get("x-sbx-error"):
24298
+ raise OSError(data.decode("utf-8", "replace").strip())
24299
+ return int(code), reason, received, data
24300
+ finally:
24301
+ writer.close()
24302
+
24303
+ class _SbxUrllib3Hook:
24304
+ """urllib3's emscripten backend is Pyodide's \`js\` fetch, which this
24305
+ runtime has no module for; its socket backend cannot leave the
24306
+ container. Both connection classes become egress ones."""
24307
+
24308
+ def find_spec(self, name, path=None, target=None):
24309
+ if name != "urllib3.contrib.emscripten":
24310
+ return None
24311
+ import importlib.util
24312
+
24313
+ def inject_into_urllib3():
24314
+ import urllib3.connection
24315
+ from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool
24316
+
24317
+ class _Plain(urllib3.connection.HTTPConnection):
24318
+ """urllib3's own connection, dialled at the egress.
24319
+
24320
+ urllib3.connection.HTTPConnection still derives from
24321
+ http.client.HTTPConnection, so request building is
24322
+ inherited; only where it connects and what goes in the
24323
+ request line change.
24324
+ """
24325
+
24326
+ _sbx_scheme = "http"
24327
+ default_port = 80
24328
+ is_verified = True
24329
+ proxy_is_verified = None
24330
+
24331
+ def __init__(self, host, port=None, **kwargs):
24332
+ # A distinct name from the http.client shim's
24333
+ # _sbx_origin: that class is further up this MRO and
24334
+ # its __init__ runs after this one, so sharing the name
24335
+ # overwrote the real destination with the egress and
24336
+ # every request went out addressed to the egress itself.
24337
+ self._sbx_target = (host, port or self.default_port)
24338
+ for unsupported in ("ssl_context", "ssl_version", "ssl_minimum_version",
24339
+ "ssl_maximum_version", "assert_hostname",
24340
+ "assert_fingerprint", "server_hostname", "ca_certs",
24341
+ "ca_cert_dir", "ca_cert_data", "cert_file", "key_file",
24342
+ "key_password", "cert_reqs"):
24343
+ kwargs.pop(unsupported, None)
24344
+ super().__init__(_egress_host, port=_egress_port, **kwargs)
24345
+
24346
+ def _sbx_absolute(self, url):
24347
+ host, port = self._sbx_target
24348
+ if url.startswith("http://") or url.startswith("https://"):
24349
+ return url
24350
+ netloc = host if port == self.default_port else f"{host}:{port}"
24351
+ return f"{self._sbx_scheme}://{netloc}{url}"
24352
+
24353
+ # urllib3 v2 writes the request line in its own request()
24354
+ # rather than through http.client's putrequest, so both
24355
+ # are absolutised; the egress refuses an origin-form target.
24356
+ def request(self, method, url, *args, **kwargs):
24357
+ return super().request(method, self._sbx_absolute(url), *args, **kwargs)
24358
+
24359
+ def putrequest(self, method, url, *args, **kwargs):
24360
+ return super().putrequest(method, self._sbx_absolute(url), *args, **kwargs)
24361
+
24362
+ def set_tunnel(self, *args, **kwargs):
24363
+ raise OSError("this container reaches the network through its egress, not CONNECT")
24364
+
24365
+ class _Tls(_Plain):
24366
+ _sbx_scheme = "https"
24367
+ default_port = 443
24368
+
24369
+ HTTPConnectionPool.ConnectionCls = _Plain
24370
+ HTTPSConnectionPool.ConnectionCls = _Tls
24371
+ urllib3.connection.HTTPConnection = _Plain
24372
+ urllib3.connection.HTTPSConnection = _Tls
24373
+
24374
+ class _Loader:
24375
+ def create_module(self, spec):
24376
+ return None
24377
+
24378
+ def exec_module(self, module):
24379
+ module.inject_into_urllib3 = inject_into_urllib3
24380
+
24381
+ return importlib.util.spec_from_loader(name, _Loader(), is_package=True)
24382
+
24383
+ sys.meta_path.insert(0, _SbxUrllib3Hook())
24384
+
24385
+ def _sbx_httpx_transports(httpx_module):
24386
+ """Sync and async httpx transports that send through the egress."""
24387
+
24388
+ class _Transport(httpx_module.BaseTransport):
24389
+ def __init__(self, *args, **kwargs):
24390
+ pass
24391
+
24392
+ def handle_request(self, request):
24393
+ body = request.read()
24394
+ sent = [(k.decode("latin-1"), v.decode("latin-1")) for k, v in request.headers.raw]
24395
+ status, reason, headers, payload = _sbx_egress_request(
24396
+ request.method, str(request.url), sent, body,
24397
+ )
24398
+ # Content-Length/Transfer-Encoding describe the egress hop's
24399
+ # framing, and httpx re-derives them from the body it is given.
24400
+ keep = [(k, v) for k, v in headers
24401
+ if k.lower() not in ("content-length", "transfer-encoding", "content-encoding")]
24402
+ return httpx_module.Response(
24403
+ status, headers=keep, content=payload, request=request,
24404
+ extensions={"reason_phrase": (reason or "").encode()},
24405
+ )
24406
+
24407
+ def close(self):
24408
+ pass
24409
+
24410
+ def __enter__(self):
24411
+ return self
24412
+
24413
+ def __exit__(self, *args):
24414
+ pass
24415
+
24416
+ class _AsyncTransport(httpx_module.AsyncBaseTransport):
24417
+ def __init__(self, *args, **kwargs):
24418
+ pass
24419
+
24420
+ async def handle_async_request(self, request):
24421
+ # Not the blocking client on the loop thread: this image
24422
+ # proxies every syscall to one thread, so a blocking recv
24423
+ # inside a coroutine stops the loop that must service it. The
24424
+ # egress is loopback, which asyncio streams reach.
24425
+ import asyncio
24426
+ body = await request.aread()
24427
+ sent = [(k.decode("latin-1"), v.decode("latin-1")) for k, v in request.headers.raw]
24428
+ status, reason, headers, payload = await _sbx_egress_request_async(
24429
+ request.method, str(request.url), sent, body,
24430
+ )
24431
+ keep = [(k, v) for k, v in headers
24432
+ if k.lower() not in ("content-length", "transfer-encoding", "content-encoding")]
24433
+ return httpx_module.Response(
24434
+ status, headers=keep, content=payload, request=request,
24435
+ extensions={"reason_phrase": (reason or "").encode()},
24436
+ )
24437
+
24438
+ async def aclose(self):
24439
+ pass
24440
+
24441
+ async def __aenter__(self):
24442
+ return self
24443
+
24444
+ async def __aexit__(self, *args):
24445
+ pass
24446
+
24447
+ return _Transport, _AsyncTransport
24448
+
24449
+ class _SbxHttpxHook:
24450
+ """httpx and httpx2 build their default transports on httpcore, which
24451
+ opens sockets. Their default transport classes are replaced as the
24452
+ module loads, so \`httpx.get(...)\` and every client built from the
24453
+ defaults leave through the egress."""
24454
+
24455
+ _PACKAGES = {"httpx": "httpx", "httpx2": "httpx2"}
24456
+
24457
+ def find_spec(self, name, path=None, target=None):
24458
+ package = self._PACKAGES.get(name)
24459
+ if package is None or getattr(self, "_loading", None) == name:
24460
+ return None
24461
+ import importlib.machinery
24462
+ spec = importlib.machinery.PathFinder.find_spec(name, path)
24463
+ if spec is None or spec.loader is None:
24464
+ return spec
24465
+ load = spec.loader.exec_module
24466
+
24467
+ def exec_module(module):
24468
+ self._loading = name
24469
+ try:
24470
+ load(module)
24471
+ finally:
24472
+ self._loading = None
24473
+ transport, async_transport = _sbx_httpx_transports(module)
24474
+ module.HTTPTransport = transport
24475
+ module.AsyncHTTPTransport = async_transport
24476
+ defaults = sys.modules.get(f"{package}._transports.default")
24477
+ if defaults is not None:
24478
+ defaults.HTTPTransport = transport
24479
+ defaults.AsyncHTTPTransport = async_transport
24480
+ client = sys.modules.get(f"{package}._client")
24481
+ if client is not None:
24482
+ client.HTTPTransport = transport
24483
+ client.AsyncHTTPTransport = async_transport
24484
+
24485
+ spec.loader.exec_module = exec_module
24486
+ return spec
24487
+
24488
+ sys.meta_path.insert(0, _SbxHttpxHook())
24489
+
24053
24490
 
24054
24491
  if os.environ.get("SBX_SERIAL_HOST_CALLS") == "1":
24055
24492
  import asyncio
@@ -24078,6 +24515,47 @@ if os.environ.get("SBX_SERIAL_HOST_CALLS") == "1":
24078
24515
  )
24079
24516
 
24080
24517
  asyncio.to_thread = _to_thread
24518
+
24519
+ class _SbxAnyioThreadHook:
24520
+ """anyio's thread offload, run inline.
24521
+
24522
+ anyio has its own worker threads, so it never reaches asyncio.to_thread
24523
+ and the refusal above -- it deadlocks instead, for the reason described
24524
+ there. Starlette sends every *synchronous* endpoint through
24525
+ anyio.to_thread.run_sync, so a plain def FastAPI route hung forever
24526
+ while an async def one answered.
24527
+
24528
+ Running the function on the loop thread keeps such an endpoint working.
24529
+ The cost is real and worth stating: a synchronous handler blocks the
24530
+ loop for its duration, so it does not overlap with other requests.
24531
+ """
24532
+
24533
+ def find_spec(self, name, path=None, target=None):
24534
+ if name != "anyio.to_thread":
24535
+ return None
24536
+ import importlib.machinery
24537
+ spec = importlib.machinery.PathFinder.find_spec(name, path)
24538
+ if spec is None or spec.loader is None:
24539
+ return spec
24540
+ load = spec.loader.exec_module
24541
+
24542
+ def exec_module(module):
24543
+ load(module)
24544
+
24545
+ async def run_sync(func, *args, abandon_on_cancel=False,
24546
+ cancellable=None, limiter=None):
24547
+ return func(*args)
24548
+
24549
+ async def run_sync_in_worker_thread(func, *args, **kwargs):
24550
+ return func(*args)
24551
+
24552
+ module.run_sync = run_sync
24553
+ module.run_sync_in_worker_thread = run_sync_in_worker_thread
24554
+
24555
+ spec.loader.exec_module = exec_module
24556
+ return spec
24557
+
24558
+ sys.meta_path.insert(0, _SbxAnyioThreadHook())
24081
24559
  `;
24082
24560
  function ensureProcessShims(ctx) {
24083
24561
  const hook = `${SITE_PACKAGES}/sitecustomize.py`;
package/dist/index.js CHANGED
@@ -20057,6 +20057,24 @@ var wheels_default = {
20057
20057
  patches: []
20058
20058
  }
20059
20059
  },
20060
+ {
20061
+ name: "brotli",
20062
+ version: "1.2.0",
20063
+ filename: "brotli-1.2.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20064
+ sha256: "b67d1b9169ba3e0790d04b8020adb1a58f0b89f8961aca360dca5f020de07a21",
20065
+ abiId: "sbxabi1-c2637d04695ad927",
20066
+ requires: [],
20067
+ provenance: {
20068
+ sourceTreeDigest: "7b563fa2e7f20edac204cc80004cae35c844b36a83758e5957184383332d1209",
20069
+ recipeSha256: "09bf4f6072d637866a6b3c3deaec419dc3ff8a32a6f3b0813f57e06316bfcf39",
20070
+ buildTools: [
20071
+ "setuptools==84.0.0",
20072
+ "pkgconfig==1.6.0"
20073
+ ],
20074
+ nativeDependencies: [],
20075
+ patches: []
20076
+ }
20077
+ },
20060
20078
  {
20061
20079
  name: "cffi",
20062
20080
  version: "2.1.1",
@@ -20082,14 +20100,14 @@ var wheels_default = {
20082
20100
  name: "cryptography",
20083
20101
  version: "50.0.1",
20084
20102
  filename: "cryptography-50.0.1-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20085
- sha256: "5c1498a64d987b68623a0e3ce5da36499cd69ad6a7e1309ec62c584ec7cc5f52",
20103
+ sha256: "10bb6568bf4368a593ea3a990ea870e57d4eeb1d3278af89bf24a8713473238e",
20086
20104
  abiId: "sbxabi1-c2637d04695ad927",
20087
20105
  requires: [
20088
20106
  'cffi>=2.0.0; platform_python_implementation != "PyPy"'
20089
20107
  ],
20090
20108
  provenance: {
20091
20109
  sourceTreeDigest: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
20092
- recipeSha256: "c7521d2b03c8bf941fe7d464ba54c8ec9356d823c715d65baab53e3223412dc6",
20110
+ recipeSha256: "082169d1d317ba001e37ccfb7a40a0dafd8d3d34e42034da8590f0207ae73ea5",
20093
20111
  buildTools: [
20094
20112
  "setuptools==84.0.0",
20095
20113
  "cffi==2.1.1"
@@ -20151,6 +20169,32 @@ var wheels_default = {
20151
20169
  patches: []
20152
20170
  }
20153
20171
  },
20172
+ {
20173
+ name: "lz4",
20174
+ version: "4.4.5",
20175
+ filename: "lz4-4.4.5-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20176
+ sha256: "f94c0ef7d84c9bf700cc86f69a28382699262e442e29d9763736afdb61141ffe",
20177
+ abiId: "sbxabi1-c2637d04695ad927",
20178
+ requires: [
20179
+ 'pytest!=3.3.0; extra == "tests"',
20180
+ 'psutil; extra == "tests"',
20181
+ 'pytest-cov; extra == "tests"',
20182
+ 'sphinx>=1.6.0; extra == "docs"',
20183
+ 'sphinx_bootstrap_theme; extra == "docs"',
20184
+ 'flake8; extra == "flake8"'
20185
+ ],
20186
+ provenance: {
20187
+ sourceTreeDigest: "5f7bda9bfb026d0a30bb5e3383a4b25ae6b7f44fd6dc74950653d57f84c9d88e",
20188
+ recipeSha256: "80250de0ae159cd9f977bdfad059de0d192f28555225b084b42b5a180f684e0c",
20189
+ buildTools: [
20190
+ "setuptools==84.0.0",
20191
+ "pkgconfig==1.6.0",
20192
+ "setuptools-scm==10.2.3"
20193
+ ],
20194
+ nativeDependencies: [],
20195
+ patches: []
20196
+ }
20197
+ },
20154
20198
  {
20155
20199
  name: "msgpack",
20156
20200
  version: "1.1.0",
@@ -20211,16 +20255,65 @@ var wheels_default = {
20211
20255
  name: "ormsgpack",
20212
20256
  version: "1.12.2",
20213
20257
  filename: "ormsgpack-1.12.2-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20214
- sha256: "42ca38d1948a36d43294c7843fd9398f1ae88bddd9e56b26ad80a644f9bdec52",
20258
+ sha256: "7fa4463a69aad2fe057c111642cce71c8f72af8465c22769d839415dbc15ac79",
20215
20259
  abiId: "sbxabi1-c2637d04695ad927",
20216
20260
  requires: [],
20217
20261
  provenance: {
20218
- sourceTreeDigest: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
20219
- recipeSha256: "6ab618fe6bc9dee1fa76f57f298c55732fe2241e122240f44007a2ce0025e1b2",
20262
+ sourceTreeDigest: "eff3a7be3bc6de2a15e479c0e3e94905c35203fd83369124073f2df5cce46409",
20263
+ recipeSha256: "bd398762801fce51d35b33439f5e57037e1d4d274f25bd795683e2ad17d34b62",
20220
20264
  buildTools: [
20221
20265
  "setuptools==84.0.0"
20222
20266
  ],
20223
20267
  nativeDependencies: [],
20268
+ patches: [
20269
+ "0001-pylong-asbytearray-313-signature.patch"
20270
+ ]
20271
+ }
20272
+ },
20273
+ {
20274
+ name: "pillow",
20275
+ version: "12.3.0",
20276
+ filename: "pillow-12.3.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20277
+ sha256: "7e585c71bf943029d8201283308a1b43f2be5f166d9beb36697d1f6bfba865e9",
20278
+ abiId: "sbxabi1-c2637d04695ad927",
20279
+ requires: [
20280
+ 'furo; extra == "docs"',
20281
+ 'olefile; extra == "docs"',
20282
+ 'sphinx>=8.2; extra == "docs"',
20283
+ 'sphinx-autobuild; extra == "docs"',
20284
+ 'sphinx-copybutton; extra == "docs"',
20285
+ 'sphinx-inline-tabs; extra == "docs"',
20286
+ 'sphinxext-opengraph; extra == "docs"',
20287
+ 'olefile; extra == "fpx"',
20288
+ 'olefile; extra == "mic"',
20289
+ 'arro3-compute; extra == "test-arrow"',
20290
+ 'arro3-core; extra == "test-arrow"',
20291
+ 'nanoarrow; extra == "test-arrow"',
20292
+ 'pyarrow; extra == "test-arrow"',
20293
+ 'coverage>=7.4.2; extra == "tests"',
20294
+ 'defusedxml; extra == "tests"',
20295
+ 'markdown2; extra == "tests"',
20296
+ 'olefile; extra == "tests"',
20297
+ 'packaging; extra == "tests"',
20298
+ 'pytest; extra == "tests"',
20299
+ 'pytest-cov; extra == "tests"',
20300
+ 'pytest-timeout; extra == "tests"',
20301
+ 'pytest-xdist; extra == "tests"',
20302
+ 'setuptools; extra == "tests"',
20303
+ 'trove-classifiers>=2024.10.12; extra == "tests"',
20304
+ 'defusedxml; extra == "xmp"'
20305
+ ],
20306
+ provenance: {
20307
+ sourceTreeDigest: "5427eff55a6a0e5f667d70527c2c3749f2c0786598903d7bbe7aaf16c4e95e31",
20308
+ recipeSha256: "77418b7e139e4779f2a422a06abbea27c941174b1f86597a2f3c4483bb236b7f",
20309
+ buildTools: [
20310
+ "setuptools==84.0.0",
20311
+ "pybind11==3.1.0"
20312
+ ],
20313
+ nativeDependencies: [
20314
+ "z==1.3.1",
20315
+ "jpeg==9f"
20316
+ ],
20224
20317
  patches: []
20225
20318
  }
20226
20319
  },
@@ -20486,6 +20579,23 @@ var wheels_default = {
20486
20579
  patches: []
20487
20580
  }
20488
20581
  },
20582
+ {
20583
+ name: "simplejson",
20584
+ version: "4.1.2",
20585
+ filename: "simplejson-4.1.2-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20586
+ sha256: "80671c2b02beb0d429108df8cefb6f27857b34e032bb5a5f1e0f957e7bf3cab6",
20587
+ abiId: "sbxabi1-c2637d04695ad927",
20588
+ requires: [],
20589
+ provenance: {
20590
+ sourceTreeDigest: "055af71755ff30d71076fdebc6f605e66f5876e6f01350a7d6075e8679006111",
20591
+ recipeSha256: "e163a1f2e78f32265c3ffdbf743dab27bc9e0829f21c553cabf0bafafd7d69fd",
20592
+ buildTools: [
20593
+ "setuptools==84.0.0"
20594
+ ],
20595
+ nativeDependencies: [],
20596
+ patches: []
20597
+ }
20598
+ },
20489
20599
  {
20490
20600
  name: "siphash24",
20491
20601
  version: "1.9",
@@ -20529,6 +20639,24 @@ var wheels_default = {
20529
20639
  patches: []
20530
20640
  }
20531
20641
  },
20642
+ {
20643
+ name: "ujson",
20644
+ version: "6.0.0",
20645
+ filename: "ujson-6.0.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
20646
+ sha256: "3d4fd009acb9fad7a2070b0e2e498f6257e9c0cc28f806950d8c04cbb6849c24",
20647
+ abiId: "sbxabi1-c2637d04695ad927",
20648
+ requires: [],
20649
+ provenance: {
20650
+ sourceTreeDigest: "2a0c67ecb71628cb89b46090372d11dc4cf6e91c2406260a4e544c17331a31da",
20651
+ recipeSha256: "e9f3fe48e14241bd38d1d20dc691413d8c646984d67ae3898f4b27d10af4bf80",
20652
+ buildTools: [
20653
+ "setuptools==84.0.0",
20654
+ "setuptools-scm==10.2.3"
20655
+ ],
20656
+ nativeDependencies: [],
20657
+ patches: []
20658
+ }
20659
+ },
20532
20660
  {
20533
20661
  name: "uuid-utils",
20534
20662
  version: "0.17.1",
@@ -20611,7 +20739,7 @@ var wheels_default = {
20611
20739
 
20612
20740
  // package.json
20613
20741
  var package_default = {
20614
- version: "0.1.92"};
20742
+ version: "0.1.93"};
20615
20743
 
20616
20744
  // src/python/extension-abi.ts
20617
20745
  var EXTENSION_ABI = {
@@ -22716,7 +22844,13 @@ async function serve(ctx, connection) {
22716
22844
  headers: forwarded,
22717
22845
  ...request.body.length > 0 ? { body: request.body } : {}
22718
22846
  });
22719
- writeResponse(connection, result.status, result.statusText, result.headers, result.body);
22847
+ const {
22848
+ "content-encoding": _encoding,
22849
+ "content-length": _length,
22850
+ "transfer-encoding": _chunked,
22851
+ ...responseHeaders
22852
+ } = result.headers;
22853
+ writeResponse(connection, result.status, result.statusText, responseHeaders, result.body);
22720
22854
  } catch (error) {
22721
22855
  const message = error instanceof Error ? error.message : String(error);
22722
22856
  try {
@@ -23015,16 +23149,33 @@ def run(main, *, loop_factory=new_event_loop, debug=None, **run_kwargs):
23015
23149
  with __asyncio.Runner(loop_factory=loop_factory, debug=debug) as runner:
23016
23150
  return runner.run(main)
23017
23151
  `;
23018
- function builtinDistribution(name, version, summary, files) {
23152
+ var HTTPX2_JSFETCH_VERSION = "1.0";
23153
+ var HTTPX2_JSFETCH_SOURCE = `"""SandboxedJs stand-in for httpx2-jsfetch.
23154
+
23155
+ The real package drives the browser fetch API through Pyodide's \`js\` module.
23156
+ This runtime has no \`js\` module but does have sockets, so httpx2's regular
23157
+ httpcore2 transports are used under the names httpx2 imports on Emscripten.
23158
+ """
23159
+
23160
+ from httpx2._transports.default import (
23161
+ AsyncHTTPTransport as AsyncJavascriptFetchTransport,
23162
+ HTTPTransport as JavascriptFetchTransport,
23163
+ )
23164
+
23165
+ __all__ = ["AsyncJavascriptFetchTransport", "JavascriptFetchTransport"]
23166
+ `;
23167
+ function builtinDistribution(name, version, summary, files, requires = []) {
23019
23168
  return {
23020
23169
  version,
23170
+ requires,
23021
23171
  files: {
23022
23172
  ...files,
23023
23173
  [`${name}-${version}.dist-info/METADATA`]: `Metadata-Version: 2.1
23024
23174
  Name: ${name}
23025
23175
  Version: ${version}
23026
23176
  Summary: ${summary}
23027
- `,
23177
+ ` + requires.map((r) => `Requires-Dist: ${r}
23178
+ `).join(""),
23028
23179
  [`${name}-${version}.dist-info/WHEEL`]: "Wheel-Version: 1.0\nGenerator: sandboxedjs\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
23029
23180
  [`${name}-${version}.dist-info/INSTALLER`]: "sandboxedjs\n",
23030
23181
  [`${name}-${version}.dist-info/RECORD`]: ""
@@ -23032,6 +23183,13 @@ Summary: ${summary}
23032
23183
  };
23033
23184
  }
23034
23185
  var BUILTINS3 = {
23186
+ "httpx2-jsfetch": builtinDistribution(
23187
+ "httpx2_jsfetch",
23188
+ HTTPX2_JSFETCH_VERSION,
23189
+ "SandboxedJs stand-in: httpx2's socket transports under the jsfetch names",
23190
+ { "httpx2_jsfetch/__init__.py": HTTPX2_JSFETCH_SOURCE },
23191
+ ["httpcore2[asyncio]", "anyio>=4.10", "truststore>=0.10"]
23192
+ ),
23035
23193
  greenlet: builtinDistribution(
23036
23194
  "greenlet",
23037
23195
  GREENLET_VERSION,
@@ -23056,7 +23214,7 @@ function builtinCandidates(name) {
23056
23214
  filename: `${name}-${builtin.version}-py3-none-any.whl`,
23057
23215
  sha256: "",
23058
23216
  metadataUrl: null,
23059
- indexedRequires: []
23217
+ indexedRequires: builtin.requires
23060
23218
  }];
23061
23219
  }
23062
23220
  function isBuiltinOnly(name) {
@@ -23952,6 +24110,36 @@ if sys.platform in ("emscripten", "wasi"):
23952
24110
 
23953
24111
  sys.meta_path.insert(0, _SbxAsyncioWatcherHook())
23954
24112
 
24113
+ class _SbxPyodideFetchHook:
24114
+ """Libraries read \`sys.platform == "emscripten"\` as "running in Pyodide"
24115
+ and switch to a browser-fetch backend that imports Pyodide's \`js\`
24116
+ module. This runtime has no \`js\` but does have sockets, so those
24117
+ backends are replaced by modules that leave the socket path in place.
24118
+ urllib3 (and so requests) imports its backend at package import, which
24119
+ made anything using requests fail with "No module named 'js'"."""
24120
+
24121
+ _STANDINS = {
24122
+ # urllib3/__init__.py calls inject_into_urllib3() on emscripten.
24123
+ "urllib3.contrib.emscripten": "def inject_into_urllib3():\\n pass\\n",
24124
+ }
24125
+
24126
+ def find_spec(self, name, path=None, target=None):
24127
+ source = self._STANDINS.get(name)
24128
+ if source is None:
24129
+ return None
24130
+ import importlib.util
24131
+
24132
+ class _Loader:
24133
+ def create_module(self, spec):
24134
+ return None
24135
+
24136
+ def exec_module(self, module):
24137
+ exec(compile(source, f"<sandboxedjs {name}>", "exec"), module.__dict__)
24138
+
24139
+ return importlib.util.spec_from_loader(name, _Loader(), is_package=False)
24140
+
24141
+ sys.meta_path.insert(0, _SbxPyodideFetchHook())
24142
+
23955
24143
 
23956
24144
  _egress = os.environ.get("SBX_HTTP_EGRESS")
23957
24145
  if _egress:
@@ -24033,6 +24221,255 @@ if _egress:
24033
24221
  http.client.HTTPConnection = _SbxEgressConnection
24034
24222
  http.client.HTTPSConnection = _SbxEgressTlsConnection
24035
24223
 
24224
+ # Everything above routes code that reaches for the standard library. The
24225
+ # popular HTTP clients do not: urllib3 (so requests), httpcore (so httpx)
24226
+ # and httpcore2 (so httpx2) open sockets themselves, and raw sockets do not
24227
+ # leave this container -- connect() to anything but loopback is
24228
+ # ENETUNREACH, and CPython's ssl cannot wrap these sockets anyway. So each
24229
+ # client's connection layer is replaced by one that speaks plain HTTP to
24230
+ # the egress endpoint with the absolute URL in the request line, which is
24231
+ # what the standard-library shims above already do.
24232
+
24233
+ def _sbx_egress_request(method, url, headers, body, timeout=None):
24234
+ """One request through the egress. Returns (status, reason, headers, body)."""
24235
+ connection = _RealHTTPConnection(_egress_host, _egress_port, timeout=timeout)
24236
+ try:
24237
+ connection.request(method, url, body, {k: v for k, v in headers if k.lower() != "host"})
24238
+ response = connection.getresponse()
24239
+ payload = response.read()
24240
+ if response.getheader("x-sbx-error"):
24241
+ raise OSError(payload.decode("utf-8", "replace").strip())
24242
+ return response.status, response.reason, response.getheaders(), payload
24243
+ finally:
24244
+ connection.close()
24245
+
24246
+ async def _sbx_egress_request_async(method, url, headers, body):
24247
+ """The same hop as _sbx_egress_request, over asyncio streams."""
24248
+ import asyncio
24249
+ reader, writer = await asyncio.open_connection(_egress_host, _egress_port)
24250
+ try:
24251
+ payload = body or b""
24252
+ if isinstance(payload, str):
24253
+ payload = payload.encode()
24254
+ lines = [f"{method} {url} HTTP/1.1", f"Host: {_egress_host}:{_egress_port}"]
24255
+ for key, value in headers:
24256
+ if key.lower() in ("host", "connection", "content-length", "transfer-encoding"):
24257
+ continue
24258
+ lines.append(f"{key}: {value}")
24259
+ lines.append(f"Content-Length: {len(payload)}")
24260
+ # Read to EOF rather than tracking keep-alive framing: one request
24261
+ # per connection, which is what this hop is for.
24262
+ lines.append("Connection: close")
24263
+ writer.write(("\\r\\n".join(lines) + "\\r\\n\\r\\n").encode("latin-1") + payload)
24264
+ await writer.drain()
24265
+
24266
+ head = await reader.readuntil(b"\\r\\n\\r\\n")
24267
+ start, _, rest = head.decode("latin-1").partition("\\r\\n")
24268
+ _, _, status_text = start.partition(" ")
24269
+ code, _, reason = status_text.partition(" ")
24270
+ received = []
24271
+ for line in rest.split("\\r\\n"):
24272
+ key, sep, value = line.partition(":")
24273
+ if sep:
24274
+ received.append((key.strip(), value.strip()))
24275
+ content = dict((k.lower(), v) for k, v in received)
24276
+ if "content-length" in content:
24277
+ data = await reader.readexactly(int(content["content-length"]))
24278
+ else:
24279
+ data = await reader.read()
24280
+ if content.get("x-sbx-error"):
24281
+ raise OSError(data.decode("utf-8", "replace").strip())
24282
+ return int(code), reason, received, data
24283
+ finally:
24284
+ writer.close()
24285
+
24286
+ class _SbxUrllib3Hook:
24287
+ """urllib3's emscripten backend is Pyodide's \`js\` fetch, which this
24288
+ runtime has no module for; its socket backend cannot leave the
24289
+ container. Both connection classes become egress ones."""
24290
+
24291
+ def find_spec(self, name, path=None, target=None):
24292
+ if name != "urllib3.contrib.emscripten":
24293
+ return None
24294
+ import importlib.util
24295
+
24296
+ def inject_into_urllib3():
24297
+ import urllib3.connection
24298
+ from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool
24299
+
24300
+ class _Plain(urllib3.connection.HTTPConnection):
24301
+ """urllib3's own connection, dialled at the egress.
24302
+
24303
+ urllib3.connection.HTTPConnection still derives from
24304
+ http.client.HTTPConnection, so request building is
24305
+ inherited; only where it connects and what goes in the
24306
+ request line change.
24307
+ """
24308
+
24309
+ _sbx_scheme = "http"
24310
+ default_port = 80
24311
+ is_verified = True
24312
+ proxy_is_verified = None
24313
+
24314
+ def __init__(self, host, port=None, **kwargs):
24315
+ # A distinct name from the http.client shim's
24316
+ # _sbx_origin: that class is further up this MRO and
24317
+ # its __init__ runs after this one, so sharing the name
24318
+ # overwrote the real destination with the egress and
24319
+ # every request went out addressed to the egress itself.
24320
+ self._sbx_target = (host, port or self.default_port)
24321
+ for unsupported in ("ssl_context", "ssl_version", "ssl_minimum_version",
24322
+ "ssl_maximum_version", "assert_hostname",
24323
+ "assert_fingerprint", "server_hostname", "ca_certs",
24324
+ "ca_cert_dir", "ca_cert_data", "cert_file", "key_file",
24325
+ "key_password", "cert_reqs"):
24326
+ kwargs.pop(unsupported, None)
24327
+ super().__init__(_egress_host, port=_egress_port, **kwargs)
24328
+
24329
+ def _sbx_absolute(self, url):
24330
+ host, port = self._sbx_target
24331
+ if url.startswith("http://") or url.startswith("https://"):
24332
+ return url
24333
+ netloc = host if port == self.default_port else f"{host}:{port}"
24334
+ return f"{self._sbx_scheme}://{netloc}{url}"
24335
+
24336
+ # urllib3 v2 writes the request line in its own request()
24337
+ # rather than through http.client's putrequest, so both
24338
+ # are absolutised; the egress refuses an origin-form target.
24339
+ def request(self, method, url, *args, **kwargs):
24340
+ return super().request(method, self._sbx_absolute(url), *args, **kwargs)
24341
+
24342
+ def putrequest(self, method, url, *args, **kwargs):
24343
+ return super().putrequest(method, self._sbx_absolute(url), *args, **kwargs)
24344
+
24345
+ def set_tunnel(self, *args, **kwargs):
24346
+ raise OSError("this container reaches the network through its egress, not CONNECT")
24347
+
24348
+ class _Tls(_Plain):
24349
+ _sbx_scheme = "https"
24350
+ default_port = 443
24351
+
24352
+ HTTPConnectionPool.ConnectionCls = _Plain
24353
+ HTTPSConnectionPool.ConnectionCls = _Tls
24354
+ urllib3.connection.HTTPConnection = _Plain
24355
+ urllib3.connection.HTTPSConnection = _Tls
24356
+
24357
+ class _Loader:
24358
+ def create_module(self, spec):
24359
+ return None
24360
+
24361
+ def exec_module(self, module):
24362
+ module.inject_into_urllib3 = inject_into_urllib3
24363
+
24364
+ return importlib.util.spec_from_loader(name, _Loader(), is_package=True)
24365
+
24366
+ sys.meta_path.insert(0, _SbxUrllib3Hook())
24367
+
24368
+ def _sbx_httpx_transports(httpx_module):
24369
+ """Sync and async httpx transports that send through the egress."""
24370
+
24371
+ class _Transport(httpx_module.BaseTransport):
24372
+ def __init__(self, *args, **kwargs):
24373
+ pass
24374
+
24375
+ def handle_request(self, request):
24376
+ body = request.read()
24377
+ sent = [(k.decode("latin-1"), v.decode("latin-1")) for k, v in request.headers.raw]
24378
+ status, reason, headers, payload = _sbx_egress_request(
24379
+ request.method, str(request.url), sent, body,
24380
+ )
24381
+ # Content-Length/Transfer-Encoding describe the egress hop's
24382
+ # framing, and httpx re-derives them from the body it is given.
24383
+ keep = [(k, v) for k, v in headers
24384
+ if k.lower() not in ("content-length", "transfer-encoding", "content-encoding")]
24385
+ return httpx_module.Response(
24386
+ status, headers=keep, content=payload, request=request,
24387
+ extensions={"reason_phrase": (reason or "").encode()},
24388
+ )
24389
+
24390
+ def close(self):
24391
+ pass
24392
+
24393
+ def __enter__(self):
24394
+ return self
24395
+
24396
+ def __exit__(self, *args):
24397
+ pass
24398
+
24399
+ class _AsyncTransport(httpx_module.AsyncBaseTransport):
24400
+ def __init__(self, *args, **kwargs):
24401
+ pass
24402
+
24403
+ async def handle_async_request(self, request):
24404
+ # Not the blocking client on the loop thread: this image
24405
+ # proxies every syscall to one thread, so a blocking recv
24406
+ # inside a coroutine stops the loop that must service it. The
24407
+ # egress is loopback, which asyncio streams reach.
24408
+ import asyncio
24409
+ body = await request.aread()
24410
+ sent = [(k.decode("latin-1"), v.decode("latin-1")) for k, v in request.headers.raw]
24411
+ status, reason, headers, payload = await _sbx_egress_request_async(
24412
+ request.method, str(request.url), sent, body,
24413
+ )
24414
+ keep = [(k, v) for k, v in headers
24415
+ if k.lower() not in ("content-length", "transfer-encoding", "content-encoding")]
24416
+ return httpx_module.Response(
24417
+ status, headers=keep, content=payload, request=request,
24418
+ extensions={"reason_phrase": (reason or "").encode()},
24419
+ )
24420
+
24421
+ async def aclose(self):
24422
+ pass
24423
+
24424
+ async def __aenter__(self):
24425
+ return self
24426
+
24427
+ async def __aexit__(self, *args):
24428
+ pass
24429
+
24430
+ return _Transport, _AsyncTransport
24431
+
24432
+ class _SbxHttpxHook:
24433
+ """httpx and httpx2 build their default transports on httpcore, which
24434
+ opens sockets. Their default transport classes are replaced as the
24435
+ module loads, so \`httpx.get(...)\` and every client built from the
24436
+ defaults leave through the egress."""
24437
+
24438
+ _PACKAGES = {"httpx": "httpx", "httpx2": "httpx2"}
24439
+
24440
+ def find_spec(self, name, path=None, target=None):
24441
+ package = self._PACKAGES.get(name)
24442
+ if package is None or getattr(self, "_loading", None) == name:
24443
+ return None
24444
+ import importlib.machinery
24445
+ spec = importlib.machinery.PathFinder.find_spec(name, path)
24446
+ if spec is None or spec.loader is None:
24447
+ return spec
24448
+ load = spec.loader.exec_module
24449
+
24450
+ def exec_module(module):
24451
+ self._loading = name
24452
+ try:
24453
+ load(module)
24454
+ finally:
24455
+ self._loading = None
24456
+ transport, async_transport = _sbx_httpx_transports(module)
24457
+ module.HTTPTransport = transport
24458
+ module.AsyncHTTPTransport = async_transport
24459
+ defaults = sys.modules.get(f"{package}._transports.default")
24460
+ if defaults is not None:
24461
+ defaults.HTTPTransport = transport
24462
+ defaults.AsyncHTTPTransport = async_transport
24463
+ client = sys.modules.get(f"{package}._client")
24464
+ if client is not None:
24465
+ client.HTTPTransport = transport
24466
+ client.AsyncHTTPTransport = async_transport
24467
+
24468
+ spec.loader.exec_module = exec_module
24469
+ return spec
24470
+
24471
+ sys.meta_path.insert(0, _SbxHttpxHook())
24472
+
24036
24473
 
24037
24474
  if os.environ.get("SBX_SERIAL_HOST_CALLS") == "1":
24038
24475
  import asyncio
@@ -24061,6 +24498,47 @@ if os.environ.get("SBX_SERIAL_HOST_CALLS") == "1":
24061
24498
  )
24062
24499
 
24063
24500
  asyncio.to_thread = _to_thread
24501
+
24502
+ class _SbxAnyioThreadHook:
24503
+ """anyio's thread offload, run inline.
24504
+
24505
+ anyio has its own worker threads, so it never reaches asyncio.to_thread
24506
+ and the refusal above -- it deadlocks instead, for the reason described
24507
+ there. Starlette sends every *synchronous* endpoint through
24508
+ anyio.to_thread.run_sync, so a plain def FastAPI route hung forever
24509
+ while an async def one answered.
24510
+
24511
+ Running the function on the loop thread keeps such an endpoint working.
24512
+ The cost is real and worth stating: a synchronous handler blocks the
24513
+ loop for its duration, so it does not overlap with other requests.
24514
+ """
24515
+
24516
+ def find_spec(self, name, path=None, target=None):
24517
+ if name != "anyio.to_thread":
24518
+ return None
24519
+ import importlib.machinery
24520
+ spec = importlib.machinery.PathFinder.find_spec(name, path)
24521
+ if spec is None or spec.loader is None:
24522
+ return spec
24523
+ load = spec.loader.exec_module
24524
+
24525
+ def exec_module(module):
24526
+ load(module)
24527
+
24528
+ async def run_sync(func, *args, abandon_on_cancel=False,
24529
+ cancellable=None, limiter=None):
24530
+ return func(*args)
24531
+
24532
+ async def run_sync_in_worker_thread(func, *args, **kwargs):
24533
+ return func(*args)
24534
+
24535
+ module.run_sync = run_sync
24536
+ module.run_sync_in_worker_thread = run_sync_in_worker_thread
24537
+
24538
+ spec.loader.exec_module = exec_module
24539
+ return spec
24540
+
24541
+ sys.meta_path.insert(0, _SbxAnyioThreadHook())
24064
24542
  `;
24065
24543
  function ensureProcessShims(ctx) {
24066
24544
  const hook = `${SITE_PACKAGES}/sitecustomize.py`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sandboxedjs",
3
- "version": "0.1.92",
3
+ "version": "0.1.93",
4
4
  "description": "A Linux-like container that runs entirely inside Node.js — POSIX shell, ~140 coreutils, Node.js and Python runtimes, virtual filesystem and networking. No Docker, no VM, no native modules.",
5
5
  "type": "module",
6
6
  "license": "MIT",