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.js
CHANGED
|
@@ -6986,8 +6986,8 @@ var WasiHost = class {
|
|
|
6986
6986
|
view.setUint16(at + 8, WASI_ESUCCESS, true);
|
|
6987
6987
|
view.setUint8(at + 10, subscription.type);
|
|
6988
6988
|
if (subscription.type === EVENTTYPE_FD_READ) {
|
|
6989
|
-
const
|
|
6990
|
-
view.setBigUint64(at + 16, BigInt(
|
|
6989
|
+
const available2 = subscription.fd === 0 ? this.opts.stdin.available : this.bytesLeft(subscription.fd);
|
|
6990
|
+
view.setBigUint64(at + 16, BigInt(available2), true);
|
|
6991
6991
|
} else if (subscription.type === EVENTTYPE_FD_WRITE) {
|
|
6992
6992
|
view.setBigUint64(at + 16, BigInt(64 * 1024), true);
|
|
6993
6993
|
}
|
|
@@ -7224,13 +7224,13 @@ var BinaryExecutionRegistry = class {
|
|
|
7224
7224
|
}
|
|
7225
7225
|
async run(ctx, path) {
|
|
7226
7226
|
const bytes2 = ctx.vfs.readFile(path, ctx.cred);
|
|
7227
|
-
let
|
|
7227
|
+
let info2;
|
|
7228
7228
|
try {
|
|
7229
|
-
|
|
7229
|
+
info2 = inspectElf(bytes2);
|
|
7230
7230
|
} catch (error) {
|
|
7231
7231
|
return ctx.fail(String(error), 126);
|
|
7232
7232
|
}
|
|
7233
|
-
const request = { path, bytes: bytes2, info, sha256: binaryDigest(bytes2) };
|
|
7233
|
+
const request = { path, bytes: bytes2, info: info2, sha256: binaryDigest(bytes2) };
|
|
7234
7234
|
const reasons = [];
|
|
7235
7235
|
const backends = [...this.backends];
|
|
7236
7236
|
for (const tier of ["compatibility", "translation", "emulation"]) {
|
|
@@ -7249,7 +7249,7 @@ var BinaryExecutionRegistry = class {
|
|
|
7249
7249
|
reasons.push(`${backend.id}: ${result.reason}`);
|
|
7250
7250
|
}
|
|
7251
7251
|
}
|
|
7252
|
-
return ctx.fail(`cannot execute ${
|
|
7252
|
+
return ctx.fail(`cannot execute ${info2.architecture} ELF: ${reasons.join("; ") || "no binary backends registered"}`, 126);
|
|
7253
7253
|
}
|
|
7254
7254
|
};
|
|
7255
7255
|
function checkedWasm(artifact) {
|
|
@@ -8513,8 +8513,8 @@ var NetworkStack = class {
|
|
|
8513
8513
|
return this.ifaces.some((i) => i.ipv4 === addr);
|
|
8514
8514
|
}
|
|
8515
8515
|
// ── listening ports ──────────────────────────────────────────────────────
|
|
8516
|
-
registerListener(port,
|
|
8517
|
-
this.listeners.set(port, { port, since: Date.now(), ...
|
|
8516
|
+
registerListener(port, info2) {
|
|
8517
|
+
this.listeners.set(port, { port, since: Date.now(), ...info2 });
|
|
8518
8518
|
}
|
|
8519
8519
|
unregisterListener(port) {
|
|
8520
8520
|
this.listeners.delete(port);
|
|
@@ -12107,21 +12107,21 @@ var uname = defineCommand({
|
|
|
12107
12107
|
{ short: "i", long: "hardware-platform" },
|
|
12108
12108
|
{ short: "o", long: "operating-system" }
|
|
12109
12109
|
]);
|
|
12110
|
-
const
|
|
12110
|
+
const info2 = unameInfo(ctx.kernel.hostname);
|
|
12111
12111
|
if (args.has("all")) {
|
|
12112
|
-
ctx.line(`${
|
|
12112
|
+
ctx.line(`${info2.sysname} ${info2.nodename} ${info2.release} ${info2.version} ${info2.machine} ${info2.operatingSystem}`);
|
|
12113
12113
|
return 0;
|
|
12114
12114
|
}
|
|
12115
12115
|
const parts = [];
|
|
12116
|
-
if (args.has("kernel-name")) parts.push(
|
|
12117
|
-
if (args.has("nodename")) parts.push(
|
|
12118
|
-
if (args.has("kernel-release")) parts.push(
|
|
12119
|
-
if (args.has("kernel-version")) parts.push(
|
|
12120
|
-
if (args.has("machine")) parts.push(
|
|
12121
|
-
if (args.has("processor")) parts.push(
|
|
12122
|
-
if (args.has("hardware-platform")) parts.push(
|
|
12123
|
-
if (args.has("operating-system")) parts.push(
|
|
12124
|
-
ctx.line(parts.length ? parts.join(" ") :
|
|
12116
|
+
if (args.has("kernel-name")) parts.push(info2.sysname);
|
|
12117
|
+
if (args.has("nodename")) parts.push(info2.nodename);
|
|
12118
|
+
if (args.has("kernel-release")) parts.push(info2.release);
|
|
12119
|
+
if (args.has("kernel-version")) parts.push(info2.version);
|
|
12120
|
+
if (args.has("machine")) parts.push(info2.machine);
|
|
12121
|
+
if (args.has("processor")) parts.push(info2.processor);
|
|
12122
|
+
if (args.has("hardware-platform")) parts.push(info2.hardwarePlatform);
|
|
12123
|
+
if (args.has("operating-system")) parts.push(info2.operatingSystem);
|
|
12124
|
+
ctx.line(parts.length ? parts.join(" ") : info2.sysname);
|
|
12125
12125
|
return 0;
|
|
12126
12126
|
}
|
|
12127
12127
|
});
|
|
@@ -15105,15 +15105,15 @@ var Interpreter = class {
|
|
|
15105
15105
|
return this.exitCode;
|
|
15106
15106
|
}
|
|
15107
15107
|
};
|
|
15108
|
-
function splitRecord(
|
|
15109
|
-
if (
|
|
15108
|
+
function splitRecord(record2, fs) {
|
|
15109
|
+
if (record2 === "") return [];
|
|
15110
15110
|
if (fs === " ") {
|
|
15111
|
-
const trimmed =
|
|
15111
|
+
const trimmed = record2.replace(/^[ \t\n]+|[ \t\n]+$/g, "");
|
|
15112
15112
|
return trimmed === "" ? [] : trimmed.split(/[ \t\n]+/);
|
|
15113
15113
|
}
|
|
15114
|
-
if (fs === "") return [...
|
|
15115
|
-
if (fs.length === 1 && !"\\^$.[]|()*+?{}".includes(fs)) return
|
|
15116
|
-
return
|
|
15114
|
+
if (fs === "") return [...record2];
|
|
15115
|
+
if (fs.length === 1 && !"\\^$.[]|()*+?{}".includes(fs)) return record2.split(fs);
|
|
15116
|
+
return record2.split(new RegExp(translateEre(fs)));
|
|
15117
15117
|
}
|
|
15118
15118
|
var ERE_CLASSES = {
|
|
15119
15119
|
alpha: "A-Za-z",
|
|
@@ -19804,6 +19804,7 @@ var Op = {
|
|
|
19804
19804
|
setsockopt: 1802,
|
|
19805
19805
|
getsockopt: 1803,
|
|
19806
19806
|
socket_close: 1804,
|
|
19807
|
+
resolve: 1805,
|
|
19807
19808
|
spawn: 2048,
|
|
19808
19809
|
waitpid: 2049,
|
|
19809
19810
|
kill: 2050
|
|
@@ -20040,6 +20041,28 @@ var wheels_default = {
|
|
|
20040
20041
|
patches: []
|
|
20041
20042
|
}
|
|
20042
20043
|
},
|
|
20044
|
+
{
|
|
20045
|
+
name: "asyncpg",
|
|
20046
|
+
version: "0.31.0",
|
|
20047
|
+
filename: "asyncpg-0.31.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
|
|
20048
|
+
sha256: "3eaf965cbd78d2314e8f12b9384e5642725323063aaff2708a8a4b3c01ef10ba",
|
|
20049
|
+
abiId: "sbxabi1-c2637d04695ad927",
|
|
20050
|
+
requires: [
|
|
20051
|
+
'async_timeout>=4.0.3; python_version < "3.11.0"',
|
|
20052
|
+
'gssapi; platform_system != "Windows" and extra == "gssauth"',
|
|
20053
|
+
'sspilib; platform_system == "Windows" and extra == "gssauth"'
|
|
20054
|
+
],
|
|
20055
|
+
provenance: {
|
|
20056
|
+
sourceTreeDigest: "a9ab0177da5c0c2d0ba635d1f5568687efbf8aefc08cd51cd086277184eba76f",
|
|
20057
|
+
recipeSha256: "fb9cf1a9fd35d8d63ac69dcf6a1671ceeebcb23f381a100b412058d75ff2eedc",
|
|
20058
|
+
buildTools: [
|
|
20059
|
+
"setuptools==84.0.0",
|
|
20060
|
+
"Cython==3.1.4"
|
|
20061
|
+
],
|
|
20062
|
+
nativeDependencies: [],
|
|
20063
|
+
patches: []
|
|
20064
|
+
}
|
|
20065
|
+
},
|
|
20043
20066
|
{
|
|
20044
20067
|
name: "bcrypt",
|
|
20045
20068
|
version: "5.0.0",
|
|
@@ -20061,18 +20084,20 @@ var wheels_default = {
|
|
|
20061
20084
|
name: "brotli",
|
|
20062
20085
|
version: "1.2.0",
|
|
20063
20086
|
filename: "brotli-1.2.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
|
|
20064
|
-
sha256: "
|
|
20087
|
+
sha256: "d8b06cc04770a048b41956c9cc767e7332c49f0229e69744f4b1c2892b888b27",
|
|
20065
20088
|
abiId: "sbxabi1-c2637d04695ad927",
|
|
20066
20089
|
requires: [],
|
|
20067
20090
|
provenance: {
|
|
20068
20091
|
sourceTreeDigest: "7b563fa2e7f20edac204cc80004cae35c844b36a83758e5957184383332d1209",
|
|
20069
|
-
recipeSha256: "
|
|
20092
|
+
recipeSha256: "1326c898e999feba536ff15f080fed9ac4b9cd14e0dbfee39a950decf19607ef",
|
|
20070
20093
|
buildTools: [
|
|
20071
20094
|
"setuptools==84.0.0",
|
|
20072
20095
|
"pkgconfig==1.6.0"
|
|
20073
20096
|
],
|
|
20074
20097
|
nativeDependencies: [],
|
|
20075
|
-
patches: [
|
|
20098
|
+
patches: [
|
|
20099
|
+
"0001-meth-noargs-signatures.patch"
|
|
20100
|
+
]
|
|
20076
20101
|
}
|
|
20077
20102
|
},
|
|
20078
20103
|
{
|
|
@@ -20133,6 +20158,25 @@ var wheels_default = {
|
|
|
20133
20158
|
patches: []
|
|
20134
20159
|
}
|
|
20135
20160
|
},
|
|
20161
|
+
{
|
|
20162
|
+
name: "google-crc32c",
|
|
20163
|
+
version: "1.8.0",
|
|
20164
|
+
filename: "google_crc32c-1.8.0-cp313-cp313-emscripten_5_0_6_wasm32.whl",
|
|
20165
|
+
sha256: "e64c21841598ea21121d7b7bb203e124d58c035db49ffce03631c55216c942cd",
|
|
20166
|
+
abiId: "sbxabi1-c2637d04695ad927",
|
|
20167
|
+
requires: [],
|
|
20168
|
+
provenance: {
|
|
20169
|
+
sourceTreeDigest: "fa277032a78adfccf6adcda32cadc4f6958a324979040c231e01f0963821cb75",
|
|
20170
|
+
recipeSha256: "843dea57727061bdfc7bcd27ac380d16a767effa538cbab6257e106939689f43",
|
|
20171
|
+
buildTools: [
|
|
20172
|
+
"setuptools==84.0.0"
|
|
20173
|
+
],
|
|
20174
|
+
nativeDependencies: [
|
|
20175
|
+
"crc32c==1.1.2"
|
|
20176
|
+
],
|
|
20177
|
+
patches: []
|
|
20178
|
+
}
|
|
20179
|
+
},
|
|
20136
20180
|
{
|
|
20137
20181
|
name: "httptools",
|
|
20138
20182
|
version: "0.8.0",
|
|
@@ -20169,6 +20213,32 @@ var wheels_default = {
|
|
|
20169
20213
|
patches: []
|
|
20170
20214
|
}
|
|
20171
20215
|
},
|
|
20216
|
+
{
|
|
20217
|
+
name: "lxml",
|
|
20218
|
+
version: "6.1.3",
|
|
20219
|
+
filename: "lxml-6.1.3-cp313-cp313-emscripten_5_0_6_wasm32.whl",
|
|
20220
|
+
sha256: "dc76d45314b806934bdaf35f9e4ae61e86e741e07bac53fae7e94f9d3dab9d4e",
|
|
20221
|
+
abiId: "sbxabi1-c2637d04695ad927",
|
|
20222
|
+
requires: [
|
|
20223
|
+
'cssselect>=0.7; extra == "cssselect"',
|
|
20224
|
+
'html5lib; extra == "html5"',
|
|
20225
|
+
'BeautifulSoup4; extra == "htmlsoup"',
|
|
20226
|
+
'lxml_html_clean; extra == "html-clean"'
|
|
20227
|
+
],
|
|
20228
|
+
provenance: {
|
|
20229
|
+
sourceTreeDigest: "aedea680c1fe58ae9a124d81ea28f2316765e444e3809691854ddbf173c22bf5",
|
|
20230
|
+
recipeSha256: "aa62da97bb13eb5fa0a047f7c61ed93554f60142be9fcae64e3053e2a5673e9f",
|
|
20231
|
+
buildTools: [
|
|
20232
|
+
"setuptools==84.0.0",
|
|
20233
|
+
"Cython==3.1.4"
|
|
20234
|
+
],
|
|
20235
|
+
nativeDependencies: [
|
|
20236
|
+
"xml2==2.13.8",
|
|
20237
|
+
"xslt==1.1.43"
|
|
20238
|
+
],
|
|
20239
|
+
patches: []
|
|
20240
|
+
}
|
|
20241
|
+
},
|
|
20172
20242
|
{
|
|
20173
20243
|
name: "lz4",
|
|
20174
20244
|
version: "4.4.5",
|
|
@@ -20270,6 +20340,113 @@ var wheels_default = {
|
|
|
20270
20340
|
]
|
|
20271
20341
|
}
|
|
20272
20342
|
},
|
|
20343
|
+
{
|
|
20344
|
+
name: "pandas",
|
|
20345
|
+
version: "3.0.5",
|
|
20346
|
+
filename: "pandas-3.0.5-cp313-cp313-emscripten_5_0_6_wasm32.whl",
|
|
20347
|
+
sha256: "ae36ad9b403b6fc28cb2bb79f54a38e1897ab43635c596fff2bce87f8148ac48",
|
|
20348
|
+
abiId: "sbxabi1-c2637d04695ad927",
|
|
20349
|
+
requires: [
|
|
20350
|
+
'numpy>=1.26.0; python_version < "3.14"',
|
|
20351
|
+
'numpy>=2.3.3; python_version >= "3.14"',
|
|
20352
|
+
"python-dateutil>=2.8.2",
|
|
20353
|
+
'tzdata; sys_platform == "win32"',
|
|
20354
|
+
'tzdata; sys_platform == "emscripten"',
|
|
20355
|
+
'hypothesis>=6.116.0; extra == "test"',
|
|
20356
|
+
'pytest<9.1,>=8.3.4; extra == "test"',
|
|
20357
|
+
'pytest-xdist>=3.6.1; extra == "test"',
|
|
20358
|
+
'pyarrow>=13.0.0; extra == "pyarrow"',
|
|
20359
|
+
'bottleneck>=1.4.2; extra == "performance"',
|
|
20360
|
+
'numba>=0.60.0; extra == "performance"',
|
|
20361
|
+
'numexpr>=2.10.2; extra == "performance"',
|
|
20362
|
+
'scipy>=1.14.1; extra == "computation"',
|
|
20363
|
+
'xarray>=2024.10.0; extra == "computation"',
|
|
20364
|
+
'fsspec>=2024.10.0; extra == "fss"',
|
|
20365
|
+
's3fs>=2024.10.0; extra == "aws"',
|
|
20366
|
+
'gcsfs>=2024.10.0; extra == "gcp"',
|
|
20367
|
+
'odfpy>=1.4.1; extra == "excel"',
|
|
20368
|
+
'openpyxl>=3.1.5; extra == "excel"',
|
|
20369
|
+
'python-calamine>=0.3.0; extra == "excel"',
|
|
20370
|
+
'pyxlsb>=1.0.10; extra == "excel"',
|
|
20371
|
+
'xlrd>=2.0.1; extra == "excel"',
|
|
20372
|
+
'xlsxwriter>=3.2.0; extra == "excel"',
|
|
20373
|
+
'pyarrow>=13.0.0; extra == "parquet"',
|
|
20374
|
+
'pyarrow>=13.0.0; extra == "feather"',
|
|
20375
|
+
'pyiceberg>=0.8.1; extra == "iceberg"',
|
|
20376
|
+
'tables>=3.10.1; extra == "hdf5"',
|
|
20377
|
+
'pyreadstat>=1.2.8; extra == "spss"',
|
|
20378
|
+
'SQLAlchemy>=2.0.36; extra == "postgresql"',
|
|
20379
|
+
'psycopg2>=2.9.10; extra == "postgresql"',
|
|
20380
|
+
'adbc-driver-postgresql>=1.2.0; extra == "postgresql"',
|
|
20381
|
+
'SQLAlchemy>=2.0.36; extra == "mysql"',
|
|
20382
|
+
'pymysql>=1.1.1; extra == "mysql"',
|
|
20383
|
+
'SQLAlchemy>=2.0.36; extra == "sql-other"',
|
|
20384
|
+
'adbc-driver-postgresql>=1.2.0; extra == "sql-other"',
|
|
20385
|
+
'adbc-driver-sqlite>=1.2.0; extra == "sql-other"',
|
|
20386
|
+
'beautifulsoup4>=4.12.3; extra == "html"',
|
|
20387
|
+
'html5lib>=1.1; extra == "html"',
|
|
20388
|
+
'lxml>=5.3.0; extra == "html"',
|
|
20389
|
+
'lxml>=5.3.0; extra == "xml"',
|
|
20390
|
+
'matplotlib>=3.9.3; extra == "plot"',
|
|
20391
|
+
'jinja2>=3.1.5; extra == "output-formatting"',
|
|
20392
|
+
'tabulate>=0.9.0; extra == "output-formatting"',
|
|
20393
|
+
'PyQt5>=5.15.9; extra == "clipboard"',
|
|
20394
|
+
'qtpy>=2.4.2; extra == "clipboard"',
|
|
20395
|
+
'zstandard>=0.23.0; extra == "compression"',
|
|
20396
|
+
'pytz>=2020.1; extra == "timezone"',
|
|
20397
|
+
'adbc-driver-postgresql>=1.2.0; extra == "all"',
|
|
20398
|
+
'adbc-driver-sqlite>=1.2.0; extra == "all"',
|
|
20399
|
+
'beautifulsoup4>=4.12.3; extra == "all"',
|
|
20400
|
+
'bottleneck>=1.4.2; extra == "all"',
|
|
20401
|
+
'fastparquet>=2024.11.0; extra == "all"',
|
|
20402
|
+
'fsspec>=2024.10.0; extra == "all"',
|
|
20403
|
+
'gcsfs>=2024.10.0; extra == "all"',
|
|
20404
|
+
'html5lib>=1.1; extra == "all"',
|
|
20405
|
+
'hypothesis>=6.116.0; extra == "all"',
|
|
20406
|
+
'jinja2>=3.1.5; extra == "all"',
|
|
20407
|
+
'lxml>=5.3.0; extra == "all"',
|
|
20408
|
+
'matplotlib>=3.9.3; extra == "all"',
|
|
20409
|
+
'numba>=0.60.0; extra == "all"',
|
|
20410
|
+
'numexpr>=2.10.2; extra == "all"',
|
|
20411
|
+
'odfpy>=1.4.1; extra == "all"',
|
|
20412
|
+
'openpyxl>=3.1.5; extra == "all"',
|
|
20413
|
+
'psycopg2>=2.9.10; extra == "all"',
|
|
20414
|
+
'pyarrow>=13.0.0; extra == "all"',
|
|
20415
|
+
'pyiceberg>=0.8.1; extra == "all"',
|
|
20416
|
+
'pymysql>=1.1.1; extra == "all"',
|
|
20417
|
+
'PyQt5>=5.15.9; extra == "all"',
|
|
20418
|
+
'pyreadstat>=1.2.8; extra == "all"',
|
|
20419
|
+
'pytest>=8.3.4; extra == "all"',
|
|
20420
|
+
'pytest-xdist>=3.6.1; extra == "all"',
|
|
20421
|
+
'python-calamine>=0.3.0; extra == "all"',
|
|
20422
|
+
'pytz>=2020.1; extra == "all"',
|
|
20423
|
+
'pyxlsb>=1.0.10; extra == "all"',
|
|
20424
|
+
'qtpy>=2.4.2; extra == "all"',
|
|
20425
|
+
'scipy>=1.14.1; extra == "all"',
|
|
20426
|
+
's3fs>=2024.10.0; extra == "all"',
|
|
20427
|
+
'SQLAlchemy>=2.0.36; extra == "all"',
|
|
20428
|
+
'tables>=3.10.1; extra == "all"',
|
|
20429
|
+
'tabulate>=0.9.0; extra == "all"',
|
|
20430
|
+
'xarray>=2024.10.0; extra == "all"',
|
|
20431
|
+
'xlrd>=2.0.1; extra == "all"',
|
|
20432
|
+
'xlsxwriter>=3.2.0; extra == "all"',
|
|
20433
|
+
'zstandard>=0.23.0; extra == "all"'
|
|
20434
|
+
],
|
|
20435
|
+
provenance: {
|
|
20436
|
+
sourceTreeDigest: "373f08151b5a209a84dd35b086316ef11a0d4030150457c2bfdab38305903d54",
|
|
20437
|
+
recipeSha256: "56976ed58b17053971334a62b503d5397b8ff739911fb596a116df23aeccabb0",
|
|
20438
|
+
buildTools: [
|
|
20439
|
+
"setuptools==84.0.0",
|
|
20440
|
+
"Cython==3.1.4",
|
|
20441
|
+
"meson-python==0.21.0",
|
|
20442
|
+
"meson==1.12.0",
|
|
20443
|
+
"ninja==1.13.2",
|
|
20444
|
+
"versioneer==0.29"
|
|
20445
|
+
],
|
|
20446
|
+
nativeDependencies: [],
|
|
20447
|
+
patches: []
|
|
20448
|
+
}
|
|
20449
|
+
},
|
|
20273
20450
|
{
|
|
20274
20451
|
name: "pillow",
|
|
20275
20452
|
version: "12.3.0",
|
|
@@ -20374,6 +20551,31 @@ var wheels_default = {
|
|
|
20374
20551
|
patches: []
|
|
20375
20552
|
}
|
|
20376
20553
|
},
|
|
20554
|
+
{
|
|
20555
|
+
name: "rapidfuzz",
|
|
20556
|
+
version: "3.14.6",
|
|
20557
|
+
filename: "rapidfuzz-3.14.6-cp313-cp313-emscripten_5_0_6_wasm32.whl",
|
|
20558
|
+
sha256: "b9ed2ea058350925beda765364c0f382bf9a7c08b3bec33840795178de021d32",
|
|
20559
|
+
abiId: "sbxabi1-c2637d04695ad927",
|
|
20560
|
+
requires: [
|
|
20561
|
+
'numpy; extra == "all"'
|
|
20562
|
+
],
|
|
20563
|
+
provenance: {
|
|
20564
|
+
sourceTreeDigest: "ae2b2bb95f7aba2babbbe668b68d48b5347ca923e6508b76d24228cd13ac7cd3",
|
|
20565
|
+
recipeSha256: "46954814226fff070aa6771d2f09d32a96b718ce8ef7a9f9404375850f7d1f77",
|
|
20566
|
+
buildTools: [
|
|
20567
|
+
"setuptools==84.0.0",
|
|
20568
|
+
"scikit-build-core==1.0.3",
|
|
20569
|
+
"cmake==4.4.3",
|
|
20570
|
+
"ninja==1.13.2",
|
|
20571
|
+
"Cython==3.1.4",
|
|
20572
|
+
"pathspec==1.1.1",
|
|
20573
|
+
"packaging==26.3"
|
|
20574
|
+
],
|
|
20575
|
+
nativeDependencies: [],
|
|
20576
|
+
patches: []
|
|
20577
|
+
}
|
|
20578
|
+
},
|
|
20377
20579
|
{
|
|
20378
20580
|
name: "regex",
|
|
20379
20581
|
version: "2026.9.10",
|
|
@@ -20739,7 +20941,7 @@ var wheels_default = {
|
|
|
20739
20941
|
|
|
20740
20942
|
// package.json
|
|
20741
20943
|
var package_default = {
|
|
20742
|
-
version: "0.1.
|
|
20944
|
+
version: "0.1.95"};
|
|
20743
20945
|
|
|
20744
20946
|
// src/python/extension-abi.ts
|
|
20745
20947
|
var EXTENSION_ABI = {
|
|
@@ -21777,6 +21979,8 @@ var VirtualTcpSocket = class _VirtualTcpSocket {
|
|
|
21777
21979
|
network;
|
|
21778
21980
|
listenerValue = null;
|
|
21779
21981
|
connectionValue = null;
|
|
21982
|
+
/** Held so closing the guest's socket closes the host's, not just the queues. */
|
|
21983
|
+
hostConnection = null;
|
|
21780
21984
|
boundPort = 0;
|
|
21781
21985
|
boundAddress = "0.0.0.0";
|
|
21782
21986
|
bind(address, port) {
|
|
@@ -21794,13 +21998,35 @@ var VirtualTcpSocket = class _VirtualTcpSocket {
|
|
|
21794
21998
|
}
|
|
21795
21999
|
this.listenerValue = this.network.listen(this.boundPort, this.boundAddress, backlog);
|
|
21796
22000
|
}
|
|
21797
|
-
|
|
22001
|
+
/**
|
|
22002
|
+
* Connect, in the container or out of it.
|
|
22003
|
+
*
|
|
22004
|
+
* The destination used to be discarded, which quietly turned every connect
|
|
22005
|
+
* into a loopback connect: a guest dialling example.com:443 would reach a
|
|
22006
|
+
* container server listening on 443 and believe it had reached the internet.
|
|
22007
|
+
* Loopback is still routed inside the container; anything else goes to the
|
|
22008
|
+
* host's outbound stack, which owns the names and the real sockets, and
|
|
22009
|
+
* which refuses with a reason when the host has no way out.
|
|
22010
|
+
*/
|
|
22011
|
+
async connect(port, address = "127.0.0.1") {
|
|
21798
22012
|
if (this.listenerValue || this.connectionValue) throw new PosixError(Errno.EISCONN);
|
|
21799
22013
|
if (!isLoopback(address)) {
|
|
21800
|
-
|
|
21801
|
-
|
|
21802
|
-
|
|
21803
|
-
|
|
22014
|
+
const outbound = this.network.outbound;
|
|
22015
|
+
if (!outbound) {
|
|
22016
|
+
throw new PosixError(
|
|
22017
|
+
Errno.ENETUNREACH,
|
|
22018
|
+
`cannot reach ${address}:${port}: outbound network access is not available through raw sockets in this container; only loopback is routed`
|
|
22019
|
+
);
|
|
22020
|
+
}
|
|
22021
|
+
const dialled = await outbound.connect(address, port, {
|
|
22022
|
+
address: this.boundAddress,
|
|
22023
|
+
port: this.boundPort
|
|
22024
|
+
});
|
|
22025
|
+
this.connectionValue = dialled.connection;
|
|
22026
|
+
this.hostConnection = dialled.host;
|
|
22027
|
+
this.boundPort = dialled.connection.local.port;
|
|
22028
|
+
this.boundAddress = dialled.connection.local.address;
|
|
22029
|
+
return;
|
|
21804
22030
|
}
|
|
21805
22031
|
this.connectionValue = this.network.connect(port, this.boundPort, this.boundAddress);
|
|
21806
22032
|
this.boundPort = this.connectionValue.local.port;
|
|
@@ -21851,6 +22077,8 @@ var VirtualTcpSocket = class _VirtualTcpSocket {
|
|
|
21851
22077
|
close() {
|
|
21852
22078
|
if (this.listenerValue) this.network.close(this.listenerValue);
|
|
21853
22079
|
this.connectionValue?.close();
|
|
22080
|
+
this.hostConnection?.close();
|
|
22081
|
+
this.hostConnection = null;
|
|
21854
22082
|
this.listenerValue = null;
|
|
21855
22083
|
this.connectionValue = null;
|
|
21856
22084
|
}
|
|
@@ -21915,7 +22143,7 @@ var SocketDescription = class _SocketDescription {
|
|
|
21915
22143
|
this.resource.listen(backlog);
|
|
21916
22144
|
}
|
|
21917
22145
|
connect(address, port) {
|
|
21918
|
-
this.resource.connect(port, address);
|
|
22146
|
+
return this.resource.connect(port, address);
|
|
21919
22147
|
}
|
|
21920
22148
|
accept() {
|
|
21921
22149
|
return new _SocketDescription(this.resource.accept(), this.flags);
|
|
@@ -21927,7 +22155,11 @@ var SocketDescription = class _SocketDescription {
|
|
|
21927
22155
|
this.options.set(`${level}:${option}`, value.slice());
|
|
21928
22156
|
}
|
|
21929
22157
|
getOption(level, option) {
|
|
21930
|
-
|
|
22158
|
+
const stored = this.options.get(`${level}:${option}`);
|
|
22159
|
+
if (stored) return stored.slice();
|
|
22160
|
+
if (level === SOL_SOCKET && option === SO_TYPE) return int32(SOCK_STREAM);
|
|
22161
|
+
if (level === SOL_SOCKET && option === SO_ERROR) return int32(0);
|
|
22162
|
+
return new Uint8Array(4);
|
|
21931
22163
|
}
|
|
21932
22164
|
localAddress() {
|
|
21933
22165
|
return this.resource.local;
|
|
@@ -21938,6 +22170,15 @@ var SocketDescription = class _SocketDescription {
|
|
|
21938
22170
|
return peer;
|
|
21939
22171
|
}
|
|
21940
22172
|
};
|
|
22173
|
+
var SOL_SOCKET = 1;
|
|
22174
|
+
var SO_ERROR = 4;
|
|
22175
|
+
var SO_TYPE = 3;
|
|
22176
|
+
var SOCK_STREAM = 1;
|
|
22177
|
+
function int32(value) {
|
|
22178
|
+
const bytes2 = new Uint8Array(4);
|
|
22179
|
+
new DataView(bytes2.buffer).setInt32(0, value, true);
|
|
22180
|
+
return bytes2;
|
|
22181
|
+
}
|
|
21941
22182
|
function isLoopback(address) {
|
|
21942
22183
|
if (address === "" || address === "localhost" || address === "0.0.0.0") return true;
|
|
21943
22184
|
if (address === "::1" || address === "::" || address === "[::1]") return true;
|
|
@@ -21949,6 +22190,18 @@ var VirtualTcpNetwork = class {
|
|
|
21949
22190
|
}
|
|
21950
22191
|
occupied;
|
|
21951
22192
|
listeners = /* @__PURE__ */ new Map();
|
|
22193
|
+
/** The host's outbound stack, when this host can open sockets at all. */
|
|
22194
|
+
outbound = null;
|
|
22195
|
+
/**
|
|
22196
|
+
* Ports held by in-container servers that are not sockets.
|
|
22197
|
+
*
|
|
22198
|
+
* A JavaScript HTTP server in the container is registered with the request
|
|
22199
|
+
* router and never binds one of these sockets, so a guest connecting to its
|
|
22200
|
+
* port found nothing listening. Before sockets could leave the container
|
|
22201
|
+
* this did not arise -- every guest client went out through the egress --
|
|
22202
|
+
* and afterwards `urllib` dialling a container server got ECONNREFUSED.
|
|
22203
|
+
*/
|
|
22204
|
+
loopbackHttp = null;
|
|
21952
22205
|
listen(port, address = "0.0.0.0", backlog = 128) {
|
|
21953
22206
|
if (this.listeners.has(port) || this.occupied?.(port)) throw new PosixError(Errno.EADDRINUSE, `port ${port} is already in use`);
|
|
21954
22207
|
const listener = new VirtualTcpListener(port, address, backlog);
|
|
@@ -21962,7 +22215,11 @@ var VirtualTcpNetwork = class {
|
|
|
21962
22215
|
}
|
|
21963
22216
|
connect(port, localPort = 0, localAddress = "127.0.0.1") {
|
|
21964
22217
|
const listener = this.listeners.get(port);
|
|
21965
|
-
if (!listener || listener.isClosed)
|
|
22218
|
+
if (!listener || listener.isClosed) {
|
|
22219
|
+
const bridged = this.connectToNonSocketServer(port, localPort, localAddress);
|
|
22220
|
+
if (bridged) return bridged;
|
|
22221
|
+
throw new PosixError(Errno.ECONNREFUSED, `port ${port} is not listening`);
|
|
22222
|
+
}
|
|
21966
22223
|
const client = new VirtualTcpConnection(
|
|
21967
22224
|
{ address: localAddress, port: localPort },
|
|
21968
22225
|
{ address: listener.address, port }
|
|
@@ -21976,6 +22233,26 @@ var VirtualTcpNetwork = class {
|
|
|
21976
22233
|
listener.enqueue(server);
|
|
21977
22234
|
return client;
|
|
21978
22235
|
}
|
|
22236
|
+
/** A connection to a port an in-container server holds without a socket. */
|
|
22237
|
+
connectToNonSocketServer(port, localPort, localAddress) {
|
|
22238
|
+
if (!this.loopbackHttp) return null;
|
|
22239
|
+
const client = new VirtualTcpConnection(
|
|
22240
|
+
{ address: localAddress, port: localPort },
|
|
22241
|
+
{ address: "127.0.0.1", port }
|
|
22242
|
+
);
|
|
22243
|
+
const server = new VirtualTcpConnection(
|
|
22244
|
+
{ address: "127.0.0.1", port },
|
|
22245
|
+
{ address: localAddress, port: localPort }
|
|
22246
|
+
);
|
|
22247
|
+
client.pairWith(server);
|
|
22248
|
+
server.pairWith(client);
|
|
22249
|
+
if (!this.loopbackHttp(port, server)) {
|
|
22250
|
+
client.close();
|
|
22251
|
+
server.close();
|
|
22252
|
+
return null;
|
|
22253
|
+
}
|
|
22254
|
+
return client;
|
|
22255
|
+
}
|
|
21979
22256
|
hasListener(port) {
|
|
21980
22257
|
return this.listeners.has(port);
|
|
21981
22258
|
}
|
|
@@ -21993,7 +22270,7 @@ var POLLIN = 1;
|
|
|
21993
22270
|
var POLLOUT = 4;
|
|
21994
22271
|
var POLLHUP = 16;
|
|
21995
22272
|
var AF_INET = 2;
|
|
21996
|
-
var
|
|
22273
|
+
var SOCK_STREAM2 = 1;
|
|
21997
22274
|
var IPPROTO_TCP = 6;
|
|
21998
22275
|
function createHostAbiServer(proc) {
|
|
21999
22276
|
const files = new FileService(proc.vfs, proc.cred);
|
|
@@ -22284,7 +22561,7 @@ function createHostAbiServer(proc) {
|
|
|
22284
22561
|
const family = r.u32();
|
|
22285
22562
|
const type = r.u32();
|
|
22286
22563
|
const protocol = r.i32();
|
|
22287
|
-
if (family !== AF_INET || (type & 15) !==
|
|
22564
|
+
if (family !== AF_INET || (type & 15) !== SOCK_STREAM2) throw new PosixError(Errno.EAFNOSUPPORT);
|
|
22288
22565
|
if (protocol !== 0 && protocol !== IPPROTO_TCP) throw new PosixError(Errno.EPROTONOSUPPORT);
|
|
22289
22566
|
if (!proc.sockets) throw new PosixError(Errno.ENOSYS);
|
|
22290
22567
|
return { status: proc.table.add(new SocketDescription(new VirtualTcpSocket(proc.sockets))) };
|
|
@@ -22308,9 +22585,15 @@ function createHostAbiServer(proc) {
|
|
|
22308
22585
|
}
|
|
22309
22586
|
case Op.connect: {
|
|
22310
22587
|
const description = socketDescription(proc.table.get(r.i32()));
|
|
22311
|
-
description.connect(r.string(), r.u32());
|
|
22588
|
+
await description.connect(r.string(), r.u32());
|
|
22312
22589
|
return { status: 0 };
|
|
22313
22590
|
}
|
|
22591
|
+
case Op.resolve: {
|
|
22592
|
+
const sockets = proc.sockets;
|
|
22593
|
+
if (!sockets?.outbound) throw new PosixError(Errno.ENOSYS);
|
|
22594
|
+
const address = sockets.outbound.resolve(r.string());
|
|
22595
|
+
return { status: 0, payload: new Writer().string(address).finish() };
|
|
22596
|
+
}
|
|
22314
22597
|
case Op.send: {
|
|
22315
22598
|
const description = socketDescription(proc.table.get(r.i32()));
|
|
22316
22599
|
return { status: await writeBlocking(description, r.bytes32()) };
|
|
@@ -22868,6 +23151,32 @@ async function serve(ctx, connection) {
|
|
|
22868
23151
|
connection.close();
|
|
22869
23152
|
}
|
|
22870
23153
|
}
|
|
23154
|
+
async function serveHttpOverConnection(connection, handle) {
|
|
23155
|
+
try {
|
|
23156
|
+
const request = await readRequest(connection);
|
|
23157
|
+
if (!request) {
|
|
23158
|
+
writeResponse(connection, 400, "Bad Request", {}, new TextEncoder().encode("malformed request\n"));
|
|
23159
|
+
return;
|
|
23160
|
+
}
|
|
23161
|
+
const result = await handle(request);
|
|
23162
|
+
writeResponse(connection, result.status, result.statusText, result.headers, result.body);
|
|
23163
|
+
} catch (error) {
|
|
23164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23165
|
+
try {
|
|
23166
|
+
writeResponse(
|
|
23167
|
+
connection,
|
|
23168
|
+
502,
|
|
23169
|
+
"Bad Gateway",
|
|
23170
|
+
{ "x-sbx-error": "1" },
|
|
23171
|
+
new TextEncoder().encode(`${message}
|
|
23172
|
+
`)
|
|
23173
|
+
);
|
|
23174
|
+
} catch {
|
|
23175
|
+
}
|
|
23176
|
+
} finally {
|
|
23177
|
+
connection.close();
|
|
23178
|
+
}
|
|
23179
|
+
}
|
|
22871
23180
|
var listeners = /* @__PURE__ */ new WeakMap();
|
|
22872
23181
|
function ensureHttpEgress(ctx) {
|
|
22873
23182
|
const sockets = ctx.kernel.pod.sockets;
|
|
@@ -23360,7 +23669,7 @@ async function resolve2(options) {
|
|
|
23360
23669
|
const clauses = (state.constraints.get(name) ?? []).flatMap((entry) => entry.spec.map((s) => `${s.op}${s.version}`)).sort();
|
|
23361
23670
|
return `${name}|${[...new Set(clauses)].join(",")}`;
|
|
23362
23671
|
};
|
|
23363
|
-
const
|
|
23672
|
+
const search2 = async (queue, state) => {
|
|
23364
23673
|
checkBudget();
|
|
23365
23674
|
if (queue.length === 0) return state;
|
|
23366
23675
|
const [edge, ...rest] = queue;
|
|
@@ -23368,7 +23677,7 @@ async function resolve2(options) {
|
|
|
23368
23677
|
const name = requirement.name;
|
|
23369
23678
|
if (!markerApplies(requirement.marker, environment, extras)) {
|
|
23370
23679
|
if (requirement.marker) skipped.push({ name, marker: requirement.marker });
|
|
23371
|
-
return
|
|
23680
|
+
return search2(rest, state);
|
|
23372
23681
|
}
|
|
23373
23682
|
const previous = state.constraints.get(name) ?? [];
|
|
23374
23683
|
const constraints = new Map(state.constraints);
|
|
@@ -23385,7 +23694,7 @@ async function resolve2(options) {
|
|
|
23385
23694
|
explain(name, next.constraints, already.version)
|
|
23386
23695
|
);
|
|
23387
23696
|
}
|
|
23388
|
-
return
|
|
23697
|
+
return search2(rest, next);
|
|
23389
23698
|
}
|
|
23390
23699
|
const signature = signatureOf(name, next);
|
|
23391
23700
|
const learned = nogoods.get(signature);
|
|
@@ -23398,7 +23707,7 @@ async function resolve2(options) {
|
|
|
23398
23707
|
if (viable.length === 0) {
|
|
23399
23708
|
const wanted = [...unusable.get(name)?.versions ?? []].filter((version) => clauses.every((s) => compareVersions(version, s.version, s.op)));
|
|
23400
23709
|
const nothingUsable = all.length === 0 || wanted.length > 0;
|
|
23401
|
-
const
|
|
23710
|
+
const error = new ResolutionError(
|
|
23402
23711
|
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`,
|
|
23403
23712
|
explain(name, next.constraints, null, all),
|
|
23404
23713
|
nothingUsable ? {
|
|
@@ -23410,8 +23719,8 @@ async function resolve2(options) {
|
|
|
23410
23719
|
sourceAvailable: unusable.get(name)?.sourceAvailable ?? false
|
|
23411
23720
|
} : { reason: "conflicting-requirements", package: name }
|
|
23412
23721
|
);
|
|
23413
|
-
nogoods.set(signature,
|
|
23414
|
-
throw
|
|
23722
|
+
nogoods.set(signature, error);
|
|
23723
|
+
throw error;
|
|
23415
23724
|
}
|
|
23416
23725
|
let failure2 = null;
|
|
23417
23726
|
const seenVersions = /* @__PURE__ */ new Set();
|
|
@@ -23423,9 +23732,9 @@ async function resolve2(options) {
|
|
|
23423
23732
|
let requires;
|
|
23424
23733
|
try {
|
|
23425
23734
|
requires = await requiresFor(candidate);
|
|
23426
|
-
} catch (
|
|
23735
|
+
} catch (error) {
|
|
23427
23736
|
failure2 = new ResolutionError(
|
|
23428
|
-
`could not read the dependencies of ${name} ${candidate.version}: ${
|
|
23737
|
+
`could not read the dependencies of ${name} ${candidate.version}: ${error.message}`
|
|
23429
23738
|
);
|
|
23430
23739
|
continue;
|
|
23431
23740
|
}
|
|
@@ -23448,16 +23757,14 @@ async function resolve2(options) {
|
|
|
23448
23757
|
const chosen = new Map(next.chosen);
|
|
23449
23758
|
chosen.set(name, { ...candidate, requires });
|
|
23450
23759
|
try {
|
|
23451
|
-
return await
|
|
23452
|
-
} catch (
|
|
23453
|
-
if (!(
|
|
23454
|
-
if (/cancelled|budget/.test(
|
|
23455
|
-
failure2 =
|
|
23760
|
+
return await search2([...edges, ...rest], { chosen, constraints: next.constraints });
|
|
23761
|
+
} catch (error) {
|
|
23762
|
+
if (!(error instanceof ResolutionError)) throw error;
|
|
23763
|
+
if (/cancelled|budget/.test(error.message)) throw error;
|
|
23764
|
+
failure2 = error;
|
|
23456
23765
|
}
|
|
23457
23766
|
}
|
|
23458
|
-
|
|
23459
|
-
nogoods.set(signature, error);
|
|
23460
|
-
throw error;
|
|
23767
|
+
throw failure2 ?? new ResolutionError(`could not resolve ${name}`);
|
|
23461
23768
|
};
|
|
23462
23769
|
const seeded = /* @__PURE__ */ new Map();
|
|
23463
23770
|
for (const root of roots) {
|
|
@@ -23465,12 +23772,12 @@ async function resolve2(options) {
|
|
|
23465
23772
|
if (specifiers.length === 0 || !markerApplies(marker, environment, root.extras)) continue;
|
|
23466
23773
|
seeded.set(name, [...seeded.get(name) ?? [], { spec: specifiers, origin: root.origin }]);
|
|
23467
23774
|
}
|
|
23468
|
-
const solved = await
|
|
23775
|
+
const solved = await search2(roots, { chosen: /* @__PURE__ */ new Map(), constraints: seeded });
|
|
23469
23776
|
const distributions = [...solved.chosen.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
23470
23777
|
const reported = new Map(skipped.map((s) => [`${s.name}\0${s.marker}`, s]));
|
|
23471
23778
|
return { distributions, skipped: [...reported.values()] };
|
|
23472
23779
|
}
|
|
23473
|
-
function explain(name, constraints, chosenVersion,
|
|
23780
|
+
function explain(name, constraints, chosenVersion, available2 = []) {
|
|
23474
23781
|
const lines = [];
|
|
23475
23782
|
for (const entry of constraints.get(name) ?? []) {
|
|
23476
23783
|
if (entry.spec.length === 0) continue;
|
|
@@ -23478,9 +23785,9 @@ function explain(name, constraints, chosenVersion, available = []) {
|
|
|
23478
23785
|
lines.push(`${entry.origin} requires ${name} ${text2}`);
|
|
23479
23786
|
}
|
|
23480
23787
|
if (chosenVersion) lines.push(`but ${name} ${chosenVersion} is already selected`);
|
|
23481
|
-
if (
|
|
23482
|
-
const versions = [...new Set(
|
|
23483
|
-
lines.push(`usable versions: ${versions.join(", ")}${
|
|
23788
|
+
if (available2.length > 0) {
|
|
23789
|
+
const versions = [...new Set(available2.map((c) => c.version))].slice(0, 8);
|
|
23790
|
+
lines.push(`usable versions: ${versions.join(", ")}${available2.length > 8 ? ", ..." : ""}`);
|
|
23484
23791
|
}
|
|
23485
23792
|
return lines;
|
|
23486
23793
|
}
|
|
@@ -23963,8 +24270,18 @@ async function runOwnedPython(ctx, argv) {
|
|
|
23963
24270
|
if (!manifest) throw new Error("no Python runtime manifest is configured");
|
|
23964
24271
|
const mounts = containerMounts(ctx);
|
|
23965
24272
|
const env2 = withSitePackages(ctx.env);
|
|
24273
|
+
await ensureTrustStore(ctx);
|
|
23966
24274
|
const egressPort = ensureHttpEgress(ctx);
|
|
23967
24275
|
if (egressPort > 0) env2[EGRESS_ENV] = `127.0.0.1:${egressPort}`;
|
|
24276
|
+
try {
|
|
24277
|
+
if (ctx.vfs.stat(CA_BUNDLE_PATH, { cred: ctx.cred }).size > 0) {
|
|
24278
|
+
env2.SSL_CERT_FILE = CA_BUNDLE_PATH;
|
|
24279
|
+
env2.SSL_CERT_DIR = "/etc/ssl/certs";
|
|
24280
|
+
env2.REQUESTS_CA_BUNDLE = CA_BUNDLE_PATH;
|
|
24281
|
+
}
|
|
24282
|
+
} catch {
|
|
24283
|
+
}
|
|
24284
|
+
if (ctx.kernel.pod.sockets?.outbound) env2.SBX_OUTBOUND_SOCKETS = "1";
|
|
23968
24285
|
env2.SBX_SERIAL_HOST_CALLS = "1";
|
|
23969
24286
|
const inherit = claimInheritance(env2);
|
|
23970
24287
|
const process2 = await startPythonProcess({
|
|
@@ -24005,6 +24322,200 @@ function isTerminal(ctx) {
|
|
|
24005
24322
|
const stdin = ctx.stdin;
|
|
24006
24323
|
return Boolean(stdin.isTTY || stdin.interactive);
|
|
24007
24324
|
}
|
|
24325
|
+
var TLS_MODULE_SOURCE = `"""TLS over a SandboxedJs socket, using this interpreter's own OpenSSL.
|
|
24326
|
+
|
|
24327
|
+
CPython's \`ssl\` cannot wrap one of these sockets directly: \`_ssl\` wants a real
|
|
24328
|
+
\`_socket.socket\` whose descriptor OpenSSL can read and write, and a socket here
|
|
24329
|
+
is a host resource reached through the SandboxedJs ABI. But OpenSSL does not
|
|
24330
|
+
need a descriptor -- \`SSLContext.wrap_bio\` drives the same protocol over a pair
|
|
24331
|
+
of memory buffers, which is how asyncio has always done TLS. So the handshake
|
|
24332
|
+
and every record are performed here, in the container, and only the ciphertext
|
|
24333
|
+
crosses to the host. Certificates are verified by this interpreter against the
|
|
24334
|
+
trust store the host writes into the image, and \`getpeercert\` answers.
|
|
24335
|
+
"""
|
|
24336
|
+
|
|
24337
|
+
import io
|
|
24338
|
+
import socket as _socket_module
|
|
24339
|
+
import ssl as _ssl
|
|
24340
|
+
|
|
24341
|
+
|
|
24342
|
+
def wrappable(sock):
|
|
24343
|
+
"""Whether this socket is one of the container's own."""
|
|
24344
|
+
target = getattr(sock, "_sock", sock)
|
|
24345
|
+
return hasattr(target, "_fd")
|
|
24346
|
+
|
|
24347
|
+
|
|
24348
|
+
class SbxTlsSocket:
|
|
24349
|
+
"""What \`SSLContext.wrap_socket\` returns for a container socket."""
|
|
24350
|
+
|
|
24351
|
+
def __init__(self, context, sock, server_hostname, suppress_ragged_eofs=True):
|
|
24352
|
+
self._sock = sock
|
|
24353
|
+
self.context = context
|
|
24354
|
+
self.server_hostname = server_hostname
|
|
24355
|
+
self.suppress_ragged_eofs = suppress_ragged_eofs
|
|
24356
|
+
self._incoming = _ssl.MemoryBIO()
|
|
24357
|
+
self._outgoing = _ssl.MemoryBIO()
|
|
24358
|
+
self._closed = False
|
|
24359
|
+
self._tls = context.wrap_bio(
|
|
24360
|
+
self._incoming, self._outgoing,
|
|
24361
|
+
server_side=False,
|
|
24362
|
+
server_hostname=server_hostname,
|
|
24363
|
+
)
|
|
24364
|
+
|
|
24365
|
+
# -- moving bytes between OpenSSL and the socket ------------------------
|
|
24366
|
+
def _flush(self):
|
|
24367
|
+
data = self._outgoing.read()
|
|
24368
|
+
if data:
|
|
24369
|
+
self._sock.sendall(data)
|
|
24370
|
+
|
|
24371
|
+
def _feed(self):
|
|
24372
|
+
chunk = self._sock.recv(32768)
|
|
24373
|
+
if not chunk:
|
|
24374
|
+
self._incoming.write_eof()
|
|
24375
|
+
return False
|
|
24376
|
+
self._incoming.write(chunk)
|
|
24377
|
+
return True
|
|
24378
|
+
|
|
24379
|
+
def _run(self, operation):
|
|
24380
|
+
"""Drive one OpenSSL operation, pumping the socket as it asks."""
|
|
24381
|
+
while True:
|
|
24382
|
+
try:
|
|
24383
|
+
result = operation()
|
|
24384
|
+
except _ssl.SSLWantReadError:
|
|
24385
|
+
self._flush()
|
|
24386
|
+
if not self._feed():
|
|
24387
|
+
raise
|
|
24388
|
+
continue
|
|
24389
|
+
except _ssl.SSLWantWriteError:
|
|
24390
|
+
self._flush()
|
|
24391
|
+
continue
|
|
24392
|
+
self._flush()
|
|
24393
|
+
return result
|
|
24394
|
+
|
|
24395
|
+
# -- the handshake ------------------------------------------------------
|
|
24396
|
+
def do_handshake(self, block=False):
|
|
24397
|
+
self._run(self._tls.do_handshake)
|
|
24398
|
+
|
|
24399
|
+
# -- the socket ---------------------------------------------------------
|
|
24400
|
+
def recv(self, length=1024, flags=0):
|
|
24401
|
+
if flags:
|
|
24402
|
+
raise OSError("socket flags are not supported")
|
|
24403
|
+
try:
|
|
24404
|
+
return self._run(lambda: self._tls.read(length))
|
|
24405
|
+
except _ssl.SSLZeroReturnError:
|
|
24406
|
+
return b""
|
|
24407
|
+
except _ssl.SSLWantReadError:
|
|
24408
|
+
# The peer closed without close_notify. Ragged, and normal enough
|
|
24409
|
+
# on the public internet that clients ask to tolerate it.
|
|
24410
|
+
if self.suppress_ragged_eofs:
|
|
24411
|
+
return b""
|
|
24412
|
+
raise
|
|
24413
|
+
|
|
24414
|
+
def recv_into(self, buffer, nbytes=0, flags=0):
|
|
24415
|
+
data = self.recv(nbytes or len(buffer), flags)
|
|
24416
|
+
buffer[:len(data)] = data
|
|
24417
|
+
return len(data)
|
|
24418
|
+
|
|
24419
|
+
def read(self, length=1024, buffer=None):
|
|
24420
|
+
if buffer is not None:
|
|
24421
|
+
return self.recv_into(buffer, length)
|
|
24422
|
+
return self.recv(length)
|
|
24423
|
+
|
|
24424
|
+
def send(self, data, flags=0):
|
|
24425
|
+
if flags:
|
|
24426
|
+
raise OSError("socket flags are not supported")
|
|
24427
|
+
return self._run(lambda: self._tls.write(data))
|
|
24428
|
+
|
|
24429
|
+
def sendall(self, data, flags=0):
|
|
24430
|
+
view = memoryview(data)
|
|
24431
|
+
while view:
|
|
24432
|
+
view = view[self.send(view, flags):]
|
|
24433
|
+
|
|
24434
|
+
def write(self, data):
|
|
24435
|
+
return self.send(data)
|
|
24436
|
+
|
|
24437
|
+
_io_refs = 0
|
|
24438
|
+
|
|
24439
|
+
def _decref_socketios(self):
|
|
24440
|
+
# socket.SocketIO calls this when a file object built on this socket is
|
|
24441
|
+
# closed; without it http.client failed with "no attribute
|
|
24442
|
+
# _decref_socketios" the moment it read a response.
|
|
24443
|
+
if self._io_refs > 0:
|
|
24444
|
+
self._io_refs -= 1
|
|
24445
|
+
|
|
24446
|
+
def makefile(self, mode="r", buffering=None, *, encoding=None, errors=None, newline=None):
|
|
24447
|
+
# http.client and everything built on it read the response through a
|
|
24448
|
+
# file object, so this has to work like socket.makefile does.
|
|
24449
|
+
raw = _socket_module.SocketIO(self, "rw" if "w" in mode else "r")
|
|
24450
|
+
self._io_refs += 1
|
|
24451
|
+
buffer = io.BufferedRWPair(raw, raw) if "w" in mode and "r" in mode else (
|
|
24452
|
+
io.BufferedWriter(raw) if "w" in mode else io.BufferedReader(raw)
|
|
24453
|
+
)
|
|
24454
|
+
if "b" in mode:
|
|
24455
|
+
return buffer
|
|
24456
|
+
text = io.TextIOWrapper(buffer, encoding, errors, newline)
|
|
24457
|
+
text.mode = mode
|
|
24458
|
+
return text
|
|
24459
|
+
|
|
24460
|
+
def settimeout(self, value): return self._sock.settimeout(value)
|
|
24461
|
+
def gettimeout(self): return self._sock.gettimeout()
|
|
24462
|
+
def setblocking(self, flag): return self._sock.setblocking(flag)
|
|
24463
|
+
def getblocking(self): return self._sock.getblocking()
|
|
24464
|
+
def setsockopt(self, *args): return self._sock.setsockopt(*args)
|
|
24465
|
+
def getsockopt(self, *args): return self._sock.getsockopt(*args)
|
|
24466
|
+
def fileno(self): return self._sock.fileno()
|
|
24467
|
+
def getpeername(self): return self._sock.getpeername()
|
|
24468
|
+
def getsockname(self): return self._sock.getsockname()
|
|
24469
|
+
def detach(self): return self._sock.detach()
|
|
24470
|
+
|
|
24471
|
+
def shutdown(self, how):
|
|
24472
|
+
return self._sock.shutdown(how)
|
|
24473
|
+
|
|
24474
|
+
def unwrap(self):
|
|
24475
|
+
self._run(self._tls.unwrap)
|
|
24476
|
+
return self._sock
|
|
24477
|
+
|
|
24478
|
+
def close(self):
|
|
24479
|
+
if not self._closed:
|
|
24480
|
+
self._closed = True
|
|
24481
|
+
self._sock.close()
|
|
24482
|
+
|
|
24483
|
+
def __enter__(self): return self
|
|
24484
|
+
def __exit__(self, *args): self.close()
|
|
24485
|
+
|
|
24486
|
+
@property
|
|
24487
|
+
def family(self): return getattr(self._sock, "family", _socket_module.AF_INET)
|
|
24488
|
+
@property
|
|
24489
|
+
def type(self): return getattr(self._sock, "type", _socket_module.SOCK_STREAM)
|
|
24490
|
+
@property
|
|
24491
|
+
def proto(self): return getattr(self._sock, "proto", 0)
|
|
24492
|
+
@property
|
|
24493
|
+
def session(self): return self._tls.session
|
|
24494
|
+
@session.setter
|
|
24495
|
+
def session(self, value): self._tls.session = value
|
|
24496
|
+
@property
|
|
24497
|
+
def session_reused(self): return self._tls.session_reused
|
|
24498
|
+
|
|
24499
|
+
# -- what OpenSSL knows about this connection ---------------------------
|
|
24500
|
+
def getpeercert(self, binary_form=False): return self._tls.getpeercert(binary_form)
|
|
24501
|
+
def get_unverified_chain(self): return self._tls.get_unverified_chain()
|
|
24502
|
+
def get_verified_chain(self): return self._tls.get_verified_chain()
|
|
24503
|
+
def cipher(self): return self._tls.cipher()
|
|
24504
|
+
def shared_ciphers(self): return self._tls.shared_ciphers()
|
|
24505
|
+
def compression(self): return self._tls.compression()
|
|
24506
|
+
def selected_alpn_protocol(self): return self._tls.selected_alpn_protocol()
|
|
24507
|
+
def selected_npn_protocol(self): return None
|
|
24508
|
+
def version(self): return self._tls.version()
|
|
24509
|
+
def pending(self): return self._tls.pending()
|
|
24510
|
+
def get_channel_binding(self, cb_type="tls-unique"):
|
|
24511
|
+
return self._tls.get_channel_binding(cb_type)
|
|
24512
|
+
|
|
24513
|
+
|
|
24514
|
+
def wrap(context, sock, server_hostname, suppress_ragged_eofs=True):
|
|
24515
|
+
wrapped = SbxTlsSocket(context, sock, server_hostname, suppress_ragged_eofs)
|
|
24516
|
+
wrapped.do_handshake()
|
|
24517
|
+
return wrapped
|
|
24518
|
+
`;
|
|
24008
24519
|
var SHIM_HEADER = "# generated by sandboxedjs - safe to delete or replace\n";
|
|
24009
24520
|
var SHIM_SOURCE = `"""Rebuild os.system and os.popen on the container's process table."""
|
|
24010
24521
|
import io, os, subprocess, sys
|
|
@@ -24141,7 +24652,51 @@ if sys.platform in ("emscripten", "wasi"):
|
|
|
24141
24652
|
sys.meta_path.insert(0, _SbxPyodideFetchHook())
|
|
24142
24653
|
|
|
24143
24654
|
|
|
24144
|
-
|
|
24655
|
+
# The egress shims exist for a host that cannot open sockets. Where the host
|
|
24656
|
+
# can, the guest's own clients are used instead: real streaming, real TLS, and
|
|
24657
|
+
# whatever protocol the library speaks rather than the subset HTTP-over-egress
|
|
24658
|
+
# allows. Both worlds are supported, and which one this is comes from the host
|
|
24659
|
+
# rather than from probing.
|
|
24660
|
+
if os.environ.get("SBX_OUTBOUND_SOCKETS") == "1":
|
|
24661
|
+
|
|
24662
|
+
class _SbxTruststoreHook:
|
|
24663
|
+
"""truststore verifies the peer's chain itself, against the operating
|
|
24664
|
+
system's trust store. In this container the handshake happens on the
|
|
24665
|
+
host, which verified the chain against exactly that store, and the
|
|
24666
|
+
chain is not carried back -- so truststore's own check cannot run and
|
|
24667
|
+
httpx2 failed with "no attribute get_unverified_chain". Its context is
|
|
24668
|
+
therefore the standard one: same verification, done once, where the
|
|
24669
|
+
connection is."""
|
|
24670
|
+
|
|
24671
|
+
def find_spec(self, name, path=None, target=None):
|
|
24672
|
+
if name != "truststore":
|
|
24673
|
+
return None
|
|
24674
|
+
import importlib.util
|
|
24675
|
+
|
|
24676
|
+
class _Loader:
|
|
24677
|
+
def create_module(self, spec):
|
|
24678
|
+
return None
|
|
24679
|
+
|
|
24680
|
+
def exec_module(self, module):
|
|
24681
|
+
import ssl as _ssl
|
|
24682
|
+
module.SSLContext = _ssl.SSLContext
|
|
24683
|
+
|
|
24684
|
+
def inject_into_ssl():
|
|
24685
|
+
pass
|
|
24686
|
+
|
|
24687
|
+
def extract_from_ssl():
|
|
24688
|
+
pass
|
|
24689
|
+
|
|
24690
|
+
module.inject_into_ssl = inject_into_ssl
|
|
24691
|
+
module.extract_from_ssl = extract_from_ssl
|
|
24692
|
+
module.__version__ = "sandboxedjs"
|
|
24693
|
+
|
|
24694
|
+
return importlib.util.spec_from_loader(name, _Loader(), is_package=True)
|
|
24695
|
+
|
|
24696
|
+
sys.meta_path.insert(0, _SbxTruststoreHook())
|
|
24697
|
+
|
|
24698
|
+
|
|
24699
|
+
_egress = None if os.environ.get("SBX_OUTBOUND_SOCKETS") == "1" else os.environ.get("SBX_HTTP_EGRESS")
|
|
24145
24700
|
if _egress:
|
|
24146
24701
|
import http.client
|
|
24147
24702
|
import urllib.request
|
|
@@ -24539,10 +25094,86 @@ if os.environ.get("SBX_SERIAL_HOST_CALLS") == "1":
|
|
|
24539
25094
|
return spec
|
|
24540
25095
|
|
|
24541
25096
|
sys.meta_path.insert(0, _SbxAnyioThreadHook())
|
|
25097
|
+
|
|
25098
|
+
class _SbxLoopResolverHook:
|
|
25099
|
+
"""asyncio resolves names on a worker thread, which cannot run here.
|
|
25100
|
+
|
|
25101
|
+
loop.getaddrinfo hands the call to a ThreadPoolExecutor and awaits
|
|
25102
|
+
it -- the deadlock described above -- so every asyncio client that
|
|
25103
|
+
connects by name hung: aiohttp never got past its first request. In
|
|
25104
|
+
this container resolution is a host call that answers immediately, so
|
|
25105
|
+
it is done on the loop thread instead of being handed away."""
|
|
25106
|
+
|
|
25107
|
+
def find_spec(self, name, path=None, target=None):
|
|
25108
|
+
if name != "asyncio.base_events":
|
|
25109
|
+
return None
|
|
25110
|
+
import importlib.machinery
|
|
25111
|
+
spec = importlib.machinery.PathFinder.find_spec(name, path)
|
|
25112
|
+
if spec is None or spec.loader is None:
|
|
25113
|
+
return spec
|
|
25114
|
+
load = spec.loader.exec_module
|
|
25115
|
+
|
|
25116
|
+
def exec_module(module):
|
|
25117
|
+
load(module)
|
|
25118
|
+
import socket as _socket
|
|
25119
|
+
|
|
25120
|
+
async def getaddrinfo(self, host, port, *, family=0, type=0, proto=0, flags=0):
|
|
25121
|
+
return _socket.getaddrinfo(host, port, family, type, proto, flags)
|
|
25122
|
+
|
|
25123
|
+
async def getnameinfo(self, sockaddr, flags=0):
|
|
25124
|
+
return _socket.getnameinfo(sockaddr, flags)
|
|
25125
|
+
|
|
25126
|
+
module.BaseEventLoop.getaddrinfo = getaddrinfo
|
|
25127
|
+
module.BaseEventLoop.getnameinfo = getnameinfo
|
|
25128
|
+
|
|
25129
|
+
spec.loader.exec_module = exec_module
|
|
25130
|
+
return spec
|
|
25131
|
+
|
|
25132
|
+
sys.meta_path.insert(0, _SbxLoopResolverHook())
|
|
25133
|
+
|
|
25134
|
+
def _sbx_patch_loop_resolver(module):
|
|
25135
|
+
"""Also patch the module if it is already imported.
|
|
25136
|
+
|
|
25137
|
+
This file imports asyncio itself, a few lines above, so by the time the
|
|
25138
|
+
hook is installed asyncio.base_events is usually already in
|
|
25139
|
+
sys.modules and the hook would never see it -- which is why aiohttp
|
|
25140
|
+
still hung after the hook existed."""
|
|
25141
|
+
import socket as _socket
|
|
25142
|
+
|
|
25143
|
+
async def getaddrinfo(self, host, port, *, family=0, type=0, proto=0, flags=0):
|
|
25144
|
+
return _socket.getaddrinfo(host, port, family, type, proto, flags)
|
|
25145
|
+
|
|
25146
|
+
async def getnameinfo(self, sockaddr, flags=0):
|
|
25147
|
+
return _socket.getnameinfo(sockaddr, flags)
|
|
25148
|
+
|
|
25149
|
+
module.BaseEventLoop.getaddrinfo = getaddrinfo
|
|
25150
|
+
module.BaseEventLoop.getnameinfo = getnameinfo
|
|
25151
|
+
|
|
25152
|
+
_loaded_base_events = sys.modules.get("asyncio.base_events")
|
|
25153
|
+
if _loaded_base_events is not None:
|
|
25154
|
+
_sbx_patch_loop_resolver(_loaded_base_events)
|
|
24542
25155
|
`;
|
|
25156
|
+
var CA_BUNDLE_PATH = "/etc/ssl/certs/ca-certificates.crt";
|
|
25157
|
+
async function ensureTrustStore(ctx) {
|
|
25158
|
+
try {
|
|
25159
|
+
if (ctx.vfs.stat(CA_BUNDLE_PATH, { cred: ctx.cred }).size > 0) return;
|
|
25160
|
+
} catch {
|
|
25161
|
+
}
|
|
25162
|
+
try {
|
|
25163
|
+
const tls = await nodeBuiltin("tls");
|
|
25164
|
+
const bundle = tls.rootCertificates.join("\n");
|
|
25165
|
+
if (!bundle) return;
|
|
25166
|
+
ctx.vfs.mkdir("/etc/ssl/certs", { cred: ctx.cred, recursive: true });
|
|
25167
|
+
ctx.vfs.writeFile(CA_BUNDLE_PATH, bundle, { privileged: true, mode: 420 });
|
|
25168
|
+
} catch {
|
|
25169
|
+
}
|
|
25170
|
+
}
|
|
24543
25171
|
function ensureProcessShims(ctx) {
|
|
24544
|
-
|
|
24545
|
-
|
|
25172
|
+
writeShim(ctx, `${SITE_PACKAGES}/sitecustomize.py`, SHIM_SOURCE);
|
|
25173
|
+
writeShim(ctx, `${SITE_PACKAGES}/_sbx_tls.py`, TLS_MODULE_SOURCE);
|
|
25174
|
+
}
|
|
25175
|
+
function writeShim(ctx, hook, body) {
|
|
25176
|
+
const source = `${SHIM_HEADER}${body}`;
|
|
24546
25177
|
try {
|
|
24547
25178
|
const existing = ctx.vfs.readText(hook, ctx.cred);
|
|
24548
25179
|
if (existing === source || !existing.startsWith(SHIM_HEADER)) return;
|
|
@@ -24589,6 +25220,7 @@ function containerMounts(ctx) {
|
|
|
24589
25220
|
|
|
24590
25221
|
// src/python/pip-command.ts
|
|
24591
25222
|
init_binary();
|
|
25223
|
+
var RESOLVE_DEADLINE_MS = 15 * 6e4;
|
|
24592
25224
|
async function requestRemoteBuild(service, requirement, line) {
|
|
24593
25225
|
line(`Asking ${new URL(service).host} to build ${requirement}`);
|
|
24594
25226
|
try {
|
|
@@ -24791,6 +25423,14 @@ environments. A package with no usable wheel is an error, never a silent skip.`,
|
|
|
24791
25423
|
vfs: ctx.vfs,
|
|
24792
25424
|
cred: ctx.cred,
|
|
24793
25425
|
requirements,
|
|
25426
|
+
/* The resolver's own default is three minutes, which is a fine bound
|
|
25427
|
+
* for a library but far too tight for a real requirements file: a
|
|
25428
|
+
* `langchain` plus `deepagents` graph is hundreds of distributions,
|
|
25429
|
+
* each needing a metadata fetch, and the install died mid-resolution
|
|
25430
|
+
* with "exceeded its 180s budget" while still making progress. `pip`
|
|
25431
|
+
* already has a 20-minute ceiling on the whole command; this leaves
|
|
25432
|
+
* room for the download phase inside it. */
|
|
25433
|
+
deadlineMs: RESOLVE_DEADLINE_MS,
|
|
24794
25434
|
pythonVersion: pythonBackend().manifest?.pythonVersion ?? "3.13.5",
|
|
24795
25435
|
progress: {
|
|
24796
25436
|
collecting: (name) => ctx.line(`Collecting ${name}`),
|
|
@@ -24943,7 +25583,7 @@ function mountContainerFs(FS, opts) {
|
|
|
24943
25583
|
const throwErrno = (code) => {
|
|
24944
25584
|
throw new FS.ErrnoError(emErrno(code));
|
|
24945
25585
|
};
|
|
24946
|
-
const
|
|
25586
|
+
const translate2 = (e) => {
|
|
24947
25587
|
if (isSysError(e)) return throwErrno(e.code);
|
|
24948
25588
|
throw new FS.ErrnoError(emErrno("EIO"));
|
|
24949
25589
|
};
|
|
@@ -24992,7 +25632,7 @@ function mountContainerFs(FS, opts) {
|
|
|
24992
25632
|
blocks: Math.ceil(st.size / 512)
|
|
24993
25633
|
};
|
|
24994
25634
|
} catch (e) {
|
|
24995
|
-
return
|
|
25635
|
+
return translate2(e);
|
|
24996
25636
|
}
|
|
24997
25637
|
},
|
|
24998
25638
|
setattr(node2, attr) {
|
|
@@ -25008,7 +25648,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25008
25648
|
opts.onWrite?.(path);
|
|
25009
25649
|
}
|
|
25010
25650
|
} catch (e) {
|
|
25011
|
-
|
|
25651
|
+
translate2(e);
|
|
25012
25652
|
}
|
|
25013
25653
|
},
|
|
25014
25654
|
lookup(parent, name) {
|
|
@@ -25033,7 +25673,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25033
25673
|
}
|
|
25034
25674
|
opts.onWrite?.(path);
|
|
25035
25675
|
} catch (e) {
|
|
25036
|
-
|
|
25676
|
+
translate2(e);
|
|
25037
25677
|
}
|
|
25038
25678
|
return backend.createNode(parent, name, mode, dev);
|
|
25039
25679
|
},
|
|
@@ -25044,7 +25684,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25044
25684
|
vfs.rename(from, to, cred);
|
|
25045
25685
|
opts.onWrite?.(to);
|
|
25046
25686
|
} catch (e) {
|
|
25047
|
-
|
|
25687
|
+
translate2(e);
|
|
25048
25688
|
}
|
|
25049
25689
|
oldNode.name = newName;
|
|
25050
25690
|
oldNode.parent = newDir;
|
|
@@ -25055,7 +25695,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25055
25695
|
vfs.unlink(path, cred);
|
|
25056
25696
|
opts.onWrite?.(path);
|
|
25057
25697
|
} catch (e) {
|
|
25058
|
-
|
|
25698
|
+
translate2(e);
|
|
25059
25699
|
}
|
|
25060
25700
|
},
|
|
25061
25701
|
rmdir(parent, name) {
|
|
@@ -25064,7 +25704,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25064
25704
|
vfs.rmdir(path, cred);
|
|
25065
25705
|
opts.onWrite?.(path);
|
|
25066
25706
|
} catch (e) {
|
|
25067
|
-
|
|
25707
|
+
translate2(e);
|
|
25068
25708
|
}
|
|
25069
25709
|
},
|
|
25070
25710
|
readdir(node2) {
|
|
@@ -25072,7 +25712,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25072
25712
|
try {
|
|
25073
25713
|
return [".", "..", ...vfs.readdir(path, cred)];
|
|
25074
25714
|
} catch (e) {
|
|
25075
|
-
return
|
|
25715
|
+
return translate2(e);
|
|
25076
25716
|
}
|
|
25077
25717
|
},
|
|
25078
25718
|
symlink(parent, newName, oldPath) {
|
|
@@ -25081,7 +25721,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25081
25721
|
vfs.symlink(oldPath, path, cred);
|
|
25082
25722
|
opts.onWrite?.(path);
|
|
25083
25723
|
} catch (e) {
|
|
25084
|
-
|
|
25724
|
+
translate2(e);
|
|
25085
25725
|
}
|
|
25086
25726
|
},
|
|
25087
25727
|
readlink(node2) {
|
|
@@ -25089,7 +25729,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25089
25729
|
try {
|
|
25090
25730
|
return vfs.readlink(path, cred);
|
|
25091
25731
|
} catch (e) {
|
|
25092
|
-
return
|
|
25732
|
+
return translate2(e);
|
|
25093
25733
|
}
|
|
25094
25734
|
}
|
|
25095
25735
|
};
|
|
@@ -25101,7 +25741,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25101
25741
|
stream.sbxBuffer = vfs.lexists(path) ? vfs.readFile(path, cred) : new Uint8Array(0);
|
|
25102
25742
|
stream.sbxDirty = false;
|
|
25103
25743
|
} catch (e) {
|
|
25104
|
-
|
|
25744
|
+
translate2(e);
|
|
25105
25745
|
}
|
|
25106
25746
|
},
|
|
25107
25747
|
close(stream) {
|
|
@@ -25111,7 +25751,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25111
25751
|
vfs.writeFile(path, stream.sbxBuffer, { cred });
|
|
25112
25752
|
opts.onWrite?.(path);
|
|
25113
25753
|
} catch (e) {
|
|
25114
|
-
|
|
25754
|
+
translate2(e);
|
|
25115
25755
|
}
|
|
25116
25756
|
stream.sbxDirty = false;
|
|
25117
25757
|
},
|
|
@@ -25137,7 +25777,7 @@ function mountContainerFs(FS, opts) {
|
|
|
25137
25777
|
opts.onWrite?.(path);
|
|
25138
25778
|
stream.sbxDirty = false;
|
|
25139
25779
|
} catch (e) {
|
|
25140
|
-
|
|
25780
|
+
translate2(e);
|
|
25141
25781
|
}
|
|
25142
25782
|
return length;
|
|
25143
25783
|
},
|
|
@@ -25744,9 +26384,9 @@ function parsePax(data) {
|
|
|
25744
26384
|
if (space < 0) break;
|
|
25745
26385
|
const length = Number(value.slice(offset, space));
|
|
25746
26386
|
if (!Number.isFinite(length) || length <= 0) break;
|
|
25747
|
-
const
|
|
25748
|
-
const equal2 =
|
|
25749
|
-
if (equal2 > 0) result[
|
|
26387
|
+
const record2 = value.slice(space + 1, offset + length - 1);
|
|
26388
|
+
const equal2 = record2.indexOf("=");
|
|
26389
|
+
if (equal2 > 0) result[record2.slice(0, equal2)] = record2.slice(equal2 + 1);
|
|
25750
26390
|
offset += length;
|
|
25751
26391
|
}
|
|
25752
26392
|
return result;
|
|
@@ -26526,27 +27166,27 @@ var apt = defineCommand({
|
|
|
26526
27166
|
switch (subcommand) {
|
|
26527
27167
|
case "list": {
|
|
26528
27168
|
const installedOnly = rest.includes("--installed");
|
|
26529
|
-
for (const [name,
|
|
26530
|
-
const
|
|
26531
|
-
if (installedOnly && !
|
|
26532
|
-
ctx.line(`${name}/stable ${
|
|
27169
|
+
for (const [name, info2] of Object.entries(provided)) {
|
|
27170
|
+
const available2 = ctx.kernel.which(name === "python3-pip" ? "pip" : name === "gawk" ? "awk" : name, ctx.cwd, ctx.env, ctx.cred) !== null;
|
|
27171
|
+
if (installedOnly && !available2) continue;
|
|
27172
|
+
ctx.line(`${name}/stable ${info2.version} amd64${available2 ? " [installed]" : ""}`);
|
|
26533
27173
|
}
|
|
26534
27174
|
return 0;
|
|
26535
27175
|
}
|
|
26536
27176
|
case "show": {
|
|
26537
27177
|
let status = 0;
|
|
26538
27178
|
for (const name of rest) {
|
|
26539
|
-
const
|
|
26540
|
-
if (!
|
|
27179
|
+
const info2 = provided[name];
|
|
27180
|
+
if (!info2) {
|
|
26541
27181
|
ctx.warn(`E: No packages found matching ${name}`);
|
|
26542
27182
|
status = 1;
|
|
26543
27183
|
continue;
|
|
26544
27184
|
}
|
|
26545
27185
|
ctx.line(`Package: ${name}`);
|
|
26546
|
-
ctx.line(`Version: ${
|
|
27186
|
+
ctx.line(`Version: ${info2.version}`);
|
|
26547
27187
|
ctx.line(`Priority: optional`);
|
|
26548
27188
|
ctx.line(`Architecture: amd64`);
|
|
26549
|
-
ctx.line(`Description: ${
|
|
27189
|
+
ctx.line(`Description: ${info2.description}`);
|
|
26550
27190
|
ctx.line("");
|
|
26551
27191
|
}
|
|
26552
27192
|
return status;
|
|
@@ -26577,10 +27217,10 @@ var apt = defineCommand({
|
|
|
26577
27217
|
return 100;
|
|
26578
27218
|
case "search": {
|
|
26579
27219
|
const pattern = new RegExp(rest[0] ?? "", "i");
|
|
26580
|
-
for (const [name,
|
|
26581
|
-
if (pattern.test(name) || pattern.test(
|
|
26582
|
-
ctx.line(`${name}/stable ${
|
|
26583
|
-
ctx.line(` ${
|
|
27220
|
+
for (const [name, info2] of Object.entries(provided)) {
|
|
27221
|
+
if (pattern.test(name) || pattern.test(info2.description)) {
|
|
27222
|
+
ctx.line(`${name}/stable ${info2.version} amd64`);
|
|
27223
|
+
ctx.line(` ${info2.description}`);
|
|
26584
27224
|
ctx.line("");
|
|
26585
27225
|
}
|
|
26586
27226
|
}
|
|
@@ -27045,6 +27685,701 @@ function browserCommands() {
|
|
|
27045
27685
|
];
|
|
27046
27686
|
}
|
|
27047
27687
|
|
|
27688
|
+
// src/apps/pm.ts
|
|
27689
|
+
init_binary();
|
|
27690
|
+
init_zip();
|
|
27691
|
+
|
|
27692
|
+
// src/apps/registry.ts
|
|
27693
|
+
var APPS_ROOT = "/opt";
|
|
27694
|
+
var APP_BIN_DIR = "/usr/local/bin";
|
|
27695
|
+
var APP_STATE = "/var/lib/pm/installed.json";
|
|
27696
|
+
var DEFAULT_REGISTRY = "https://sandboxedjswheels.pages.dev/apps";
|
|
27697
|
+
var BUILT_IN_PUBLISHERS = [
|
|
27698
|
+
{
|
|
27699
|
+
publisher: "SandboxedJs",
|
|
27700
|
+
publicKey: "MCowBQYDK2VwAyEAuyxKMeyeO/05mr5Qu8vc+Dd9yMG2/oKDwt7KA+C9Mj4="
|
|
27701
|
+
}
|
|
27702
|
+
];
|
|
27703
|
+
var AppRegistryError = class extends Error {
|
|
27704
|
+
};
|
|
27705
|
+
function parseIndex(text2, source) {
|
|
27706
|
+
let parsed;
|
|
27707
|
+
try {
|
|
27708
|
+
parsed = JSON.parse(text2);
|
|
27709
|
+
} catch (error) {
|
|
27710
|
+
throw new AppRegistryError(`${source} is not JSON: ${error.message}`);
|
|
27711
|
+
}
|
|
27712
|
+
const index = parsed;
|
|
27713
|
+
if (index?.format !== "sandboxedjs-app-index") {
|
|
27714
|
+
throw new AppRegistryError(
|
|
27715
|
+
`${source} is not an application index (its format is ${JSON.stringify(index?.format ?? null)})`
|
|
27716
|
+
);
|
|
27717
|
+
}
|
|
27718
|
+
if (!Array.isArray(index.apps)) {
|
|
27719
|
+
throw new AppRegistryError(`${source} has no apps`);
|
|
27720
|
+
}
|
|
27721
|
+
for (const app of index.apps) {
|
|
27722
|
+
for (const field of ["name", "version", "filename", "sha256"]) {
|
|
27723
|
+
if (typeof app[field] !== "string" || !app[field]) {
|
|
27724
|
+
throw new AppRegistryError(`${source}: an entry is missing ${field}`);
|
|
27725
|
+
}
|
|
27726
|
+
}
|
|
27727
|
+
}
|
|
27728
|
+
return index;
|
|
27729
|
+
}
|
|
27730
|
+
function selectRelease(index, name, version) {
|
|
27731
|
+
const matches3 = index.apps.filter((app) => app.name === name);
|
|
27732
|
+
if (matches3.length === 0) {
|
|
27733
|
+
const names = index.apps.map((app) => app.name).sort();
|
|
27734
|
+
throw new AppRegistryError(
|
|
27735
|
+
`no application named ${name}. The registry has: ${names.join(", ") || "nothing"}`
|
|
27736
|
+
);
|
|
27737
|
+
}
|
|
27738
|
+
if (version) {
|
|
27739
|
+
const exact = matches3.find((app) => app.version === version);
|
|
27740
|
+
if (!exact) {
|
|
27741
|
+
const versions = matches3.map((app) => app.version).join(", ");
|
|
27742
|
+
throw new AppRegistryError(`${name} has no version ${version}; it has ${versions}`);
|
|
27743
|
+
}
|
|
27744
|
+
return exact;
|
|
27745
|
+
}
|
|
27746
|
+
return matches3.slice().sort((a, b) => compareVersions2(b.version, a.version))[0];
|
|
27747
|
+
}
|
|
27748
|
+
function compareVersions2(a, b) {
|
|
27749
|
+
const left = a.split(/[.-]/);
|
|
27750
|
+
const right = b.split(/[.-]/);
|
|
27751
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
27752
|
+
const x = left[index] ?? "";
|
|
27753
|
+
const y = right[index] ?? "";
|
|
27754
|
+
const nx = Number(x);
|
|
27755
|
+
const ny = Number(y);
|
|
27756
|
+
if (Number.isFinite(nx) && Number.isFinite(ny) && x !== "" && y !== "") {
|
|
27757
|
+
if (nx !== ny) return nx - ny;
|
|
27758
|
+
continue;
|
|
27759
|
+
}
|
|
27760
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
27761
|
+
}
|
|
27762
|
+
return 0;
|
|
27763
|
+
}
|
|
27764
|
+
function parseSpecifier(specifier) {
|
|
27765
|
+
const at = specifier.lastIndexOf("@");
|
|
27766
|
+
if (at <= 0) return { name: specifier };
|
|
27767
|
+
return { name: specifier.slice(0, at), version: specifier.slice(at + 1) };
|
|
27768
|
+
}
|
|
27769
|
+
function validateManifest2(manifest, source) {
|
|
27770
|
+
const app = manifest;
|
|
27771
|
+
if (typeof app?.name !== "string" || typeof app?.version !== "string") {
|
|
27772
|
+
throw new AppRegistryError(`${source} is not an application manifest`);
|
|
27773
|
+
}
|
|
27774
|
+
for (const binary of app.bins ?? []) {
|
|
27775
|
+
if (typeof binary?.name !== "string" || typeof binary?.path !== "string") {
|
|
27776
|
+
throw new AppRegistryError(`${source}: a bins entry needs a name and a path`);
|
|
27777
|
+
}
|
|
27778
|
+
if (binary.name.includes("/")) {
|
|
27779
|
+
throw new AppRegistryError(`${source}: a command name cannot contain a slash`);
|
|
27780
|
+
}
|
|
27781
|
+
}
|
|
27782
|
+
for (const service of app.services ?? []) {
|
|
27783
|
+
if (typeof service?.port !== "number" || !Number.isInteger(service.port)) {
|
|
27784
|
+
throw new AppRegistryError(`${source}: a service needs an integer port`);
|
|
27785
|
+
}
|
|
27786
|
+
}
|
|
27787
|
+
return app;
|
|
27788
|
+
}
|
|
27789
|
+
|
|
27790
|
+
// src/apps/sbjs.ts
|
|
27791
|
+
init_binary();
|
|
27792
|
+
init_zip();
|
|
27793
|
+
var MANIFEST_PATH = "META/manifest.json";
|
|
27794
|
+
var SIGNATURE_PATH = "META/signature.json";
|
|
27795
|
+
var TRUSTED_KEYS_PATH = "/etc/pm/trusted-keys.json";
|
|
27796
|
+
var PackageError = class extends Error {
|
|
27797
|
+
};
|
|
27798
|
+
async function openPackage(bytes2, trusted) {
|
|
27799
|
+
const entries = readZip(bytes2);
|
|
27800
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
27801
|
+
const manifestEntry = byName.get(MANIFEST_PATH);
|
|
27802
|
+
if (!manifestEntry) {
|
|
27803
|
+
throw new PackageError(
|
|
27804
|
+
`not a SandboxedJs package: it has no ${MANIFEST_PATH}. A plain zip can be installed with --allow-unsigned, but it carries no publisher.`
|
|
27805
|
+
);
|
|
27806
|
+
}
|
|
27807
|
+
const manifestBytes = manifestEntry.data();
|
|
27808
|
+
const manifest = parseManifest(manifestBytes);
|
|
27809
|
+
validateManifest2(manifest.app, `${MANIFEST_PATH}'s app`);
|
|
27810
|
+
for (const entry of entries) {
|
|
27811
|
+
if (entry.isDirectory || entry.name.startsWith("META/")) continue;
|
|
27812
|
+
const expected = manifest.files[entry.name];
|
|
27813
|
+
if (!expected) {
|
|
27814
|
+
throw new PackageError(`${entry.name} is in the package but not in its manifest`);
|
|
27815
|
+
}
|
|
27816
|
+
const actual = await digestHex("sha256", entry.data());
|
|
27817
|
+
if (actual !== expected) {
|
|
27818
|
+
throw new PackageError(
|
|
27819
|
+
`${entry.name} does not match the manifest (expected ${expected}, found ${actual})`
|
|
27820
|
+
);
|
|
27821
|
+
}
|
|
27822
|
+
}
|
|
27823
|
+
for (const name of Object.keys(manifest.files)) {
|
|
27824
|
+
if (!byName.has(name)) {
|
|
27825
|
+
throw new PackageError(`${name} is in the manifest but missing from the package`);
|
|
27826
|
+
}
|
|
27827
|
+
}
|
|
27828
|
+
const signatureEntry = byName.get(SIGNATURE_PATH);
|
|
27829
|
+
if (!signatureEntry) {
|
|
27830
|
+
return { manifest, signature: null, entries, trustedBy: null };
|
|
27831
|
+
}
|
|
27832
|
+
const signature = parseSignature(signatureEntry.data());
|
|
27833
|
+
const valid = await verifySignature(manifestBytes, signature);
|
|
27834
|
+
if (!valid) {
|
|
27835
|
+
throw new PackageError(
|
|
27836
|
+
`the signature on ${manifest.app.name} does not match its manifest; the package has been altered since it was signed`
|
|
27837
|
+
);
|
|
27838
|
+
}
|
|
27839
|
+
const trustedBy = trusted.find((key) => key.publicKey === signature.publicKey) ?? null;
|
|
27840
|
+
return { manifest, signature, entries, trustedBy };
|
|
27841
|
+
}
|
|
27842
|
+
function parseManifest(bytes2) {
|
|
27843
|
+
let parsed;
|
|
27844
|
+
try {
|
|
27845
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes2));
|
|
27846
|
+
} catch (error) {
|
|
27847
|
+
throw new PackageError(`${MANIFEST_PATH} is not JSON: ${error.message}`);
|
|
27848
|
+
}
|
|
27849
|
+
const manifest = parsed;
|
|
27850
|
+
if (manifest?.format !== "sandboxedjs-package") {
|
|
27851
|
+
throw new PackageError(`${MANIFEST_PATH} is not a package manifest`);
|
|
27852
|
+
}
|
|
27853
|
+
if (!manifest.files || typeof manifest.files !== "object") {
|
|
27854
|
+
throw new PackageError(`${MANIFEST_PATH} lists no files`);
|
|
27855
|
+
}
|
|
27856
|
+
if (!manifest.app) {
|
|
27857
|
+
throw new PackageError(`${MANIFEST_PATH} carries no application manifest`);
|
|
27858
|
+
}
|
|
27859
|
+
return manifest;
|
|
27860
|
+
}
|
|
27861
|
+
function parseSignature(bytes2) {
|
|
27862
|
+
let parsed;
|
|
27863
|
+
try {
|
|
27864
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes2));
|
|
27865
|
+
} catch (error) {
|
|
27866
|
+
throw new PackageError(`${SIGNATURE_PATH} is not JSON: ${error.message}`);
|
|
27867
|
+
}
|
|
27868
|
+
const signature = parsed;
|
|
27869
|
+
if (signature?.format !== "sandboxedjs-signature") {
|
|
27870
|
+
throw new PackageError(`${SIGNATURE_PATH} is not a signature`);
|
|
27871
|
+
}
|
|
27872
|
+
if (signature.algorithm !== "ed25519") {
|
|
27873
|
+
throw new PackageError(
|
|
27874
|
+
`${signature.algorithm} signatures are not supported by this runtime; only ed25519 is`
|
|
27875
|
+
);
|
|
27876
|
+
}
|
|
27877
|
+
for (const field of ["publicKey", "signature"]) {
|
|
27878
|
+
if (typeof signature[field] !== "string" || !signature[field]) {
|
|
27879
|
+
throw new PackageError(`${SIGNATURE_PATH} has no ${field}`);
|
|
27880
|
+
}
|
|
27881
|
+
}
|
|
27882
|
+
return signature;
|
|
27883
|
+
}
|
|
27884
|
+
async function verifySignature(manifestBytes, signature) {
|
|
27885
|
+
const subtle = globalThis.crypto?.subtle;
|
|
27886
|
+
if (!subtle) {
|
|
27887
|
+
throw new PackageError(
|
|
27888
|
+
"this runtime has no WebCrypto, so a package signature cannot be checked"
|
|
27889
|
+
);
|
|
27890
|
+
}
|
|
27891
|
+
let key;
|
|
27892
|
+
try {
|
|
27893
|
+
key = await subtle.importKey(
|
|
27894
|
+
"spki",
|
|
27895
|
+
intoBuffer(base64ToBytes(signature.publicKey)),
|
|
27896
|
+
{ name: "Ed25519" },
|
|
27897
|
+
false,
|
|
27898
|
+
["verify"]
|
|
27899
|
+
);
|
|
27900
|
+
} catch (error) {
|
|
27901
|
+
throw new PackageError(
|
|
27902
|
+
`cannot use the package's public key: ${error.message}. Ed25519 needs Node 20 or a browser that implements it.`
|
|
27903
|
+
);
|
|
27904
|
+
}
|
|
27905
|
+
return subtle.verify(
|
|
27906
|
+
{ name: "Ed25519" },
|
|
27907
|
+
key,
|
|
27908
|
+
intoBuffer(base64ToBytes(signature.signature)),
|
|
27909
|
+
intoBuffer(manifestBytes)
|
|
27910
|
+
);
|
|
27911
|
+
}
|
|
27912
|
+
function intoBuffer(bytes2) {
|
|
27913
|
+
return bytes2.slice().buffer;
|
|
27914
|
+
}
|
|
27915
|
+
function base64ToBytes(text2) {
|
|
27916
|
+
const binary = atob(text2);
|
|
27917
|
+
const bytes2 = new Uint8Array(binary.length);
|
|
27918
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
27919
|
+
bytes2[index] = binary.charCodeAt(index);
|
|
27920
|
+
}
|
|
27921
|
+
return bytes2;
|
|
27922
|
+
}
|
|
27923
|
+
async function fingerprint(publicKey) {
|
|
27924
|
+
const digest2 = await digestHex("sha256", base64ToBytes(publicKey));
|
|
27925
|
+
return digest2.slice(0, 32).replace(/(.{4})(?=.)/g, "$1-");
|
|
27926
|
+
}
|
|
27927
|
+
function asRegistryError(error) {
|
|
27928
|
+
return new AppRegistryError(error.message);
|
|
27929
|
+
}
|
|
27930
|
+
|
|
27931
|
+
// src/apps/pm.ts
|
|
27932
|
+
function registryBase(ctx) {
|
|
27933
|
+
const configured = ctx.env.SBX_PM_REGISTRY ?? ctx.env.PM_REGISTRY;
|
|
27934
|
+
return configured && configured.trim() || DEFAULT_REGISTRY;
|
|
27935
|
+
}
|
|
27936
|
+
var pm = defineCommand({
|
|
27937
|
+
name: "pm",
|
|
27938
|
+
path: "/usr/bin/pm",
|
|
27939
|
+
summary: "install and manage applications",
|
|
27940
|
+
usage: "pm <install|remove|list|info|search|available> [name[@version]]",
|
|
27941
|
+
manual: `Applications are programs the container runs, packaged with what
|
|
27942
|
+
they need. They are not Python or Node packages: use pip or npm for those.
|
|
27943
|
+
|
|
27944
|
+
pm install NAME[@VERSION] download, verify and install
|
|
27945
|
+
pm remove NAME remove it and the commands it installed
|
|
27946
|
+
pm list what is installed here
|
|
27947
|
+
pm info NAME what a package provides
|
|
27948
|
+
pm search TEXT search the registry
|
|
27949
|
+
pm available everything the registry offers
|
|
27950
|
+
pm trust list|add|remove publisher keys this container trusts
|
|
27951
|
+
|
|
27952
|
+
Packages come from ${DEFAULT_REGISTRY} unless SBX_PM_REGISTRY names another.
|
|
27953
|
+
|
|
27954
|
+
A package is a .sbjs file: a zip carrying META/manifest.json, which lists a
|
|
27955
|
+
sha256 for every file in it, and META/signature.json, an Ed25519 signature
|
|
27956
|
+
over that manifest. Installing checks both, and refuses a package signed by a
|
|
27957
|
+
key this container does not trust. --allow-untrusted and --allow-unsigned
|
|
27958
|
+
override that, one install at a time.
|
|
27959
|
+
|
|
27960
|
+
A signature says who published a package and that nothing in it has changed
|
|
27961
|
+
since. It does not say the program is safe to run.`,
|
|
27962
|
+
async run(ctx) {
|
|
27963
|
+
const [subcommand, ...rest] = ctx.args;
|
|
27964
|
+
try {
|
|
27965
|
+
switch (subcommand) {
|
|
27966
|
+
case "install":
|
|
27967
|
+
return await install2(ctx, rest);
|
|
27968
|
+
case "remove":
|
|
27969
|
+
case "uninstall":
|
|
27970
|
+
return await remove(ctx, rest);
|
|
27971
|
+
case "list":
|
|
27972
|
+
return listInstalled2(ctx);
|
|
27973
|
+
case "info":
|
|
27974
|
+
return await info(ctx, rest);
|
|
27975
|
+
case "search":
|
|
27976
|
+
return await search(ctx, rest);
|
|
27977
|
+
case "available":
|
|
27978
|
+
return await available(ctx);
|
|
27979
|
+
case "trust":
|
|
27980
|
+
return await trust(ctx, rest);
|
|
27981
|
+
case void 0:
|
|
27982
|
+
case "--help":
|
|
27983
|
+
case "-h":
|
|
27984
|
+
ctx.line(pm.usage);
|
|
27985
|
+
return 0;
|
|
27986
|
+
default:
|
|
27987
|
+
ctx.warn(`pm: unknown command ${subcommand}`);
|
|
27988
|
+
return 2;
|
|
27989
|
+
}
|
|
27990
|
+
} catch (error) {
|
|
27991
|
+
ctx.warn(`pm: ${error.message}`);
|
|
27992
|
+
return error instanceof AppRegistryError ? 2 : 1;
|
|
27993
|
+
}
|
|
27994
|
+
}
|
|
27995
|
+
});
|
|
27996
|
+
async function install2(ctx, args) {
|
|
27997
|
+
if (args.length === 0) {
|
|
27998
|
+
ctx.warn("pm install: name it, as `pm install officesuite`");
|
|
27999
|
+
return 2;
|
|
28000
|
+
}
|
|
28001
|
+
const options = {
|
|
28002
|
+
allowUnsigned: args.includes("--allow-unsigned"),
|
|
28003
|
+
allowUntrusted: args.includes("--allow-untrusted") || args.includes("--allow-unsigned")
|
|
28004
|
+
};
|
|
28005
|
+
const names = args.filter((argument) => !argument.startsWith("--"));
|
|
28006
|
+
if (names.length === 0) {
|
|
28007
|
+
ctx.warn("pm install: name it, as `pm install officesuite`");
|
|
28008
|
+
return 2;
|
|
28009
|
+
}
|
|
28010
|
+
const index = await fetchIndex(ctx);
|
|
28011
|
+
for (const specifier of names) {
|
|
28012
|
+
const { name, version } = parseSpecifier(specifier);
|
|
28013
|
+
const release = selectRelease(index, name, version);
|
|
28014
|
+
await installRelease(ctx, index, release, /* @__PURE__ */ new Set(), options);
|
|
28015
|
+
}
|
|
28016
|
+
return 0;
|
|
28017
|
+
}
|
|
28018
|
+
async function installRelease(ctx, index, release, seen, options) {
|
|
28019
|
+
if (seen.has(release.name)) return;
|
|
28020
|
+
seen.add(release.name);
|
|
28021
|
+
for (const required of release.requires ?? []) {
|
|
28022
|
+
const { name, version } = parseSpecifier(required);
|
|
28023
|
+
if (installedApps(ctx).some((app) => app.name === name)) continue;
|
|
28024
|
+
await installRelease(ctx, index, selectRelease(index, name, version), seen, options);
|
|
28025
|
+
}
|
|
28026
|
+
const base2 = registryBase(ctx).replace(/\/$/, "");
|
|
28027
|
+
const url = `${base2}/${release.filename}`;
|
|
28028
|
+
ctx.line(`Fetching ${release.name} ${release.version}`);
|
|
28029
|
+
const archive = await download(url);
|
|
28030
|
+
const digest2 = await digestHex("sha256", archive);
|
|
28031
|
+
if (digest2 !== release.sha256) {
|
|
28032
|
+
throw new AppRegistryError(
|
|
28033
|
+
`${release.filename} failed verification: the registry publishes sha256 ${release.sha256} but the download hashes to ${digest2}`
|
|
28034
|
+
);
|
|
28035
|
+
}
|
|
28036
|
+
const verified = await verify(ctx, archive, release, options);
|
|
28037
|
+
const root = `${APPS_ROOT}/${release.name}`;
|
|
28038
|
+
removeInstalled(ctx, release.name, { quiet: true });
|
|
28039
|
+
const manifest = unpack(ctx, archive, root, release, verified);
|
|
28040
|
+
const bins = linkBinaries(ctx, manifest, root);
|
|
28041
|
+
record(ctx, {
|
|
28042
|
+
...manifest,
|
|
28043
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28044
|
+
root,
|
|
28045
|
+
installedBins: bins,
|
|
28046
|
+
sha256: release.sha256,
|
|
28047
|
+
...verified?.signature ? {
|
|
28048
|
+
publisher: verified.signature.publisher ?? verified.trustedBy?.publisher ?? "unknown",
|
|
28049
|
+
publicKey: verified.signature.publicKey
|
|
28050
|
+
} : {}
|
|
28051
|
+
});
|
|
28052
|
+
ctx.line(`Installed ${manifest.name} ${manifest.version} in ${root}`);
|
|
28053
|
+
if (bins.length > 0) ctx.line(` commands: ${bins.join(", ")}`);
|
|
28054
|
+
for (const service of manifest.services ?? []) {
|
|
28055
|
+
ctx.line(
|
|
28056
|
+
` service: ${service.name} on port ${service.port}` + (service.path ? ` at ${service.path}` : "") + ` (start it with \`${service.command}\`)`
|
|
28057
|
+
);
|
|
28058
|
+
}
|
|
28059
|
+
if (manifest.docs) ctx.line(` docs: ${root}/${manifest.docs}`);
|
|
28060
|
+
}
|
|
28061
|
+
async function download(url) {
|
|
28062
|
+
const response = await fetch(url);
|
|
28063
|
+
if (!response.ok) {
|
|
28064
|
+
throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
|
|
28065
|
+
}
|
|
28066
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
28067
|
+
}
|
|
28068
|
+
async function verify(ctx, archive, release, options) {
|
|
28069
|
+
let opened;
|
|
28070
|
+
try {
|
|
28071
|
+
opened = await openPackage(archive, trustedKeys(ctx));
|
|
28072
|
+
} catch (error) {
|
|
28073
|
+
if (options.allowUnsigned && /has no META\/manifest\.json/.test(error.message)) {
|
|
28074
|
+
ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
|
|
28075
|
+
return null;
|
|
28076
|
+
}
|
|
28077
|
+
throw asRegistryError(error);
|
|
28078
|
+
}
|
|
28079
|
+
if (!opened.signature) {
|
|
28080
|
+
if (!options.allowUnsigned) {
|
|
28081
|
+
throw new AppRegistryError(
|
|
28082
|
+
`${release.name} carries no signature, so its publisher cannot be established. Install it with --allow-unsigned if you got it from somewhere you trust.`
|
|
28083
|
+
);
|
|
28084
|
+
}
|
|
28085
|
+
ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
|
|
28086
|
+
return opened;
|
|
28087
|
+
}
|
|
28088
|
+
const print = await fingerprint(opened.signature.publicKey);
|
|
28089
|
+
if (!opened.trustedBy && !options.allowUntrusted) {
|
|
28090
|
+
throw new AppRegistryError(
|
|
28091
|
+
`${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.`
|
|
28092
|
+
);
|
|
28093
|
+
}
|
|
28094
|
+
ctx.line(
|
|
28095
|
+
opened.trustedBy ? ` signed by ${opened.trustedBy.publisher} (${print})` : ` signed by an untrusted key (${opened.signature.publisher ?? "unnamed"}, ${print})`
|
|
28096
|
+
);
|
|
28097
|
+
return opened;
|
|
28098
|
+
}
|
|
28099
|
+
function trustedKeys(ctx) {
|
|
28100
|
+
let stored = [];
|
|
28101
|
+
try {
|
|
28102
|
+
const parsed = JSON.parse(ctx.vfs.readText(TRUSTED_KEYS_PATH, ctx.cred));
|
|
28103
|
+
stored = Array.isArray(parsed.keys) ? parsed.keys : [];
|
|
28104
|
+
} catch {
|
|
28105
|
+
}
|
|
28106
|
+
const known = new Set(stored.map((key) => key.publicKey));
|
|
28107
|
+
return [...stored, ...BUILT_IN_PUBLISHERS.filter((key) => !known.has(key.publicKey))];
|
|
28108
|
+
}
|
|
28109
|
+
function writeTrustedKeys(ctx, keys) {
|
|
28110
|
+
ctx.vfs.mkdir("/etc/pm", { cred: ctx.cred, recursive: true });
|
|
28111
|
+
ctx.vfs.writeFile(
|
|
28112
|
+
TRUSTED_KEYS_PATH,
|
|
28113
|
+
`${JSON.stringify({ format: "sandboxedjs-trusted-keys", keys }, null, 2)}
|
|
28114
|
+
`,
|
|
28115
|
+
{ cred: ctx.cred, mode: 420 }
|
|
28116
|
+
);
|
|
28117
|
+
}
|
|
28118
|
+
async function trust(ctx, args) {
|
|
28119
|
+
const [action, ...rest] = args;
|
|
28120
|
+
const keys = trustedKeys(ctx);
|
|
28121
|
+
switch (action) {
|
|
28122
|
+
case "list":
|
|
28123
|
+
case void 0: {
|
|
28124
|
+
if (keys.length === 0) {
|
|
28125
|
+
ctx.line("No publisher keys are trusted; only unsigned installs are possible.");
|
|
28126
|
+
return 0;
|
|
28127
|
+
}
|
|
28128
|
+
for (const key of keys) {
|
|
28129
|
+
ctx.line(`${key.publisher} ${await fingerprint(key.publicKey)}`);
|
|
28130
|
+
}
|
|
28131
|
+
return 0;
|
|
28132
|
+
}
|
|
28133
|
+
case "add": {
|
|
28134
|
+
const [publisher, publicKey] = rest;
|
|
28135
|
+
if (!publisher || !publicKey) {
|
|
28136
|
+
ctx.warn("pm trust add: give a publisher name and its base64 public key");
|
|
28137
|
+
return 2;
|
|
28138
|
+
}
|
|
28139
|
+
const next = keys.filter((key) => key.publicKey !== publicKey);
|
|
28140
|
+
next.push({ publisher, publicKey, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
28141
|
+
writeTrustedKeys(ctx, next);
|
|
28142
|
+
ctx.line(`Trusting ${publisher} (${await fingerprint(publicKey)})`);
|
|
28143
|
+
return 0;
|
|
28144
|
+
}
|
|
28145
|
+
case "remove": {
|
|
28146
|
+
const [publisher] = rest;
|
|
28147
|
+
const next = keys.filter((key) => key.publisher !== publisher);
|
|
28148
|
+
if (next.length === keys.length) {
|
|
28149
|
+
ctx.warn(`pm: no trusted key named ${publisher}`);
|
|
28150
|
+
return 1;
|
|
28151
|
+
}
|
|
28152
|
+
writeTrustedKeys(ctx, next);
|
|
28153
|
+
ctx.line(`Removed ${publisher}`);
|
|
28154
|
+
return 0;
|
|
28155
|
+
}
|
|
28156
|
+
default:
|
|
28157
|
+
ctx.warn(`pm trust: unknown action ${action}`);
|
|
28158
|
+
return 2;
|
|
28159
|
+
}
|
|
28160
|
+
}
|
|
28161
|
+
function unpack(ctx, archive, root, release, verified) {
|
|
28162
|
+
const entries = verified?.entries ?? readZip(archive);
|
|
28163
|
+
const manifest = verified?.manifest.app ?? readAppManifest(entries, release);
|
|
28164
|
+
if (manifest.name !== release.name || manifest.version !== release.version) {
|
|
28165
|
+
throw new AppRegistryError(
|
|
28166
|
+
`${release.filename} contains ${manifest.name} ${manifest.version}, but the registry lists it as ${release.name} ${release.version}`
|
|
28167
|
+
);
|
|
28168
|
+
}
|
|
28169
|
+
ctx.vfs.mkdir(root, { cred: ctx.cred, recursive: true });
|
|
28170
|
+
for (const entry of entries) {
|
|
28171
|
+
if (entry.name.startsWith("META/")) continue;
|
|
28172
|
+
const target = resolveInside(root, entry.name);
|
|
28173
|
+
if (entry.isDirectory) {
|
|
28174
|
+
ctx.vfs.mkdir(target, { cred: ctx.cred, recursive: true });
|
|
28175
|
+
continue;
|
|
28176
|
+
}
|
|
28177
|
+
const parent = target.slice(0, target.lastIndexOf("/"));
|
|
28178
|
+
if (parent) ctx.vfs.mkdir(parent, { cred: ctx.cred, recursive: true });
|
|
28179
|
+
const executable = (entry.mode & 73) !== 0 || (manifest.bins ?? []).some((binary) => binary.path === entry.name);
|
|
28180
|
+
ctx.vfs.writeFile(target, entry.data(), {
|
|
28181
|
+
cred: ctx.cred,
|
|
28182
|
+
mode: executable ? 493 : 420
|
|
28183
|
+
});
|
|
28184
|
+
}
|
|
28185
|
+
return manifest;
|
|
28186
|
+
}
|
|
28187
|
+
function readAppManifest(entries, release) {
|
|
28188
|
+
const entry = entries.find((candidate) => candidate.name === "app.json");
|
|
28189
|
+
if (!entry) throw new AppRegistryError(`${release.filename} has no app.json`);
|
|
28190
|
+
return validateManifest2(
|
|
28191
|
+
JSON.parse(new TextDecoder().decode(entry.data())),
|
|
28192
|
+
`${release.name}'s app.json`
|
|
28193
|
+
);
|
|
28194
|
+
}
|
|
28195
|
+
function resolveInside(root, name) {
|
|
28196
|
+
const clean2 = name.replace(/\\/g, "/");
|
|
28197
|
+
if (clean2.startsWith("/")) {
|
|
28198
|
+
throw new AppRegistryError(`archive entry ${name} is an absolute path`);
|
|
28199
|
+
}
|
|
28200
|
+
const parts = [];
|
|
28201
|
+
for (const part of clean2.split("/")) {
|
|
28202
|
+
if (part === "" || part === ".") continue;
|
|
28203
|
+
if (part === "..") {
|
|
28204
|
+
throw new AppRegistryError(`archive entry ${name} escapes the package directory`);
|
|
28205
|
+
}
|
|
28206
|
+
parts.push(part);
|
|
28207
|
+
}
|
|
28208
|
+
return `${root}/${parts.join("/")}`;
|
|
28209
|
+
}
|
|
28210
|
+
function linkBinaries(ctx, manifest, root) {
|
|
28211
|
+
const installed3 = [];
|
|
28212
|
+
if ((manifest.bins ?? []).length === 0) return installed3;
|
|
28213
|
+
ctx.vfs.mkdir(APP_BIN_DIR, { cred: ctx.cred, recursive: true });
|
|
28214
|
+
for (const binary of manifest.bins ?? []) {
|
|
28215
|
+
const target = resolveInside(root, binary.path);
|
|
28216
|
+
const wrapper = `${APP_BIN_DIR}/${binary.name}`;
|
|
28217
|
+
ctx.vfs.writeFile(
|
|
28218
|
+
wrapper,
|
|
28219
|
+
`#!/bin/sh
|
|
28220
|
+
# installed by pm for ${manifest.name} ${manifest.version}
|
|
28221
|
+
exec ${target} "$@"
|
|
28222
|
+
`,
|
|
28223
|
+
{ cred: ctx.cred, mode: 493 }
|
|
28224
|
+
);
|
|
28225
|
+
installed3.push(binary.name);
|
|
28226
|
+
}
|
|
28227
|
+
return installed3;
|
|
28228
|
+
}
|
|
28229
|
+
async function remove(ctx, args) {
|
|
28230
|
+
if (args.length === 0) {
|
|
28231
|
+
ctx.warn("pm remove: name it");
|
|
28232
|
+
return 2;
|
|
28233
|
+
}
|
|
28234
|
+
let code = 0;
|
|
28235
|
+
for (const specifier of args) {
|
|
28236
|
+
const { name } = parseSpecifier(specifier);
|
|
28237
|
+
if (!removeInstalled(ctx, name, { quiet: false })) {
|
|
28238
|
+
ctx.warn(`pm: ${name} is not installed`);
|
|
28239
|
+
code = 1;
|
|
28240
|
+
} else {
|
|
28241
|
+
ctx.line(`Removed ${name}`);
|
|
28242
|
+
}
|
|
28243
|
+
}
|
|
28244
|
+
return code;
|
|
28245
|
+
}
|
|
28246
|
+
function removeInstalled(ctx, name, options) {
|
|
28247
|
+
const apps = installedApps(ctx);
|
|
28248
|
+
const app = apps.find((entry) => entry.name === name);
|
|
28249
|
+
if (!app) return false;
|
|
28250
|
+
for (const binary of app.installedBins) {
|
|
28251
|
+
try {
|
|
28252
|
+
ctx.vfs.unlink(`${APP_BIN_DIR}/${binary}`, ctx.cred);
|
|
28253
|
+
} catch {
|
|
28254
|
+
}
|
|
28255
|
+
}
|
|
28256
|
+
try {
|
|
28257
|
+
removeTree(ctx, app.root);
|
|
28258
|
+
} catch (error) {
|
|
28259
|
+
if (!options.quiet) ctx.warn(`pm: could not remove ${app.root}: ${error.message}`);
|
|
28260
|
+
}
|
|
28261
|
+
writeState(ctx, apps.filter((entry) => entry.name !== name));
|
|
28262
|
+
return true;
|
|
28263
|
+
}
|
|
28264
|
+
function removeTree(ctx, path) {
|
|
28265
|
+
let stats;
|
|
28266
|
+
try {
|
|
28267
|
+
stats = ctx.vfs.stat(path, { cred: ctx.cred });
|
|
28268
|
+
} catch {
|
|
28269
|
+
return;
|
|
28270
|
+
}
|
|
28271
|
+
if (stats.isDirectory()) {
|
|
28272
|
+
for (const entry of ctx.vfs.readdir(path, ctx.cred)) {
|
|
28273
|
+
removeTree(ctx, `${path}/${entry}`);
|
|
28274
|
+
}
|
|
28275
|
+
ctx.vfs.rmdir(path, ctx.cred);
|
|
28276
|
+
return;
|
|
28277
|
+
}
|
|
28278
|
+
ctx.vfs.unlink(path, ctx.cred);
|
|
28279
|
+
}
|
|
28280
|
+
function listInstalled2(ctx) {
|
|
28281
|
+
const apps = installedApps(ctx);
|
|
28282
|
+
if (apps.length === 0) {
|
|
28283
|
+
ctx.line("No applications installed. `pm available` lists what there is.");
|
|
28284
|
+
return 0;
|
|
28285
|
+
}
|
|
28286
|
+
for (const app of apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
28287
|
+
const commands12 = app.installedBins.length > 0 ? ` (${app.installedBins.join(", ")})` : "";
|
|
28288
|
+
ctx.line(`${app.name} ${app.version}${commands12}`);
|
|
28289
|
+
}
|
|
28290
|
+
return 0;
|
|
28291
|
+
}
|
|
28292
|
+
async function info(ctx, args) {
|
|
28293
|
+
const name = args[0];
|
|
28294
|
+
if (!name) {
|
|
28295
|
+
ctx.warn("pm info: name it");
|
|
28296
|
+
return 2;
|
|
28297
|
+
}
|
|
28298
|
+
const local = installedApps(ctx).find((app) => app.name === name);
|
|
28299
|
+
if (local) {
|
|
28300
|
+
ctx.line(`${local.name} ${local.version} (installed ${local.installedAt})`);
|
|
28301
|
+
if (local.summary) ctx.line(` ${local.summary}`);
|
|
28302
|
+
ctx.line(` root: ${local.root}`);
|
|
28303
|
+
if (local.installedBins.length > 0) ctx.line(` commands: ${local.installedBins.join(", ")}`);
|
|
28304
|
+
for (const service of local.services ?? []) {
|
|
28305
|
+
ctx.line(` service: ${service.name} on port ${service.port}${service.path ?? ""}`);
|
|
28306
|
+
}
|
|
28307
|
+
if (local.docs) ctx.line(` docs: ${local.root}/${local.docs}`);
|
|
28308
|
+
return 0;
|
|
28309
|
+
}
|
|
28310
|
+
const release = selectRelease(await fetchIndex(ctx), name);
|
|
28311
|
+
ctx.line(`${release.name} ${release.version} (not installed)`);
|
|
28312
|
+
if (release.summary) ctx.line(` ${release.summary}`);
|
|
28313
|
+
ctx.line(` size: ${Math.round(release.size / 1024)} kB`);
|
|
28314
|
+
if ((release.bins ?? []).length > 0) {
|
|
28315
|
+
ctx.line(` commands: ${(release.bins ?? []).map((b) => b.name).join(", ")}`);
|
|
28316
|
+
}
|
|
28317
|
+
return 0;
|
|
28318
|
+
}
|
|
28319
|
+
async function search(ctx, args) {
|
|
28320
|
+
const term = (args[0] ?? "").toLowerCase();
|
|
28321
|
+
const index = await fetchIndex(ctx);
|
|
28322
|
+
const matches3 = index.apps.filter(
|
|
28323
|
+
(app) => app.name.toLowerCase().includes(term) || (app.summary ?? "").toLowerCase().includes(term)
|
|
28324
|
+
);
|
|
28325
|
+
if (matches3.length === 0) {
|
|
28326
|
+
ctx.line(`nothing matches ${JSON.stringify(args[0] ?? "")}`);
|
|
28327
|
+
return 1;
|
|
28328
|
+
}
|
|
28329
|
+
for (const app of matches3) ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
|
|
28330
|
+
return 0;
|
|
28331
|
+
}
|
|
28332
|
+
async function available(ctx) {
|
|
28333
|
+
const index = await fetchIndex(ctx);
|
|
28334
|
+
for (const app of index.apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
28335
|
+
ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
|
|
28336
|
+
}
|
|
28337
|
+
return 0;
|
|
28338
|
+
}
|
|
28339
|
+
async function fetchIndex(ctx) {
|
|
28340
|
+
const base2 = registryBase(ctx).replace(/\/$/, "");
|
|
28341
|
+
const url = `${base2}/index.json`;
|
|
28342
|
+
let response;
|
|
28343
|
+
try {
|
|
28344
|
+
response = await fetch(url);
|
|
28345
|
+
} catch (error) {
|
|
28346
|
+
throw new AppRegistryError(
|
|
28347
|
+
`cannot reach the application registry at ${url}: ${error.message}`
|
|
28348
|
+
);
|
|
28349
|
+
}
|
|
28350
|
+
if (!response.ok) {
|
|
28351
|
+
throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
|
|
28352
|
+
}
|
|
28353
|
+
return parseIndex(await response.text(), url);
|
|
28354
|
+
}
|
|
28355
|
+
function installedApps(ctx) {
|
|
28356
|
+
try {
|
|
28357
|
+
const text2 = ctx.vfs.readText(APP_STATE, ctx.cred);
|
|
28358
|
+
const parsed = JSON.parse(text2);
|
|
28359
|
+
return Array.isArray(parsed.apps) ? parsed.apps : [];
|
|
28360
|
+
} catch {
|
|
28361
|
+
return [];
|
|
28362
|
+
}
|
|
28363
|
+
}
|
|
28364
|
+
function record(ctx, app) {
|
|
28365
|
+
const apps = installedApps(ctx).filter((entry) => entry.name !== app.name);
|
|
28366
|
+
apps.push(app);
|
|
28367
|
+
writeState(ctx, apps);
|
|
28368
|
+
}
|
|
28369
|
+
function writeState(ctx, apps) {
|
|
28370
|
+
const directory2 = APP_STATE.slice(0, APP_STATE.lastIndexOf("/"));
|
|
28371
|
+
ctx.vfs.mkdir(directory2, { cred: ctx.cred, recursive: true });
|
|
28372
|
+
ctx.vfs.writeFile(
|
|
28373
|
+
APP_STATE,
|
|
28374
|
+
`${JSON.stringify({ format: "sandboxedjs-installed-apps", apps }, null, 2)}
|
|
28375
|
+
`,
|
|
28376
|
+
{ cred: ctx.cred, mode: 420 }
|
|
28377
|
+
);
|
|
28378
|
+
}
|
|
28379
|
+
function appCommands() {
|
|
28380
|
+
return [pm];
|
|
28381
|
+
}
|
|
28382
|
+
|
|
27048
28383
|
// src/bin/index.ts
|
|
27049
28384
|
function allCommands() {
|
|
27050
28385
|
return [
|
|
@@ -27064,7 +28399,8 @@ function allCommands() {
|
|
|
27064
28399
|
...ffmpegCommands(),
|
|
27065
28400
|
...wasiCommands(),
|
|
27066
28401
|
...packageCommands(),
|
|
27067
|
-
...browserCommands()
|
|
28402
|
+
...browserCommands(),
|
|
28403
|
+
...appCommands()
|
|
27068
28404
|
];
|
|
27069
28405
|
}
|
|
27070
28406
|
function installUserland(kernel) {
|
|
@@ -30783,7 +32119,7 @@ ${indent(detail, 4)}
|
|
|
30783
32119
|
if (detail) host2.write(`${indent(detail, depth * 2 + 2)}
|
|
30784
32120
|
`);
|
|
30785
32121
|
};
|
|
30786
|
-
const
|
|
32122
|
+
const record2 = (outcome, name, error) => {
|
|
30787
32123
|
counts.tests += 1;
|
|
30788
32124
|
if (outcome === "pass") counts.pass += 1;
|
|
30789
32125
|
else if (outcome === "fail") {
|
|
@@ -30805,13 +32141,13 @@ ${indent(detail, 4)}
|
|
|
30805
32141
|
if (!matchesPattern2(display)) return true;
|
|
30806
32142
|
if (runOnly && !options.only) return true;
|
|
30807
32143
|
if (options.skip) {
|
|
30808
|
-
|
|
32144
|
+
record2("skip", display);
|
|
30809
32145
|
report(depth, "skip", name, started);
|
|
30810
32146
|
return true;
|
|
30811
32147
|
}
|
|
30812
32148
|
if (!fn) {
|
|
30813
32149
|
const outcome = options.todo ? "todo" : "skip";
|
|
30814
|
-
|
|
32150
|
+
record2(outcome, display);
|
|
30815
32151
|
report(depth, outcome, name, started);
|
|
30816
32152
|
return true;
|
|
30817
32153
|
}
|
|
@@ -30875,21 +32211,21 @@ ${indent(detail, 4)}
|
|
|
30875
32211
|
controller.abort();
|
|
30876
32212
|
}
|
|
30877
32213
|
if (skipped) {
|
|
30878
|
-
|
|
32214
|
+
record2("skip", display);
|
|
30879
32215
|
report(depth, "skip", name, started);
|
|
30880
32216
|
return true;
|
|
30881
32217
|
}
|
|
30882
32218
|
if (error !== void 0) {
|
|
30883
32219
|
if (todo) {
|
|
30884
|
-
|
|
32220
|
+
record2("todo", display);
|
|
30885
32221
|
report(depth, "todo", name, started);
|
|
30886
32222
|
return true;
|
|
30887
32223
|
}
|
|
30888
|
-
|
|
32224
|
+
record2("fail", display, error);
|
|
30889
32225
|
report(depth, "fail", name, started, errorText(error));
|
|
30890
32226
|
return false;
|
|
30891
32227
|
}
|
|
30892
|
-
|
|
32228
|
+
record2(todo ? "todo" : "pass", display);
|
|
30893
32229
|
report(depth, todo ? "todo" : "pass", name, started);
|
|
30894
32230
|
return true;
|
|
30895
32231
|
};
|
|
@@ -30904,7 +32240,7 @@ ${indent(detail, 4)}
|
|
|
30904
32240
|
else host2.write(`${" ".repeat(depth)}\u25B6 ${suite.name}
|
|
30905
32241
|
`);
|
|
30906
32242
|
if (suite.options.skip) {
|
|
30907
|
-
for (const child of suite.children) if (child.kind === "test")
|
|
32243
|
+
for (const child of suite.children) if (child.kind === "test") record2("skip", fullName(child));
|
|
30908
32244
|
report(depth, "skip", suite.name, started);
|
|
30909
32245
|
return true;
|
|
30910
32246
|
}
|
|
@@ -30916,7 +32252,7 @@ ${indent(detail, 4)}
|
|
|
30916
32252
|
} catch (error) {
|
|
30917
32253
|
for (const child of suite.children) {
|
|
30918
32254
|
if (child.kind === "test") {
|
|
30919
|
-
|
|
32255
|
+
record2("cancelled", fullName(child), error);
|
|
30920
32256
|
report(depth + (isRoot ? 0 : 1), "cancelled", child.name, started, errorText(error));
|
|
30921
32257
|
}
|
|
30922
32258
|
}
|
|
@@ -34818,6 +36154,212 @@ ${BUNDLER_HINT}`),
|
|
|
34818
36154
|
|
|
34819
36155
|
// src/worker/local-runtime-pod.ts
|
|
34820
36156
|
init_path();
|
|
36157
|
+
|
|
36158
|
+
// src/net/outbound-tcp.ts
|
|
36159
|
+
var FIRST_SYNTHETIC = 2887581696;
|
|
36160
|
+
var SYNTHETIC_COUNT = 65534;
|
|
36161
|
+
function toDotted(value) {
|
|
36162
|
+
return [value >>> 24, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
|
|
36163
|
+
}
|
|
36164
|
+
var OutboundTcp = class {
|
|
36165
|
+
constructor(policy, dialer) {
|
|
36166
|
+
this.policy = policy;
|
|
36167
|
+
this.dialer = dialer;
|
|
36168
|
+
}
|
|
36169
|
+
policy;
|
|
36170
|
+
dialer;
|
|
36171
|
+
byName = /* @__PURE__ */ new Map();
|
|
36172
|
+
byAddress = /* @__PURE__ */ new Map();
|
|
36173
|
+
next = 0;
|
|
36174
|
+
/** Whether this address was handed out by `resolve`. */
|
|
36175
|
+
knows(address) {
|
|
36176
|
+
return this.byAddress.has(address);
|
|
36177
|
+
}
|
|
36178
|
+
hostnameFor(address) {
|
|
36179
|
+
return this.byAddress.get(address);
|
|
36180
|
+
}
|
|
36181
|
+
/**
|
|
36182
|
+
* The address for `hostname`, allocating one on first use.
|
|
36183
|
+
*
|
|
36184
|
+
* Refusal happens here as well as at connect, because a name that cannot be
|
|
36185
|
+
* reached should fail as a resolution failure -- which is what every client
|
|
36186
|
+
* reports as "unknown host" rather than as a mid-connection error.
|
|
36187
|
+
*/
|
|
36188
|
+
resolve(hostname) {
|
|
36189
|
+
const name = hostname.trim().toLowerCase();
|
|
36190
|
+
if (!name) throw new PosixError(Errno.EINVAL, "empty hostname");
|
|
36191
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(name)) return name;
|
|
36192
|
+
if (isLoopbackHostname(name)) return "127.0.0.1";
|
|
36193
|
+
const known = this.byName.get(name);
|
|
36194
|
+
if (known) return known;
|
|
36195
|
+
if (!this.allowed(name)) {
|
|
36196
|
+
throw new PosixError(
|
|
36197
|
+
Errno.ENETUNREACH,
|
|
36198
|
+
`cannot resolve ${hostname}: outbound network access is not allowed for this container`
|
|
36199
|
+
);
|
|
36200
|
+
}
|
|
36201
|
+
if (this.next >= SYNTHETIC_COUNT) {
|
|
36202
|
+
throw new PosixError(Errno.ENOSPC, "no addresses left for resolved names");
|
|
36203
|
+
}
|
|
36204
|
+
const address = toDotted(FIRST_SYNTHETIC + this.next + 1);
|
|
36205
|
+
this.next += 1;
|
|
36206
|
+
this.byName.set(name, address);
|
|
36207
|
+
this.byAddress.set(address, name);
|
|
36208
|
+
return address;
|
|
36209
|
+
}
|
|
36210
|
+
/** Open a connection to a resolved address, or to a literal one. */
|
|
36211
|
+
async connect(address, port, local) {
|
|
36212
|
+
const hostname = this.byAddress.get(address) ?? address;
|
|
36213
|
+
if (!this.allowed(hostname)) {
|
|
36214
|
+
throw new PosixError(
|
|
36215
|
+
Errno.ENETUNREACH,
|
|
36216
|
+
`cannot reach ${hostname}:${port}: outbound network access is not allowed for this container`
|
|
36217
|
+
);
|
|
36218
|
+
}
|
|
36219
|
+
if (!this.dialer) {
|
|
36220
|
+
throw new PosixError(
|
|
36221
|
+
Errno.ENETUNREACH,
|
|
36222
|
+
`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`
|
|
36223
|
+
);
|
|
36224
|
+
}
|
|
36225
|
+
const host2 = await this.dialer(hostname, port);
|
|
36226
|
+
const guest = new VirtualTcpConnection(
|
|
36227
|
+
{ address: local.address, port: host2.localPort || local.port },
|
|
36228
|
+
{ address, port }
|
|
36229
|
+
);
|
|
36230
|
+
const bridge = new VirtualTcpConnection({ address, port }, { address: local.address, port: local.port });
|
|
36231
|
+
guest.pairWith(bridge);
|
|
36232
|
+
bridge.pairWith(guest);
|
|
36233
|
+
host2.onData((bytes2) => {
|
|
36234
|
+
try {
|
|
36235
|
+
bridge.write(bytes2);
|
|
36236
|
+
} catch {
|
|
36237
|
+
host2.close();
|
|
36238
|
+
}
|
|
36239
|
+
});
|
|
36240
|
+
host2.onClose(() => {
|
|
36241
|
+
bridge.shutdown(false, true);
|
|
36242
|
+
bridge.close();
|
|
36243
|
+
});
|
|
36244
|
+
void (async () => {
|
|
36245
|
+
for (; ; ) {
|
|
36246
|
+
if (bridge.readable) {
|
|
36247
|
+
let chunk;
|
|
36248
|
+
try {
|
|
36249
|
+
chunk = bridge.read(64 * 1024);
|
|
36250
|
+
} catch {
|
|
36251
|
+
break;
|
|
36252
|
+
}
|
|
36253
|
+
if (chunk.length === 0) {
|
|
36254
|
+
host2.end();
|
|
36255
|
+
break;
|
|
36256
|
+
}
|
|
36257
|
+
host2.write(chunk);
|
|
36258
|
+
continue;
|
|
36259
|
+
}
|
|
36260
|
+
await bridge.waitForChange();
|
|
36261
|
+
}
|
|
36262
|
+
})();
|
|
36263
|
+
return { connection: guest, host: host2 };
|
|
36264
|
+
}
|
|
36265
|
+
allowed(hostname) {
|
|
36266
|
+
const policy = this.policy();
|
|
36267
|
+
if (isLoopbackHostname(hostname)) return true;
|
|
36268
|
+
if (!policy.allowOutbound) return false;
|
|
36269
|
+
const hosts = policy.allowedHosts;
|
|
36270
|
+
if (!hosts) return true;
|
|
36271
|
+
return hosts.some((allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`));
|
|
36272
|
+
}
|
|
36273
|
+
};
|
|
36274
|
+
|
|
36275
|
+
// src/net/node-tcp-dialer.ts
|
|
36276
|
+
init_binary();
|
|
36277
|
+
function canDialTcp() {
|
|
36278
|
+
return typeof process !== "undefined" && Boolean(process.versions?.node);
|
|
36279
|
+
}
|
|
36280
|
+
function createNodeTcpDialer() {
|
|
36281
|
+
return async (host2, port) => {
|
|
36282
|
+
const net = await nodeBuiltin("net");
|
|
36283
|
+
return await new Promise((resolve3, reject) => {
|
|
36284
|
+
const socket = net.connect({ host: host2, port });
|
|
36285
|
+
const pending = [];
|
|
36286
|
+
let onData = null;
|
|
36287
|
+
let onClose = null;
|
|
36288
|
+
let closed = false;
|
|
36289
|
+
socket.on("data", (chunk) => {
|
|
36290
|
+
const bytes2 = new Uint8Array(chunk);
|
|
36291
|
+
if (onData) onData(bytes2);
|
|
36292
|
+
else pending.push(bytes2);
|
|
36293
|
+
});
|
|
36294
|
+
const finish = () => {
|
|
36295
|
+
if (closed) return;
|
|
36296
|
+
closed = true;
|
|
36297
|
+
onClose?.();
|
|
36298
|
+
};
|
|
36299
|
+
socket.on("close", finish);
|
|
36300
|
+
socket.on("end", finish);
|
|
36301
|
+
socket.on("error", (error) => {
|
|
36302
|
+
if (!closed) {
|
|
36303
|
+
closed = true;
|
|
36304
|
+
onClose?.();
|
|
36305
|
+
}
|
|
36306
|
+
reject(translate(error, host2, port));
|
|
36307
|
+
});
|
|
36308
|
+
socket.on("connect", () => {
|
|
36309
|
+
socket.setNoDelay(true);
|
|
36310
|
+
resolve3({
|
|
36311
|
+
write(bytes2) {
|
|
36312
|
+
socket.write(bytes2);
|
|
36313
|
+
},
|
|
36314
|
+
end() {
|
|
36315
|
+
socket.end();
|
|
36316
|
+
},
|
|
36317
|
+
close() {
|
|
36318
|
+
socket.destroy();
|
|
36319
|
+
},
|
|
36320
|
+
onData(handler) {
|
|
36321
|
+
onData = handler;
|
|
36322
|
+
for (const chunk of pending.splice(0)) handler(chunk);
|
|
36323
|
+
},
|
|
36324
|
+
onClose(handler) {
|
|
36325
|
+
onClose = handler;
|
|
36326
|
+
if (closed) handler();
|
|
36327
|
+
},
|
|
36328
|
+
get remoteAddress() {
|
|
36329
|
+
return socket.remoteAddress ?? host2;
|
|
36330
|
+
},
|
|
36331
|
+
get remotePort() {
|
|
36332
|
+
return socket.remotePort ?? port;
|
|
36333
|
+
},
|
|
36334
|
+
get localPort() {
|
|
36335
|
+
return socket.localPort ?? 0;
|
|
36336
|
+
}
|
|
36337
|
+
});
|
|
36338
|
+
});
|
|
36339
|
+
});
|
|
36340
|
+
};
|
|
36341
|
+
}
|
|
36342
|
+
function translate(error, host2, port) {
|
|
36343
|
+
const where = `${host2}:${port}`;
|
|
36344
|
+
switch (error.code) {
|
|
36345
|
+
case "ECONNREFUSED":
|
|
36346
|
+
return new PosixError(Errno.ECONNREFUSED, `connection refused by ${where}`);
|
|
36347
|
+
case "ENOTFOUND":
|
|
36348
|
+
case "EAI_AGAIN":
|
|
36349
|
+
return new PosixError(Errno.ENETUNREACH, `cannot resolve ${host2}`);
|
|
36350
|
+
case "ETIMEDOUT":
|
|
36351
|
+
return new PosixError(Errno.ETIMEDOUT, `connection to ${where} timed out`);
|
|
36352
|
+
case "ECONNRESET":
|
|
36353
|
+
return new PosixError(Errno.ECONNRESET, `connection to ${where} was reset`);
|
|
36354
|
+
case "EHOSTUNREACH":
|
|
36355
|
+
case "ENETUNREACH":
|
|
36356
|
+
return new PosixError(Errno.ENETUNREACH, `cannot reach ${where}`);
|
|
36357
|
+
default:
|
|
36358
|
+
return new PosixError(Errno.ECONNREFUSED, `connection to ${where} failed: ${error.message}`);
|
|
36359
|
+
}
|
|
36360
|
+
}
|
|
36361
|
+
|
|
36362
|
+
// src/worker/local-runtime-pod.ts
|
|
34821
36363
|
var WASM_ALIASES = {
|
|
34822
36364
|
esbuild: "esbuild-wasm",
|
|
34823
36365
|
rollup: "@rollup/wasm-node"
|
|
@@ -35173,6 +36715,32 @@ var LocalRuntimePod = class _LocalRuntimePod {
|
|
|
35173
36715
|
constructor(options) {
|
|
35174
36716
|
ensureProcessGlobal();
|
|
35175
36717
|
this.sockets = new VirtualTcpNetwork((port) => this.router.activePortsIncludes(port));
|
|
36718
|
+
this.sockets.loopbackHttp = (port, connection) => {
|
|
36719
|
+
if (!this.router.activePortsIncludes(port)) return false;
|
|
36720
|
+
void serveHttpOverConnection(connection, async (request) => {
|
|
36721
|
+
const url = new URL(request.target, `http://127.0.0.1:${port}`);
|
|
36722
|
+
const answer = await this.router.request(port, {
|
|
36723
|
+
method: request.method,
|
|
36724
|
+
path: `${url.pathname}${url.search}`,
|
|
36725
|
+
headers: request.headers,
|
|
36726
|
+
...request.body.length > 0 ? { body: request.body } : {}
|
|
36727
|
+
});
|
|
36728
|
+
const payload = answer.body ?? new Uint8Array();
|
|
36729
|
+
return {
|
|
36730
|
+
status: Number(answer.statusCode ?? 200),
|
|
36731
|
+
statusText: String(answer.statusMessage ?? ""),
|
|
36732
|
+
headers: answer.headers ?? {},
|
|
36733
|
+
body: typeof payload === "string" ? new TextEncoder().encode(payload) : payload instanceof Uint8Array ? payload : new Uint8Array(payload)
|
|
36734
|
+
};
|
|
36735
|
+
});
|
|
36736
|
+
return true;
|
|
36737
|
+
};
|
|
36738
|
+
if (canDialTcp()) {
|
|
36739
|
+
this.sockets.outbound = new OutboundTcp(
|
|
36740
|
+
() => this.networkPolicy ?? { allowOutbound: false, allowedHosts: null },
|
|
36741
|
+
createNodeTcpDialer()
|
|
36742
|
+
);
|
|
36743
|
+
}
|
|
35176
36744
|
this.router = new VirtualHttpRouter(this.sockets);
|
|
35177
36745
|
this.workdir = options.workdir ?? "/";
|
|
35178
36746
|
this.env = { ...options.env };
|
|
@@ -36142,6 +37710,14 @@ var Container = class _Container {
|
|
|
36142
37710
|
installUserland(kernel);
|
|
36143
37711
|
const container = new _Container(kernel, pod, opts);
|
|
36144
37712
|
if (opts.files) container.mountSync(opts.files, { cwd: opts.cwd ?? "/" });
|
|
37713
|
+
if (opts.modules && opts.modules.length > 0) {
|
|
37714
|
+
const result = await container.exec(`pm install ${opts.modules.join(" ")}`);
|
|
37715
|
+
if (result.exitCode !== 0) {
|
|
37716
|
+
throw new Error(
|
|
37717
|
+
`could not install ${opts.modules.join(", ")}: ${(result.stderr || result.stdout).trim() || `pm exited ${result.exitCode}`}`
|
|
37718
|
+
);
|
|
37719
|
+
}
|
|
37720
|
+
}
|
|
36145
37721
|
return container;
|
|
36146
37722
|
}
|
|
36147
37723
|
// ── file seeding ──────────────────────────────────────────────────────────
|