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.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",
|
|
@@ -20941,7 +20941,7 @@ var wheels_default = {
|
|
|
20941
20941
|
|
|
20942
20942
|
// package.json
|
|
20943
20943
|
var package_default = {
|
|
20944
|
-
version: "0.1.
|
|
20944
|
+
version: "0.1.97"};
|
|
20945
20945
|
|
|
20946
20946
|
// src/python/extension-abi.ts
|
|
20947
20947
|
var EXTENSION_ABI = {
|
|
@@ -23669,7 +23669,7 @@ async function resolve2(options) {
|
|
|
23669
23669
|
const clauses = (state.constraints.get(name) ?? []).flatMap((entry) => entry.spec.map((s) => `${s.op}${s.version}`)).sort();
|
|
23670
23670
|
return `${name}|${[...new Set(clauses)].join(",")}`;
|
|
23671
23671
|
};
|
|
23672
|
-
const
|
|
23672
|
+
const search2 = async (queue, state) => {
|
|
23673
23673
|
checkBudget();
|
|
23674
23674
|
if (queue.length === 0) return state;
|
|
23675
23675
|
const [edge, ...rest] = queue;
|
|
@@ -23677,7 +23677,7 @@ async function resolve2(options) {
|
|
|
23677
23677
|
const name = requirement.name;
|
|
23678
23678
|
if (!markerApplies(requirement.marker, environment, extras)) {
|
|
23679
23679
|
if (requirement.marker) skipped.push({ name, marker: requirement.marker });
|
|
23680
|
-
return
|
|
23680
|
+
return search2(rest, state);
|
|
23681
23681
|
}
|
|
23682
23682
|
const previous = state.constraints.get(name) ?? [];
|
|
23683
23683
|
const constraints = new Map(state.constraints);
|
|
@@ -23694,7 +23694,7 @@ async function resolve2(options) {
|
|
|
23694
23694
|
explain(name, next.constraints, already.version)
|
|
23695
23695
|
);
|
|
23696
23696
|
}
|
|
23697
|
-
return
|
|
23697
|
+
return search2(rest, next);
|
|
23698
23698
|
}
|
|
23699
23699
|
const signature = signatureOf(name, next);
|
|
23700
23700
|
const learned = nogoods.get(signature);
|
|
@@ -23757,7 +23757,7 @@ async function resolve2(options) {
|
|
|
23757
23757
|
const chosen = new Map(next.chosen);
|
|
23758
23758
|
chosen.set(name, { ...candidate, requires });
|
|
23759
23759
|
try {
|
|
23760
|
-
return await
|
|
23760
|
+
return await search2([...edges, ...rest], { chosen, constraints: next.constraints });
|
|
23761
23761
|
} catch (error) {
|
|
23762
23762
|
if (!(error instanceof ResolutionError)) throw error;
|
|
23763
23763
|
if (/cancelled|budget/.test(error.message)) throw error;
|
|
@@ -23772,12 +23772,12 @@ async function resolve2(options) {
|
|
|
23772
23772
|
if (specifiers.length === 0 || !markerApplies(marker, environment, root.extras)) continue;
|
|
23773
23773
|
seeded.set(name, [...seeded.get(name) ?? [], { spec: specifiers, origin: root.origin }]);
|
|
23774
23774
|
}
|
|
23775
|
-
const solved = await
|
|
23775
|
+
const solved = await search2(roots, { chosen: /* @__PURE__ */ new Map(), constraints: seeded });
|
|
23776
23776
|
const distributions = [...solved.chosen.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
23777
23777
|
const reported = new Map(skipped.map((s) => [`${s.name}\0${s.marker}`, s]));
|
|
23778
23778
|
return { distributions, skipped: [...reported.values()] };
|
|
23779
23779
|
}
|
|
23780
|
-
function explain(name, constraints, chosenVersion,
|
|
23780
|
+
function explain(name, constraints, chosenVersion, available2 = []) {
|
|
23781
23781
|
const lines = [];
|
|
23782
23782
|
for (const entry of constraints.get(name) ?? []) {
|
|
23783
23783
|
if (entry.spec.length === 0) continue;
|
|
@@ -23785,9 +23785,9 @@ function explain(name, constraints, chosenVersion, available = []) {
|
|
|
23785
23785
|
lines.push(`${entry.origin} requires ${name} ${text2}`);
|
|
23786
23786
|
}
|
|
23787
23787
|
if (chosenVersion) lines.push(`but ${name} ${chosenVersion} is already selected`);
|
|
23788
|
-
if (
|
|
23789
|
-
const versions = [...new Set(
|
|
23790
|
-
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 ? ", ..." : ""}`);
|
|
23791
23791
|
}
|
|
23792
23792
|
return lines;
|
|
23793
23793
|
}
|
|
@@ -23916,9 +23916,7 @@ async function installRequirements(options) {
|
|
|
23916
23916
|
continue;
|
|
23917
23917
|
}
|
|
23918
23918
|
options.progress.downloading(distribution.name, distribution.version);
|
|
23919
|
-
const bytes2 = await options.client
|
|
23920
|
-
requireArchive(distribution, bytes2);
|
|
23921
|
-
verifyDigest(distribution, bytes2);
|
|
23919
|
+
const bytes2 = await download(options.client, distribution);
|
|
23922
23920
|
const files = stageWheel(readZip(bytes2));
|
|
23923
23921
|
staged.push(...distribution.kind === "substituted" ? substituteFiles(distribution.name, files, SITE_PACKAGES) : files);
|
|
23924
23922
|
installed3.push({ name: distribution.name, version: distribution.version });
|
|
@@ -23934,18 +23932,41 @@ function requireArchive(distribution, bytes2) {
|
|
|
23934
23932
|
`${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)}`)
|
|
23935
23933
|
);
|
|
23936
23934
|
}
|
|
23937
|
-
function
|
|
23935
|
+
async function download(client, distribution) {
|
|
23936
|
+
const bytes2 = await client.bytes(distribution.url, { timeoutMs: 12e4 });
|
|
23937
|
+
requireArchive(distribution, bytes2);
|
|
23938
|
+
const mismatch = digestMismatch(distribution, bytes2);
|
|
23939
|
+
if (!mismatch) return bytes2;
|
|
23940
|
+
const fresh = await refetch(client, distribution);
|
|
23941
|
+
if (fresh && !digestMismatch(distribution, fresh)) {
|
|
23942
|
+
requireArchive(distribution, fresh);
|
|
23943
|
+
return fresh;
|
|
23944
|
+
}
|
|
23945
|
+
throw new Error(
|
|
23946
|
+
`${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."}`
|
|
23947
|
+
);
|
|
23948
|
+
}
|
|
23949
|
+
async function refetch(client, distribution) {
|
|
23950
|
+
const separator = distribution.url.includes("?") ? "&" : "?";
|
|
23951
|
+
try {
|
|
23952
|
+
const bytes2 = await client.bytes(
|
|
23953
|
+
`${distribution.url}${separator}sha256=${distribution.sha256}`,
|
|
23954
|
+
{ timeoutMs: 6e4 }
|
|
23955
|
+
);
|
|
23956
|
+
return bytes2.length > 1 && bytes2[0] === 80 && bytes2[1] === 75 ? bytes2 : null;
|
|
23957
|
+
} catch {
|
|
23958
|
+
return null;
|
|
23959
|
+
}
|
|
23960
|
+
}
|
|
23961
|
+
function digestMismatch(distribution, bytes2) {
|
|
23938
23962
|
if (!distribution.sha256) {
|
|
23939
23963
|
throw new Error(
|
|
23940
23964
|
`${distribution.filename} was published without a sha256 digest, so it cannot be verified`
|
|
23941
23965
|
);
|
|
23942
23966
|
}
|
|
23943
23967
|
const actual = [...sha256(bytes2)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
23944
|
-
if (actual
|
|
23945
|
-
|
|
23946
|
-
`${distribution.filename} failed verification: the index published sha256 ${distribution.sha256} but the download hashes to ${actual}`
|
|
23947
|
-
);
|
|
23948
|
-
}
|
|
23968
|
+
if (actual === distribution.sha256) return null;
|
|
23969
|
+
return `${distribution.filename} failed verification: the index published sha256 ${distribution.sha256} but the download hashes to ${actual}.`;
|
|
23949
23970
|
}
|
|
23950
23971
|
function stageWheel(entries, resolved) {
|
|
23951
23972
|
const staged = [];
|
|
@@ -26384,9 +26405,9 @@ function parsePax(data) {
|
|
|
26384
26405
|
if (space < 0) break;
|
|
26385
26406
|
const length = Number(value.slice(offset, space));
|
|
26386
26407
|
if (!Number.isFinite(length) || length <= 0) break;
|
|
26387
|
-
const
|
|
26388
|
-
const equal2 =
|
|
26389
|
-
if (equal2 > 0) result[
|
|
26408
|
+
const record2 = value.slice(space + 1, offset + length - 1);
|
|
26409
|
+
const equal2 = record2.indexOf("=");
|
|
26410
|
+
if (equal2 > 0) result[record2.slice(0, equal2)] = record2.slice(equal2 + 1);
|
|
26390
26411
|
offset += length;
|
|
26391
26412
|
}
|
|
26392
26413
|
return result;
|
|
@@ -27166,27 +27187,27 @@ var apt = defineCommand({
|
|
|
27166
27187
|
switch (subcommand) {
|
|
27167
27188
|
case "list": {
|
|
27168
27189
|
const installedOnly = rest.includes("--installed");
|
|
27169
|
-
for (const [name,
|
|
27170
|
-
const
|
|
27171
|
-
if (installedOnly && !
|
|
27172
|
-
ctx.line(`${name}/stable ${
|
|
27190
|
+
for (const [name, info2] of Object.entries(provided)) {
|
|
27191
|
+
const available2 = ctx.kernel.which(name === "python3-pip" ? "pip" : name === "gawk" ? "awk" : name, ctx.cwd, ctx.env, ctx.cred) !== null;
|
|
27192
|
+
if (installedOnly && !available2) continue;
|
|
27193
|
+
ctx.line(`${name}/stable ${info2.version} amd64${available2 ? " [installed]" : ""}`);
|
|
27173
27194
|
}
|
|
27174
27195
|
return 0;
|
|
27175
27196
|
}
|
|
27176
27197
|
case "show": {
|
|
27177
27198
|
let status = 0;
|
|
27178
27199
|
for (const name of rest) {
|
|
27179
|
-
const
|
|
27180
|
-
if (!
|
|
27200
|
+
const info2 = provided[name];
|
|
27201
|
+
if (!info2) {
|
|
27181
27202
|
ctx.warn(`E: No packages found matching ${name}`);
|
|
27182
27203
|
status = 1;
|
|
27183
27204
|
continue;
|
|
27184
27205
|
}
|
|
27185
27206
|
ctx.line(`Package: ${name}`);
|
|
27186
|
-
ctx.line(`Version: ${
|
|
27207
|
+
ctx.line(`Version: ${info2.version}`);
|
|
27187
27208
|
ctx.line(`Priority: optional`);
|
|
27188
27209
|
ctx.line(`Architecture: amd64`);
|
|
27189
|
-
ctx.line(`Description: ${
|
|
27210
|
+
ctx.line(`Description: ${info2.description}`);
|
|
27190
27211
|
ctx.line("");
|
|
27191
27212
|
}
|
|
27192
27213
|
return status;
|
|
@@ -27217,10 +27238,10 @@ var apt = defineCommand({
|
|
|
27217
27238
|
return 100;
|
|
27218
27239
|
case "search": {
|
|
27219
27240
|
const pattern = new RegExp(rest[0] ?? "", "i");
|
|
27220
|
-
for (const [name,
|
|
27221
|
-
if (pattern.test(name) || pattern.test(
|
|
27222
|
-
ctx.line(`${name}/stable ${
|
|
27223
|
-
ctx.line(` ${
|
|
27241
|
+
for (const [name, info2] of Object.entries(provided)) {
|
|
27242
|
+
if (pattern.test(name) || pattern.test(info2.description)) {
|
|
27243
|
+
ctx.line(`${name}/stable ${info2.version} amd64`);
|
|
27244
|
+
ctx.line(` ${info2.description}`);
|
|
27224
27245
|
ctx.line("");
|
|
27225
27246
|
}
|
|
27226
27247
|
}
|
|
@@ -27685,6 +27706,708 @@ function browserCommands() {
|
|
|
27685
27706
|
];
|
|
27686
27707
|
}
|
|
27687
27708
|
|
|
27709
|
+
// src/apps/pm.ts
|
|
27710
|
+
init_binary();
|
|
27711
|
+
init_zip();
|
|
27712
|
+
|
|
27713
|
+
// src/apps/registry.ts
|
|
27714
|
+
var APPS_ROOT = "/opt";
|
|
27715
|
+
var APP_BIN_DIR = "/usr/local/bin";
|
|
27716
|
+
var APP_STATE = "/var/lib/pm/installed.json";
|
|
27717
|
+
var DEFAULT_REGISTRY = "https://sandboxedjswheels.pages.dev/apps";
|
|
27718
|
+
var BUILT_IN_PUBLISHERS = [
|
|
27719
|
+
{
|
|
27720
|
+
publisher: "SandboxedJs",
|
|
27721
|
+
publicKey: "MCowBQYDK2VwAyEAuyxKMeyeO/05mr5Qu8vc+Dd9yMG2/oKDwt7KA+C9Mj4="
|
|
27722
|
+
}
|
|
27723
|
+
];
|
|
27724
|
+
var AppRegistryError = class extends Error {
|
|
27725
|
+
};
|
|
27726
|
+
function parseIndex(text2, source) {
|
|
27727
|
+
let parsed;
|
|
27728
|
+
try {
|
|
27729
|
+
parsed = JSON.parse(text2);
|
|
27730
|
+
} catch (error) {
|
|
27731
|
+
throw new AppRegistryError(`${source} is not JSON: ${error.message}`);
|
|
27732
|
+
}
|
|
27733
|
+
const index = parsed;
|
|
27734
|
+
if (index?.format !== "sandboxedjs-app-index") {
|
|
27735
|
+
throw new AppRegistryError(
|
|
27736
|
+
`${source} is not an application index (its format is ${JSON.stringify(index?.format ?? null)})`
|
|
27737
|
+
);
|
|
27738
|
+
}
|
|
27739
|
+
if (!Array.isArray(index.apps)) {
|
|
27740
|
+
throw new AppRegistryError(`${source} has no apps`);
|
|
27741
|
+
}
|
|
27742
|
+
for (const app of index.apps) {
|
|
27743
|
+
for (const field of ["name", "version", "filename", "sha256"]) {
|
|
27744
|
+
if (typeof app[field] !== "string" || !app[field]) {
|
|
27745
|
+
throw new AppRegistryError(`${source}: an entry is missing ${field}`);
|
|
27746
|
+
}
|
|
27747
|
+
}
|
|
27748
|
+
}
|
|
27749
|
+
return index;
|
|
27750
|
+
}
|
|
27751
|
+
function selectRelease(index, name, version) {
|
|
27752
|
+
const matches3 = index.apps.filter((app) => app.name === name);
|
|
27753
|
+
if (matches3.length === 0) {
|
|
27754
|
+
const names = index.apps.map((app) => app.name).sort();
|
|
27755
|
+
throw new AppRegistryError(
|
|
27756
|
+
`no application named ${name}. The registry has: ${names.join(", ") || "nothing"}`
|
|
27757
|
+
);
|
|
27758
|
+
}
|
|
27759
|
+
if (version) {
|
|
27760
|
+
const exact = matches3.find((app) => app.version === version);
|
|
27761
|
+
if (!exact) {
|
|
27762
|
+
const versions = matches3.map((app) => app.version).join(", ");
|
|
27763
|
+
throw new AppRegistryError(`${name} has no version ${version}; it has ${versions}`);
|
|
27764
|
+
}
|
|
27765
|
+
return exact;
|
|
27766
|
+
}
|
|
27767
|
+
return matches3.slice().sort((a, b) => compareVersions2(b.version, a.version))[0];
|
|
27768
|
+
}
|
|
27769
|
+
function compareVersions2(a, b) {
|
|
27770
|
+
const left = a.split(/[.-]/);
|
|
27771
|
+
const right = b.split(/[.-]/);
|
|
27772
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
27773
|
+
const x = left[index] ?? "";
|
|
27774
|
+
const y = right[index] ?? "";
|
|
27775
|
+
const nx = Number(x);
|
|
27776
|
+
const ny = Number(y);
|
|
27777
|
+
if (Number.isFinite(nx) && Number.isFinite(ny) && x !== "" && y !== "") {
|
|
27778
|
+
if (nx !== ny) return nx - ny;
|
|
27779
|
+
continue;
|
|
27780
|
+
}
|
|
27781
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
27782
|
+
}
|
|
27783
|
+
return 0;
|
|
27784
|
+
}
|
|
27785
|
+
function parseSpecifier(specifier) {
|
|
27786
|
+
const at = specifier.lastIndexOf("@");
|
|
27787
|
+
if (at <= 0) return { name: specifier };
|
|
27788
|
+
return { name: specifier.slice(0, at), version: specifier.slice(at + 1) };
|
|
27789
|
+
}
|
|
27790
|
+
function validateManifest2(manifest, source) {
|
|
27791
|
+
const app = manifest;
|
|
27792
|
+
if (typeof app?.name !== "string" || typeof app?.version !== "string") {
|
|
27793
|
+
throw new AppRegistryError(`${source} is not an application manifest`);
|
|
27794
|
+
}
|
|
27795
|
+
for (const binary of app.bins ?? []) {
|
|
27796
|
+
if (typeof binary?.name !== "string" || typeof binary?.path !== "string") {
|
|
27797
|
+
throw new AppRegistryError(`${source}: a bins entry needs a name and a path`);
|
|
27798
|
+
}
|
|
27799
|
+
if (binary.name.includes("/")) {
|
|
27800
|
+
throw new AppRegistryError(`${source}: a command name cannot contain a slash`);
|
|
27801
|
+
}
|
|
27802
|
+
}
|
|
27803
|
+
for (const service of app.services ?? []) {
|
|
27804
|
+
if (typeof service?.port !== "number" || !Number.isInteger(service.port)) {
|
|
27805
|
+
throw new AppRegistryError(`${source}: a service needs an integer port`);
|
|
27806
|
+
}
|
|
27807
|
+
}
|
|
27808
|
+
return app;
|
|
27809
|
+
}
|
|
27810
|
+
|
|
27811
|
+
// src/apps/sbjs.ts
|
|
27812
|
+
init_binary();
|
|
27813
|
+
init_zip();
|
|
27814
|
+
var MANIFEST_PATH = "META/manifest.json";
|
|
27815
|
+
var SIGNATURE_PATH = "META/signature.json";
|
|
27816
|
+
var TRUSTED_KEYS_PATH = "/etc/pm/trusted-keys.json";
|
|
27817
|
+
var PackageError = class extends Error {
|
|
27818
|
+
};
|
|
27819
|
+
async function openPackage(bytes2, trusted) {
|
|
27820
|
+
const entries = readZip(bytes2);
|
|
27821
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
27822
|
+
const manifestEntry = byName.get(MANIFEST_PATH);
|
|
27823
|
+
if (!manifestEntry) {
|
|
27824
|
+
throw new PackageError(
|
|
27825
|
+
`not a SandboxedJs package: it has no ${MANIFEST_PATH}. A plain zip can be installed with --allow-unsigned, but it carries no publisher.`
|
|
27826
|
+
);
|
|
27827
|
+
}
|
|
27828
|
+
const manifestBytes = manifestEntry.data();
|
|
27829
|
+
const manifest = parseManifest(manifestBytes);
|
|
27830
|
+
validateManifest2(manifest.app, `${MANIFEST_PATH}'s app`);
|
|
27831
|
+
for (const entry of entries) {
|
|
27832
|
+
if (entry.isDirectory || entry.name.startsWith("META/")) continue;
|
|
27833
|
+
const expected = manifest.files[entry.name];
|
|
27834
|
+
if (!expected) {
|
|
27835
|
+
throw new PackageError(`${entry.name} is in the package but not in its manifest`);
|
|
27836
|
+
}
|
|
27837
|
+
const actual = await digestHex("sha256", entry.data());
|
|
27838
|
+
if (actual !== expected) {
|
|
27839
|
+
throw new PackageError(
|
|
27840
|
+
`${entry.name} does not match the manifest (expected ${expected}, found ${actual})`
|
|
27841
|
+
);
|
|
27842
|
+
}
|
|
27843
|
+
}
|
|
27844
|
+
for (const name of Object.keys(manifest.files)) {
|
|
27845
|
+
if (!byName.has(name)) {
|
|
27846
|
+
throw new PackageError(`${name} is in the manifest but missing from the package`);
|
|
27847
|
+
}
|
|
27848
|
+
}
|
|
27849
|
+
const signatureEntry = byName.get(SIGNATURE_PATH);
|
|
27850
|
+
if (!signatureEntry) {
|
|
27851
|
+
return { manifest, signature: null, entries, trustedBy: null };
|
|
27852
|
+
}
|
|
27853
|
+
const signature = parseSignature(signatureEntry.data());
|
|
27854
|
+
const valid = await verifySignature(manifestBytes, signature);
|
|
27855
|
+
if (!valid) {
|
|
27856
|
+
throw new PackageError(
|
|
27857
|
+
`the signature on ${manifest.app.name} does not match its manifest; the package has been altered since it was signed`
|
|
27858
|
+
);
|
|
27859
|
+
}
|
|
27860
|
+
const trustedBy = trusted.find((key) => key.publicKey === signature.publicKey) ?? null;
|
|
27861
|
+
return { manifest, signature, entries, trustedBy };
|
|
27862
|
+
}
|
|
27863
|
+
function parseManifest(bytes2) {
|
|
27864
|
+
let parsed;
|
|
27865
|
+
try {
|
|
27866
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes2));
|
|
27867
|
+
} catch (error) {
|
|
27868
|
+
throw new PackageError(`${MANIFEST_PATH} is not JSON: ${error.message}`);
|
|
27869
|
+
}
|
|
27870
|
+
const manifest = parsed;
|
|
27871
|
+
if (manifest?.format !== "sandboxedjs-package") {
|
|
27872
|
+
throw new PackageError(`${MANIFEST_PATH} is not a package manifest`);
|
|
27873
|
+
}
|
|
27874
|
+
if (!manifest.files || typeof manifest.files !== "object") {
|
|
27875
|
+
throw new PackageError(`${MANIFEST_PATH} lists no files`);
|
|
27876
|
+
}
|
|
27877
|
+
if (!manifest.app) {
|
|
27878
|
+
throw new PackageError(`${MANIFEST_PATH} carries no application manifest`);
|
|
27879
|
+
}
|
|
27880
|
+
return manifest;
|
|
27881
|
+
}
|
|
27882
|
+
function parseSignature(bytes2) {
|
|
27883
|
+
let parsed;
|
|
27884
|
+
try {
|
|
27885
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes2));
|
|
27886
|
+
} catch (error) {
|
|
27887
|
+
throw new PackageError(`${SIGNATURE_PATH} is not JSON: ${error.message}`);
|
|
27888
|
+
}
|
|
27889
|
+
const signature = parsed;
|
|
27890
|
+
if (signature?.format !== "sandboxedjs-signature") {
|
|
27891
|
+
throw new PackageError(`${SIGNATURE_PATH} is not a signature`);
|
|
27892
|
+
}
|
|
27893
|
+
if (signature.algorithm !== "ed25519") {
|
|
27894
|
+
throw new PackageError(
|
|
27895
|
+
`${signature.algorithm} signatures are not supported by this runtime; only ed25519 is`
|
|
27896
|
+
);
|
|
27897
|
+
}
|
|
27898
|
+
for (const field of ["publicKey", "signature"]) {
|
|
27899
|
+
if (typeof signature[field] !== "string" || !signature[field]) {
|
|
27900
|
+
throw new PackageError(`${SIGNATURE_PATH} has no ${field}`);
|
|
27901
|
+
}
|
|
27902
|
+
}
|
|
27903
|
+
return signature;
|
|
27904
|
+
}
|
|
27905
|
+
async function verifySignature(manifestBytes, signature) {
|
|
27906
|
+
const subtle = globalThis.crypto?.subtle;
|
|
27907
|
+
if (!subtle) {
|
|
27908
|
+
throw new PackageError(
|
|
27909
|
+
"this runtime has no WebCrypto, so a package signature cannot be checked"
|
|
27910
|
+
);
|
|
27911
|
+
}
|
|
27912
|
+
let key;
|
|
27913
|
+
try {
|
|
27914
|
+
key = await subtle.importKey(
|
|
27915
|
+
"spki",
|
|
27916
|
+
intoBuffer(base64ToBytes(signature.publicKey)),
|
|
27917
|
+
{ name: "Ed25519" },
|
|
27918
|
+
false,
|
|
27919
|
+
["verify"]
|
|
27920
|
+
);
|
|
27921
|
+
} catch (error) {
|
|
27922
|
+
throw new PackageError(
|
|
27923
|
+
`cannot use the package's public key: ${error.message}. Ed25519 needs Node 20 or a browser that implements it.`
|
|
27924
|
+
);
|
|
27925
|
+
}
|
|
27926
|
+
return subtle.verify(
|
|
27927
|
+
{ name: "Ed25519" },
|
|
27928
|
+
key,
|
|
27929
|
+
intoBuffer(base64ToBytes(signature.signature)),
|
|
27930
|
+
intoBuffer(manifestBytes)
|
|
27931
|
+
);
|
|
27932
|
+
}
|
|
27933
|
+
function intoBuffer(bytes2) {
|
|
27934
|
+
return bytes2.slice().buffer;
|
|
27935
|
+
}
|
|
27936
|
+
function base64ToBytes(text2) {
|
|
27937
|
+
const binary = atob(text2);
|
|
27938
|
+
const bytes2 = new Uint8Array(binary.length);
|
|
27939
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
27940
|
+
bytes2[index] = binary.charCodeAt(index);
|
|
27941
|
+
}
|
|
27942
|
+
return bytes2;
|
|
27943
|
+
}
|
|
27944
|
+
async function fingerprint(publicKey) {
|
|
27945
|
+
const digest2 = await digestHex("sha256", base64ToBytes(publicKey));
|
|
27946
|
+
return digest2.slice(0, 32).replace(/(.{4})(?=.)/g, "$1-");
|
|
27947
|
+
}
|
|
27948
|
+
function asRegistryError(error) {
|
|
27949
|
+
return new AppRegistryError(error.message);
|
|
27950
|
+
}
|
|
27951
|
+
|
|
27952
|
+
// src/apps/pm.ts
|
|
27953
|
+
function registryBase(ctx) {
|
|
27954
|
+
const configured = ctx.env.SBX_PM_REGISTRY ?? ctx.env.PM_REGISTRY;
|
|
27955
|
+
return configured && configured.trim() || DEFAULT_REGISTRY;
|
|
27956
|
+
}
|
|
27957
|
+
var pm = defineCommand({
|
|
27958
|
+
name: "pm",
|
|
27959
|
+
path: "/usr/bin/pm",
|
|
27960
|
+
summary: "install and manage applications",
|
|
27961
|
+
usage: "pm <install|remove|list|info|search|available> [name[@version]]",
|
|
27962
|
+
manual: `Applications are programs the container runs, packaged with what
|
|
27963
|
+
they need. They are not Python or Node packages: use pip or npm for those.
|
|
27964
|
+
|
|
27965
|
+
pm install NAME[@VERSION] download, verify and install
|
|
27966
|
+
pm remove NAME remove it and the commands it installed
|
|
27967
|
+
pm list what is installed here
|
|
27968
|
+
pm info NAME what a package provides
|
|
27969
|
+
pm search TEXT search the registry
|
|
27970
|
+
pm available everything the registry offers
|
|
27971
|
+
pm trust list|add|remove publisher keys this container trusts
|
|
27972
|
+
|
|
27973
|
+
Packages come from ${DEFAULT_REGISTRY} unless SBX_PM_REGISTRY names another.
|
|
27974
|
+
|
|
27975
|
+
A package is a .sbjs file: a zip carrying META/manifest.json, which lists a
|
|
27976
|
+
sha256 for every file in it, and META/signature.json, an Ed25519 signature
|
|
27977
|
+
over that manifest. Installing checks both, and refuses a package signed by a
|
|
27978
|
+
key this container does not trust. --allow-untrusted and --allow-unsigned
|
|
27979
|
+
override that, one install at a time.
|
|
27980
|
+
|
|
27981
|
+
A signature says who published a package and that nothing in it has changed
|
|
27982
|
+
since. It does not say the program is safe to run.`,
|
|
27983
|
+
async run(ctx) {
|
|
27984
|
+
const [subcommand, ...rest] = ctx.args;
|
|
27985
|
+
try {
|
|
27986
|
+
switch (subcommand) {
|
|
27987
|
+
case "install":
|
|
27988
|
+
return await install2(ctx, rest);
|
|
27989
|
+
case "remove":
|
|
27990
|
+
case "uninstall":
|
|
27991
|
+
return await remove(ctx, rest);
|
|
27992
|
+
case "list":
|
|
27993
|
+
return listInstalled2(ctx);
|
|
27994
|
+
case "info":
|
|
27995
|
+
return await info(ctx, rest);
|
|
27996
|
+
case "search":
|
|
27997
|
+
return await search(ctx, rest);
|
|
27998
|
+
case "available":
|
|
27999
|
+
return await available(ctx);
|
|
28000
|
+
case "trust":
|
|
28001
|
+
return await trust(ctx, rest);
|
|
28002
|
+
case void 0:
|
|
28003
|
+
case "--help":
|
|
28004
|
+
case "-h":
|
|
28005
|
+
ctx.line(pm.usage);
|
|
28006
|
+
return 0;
|
|
28007
|
+
default:
|
|
28008
|
+
ctx.warn(`pm: unknown command ${subcommand}`);
|
|
28009
|
+
return 2;
|
|
28010
|
+
}
|
|
28011
|
+
} catch (error) {
|
|
28012
|
+
ctx.warn(`pm: ${error.message}`);
|
|
28013
|
+
return error instanceof AppRegistryError ? 2 : 1;
|
|
28014
|
+
}
|
|
28015
|
+
}
|
|
28016
|
+
});
|
|
28017
|
+
async function install2(ctx, args) {
|
|
28018
|
+
if (args.length === 0) {
|
|
28019
|
+
ctx.warn("pm install: name it, as `pm install officesuite`");
|
|
28020
|
+
return 2;
|
|
28021
|
+
}
|
|
28022
|
+
const options = {
|
|
28023
|
+
allowUnsigned: args.includes("--allow-unsigned"),
|
|
28024
|
+
allowUntrusted: args.includes("--allow-untrusted") || args.includes("--allow-unsigned")
|
|
28025
|
+
};
|
|
28026
|
+
const names = args.filter((argument) => !argument.startsWith("--"));
|
|
28027
|
+
if (names.length === 0) {
|
|
28028
|
+
ctx.warn("pm install: name it, as `pm install officesuite`");
|
|
28029
|
+
return 2;
|
|
28030
|
+
}
|
|
28031
|
+
const index = await fetchIndex(ctx);
|
|
28032
|
+
for (const specifier of names) {
|
|
28033
|
+
const { name, version } = parseSpecifier(specifier);
|
|
28034
|
+
const release = selectRelease(index, name, version);
|
|
28035
|
+
await installRelease(ctx, index, release, /* @__PURE__ */ new Set(), options);
|
|
28036
|
+
}
|
|
28037
|
+
return 0;
|
|
28038
|
+
}
|
|
28039
|
+
async function installRelease(ctx, index, release, seen, options) {
|
|
28040
|
+
if (seen.has(release.name)) return;
|
|
28041
|
+
seen.add(release.name);
|
|
28042
|
+
for (const required of release.requires ?? []) {
|
|
28043
|
+
const { name, version } = parseSpecifier(required);
|
|
28044
|
+
if (installedApps(ctx).some((app) => app.name === name)) continue;
|
|
28045
|
+
await installRelease(ctx, index, selectRelease(index, name, version), seen, options);
|
|
28046
|
+
}
|
|
28047
|
+
const base2 = registryBase(ctx).replace(/\/$/, "");
|
|
28048
|
+
const url = `${base2}/${release.filename}`;
|
|
28049
|
+
ctx.line(`Fetching ${release.name} ${release.version}`);
|
|
28050
|
+
let archive = await download2(url);
|
|
28051
|
+
let digest2 = await digestHex("sha256", archive);
|
|
28052
|
+
if (digest2 !== release.sha256) {
|
|
28053
|
+
const retried = await download2(`${url}${url.includes("?") ? "&" : "?"}sha256=${release.sha256}`).catch(() => null);
|
|
28054
|
+
const retriedDigest = retried && await digestHex("sha256", retried);
|
|
28055
|
+
if (retried && retriedDigest === release.sha256) {
|
|
28056
|
+
archive = retried;
|
|
28057
|
+
digest2 = retriedDigest;
|
|
28058
|
+
} else {
|
|
28059
|
+
throw new AppRegistryError(
|
|
28060
|
+
`${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.")
|
|
28061
|
+
);
|
|
28062
|
+
}
|
|
28063
|
+
}
|
|
28064
|
+
const verified = await verify(ctx, archive, release, options);
|
|
28065
|
+
const root = `${APPS_ROOT}/${release.name}`;
|
|
28066
|
+
removeInstalled(ctx, release.name, { quiet: true });
|
|
28067
|
+
const manifest = unpack(ctx, archive, root, release, verified);
|
|
28068
|
+
const bins = linkBinaries(ctx, manifest, root);
|
|
28069
|
+
record(ctx, {
|
|
28070
|
+
...manifest,
|
|
28071
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28072
|
+
root,
|
|
28073
|
+
installedBins: bins,
|
|
28074
|
+
sha256: release.sha256,
|
|
28075
|
+
...verified?.signature ? {
|
|
28076
|
+
publisher: verified.signature.publisher ?? verified.trustedBy?.publisher ?? "unknown",
|
|
28077
|
+
publicKey: verified.signature.publicKey
|
|
28078
|
+
} : {}
|
|
28079
|
+
});
|
|
28080
|
+
ctx.line(`Installed ${manifest.name} ${manifest.version} in ${root}`);
|
|
28081
|
+
if (bins.length > 0) ctx.line(` commands: ${bins.join(", ")}`);
|
|
28082
|
+
for (const service of manifest.services ?? []) {
|
|
28083
|
+
ctx.line(
|
|
28084
|
+
` service: ${service.name} on port ${service.port}` + (service.path ? ` at ${service.path}` : "") + ` (start it with \`${service.command}\`)`
|
|
28085
|
+
);
|
|
28086
|
+
}
|
|
28087
|
+
if (manifest.docs) ctx.line(` docs: ${root}/${manifest.docs}`);
|
|
28088
|
+
}
|
|
28089
|
+
async function download2(url) {
|
|
28090
|
+
const response = await fetch(url);
|
|
28091
|
+
if (!response.ok) {
|
|
28092
|
+
throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
|
|
28093
|
+
}
|
|
28094
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
28095
|
+
}
|
|
28096
|
+
async function verify(ctx, archive, release, options) {
|
|
28097
|
+
let opened;
|
|
28098
|
+
try {
|
|
28099
|
+
opened = await openPackage(archive, trustedKeys(ctx));
|
|
28100
|
+
} catch (error) {
|
|
28101
|
+
if (options.allowUnsigned && /has no META\/manifest\.json/.test(error.message)) {
|
|
28102
|
+
ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
|
|
28103
|
+
return null;
|
|
28104
|
+
}
|
|
28105
|
+
throw asRegistryError(error);
|
|
28106
|
+
}
|
|
28107
|
+
if (!opened.signature) {
|
|
28108
|
+
if (!options.allowUnsigned) {
|
|
28109
|
+
throw new AppRegistryError(
|
|
28110
|
+
`${release.name} carries no signature, so its publisher cannot be established. Install it with --allow-unsigned if you got it from somewhere you trust.`
|
|
28111
|
+
);
|
|
28112
|
+
}
|
|
28113
|
+
ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
|
|
28114
|
+
return opened;
|
|
28115
|
+
}
|
|
28116
|
+
const print = await fingerprint(opened.signature.publicKey);
|
|
28117
|
+
if (!opened.trustedBy && !options.allowUntrusted) {
|
|
28118
|
+
throw new AppRegistryError(
|
|
28119
|
+
`${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.`
|
|
28120
|
+
);
|
|
28121
|
+
}
|
|
28122
|
+
ctx.line(
|
|
28123
|
+
opened.trustedBy ? ` signed by ${opened.trustedBy.publisher} (${print})` : ` signed by an untrusted key (${opened.signature.publisher ?? "unnamed"}, ${print})`
|
|
28124
|
+
);
|
|
28125
|
+
return opened;
|
|
28126
|
+
}
|
|
28127
|
+
function trustedKeys(ctx) {
|
|
28128
|
+
let stored = [];
|
|
28129
|
+
try {
|
|
28130
|
+
const parsed = JSON.parse(ctx.vfs.readText(TRUSTED_KEYS_PATH, ctx.cred));
|
|
28131
|
+
stored = Array.isArray(parsed.keys) ? parsed.keys : [];
|
|
28132
|
+
} catch {
|
|
28133
|
+
}
|
|
28134
|
+
const known = new Set(stored.map((key) => key.publicKey));
|
|
28135
|
+
return [...stored, ...BUILT_IN_PUBLISHERS.filter((key) => !known.has(key.publicKey))];
|
|
28136
|
+
}
|
|
28137
|
+
function writeTrustedKeys(ctx, keys) {
|
|
28138
|
+
ctx.vfs.mkdir("/etc/pm", { cred: ctx.cred, recursive: true });
|
|
28139
|
+
ctx.vfs.writeFile(
|
|
28140
|
+
TRUSTED_KEYS_PATH,
|
|
28141
|
+
`${JSON.stringify({ format: "sandboxedjs-trusted-keys", keys }, null, 2)}
|
|
28142
|
+
`,
|
|
28143
|
+
{ cred: ctx.cred, mode: 420 }
|
|
28144
|
+
);
|
|
28145
|
+
}
|
|
28146
|
+
async function trust(ctx, args) {
|
|
28147
|
+
const [action, ...rest] = args;
|
|
28148
|
+
const keys = trustedKeys(ctx);
|
|
28149
|
+
switch (action) {
|
|
28150
|
+
case "list":
|
|
28151
|
+
case void 0: {
|
|
28152
|
+
if (keys.length === 0) {
|
|
28153
|
+
ctx.line("No publisher keys are trusted; only unsigned installs are possible.");
|
|
28154
|
+
return 0;
|
|
28155
|
+
}
|
|
28156
|
+
for (const key of keys) {
|
|
28157
|
+
ctx.line(`${key.publisher} ${await fingerprint(key.publicKey)}`);
|
|
28158
|
+
}
|
|
28159
|
+
return 0;
|
|
28160
|
+
}
|
|
28161
|
+
case "add": {
|
|
28162
|
+
const [publisher, publicKey] = rest;
|
|
28163
|
+
if (!publisher || !publicKey) {
|
|
28164
|
+
ctx.warn("pm trust add: give a publisher name and its base64 public key");
|
|
28165
|
+
return 2;
|
|
28166
|
+
}
|
|
28167
|
+
const next = keys.filter((key) => key.publicKey !== publicKey);
|
|
28168
|
+
next.push({ publisher, publicKey, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
28169
|
+
writeTrustedKeys(ctx, next);
|
|
28170
|
+
ctx.line(`Trusting ${publisher} (${await fingerprint(publicKey)})`);
|
|
28171
|
+
return 0;
|
|
28172
|
+
}
|
|
28173
|
+
case "remove": {
|
|
28174
|
+
const [publisher] = rest;
|
|
28175
|
+
const next = keys.filter((key) => key.publisher !== publisher);
|
|
28176
|
+
if (next.length === keys.length) {
|
|
28177
|
+
ctx.warn(`pm: no trusted key named ${publisher}`);
|
|
28178
|
+
return 1;
|
|
28179
|
+
}
|
|
28180
|
+
writeTrustedKeys(ctx, next);
|
|
28181
|
+
ctx.line(`Removed ${publisher}`);
|
|
28182
|
+
return 0;
|
|
28183
|
+
}
|
|
28184
|
+
default:
|
|
28185
|
+
ctx.warn(`pm trust: unknown action ${action}`);
|
|
28186
|
+
return 2;
|
|
28187
|
+
}
|
|
28188
|
+
}
|
|
28189
|
+
function unpack(ctx, archive, root, release, verified) {
|
|
28190
|
+
const entries = verified?.entries ?? readZip(archive);
|
|
28191
|
+
const manifest = verified?.manifest.app ?? readAppManifest(entries, release);
|
|
28192
|
+
if (manifest.name !== release.name || manifest.version !== release.version) {
|
|
28193
|
+
throw new AppRegistryError(
|
|
28194
|
+
`${release.filename} contains ${manifest.name} ${manifest.version}, but the registry lists it as ${release.name} ${release.version}`
|
|
28195
|
+
);
|
|
28196
|
+
}
|
|
28197
|
+
ctx.vfs.mkdir(root, { cred: ctx.cred, recursive: true });
|
|
28198
|
+
for (const entry of entries) {
|
|
28199
|
+
if (entry.name.startsWith("META/")) continue;
|
|
28200
|
+
const target = resolveInside(root, entry.name);
|
|
28201
|
+
if (entry.isDirectory) {
|
|
28202
|
+
ctx.vfs.mkdir(target, { cred: ctx.cred, recursive: true });
|
|
28203
|
+
continue;
|
|
28204
|
+
}
|
|
28205
|
+
const parent = target.slice(0, target.lastIndexOf("/"));
|
|
28206
|
+
if (parent) ctx.vfs.mkdir(parent, { cred: ctx.cred, recursive: true });
|
|
28207
|
+
const executable = (entry.mode & 73) !== 0 || (manifest.bins ?? []).some((binary) => binary.path === entry.name);
|
|
28208
|
+
ctx.vfs.writeFile(target, entry.data(), {
|
|
28209
|
+
cred: ctx.cred,
|
|
28210
|
+
mode: executable ? 493 : 420
|
|
28211
|
+
});
|
|
28212
|
+
}
|
|
28213
|
+
return manifest;
|
|
28214
|
+
}
|
|
28215
|
+
function readAppManifest(entries, release) {
|
|
28216
|
+
const entry = entries.find((candidate) => candidate.name === "app.json");
|
|
28217
|
+
if (!entry) throw new AppRegistryError(`${release.filename} has no app.json`);
|
|
28218
|
+
return validateManifest2(
|
|
28219
|
+
JSON.parse(new TextDecoder().decode(entry.data())),
|
|
28220
|
+
`${release.name}'s app.json`
|
|
28221
|
+
);
|
|
28222
|
+
}
|
|
28223
|
+
function resolveInside(root, name) {
|
|
28224
|
+
const clean2 = name.replace(/\\/g, "/");
|
|
28225
|
+
if (clean2.startsWith("/")) {
|
|
28226
|
+
throw new AppRegistryError(`archive entry ${name} is an absolute path`);
|
|
28227
|
+
}
|
|
28228
|
+
const parts = [];
|
|
28229
|
+
for (const part of clean2.split("/")) {
|
|
28230
|
+
if (part === "" || part === ".") continue;
|
|
28231
|
+
if (part === "..") {
|
|
28232
|
+
throw new AppRegistryError(`archive entry ${name} escapes the package directory`);
|
|
28233
|
+
}
|
|
28234
|
+
parts.push(part);
|
|
28235
|
+
}
|
|
28236
|
+
return `${root}/${parts.join("/")}`;
|
|
28237
|
+
}
|
|
28238
|
+
function linkBinaries(ctx, manifest, root) {
|
|
28239
|
+
const installed3 = [];
|
|
28240
|
+
if ((manifest.bins ?? []).length === 0) return installed3;
|
|
28241
|
+
ctx.vfs.mkdir(APP_BIN_DIR, { cred: ctx.cred, recursive: true });
|
|
28242
|
+
for (const binary of manifest.bins ?? []) {
|
|
28243
|
+
const target = resolveInside(root, binary.path);
|
|
28244
|
+
const wrapper = `${APP_BIN_DIR}/${binary.name}`;
|
|
28245
|
+
ctx.vfs.writeFile(
|
|
28246
|
+
wrapper,
|
|
28247
|
+
`#!/bin/sh
|
|
28248
|
+
# installed by pm for ${manifest.name} ${manifest.version}
|
|
28249
|
+
exec ${target} "$@"
|
|
28250
|
+
`,
|
|
28251
|
+
{ cred: ctx.cred, mode: 493 }
|
|
28252
|
+
);
|
|
28253
|
+
installed3.push(binary.name);
|
|
28254
|
+
}
|
|
28255
|
+
return installed3;
|
|
28256
|
+
}
|
|
28257
|
+
async function remove(ctx, args) {
|
|
28258
|
+
if (args.length === 0) {
|
|
28259
|
+
ctx.warn("pm remove: name it");
|
|
28260
|
+
return 2;
|
|
28261
|
+
}
|
|
28262
|
+
let code = 0;
|
|
28263
|
+
for (const specifier of args) {
|
|
28264
|
+
const { name } = parseSpecifier(specifier);
|
|
28265
|
+
if (!removeInstalled(ctx, name, { quiet: false })) {
|
|
28266
|
+
ctx.warn(`pm: ${name} is not installed`);
|
|
28267
|
+
code = 1;
|
|
28268
|
+
} else {
|
|
28269
|
+
ctx.line(`Removed ${name}`);
|
|
28270
|
+
}
|
|
28271
|
+
}
|
|
28272
|
+
return code;
|
|
28273
|
+
}
|
|
28274
|
+
function removeInstalled(ctx, name, options) {
|
|
28275
|
+
const apps = installedApps(ctx);
|
|
28276
|
+
const app = apps.find((entry) => entry.name === name);
|
|
28277
|
+
if (!app) return false;
|
|
28278
|
+
for (const binary of app.installedBins) {
|
|
28279
|
+
try {
|
|
28280
|
+
ctx.vfs.unlink(`${APP_BIN_DIR}/${binary}`, ctx.cred);
|
|
28281
|
+
} catch {
|
|
28282
|
+
}
|
|
28283
|
+
}
|
|
28284
|
+
try {
|
|
28285
|
+
removeTree(ctx, app.root);
|
|
28286
|
+
} catch (error) {
|
|
28287
|
+
if (!options.quiet) ctx.warn(`pm: could not remove ${app.root}: ${error.message}`);
|
|
28288
|
+
}
|
|
28289
|
+
writeState(ctx, apps.filter((entry) => entry.name !== name));
|
|
28290
|
+
return true;
|
|
28291
|
+
}
|
|
28292
|
+
function removeTree(ctx, path) {
|
|
28293
|
+
let stats;
|
|
28294
|
+
try {
|
|
28295
|
+
stats = ctx.vfs.stat(path, { cred: ctx.cred });
|
|
28296
|
+
} catch {
|
|
28297
|
+
return;
|
|
28298
|
+
}
|
|
28299
|
+
if (stats.isDirectory()) {
|
|
28300
|
+
for (const entry of ctx.vfs.readdir(path, ctx.cred)) {
|
|
28301
|
+
removeTree(ctx, `${path}/${entry}`);
|
|
28302
|
+
}
|
|
28303
|
+
ctx.vfs.rmdir(path, ctx.cred);
|
|
28304
|
+
return;
|
|
28305
|
+
}
|
|
28306
|
+
ctx.vfs.unlink(path, ctx.cred);
|
|
28307
|
+
}
|
|
28308
|
+
function listInstalled2(ctx) {
|
|
28309
|
+
const apps = installedApps(ctx);
|
|
28310
|
+
if (apps.length === 0) {
|
|
28311
|
+
ctx.line("No applications installed. `pm available` lists what there is.");
|
|
28312
|
+
return 0;
|
|
28313
|
+
}
|
|
28314
|
+
for (const app of apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
28315
|
+
const commands12 = app.installedBins.length > 0 ? ` (${app.installedBins.join(", ")})` : "";
|
|
28316
|
+
ctx.line(`${app.name} ${app.version}${commands12}`);
|
|
28317
|
+
}
|
|
28318
|
+
return 0;
|
|
28319
|
+
}
|
|
28320
|
+
async function info(ctx, args) {
|
|
28321
|
+
const name = args[0];
|
|
28322
|
+
if (!name) {
|
|
28323
|
+
ctx.warn("pm info: name it");
|
|
28324
|
+
return 2;
|
|
28325
|
+
}
|
|
28326
|
+
const local = installedApps(ctx).find((app) => app.name === name);
|
|
28327
|
+
if (local) {
|
|
28328
|
+
ctx.line(`${local.name} ${local.version} (installed ${local.installedAt})`);
|
|
28329
|
+
if (local.summary) ctx.line(` ${local.summary}`);
|
|
28330
|
+
ctx.line(` root: ${local.root}`);
|
|
28331
|
+
if (local.installedBins.length > 0) ctx.line(` commands: ${local.installedBins.join(", ")}`);
|
|
28332
|
+
for (const service of local.services ?? []) {
|
|
28333
|
+
ctx.line(` service: ${service.name} on port ${service.port}${service.path ?? ""}`);
|
|
28334
|
+
}
|
|
28335
|
+
if (local.docs) ctx.line(` docs: ${local.root}/${local.docs}`);
|
|
28336
|
+
return 0;
|
|
28337
|
+
}
|
|
28338
|
+
const release = selectRelease(await fetchIndex(ctx), name);
|
|
28339
|
+
ctx.line(`${release.name} ${release.version} (not installed)`);
|
|
28340
|
+
if (release.summary) ctx.line(` ${release.summary}`);
|
|
28341
|
+
ctx.line(` size: ${Math.round(release.size / 1024)} kB`);
|
|
28342
|
+
if ((release.bins ?? []).length > 0) {
|
|
28343
|
+
ctx.line(` commands: ${(release.bins ?? []).map((b) => b.name).join(", ")}`);
|
|
28344
|
+
}
|
|
28345
|
+
return 0;
|
|
28346
|
+
}
|
|
28347
|
+
async function search(ctx, args) {
|
|
28348
|
+
const term = (args[0] ?? "").toLowerCase();
|
|
28349
|
+
const index = await fetchIndex(ctx);
|
|
28350
|
+
const matches3 = index.apps.filter(
|
|
28351
|
+
(app) => app.name.toLowerCase().includes(term) || (app.summary ?? "").toLowerCase().includes(term)
|
|
28352
|
+
);
|
|
28353
|
+
if (matches3.length === 0) {
|
|
28354
|
+
ctx.line(`nothing matches ${JSON.stringify(args[0] ?? "")}`);
|
|
28355
|
+
return 1;
|
|
28356
|
+
}
|
|
28357
|
+
for (const app of matches3) ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
|
|
28358
|
+
return 0;
|
|
28359
|
+
}
|
|
28360
|
+
async function available(ctx) {
|
|
28361
|
+
const index = await fetchIndex(ctx);
|
|
28362
|
+
for (const app of index.apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
|
|
28363
|
+
ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
|
|
28364
|
+
}
|
|
28365
|
+
return 0;
|
|
28366
|
+
}
|
|
28367
|
+
async function fetchIndex(ctx) {
|
|
28368
|
+
const base2 = registryBase(ctx).replace(/\/$/, "");
|
|
28369
|
+
const url = `${base2}/index.json`;
|
|
28370
|
+
let response;
|
|
28371
|
+
try {
|
|
28372
|
+
response = await fetch(url);
|
|
28373
|
+
} catch (error) {
|
|
28374
|
+
throw new AppRegistryError(
|
|
28375
|
+
`cannot reach the application registry at ${url}: ${error.message}`
|
|
28376
|
+
);
|
|
28377
|
+
}
|
|
28378
|
+
if (!response.ok) {
|
|
28379
|
+
throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
|
|
28380
|
+
}
|
|
28381
|
+
return parseIndex(await response.text(), url);
|
|
28382
|
+
}
|
|
28383
|
+
function installedApps(ctx) {
|
|
28384
|
+
try {
|
|
28385
|
+
const text2 = ctx.vfs.readText(APP_STATE, ctx.cred);
|
|
28386
|
+
const parsed = JSON.parse(text2);
|
|
28387
|
+
return Array.isArray(parsed.apps) ? parsed.apps : [];
|
|
28388
|
+
} catch {
|
|
28389
|
+
return [];
|
|
28390
|
+
}
|
|
28391
|
+
}
|
|
28392
|
+
function record(ctx, app) {
|
|
28393
|
+
const apps = installedApps(ctx).filter((entry) => entry.name !== app.name);
|
|
28394
|
+
apps.push(app);
|
|
28395
|
+
writeState(ctx, apps);
|
|
28396
|
+
}
|
|
28397
|
+
function writeState(ctx, apps) {
|
|
28398
|
+
const directory2 = APP_STATE.slice(0, APP_STATE.lastIndexOf("/"));
|
|
28399
|
+
ctx.vfs.mkdir(directory2, { cred: ctx.cred, recursive: true });
|
|
28400
|
+
ctx.vfs.writeFile(
|
|
28401
|
+
APP_STATE,
|
|
28402
|
+
`${JSON.stringify({ format: "sandboxedjs-installed-apps", apps }, null, 2)}
|
|
28403
|
+
`,
|
|
28404
|
+
{ cred: ctx.cred, mode: 420 }
|
|
28405
|
+
);
|
|
28406
|
+
}
|
|
28407
|
+
function appCommands() {
|
|
28408
|
+
return [pm];
|
|
28409
|
+
}
|
|
28410
|
+
|
|
27688
28411
|
// src/bin/index.ts
|
|
27689
28412
|
function allCommands() {
|
|
27690
28413
|
return [
|
|
@@ -27704,7 +28427,8 @@ function allCommands() {
|
|
|
27704
28427
|
...ffmpegCommands(),
|
|
27705
28428
|
...wasiCommands(),
|
|
27706
28429
|
...packageCommands(),
|
|
27707
|
-
...browserCommands()
|
|
28430
|
+
...browserCommands(),
|
|
28431
|
+
...appCommands()
|
|
27708
28432
|
];
|
|
27709
28433
|
}
|
|
27710
28434
|
function installUserland(kernel) {
|
|
@@ -31423,7 +32147,7 @@ ${indent(detail, 4)}
|
|
|
31423
32147
|
if (detail) host2.write(`${indent(detail, depth * 2 + 2)}
|
|
31424
32148
|
`);
|
|
31425
32149
|
};
|
|
31426
|
-
const
|
|
32150
|
+
const record2 = (outcome, name, error) => {
|
|
31427
32151
|
counts.tests += 1;
|
|
31428
32152
|
if (outcome === "pass") counts.pass += 1;
|
|
31429
32153
|
else if (outcome === "fail") {
|
|
@@ -31445,13 +32169,13 @@ ${indent(detail, 4)}
|
|
|
31445
32169
|
if (!matchesPattern2(display)) return true;
|
|
31446
32170
|
if (runOnly && !options.only) return true;
|
|
31447
32171
|
if (options.skip) {
|
|
31448
|
-
|
|
32172
|
+
record2("skip", display);
|
|
31449
32173
|
report(depth, "skip", name, started);
|
|
31450
32174
|
return true;
|
|
31451
32175
|
}
|
|
31452
32176
|
if (!fn) {
|
|
31453
32177
|
const outcome = options.todo ? "todo" : "skip";
|
|
31454
|
-
|
|
32178
|
+
record2(outcome, display);
|
|
31455
32179
|
report(depth, outcome, name, started);
|
|
31456
32180
|
return true;
|
|
31457
32181
|
}
|
|
@@ -31515,21 +32239,21 @@ ${indent(detail, 4)}
|
|
|
31515
32239
|
controller.abort();
|
|
31516
32240
|
}
|
|
31517
32241
|
if (skipped) {
|
|
31518
|
-
|
|
32242
|
+
record2("skip", display);
|
|
31519
32243
|
report(depth, "skip", name, started);
|
|
31520
32244
|
return true;
|
|
31521
32245
|
}
|
|
31522
32246
|
if (error !== void 0) {
|
|
31523
32247
|
if (todo) {
|
|
31524
|
-
|
|
32248
|
+
record2("todo", display);
|
|
31525
32249
|
report(depth, "todo", name, started);
|
|
31526
32250
|
return true;
|
|
31527
32251
|
}
|
|
31528
|
-
|
|
32252
|
+
record2("fail", display, error);
|
|
31529
32253
|
report(depth, "fail", name, started, errorText(error));
|
|
31530
32254
|
return false;
|
|
31531
32255
|
}
|
|
31532
|
-
|
|
32256
|
+
record2(todo ? "todo" : "pass", display);
|
|
31533
32257
|
report(depth, todo ? "todo" : "pass", name, started);
|
|
31534
32258
|
return true;
|
|
31535
32259
|
};
|
|
@@ -31544,7 +32268,7 @@ ${indent(detail, 4)}
|
|
|
31544
32268
|
else host2.write(`${" ".repeat(depth)}\u25B6 ${suite.name}
|
|
31545
32269
|
`);
|
|
31546
32270
|
if (suite.options.skip) {
|
|
31547
|
-
for (const child of suite.children) if (child.kind === "test")
|
|
32271
|
+
for (const child of suite.children) if (child.kind === "test") record2("skip", fullName(child));
|
|
31548
32272
|
report(depth, "skip", suite.name, started);
|
|
31549
32273
|
return true;
|
|
31550
32274
|
}
|
|
@@ -31556,7 +32280,7 @@ ${indent(detail, 4)}
|
|
|
31556
32280
|
} catch (error) {
|
|
31557
32281
|
for (const child of suite.children) {
|
|
31558
32282
|
if (child.kind === "test") {
|
|
31559
|
-
|
|
32283
|
+
record2("cancelled", fullName(child), error);
|
|
31560
32284
|
report(depth + (isRoot ? 0 : 1), "cancelled", child.name, started, errorText(error));
|
|
31561
32285
|
}
|
|
31562
32286
|
}
|
|
@@ -37014,6 +37738,14 @@ var Container = class _Container {
|
|
|
37014
37738
|
installUserland(kernel);
|
|
37015
37739
|
const container = new _Container(kernel, pod, opts);
|
|
37016
37740
|
if (opts.files) container.mountSync(opts.files, { cwd: opts.cwd ?? "/" });
|
|
37741
|
+
if (opts.modules && opts.modules.length > 0) {
|
|
37742
|
+
const result = await container.exec(`pm install ${opts.modules.join(" ")}`);
|
|
37743
|
+
if (result.exitCode !== 0) {
|
|
37744
|
+
throw new Error(
|
|
37745
|
+
`could not install ${opts.modules.join(", ")}: ${(result.stderr || result.stdout).trim() || `pm exited ${result.exitCode}`}`
|
|
37746
|
+
);
|
|
37747
|
+
}
|
|
37748
|
+
}
|
|
37017
37749
|
return container;
|
|
37018
37750
|
}
|
|
37019
37751
|
// ── file seeding ──────────────────────────────────────────────────────────
|