sandboxedjs 0.1.93 → 0.1.95
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/bin/sandboxedjs-pack.mjs +363 -0
- package/dist/agent.d.cts +2 -2
- package/dist/agent.d.ts +2 -2
- package/dist/{container-BGmhPbSD.d.cts → container-CFieZcbe.d.ts} +10 -1
- package/dist/{container-DCwYX_Cx.d.ts → container-CPwyHoUy.d.cts} +10 -1
- package/dist/{contracts-BkWBTH8E.d.cts → contracts-B6SHFjma.d.cts} +109 -21
- package/dist/{contracts-BkWBTH8E.d.ts → contracts-B6SHFjma.d.ts} +109 -21
- package/dist/index.cjs +1678 -102
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1678 -102
- package/dist/{memory-volume-DCm-DiXH.d.cts → memory-volume-CRhXUyCN.d.cts} +1 -1
- package/dist/{memory-volume-DJA-vFYW.d.ts → memory-volume-CwiXvRfs.d.ts} +1 -1
- package/dist/python/python.data +461 -303
- package/dist/python/python.js +1 -1
- package/dist/python/python.wasm +0 -0
- package/dist/python/runtime.json +3 -3
- package/dist/python-abi.cjs +56 -10
- package/dist/python-abi.d.cts +4 -3
- package/dist/python-abi.d.ts +4 -3
- package/dist/python-abi.js +56 -10
- package/dist/python-worker.js +1 -0
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -7003,8 +7003,8 @@ var WasiHost = class {
|
|
|
7003
7003
|
view.setUint16(at + 8, WASI_ESUCCESS, true);
|
|
7004
7004
|
view.setUint8(at + 10, subscription.type);
|
|
7005
7005
|
if (subscription.type === EVENTTYPE_FD_READ) {
|
|
7006
|
-
const
|
|
7007
|
-
view.setBigUint64(at + 16, BigInt(
|
|
7006
|
+
const available2 = subscription.fd === 0 ? this.opts.stdin.available : this.bytesLeft(subscription.fd);
|
|
7007
|
+
view.setBigUint64(at + 16, BigInt(available2), true);
|
|
7008
7008
|
} else if (subscription.type === EVENTTYPE_FD_WRITE) {
|
|
7009
7009
|
view.setBigUint64(at + 16, BigInt(64 * 1024), true);
|
|
7010
7010
|
}
|
|
@@ -7241,13 +7241,13 @@ var BinaryExecutionRegistry = class {
|
|
|
7241
7241
|
}
|
|
7242
7242
|
async run(ctx, path) {
|
|
7243
7243
|
const bytes2 = ctx.vfs.readFile(path, ctx.cred);
|
|
7244
|
-
let
|
|
7244
|
+
let info2;
|
|
7245
7245
|
try {
|
|
7246
|
-
|
|
7246
|
+
info2 = inspectElf(bytes2);
|
|
7247
7247
|
} catch (error) {
|
|
7248
7248
|
return ctx.fail(String(error), 126);
|
|
7249
7249
|
}
|
|
7250
|
-
const request = { path, bytes: bytes2, info, sha256: binaryDigest(bytes2) };
|
|
7250
|
+
const request = { path, bytes: bytes2, info: info2, sha256: binaryDigest(bytes2) };
|
|
7251
7251
|
const reasons = [];
|
|
7252
7252
|
const backends = [...this.backends];
|
|
7253
7253
|
for (const tier of ["compatibility", "translation", "emulation"]) {
|
|
@@ -7266,7 +7266,7 @@ var BinaryExecutionRegistry = class {
|
|
|
7266
7266
|
reasons.push(`${backend.id}: ${result.reason}`);
|
|
7267
7267
|
}
|
|
7268
7268
|
}
|
|
7269
|
-
return ctx.fail(`cannot execute ${
|
|
7269
|
+
return ctx.fail(`cannot execute ${info2.architecture} ELF: ${reasons.join("; ") || "no binary backends registered"}`, 126);
|
|
7270
7270
|
}
|
|
7271
7271
|
};
|
|
7272
7272
|
function checkedWasm(artifact) {
|
|
@@ -8530,8 +8530,8 @@ var NetworkStack = class {
|
|
|
8530
8530
|
return this.ifaces.some((i) => i.ipv4 === addr);
|
|
8531
8531
|
}
|
|
8532
8532
|
// ── listening ports ──────────────────────────────────────────────────────
|
|
8533
|
-
registerListener(port,
|
|
8534
|
-
this.listeners.set(port, { port, since: Date.now(), ...
|
|
8533
|
+
registerListener(port, info2) {
|
|
8534
|
+
this.listeners.set(port, { port, since: Date.now(), ...info2 });
|
|
8535
8535
|
}
|
|
8536
8536
|
unregisterListener(port) {
|
|
8537
8537
|
this.listeners.delete(port);
|
|
@@ -12124,21 +12124,21 @@ var uname = defineCommand({
|
|
|
12124
12124
|
{ short: "i", long: "hardware-platform" },
|
|
12125
12125
|
{ short: "o", long: "operating-system" }
|
|
12126
12126
|
]);
|
|
12127
|
-
const
|
|
12127
|
+
const info2 = unameInfo(ctx.kernel.hostname);
|
|
12128
12128
|
if (args.has("all")) {
|
|
12129
|
-
ctx.line(`${
|
|
12129
|
+
ctx.line(`${info2.sysname} ${info2.nodename} ${info2.release} ${info2.version} ${info2.machine} ${info2.operatingSystem}`);
|
|
12130
12130
|
return 0;
|
|
12131
12131
|
}
|
|
12132
12132
|
const parts = [];
|
|
12133
|
-
if (args.has("kernel-name")) parts.push(
|
|
12134
|
-
if (args.has("nodename")) parts.push(
|
|
12135
|
-
if (args.has("kernel-release")) parts.push(
|
|
12136
|
-
if (args.has("kernel-version")) parts.push(
|
|
12137
|
-
if (args.has("machine")) parts.push(
|
|
12138
|
-
if (args.has("processor")) parts.push(
|
|
12139
|
-
if (args.has("hardware-platform")) parts.push(
|
|
12140
|
-
if (args.has("operating-system")) parts.push(
|
|
12141
|
-
ctx.line(parts.length ? parts.join(" ") :
|
|
12133
|
+
if (args.has("kernel-name")) parts.push(info2.sysname);
|
|
12134
|
+
if (args.has("nodename")) parts.push(info2.nodename);
|
|
12135
|
+
if (args.has("kernel-release")) parts.push(info2.release);
|
|
12136
|
+
if (args.has("kernel-version")) parts.push(info2.version);
|
|
12137
|
+
if (args.has("machine")) parts.push(info2.machine);
|
|
12138
|
+
if (args.has("processor")) parts.push(info2.processor);
|
|
12139
|
+
if (args.has("hardware-platform")) parts.push(info2.hardwarePlatform);
|
|
12140
|
+
if (args.has("operating-system")) parts.push(info2.operatingSystem);
|
|
12141
|
+
ctx.line(parts.length ? parts.join(" ") : info2.sysname);
|
|
12142
12142
|
return 0;
|
|
12143
12143
|
}
|
|
12144
12144
|
});
|
|
@@ -15122,15 +15122,15 @@ var Interpreter = class {
|
|
|
15122
15122
|
return this.exitCode;
|
|
15123
15123
|
}
|
|
15124
15124
|
};
|
|
15125
|
-
function splitRecord(
|
|
15126
|
-
if (
|
|
15125
|
+
function splitRecord(record2, fs) {
|
|
15126
|
+
if (record2 === "") return [];
|
|
15127
15127
|
if (fs === " ") {
|
|
15128
|
-
const trimmed =
|
|
15128
|
+
const trimmed = record2.replace(/^[ \t\n]+|[ \t\n]+$/g, "");
|
|
15129
15129
|
return trimmed === "" ? [] : trimmed.split(/[ \t\n]+/);
|
|
15130
15130
|
}
|
|
15131
|
-
if (fs === "") return [...
|
|
15132
|
-
if (fs.length === 1 && !"\\^$.[]|()*+?{}".includes(fs)) return
|
|
15133
|
-
return
|
|
15131
|
+
if (fs === "") return [...record2];
|
|
15132
|
+
if (fs.length === 1 && !"\\^$.[]|()*+?{}".includes(fs)) return record2.split(fs);
|
|
15133
|
+
return record2.split(new RegExp(translateEre(fs)));
|
|
15134
15134
|
}
|
|
15135
15135
|
var ERE_CLASSES = {
|
|
15136
15136
|
alpha: "A-Za-z",
|
|
@@ -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: "
|
|
20104
|
+
sha256: "d8b06cc04770a048b41956c9cc767e7332c49f0229e69744f4b1c2892b888b27",
|
|
20082
20105
|
abiId: "sbxabi1-c2637d04695ad927",
|
|
20083
20106
|
requires: [],
|
|
20084
20107
|
provenance: {
|
|
20085
20108
|
sourceTreeDigest: "7b563fa2e7f20edac204cc80004cae35c844b36a83758e5957184383332d1209",
|
|
20086
|
-
recipeSha256: "
|
|
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.
|
|
20961
|
+
version: "0.1.95"};
|
|
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
|
-
|
|
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
|
-
|
|
21818
|
-
|
|
21819
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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) !==
|
|
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;
|
|
@@ -23377,7 +23686,7 @@ async function resolve2(options) {
|
|
|
23377
23686
|
const clauses = (state.constraints.get(name) ?? []).flatMap((entry) => entry.spec.map((s) => `${s.op}${s.version}`)).sort();
|
|
23378
23687
|
return `${name}|${[...new Set(clauses)].join(",")}`;
|
|
23379
23688
|
};
|
|
23380
|
-
const
|
|
23689
|
+
const search2 = async (queue, state) => {
|
|
23381
23690
|
checkBudget();
|
|
23382
23691
|
if (queue.length === 0) return state;
|
|
23383
23692
|
const [edge, ...rest] = queue;
|
|
@@ -23385,7 +23694,7 @@ async function resolve2(options) {
|
|
|
23385
23694
|
const name = requirement.name;
|
|
23386
23695
|
if (!markerApplies(requirement.marker, environment, extras)) {
|
|
23387
23696
|
if (requirement.marker) skipped.push({ name, marker: requirement.marker });
|
|
23388
|
-
return
|
|
23697
|
+
return search2(rest, state);
|
|
23389
23698
|
}
|
|
23390
23699
|
const previous = state.constraints.get(name) ?? [];
|
|
23391
23700
|
const constraints = new Map(state.constraints);
|
|
@@ -23402,7 +23711,7 @@ async function resolve2(options) {
|
|
|
23402
23711
|
explain(name, next.constraints, already.version)
|
|
23403
23712
|
);
|
|
23404
23713
|
}
|
|
23405
|
-
return
|
|
23714
|
+
return search2(rest, next);
|
|
23406
23715
|
}
|
|
23407
23716
|
const signature = signatureOf(name, next);
|
|
23408
23717
|
const learned = nogoods.get(signature);
|
|
@@ -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
|
|
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,
|
|
23431
|
-
throw
|
|
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 (
|
|
23752
|
+
} catch (error) {
|
|
23444
23753
|
failure2 = new ResolutionError(
|
|
23445
|
-
`could not read the dependencies of ${name} ${candidate.version}: ${
|
|
23754
|
+
`could not read the dependencies of ${name} ${candidate.version}: ${error.message}`
|
|
23446
23755
|
);
|
|
23447
23756
|
continue;
|
|
23448
23757
|
}
|
|
@@ -23465,16 +23774,14 @@ async function resolve2(options) {
|
|
|
23465
23774
|
const chosen = new Map(next.chosen);
|
|
23466
23775
|
chosen.set(name, { ...candidate, requires });
|
|
23467
23776
|
try {
|
|
23468
|
-
return await
|
|
23469
|
-
} catch (
|
|
23470
|
-
if (!(
|
|
23471
|
-
if (/cancelled|budget/.test(
|
|
23472
|
-
failure2 =
|
|
23777
|
+
return await search2([...edges, ...rest], { chosen, constraints: next.constraints });
|
|
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
|
-
|
|
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) {
|
|
@@ -23482,12 +23789,12 @@ async function resolve2(options) {
|
|
|
23482
23789
|
if (specifiers.length === 0 || !markerApplies(marker, environment, root.extras)) continue;
|
|
23483
23790
|
seeded.set(name, [...seeded.get(name) ?? [], { spec: specifiers, origin: root.origin }]);
|
|
23484
23791
|
}
|
|
23485
|
-
const solved = await
|
|
23792
|
+
const solved = await search2(roots, { chosen: /* @__PURE__ */ new Map(), constraints: seeded });
|
|
23486
23793
|
const distributions = [...solved.chosen.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
23487
23794
|
const reported = new Map(skipped.map((s) => [`${s.name}\0${s.marker}`, s]));
|
|
23488
23795
|
return { distributions, skipped: [...reported.values()] };
|
|
23489
23796
|
}
|
|
23490
|
-
function explain(name, constraints, chosenVersion,
|
|
23797
|
+
function explain(name, constraints, chosenVersion, available2 = []) {
|
|
23491
23798
|
const lines = [];
|
|
23492
23799
|
for (const entry of constraints.get(name) ?? []) {
|
|
23493
23800
|
if (entry.spec.length === 0) continue;
|
|
@@ -23495,9 +23802,9 @@ function explain(name, constraints, chosenVersion, available = []) {
|
|
|
23495
23802
|
lines.push(`${entry.origin} requires ${name} ${text2}`);
|
|
23496
23803
|
}
|
|
23497
23804
|
if (chosenVersion) lines.push(`but ${name} ${chosenVersion} is already selected`);
|
|
23498
|
-
if (
|
|
23499
|
-
const versions = [...new Set(
|
|
23500
|
-
lines.push(`usable versions: ${versions.join(", ")}${
|
|
23805
|
+
if (available2.length > 0) {
|
|
23806
|
+
const versions = [...new Set(available2.map((c) => c.version))].slice(0, 8);
|
|
23807
|
+
lines.push(`usable versions: ${versions.join(", ")}${available2.length > 8 ? ", ..." : ""}`);
|
|
23501
23808
|
}
|
|
23502
23809
|
return lines;
|
|
23503
23810
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
24562
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25797
|
+
translate2(e);
|
|
25158
25798
|
}
|
|
25159
25799
|
return length;
|
|
25160
25800
|
},
|
|
@@ -25761,9 +26401,9 @@ function parsePax(data) {
|
|
|
25761
26401
|
if (space < 0) break;
|
|
25762
26402
|
const length = Number(value.slice(offset, space));
|
|
25763
26403
|
if (!Number.isFinite(length) || length <= 0) break;
|
|
25764
|
-
const
|
|
25765
|
-
const equal2 =
|
|
25766
|
-
if (equal2 > 0) result[
|
|
26404
|
+
const record2 = value.slice(space + 1, offset + length - 1);
|
|
26405
|
+
const equal2 = record2.indexOf("=");
|
|
26406
|
+
if (equal2 > 0) result[record2.slice(0, equal2)] = record2.slice(equal2 + 1);
|
|
25767
26407
|
offset += length;
|
|
25768
26408
|
}
|
|
25769
26409
|
return result;
|
|
@@ -26543,27 +27183,27 @@ var apt = defineCommand({
|
|
|
26543
27183
|
switch (subcommand) {
|
|
26544
27184
|
case "list": {
|
|
26545
27185
|
const installedOnly = rest.includes("--installed");
|
|
26546
|
-
for (const [name,
|
|
26547
|
-
const
|
|
26548
|
-
if (installedOnly && !
|
|
26549
|
-
ctx.line(`${name}/stable ${
|
|
27186
|
+
for (const [name, info2] of Object.entries(provided)) {
|
|
27187
|
+
const available2 = ctx.kernel.which(name === "python3-pip" ? "pip" : name === "gawk" ? "awk" : name, ctx.cwd, ctx.env, ctx.cred) !== null;
|
|
27188
|
+
if (installedOnly && !available2) continue;
|
|
27189
|
+
ctx.line(`${name}/stable ${info2.version} amd64${available2 ? " [installed]" : ""}`);
|
|
26550
27190
|
}
|
|
26551
27191
|
return 0;
|
|
26552
27192
|
}
|
|
26553
27193
|
case "show": {
|
|
26554
27194
|
let status = 0;
|
|
26555
27195
|
for (const name of rest) {
|
|
26556
|
-
const
|
|
26557
|
-
if (!
|
|
27196
|
+
const info2 = provided[name];
|
|
27197
|
+
if (!info2) {
|
|
26558
27198
|
ctx.warn(`E: No packages found matching ${name}`);
|
|
26559
27199
|
status = 1;
|
|
26560
27200
|
continue;
|
|
26561
27201
|
}
|
|
26562
27202
|
ctx.line(`Package: ${name}`);
|
|
26563
|
-
ctx.line(`Version: ${
|
|
27203
|
+
ctx.line(`Version: ${info2.version}`);
|
|
26564
27204
|
ctx.line(`Priority: optional`);
|
|
26565
27205
|
ctx.line(`Architecture: amd64`);
|
|
26566
|
-
ctx.line(`Description: ${
|
|
27206
|
+
ctx.line(`Description: ${info2.description}`);
|
|
26567
27207
|
ctx.line("");
|
|
26568
27208
|
}
|
|
26569
27209
|
return status;
|
|
@@ -26594,10 +27234,10 @@ var apt = defineCommand({
|
|
|
26594
27234
|
return 100;
|
|
26595
27235
|
case "search": {
|
|
26596
27236
|
const pattern = new RegExp(rest[0] ?? "", "i");
|
|
26597
|
-
for (const [name,
|
|
26598
|
-
if (pattern.test(name) || pattern.test(
|
|
26599
|
-
ctx.line(`${name}/stable ${
|
|
26600
|
-
ctx.line(` ${
|
|
27237
|
+
for (const [name, info2] of Object.entries(provided)) {
|
|
27238
|
+
if (pattern.test(name) || pattern.test(info2.description)) {
|
|
27239
|
+
ctx.line(`${name}/stable ${info2.version} amd64`);
|
|
27240
|
+
ctx.line(` ${info2.description}`);
|
|
26601
27241
|
ctx.line("");
|
|
26602
27242
|
}
|
|
26603
27243
|
}
|
|
@@ -27062,6 +27702,701 @@ function browserCommands() {
|
|
|
27062
27702
|
];
|
|
27063
27703
|
}
|
|
27064
27704
|
|
|
27705
|
+
// src/apps/pm.ts
|
|
27706
|
+
init_binary();
|
|
27707
|
+
init_zip();
|
|
27708
|
+
|
|
27709
|
+
// src/apps/registry.ts
|
|
27710
|
+
var APPS_ROOT = "/opt";
|
|
27711
|
+
var APP_BIN_DIR = "/usr/local/bin";
|
|
27712
|
+
var APP_STATE = "/var/lib/pm/installed.json";
|
|
27713
|
+
var DEFAULT_REGISTRY = "https://sandboxedjswheels.pages.dev/apps";
|
|
27714
|
+
var BUILT_IN_PUBLISHERS = [
|
|
27715
|
+
{
|
|
27716
|
+
publisher: "SandboxedJs",
|
|
27717
|
+
publicKey: "MCowBQYDK2VwAyEAuyxKMeyeO/05mr5Qu8vc+Dd9yMG2/oKDwt7KA+C9Mj4="
|
|
27718
|
+
}
|
|
27719
|
+
];
|
|
27720
|
+
var AppRegistryError = class extends Error {
|
|
27721
|
+
};
|
|
27722
|
+
function parseIndex(text2, source) {
|
|
27723
|
+
let parsed;
|
|
27724
|
+
try {
|
|
27725
|
+
parsed = JSON.parse(text2);
|
|
27726
|
+
} catch (error) {
|
|
27727
|
+
throw new AppRegistryError(`${source} is not JSON: ${error.message}`);
|
|
27728
|
+
}
|
|
27729
|
+
const index = parsed;
|
|
27730
|
+
if (index?.format !== "sandboxedjs-app-index") {
|
|
27731
|
+
throw new AppRegistryError(
|
|
27732
|
+
`${source} is not an application index (its format is ${JSON.stringify(index?.format ?? null)})`
|
|
27733
|
+
);
|
|
27734
|
+
}
|
|
27735
|
+
if (!Array.isArray(index.apps)) {
|
|
27736
|
+
throw new AppRegistryError(`${source} has no apps`);
|
|
27737
|
+
}
|
|
27738
|
+
for (const app of index.apps) {
|
|
27739
|
+
for (const field of ["name", "version", "filename", "sha256"]) {
|
|
27740
|
+
if (typeof app[field] !== "string" || !app[field]) {
|
|
27741
|
+
throw new AppRegistryError(`${source}: an entry is missing ${field}`);
|
|
27742
|
+
}
|
|
27743
|
+
}
|
|
27744
|
+
}
|
|
27745
|
+
return index;
|
|
27746
|
+
}
|
|
27747
|
+
function selectRelease(index, name, version) {
|
|
27748
|
+
const matches3 = index.apps.filter((app) => app.name === name);
|
|
27749
|
+
if (matches3.length === 0) {
|
|
27750
|
+
const names = index.apps.map((app) => app.name).sort();
|
|
27751
|
+
throw new AppRegistryError(
|
|
27752
|
+
`no application named ${name}. The registry has: ${names.join(", ") || "nothing"}`
|
|
27753
|
+
);
|
|
27754
|
+
}
|
|
27755
|
+
if (version) {
|
|
27756
|
+
const exact = matches3.find((app) => app.version === version);
|
|
27757
|
+
if (!exact) {
|
|
27758
|
+
const versions = matches3.map((app) => app.version).join(", ");
|
|
27759
|
+
throw new AppRegistryError(`${name} has no version ${version}; it has ${versions}`);
|
|
27760
|
+
}
|
|
27761
|
+
return exact;
|
|
27762
|
+
}
|
|
27763
|
+
return matches3.slice().sort((a, b) => compareVersions2(b.version, a.version))[0];
|
|
27764
|
+
}
|
|
27765
|
+
function compareVersions2(a, b) {
|
|
27766
|
+
const left = a.split(/[.-]/);
|
|
27767
|
+
const right = b.split(/[.-]/);
|
|
27768
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
27769
|
+
const x = left[index] ?? "";
|
|
27770
|
+
const y = right[index] ?? "";
|
|
27771
|
+
const nx = Number(x);
|
|
27772
|
+
const ny = Number(y);
|
|
27773
|
+
if (Number.isFinite(nx) && Number.isFinite(ny) && x !== "" && y !== "") {
|
|
27774
|
+
if (nx !== ny) return nx - ny;
|
|
27775
|
+
continue;
|
|
27776
|
+
}
|
|
27777
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
27778
|
+
}
|
|
27779
|
+
return 0;
|
|
27780
|
+
}
|
|
27781
|
+
function parseSpecifier(specifier) {
|
|
27782
|
+
const at = specifier.lastIndexOf("@");
|
|
27783
|
+
if (at <= 0) return { name: specifier };
|
|
27784
|
+
return { name: specifier.slice(0, at), version: specifier.slice(at + 1) };
|
|
27785
|
+
}
|
|
27786
|
+
function validateManifest2(manifest, source) {
|
|
27787
|
+
const app = manifest;
|
|
27788
|
+
if (typeof app?.name !== "string" || typeof app?.version !== "string") {
|
|
27789
|
+
throw new AppRegistryError(`${source} is not an application manifest`);
|
|
27790
|
+
}
|
|
27791
|
+
for (const binary of app.bins ?? []) {
|
|
27792
|
+
if (typeof binary?.name !== "string" || typeof binary?.path !== "string") {
|
|
27793
|
+
throw new AppRegistryError(`${source}: a bins entry needs a name and a path`);
|
|
27794
|
+
}
|
|
27795
|
+
if (binary.name.includes("/")) {
|
|
27796
|
+
throw new AppRegistryError(`${source}: a command name cannot contain a slash`);
|
|
27797
|
+
}
|
|
27798
|
+
}
|
|
27799
|
+
for (const service of app.services ?? []) {
|
|
27800
|
+
if (typeof service?.port !== "number" || !Number.isInteger(service.port)) {
|
|
27801
|
+
throw new AppRegistryError(`${source}: a service needs an integer port`);
|
|
27802
|
+
}
|
|
27803
|
+
}
|
|
27804
|
+
return app;
|
|
27805
|
+
}
|
|
27806
|
+
|
|
27807
|
+
// src/apps/sbjs.ts
|
|
27808
|
+
init_binary();
|
|
27809
|
+
init_zip();
|
|
27810
|
+
var MANIFEST_PATH = "META/manifest.json";
|
|
27811
|
+
var SIGNATURE_PATH = "META/signature.json";
|
|
27812
|
+
var TRUSTED_KEYS_PATH = "/etc/pm/trusted-keys.json";
|
|
27813
|
+
var PackageError = class extends Error {
|
|
27814
|
+
};
|
|
27815
|
+
async function openPackage(bytes2, trusted) {
|
|
27816
|
+
const entries = readZip(bytes2);
|
|
27817
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
27818
|
+
const manifestEntry = byName.get(MANIFEST_PATH);
|
|
27819
|
+
if (!manifestEntry) {
|
|
27820
|
+
throw new PackageError(
|
|
27821
|
+
`not a SandboxedJs package: it has no ${MANIFEST_PATH}. A plain zip can be installed with --allow-unsigned, but it carries no publisher.`
|
|
27822
|
+
);
|
|
27823
|
+
}
|
|
27824
|
+
const manifestBytes = manifestEntry.data();
|
|
27825
|
+
const manifest = parseManifest(manifestBytes);
|
|
27826
|
+
validateManifest2(manifest.app, `${MANIFEST_PATH}'s app`);
|
|
27827
|
+
for (const entry of entries) {
|
|
27828
|
+
if (entry.isDirectory || entry.name.startsWith("META/")) continue;
|
|
27829
|
+
const expected = manifest.files[entry.name];
|
|
27830
|
+
if (!expected) {
|
|
27831
|
+
throw new PackageError(`${entry.name} is in the package but not in its manifest`);
|
|
27832
|
+
}
|
|
27833
|
+
const actual = await digestHex("sha256", entry.data());
|
|
27834
|
+
if (actual !== expected) {
|
|
27835
|
+
throw new PackageError(
|
|
27836
|
+
`${entry.name} does not match the manifest (expected ${expected}, found ${actual})`
|
|
27837
|
+
);
|
|
27838
|
+
}
|
|
27839
|
+
}
|
|
27840
|
+
for (const name of Object.keys(manifest.files)) {
|
|
27841
|
+
if (!byName.has(name)) {
|
|
27842
|
+
throw new PackageError(`${name} is in the manifest but missing from the package`);
|
|
27843
|
+
}
|
|
27844
|
+
}
|
|
27845
|
+
const signatureEntry = byName.get(SIGNATURE_PATH);
|
|
27846
|
+
if (!signatureEntry) {
|
|
27847
|
+
return { manifest, signature: null, entries, trustedBy: null };
|
|
27848
|
+
}
|
|
27849
|
+
const signature = parseSignature(signatureEntry.data());
|
|
27850
|
+
const valid = await verifySignature(manifestBytes, signature);
|
|
27851
|
+
if (!valid) {
|
|
27852
|
+
throw new PackageError(
|
|
27853
|
+
`the signature on ${manifest.app.name} does not match its manifest; the package has been altered since it was signed`
|
|
27854
|
+
);
|
|
27855
|
+
}
|
|
27856
|
+
const trustedBy = trusted.find((key) => key.publicKey === signature.publicKey) ?? null;
|
|
27857
|
+
return { manifest, signature, entries, trustedBy };
|
|
27858
|
+
}
|
|
27859
|
+
function parseManifest(bytes2) {
|
|
27860
|
+
let parsed;
|
|
27861
|
+
try {
|
|
27862
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes2));
|
|
27863
|
+
} catch (error) {
|
|
27864
|
+
throw new PackageError(`${MANIFEST_PATH} is not JSON: ${error.message}`);
|
|
27865
|
+
}
|
|
27866
|
+
const manifest = parsed;
|
|
27867
|
+
if (manifest?.format !== "sandboxedjs-package") {
|
|
27868
|
+
throw new PackageError(`${MANIFEST_PATH} is not a package manifest`);
|
|
27869
|
+
}
|
|
27870
|
+
if (!manifest.files || typeof manifest.files !== "object") {
|
|
27871
|
+
throw new PackageError(`${MANIFEST_PATH} lists no files`);
|
|
27872
|
+
}
|
|
27873
|
+
if (!manifest.app) {
|
|
27874
|
+
throw new PackageError(`${MANIFEST_PATH} carries no application manifest`);
|
|
27875
|
+
}
|
|
27876
|
+
return manifest;
|
|
27877
|
+
}
|
|
27878
|
+
function parseSignature(bytes2) {
|
|
27879
|
+
let parsed;
|
|
27880
|
+
try {
|
|
27881
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes2));
|
|
27882
|
+
} catch (error) {
|
|
27883
|
+
throw new PackageError(`${SIGNATURE_PATH} is not JSON: ${error.message}`);
|
|
27884
|
+
}
|
|
27885
|
+
const signature = parsed;
|
|
27886
|
+
if (signature?.format !== "sandboxedjs-signature") {
|
|
27887
|
+
throw new PackageError(`${SIGNATURE_PATH} is not a signature`);
|
|
27888
|
+
}
|
|
27889
|
+
if (signature.algorithm !== "ed25519") {
|
|
27890
|
+
throw new PackageError(
|
|
27891
|
+
`${signature.algorithm} signatures are not supported by this runtime; only ed25519 is`
|
|
27892
|
+
);
|
|
27893
|
+
}
|
|
27894
|
+
for (const field of ["publicKey", "signature"]) {
|
|
27895
|
+
if (typeof signature[field] !== "string" || !signature[field]) {
|
|
27896
|
+
throw new PackageError(`${SIGNATURE_PATH} has no ${field}`);
|
|
27897
|
+
}
|
|
27898
|
+
}
|
|
27899
|
+
return signature;
|
|
27900
|
+
}
|
|
27901
|
+
async function verifySignature(manifestBytes, signature) {
|
|
27902
|
+
const subtle = globalThis.crypto?.subtle;
|
|
27903
|
+
if (!subtle) {
|
|
27904
|
+
throw new PackageError(
|
|
27905
|
+
"this runtime has no WebCrypto, so a package signature cannot be checked"
|
|
27906
|
+
);
|
|
27907
|
+
}
|
|
27908
|
+
let key;
|
|
27909
|
+
try {
|
|
27910
|
+
key = await subtle.importKey(
|
|
27911
|
+
"spki",
|
|
27912
|
+
intoBuffer(base64ToBytes(signature.publicKey)),
|
|
27913
|
+
{ name: "Ed25519" },
|
|
27914
|
+
false,
|
|
27915
|
+
["verify"]
|
|
27916
|
+
);
|
|
27917
|
+
} catch (error) {
|
|
27918
|
+
throw new PackageError(
|
|
27919
|
+
`cannot use the package's public key: ${error.message}. Ed25519 needs Node 20 or a browser that implements it.`
|
|
27920
|
+
);
|
|
27921
|
+
}
|
|
27922
|
+
return subtle.verify(
|
|
27923
|
+
{ name: "Ed25519" },
|
|
27924
|
+
key,
|
|
27925
|
+
intoBuffer(base64ToBytes(signature.signature)),
|
|
27926
|
+
intoBuffer(manifestBytes)
|
|
27927
|
+
);
|
|
27928
|
+
}
|
|
27929
|
+
function intoBuffer(bytes2) {
|
|
27930
|
+
return bytes2.slice().buffer;
|
|
27931
|
+
}
|
|
27932
|
+
function base64ToBytes(text2) {
|
|
27933
|
+
const binary = atob(text2);
|
|
27934
|
+
const bytes2 = new Uint8Array(binary.length);
|
|
27935
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
27936
|
+
bytes2[index] = binary.charCodeAt(index);
|
|
27937
|
+
}
|
|
27938
|
+
return bytes2;
|
|
27939
|
+
}
|
|
27940
|
+
async function fingerprint(publicKey) {
|
|
27941
|
+
const digest2 = await digestHex("sha256", base64ToBytes(publicKey));
|
|
27942
|
+
return digest2.slice(0, 32).replace(/(.{4})(?=.)/g, "$1-");
|
|
27943
|
+
}
|
|
27944
|
+
function asRegistryError(error) {
|
|
27945
|
+
return new AppRegistryError(error.message);
|
|
27946
|
+
}
|
|
27947
|
+
|
|
27948
|
+
// src/apps/pm.ts
|
|
27949
|
+
function registryBase(ctx) {
|
|
27950
|
+
const configured = ctx.env.SBX_PM_REGISTRY ?? ctx.env.PM_REGISTRY;
|
|
27951
|
+
return configured && configured.trim() || DEFAULT_REGISTRY;
|
|
27952
|
+
}
|
|
27953
|
+
var pm = defineCommand({
|
|
27954
|
+
name: "pm",
|
|
27955
|
+
path: "/usr/bin/pm",
|
|
27956
|
+
summary: "install and manage applications",
|
|
27957
|
+
usage: "pm <install|remove|list|info|search|available> [name[@version]]",
|
|
27958
|
+
manual: `Applications are programs the container runs, packaged with what
|
|
27959
|
+
they need. They are not Python or Node packages: use pip or npm for those.
|
|
27960
|
+
|
|
27961
|
+
pm install NAME[@VERSION] download, verify and install
|
|
27962
|
+
pm remove NAME remove it and the commands it installed
|
|
27963
|
+
pm list what is installed here
|
|
27964
|
+
pm info NAME what a package provides
|
|
27965
|
+
pm search TEXT search the registry
|
|
27966
|
+
pm available everything the registry offers
|
|
27967
|
+
pm trust list|add|remove publisher keys this container trusts
|
|
27968
|
+
|
|
27969
|
+
Packages come from ${DEFAULT_REGISTRY} unless SBX_PM_REGISTRY names another.
|
|
27970
|
+
|
|
27971
|
+
A package is a .sbjs file: a zip carrying META/manifest.json, which lists a
|
|
27972
|
+
sha256 for every file in it, and META/signature.json, an Ed25519 signature
|
|
27973
|
+
over that manifest. Installing checks both, and refuses a package signed by a
|
|
27974
|
+
key this container does not trust. --allow-untrusted and --allow-unsigned
|
|
27975
|
+
override that, one install at a time.
|
|
27976
|
+
|
|
27977
|
+
A signature says who published a package and that nothing in it has changed
|
|
27978
|
+
since. It does not say the program is safe to run.`,
|
|
27979
|
+
async run(ctx) {
|
|
27980
|
+
const [subcommand, ...rest] = ctx.args;
|
|
27981
|
+
try {
|
|
27982
|
+
switch (subcommand) {
|
|
27983
|
+
case "install":
|
|
27984
|
+
return await install2(ctx, rest);
|
|
27985
|
+
case "remove":
|
|
27986
|
+
case "uninstall":
|
|
27987
|
+
return await remove(ctx, rest);
|
|
27988
|
+
case "list":
|
|
27989
|
+
return listInstalled2(ctx);
|
|
27990
|
+
case "info":
|
|
27991
|
+
return await info(ctx, rest);
|
|
27992
|
+
case "search":
|
|
27993
|
+
return await search(ctx, rest);
|
|
27994
|
+
case "available":
|
|
27995
|
+
return await available(ctx);
|
|
27996
|
+
case "trust":
|
|
27997
|
+
return await trust(ctx, rest);
|
|
27998
|
+
case void 0:
|
|
27999
|
+
case "--help":
|
|
28000
|
+
case "-h":
|
|
28001
|
+
ctx.line(pm.usage);
|
|
28002
|
+
return 0;
|
|
28003
|
+
default:
|
|
28004
|
+
ctx.warn(`pm: unknown command ${subcommand}`);
|
|
28005
|
+
return 2;
|
|
28006
|
+
}
|
|
28007
|
+
} catch (error) {
|
|
28008
|
+
ctx.warn(`pm: ${error.message}`);
|
|
28009
|
+
return error instanceof AppRegistryError ? 2 : 1;
|
|
28010
|
+
}
|
|
28011
|
+
}
|
|
28012
|
+
});
|
|
28013
|
+
async function install2(ctx, args) {
|
|
28014
|
+
if (args.length === 0) {
|
|
28015
|
+
ctx.warn("pm install: name it, as `pm install officesuite`");
|
|
28016
|
+
return 2;
|
|
28017
|
+
}
|
|
28018
|
+
const options = {
|
|
28019
|
+
allowUnsigned: args.includes("--allow-unsigned"),
|
|
28020
|
+
allowUntrusted: args.includes("--allow-untrusted") || args.includes("--allow-unsigned")
|
|
28021
|
+
};
|
|
28022
|
+
const names = args.filter((argument) => !argument.startsWith("--"));
|
|
28023
|
+
if (names.length === 0) {
|
|
28024
|
+
ctx.warn("pm install: name it, as `pm install officesuite`");
|
|
28025
|
+
return 2;
|
|
28026
|
+
}
|
|
28027
|
+
const index = await fetchIndex(ctx);
|
|
28028
|
+
for (const specifier of names) {
|
|
28029
|
+
const { name, version } = parseSpecifier(specifier);
|
|
28030
|
+
const release = selectRelease(index, name, version);
|
|
28031
|
+
await installRelease(ctx, index, release, /* @__PURE__ */ new Set(), options);
|
|
28032
|
+
}
|
|
28033
|
+
return 0;
|
|
28034
|
+
}
|
|
28035
|
+
async function installRelease(ctx, index, release, seen, options) {
|
|
28036
|
+
if (seen.has(release.name)) return;
|
|
28037
|
+
seen.add(release.name);
|
|
28038
|
+
for (const required of release.requires ?? []) {
|
|
28039
|
+
const { name, version } = parseSpecifier(required);
|
|
28040
|
+
if (installedApps(ctx).some((app) => app.name === name)) continue;
|
|
28041
|
+
await installRelease(ctx, index, selectRelease(index, name, version), seen, options);
|
|
28042
|
+
}
|
|
28043
|
+
const base2 = registryBase(ctx).replace(/\/$/, "");
|
|
28044
|
+
const url = `${base2}/${release.filename}`;
|
|
28045
|
+
ctx.line(`Fetching ${release.name} ${release.version}`);
|
|
28046
|
+
const archive = await download(url);
|
|
28047
|
+
const digest2 = await digestHex("sha256", archive);
|
|
28048
|
+
if (digest2 !== release.sha256) {
|
|
28049
|
+
throw new AppRegistryError(
|
|
28050
|
+
`${release.filename} failed verification: the registry publishes sha256 ${release.sha256} but the download hashes to ${digest2}`
|
|
28051
|
+
);
|
|
28052
|
+
}
|
|
28053
|
+
const verified = await verify(ctx, archive, release, options);
|
|
28054
|
+
const root = `${APPS_ROOT}/${release.name}`;
|
|
28055
|
+
removeInstalled(ctx, release.name, { quiet: true });
|
|
28056
|
+
const manifest = unpack(ctx, archive, root, release, verified);
|
|
28057
|
+
const bins = linkBinaries(ctx, manifest, root);
|
|
28058
|
+
record(ctx, {
|
|
28059
|
+
...manifest,
|
|
28060
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28061
|
+
root,
|
|
28062
|
+
installedBins: bins,
|
|
28063
|
+
sha256: release.sha256,
|
|
28064
|
+
...verified?.signature ? {
|
|
28065
|
+
publisher: verified.signature.publisher ?? verified.trustedBy?.publisher ?? "unknown",
|
|
28066
|
+
publicKey: verified.signature.publicKey
|
|
28067
|
+
} : {}
|
|
28068
|
+
});
|
|
28069
|
+
ctx.line(`Installed ${manifest.name} ${manifest.version} in ${root}`);
|
|
28070
|
+
if (bins.length > 0) ctx.line(` commands: ${bins.join(", ")}`);
|
|
28071
|
+
for (const service of manifest.services ?? []) {
|
|
28072
|
+
ctx.line(
|
|
28073
|
+
` service: ${service.name} on port ${service.port}` + (service.path ? ` at ${service.path}` : "") + ` (start it with \`${service.command}\`)`
|
|
28074
|
+
);
|
|
28075
|
+
}
|
|
28076
|
+
if (manifest.docs) ctx.line(` docs: ${root}/${manifest.docs}`);
|
|
28077
|
+
}
|
|
28078
|
+
async function download(url) {
|
|
28079
|
+
const response = await fetch(url);
|
|
28080
|
+
if (!response.ok) {
|
|
28081
|
+
throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
|
|
28082
|
+
}
|
|
28083
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
28084
|
+
}
|
|
28085
|
+
async function verify(ctx, archive, release, options) {
|
|
28086
|
+
let opened;
|
|
28087
|
+
try {
|
|
28088
|
+
opened = await openPackage(archive, trustedKeys(ctx));
|
|
28089
|
+
} catch (error) {
|
|
28090
|
+
if (options.allowUnsigned && /has no META\/manifest\.json/.test(error.message)) {
|
|
28091
|
+
ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
|
|
28092
|
+
return null;
|
|
28093
|
+
}
|
|
28094
|
+
throw asRegistryError(error);
|
|
28095
|
+
}
|
|
28096
|
+
if (!opened.signature) {
|
|
28097
|
+
if (!options.allowUnsigned) {
|
|
28098
|
+
throw new AppRegistryError(
|
|
28099
|
+
`${release.name} carries no signature, so its publisher cannot be established. Install it with --allow-unsigned if you got it from somewhere you trust.`
|
|
28100
|
+
);
|
|
28101
|
+
}
|
|
28102
|
+
ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
|
|
28103
|
+
return opened;
|
|
28104
|
+
}
|
|
28105
|
+
const print = await fingerprint(opened.signature.publicKey);
|
|
28106
|
+
if (!opened.trustedBy && !options.allowUntrusted) {
|
|
28107
|
+
throw new AppRegistryError(
|
|
28108
|
+
`${release.name} is signed by a key this container does not trust (${opened.signature.publisher ?? "unnamed"}, ${print}). Add it with \`pm trust add ${print}\` after checking it, or install with --allow-untrusted.`
|
|
28109
|
+
);
|
|
28110
|
+
}
|
|
28111
|
+
ctx.line(
|
|
28112
|
+
opened.trustedBy ? ` signed by ${opened.trustedBy.publisher} (${print})` : ` signed by an untrusted key (${opened.signature.publisher ?? "unnamed"}, ${print})`
|
|
28113
|
+
);
|
|
28114
|
+
return opened;
|
|
28115
|
+
}
|
|
28116
|
+
function trustedKeys(ctx) {
|
|
28117
|
+
let stored = [];
|
|
28118
|
+
try {
|
|
28119
|
+
const parsed = JSON.parse(ctx.vfs.readText(TRUSTED_KEYS_PATH, ctx.cred));
|
|
28120
|
+
stored = Array.isArray(parsed.keys) ? parsed.keys : [];
|
|
28121
|
+
} catch {
|
|
28122
|
+
}
|
|
28123
|
+
const known = new Set(stored.map((key) => key.publicKey));
|
|
28124
|
+
return [...stored, ...BUILT_IN_PUBLISHERS.filter((key) => !known.has(key.publicKey))];
|
|
28125
|
+
}
|
|
28126
|
+
function writeTrustedKeys(ctx, keys) {
|
|
28127
|
+
ctx.vfs.mkdir("/etc/pm", { cred: ctx.cred, recursive: true });
|
|
28128
|
+
ctx.vfs.writeFile(
|
|
28129
|
+
TRUSTED_KEYS_PATH,
|
|
28130
|
+
`${JSON.stringify({ format: "sandboxedjs-trusted-keys", keys }, null, 2)}
|
|
28131
|
+
`,
|
|
28132
|
+
{ cred: ctx.cred, mode: 420 }
|
|
28133
|
+
);
|
|
28134
|
+
}
|
|
28135
|
+
async function trust(ctx, args) {
|
|
28136
|
+
const [action, ...rest] = args;
|
|
28137
|
+
const keys = trustedKeys(ctx);
|
|
28138
|
+
switch (action) {
|
|
28139
|
+
case "list":
|
|
28140
|
+
case void 0: {
|
|
28141
|
+
if (keys.length === 0) {
|
|
28142
|
+
ctx.line("No publisher keys are trusted; only unsigned installs are possible.");
|
|
28143
|
+
return 0;
|
|
28144
|
+
}
|
|
28145
|
+
for (const key of keys) {
|
|
28146
|
+
ctx.line(`${key.publisher} ${await fingerprint(key.publicKey)}`);
|
|
28147
|
+
}
|
|
28148
|
+
return 0;
|
|
28149
|
+
}
|
|
28150
|
+
case "add": {
|
|
28151
|
+
const [publisher, publicKey] = rest;
|
|
28152
|
+
if (!publisher || !publicKey) {
|
|
28153
|
+
ctx.warn("pm trust add: give a publisher name and its base64 public key");
|
|
28154
|
+
return 2;
|
|
28155
|
+
}
|
|
28156
|
+
const next = keys.filter((key) => key.publicKey !== publicKey);
|
|
28157
|
+
next.push({ publisher, publicKey, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
28158
|
+
writeTrustedKeys(ctx, next);
|
|
28159
|
+
ctx.line(`Trusting ${publisher} (${await fingerprint(publicKey)})`);
|
|
28160
|
+
return 0;
|
|
28161
|
+
}
|
|
28162
|
+
case "remove": {
|
|
28163
|
+
const [publisher] = rest;
|
|
28164
|
+
const next = keys.filter((key) => key.publisher !== publisher);
|
|
28165
|
+
if (next.length === keys.length) {
|
|
28166
|
+
ctx.warn(`pm: no trusted key named ${publisher}`);
|
|
28167
|
+
return 1;
|
|
28168
|
+
}
|
|
28169
|
+
writeTrustedKeys(ctx, next);
|
|
28170
|
+
ctx.line(`Removed ${publisher}`);
|
|
28171
|
+
return 0;
|
|
28172
|
+
}
|
|
28173
|
+
default:
|
|
28174
|
+
ctx.warn(`pm trust: unknown action ${action}`);
|
|
28175
|
+
return 2;
|
|
28176
|
+
}
|
|
28177
|
+
}
|
|
28178
|
+
function unpack(ctx, archive, root, release, verified) {
|
|
28179
|
+
const entries = verified?.entries ?? readZip(archive);
|
|
28180
|
+
const manifest = verified?.manifest.app ?? readAppManifest(entries, release);
|
|
28181
|
+
if (manifest.name !== release.name || manifest.version !== release.version) {
|
|
28182
|
+
throw new AppRegistryError(
|
|
28183
|
+
`${release.filename} contains ${manifest.name} ${manifest.version}, but the registry lists it as ${release.name} ${release.version}`
|
|
28184
|
+
);
|
|
28185
|
+
}
|
|
28186
|
+
ctx.vfs.mkdir(root, { cred: ctx.cred, recursive: true });
|
|
28187
|
+
for (const entry of entries) {
|
|
28188
|
+
if (entry.name.startsWith("META/")) continue;
|
|
28189
|
+
const target = resolveInside(root, entry.name);
|
|
28190
|
+
if (entry.isDirectory) {
|
|
28191
|
+
ctx.vfs.mkdir(target, { cred: ctx.cred, recursive: true });
|
|
28192
|
+
continue;
|
|
28193
|
+
}
|
|
28194
|
+
const parent = target.slice(0, target.lastIndexOf("/"));
|
|
28195
|
+
if (parent) ctx.vfs.mkdir(parent, { cred: ctx.cred, recursive: true });
|
|
28196
|
+
const executable = (entry.mode & 73) !== 0 || (manifest.bins ?? []).some((binary) => binary.path === entry.name);
|
|
28197
|
+
ctx.vfs.writeFile(target, entry.data(), {
|
|
28198
|
+
cred: ctx.cred,
|
|
28199
|
+
mode: executable ? 493 : 420
|
|
28200
|
+
});
|
|
28201
|
+
}
|
|
28202
|
+
return manifest;
|
|
28203
|
+
}
|
|
28204
|
+
function readAppManifest(entries, release) {
|
|
28205
|
+
const entry = entries.find((candidate) => candidate.name === "app.json");
|
|
28206
|
+
if (!entry) throw new AppRegistryError(`${release.filename} has no app.json`);
|
|
28207
|
+
return validateManifest2(
|
|
28208
|
+
JSON.parse(new TextDecoder().decode(entry.data())),
|
|
28209
|
+
`${release.name}'s app.json`
|
|
28210
|
+
);
|
|
28211
|
+
}
|
|
28212
|
+
function resolveInside(root, name) {
|
|
28213
|
+
const clean2 = name.replace(/\\/g, "/");
|
|
28214
|
+
if (clean2.startsWith("/")) {
|
|
28215
|
+
throw new AppRegistryError(`archive entry ${name} is an absolute path`);
|
|
28216
|
+
}
|
|
28217
|
+
const parts = [];
|
|
28218
|
+
for (const part of clean2.split("/")) {
|
|
28219
|
+
if (part === "" || part === ".") continue;
|
|
28220
|
+
if (part === "..") {
|
|
28221
|
+
throw new AppRegistryError(`archive entry ${name} escapes the package directory`);
|
|
28222
|
+
}
|
|
28223
|
+
parts.push(part);
|
|
28224
|
+
}
|
|
28225
|
+
return `${root}/${parts.join("/")}`;
|
|
28226
|
+
}
|
|
28227
|
+
function linkBinaries(ctx, manifest, root) {
|
|
28228
|
+
const installed3 = [];
|
|
28229
|
+
if ((manifest.bins ?? []).length === 0) return installed3;
|
|
28230
|
+
ctx.vfs.mkdir(APP_BIN_DIR, { cred: ctx.cred, recursive: true });
|
|
28231
|
+
for (const binary of manifest.bins ?? []) {
|
|
28232
|
+
const target = resolveInside(root, binary.path);
|
|
28233
|
+
const wrapper = `${APP_BIN_DIR}/${binary.name}`;
|
|
28234
|
+
ctx.vfs.writeFile(
|
|
28235
|
+
wrapper,
|
|
28236
|
+
`#!/bin/sh
|
|
28237
|
+
# installed by pm for ${manifest.name} ${manifest.version}
|
|
28238
|
+
exec ${target} "$@"
|
|
28239
|
+
`,
|
|
28240
|
+
{ cred: ctx.cred, mode: 493 }
|
|
28241
|
+
);
|
|
28242
|
+
installed3.push(binary.name);
|
|
28243
|
+
}
|
|
28244
|
+
return installed3;
|
|
28245
|
+
}
|
|
28246
|
+
async function remove(ctx, args) {
|
|
28247
|
+
if (args.length === 0) {
|
|
28248
|
+
ctx.warn("pm remove: name it");
|
|
28249
|
+
return 2;
|
|
28250
|
+
}
|
|
28251
|
+
let code = 0;
|
|
28252
|
+
for (const specifier of args) {
|
|
28253
|
+
const { name } = parseSpecifier(specifier);
|
|
28254
|
+
if (!removeInstalled(ctx, name, { quiet: false })) {
|
|
28255
|
+
ctx.warn(`pm: ${name} is not installed`);
|
|
28256
|
+
code = 1;
|
|
28257
|
+
} else {
|
|
28258
|
+
ctx.line(`Removed ${name}`);
|
|
28259
|
+
}
|
|
28260
|
+
}
|
|
28261
|
+
return code;
|
|
28262
|
+
}
|
|
28263
|
+
function removeInstalled(ctx, name, options) {
|
|
28264
|
+
const apps = installedApps(ctx);
|
|
28265
|
+
const app = apps.find((entry) => entry.name === name);
|
|
28266
|
+
if (!app) return false;
|
|
28267
|
+
for (const binary of app.installedBins) {
|
|
28268
|
+
try {
|
|
28269
|
+
ctx.vfs.unlink(`${APP_BIN_DIR}/${binary}`, ctx.cred);
|
|
28270
|
+
} catch {
|
|
28271
|
+
}
|
|
28272
|
+
}
|
|
28273
|
+
try {
|
|
28274
|
+
removeTree(ctx, app.root);
|
|
28275
|
+
} catch (error) {
|
|
28276
|
+
if (!options.quiet) ctx.warn(`pm: could not remove ${app.root}: ${error.message}`);
|
|
28277
|
+
}
|
|
28278
|
+
writeState(ctx, apps.filter((entry) => entry.name !== name));
|
|
28279
|
+
return true;
|
|
28280
|
+
}
|
|
28281
|
+
function removeTree(ctx, path) {
|
|
28282
|
+
let stats;
|
|
28283
|
+
try {
|
|
28284
|
+
stats = ctx.vfs.stat(path, { cred: ctx.cred });
|
|
28285
|
+
} catch {
|
|
28286
|
+
return;
|
|
28287
|
+
}
|
|
28288
|
+
if (stats.isDirectory()) {
|
|
28289
|
+
for (const entry of ctx.vfs.readdir(path, ctx.cred)) {
|
|
28290
|
+
removeTree(ctx, `${path}/${entry}`);
|
|
28291
|
+
}
|
|
28292
|
+
ctx.vfs.rmdir(path, ctx.cred);
|
|
28293
|
+
return;
|
|
28294
|
+
}
|
|
28295
|
+
ctx.vfs.unlink(path, ctx.cred);
|
|
28296
|
+
}
|
|
28297
|
+
function listInstalled2(ctx) {
|
|
28298
|
+
const apps = installedApps(ctx);
|
|
28299
|
+
if (apps.length === 0) {
|
|
28300
|
+
ctx.line("No applications installed. `pm available` lists what there is.");
|
|
28301
|
+
return 0;
|
|
28302
|
+
}
|
|
28303
|
+
for (const app of apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
28304
|
+
const commands12 = app.installedBins.length > 0 ? ` (${app.installedBins.join(", ")})` : "";
|
|
28305
|
+
ctx.line(`${app.name} ${app.version}${commands12}`);
|
|
28306
|
+
}
|
|
28307
|
+
return 0;
|
|
28308
|
+
}
|
|
28309
|
+
async function info(ctx, args) {
|
|
28310
|
+
const name = args[0];
|
|
28311
|
+
if (!name) {
|
|
28312
|
+
ctx.warn("pm info: name it");
|
|
28313
|
+
return 2;
|
|
28314
|
+
}
|
|
28315
|
+
const local = installedApps(ctx).find((app) => app.name === name);
|
|
28316
|
+
if (local) {
|
|
28317
|
+
ctx.line(`${local.name} ${local.version} (installed ${local.installedAt})`);
|
|
28318
|
+
if (local.summary) ctx.line(` ${local.summary}`);
|
|
28319
|
+
ctx.line(` root: ${local.root}`);
|
|
28320
|
+
if (local.installedBins.length > 0) ctx.line(` commands: ${local.installedBins.join(", ")}`);
|
|
28321
|
+
for (const service of local.services ?? []) {
|
|
28322
|
+
ctx.line(` service: ${service.name} on port ${service.port}${service.path ?? ""}`);
|
|
28323
|
+
}
|
|
28324
|
+
if (local.docs) ctx.line(` docs: ${local.root}/${local.docs}`);
|
|
28325
|
+
return 0;
|
|
28326
|
+
}
|
|
28327
|
+
const release = selectRelease(await fetchIndex(ctx), name);
|
|
28328
|
+
ctx.line(`${release.name} ${release.version} (not installed)`);
|
|
28329
|
+
if (release.summary) ctx.line(` ${release.summary}`);
|
|
28330
|
+
ctx.line(` size: ${Math.round(release.size / 1024)} kB`);
|
|
28331
|
+
if ((release.bins ?? []).length > 0) {
|
|
28332
|
+
ctx.line(` commands: ${(release.bins ?? []).map((b) => b.name).join(", ")}`);
|
|
28333
|
+
}
|
|
28334
|
+
return 0;
|
|
28335
|
+
}
|
|
28336
|
+
async function search(ctx, args) {
|
|
28337
|
+
const term = (args[0] ?? "").toLowerCase();
|
|
28338
|
+
const index = await fetchIndex(ctx);
|
|
28339
|
+
const matches3 = index.apps.filter(
|
|
28340
|
+
(app) => app.name.toLowerCase().includes(term) || (app.summary ?? "").toLowerCase().includes(term)
|
|
28341
|
+
);
|
|
28342
|
+
if (matches3.length === 0) {
|
|
28343
|
+
ctx.line(`nothing matches ${JSON.stringify(args[0] ?? "")}`);
|
|
28344
|
+
return 1;
|
|
28345
|
+
}
|
|
28346
|
+
for (const app of matches3) ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
|
|
28347
|
+
return 0;
|
|
28348
|
+
}
|
|
28349
|
+
async function available(ctx) {
|
|
28350
|
+
const index = await fetchIndex(ctx);
|
|
28351
|
+
for (const app of index.apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
28352
|
+
ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
|
|
28353
|
+
}
|
|
28354
|
+
return 0;
|
|
28355
|
+
}
|
|
28356
|
+
async function fetchIndex(ctx) {
|
|
28357
|
+
const base2 = registryBase(ctx).replace(/\/$/, "");
|
|
28358
|
+
const url = `${base2}/index.json`;
|
|
28359
|
+
let response;
|
|
28360
|
+
try {
|
|
28361
|
+
response = await fetch(url);
|
|
28362
|
+
} catch (error) {
|
|
28363
|
+
throw new AppRegistryError(
|
|
28364
|
+
`cannot reach the application registry at ${url}: ${error.message}`
|
|
28365
|
+
);
|
|
28366
|
+
}
|
|
28367
|
+
if (!response.ok) {
|
|
28368
|
+
throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
|
|
28369
|
+
}
|
|
28370
|
+
return parseIndex(await response.text(), url);
|
|
28371
|
+
}
|
|
28372
|
+
function installedApps(ctx) {
|
|
28373
|
+
try {
|
|
28374
|
+
const text2 = ctx.vfs.readText(APP_STATE, ctx.cred);
|
|
28375
|
+
const parsed = JSON.parse(text2);
|
|
28376
|
+
return Array.isArray(parsed.apps) ? parsed.apps : [];
|
|
28377
|
+
} catch {
|
|
28378
|
+
return [];
|
|
28379
|
+
}
|
|
28380
|
+
}
|
|
28381
|
+
function record(ctx, app) {
|
|
28382
|
+
const apps = installedApps(ctx).filter((entry) => entry.name !== app.name);
|
|
28383
|
+
apps.push(app);
|
|
28384
|
+
writeState(ctx, apps);
|
|
28385
|
+
}
|
|
28386
|
+
function writeState(ctx, apps) {
|
|
28387
|
+
const directory2 = APP_STATE.slice(0, APP_STATE.lastIndexOf("/"));
|
|
28388
|
+
ctx.vfs.mkdir(directory2, { cred: ctx.cred, recursive: true });
|
|
28389
|
+
ctx.vfs.writeFile(
|
|
28390
|
+
APP_STATE,
|
|
28391
|
+
`${JSON.stringify({ format: "sandboxedjs-installed-apps", apps }, null, 2)}
|
|
28392
|
+
`,
|
|
28393
|
+
{ cred: ctx.cred, mode: 420 }
|
|
28394
|
+
);
|
|
28395
|
+
}
|
|
28396
|
+
function appCommands() {
|
|
28397
|
+
return [pm];
|
|
28398
|
+
}
|
|
28399
|
+
|
|
27065
28400
|
// src/bin/index.ts
|
|
27066
28401
|
function allCommands() {
|
|
27067
28402
|
return [
|
|
@@ -27081,7 +28416,8 @@ function allCommands() {
|
|
|
27081
28416
|
...ffmpegCommands(),
|
|
27082
28417
|
...wasiCommands(),
|
|
27083
28418
|
...packageCommands(),
|
|
27084
|
-
...browserCommands()
|
|
28419
|
+
...browserCommands(),
|
|
28420
|
+
...appCommands()
|
|
27085
28421
|
];
|
|
27086
28422
|
}
|
|
27087
28423
|
function installUserland(kernel) {
|
|
@@ -30800,7 +32136,7 @@ ${indent(detail, 4)}
|
|
|
30800
32136
|
if (detail) host2.write(`${indent(detail, depth * 2 + 2)}
|
|
30801
32137
|
`);
|
|
30802
32138
|
};
|
|
30803
|
-
const
|
|
32139
|
+
const record2 = (outcome, name, error) => {
|
|
30804
32140
|
counts.tests += 1;
|
|
30805
32141
|
if (outcome === "pass") counts.pass += 1;
|
|
30806
32142
|
else if (outcome === "fail") {
|
|
@@ -30822,13 +32158,13 @@ ${indent(detail, 4)}
|
|
|
30822
32158
|
if (!matchesPattern2(display)) return true;
|
|
30823
32159
|
if (runOnly && !options.only) return true;
|
|
30824
32160
|
if (options.skip) {
|
|
30825
|
-
|
|
32161
|
+
record2("skip", display);
|
|
30826
32162
|
report(depth, "skip", name, started);
|
|
30827
32163
|
return true;
|
|
30828
32164
|
}
|
|
30829
32165
|
if (!fn) {
|
|
30830
32166
|
const outcome = options.todo ? "todo" : "skip";
|
|
30831
|
-
|
|
32167
|
+
record2(outcome, display);
|
|
30832
32168
|
report(depth, outcome, name, started);
|
|
30833
32169
|
return true;
|
|
30834
32170
|
}
|
|
@@ -30892,21 +32228,21 @@ ${indent(detail, 4)}
|
|
|
30892
32228
|
controller.abort();
|
|
30893
32229
|
}
|
|
30894
32230
|
if (skipped) {
|
|
30895
|
-
|
|
32231
|
+
record2("skip", display);
|
|
30896
32232
|
report(depth, "skip", name, started);
|
|
30897
32233
|
return true;
|
|
30898
32234
|
}
|
|
30899
32235
|
if (error !== void 0) {
|
|
30900
32236
|
if (todo) {
|
|
30901
|
-
|
|
32237
|
+
record2("todo", display);
|
|
30902
32238
|
report(depth, "todo", name, started);
|
|
30903
32239
|
return true;
|
|
30904
32240
|
}
|
|
30905
|
-
|
|
32241
|
+
record2("fail", display, error);
|
|
30906
32242
|
report(depth, "fail", name, started, errorText(error));
|
|
30907
32243
|
return false;
|
|
30908
32244
|
}
|
|
30909
|
-
|
|
32245
|
+
record2(todo ? "todo" : "pass", display);
|
|
30910
32246
|
report(depth, todo ? "todo" : "pass", name, started);
|
|
30911
32247
|
return true;
|
|
30912
32248
|
};
|
|
@@ -30921,7 +32257,7 @@ ${indent(detail, 4)}
|
|
|
30921
32257
|
else host2.write(`${" ".repeat(depth)}\u25B6 ${suite.name}
|
|
30922
32258
|
`);
|
|
30923
32259
|
if (suite.options.skip) {
|
|
30924
|
-
for (const child of suite.children) if (child.kind === "test")
|
|
32260
|
+
for (const child of suite.children) if (child.kind === "test") record2("skip", fullName(child));
|
|
30925
32261
|
report(depth, "skip", suite.name, started);
|
|
30926
32262
|
return true;
|
|
30927
32263
|
}
|
|
@@ -30933,7 +32269,7 @@ ${indent(detail, 4)}
|
|
|
30933
32269
|
} catch (error) {
|
|
30934
32270
|
for (const child of suite.children) {
|
|
30935
32271
|
if (child.kind === "test") {
|
|
30936
|
-
|
|
32272
|
+
record2("cancelled", fullName(child), error);
|
|
30937
32273
|
report(depth + (isRoot ? 0 : 1), "cancelled", child.name, started, errorText(error));
|
|
30938
32274
|
}
|
|
30939
32275
|
}
|
|
@@ -34835,6 +36171,212 @@ ${BUNDLER_HINT}`),
|
|
|
34835
36171
|
|
|
34836
36172
|
// src/worker/local-runtime-pod.ts
|
|
34837
36173
|
init_path();
|
|
36174
|
+
|
|
36175
|
+
// src/net/outbound-tcp.ts
|
|
36176
|
+
var FIRST_SYNTHETIC = 2887581696;
|
|
36177
|
+
var SYNTHETIC_COUNT = 65534;
|
|
36178
|
+
function toDotted(value) {
|
|
36179
|
+
return [value >>> 24, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
|
|
36180
|
+
}
|
|
36181
|
+
var OutboundTcp = class {
|
|
36182
|
+
constructor(policy, dialer) {
|
|
36183
|
+
this.policy = policy;
|
|
36184
|
+
this.dialer = dialer;
|
|
36185
|
+
}
|
|
36186
|
+
policy;
|
|
36187
|
+
dialer;
|
|
36188
|
+
byName = /* @__PURE__ */ new Map();
|
|
36189
|
+
byAddress = /* @__PURE__ */ new Map();
|
|
36190
|
+
next = 0;
|
|
36191
|
+
/** Whether this address was handed out by `resolve`. */
|
|
36192
|
+
knows(address) {
|
|
36193
|
+
return this.byAddress.has(address);
|
|
36194
|
+
}
|
|
36195
|
+
hostnameFor(address) {
|
|
36196
|
+
return this.byAddress.get(address);
|
|
36197
|
+
}
|
|
36198
|
+
/**
|
|
36199
|
+
* The address for `hostname`, allocating one on first use.
|
|
36200
|
+
*
|
|
36201
|
+
* Refusal happens here as well as at connect, because a name that cannot be
|
|
36202
|
+
* reached should fail as a resolution failure -- which is what every client
|
|
36203
|
+
* reports as "unknown host" rather than as a mid-connection error.
|
|
36204
|
+
*/
|
|
36205
|
+
resolve(hostname) {
|
|
36206
|
+
const name = hostname.trim().toLowerCase();
|
|
36207
|
+
if (!name) throw new PosixError(Errno.EINVAL, "empty hostname");
|
|
36208
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(name)) return name;
|
|
36209
|
+
if (isLoopbackHostname(name)) return "127.0.0.1";
|
|
36210
|
+
const known = this.byName.get(name);
|
|
36211
|
+
if (known) return known;
|
|
36212
|
+
if (!this.allowed(name)) {
|
|
36213
|
+
throw new PosixError(
|
|
36214
|
+
Errno.ENETUNREACH,
|
|
36215
|
+
`cannot resolve ${hostname}: outbound network access is not allowed for this container`
|
|
36216
|
+
);
|
|
36217
|
+
}
|
|
36218
|
+
if (this.next >= SYNTHETIC_COUNT) {
|
|
36219
|
+
throw new PosixError(Errno.ENOSPC, "no addresses left for resolved names");
|
|
36220
|
+
}
|
|
36221
|
+
const address = toDotted(FIRST_SYNTHETIC + this.next + 1);
|
|
36222
|
+
this.next += 1;
|
|
36223
|
+
this.byName.set(name, address);
|
|
36224
|
+
this.byAddress.set(address, name);
|
|
36225
|
+
return address;
|
|
36226
|
+
}
|
|
36227
|
+
/** Open a connection to a resolved address, or to a literal one. */
|
|
36228
|
+
async connect(address, port, local) {
|
|
36229
|
+
const hostname = this.byAddress.get(address) ?? address;
|
|
36230
|
+
if (!this.allowed(hostname)) {
|
|
36231
|
+
throw new PosixError(
|
|
36232
|
+
Errno.ENETUNREACH,
|
|
36233
|
+
`cannot reach ${hostname}:${port}: outbound network access is not allowed for this container`
|
|
36234
|
+
);
|
|
36235
|
+
}
|
|
36236
|
+
if (!this.dialer) {
|
|
36237
|
+
throw new PosixError(
|
|
36238
|
+
Errno.ENETUNREACH,
|
|
36239
|
+
`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`
|
|
36240
|
+
);
|
|
36241
|
+
}
|
|
36242
|
+
const host2 = await this.dialer(hostname, port);
|
|
36243
|
+
const guest = new VirtualTcpConnection(
|
|
36244
|
+
{ address: local.address, port: host2.localPort || local.port },
|
|
36245
|
+
{ address, port }
|
|
36246
|
+
);
|
|
36247
|
+
const bridge = new VirtualTcpConnection({ address, port }, { address: local.address, port: local.port });
|
|
36248
|
+
guest.pairWith(bridge);
|
|
36249
|
+
bridge.pairWith(guest);
|
|
36250
|
+
host2.onData((bytes2) => {
|
|
36251
|
+
try {
|
|
36252
|
+
bridge.write(bytes2);
|
|
36253
|
+
} catch {
|
|
36254
|
+
host2.close();
|
|
36255
|
+
}
|
|
36256
|
+
});
|
|
36257
|
+
host2.onClose(() => {
|
|
36258
|
+
bridge.shutdown(false, true);
|
|
36259
|
+
bridge.close();
|
|
36260
|
+
});
|
|
36261
|
+
void (async () => {
|
|
36262
|
+
for (; ; ) {
|
|
36263
|
+
if (bridge.readable) {
|
|
36264
|
+
let chunk;
|
|
36265
|
+
try {
|
|
36266
|
+
chunk = bridge.read(64 * 1024);
|
|
36267
|
+
} catch {
|
|
36268
|
+
break;
|
|
36269
|
+
}
|
|
36270
|
+
if (chunk.length === 0) {
|
|
36271
|
+
host2.end();
|
|
36272
|
+
break;
|
|
36273
|
+
}
|
|
36274
|
+
host2.write(chunk);
|
|
36275
|
+
continue;
|
|
36276
|
+
}
|
|
36277
|
+
await bridge.waitForChange();
|
|
36278
|
+
}
|
|
36279
|
+
})();
|
|
36280
|
+
return { connection: guest, host: host2 };
|
|
36281
|
+
}
|
|
36282
|
+
allowed(hostname) {
|
|
36283
|
+
const policy = this.policy();
|
|
36284
|
+
if (isLoopbackHostname(hostname)) return true;
|
|
36285
|
+
if (!policy.allowOutbound) return false;
|
|
36286
|
+
const hosts = policy.allowedHosts;
|
|
36287
|
+
if (!hosts) return true;
|
|
36288
|
+
return hosts.some((allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`));
|
|
36289
|
+
}
|
|
36290
|
+
};
|
|
36291
|
+
|
|
36292
|
+
// src/net/node-tcp-dialer.ts
|
|
36293
|
+
init_binary();
|
|
36294
|
+
function canDialTcp() {
|
|
36295
|
+
return typeof process !== "undefined" && Boolean(process.versions?.node);
|
|
36296
|
+
}
|
|
36297
|
+
function createNodeTcpDialer() {
|
|
36298
|
+
return async (host2, port) => {
|
|
36299
|
+
const net = await nodeBuiltin("net");
|
|
36300
|
+
return await new Promise((resolve3, reject) => {
|
|
36301
|
+
const socket = net.connect({ host: host2, port });
|
|
36302
|
+
const pending = [];
|
|
36303
|
+
let onData = null;
|
|
36304
|
+
let onClose = null;
|
|
36305
|
+
let closed = false;
|
|
36306
|
+
socket.on("data", (chunk) => {
|
|
36307
|
+
const bytes2 = new Uint8Array(chunk);
|
|
36308
|
+
if (onData) onData(bytes2);
|
|
36309
|
+
else pending.push(bytes2);
|
|
36310
|
+
});
|
|
36311
|
+
const finish = () => {
|
|
36312
|
+
if (closed) return;
|
|
36313
|
+
closed = true;
|
|
36314
|
+
onClose?.();
|
|
36315
|
+
};
|
|
36316
|
+
socket.on("close", finish);
|
|
36317
|
+
socket.on("end", finish);
|
|
36318
|
+
socket.on("error", (error) => {
|
|
36319
|
+
if (!closed) {
|
|
36320
|
+
closed = true;
|
|
36321
|
+
onClose?.();
|
|
36322
|
+
}
|
|
36323
|
+
reject(translate(error, host2, port));
|
|
36324
|
+
});
|
|
36325
|
+
socket.on("connect", () => {
|
|
36326
|
+
socket.setNoDelay(true);
|
|
36327
|
+
resolve3({
|
|
36328
|
+
write(bytes2) {
|
|
36329
|
+
socket.write(bytes2);
|
|
36330
|
+
},
|
|
36331
|
+
end() {
|
|
36332
|
+
socket.end();
|
|
36333
|
+
},
|
|
36334
|
+
close() {
|
|
36335
|
+
socket.destroy();
|
|
36336
|
+
},
|
|
36337
|
+
onData(handler) {
|
|
36338
|
+
onData = handler;
|
|
36339
|
+
for (const chunk of pending.splice(0)) handler(chunk);
|
|
36340
|
+
},
|
|
36341
|
+
onClose(handler) {
|
|
36342
|
+
onClose = handler;
|
|
36343
|
+
if (closed) handler();
|
|
36344
|
+
},
|
|
36345
|
+
get remoteAddress() {
|
|
36346
|
+
return socket.remoteAddress ?? host2;
|
|
36347
|
+
},
|
|
36348
|
+
get remotePort() {
|
|
36349
|
+
return socket.remotePort ?? port;
|
|
36350
|
+
},
|
|
36351
|
+
get localPort() {
|
|
36352
|
+
return socket.localPort ?? 0;
|
|
36353
|
+
}
|
|
36354
|
+
});
|
|
36355
|
+
});
|
|
36356
|
+
});
|
|
36357
|
+
};
|
|
36358
|
+
}
|
|
36359
|
+
function translate(error, host2, port) {
|
|
36360
|
+
const where = `${host2}:${port}`;
|
|
36361
|
+
switch (error.code) {
|
|
36362
|
+
case "ECONNREFUSED":
|
|
36363
|
+
return new PosixError(Errno.ECONNREFUSED, `connection refused by ${where}`);
|
|
36364
|
+
case "ENOTFOUND":
|
|
36365
|
+
case "EAI_AGAIN":
|
|
36366
|
+
return new PosixError(Errno.ENETUNREACH, `cannot resolve ${host2}`);
|
|
36367
|
+
case "ETIMEDOUT":
|
|
36368
|
+
return new PosixError(Errno.ETIMEDOUT, `connection to ${where} timed out`);
|
|
36369
|
+
case "ECONNRESET":
|
|
36370
|
+
return new PosixError(Errno.ECONNRESET, `connection to ${where} was reset`);
|
|
36371
|
+
case "EHOSTUNREACH":
|
|
36372
|
+
case "ENETUNREACH":
|
|
36373
|
+
return new PosixError(Errno.ENETUNREACH, `cannot reach ${where}`);
|
|
36374
|
+
default:
|
|
36375
|
+
return new PosixError(Errno.ECONNREFUSED, `connection to ${where} failed: ${error.message}`);
|
|
36376
|
+
}
|
|
36377
|
+
}
|
|
36378
|
+
|
|
36379
|
+
// src/worker/local-runtime-pod.ts
|
|
34838
36380
|
var WASM_ALIASES = {
|
|
34839
36381
|
esbuild: "esbuild-wasm",
|
|
34840
36382
|
rollup: "@rollup/wasm-node"
|
|
@@ -35190,6 +36732,32 @@ var LocalRuntimePod = class _LocalRuntimePod {
|
|
|
35190
36732
|
constructor(options) {
|
|
35191
36733
|
ensureProcessGlobal();
|
|
35192
36734
|
this.sockets = new VirtualTcpNetwork((port) => this.router.activePortsIncludes(port));
|
|
36735
|
+
this.sockets.loopbackHttp = (port, connection) => {
|
|
36736
|
+
if (!this.router.activePortsIncludes(port)) return false;
|
|
36737
|
+
void serveHttpOverConnection(connection, async (request) => {
|
|
36738
|
+
const url = new URL(request.target, `http://127.0.0.1:${port}`);
|
|
36739
|
+
const answer = await this.router.request(port, {
|
|
36740
|
+
method: request.method,
|
|
36741
|
+
path: `${url.pathname}${url.search}`,
|
|
36742
|
+
headers: request.headers,
|
|
36743
|
+
...request.body.length > 0 ? { body: request.body } : {}
|
|
36744
|
+
});
|
|
36745
|
+
const payload = answer.body ?? new Uint8Array();
|
|
36746
|
+
return {
|
|
36747
|
+
status: Number(answer.statusCode ?? 200),
|
|
36748
|
+
statusText: String(answer.statusMessage ?? ""),
|
|
36749
|
+
headers: answer.headers ?? {},
|
|
36750
|
+
body: typeof payload === "string" ? new TextEncoder().encode(payload) : payload instanceof Uint8Array ? payload : new Uint8Array(payload)
|
|
36751
|
+
};
|
|
36752
|
+
});
|
|
36753
|
+
return true;
|
|
36754
|
+
};
|
|
36755
|
+
if (canDialTcp()) {
|
|
36756
|
+
this.sockets.outbound = new OutboundTcp(
|
|
36757
|
+
() => this.networkPolicy ?? { allowOutbound: false, allowedHosts: null },
|
|
36758
|
+
createNodeTcpDialer()
|
|
36759
|
+
);
|
|
36760
|
+
}
|
|
35193
36761
|
this.router = new VirtualHttpRouter(this.sockets);
|
|
35194
36762
|
this.workdir = options.workdir ?? "/";
|
|
35195
36763
|
this.env = { ...options.env };
|
|
@@ -36159,6 +37727,14 @@ var Container = class _Container {
|
|
|
36159
37727
|
installUserland(kernel);
|
|
36160
37728
|
const container = new _Container(kernel, pod, opts);
|
|
36161
37729
|
if (opts.files) container.mountSync(opts.files, { cwd: opts.cwd ?? "/" });
|
|
37730
|
+
if (opts.modules && opts.modules.length > 0) {
|
|
37731
|
+
const result = await container.exec(`pm install ${opts.modules.join(" ")}`);
|
|
37732
|
+
if (result.exitCode !== 0) {
|
|
37733
|
+
throw new Error(
|
|
37734
|
+
`could not install ${opts.modules.join(", ")}: ${(result.stderr || result.stdout).trim() || `pm exited ${result.exitCode}`}`
|
|
37735
|
+
);
|
|
37736
|
+
}
|
|
37737
|
+
}
|
|
36162
37738
|
return container;
|
|
36163
37739
|
}
|
|
36164
37740
|
// ── file seeding ──────────────────────────────────────────────────────────
|