sandboxedjs 0.1.94 → 0.1.97
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 +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/{container-WEYRUg6Q.d.ts → container-CFieZcbe.d.ts} +9 -0
- package/dist/{container-C3cgcnXZ.d.cts → container-CPwyHoUy.d.cts} +9 -0
- package/dist/index.cjs +801 -69
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +801 -69
- package/package.json +3 -2
package/dist/index.cjs
CHANGED
|
@@ -7003,8 +7003,8 @@ var WasiHost = class {
|
|
|
7003
7003
|
view.setUint16(at + 8, WASI_ESUCCESS, true);
|
|
7004
7004
|
view.setUint8(at + 10, subscription.type);
|
|
7005
7005
|
if (subscription.type === EVENTTYPE_FD_READ) {
|
|
7006
|
-
const
|
|
7007
|
-
view.setBigUint64(at + 16, BigInt(
|
|
7006
|
+
const available2 = subscription.fd === 0 ? this.opts.stdin.available : this.bytesLeft(subscription.fd);
|
|
7007
|
+
view.setBigUint64(at + 16, BigInt(available2), true);
|
|
7008
7008
|
} else if (subscription.type === EVENTTYPE_FD_WRITE) {
|
|
7009
7009
|
view.setBigUint64(at + 16, BigInt(64 * 1024), true);
|
|
7010
7010
|
}
|
|
@@ -7241,13 +7241,13 @@ var BinaryExecutionRegistry = class {
|
|
|
7241
7241
|
}
|
|
7242
7242
|
async run(ctx, path) {
|
|
7243
7243
|
const bytes2 = ctx.vfs.readFile(path, ctx.cred);
|
|
7244
|
-
let
|
|
7244
|
+
let info2;
|
|
7245
7245
|
try {
|
|
7246
|
-
|
|
7246
|
+
info2 = inspectElf(bytes2);
|
|
7247
7247
|
} catch (error) {
|
|
7248
7248
|
return ctx.fail(String(error), 126);
|
|
7249
7249
|
}
|
|
7250
|
-
const request = { path, bytes: bytes2, info, sha256: binaryDigest(bytes2) };
|
|
7250
|
+
const request = { path, bytes: bytes2, info: info2, sha256: binaryDigest(bytes2) };
|
|
7251
7251
|
const reasons = [];
|
|
7252
7252
|
const backends = [...this.backends];
|
|
7253
7253
|
for (const tier of ["compatibility", "translation", "emulation"]) {
|
|
@@ -7266,7 +7266,7 @@ var BinaryExecutionRegistry = class {
|
|
|
7266
7266
|
reasons.push(`${backend.id}: ${result.reason}`);
|
|
7267
7267
|
}
|
|
7268
7268
|
}
|
|
7269
|
-
return ctx.fail(`cannot execute ${
|
|
7269
|
+
return ctx.fail(`cannot execute ${info2.architecture} ELF: ${reasons.join("; ") || "no binary backends registered"}`, 126);
|
|
7270
7270
|
}
|
|
7271
7271
|
};
|
|
7272
7272
|
function checkedWasm(artifact) {
|
|
@@ -8530,8 +8530,8 @@ var NetworkStack = class {
|
|
|
8530
8530
|
return this.ifaces.some((i) => i.ipv4 === addr);
|
|
8531
8531
|
}
|
|
8532
8532
|
// ── listening ports ──────────────────────────────────────────────────────
|
|
8533
|
-
registerListener(port,
|
|
8534
|
-
this.listeners.set(port, { port, since: Date.now(), ...
|
|
8533
|
+
registerListener(port, info2) {
|
|
8534
|
+
this.listeners.set(port, { port, since: Date.now(), ...info2 });
|
|
8535
8535
|
}
|
|
8536
8536
|
unregisterListener(port) {
|
|
8537
8537
|
this.listeners.delete(port);
|
|
@@ -12124,21 +12124,21 @@ var uname = defineCommand({
|
|
|
12124
12124
|
{ short: "i", long: "hardware-platform" },
|
|
12125
12125
|
{ short: "o", long: "operating-system" }
|
|
12126
12126
|
]);
|
|
12127
|
-
const
|
|
12127
|
+
const info2 = unameInfo(ctx.kernel.hostname);
|
|
12128
12128
|
if (args.has("all")) {
|
|
12129
|
-
ctx.line(`${
|
|
12129
|
+
ctx.line(`${info2.sysname} ${info2.nodename} ${info2.release} ${info2.version} ${info2.machine} ${info2.operatingSystem}`);
|
|
12130
12130
|
return 0;
|
|
12131
12131
|
}
|
|
12132
12132
|
const parts = [];
|
|
12133
|
-
if (args.has("kernel-name")) parts.push(
|
|
12134
|
-
if (args.has("nodename")) parts.push(
|
|
12135
|
-
if (args.has("kernel-release")) parts.push(
|
|
12136
|
-
if (args.has("kernel-version")) parts.push(
|
|
12137
|
-
if (args.has("machine")) parts.push(
|
|
12138
|
-
if (args.has("processor")) parts.push(
|
|
12139
|
-
if (args.has("hardware-platform")) parts.push(
|
|
12140
|
-
if (args.has("operating-system")) parts.push(
|
|
12141
|
-
ctx.line(parts.length ? parts.join(" ") :
|
|
12133
|
+
if (args.has("kernel-name")) parts.push(info2.sysname);
|
|
12134
|
+
if (args.has("nodename")) parts.push(info2.nodename);
|
|
12135
|
+
if (args.has("kernel-release")) parts.push(info2.release);
|
|
12136
|
+
if (args.has("kernel-version")) parts.push(info2.version);
|
|
12137
|
+
if (args.has("machine")) parts.push(info2.machine);
|
|
12138
|
+
if (args.has("processor")) parts.push(info2.processor);
|
|
12139
|
+
if (args.has("hardware-platform")) parts.push(info2.hardwarePlatform);
|
|
12140
|
+
if (args.has("operating-system")) parts.push(info2.operatingSystem);
|
|
12141
|
+
ctx.line(parts.length ? parts.join(" ") : info2.sysname);
|
|
12142
12142
|
return 0;
|
|
12143
12143
|
}
|
|
12144
12144
|
});
|
|
@@ -15122,15 +15122,15 @@ var Interpreter = class {
|
|
|
15122
15122
|
return this.exitCode;
|
|
15123
15123
|
}
|
|
15124
15124
|
};
|
|
15125
|
-
function splitRecord(
|
|
15126
|
-
if (
|
|
15125
|
+
function splitRecord(record2, fs) {
|
|
15126
|
+
if (record2 === "") return [];
|
|
15127
15127
|
if (fs === " ") {
|
|
15128
|
-
const trimmed =
|
|
15128
|
+
const trimmed = record2.replace(/^[ \t\n]+|[ \t\n]+$/g, "");
|
|
15129
15129
|
return trimmed === "" ? [] : trimmed.split(/[ \t\n]+/);
|
|
15130
15130
|
}
|
|
15131
|
-
if (fs === "") return [...
|
|
15132
|
-
if (fs.length === 1 && !"\\^$.[]|()*+?{}".includes(fs)) return
|
|
15133
|
-
return
|
|
15131
|
+
if (fs === "") return [...record2];
|
|
15132
|
+
if (fs.length === 1 && !"\\^$.[]|()*+?{}".includes(fs)) return record2.split(fs);
|
|
15133
|
+
return record2.split(new RegExp(translateEre(fs)));
|
|
15134
15134
|
}
|
|
15135
15135
|
var ERE_CLASSES = {
|
|
15136
15136
|
alpha: "A-Za-z",
|
|
@@ -20958,7 +20958,7 @@ var wheels_default = {
|
|
|
20958
20958
|
|
|
20959
20959
|
// package.json
|
|
20960
20960
|
var package_default = {
|
|
20961
|
-
version: "0.1.
|
|
20961
|
+
version: "0.1.97"};
|
|
20962
20962
|
|
|
20963
20963
|
// src/python/extension-abi.ts
|
|
20964
20964
|
var EXTENSION_ABI = {
|
|
@@ -23686,7 +23686,7 @@ async function resolve2(options) {
|
|
|
23686
23686
|
const clauses = (state.constraints.get(name) ?? []).flatMap((entry) => entry.spec.map((s) => `${s.op}${s.version}`)).sort();
|
|
23687
23687
|
return `${name}|${[...new Set(clauses)].join(",")}`;
|
|
23688
23688
|
};
|
|
23689
|
-
const
|
|
23689
|
+
const search2 = async (queue, state) => {
|
|
23690
23690
|
checkBudget();
|
|
23691
23691
|
if (queue.length === 0) return state;
|
|
23692
23692
|
const [edge, ...rest] = queue;
|
|
@@ -23694,7 +23694,7 @@ async function resolve2(options) {
|
|
|
23694
23694
|
const name = requirement.name;
|
|
23695
23695
|
if (!markerApplies(requirement.marker, environment, extras)) {
|
|
23696
23696
|
if (requirement.marker) skipped.push({ name, marker: requirement.marker });
|
|
23697
|
-
return
|
|
23697
|
+
return search2(rest, state);
|
|
23698
23698
|
}
|
|
23699
23699
|
const previous = state.constraints.get(name) ?? [];
|
|
23700
23700
|
const constraints = new Map(state.constraints);
|
|
@@ -23711,7 +23711,7 @@ async function resolve2(options) {
|
|
|
23711
23711
|
explain(name, next.constraints, already.version)
|
|
23712
23712
|
);
|
|
23713
23713
|
}
|
|
23714
|
-
return
|
|
23714
|
+
return search2(rest, next);
|
|
23715
23715
|
}
|
|
23716
23716
|
const signature = signatureOf(name, next);
|
|
23717
23717
|
const learned = nogoods.get(signature);
|
|
@@ -23774,7 +23774,7 @@ async function resolve2(options) {
|
|
|
23774
23774
|
const chosen = new Map(next.chosen);
|
|
23775
23775
|
chosen.set(name, { ...candidate, requires });
|
|
23776
23776
|
try {
|
|
23777
|
-
return await
|
|
23777
|
+
return await search2([...edges, ...rest], { chosen, constraints: next.constraints });
|
|
23778
23778
|
} catch (error) {
|
|
23779
23779
|
if (!(error instanceof ResolutionError)) throw error;
|
|
23780
23780
|
if (/cancelled|budget/.test(error.message)) throw error;
|
|
@@ -23789,12 +23789,12 @@ async function resolve2(options) {
|
|
|
23789
23789
|
if (specifiers.length === 0 || !markerApplies(marker, environment, root.extras)) continue;
|
|
23790
23790
|
seeded.set(name, [...seeded.get(name) ?? [], { spec: specifiers, origin: root.origin }]);
|
|
23791
23791
|
}
|
|
23792
|
-
const solved = await
|
|
23792
|
+
const solved = await search2(roots, { chosen: /* @__PURE__ */ new Map(), constraints: seeded });
|
|
23793
23793
|
const distributions = [...solved.chosen.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
23794
23794
|
const reported = new Map(skipped.map((s) => [`${s.name}\0${s.marker}`, s]));
|
|
23795
23795
|
return { distributions, skipped: [...reported.values()] };
|
|
23796
23796
|
}
|
|
23797
|
-
function explain(name, constraints, chosenVersion,
|
|
23797
|
+
function explain(name, constraints, chosenVersion, available2 = []) {
|
|
23798
23798
|
const lines = [];
|
|
23799
23799
|
for (const entry of constraints.get(name) ?? []) {
|
|
23800
23800
|
if (entry.spec.length === 0) continue;
|
|
@@ -23802,9 +23802,9 @@ function explain(name, constraints, chosenVersion, available = []) {
|
|
|
23802
23802
|
lines.push(`${entry.origin} requires ${name} ${text2}`);
|
|
23803
23803
|
}
|
|
23804
23804
|
if (chosenVersion) lines.push(`but ${name} ${chosenVersion} is already selected`);
|
|
23805
|
-
if (
|
|
23806
|
-
const versions = [...new Set(
|
|
23807
|
-
lines.push(`usable versions: ${versions.join(", ")}${
|
|
23805
|
+
if (available2.length > 0) {
|
|
23806
|
+
const versions = [...new Set(available2.map((c) => c.version))].slice(0, 8);
|
|
23807
|
+
lines.push(`usable versions: ${versions.join(", ")}${available2.length > 8 ? ", ..." : ""}`);
|
|
23808
23808
|
}
|
|
23809
23809
|
return lines;
|
|
23810
23810
|
}
|
|
@@ -23933,9 +23933,7 @@ async function installRequirements(options) {
|
|
|
23933
23933
|
continue;
|
|
23934
23934
|
}
|
|
23935
23935
|
options.progress.downloading(distribution.name, distribution.version);
|
|
23936
|
-
const bytes2 = await options.client
|
|
23937
|
-
requireArchive(distribution, bytes2);
|
|
23938
|
-
verifyDigest(distribution, bytes2);
|
|
23936
|
+
const bytes2 = await download(options.client, distribution);
|
|
23939
23937
|
const files = stageWheel(readZip(bytes2));
|
|
23940
23938
|
staged.push(...distribution.kind === "substituted" ? substituteFiles(distribution.name, files, SITE_PACKAGES) : files);
|
|
23941
23939
|
installed3.push({ name: distribution.name, version: distribution.version });
|
|
@@ -23951,18 +23949,41 @@ function requireArchive(distribution, bytes2) {
|
|
|
23951
23949
|
`${distribution.filename} did not arrive as a wheel from ${distribution.url}: ` + (looksLikeMarkup ? "the server answered with an HTML page, which usually means the wheel is not published at that address and the server returned its index page instead" : `expected a zip archive, got ${bytes2.length} bytes beginning ${JSON.stringify(opening)}`)
|
|
23952
23950
|
);
|
|
23953
23951
|
}
|
|
23954
|
-
function
|
|
23952
|
+
async function download(client, distribution) {
|
|
23953
|
+
const bytes2 = await client.bytes(distribution.url, { timeoutMs: 12e4 });
|
|
23954
|
+
requireArchive(distribution, bytes2);
|
|
23955
|
+
const mismatch = digestMismatch(distribution, bytes2);
|
|
23956
|
+
if (!mismatch) return bytes2;
|
|
23957
|
+
const fresh = await refetch(client, distribution);
|
|
23958
|
+
if (fresh && !digestMismatch(distribution, fresh)) {
|
|
23959
|
+
requireArchive(distribution, fresh);
|
|
23960
|
+
return fresh;
|
|
23961
|
+
}
|
|
23962
|
+
throw new Error(
|
|
23963
|
+
`${mismatch} ${fresh ? "A retry past any cache returned the same bytes, so the index and the file it names disagree." : "A retry past any cache could not be made, so this may be a cached copy of an older build; clearing the cache, or asking the index's host to purge it, is worth trying before the index is blamed."}`
|
|
23964
|
+
);
|
|
23965
|
+
}
|
|
23966
|
+
async function refetch(client, distribution) {
|
|
23967
|
+
const separator = distribution.url.includes("?") ? "&" : "?";
|
|
23968
|
+
try {
|
|
23969
|
+
const bytes2 = await client.bytes(
|
|
23970
|
+
`${distribution.url}${separator}sha256=${distribution.sha256}`,
|
|
23971
|
+
{ timeoutMs: 6e4 }
|
|
23972
|
+
);
|
|
23973
|
+
return bytes2.length > 1 && bytes2[0] === 80 && bytes2[1] === 75 ? bytes2 : null;
|
|
23974
|
+
} catch {
|
|
23975
|
+
return null;
|
|
23976
|
+
}
|
|
23977
|
+
}
|
|
23978
|
+
function digestMismatch(distribution, bytes2) {
|
|
23955
23979
|
if (!distribution.sha256) {
|
|
23956
23980
|
throw new Error(
|
|
23957
23981
|
`${distribution.filename} was published without a sha256 digest, so it cannot be verified`
|
|
23958
23982
|
);
|
|
23959
23983
|
}
|
|
23960
23984
|
const actual = [...sha256.sha256(bytes2)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
23961
|
-
if (actual
|
|
23962
|
-
|
|
23963
|
-
`${distribution.filename} failed verification: the index published sha256 ${distribution.sha256} but the download hashes to ${actual}`
|
|
23964
|
-
);
|
|
23965
|
-
}
|
|
23985
|
+
if (actual === distribution.sha256) return null;
|
|
23986
|
+
return `${distribution.filename} failed verification: the index published sha256 ${distribution.sha256} but the download hashes to ${actual}.`;
|
|
23966
23987
|
}
|
|
23967
23988
|
function stageWheel(entries, resolved) {
|
|
23968
23989
|
const staged = [];
|
|
@@ -26401,9 +26422,9 @@ function parsePax(data) {
|
|
|
26401
26422
|
if (space < 0) break;
|
|
26402
26423
|
const length = Number(value.slice(offset, space));
|
|
26403
26424
|
if (!Number.isFinite(length) || length <= 0) break;
|
|
26404
|
-
const
|
|
26405
|
-
const equal2 =
|
|
26406
|
-
if (equal2 > 0) result[
|
|
26425
|
+
const record2 = value.slice(space + 1, offset + length - 1);
|
|
26426
|
+
const equal2 = record2.indexOf("=");
|
|
26427
|
+
if (equal2 > 0) result[record2.slice(0, equal2)] = record2.slice(equal2 + 1);
|
|
26407
26428
|
offset += length;
|
|
26408
26429
|
}
|
|
26409
26430
|
return result;
|
|
@@ -27183,27 +27204,27 @@ var apt = defineCommand({
|
|
|
27183
27204
|
switch (subcommand) {
|
|
27184
27205
|
case "list": {
|
|
27185
27206
|
const installedOnly = rest.includes("--installed");
|
|
27186
|
-
for (const [name,
|
|
27187
|
-
const
|
|
27188
|
-
if (installedOnly && !
|
|
27189
|
-
ctx.line(`${name}/stable ${
|
|
27207
|
+
for (const [name, info2] of Object.entries(provided)) {
|
|
27208
|
+
const available2 = ctx.kernel.which(name === "python3-pip" ? "pip" : name === "gawk" ? "awk" : name, ctx.cwd, ctx.env, ctx.cred) !== null;
|
|
27209
|
+
if (installedOnly && !available2) continue;
|
|
27210
|
+
ctx.line(`${name}/stable ${info2.version} amd64${available2 ? " [installed]" : ""}`);
|
|
27190
27211
|
}
|
|
27191
27212
|
return 0;
|
|
27192
27213
|
}
|
|
27193
27214
|
case "show": {
|
|
27194
27215
|
let status = 0;
|
|
27195
27216
|
for (const name of rest) {
|
|
27196
|
-
const
|
|
27197
|
-
if (!
|
|
27217
|
+
const info2 = provided[name];
|
|
27218
|
+
if (!info2) {
|
|
27198
27219
|
ctx.warn(`E: No packages found matching ${name}`);
|
|
27199
27220
|
status = 1;
|
|
27200
27221
|
continue;
|
|
27201
27222
|
}
|
|
27202
27223
|
ctx.line(`Package: ${name}`);
|
|
27203
|
-
ctx.line(`Version: ${
|
|
27224
|
+
ctx.line(`Version: ${info2.version}`);
|
|
27204
27225
|
ctx.line(`Priority: optional`);
|
|
27205
27226
|
ctx.line(`Architecture: amd64`);
|
|
27206
|
-
ctx.line(`Description: ${
|
|
27227
|
+
ctx.line(`Description: ${info2.description}`);
|
|
27207
27228
|
ctx.line("");
|
|
27208
27229
|
}
|
|
27209
27230
|
return status;
|
|
@@ -27234,10 +27255,10 @@ var apt = defineCommand({
|
|
|
27234
27255
|
return 100;
|
|
27235
27256
|
case "search": {
|
|
27236
27257
|
const pattern = new RegExp(rest[0] ?? "", "i");
|
|
27237
|
-
for (const [name,
|
|
27238
|
-
if (pattern.test(name) || pattern.test(
|
|
27239
|
-
ctx.line(`${name}/stable ${
|
|
27240
|
-
ctx.line(` ${
|
|
27258
|
+
for (const [name, info2] of Object.entries(provided)) {
|
|
27259
|
+
if (pattern.test(name) || pattern.test(info2.description)) {
|
|
27260
|
+
ctx.line(`${name}/stable ${info2.version} amd64`);
|
|
27261
|
+
ctx.line(` ${info2.description}`);
|
|
27241
27262
|
ctx.line("");
|
|
27242
27263
|
}
|
|
27243
27264
|
}
|
|
@@ -27702,6 +27723,708 @@ function browserCommands() {
|
|
|
27702
27723
|
];
|
|
27703
27724
|
}
|
|
27704
27725
|
|
|
27726
|
+
// src/apps/pm.ts
|
|
27727
|
+
init_binary();
|
|
27728
|
+
init_zip();
|
|
27729
|
+
|
|
27730
|
+
// src/apps/registry.ts
|
|
27731
|
+
var APPS_ROOT = "/opt";
|
|
27732
|
+
var APP_BIN_DIR = "/usr/local/bin";
|
|
27733
|
+
var APP_STATE = "/var/lib/pm/installed.json";
|
|
27734
|
+
var DEFAULT_REGISTRY = "https://sandboxedjswheels.pages.dev/apps";
|
|
27735
|
+
var BUILT_IN_PUBLISHERS = [
|
|
27736
|
+
{
|
|
27737
|
+
publisher: "SandboxedJs",
|
|
27738
|
+
publicKey: "MCowBQYDK2VwAyEAuyxKMeyeO/05mr5Qu8vc+Dd9yMG2/oKDwt7KA+C9Mj4="
|
|
27739
|
+
}
|
|
27740
|
+
];
|
|
27741
|
+
var AppRegistryError = class extends Error {
|
|
27742
|
+
};
|
|
27743
|
+
function parseIndex(text2, source) {
|
|
27744
|
+
let parsed;
|
|
27745
|
+
try {
|
|
27746
|
+
parsed = JSON.parse(text2);
|
|
27747
|
+
} catch (error) {
|
|
27748
|
+
throw new AppRegistryError(`${source} is not JSON: ${error.message}`);
|
|
27749
|
+
}
|
|
27750
|
+
const index = parsed;
|
|
27751
|
+
if (index?.format !== "sandboxedjs-app-index") {
|
|
27752
|
+
throw new AppRegistryError(
|
|
27753
|
+
`${source} is not an application index (its format is ${JSON.stringify(index?.format ?? null)})`
|
|
27754
|
+
);
|
|
27755
|
+
}
|
|
27756
|
+
if (!Array.isArray(index.apps)) {
|
|
27757
|
+
throw new AppRegistryError(`${source} has no apps`);
|
|
27758
|
+
}
|
|
27759
|
+
for (const app of index.apps) {
|
|
27760
|
+
for (const field of ["name", "version", "filename", "sha256"]) {
|
|
27761
|
+
if (typeof app[field] !== "string" || !app[field]) {
|
|
27762
|
+
throw new AppRegistryError(`${source}: an entry is missing ${field}`);
|
|
27763
|
+
}
|
|
27764
|
+
}
|
|
27765
|
+
}
|
|
27766
|
+
return index;
|
|
27767
|
+
}
|
|
27768
|
+
function selectRelease(index, name, version) {
|
|
27769
|
+
const matches3 = index.apps.filter((app) => app.name === name);
|
|
27770
|
+
if (matches3.length === 0) {
|
|
27771
|
+
const names = index.apps.map((app) => app.name).sort();
|
|
27772
|
+
throw new AppRegistryError(
|
|
27773
|
+
`no application named ${name}. The registry has: ${names.join(", ") || "nothing"}`
|
|
27774
|
+
);
|
|
27775
|
+
}
|
|
27776
|
+
if (version) {
|
|
27777
|
+
const exact = matches3.find((app) => app.version === version);
|
|
27778
|
+
if (!exact) {
|
|
27779
|
+
const versions = matches3.map((app) => app.version).join(", ");
|
|
27780
|
+
throw new AppRegistryError(`${name} has no version ${version}; it has ${versions}`);
|
|
27781
|
+
}
|
|
27782
|
+
return exact;
|
|
27783
|
+
}
|
|
27784
|
+
return matches3.slice().sort((a, b) => compareVersions2(b.version, a.version))[0];
|
|
27785
|
+
}
|
|
27786
|
+
function compareVersions2(a, b) {
|
|
27787
|
+
const left = a.split(/[.-]/);
|
|
27788
|
+
const right = b.split(/[.-]/);
|
|
27789
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
27790
|
+
const x = left[index] ?? "";
|
|
27791
|
+
const y = right[index] ?? "";
|
|
27792
|
+
const nx = Number(x);
|
|
27793
|
+
const ny = Number(y);
|
|
27794
|
+
if (Number.isFinite(nx) && Number.isFinite(ny) && x !== "" && y !== "") {
|
|
27795
|
+
if (nx !== ny) return nx - ny;
|
|
27796
|
+
continue;
|
|
27797
|
+
}
|
|
27798
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
27799
|
+
}
|
|
27800
|
+
return 0;
|
|
27801
|
+
}
|
|
27802
|
+
function parseSpecifier(specifier) {
|
|
27803
|
+
const at = specifier.lastIndexOf("@");
|
|
27804
|
+
if (at <= 0) return { name: specifier };
|
|
27805
|
+
return { name: specifier.slice(0, at), version: specifier.slice(at + 1) };
|
|
27806
|
+
}
|
|
27807
|
+
function validateManifest2(manifest, source) {
|
|
27808
|
+
const app = manifest;
|
|
27809
|
+
if (typeof app?.name !== "string" || typeof app?.version !== "string") {
|
|
27810
|
+
throw new AppRegistryError(`${source} is not an application manifest`);
|
|
27811
|
+
}
|
|
27812
|
+
for (const binary of app.bins ?? []) {
|
|
27813
|
+
if (typeof binary?.name !== "string" || typeof binary?.path !== "string") {
|
|
27814
|
+
throw new AppRegistryError(`${source}: a bins entry needs a name and a path`);
|
|
27815
|
+
}
|
|
27816
|
+
if (binary.name.includes("/")) {
|
|
27817
|
+
throw new AppRegistryError(`${source}: a command name cannot contain a slash`);
|
|
27818
|
+
}
|
|
27819
|
+
}
|
|
27820
|
+
for (const service of app.services ?? []) {
|
|
27821
|
+
if (typeof service?.port !== "number" || !Number.isInteger(service.port)) {
|
|
27822
|
+
throw new AppRegistryError(`${source}: a service needs an integer port`);
|
|
27823
|
+
}
|
|
27824
|
+
}
|
|
27825
|
+
return app;
|
|
27826
|
+
}
|
|
27827
|
+
|
|
27828
|
+
// src/apps/sbjs.ts
|
|
27829
|
+
init_binary();
|
|
27830
|
+
init_zip();
|
|
27831
|
+
var MANIFEST_PATH = "META/manifest.json";
|
|
27832
|
+
var SIGNATURE_PATH = "META/signature.json";
|
|
27833
|
+
var TRUSTED_KEYS_PATH = "/etc/pm/trusted-keys.json";
|
|
27834
|
+
var PackageError = class extends Error {
|
|
27835
|
+
};
|
|
27836
|
+
async function openPackage(bytes2, trusted) {
|
|
27837
|
+
const entries = readZip(bytes2);
|
|
27838
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
27839
|
+
const manifestEntry = byName.get(MANIFEST_PATH);
|
|
27840
|
+
if (!manifestEntry) {
|
|
27841
|
+
throw new PackageError(
|
|
27842
|
+
`not a SandboxedJs package: it has no ${MANIFEST_PATH}. A plain zip can be installed with --allow-unsigned, but it carries no publisher.`
|
|
27843
|
+
);
|
|
27844
|
+
}
|
|
27845
|
+
const manifestBytes = manifestEntry.data();
|
|
27846
|
+
const manifest = parseManifest(manifestBytes);
|
|
27847
|
+
validateManifest2(manifest.app, `${MANIFEST_PATH}'s app`);
|
|
27848
|
+
for (const entry of entries) {
|
|
27849
|
+
if (entry.isDirectory || entry.name.startsWith("META/")) continue;
|
|
27850
|
+
const expected = manifest.files[entry.name];
|
|
27851
|
+
if (!expected) {
|
|
27852
|
+
throw new PackageError(`${entry.name} is in the package but not in its manifest`);
|
|
27853
|
+
}
|
|
27854
|
+
const actual = await digestHex("sha256", entry.data());
|
|
27855
|
+
if (actual !== expected) {
|
|
27856
|
+
throw new PackageError(
|
|
27857
|
+
`${entry.name} does not match the manifest (expected ${expected}, found ${actual})`
|
|
27858
|
+
);
|
|
27859
|
+
}
|
|
27860
|
+
}
|
|
27861
|
+
for (const name of Object.keys(manifest.files)) {
|
|
27862
|
+
if (!byName.has(name)) {
|
|
27863
|
+
throw new PackageError(`${name} is in the manifest but missing from the package`);
|
|
27864
|
+
}
|
|
27865
|
+
}
|
|
27866
|
+
const signatureEntry = byName.get(SIGNATURE_PATH);
|
|
27867
|
+
if (!signatureEntry) {
|
|
27868
|
+
return { manifest, signature: null, entries, trustedBy: null };
|
|
27869
|
+
}
|
|
27870
|
+
const signature = parseSignature(signatureEntry.data());
|
|
27871
|
+
const valid = await verifySignature(manifestBytes, signature);
|
|
27872
|
+
if (!valid) {
|
|
27873
|
+
throw new PackageError(
|
|
27874
|
+
`the signature on ${manifest.app.name} does not match its manifest; the package has been altered since it was signed`
|
|
27875
|
+
);
|
|
27876
|
+
}
|
|
27877
|
+
const trustedBy = trusted.find((key) => key.publicKey === signature.publicKey) ?? null;
|
|
27878
|
+
return { manifest, signature, entries, trustedBy };
|
|
27879
|
+
}
|
|
27880
|
+
function parseManifest(bytes2) {
|
|
27881
|
+
let parsed;
|
|
27882
|
+
try {
|
|
27883
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes2));
|
|
27884
|
+
} catch (error) {
|
|
27885
|
+
throw new PackageError(`${MANIFEST_PATH} is not JSON: ${error.message}`);
|
|
27886
|
+
}
|
|
27887
|
+
const manifest = parsed;
|
|
27888
|
+
if (manifest?.format !== "sandboxedjs-package") {
|
|
27889
|
+
throw new PackageError(`${MANIFEST_PATH} is not a package manifest`);
|
|
27890
|
+
}
|
|
27891
|
+
if (!manifest.files || typeof manifest.files !== "object") {
|
|
27892
|
+
throw new PackageError(`${MANIFEST_PATH} lists no files`);
|
|
27893
|
+
}
|
|
27894
|
+
if (!manifest.app) {
|
|
27895
|
+
throw new PackageError(`${MANIFEST_PATH} carries no application manifest`);
|
|
27896
|
+
}
|
|
27897
|
+
return manifest;
|
|
27898
|
+
}
|
|
27899
|
+
function parseSignature(bytes2) {
|
|
27900
|
+
let parsed;
|
|
27901
|
+
try {
|
|
27902
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes2));
|
|
27903
|
+
} catch (error) {
|
|
27904
|
+
throw new PackageError(`${SIGNATURE_PATH} is not JSON: ${error.message}`);
|
|
27905
|
+
}
|
|
27906
|
+
const signature = parsed;
|
|
27907
|
+
if (signature?.format !== "sandboxedjs-signature") {
|
|
27908
|
+
throw new PackageError(`${SIGNATURE_PATH} is not a signature`);
|
|
27909
|
+
}
|
|
27910
|
+
if (signature.algorithm !== "ed25519") {
|
|
27911
|
+
throw new PackageError(
|
|
27912
|
+
`${signature.algorithm} signatures are not supported by this runtime; only ed25519 is`
|
|
27913
|
+
);
|
|
27914
|
+
}
|
|
27915
|
+
for (const field of ["publicKey", "signature"]) {
|
|
27916
|
+
if (typeof signature[field] !== "string" || !signature[field]) {
|
|
27917
|
+
throw new PackageError(`${SIGNATURE_PATH} has no ${field}`);
|
|
27918
|
+
}
|
|
27919
|
+
}
|
|
27920
|
+
return signature;
|
|
27921
|
+
}
|
|
27922
|
+
async function verifySignature(manifestBytes, signature) {
|
|
27923
|
+
const subtle = globalThis.crypto?.subtle;
|
|
27924
|
+
if (!subtle) {
|
|
27925
|
+
throw new PackageError(
|
|
27926
|
+
"this runtime has no WebCrypto, so a package signature cannot be checked"
|
|
27927
|
+
);
|
|
27928
|
+
}
|
|
27929
|
+
let key;
|
|
27930
|
+
try {
|
|
27931
|
+
key = await subtle.importKey(
|
|
27932
|
+
"spki",
|
|
27933
|
+
intoBuffer(base64ToBytes(signature.publicKey)),
|
|
27934
|
+
{ name: "Ed25519" },
|
|
27935
|
+
false,
|
|
27936
|
+
["verify"]
|
|
27937
|
+
);
|
|
27938
|
+
} catch (error) {
|
|
27939
|
+
throw new PackageError(
|
|
27940
|
+
`cannot use the package's public key: ${error.message}. Ed25519 needs Node 20 or a browser that implements it.`
|
|
27941
|
+
);
|
|
27942
|
+
}
|
|
27943
|
+
return subtle.verify(
|
|
27944
|
+
{ name: "Ed25519" },
|
|
27945
|
+
key,
|
|
27946
|
+
intoBuffer(base64ToBytes(signature.signature)),
|
|
27947
|
+
intoBuffer(manifestBytes)
|
|
27948
|
+
);
|
|
27949
|
+
}
|
|
27950
|
+
function intoBuffer(bytes2) {
|
|
27951
|
+
return bytes2.slice().buffer;
|
|
27952
|
+
}
|
|
27953
|
+
function base64ToBytes(text2) {
|
|
27954
|
+
const binary = atob(text2);
|
|
27955
|
+
const bytes2 = new Uint8Array(binary.length);
|
|
27956
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
27957
|
+
bytes2[index] = binary.charCodeAt(index);
|
|
27958
|
+
}
|
|
27959
|
+
return bytes2;
|
|
27960
|
+
}
|
|
27961
|
+
async function fingerprint(publicKey) {
|
|
27962
|
+
const digest2 = await digestHex("sha256", base64ToBytes(publicKey));
|
|
27963
|
+
return digest2.slice(0, 32).replace(/(.{4})(?=.)/g, "$1-");
|
|
27964
|
+
}
|
|
27965
|
+
function asRegistryError(error) {
|
|
27966
|
+
return new AppRegistryError(error.message);
|
|
27967
|
+
}
|
|
27968
|
+
|
|
27969
|
+
// src/apps/pm.ts
|
|
27970
|
+
function registryBase(ctx) {
|
|
27971
|
+
const configured = ctx.env.SBX_PM_REGISTRY ?? ctx.env.PM_REGISTRY;
|
|
27972
|
+
return configured && configured.trim() || DEFAULT_REGISTRY;
|
|
27973
|
+
}
|
|
27974
|
+
var pm = defineCommand({
|
|
27975
|
+
name: "pm",
|
|
27976
|
+
path: "/usr/bin/pm",
|
|
27977
|
+
summary: "install and manage applications",
|
|
27978
|
+
usage: "pm <install|remove|list|info|search|available> [name[@version]]",
|
|
27979
|
+
manual: `Applications are programs the container runs, packaged with what
|
|
27980
|
+
they need. They are not Python or Node packages: use pip or npm for those.
|
|
27981
|
+
|
|
27982
|
+
pm install NAME[@VERSION] download, verify and install
|
|
27983
|
+
pm remove NAME remove it and the commands it installed
|
|
27984
|
+
pm list what is installed here
|
|
27985
|
+
pm info NAME what a package provides
|
|
27986
|
+
pm search TEXT search the registry
|
|
27987
|
+
pm available everything the registry offers
|
|
27988
|
+
pm trust list|add|remove publisher keys this container trusts
|
|
27989
|
+
|
|
27990
|
+
Packages come from ${DEFAULT_REGISTRY} unless SBX_PM_REGISTRY names another.
|
|
27991
|
+
|
|
27992
|
+
A package is a .sbjs file: a zip carrying META/manifest.json, which lists a
|
|
27993
|
+
sha256 for every file in it, and META/signature.json, an Ed25519 signature
|
|
27994
|
+
over that manifest. Installing checks both, and refuses a package signed by a
|
|
27995
|
+
key this container does not trust. --allow-untrusted and --allow-unsigned
|
|
27996
|
+
override that, one install at a time.
|
|
27997
|
+
|
|
27998
|
+
A signature says who published a package and that nothing in it has changed
|
|
27999
|
+
since. It does not say the program is safe to run.`,
|
|
28000
|
+
async run(ctx) {
|
|
28001
|
+
const [subcommand, ...rest] = ctx.args;
|
|
28002
|
+
try {
|
|
28003
|
+
switch (subcommand) {
|
|
28004
|
+
case "install":
|
|
28005
|
+
return await install2(ctx, rest);
|
|
28006
|
+
case "remove":
|
|
28007
|
+
case "uninstall":
|
|
28008
|
+
return await remove(ctx, rest);
|
|
28009
|
+
case "list":
|
|
28010
|
+
return listInstalled2(ctx);
|
|
28011
|
+
case "info":
|
|
28012
|
+
return await info(ctx, rest);
|
|
28013
|
+
case "search":
|
|
28014
|
+
return await search(ctx, rest);
|
|
28015
|
+
case "available":
|
|
28016
|
+
return await available(ctx);
|
|
28017
|
+
case "trust":
|
|
28018
|
+
return await trust(ctx, rest);
|
|
28019
|
+
case void 0:
|
|
28020
|
+
case "--help":
|
|
28021
|
+
case "-h":
|
|
28022
|
+
ctx.line(pm.usage);
|
|
28023
|
+
return 0;
|
|
28024
|
+
default:
|
|
28025
|
+
ctx.warn(`pm: unknown command ${subcommand}`);
|
|
28026
|
+
return 2;
|
|
28027
|
+
}
|
|
28028
|
+
} catch (error) {
|
|
28029
|
+
ctx.warn(`pm: ${error.message}`);
|
|
28030
|
+
return error instanceof AppRegistryError ? 2 : 1;
|
|
28031
|
+
}
|
|
28032
|
+
}
|
|
28033
|
+
});
|
|
28034
|
+
async function install2(ctx, args) {
|
|
28035
|
+
if (args.length === 0) {
|
|
28036
|
+
ctx.warn("pm install: name it, as `pm install officesuite`");
|
|
28037
|
+
return 2;
|
|
28038
|
+
}
|
|
28039
|
+
const options = {
|
|
28040
|
+
allowUnsigned: args.includes("--allow-unsigned"),
|
|
28041
|
+
allowUntrusted: args.includes("--allow-untrusted") || args.includes("--allow-unsigned")
|
|
28042
|
+
};
|
|
28043
|
+
const names = args.filter((argument) => !argument.startsWith("--"));
|
|
28044
|
+
if (names.length === 0) {
|
|
28045
|
+
ctx.warn("pm install: name it, as `pm install officesuite`");
|
|
28046
|
+
return 2;
|
|
28047
|
+
}
|
|
28048
|
+
const index = await fetchIndex(ctx);
|
|
28049
|
+
for (const specifier of names) {
|
|
28050
|
+
const { name, version } = parseSpecifier(specifier);
|
|
28051
|
+
const release = selectRelease(index, name, version);
|
|
28052
|
+
await installRelease(ctx, index, release, /* @__PURE__ */ new Set(), options);
|
|
28053
|
+
}
|
|
28054
|
+
return 0;
|
|
28055
|
+
}
|
|
28056
|
+
async function installRelease(ctx, index, release, seen, options) {
|
|
28057
|
+
if (seen.has(release.name)) return;
|
|
28058
|
+
seen.add(release.name);
|
|
28059
|
+
for (const required of release.requires ?? []) {
|
|
28060
|
+
const { name, version } = parseSpecifier(required);
|
|
28061
|
+
if (installedApps(ctx).some((app) => app.name === name)) continue;
|
|
28062
|
+
await installRelease(ctx, index, selectRelease(index, name, version), seen, options);
|
|
28063
|
+
}
|
|
28064
|
+
const base2 = registryBase(ctx).replace(/\/$/, "");
|
|
28065
|
+
const url = `${base2}/${release.filename}`;
|
|
28066
|
+
ctx.line(`Fetching ${release.name} ${release.version}`);
|
|
28067
|
+
let archive = await download2(url);
|
|
28068
|
+
let digest2 = await digestHex("sha256", archive);
|
|
28069
|
+
if (digest2 !== release.sha256) {
|
|
28070
|
+
const retried = await download2(`${url}${url.includes("?") ? "&" : "?"}sha256=${release.sha256}`).catch(() => null);
|
|
28071
|
+
const retriedDigest = retried && await digestHex("sha256", retried);
|
|
28072
|
+
if (retried && retriedDigest === release.sha256) {
|
|
28073
|
+
archive = retried;
|
|
28074
|
+
digest2 = retriedDigest;
|
|
28075
|
+
} else {
|
|
28076
|
+
throw new AppRegistryError(
|
|
28077
|
+
`${release.filename} failed verification: the registry publishes sha256 ${release.sha256} but the download hashes to ${digest2}. ` + (retried ? "A retry past any cache returned the same bytes, so the registry and the file it names disagree." : "A retry past any cache could not be made; a cached copy of an older build would look like this, so clearing the cache is worth trying before the registry is blamed.")
|
|
28078
|
+
);
|
|
28079
|
+
}
|
|
28080
|
+
}
|
|
28081
|
+
const verified = await verify(ctx, archive, release, options);
|
|
28082
|
+
const root = `${APPS_ROOT}/${release.name}`;
|
|
28083
|
+
removeInstalled(ctx, release.name, { quiet: true });
|
|
28084
|
+
const manifest = unpack(ctx, archive, root, release, verified);
|
|
28085
|
+
const bins = linkBinaries(ctx, manifest, root);
|
|
28086
|
+
record(ctx, {
|
|
28087
|
+
...manifest,
|
|
28088
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28089
|
+
root,
|
|
28090
|
+
installedBins: bins,
|
|
28091
|
+
sha256: release.sha256,
|
|
28092
|
+
...verified?.signature ? {
|
|
28093
|
+
publisher: verified.signature.publisher ?? verified.trustedBy?.publisher ?? "unknown",
|
|
28094
|
+
publicKey: verified.signature.publicKey
|
|
28095
|
+
} : {}
|
|
28096
|
+
});
|
|
28097
|
+
ctx.line(`Installed ${manifest.name} ${manifest.version} in ${root}`);
|
|
28098
|
+
if (bins.length > 0) ctx.line(` commands: ${bins.join(", ")}`);
|
|
28099
|
+
for (const service of manifest.services ?? []) {
|
|
28100
|
+
ctx.line(
|
|
28101
|
+
` service: ${service.name} on port ${service.port}` + (service.path ? ` at ${service.path}` : "") + ` (start it with \`${service.command}\`)`
|
|
28102
|
+
);
|
|
28103
|
+
}
|
|
28104
|
+
if (manifest.docs) ctx.line(` docs: ${root}/${manifest.docs}`);
|
|
28105
|
+
}
|
|
28106
|
+
async function download2(url) {
|
|
28107
|
+
const response = await fetch(url);
|
|
28108
|
+
if (!response.ok) {
|
|
28109
|
+
throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
|
|
28110
|
+
}
|
|
28111
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
28112
|
+
}
|
|
28113
|
+
async function verify(ctx, archive, release, options) {
|
|
28114
|
+
let opened;
|
|
28115
|
+
try {
|
|
28116
|
+
opened = await openPackage(archive, trustedKeys(ctx));
|
|
28117
|
+
} catch (error) {
|
|
28118
|
+
if (options.allowUnsigned && /has no META\/manifest\.json/.test(error.message)) {
|
|
28119
|
+
ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
|
|
28120
|
+
return null;
|
|
28121
|
+
}
|
|
28122
|
+
throw asRegistryError(error);
|
|
28123
|
+
}
|
|
28124
|
+
if (!opened.signature) {
|
|
28125
|
+
if (!options.allowUnsigned) {
|
|
28126
|
+
throw new AppRegistryError(
|
|
28127
|
+
`${release.name} carries no signature, so its publisher cannot be established. Install it with --allow-unsigned if you got it from somewhere you trust.`
|
|
28128
|
+
);
|
|
28129
|
+
}
|
|
28130
|
+
ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
|
|
28131
|
+
return opened;
|
|
28132
|
+
}
|
|
28133
|
+
const print = await fingerprint(opened.signature.publicKey);
|
|
28134
|
+
if (!opened.trustedBy && !options.allowUntrusted) {
|
|
28135
|
+
throw new AppRegistryError(
|
|
28136
|
+
`${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.`
|
|
28137
|
+
);
|
|
28138
|
+
}
|
|
28139
|
+
ctx.line(
|
|
28140
|
+
opened.trustedBy ? ` signed by ${opened.trustedBy.publisher} (${print})` : ` signed by an untrusted key (${opened.signature.publisher ?? "unnamed"}, ${print})`
|
|
28141
|
+
);
|
|
28142
|
+
return opened;
|
|
28143
|
+
}
|
|
28144
|
+
function trustedKeys(ctx) {
|
|
28145
|
+
let stored = [];
|
|
28146
|
+
try {
|
|
28147
|
+
const parsed = JSON.parse(ctx.vfs.readText(TRUSTED_KEYS_PATH, ctx.cred));
|
|
28148
|
+
stored = Array.isArray(parsed.keys) ? parsed.keys : [];
|
|
28149
|
+
} catch {
|
|
28150
|
+
}
|
|
28151
|
+
const known = new Set(stored.map((key) => key.publicKey));
|
|
28152
|
+
return [...stored, ...BUILT_IN_PUBLISHERS.filter((key) => !known.has(key.publicKey))];
|
|
28153
|
+
}
|
|
28154
|
+
function writeTrustedKeys(ctx, keys) {
|
|
28155
|
+
ctx.vfs.mkdir("/etc/pm", { cred: ctx.cred, recursive: true });
|
|
28156
|
+
ctx.vfs.writeFile(
|
|
28157
|
+
TRUSTED_KEYS_PATH,
|
|
28158
|
+
`${JSON.stringify({ format: "sandboxedjs-trusted-keys", keys }, null, 2)}
|
|
28159
|
+
`,
|
|
28160
|
+
{ cred: ctx.cred, mode: 420 }
|
|
28161
|
+
);
|
|
28162
|
+
}
|
|
28163
|
+
async function trust(ctx, args) {
|
|
28164
|
+
const [action, ...rest] = args;
|
|
28165
|
+
const keys = trustedKeys(ctx);
|
|
28166
|
+
switch (action) {
|
|
28167
|
+
case "list":
|
|
28168
|
+
case void 0: {
|
|
28169
|
+
if (keys.length === 0) {
|
|
28170
|
+
ctx.line("No publisher keys are trusted; only unsigned installs are possible.");
|
|
28171
|
+
return 0;
|
|
28172
|
+
}
|
|
28173
|
+
for (const key of keys) {
|
|
28174
|
+
ctx.line(`${key.publisher} ${await fingerprint(key.publicKey)}`);
|
|
28175
|
+
}
|
|
28176
|
+
return 0;
|
|
28177
|
+
}
|
|
28178
|
+
case "add": {
|
|
28179
|
+
const [publisher, publicKey] = rest;
|
|
28180
|
+
if (!publisher || !publicKey) {
|
|
28181
|
+
ctx.warn("pm trust add: give a publisher name and its base64 public key");
|
|
28182
|
+
return 2;
|
|
28183
|
+
}
|
|
28184
|
+
const next = keys.filter((key) => key.publicKey !== publicKey);
|
|
28185
|
+
next.push({ publisher, publicKey, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
28186
|
+
writeTrustedKeys(ctx, next);
|
|
28187
|
+
ctx.line(`Trusting ${publisher} (${await fingerprint(publicKey)})`);
|
|
28188
|
+
return 0;
|
|
28189
|
+
}
|
|
28190
|
+
case "remove": {
|
|
28191
|
+
const [publisher] = rest;
|
|
28192
|
+
const next = keys.filter((key) => key.publisher !== publisher);
|
|
28193
|
+
if (next.length === keys.length) {
|
|
28194
|
+
ctx.warn(`pm: no trusted key named ${publisher}`);
|
|
28195
|
+
return 1;
|
|
28196
|
+
}
|
|
28197
|
+
writeTrustedKeys(ctx, next);
|
|
28198
|
+
ctx.line(`Removed ${publisher}`);
|
|
28199
|
+
return 0;
|
|
28200
|
+
}
|
|
28201
|
+
default:
|
|
28202
|
+
ctx.warn(`pm trust: unknown action ${action}`);
|
|
28203
|
+
return 2;
|
|
28204
|
+
}
|
|
28205
|
+
}
|
|
28206
|
+
function unpack(ctx, archive, root, release, verified) {
|
|
28207
|
+
const entries = verified?.entries ?? readZip(archive);
|
|
28208
|
+
const manifest = verified?.manifest.app ?? readAppManifest(entries, release);
|
|
28209
|
+
if (manifest.name !== release.name || manifest.version !== release.version) {
|
|
28210
|
+
throw new AppRegistryError(
|
|
28211
|
+
`${release.filename} contains ${manifest.name} ${manifest.version}, but the registry lists it as ${release.name} ${release.version}`
|
|
28212
|
+
);
|
|
28213
|
+
}
|
|
28214
|
+
ctx.vfs.mkdir(root, { cred: ctx.cred, recursive: true });
|
|
28215
|
+
for (const entry of entries) {
|
|
28216
|
+
if (entry.name.startsWith("META/")) continue;
|
|
28217
|
+
const target = resolveInside(root, entry.name);
|
|
28218
|
+
if (entry.isDirectory) {
|
|
28219
|
+
ctx.vfs.mkdir(target, { cred: ctx.cred, recursive: true });
|
|
28220
|
+
continue;
|
|
28221
|
+
}
|
|
28222
|
+
const parent = target.slice(0, target.lastIndexOf("/"));
|
|
28223
|
+
if (parent) ctx.vfs.mkdir(parent, { cred: ctx.cred, recursive: true });
|
|
28224
|
+
const executable = (entry.mode & 73) !== 0 || (manifest.bins ?? []).some((binary) => binary.path === entry.name);
|
|
28225
|
+
ctx.vfs.writeFile(target, entry.data(), {
|
|
28226
|
+
cred: ctx.cred,
|
|
28227
|
+
mode: executable ? 493 : 420
|
|
28228
|
+
});
|
|
28229
|
+
}
|
|
28230
|
+
return manifest;
|
|
28231
|
+
}
|
|
28232
|
+
function readAppManifest(entries, release) {
|
|
28233
|
+
const entry = entries.find((candidate) => candidate.name === "app.json");
|
|
28234
|
+
if (!entry) throw new AppRegistryError(`${release.filename} has no app.json`);
|
|
28235
|
+
return validateManifest2(
|
|
28236
|
+
JSON.parse(new TextDecoder().decode(entry.data())),
|
|
28237
|
+
`${release.name}'s app.json`
|
|
28238
|
+
);
|
|
28239
|
+
}
|
|
28240
|
+
function resolveInside(root, name) {
|
|
28241
|
+
const clean2 = name.replace(/\\/g, "/");
|
|
28242
|
+
if (clean2.startsWith("/")) {
|
|
28243
|
+
throw new AppRegistryError(`archive entry ${name} is an absolute path`);
|
|
28244
|
+
}
|
|
28245
|
+
const parts = [];
|
|
28246
|
+
for (const part of clean2.split("/")) {
|
|
28247
|
+
if (part === "" || part === ".") continue;
|
|
28248
|
+
if (part === "..") {
|
|
28249
|
+
throw new AppRegistryError(`archive entry ${name} escapes the package directory`);
|
|
28250
|
+
}
|
|
28251
|
+
parts.push(part);
|
|
28252
|
+
}
|
|
28253
|
+
return `${root}/${parts.join("/")}`;
|
|
28254
|
+
}
|
|
28255
|
+
function linkBinaries(ctx, manifest, root) {
|
|
28256
|
+
const installed3 = [];
|
|
28257
|
+
if ((manifest.bins ?? []).length === 0) return installed3;
|
|
28258
|
+
ctx.vfs.mkdir(APP_BIN_DIR, { cred: ctx.cred, recursive: true });
|
|
28259
|
+
for (const binary of manifest.bins ?? []) {
|
|
28260
|
+
const target = resolveInside(root, binary.path);
|
|
28261
|
+
const wrapper = `${APP_BIN_DIR}/${binary.name}`;
|
|
28262
|
+
ctx.vfs.writeFile(
|
|
28263
|
+
wrapper,
|
|
28264
|
+
`#!/bin/sh
|
|
28265
|
+
# installed by pm for ${manifest.name} ${manifest.version}
|
|
28266
|
+
exec ${target} "$@"
|
|
28267
|
+
`,
|
|
28268
|
+
{ cred: ctx.cred, mode: 493 }
|
|
28269
|
+
);
|
|
28270
|
+
installed3.push(binary.name);
|
|
28271
|
+
}
|
|
28272
|
+
return installed3;
|
|
28273
|
+
}
|
|
28274
|
+
async function remove(ctx, args) {
|
|
28275
|
+
if (args.length === 0) {
|
|
28276
|
+
ctx.warn("pm remove: name it");
|
|
28277
|
+
return 2;
|
|
28278
|
+
}
|
|
28279
|
+
let code = 0;
|
|
28280
|
+
for (const specifier of args) {
|
|
28281
|
+
const { name } = parseSpecifier(specifier);
|
|
28282
|
+
if (!removeInstalled(ctx, name, { quiet: false })) {
|
|
28283
|
+
ctx.warn(`pm: ${name} is not installed`);
|
|
28284
|
+
code = 1;
|
|
28285
|
+
} else {
|
|
28286
|
+
ctx.line(`Removed ${name}`);
|
|
28287
|
+
}
|
|
28288
|
+
}
|
|
28289
|
+
return code;
|
|
28290
|
+
}
|
|
28291
|
+
function removeInstalled(ctx, name, options) {
|
|
28292
|
+
const apps = installedApps(ctx);
|
|
28293
|
+
const app = apps.find((entry) => entry.name === name);
|
|
28294
|
+
if (!app) return false;
|
|
28295
|
+
for (const binary of app.installedBins) {
|
|
28296
|
+
try {
|
|
28297
|
+
ctx.vfs.unlink(`${APP_BIN_DIR}/${binary}`, ctx.cred);
|
|
28298
|
+
} catch {
|
|
28299
|
+
}
|
|
28300
|
+
}
|
|
28301
|
+
try {
|
|
28302
|
+
removeTree(ctx, app.root);
|
|
28303
|
+
} catch (error) {
|
|
28304
|
+
if (!options.quiet) ctx.warn(`pm: could not remove ${app.root}: ${error.message}`);
|
|
28305
|
+
}
|
|
28306
|
+
writeState(ctx, apps.filter((entry) => entry.name !== name));
|
|
28307
|
+
return true;
|
|
28308
|
+
}
|
|
28309
|
+
function removeTree(ctx, path) {
|
|
28310
|
+
let stats;
|
|
28311
|
+
try {
|
|
28312
|
+
stats = ctx.vfs.stat(path, { cred: ctx.cred });
|
|
28313
|
+
} catch {
|
|
28314
|
+
return;
|
|
28315
|
+
}
|
|
28316
|
+
if (stats.isDirectory()) {
|
|
28317
|
+
for (const entry of ctx.vfs.readdir(path, ctx.cred)) {
|
|
28318
|
+
removeTree(ctx, `${path}/${entry}`);
|
|
28319
|
+
}
|
|
28320
|
+
ctx.vfs.rmdir(path, ctx.cred);
|
|
28321
|
+
return;
|
|
28322
|
+
}
|
|
28323
|
+
ctx.vfs.unlink(path, ctx.cred);
|
|
28324
|
+
}
|
|
28325
|
+
function listInstalled2(ctx) {
|
|
28326
|
+
const apps = installedApps(ctx);
|
|
28327
|
+
if (apps.length === 0) {
|
|
28328
|
+
ctx.line("No applications installed. `pm available` lists what there is.");
|
|
28329
|
+
return 0;
|
|
28330
|
+
}
|
|
28331
|
+
for (const app of apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
28332
|
+
const commands12 = app.installedBins.length > 0 ? ` (${app.installedBins.join(", ")})` : "";
|
|
28333
|
+
ctx.line(`${app.name} ${app.version}${commands12}`);
|
|
28334
|
+
}
|
|
28335
|
+
return 0;
|
|
28336
|
+
}
|
|
28337
|
+
async function info(ctx, args) {
|
|
28338
|
+
const name = args[0];
|
|
28339
|
+
if (!name) {
|
|
28340
|
+
ctx.warn("pm info: name it");
|
|
28341
|
+
return 2;
|
|
28342
|
+
}
|
|
28343
|
+
const local = installedApps(ctx).find((app) => app.name === name);
|
|
28344
|
+
if (local) {
|
|
28345
|
+
ctx.line(`${local.name} ${local.version} (installed ${local.installedAt})`);
|
|
28346
|
+
if (local.summary) ctx.line(` ${local.summary}`);
|
|
28347
|
+
ctx.line(` root: ${local.root}`);
|
|
28348
|
+
if (local.installedBins.length > 0) ctx.line(` commands: ${local.installedBins.join(", ")}`);
|
|
28349
|
+
for (const service of local.services ?? []) {
|
|
28350
|
+
ctx.line(` service: ${service.name} on port ${service.port}${service.path ?? ""}`);
|
|
28351
|
+
}
|
|
28352
|
+
if (local.docs) ctx.line(` docs: ${local.root}/${local.docs}`);
|
|
28353
|
+
return 0;
|
|
28354
|
+
}
|
|
28355
|
+
const release = selectRelease(await fetchIndex(ctx), name);
|
|
28356
|
+
ctx.line(`${release.name} ${release.version} (not installed)`);
|
|
28357
|
+
if (release.summary) ctx.line(` ${release.summary}`);
|
|
28358
|
+
ctx.line(` size: ${Math.round(release.size / 1024)} kB`);
|
|
28359
|
+
if ((release.bins ?? []).length > 0) {
|
|
28360
|
+
ctx.line(` commands: ${(release.bins ?? []).map((b) => b.name).join(", ")}`);
|
|
28361
|
+
}
|
|
28362
|
+
return 0;
|
|
28363
|
+
}
|
|
28364
|
+
async function search(ctx, args) {
|
|
28365
|
+
const term = (args[0] ?? "").toLowerCase();
|
|
28366
|
+
const index = await fetchIndex(ctx);
|
|
28367
|
+
const matches3 = index.apps.filter(
|
|
28368
|
+
(app) => app.name.toLowerCase().includes(term) || (app.summary ?? "").toLowerCase().includes(term)
|
|
28369
|
+
);
|
|
28370
|
+
if (matches3.length === 0) {
|
|
28371
|
+
ctx.line(`nothing matches ${JSON.stringify(args[0] ?? "")}`);
|
|
28372
|
+
return 1;
|
|
28373
|
+
}
|
|
28374
|
+
for (const app of matches3) ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
|
|
28375
|
+
return 0;
|
|
28376
|
+
}
|
|
28377
|
+
async function available(ctx) {
|
|
28378
|
+
const index = await fetchIndex(ctx);
|
|
28379
|
+
for (const app of index.apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
28380
|
+
ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
|
|
28381
|
+
}
|
|
28382
|
+
return 0;
|
|
28383
|
+
}
|
|
28384
|
+
async function fetchIndex(ctx) {
|
|
28385
|
+
const base2 = registryBase(ctx).replace(/\/$/, "");
|
|
28386
|
+
const url = `${base2}/index.json`;
|
|
28387
|
+
let response;
|
|
28388
|
+
try {
|
|
28389
|
+
response = await fetch(url);
|
|
28390
|
+
} catch (error) {
|
|
28391
|
+
throw new AppRegistryError(
|
|
28392
|
+
`cannot reach the application registry at ${url}: ${error.message}`
|
|
28393
|
+
);
|
|
28394
|
+
}
|
|
28395
|
+
if (!response.ok) {
|
|
28396
|
+
throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
|
|
28397
|
+
}
|
|
28398
|
+
return parseIndex(await response.text(), url);
|
|
28399
|
+
}
|
|
28400
|
+
function installedApps(ctx) {
|
|
28401
|
+
try {
|
|
28402
|
+
const text2 = ctx.vfs.readText(APP_STATE, ctx.cred);
|
|
28403
|
+
const parsed = JSON.parse(text2);
|
|
28404
|
+
return Array.isArray(parsed.apps) ? parsed.apps : [];
|
|
28405
|
+
} catch {
|
|
28406
|
+
return [];
|
|
28407
|
+
}
|
|
28408
|
+
}
|
|
28409
|
+
function record(ctx, app) {
|
|
28410
|
+
const apps = installedApps(ctx).filter((entry) => entry.name !== app.name);
|
|
28411
|
+
apps.push(app);
|
|
28412
|
+
writeState(ctx, apps);
|
|
28413
|
+
}
|
|
28414
|
+
function writeState(ctx, apps) {
|
|
28415
|
+
const directory2 = APP_STATE.slice(0, APP_STATE.lastIndexOf("/"));
|
|
28416
|
+
ctx.vfs.mkdir(directory2, { cred: ctx.cred, recursive: true });
|
|
28417
|
+
ctx.vfs.writeFile(
|
|
28418
|
+
APP_STATE,
|
|
28419
|
+
`${JSON.stringify({ format: "sandboxedjs-installed-apps", apps }, null, 2)}
|
|
28420
|
+
`,
|
|
28421
|
+
{ cred: ctx.cred, mode: 420 }
|
|
28422
|
+
);
|
|
28423
|
+
}
|
|
28424
|
+
function appCommands() {
|
|
28425
|
+
return [pm];
|
|
28426
|
+
}
|
|
28427
|
+
|
|
27705
28428
|
// src/bin/index.ts
|
|
27706
28429
|
function allCommands() {
|
|
27707
28430
|
return [
|
|
@@ -27721,7 +28444,8 @@ function allCommands() {
|
|
|
27721
28444
|
...ffmpegCommands(),
|
|
27722
28445
|
...wasiCommands(),
|
|
27723
28446
|
...packageCommands(),
|
|
27724
|
-
...browserCommands()
|
|
28447
|
+
...browserCommands(),
|
|
28448
|
+
...appCommands()
|
|
27725
28449
|
];
|
|
27726
28450
|
}
|
|
27727
28451
|
function installUserland(kernel) {
|
|
@@ -31440,7 +32164,7 @@ ${indent(detail, 4)}
|
|
|
31440
32164
|
if (detail) host2.write(`${indent(detail, depth * 2 + 2)}
|
|
31441
32165
|
`);
|
|
31442
32166
|
};
|
|
31443
|
-
const
|
|
32167
|
+
const record2 = (outcome, name, error) => {
|
|
31444
32168
|
counts.tests += 1;
|
|
31445
32169
|
if (outcome === "pass") counts.pass += 1;
|
|
31446
32170
|
else if (outcome === "fail") {
|
|
@@ -31462,13 +32186,13 @@ ${indent(detail, 4)}
|
|
|
31462
32186
|
if (!matchesPattern2(display)) return true;
|
|
31463
32187
|
if (runOnly && !options.only) return true;
|
|
31464
32188
|
if (options.skip) {
|
|
31465
|
-
|
|
32189
|
+
record2("skip", display);
|
|
31466
32190
|
report(depth, "skip", name, started);
|
|
31467
32191
|
return true;
|
|
31468
32192
|
}
|
|
31469
32193
|
if (!fn) {
|
|
31470
32194
|
const outcome = options.todo ? "todo" : "skip";
|
|
31471
|
-
|
|
32195
|
+
record2(outcome, display);
|
|
31472
32196
|
report(depth, outcome, name, started);
|
|
31473
32197
|
return true;
|
|
31474
32198
|
}
|
|
@@ -31532,21 +32256,21 @@ ${indent(detail, 4)}
|
|
|
31532
32256
|
controller.abort();
|
|
31533
32257
|
}
|
|
31534
32258
|
if (skipped) {
|
|
31535
|
-
|
|
32259
|
+
record2("skip", display);
|
|
31536
32260
|
report(depth, "skip", name, started);
|
|
31537
32261
|
return true;
|
|
31538
32262
|
}
|
|
31539
32263
|
if (error !== void 0) {
|
|
31540
32264
|
if (todo) {
|
|
31541
|
-
|
|
32265
|
+
record2("todo", display);
|
|
31542
32266
|
report(depth, "todo", name, started);
|
|
31543
32267
|
return true;
|
|
31544
32268
|
}
|
|
31545
|
-
|
|
32269
|
+
record2("fail", display, error);
|
|
31546
32270
|
report(depth, "fail", name, started, errorText(error));
|
|
31547
32271
|
return false;
|
|
31548
32272
|
}
|
|
31549
|
-
|
|
32273
|
+
record2(todo ? "todo" : "pass", display);
|
|
31550
32274
|
report(depth, todo ? "todo" : "pass", name, started);
|
|
31551
32275
|
return true;
|
|
31552
32276
|
};
|
|
@@ -31561,7 +32285,7 @@ ${indent(detail, 4)}
|
|
|
31561
32285
|
else host2.write(`${" ".repeat(depth)}\u25B6 ${suite.name}
|
|
31562
32286
|
`);
|
|
31563
32287
|
if (suite.options.skip) {
|
|
31564
|
-
for (const child of suite.children) if (child.kind === "test")
|
|
32288
|
+
for (const child of suite.children) if (child.kind === "test") record2("skip", fullName(child));
|
|
31565
32289
|
report(depth, "skip", suite.name, started);
|
|
31566
32290
|
return true;
|
|
31567
32291
|
}
|
|
@@ -31573,7 +32297,7 @@ ${indent(detail, 4)}
|
|
|
31573
32297
|
} catch (error) {
|
|
31574
32298
|
for (const child of suite.children) {
|
|
31575
32299
|
if (child.kind === "test") {
|
|
31576
|
-
|
|
32300
|
+
record2("cancelled", fullName(child), error);
|
|
31577
32301
|
report(depth + (isRoot ? 0 : 1), "cancelled", child.name, started, errorText(error));
|
|
31578
32302
|
}
|
|
31579
32303
|
}
|
|
@@ -37031,6 +37755,14 @@ var Container = class _Container {
|
|
|
37031
37755
|
installUserland(kernel);
|
|
37032
37756
|
const container = new _Container(kernel, pod, opts);
|
|
37033
37757
|
if (opts.files) container.mountSync(opts.files, { cwd: opts.cwd ?? "/" });
|
|
37758
|
+
if (opts.modules && opts.modules.length > 0) {
|
|
37759
|
+
const result = await container.exec(`pm install ${opts.modules.join(" ")}`);
|
|
37760
|
+
if (result.exitCode !== 0) {
|
|
37761
|
+
throw new Error(
|
|
37762
|
+
`could not install ${opts.modules.join(", ")}: ${(result.stderr || result.stdout).trim() || `pm exited ${result.exitCode}`}`
|
|
37763
|
+
);
|
|
37764
|
+
}
|
|
37765
|
+
}
|
|
37034
37766
|
return container;
|
|
37035
37767
|
}
|
|
37036
37768
|
// ── file seeding ──────────────────────────────────────────────────────────
|