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/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 available = subscription.fd === 0 ? this.opts.stdin.available : this.bytesLeft(subscription.fd);
7007
- view.setBigUint64(at + 16, BigInt(available), true);
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 info;
7244
+ let info2;
7245
7245
  try {
7246
- info = inspectElf(bytes2);
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 ${info.architecture} ELF: ${reasons.join("; ") || "no binary backends registered"}`, 126);
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, info) {
8534
- this.listeners.set(port, { port, since: Date.now(), ...info });
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 info = unameInfo(ctx.kernel.hostname);
12127
+ const info2 = unameInfo(ctx.kernel.hostname);
12128
12128
  if (args.has("all")) {
12129
- ctx.line(`${info.sysname} ${info.nodename} ${info.release} ${info.version} ${info.machine} ${info.operatingSystem}`);
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(info.sysname);
12134
- if (args.has("nodename")) parts.push(info.nodename);
12135
- if (args.has("kernel-release")) parts.push(info.release);
12136
- if (args.has("kernel-version")) parts.push(info.version);
12137
- if (args.has("machine")) parts.push(info.machine);
12138
- if (args.has("processor")) parts.push(info.processor);
12139
- if (args.has("hardware-platform")) parts.push(info.hardwarePlatform);
12140
- if (args.has("operating-system")) parts.push(info.operatingSystem);
12141
- ctx.line(parts.length ? parts.join(" ") : info.sysname);
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(record, fs) {
15126
- if (record === "") return [];
15125
+ function splitRecord(record2, fs) {
15126
+ if (record2 === "") return [];
15127
15127
  if (fs === " ") {
15128
- const trimmed = record.replace(/^[ \t\n]+|[ \t\n]+$/g, "");
15128
+ const trimmed = record2.replace(/^[ \t\n]+|[ \t\n]+$/g, "");
15129
15129
  return trimmed === "" ? [] : trimmed.split(/[ \t\n]+/);
15130
15130
  }
15131
- if (fs === "") return [...record];
15132
- if (fs.length === 1 && !"\\^$.[]|()*+?{}".includes(fs)) return record.split(fs);
15133
- return record.split(new RegExp(translateEre(fs)));
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.94"};
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 search = async (queue, state) => {
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 search(rest, state);
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 search(rest, next);
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 search([...edges, ...rest], { chosen, constraints: next.constraints });
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 search(roots, { chosen: /* @__PURE__ */ new Map(), constraints: seeded });
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, available = []) {
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 (available.length > 0) {
23806
- const versions = [...new Set(available.map((c) => c.version))].slice(0, 8);
23807
- lines.push(`usable versions: ${versions.join(", ")}${available.length > 8 ? ", ..." : ""}`);
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.bytes(distribution.url, { timeoutMs: 12e4 });
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 verifyDigest(distribution, bytes2) {
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 !== distribution.sha256) {
23962
- throw new Error(
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 record = value.slice(space + 1, offset + length - 1);
26405
- const equal2 = record.indexOf("=");
26406
- if (equal2 > 0) result[record.slice(0, equal2)] = record.slice(equal2 + 1);
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, info] of Object.entries(provided)) {
27187
- const available = ctx.kernel.which(name === "python3-pip" ? "pip" : name === "gawk" ? "awk" : name, ctx.cwd, ctx.env, ctx.cred) !== null;
27188
- if (installedOnly && !available) continue;
27189
- ctx.line(`${name}/stable ${info.version} amd64${available ? " [installed]" : ""}`);
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 info = provided[name];
27197
- if (!info) {
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: ${info.version}`);
27224
+ ctx.line(`Version: ${info2.version}`);
27204
27225
  ctx.line(`Priority: optional`);
27205
27226
  ctx.line(`Architecture: amd64`);
27206
- ctx.line(`Description: ${info.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, info] of Object.entries(provided)) {
27238
- if (pattern.test(name) || pattern.test(info.description)) {
27239
- ctx.line(`${name}/stable ${info.version} amd64`);
27240
- ctx.line(` ${info.description}`);
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 record = (outcome, name, error) => {
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
- record("skip", display);
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
- record(outcome, display);
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
- record("skip", display);
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
- record("todo", display);
32265
+ record2("todo", display);
31542
32266
  report(depth, "todo", name, started);
31543
32267
  return true;
31544
32268
  }
31545
- record("fail", display, error);
32269
+ record2("fail", display, error);
31546
32270
  report(depth, "fail", name, started, errorText(error));
31547
32271
  return false;
31548
32272
  }
31549
- record(todo ? "todo" : "pass", display);
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") record("skip", fullName(child));
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
- record("cancelled", fullName(child), error);
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 ──────────────────────────────────────────────────────────