sandboxedjs 0.1.94 → 0.1.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 available = subscription.fd === 0 ? this.opts.stdin.available : this.bytesLeft(subscription.fd);
6990
- view.setBigUint64(at + 16, BigInt(available), true);
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 info;
7227
+ let info2;
7228
7228
  try {
7229
- info = inspectElf(bytes2);
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 ${info.architecture} ELF: ${reasons.join("; ") || "no binary backends registered"}`, 126);
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, info) {
8517
- this.listeners.set(port, { port, since: Date.now(), ...info });
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 info = unameInfo(ctx.kernel.hostname);
12110
+ const info2 = unameInfo(ctx.kernel.hostname);
12111
12111
  if (args.has("all")) {
12112
- ctx.line(`${info.sysname} ${info.nodename} ${info.release} ${info.version} ${info.machine} ${info.operatingSystem}`);
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(info.sysname);
12117
- if (args.has("nodename")) parts.push(info.nodename);
12118
- if (args.has("kernel-release")) parts.push(info.release);
12119
- if (args.has("kernel-version")) parts.push(info.version);
12120
- if (args.has("machine")) parts.push(info.machine);
12121
- if (args.has("processor")) parts.push(info.processor);
12122
- if (args.has("hardware-platform")) parts.push(info.hardwarePlatform);
12123
- if (args.has("operating-system")) parts.push(info.operatingSystem);
12124
- ctx.line(parts.length ? parts.join(" ") : info.sysname);
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(record, fs) {
15109
- if (record === "") return [];
15108
+ function splitRecord(record2, fs) {
15109
+ if (record2 === "") return [];
15110
15110
  if (fs === " ") {
15111
- const trimmed = record.replace(/^[ \t\n]+|[ \t\n]+$/g, "");
15111
+ const trimmed = record2.replace(/^[ \t\n]+|[ \t\n]+$/g, "");
15112
15112
  return trimmed === "" ? [] : trimmed.split(/[ \t\n]+/);
15113
15113
  }
15114
- if (fs === "") return [...record];
15115
- if (fs.length === 1 && !"\\^$.[]|()*+?{}".includes(fs)) return record.split(fs);
15116
- return record.split(new RegExp(translateEre(fs)));
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.94"};
20944
+ version: "0.1.95"};
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 search = async (queue, state) => {
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 search(rest, state);
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 search(rest, next);
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 search([...edges, ...rest], { chosen, constraints: next.constraints });
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 search(roots, { chosen: /* @__PURE__ */ new Map(), constraints: seeded });
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, available = []) {
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 (available.length > 0) {
23789
- const versions = [...new Set(available.map((c) => c.version))].slice(0, 8);
23790
- lines.push(`usable versions: ${versions.join(", ")}${available.length > 8 ? ", ..." : ""}`);
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
  }
@@ -26384,9 +26384,9 @@ function parsePax(data) {
26384
26384
  if (space < 0) break;
26385
26385
  const length = Number(value.slice(offset, space));
26386
26386
  if (!Number.isFinite(length) || length <= 0) break;
26387
- const record = value.slice(space + 1, offset + length - 1);
26388
- const equal2 = record.indexOf("=");
26389
- if (equal2 > 0) result[record.slice(0, equal2)] = record.slice(equal2 + 1);
26387
+ const record2 = value.slice(space + 1, offset + length - 1);
26388
+ const equal2 = record2.indexOf("=");
26389
+ if (equal2 > 0) result[record2.slice(0, equal2)] = record2.slice(equal2 + 1);
26390
26390
  offset += length;
26391
26391
  }
26392
26392
  return result;
@@ -27166,27 +27166,27 @@ var apt = defineCommand({
27166
27166
  switch (subcommand) {
27167
27167
  case "list": {
27168
27168
  const installedOnly = rest.includes("--installed");
27169
- for (const [name, info] of Object.entries(provided)) {
27170
- const available = ctx.kernel.which(name === "python3-pip" ? "pip" : name === "gawk" ? "awk" : name, ctx.cwd, ctx.env, ctx.cred) !== null;
27171
- if (installedOnly && !available) continue;
27172
- ctx.line(`${name}/stable ${info.version} amd64${available ? " [installed]" : ""}`);
27169
+ for (const [name, info2] of Object.entries(provided)) {
27170
+ const available2 = ctx.kernel.which(name === "python3-pip" ? "pip" : name === "gawk" ? "awk" : name, ctx.cwd, ctx.env, ctx.cred) !== null;
27171
+ if (installedOnly && !available2) continue;
27172
+ ctx.line(`${name}/stable ${info2.version} amd64${available2 ? " [installed]" : ""}`);
27173
27173
  }
27174
27174
  return 0;
27175
27175
  }
27176
27176
  case "show": {
27177
27177
  let status = 0;
27178
27178
  for (const name of rest) {
27179
- const info = provided[name];
27180
- if (!info) {
27179
+ const info2 = provided[name];
27180
+ if (!info2) {
27181
27181
  ctx.warn(`E: No packages found matching ${name}`);
27182
27182
  status = 1;
27183
27183
  continue;
27184
27184
  }
27185
27185
  ctx.line(`Package: ${name}`);
27186
- ctx.line(`Version: ${info.version}`);
27186
+ ctx.line(`Version: ${info2.version}`);
27187
27187
  ctx.line(`Priority: optional`);
27188
27188
  ctx.line(`Architecture: amd64`);
27189
- ctx.line(`Description: ${info.description}`);
27189
+ ctx.line(`Description: ${info2.description}`);
27190
27190
  ctx.line("");
27191
27191
  }
27192
27192
  return status;
@@ -27217,10 +27217,10 @@ var apt = defineCommand({
27217
27217
  return 100;
27218
27218
  case "search": {
27219
27219
  const pattern = new RegExp(rest[0] ?? "", "i");
27220
- for (const [name, info] of Object.entries(provided)) {
27221
- if (pattern.test(name) || pattern.test(info.description)) {
27222
- ctx.line(`${name}/stable ${info.version} amd64`);
27223
- ctx.line(` ${info.description}`);
27220
+ for (const [name, info2] of Object.entries(provided)) {
27221
+ if (pattern.test(name) || pattern.test(info2.description)) {
27222
+ ctx.line(`${name}/stable ${info2.version} amd64`);
27223
+ ctx.line(` ${info2.description}`);
27224
27224
  ctx.line("");
27225
27225
  }
27226
27226
  }
@@ -27685,6 +27685,701 @@ function browserCommands() {
27685
27685
  ];
27686
27686
  }
27687
27687
 
27688
+ // src/apps/pm.ts
27689
+ init_binary();
27690
+ init_zip();
27691
+
27692
+ // src/apps/registry.ts
27693
+ var APPS_ROOT = "/opt";
27694
+ var APP_BIN_DIR = "/usr/local/bin";
27695
+ var APP_STATE = "/var/lib/pm/installed.json";
27696
+ var DEFAULT_REGISTRY = "https://sandboxedjswheels.pages.dev/apps";
27697
+ var BUILT_IN_PUBLISHERS = [
27698
+ {
27699
+ publisher: "SandboxedJs",
27700
+ publicKey: "MCowBQYDK2VwAyEAuyxKMeyeO/05mr5Qu8vc+Dd9yMG2/oKDwt7KA+C9Mj4="
27701
+ }
27702
+ ];
27703
+ var AppRegistryError = class extends Error {
27704
+ };
27705
+ function parseIndex(text2, source) {
27706
+ let parsed;
27707
+ try {
27708
+ parsed = JSON.parse(text2);
27709
+ } catch (error) {
27710
+ throw new AppRegistryError(`${source} is not JSON: ${error.message}`);
27711
+ }
27712
+ const index = parsed;
27713
+ if (index?.format !== "sandboxedjs-app-index") {
27714
+ throw new AppRegistryError(
27715
+ `${source} is not an application index (its format is ${JSON.stringify(index?.format ?? null)})`
27716
+ );
27717
+ }
27718
+ if (!Array.isArray(index.apps)) {
27719
+ throw new AppRegistryError(`${source} has no apps`);
27720
+ }
27721
+ for (const app of index.apps) {
27722
+ for (const field of ["name", "version", "filename", "sha256"]) {
27723
+ if (typeof app[field] !== "string" || !app[field]) {
27724
+ throw new AppRegistryError(`${source}: an entry is missing ${field}`);
27725
+ }
27726
+ }
27727
+ }
27728
+ return index;
27729
+ }
27730
+ function selectRelease(index, name, version) {
27731
+ const matches3 = index.apps.filter((app) => app.name === name);
27732
+ if (matches3.length === 0) {
27733
+ const names = index.apps.map((app) => app.name).sort();
27734
+ throw new AppRegistryError(
27735
+ `no application named ${name}. The registry has: ${names.join(", ") || "nothing"}`
27736
+ );
27737
+ }
27738
+ if (version) {
27739
+ const exact = matches3.find((app) => app.version === version);
27740
+ if (!exact) {
27741
+ const versions = matches3.map((app) => app.version).join(", ");
27742
+ throw new AppRegistryError(`${name} has no version ${version}; it has ${versions}`);
27743
+ }
27744
+ return exact;
27745
+ }
27746
+ return matches3.slice().sort((a, b) => compareVersions2(b.version, a.version))[0];
27747
+ }
27748
+ function compareVersions2(a, b) {
27749
+ const left = a.split(/[.-]/);
27750
+ const right = b.split(/[.-]/);
27751
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
27752
+ const x = left[index] ?? "";
27753
+ const y = right[index] ?? "";
27754
+ const nx = Number(x);
27755
+ const ny = Number(y);
27756
+ if (Number.isFinite(nx) && Number.isFinite(ny) && x !== "" && y !== "") {
27757
+ if (nx !== ny) return nx - ny;
27758
+ continue;
27759
+ }
27760
+ if (x !== y) return x < y ? -1 : 1;
27761
+ }
27762
+ return 0;
27763
+ }
27764
+ function parseSpecifier(specifier) {
27765
+ const at = specifier.lastIndexOf("@");
27766
+ if (at <= 0) return { name: specifier };
27767
+ return { name: specifier.slice(0, at), version: specifier.slice(at + 1) };
27768
+ }
27769
+ function validateManifest2(manifest, source) {
27770
+ const app = manifest;
27771
+ if (typeof app?.name !== "string" || typeof app?.version !== "string") {
27772
+ throw new AppRegistryError(`${source} is not an application manifest`);
27773
+ }
27774
+ for (const binary of app.bins ?? []) {
27775
+ if (typeof binary?.name !== "string" || typeof binary?.path !== "string") {
27776
+ throw new AppRegistryError(`${source}: a bins entry needs a name and a path`);
27777
+ }
27778
+ if (binary.name.includes("/")) {
27779
+ throw new AppRegistryError(`${source}: a command name cannot contain a slash`);
27780
+ }
27781
+ }
27782
+ for (const service of app.services ?? []) {
27783
+ if (typeof service?.port !== "number" || !Number.isInteger(service.port)) {
27784
+ throw new AppRegistryError(`${source}: a service needs an integer port`);
27785
+ }
27786
+ }
27787
+ return app;
27788
+ }
27789
+
27790
+ // src/apps/sbjs.ts
27791
+ init_binary();
27792
+ init_zip();
27793
+ var MANIFEST_PATH = "META/manifest.json";
27794
+ var SIGNATURE_PATH = "META/signature.json";
27795
+ var TRUSTED_KEYS_PATH = "/etc/pm/trusted-keys.json";
27796
+ var PackageError = class extends Error {
27797
+ };
27798
+ async function openPackage(bytes2, trusted) {
27799
+ const entries = readZip(bytes2);
27800
+ const byName = new Map(entries.map((entry) => [entry.name, entry]));
27801
+ const manifestEntry = byName.get(MANIFEST_PATH);
27802
+ if (!manifestEntry) {
27803
+ throw new PackageError(
27804
+ `not a SandboxedJs package: it has no ${MANIFEST_PATH}. A plain zip can be installed with --allow-unsigned, but it carries no publisher.`
27805
+ );
27806
+ }
27807
+ const manifestBytes = manifestEntry.data();
27808
+ const manifest = parseManifest(manifestBytes);
27809
+ validateManifest2(manifest.app, `${MANIFEST_PATH}'s app`);
27810
+ for (const entry of entries) {
27811
+ if (entry.isDirectory || entry.name.startsWith("META/")) continue;
27812
+ const expected = manifest.files[entry.name];
27813
+ if (!expected) {
27814
+ throw new PackageError(`${entry.name} is in the package but not in its manifest`);
27815
+ }
27816
+ const actual = await digestHex("sha256", entry.data());
27817
+ if (actual !== expected) {
27818
+ throw new PackageError(
27819
+ `${entry.name} does not match the manifest (expected ${expected}, found ${actual})`
27820
+ );
27821
+ }
27822
+ }
27823
+ for (const name of Object.keys(manifest.files)) {
27824
+ if (!byName.has(name)) {
27825
+ throw new PackageError(`${name} is in the manifest but missing from the package`);
27826
+ }
27827
+ }
27828
+ const signatureEntry = byName.get(SIGNATURE_PATH);
27829
+ if (!signatureEntry) {
27830
+ return { manifest, signature: null, entries, trustedBy: null };
27831
+ }
27832
+ const signature = parseSignature(signatureEntry.data());
27833
+ const valid = await verifySignature(manifestBytes, signature);
27834
+ if (!valid) {
27835
+ throw new PackageError(
27836
+ `the signature on ${manifest.app.name} does not match its manifest; the package has been altered since it was signed`
27837
+ );
27838
+ }
27839
+ const trustedBy = trusted.find((key) => key.publicKey === signature.publicKey) ?? null;
27840
+ return { manifest, signature, entries, trustedBy };
27841
+ }
27842
+ function parseManifest(bytes2) {
27843
+ let parsed;
27844
+ try {
27845
+ parsed = JSON.parse(new TextDecoder().decode(bytes2));
27846
+ } catch (error) {
27847
+ throw new PackageError(`${MANIFEST_PATH} is not JSON: ${error.message}`);
27848
+ }
27849
+ const manifest = parsed;
27850
+ if (manifest?.format !== "sandboxedjs-package") {
27851
+ throw new PackageError(`${MANIFEST_PATH} is not a package manifest`);
27852
+ }
27853
+ if (!manifest.files || typeof manifest.files !== "object") {
27854
+ throw new PackageError(`${MANIFEST_PATH} lists no files`);
27855
+ }
27856
+ if (!manifest.app) {
27857
+ throw new PackageError(`${MANIFEST_PATH} carries no application manifest`);
27858
+ }
27859
+ return manifest;
27860
+ }
27861
+ function parseSignature(bytes2) {
27862
+ let parsed;
27863
+ try {
27864
+ parsed = JSON.parse(new TextDecoder().decode(bytes2));
27865
+ } catch (error) {
27866
+ throw new PackageError(`${SIGNATURE_PATH} is not JSON: ${error.message}`);
27867
+ }
27868
+ const signature = parsed;
27869
+ if (signature?.format !== "sandboxedjs-signature") {
27870
+ throw new PackageError(`${SIGNATURE_PATH} is not a signature`);
27871
+ }
27872
+ if (signature.algorithm !== "ed25519") {
27873
+ throw new PackageError(
27874
+ `${signature.algorithm} signatures are not supported by this runtime; only ed25519 is`
27875
+ );
27876
+ }
27877
+ for (const field of ["publicKey", "signature"]) {
27878
+ if (typeof signature[field] !== "string" || !signature[field]) {
27879
+ throw new PackageError(`${SIGNATURE_PATH} has no ${field}`);
27880
+ }
27881
+ }
27882
+ return signature;
27883
+ }
27884
+ async function verifySignature(manifestBytes, signature) {
27885
+ const subtle = globalThis.crypto?.subtle;
27886
+ if (!subtle) {
27887
+ throw new PackageError(
27888
+ "this runtime has no WebCrypto, so a package signature cannot be checked"
27889
+ );
27890
+ }
27891
+ let key;
27892
+ try {
27893
+ key = await subtle.importKey(
27894
+ "spki",
27895
+ intoBuffer(base64ToBytes(signature.publicKey)),
27896
+ { name: "Ed25519" },
27897
+ false,
27898
+ ["verify"]
27899
+ );
27900
+ } catch (error) {
27901
+ throw new PackageError(
27902
+ `cannot use the package's public key: ${error.message}. Ed25519 needs Node 20 or a browser that implements it.`
27903
+ );
27904
+ }
27905
+ return subtle.verify(
27906
+ { name: "Ed25519" },
27907
+ key,
27908
+ intoBuffer(base64ToBytes(signature.signature)),
27909
+ intoBuffer(manifestBytes)
27910
+ );
27911
+ }
27912
+ function intoBuffer(bytes2) {
27913
+ return bytes2.slice().buffer;
27914
+ }
27915
+ function base64ToBytes(text2) {
27916
+ const binary = atob(text2);
27917
+ const bytes2 = new Uint8Array(binary.length);
27918
+ for (let index = 0; index < binary.length; index += 1) {
27919
+ bytes2[index] = binary.charCodeAt(index);
27920
+ }
27921
+ return bytes2;
27922
+ }
27923
+ async function fingerprint(publicKey) {
27924
+ const digest2 = await digestHex("sha256", base64ToBytes(publicKey));
27925
+ return digest2.slice(0, 32).replace(/(.{4})(?=.)/g, "$1-");
27926
+ }
27927
+ function asRegistryError(error) {
27928
+ return new AppRegistryError(error.message);
27929
+ }
27930
+
27931
+ // src/apps/pm.ts
27932
+ function registryBase(ctx) {
27933
+ const configured = ctx.env.SBX_PM_REGISTRY ?? ctx.env.PM_REGISTRY;
27934
+ return configured && configured.trim() || DEFAULT_REGISTRY;
27935
+ }
27936
+ var pm = defineCommand({
27937
+ name: "pm",
27938
+ path: "/usr/bin/pm",
27939
+ summary: "install and manage applications",
27940
+ usage: "pm <install|remove|list|info|search|available> [name[@version]]",
27941
+ manual: `Applications are programs the container runs, packaged with what
27942
+ they need. They are not Python or Node packages: use pip or npm for those.
27943
+
27944
+ pm install NAME[@VERSION] download, verify and install
27945
+ pm remove NAME remove it and the commands it installed
27946
+ pm list what is installed here
27947
+ pm info NAME what a package provides
27948
+ pm search TEXT search the registry
27949
+ pm available everything the registry offers
27950
+ pm trust list|add|remove publisher keys this container trusts
27951
+
27952
+ Packages come from ${DEFAULT_REGISTRY} unless SBX_PM_REGISTRY names another.
27953
+
27954
+ A package is a .sbjs file: a zip carrying META/manifest.json, which lists a
27955
+ sha256 for every file in it, and META/signature.json, an Ed25519 signature
27956
+ over that manifest. Installing checks both, and refuses a package signed by a
27957
+ key this container does not trust. --allow-untrusted and --allow-unsigned
27958
+ override that, one install at a time.
27959
+
27960
+ A signature says who published a package and that nothing in it has changed
27961
+ since. It does not say the program is safe to run.`,
27962
+ async run(ctx) {
27963
+ const [subcommand, ...rest] = ctx.args;
27964
+ try {
27965
+ switch (subcommand) {
27966
+ case "install":
27967
+ return await install2(ctx, rest);
27968
+ case "remove":
27969
+ case "uninstall":
27970
+ return await remove(ctx, rest);
27971
+ case "list":
27972
+ return listInstalled2(ctx);
27973
+ case "info":
27974
+ return await info(ctx, rest);
27975
+ case "search":
27976
+ return await search(ctx, rest);
27977
+ case "available":
27978
+ return await available(ctx);
27979
+ case "trust":
27980
+ return await trust(ctx, rest);
27981
+ case void 0:
27982
+ case "--help":
27983
+ case "-h":
27984
+ ctx.line(pm.usage);
27985
+ return 0;
27986
+ default:
27987
+ ctx.warn(`pm: unknown command ${subcommand}`);
27988
+ return 2;
27989
+ }
27990
+ } catch (error) {
27991
+ ctx.warn(`pm: ${error.message}`);
27992
+ return error instanceof AppRegistryError ? 2 : 1;
27993
+ }
27994
+ }
27995
+ });
27996
+ async function install2(ctx, args) {
27997
+ if (args.length === 0) {
27998
+ ctx.warn("pm install: name it, as `pm install officesuite`");
27999
+ return 2;
28000
+ }
28001
+ const options = {
28002
+ allowUnsigned: args.includes("--allow-unsigned"),
28003
+ allowUntrusted: args.includes("--allow-untrusted") || args.includes("--allow-unsigned")
28004
+ };
28005
+ const names = args.filter((argument) => !argument.startsWith("--"));
28006
+ if (names.length === 0) {
28007
+ ctx.warn("pm install: name it, as `pm install officesuite`");
28008
+ return 2;
28009
+ }
28010
+ const index = await fetchIndex(ctx);
28011
+ for (const specifier of names) {
28012
+ const { name, version } = parseSpecifier(specifier);
28013
+ const release = selectRelease(index, name, version);
28014
+ await installRelease(ctx, index, release, /* @__PURE__ */ new Set(), options);
28015
+ }
28016
+ return 0;
28017
+ }
28018
+ async function installRelease(ctx, index, release, seen, options) {
28019
+ if (seen.has(release.name)) return;
28020
+ seen.add(release.name);
28021
+ for (const required of release.requires ?? []) {
28022
+ const { name, version } = parseSpecifier(required);
28023
+ if (installedApps(ctx).some((app) => app.name === name)) continue;
28024
+ await installRelease(ctx, index, selectRelease(index, name, version), seen, options);
28025
+ }
28026
+ const base2 = registryBase(ctx).replace(/\/$/, "");
28027
+ const url = `${base2}/${release.filename}`;
28028
+ ctx.line(`Fetching ${release.name} ${release.version}`);
28029
+ const archive = await download(url);
28030
+ const digest2 = await digestHex("sha256", archive);
28031
+ if (digest2 !== release.sha256) {
28032
+ throw new AppRegistryError(
28033
+ `${release.filename} failed verification: the registry publishes sha256 ${release.sha256} but the download hashes to ${digest2}`
28034
+ );
28035
+ }
28036
+ const verified = await verify(ctx, archive, release, options);
28037
+ const root = `${APPS_ROOT}/${release.name}`;
28038
+ removeInstalled(ctx, release.name, { quiet: true });
28039
+ const manifest = unpack(ctx, archive, root, release, verified);
28040
+ const bins = linkBinaries(ctx, manifest, root);
28041
+ record(ctx, {
28042
+ ...manifest,
28043
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
28044
+ root,
28045
+ installedBins: bins,
28046
+ sha256: release.sha256,
28047
+ ...verified?.signature ? {
28048
+ publisher: verified.signature.publisher ?? verified.trustedBy?.publisher ?? "unknown",
28049
+ publicKey: verified.signature.publicKey
28050
+ } : {}
28051
+ });
28052
+ ctx.line(`Installed ${manifest.name} ${manifest.version} in ${root}`);
28053
+ if (bins.length > 0) ctx.line(` commands: ${bins.join(", ")}`);
28054
+ for (const service of manifest.services ?? []) {
28055
+ ctx.line(
28056
+ ` service: ${service.name} on port ${service.port}` + (service.path ? ` at ${service.path}` : "") + ` (start it with \`${service.command}\`)`
28057
+ );
28058
+ }
28059
+ if (manifest.docs) ctx.line(` docs: ${root}/${manifest.docs}`);
28060
+ }
28061
+ async function download(url) {
28062
+ const response = await fetch(url);
28063
+ if (!response.ok) {
28064
+ throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
28065
+ }
28066
+ return new Uint8Array(await response.arrayBuffer());
28067
+ }
28068
+ async function verify(ctx, archive, release, options) {
28069
+ let opened;
28070
+ try {
28071
+ opened = await openPackage(archive, trustedKeys(ctx));
28072
+ } catch (error) {
28073
+ if (options.allowUnsigned && /has no META\/manifest\.json/.test(error.message)) {
28074
+ ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
28075
+ return null;
28076
+ }
28077
+ throw asRegistryError(error);
28078
+ }
28079
+ if (!opened.signature) {
28080
+ if (!options.allowUnsigned) {
28081
+ throw new AppRegistryError(
28082
+ `${release.name} carries no signature, so its publisher cannot be established. Install it with --allow-unsigned if you got it from somewhere you trust.`
28083
+ );
28084
+ }
28085
+ ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
28086
+ return opened;
28087
+ }
28088
+ const print = await fingerprint(opened.signature.publicKey);
28089
+ if (!opened.trustedBy && !options.allowUntrusted) {
28090
+ throw new AppRegistryError(
28091
+ `${release.name} is signed by a key this container does not trust (${opened.signature.publisher ?? "unnamed"}, ${print}). Add it with \`pm trust add ${print}\` after checking it, or install with --allow-untrusted.`
28092
+ );
28093
+ }
28094
+ ctx.line(
28095
+ opened.trustedBy ? ` signed by ${opened.trustedBy.publisher} (${print})` : ` signed by an untrusted key (${opened.signature.publisher ?? "unnamed"}, ${print})`
28096
+ );
28097
+ return opened;
28098
+ }
28099
+ function trustedKeys(ctx) {
28100
+ let stored = [];
28101
+ try {
28102
+ const parsed = JSON.parse(ctx.vfs.readText(TRUSTED_KEYS_PATH, ctx.cred));
28103
+ stored = Array.isArray(parsed.keys) ? parsed.keys : [];
28104
+ } catch {
28105
+ }
28106
+ const known = new Set(stored.map((key) => key.publicKey));
28107
+ return [...stored, ...BUILT_IN_PUBLISHERS.filter((key) => !known.has(key.publicKey))];
28108
+ }
28109
+ function writeTrustedKeys(ctx, keys) {
28110
+ ctx.vfs.mkdir("/etc/pm", { cred: ctx.cred, recursive: true });
28111
+ ctx.vfs.writeFile(
28112
+ TRUSTED_KEYS_PATH,
28113
+ `${JSON.stringify({ format: "sandboxedjs-trusted-keys", keys }, null, 2)}
28114
+ `,
28115
+ { cred: ctx.cred, mode: 420 }
28116
+ );
28117
+ }
28118
+ async function trust(ctx, args) {
28119
+ const [action, ...rest] = args;
28120
+ const keys = trustedKeys(ctx);
28121
+ switch (action) {
28122
+ case "list":
28123
+ case void 0: {
28124
+ if (keys.length === 0) {
28125
+ ctx.line("No publisher keys are trusted; only unsigned installs are possible.");
28126
+ return 0;
28127
+ }
28128
+ for (const key of keys) {
28129
+ ctx.line(`${key.publisher} ${await fingerprint(key.publicKey)}`);
28130
+ }
28131
+ return 0;
28132
+ }
28133
+ case "add": {
28134
+ const [publisher, publicKey] = rest;
28135
+ if (!publisher || !publicKey) {
28136
+ ctx.warn("pm trust add: give a publisher name and its base64 public key");
28137
+ return 2;
28138
+ }
28139
+ const next = keys.filter((key) => key.publicKey !== publicKey);
28140
+ next.push({ publisher, publicKey, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
28141
+ writeTrustedKeys(ctx, next);
28142
+ ctx.line(`Trusting ${publisher} (${await fingerprint(publicKey)})`);
28143
+ return 0;
28144
+ }
28145
+ case "remove": {
28146
+ const [publisher] = rest;
28147
+ const next = keys.filter((key) => key.publisher !== publisher);
28148
+ if (next.length === keys.length) {
28149
+ ctx.warn(`pm: no trusted key named ${publisher}`);
28150
+ return 1;
28151
+ }
28152
+ writeTrustedKeys(ctx, next);
28153
+ ctx.line(`Removed ${publisher}`);
28154
+ return 0;
28155
+ }
28156
+ default:
28157
+ ctx.warn(`pm trust: unknown action ${action}`);
28158
+ return 2;
28159
+ }
28160
+ }
28161
+ function unpack(ctx, archive, root, release, verified) {
28162
+ const entries = verified?.entries ?? readZip(archive);
28163
+ const manifest = verified?.manifest.app ?? readAppManifest(entries, release);
28164
+ if (manifest.name !== release.name || manifest.version !== release.version) {
28165
+ throw new AppRegistryError(
28166
+ `${release.filename} contains ${manifest.name} ${manifest.version}, but the registry lists it as ${release.name} ${release.version}`
28167
+ );
28168
+ }
28169
+ ctx.vfs.mkdir(root, { cred: ctx.cred, recursive: true });
28170
+ for (const entry of entries) {
28171
+ if (entry.name.startsWith("META/")) continue;
28172
+ const target = resolveInside(root, entry.name);
28173
+ if (entry.isDirectory) {
28174
+ ctx.vfs.mkdir(target, { cred: ctx.cred, recursive: true });
28175
+ continue;
28176
+ }
28177
+ const parent = target.slice(0, target.lastIndexOf("/"));
28178
+ if (parent) ctx.vfs.mkdir(parent, { cred: ctx.cred, recursive: true });
28179
+ const executable = (entry.mode & 73) !== 0 || (manifest.bins ?? []).some((binary) => binary.path === entry.name);
28180
+ ctx.vfs.writeFile(target, entry.data(), {
28181
+ cred: ctx.cred,
28182
+ mode: executable ? 493 : 420
28183
+ });
28184
+ }
28185
+ return manifest;
28186
+ }
28187
+ function readAppManifest(entries, release) {
28188
+ const entry = entries.find((candidate) => candidate.name === "app.json");
28189
+ if (!entry) throw new AppRegistryError(`${release.filename} has no app.json`);
28190
+ return validateManifest2(
28191
+ JSON.parse(new TextDecoder().decode(entry.data())),
28192
+ `${release.name}'s app.json`
28193
+ );
28194
+ }
28195
+ function resolveInside(root, name) {
28196
+ const clean2 = name.replace(/\\/g, "/");
28197
+ if (clean2.startsWith("/")) {
28198
+ throw new AppRegistryError(`archive entry ${name} is an absolute path`);
28199
+ }
28200
+ const parts = [];
28201
+ for (const part of clean2.split("/")) {
28202
+ if (part === "" || part === ".") continue;
28203
+ if (part === "..") {
28204
+ throw new AppRegistryError(`archive entry ${name} escapes the package directory`);
28205
+ }
28206
+ parts.push(part);
28207
+ }
28208
+ return `${root}/${parts.join("/")}`;
28209
+ }
28210
+ function linkBinaries(ctx, manifest, root) {
28211
+ const installed3 = [];
28212
+ if ((manifest.bins ?? []).length === 0) return installed3;
28213
+ ctx.vfs.mkdir(APP_BIN_DIR, { cred: ctx.cred, recursive: true });
28214
+ for (const binary of manifest.bins ?? []) {
28215
+ const target = resolveInside(root, binary.path);
28216
+ const wrapper = `${APP_BIN_DIR}/${binary.name}`;
28217
+ ctx.vfs.writeFile(
28218
+ wrapper,
28219
+ `#!/bin/sh
28220
+ # installed by pm for ${manifest.name} ${manifest.version}
28221
+ exec ${target} "$@"
28222
+ `,
28223
+ { cred: ctx.cred, mode: 493 }
28224
+ );
28225
+ installed3.push(binary.name);
28226
+ }
28227
+ return installed3;
28228
+ }
28229
+ async function remove(ctx, args) {
28230
+ if (args.length === 0) {
28231
+ ctx.warn("pm remove: name it");
28232
+ return 2;
28233
+ }
28234
+ let code = 0;
28235
+ for (const specifier of args) {
28236
+ const { name } = parseSpecifier(specifier);
28237
+ if (!removeInstalled(ctx, name, { quiet: false })) {
28238
+ ctx.warn(`pm: ${name} is not installed`);
28239
+ code = 1;
28240
+ } else {
28241
+ ctx.line(`Removed ${name}`);
28242
+ }
28243
+ }
28244
+ return code;
28245
+ }
28246
+ function removeInstalled(ctx, name, options) {
28247
+ const apps = installedApps(ctx);
28248
+ const app = apps.find((entry) => entry.name === name);
28249
+ if (!app) return false;
28250
+ for (const binary of app.installedBins) {
28251
+ try {
28252
+ ctx.vfs.unlink(`${APP_BIN_DIR}/${binary}`, ctx.cred);
28253
+ } catch {
28254
+ }
28255
+ }
28256
+ try {
28257
+ removeTree(ctx, app.root);
28258
+ } catch (error) {
28259
+ if (!options.quiet) ctx.warn(`pm: could not remove ${app.root}: ${error.message}`);
28260
+ }
28261
+ writeState(ctx, apps.filter((entry) => entry.name !== name));
28262
+ return true;
28263
+ }
28264
+ function removeTree(ctx, path) {
28265
+ let stats;
28266
+ try {
28267
+ stats = ctx.vfs.stat(path, { cred: ctx.cred });
28268
+ } catch {
28269
+ return;
28270
+ }
28271
+ if (stats.isDirectory()) {
28272
+ for (const entry of ctx.vfs.readdir(path, ctx.cred)) {
28273
+ removeTree(ctx, `${path}/${entry}`);
28274
+ }
28275
+ ctx.vfs.rmdir(path, ctx.cred);
28276
+ return;
28277
+ }
28278
+ ctx.vfs.unlink(path, ctx.cred);
28279
+ }
28280
+ function listInstalled2(ctx) {
28281
+ const apps = installedApps(ctx);
28282
+ if (apps.length === 0) {
28283
+ ctx.line("No applications installed. `pm available` lists what there is.");
28284
+ return 0;
28285
+ }
28286
+ for (const app of apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
28287
+ const commands12 = app.installedBins.length > 0 ? ` (${app.installedBins.join(", ")})` : "";
28288
+ ctx.line(`${app.name} ${app.version}${commands12}`);
28289
+ }
28290
+ return 0;
28291
+ }
28292
+ async function info(ctx, args) {
28293
+ const name = args[0];
28294
+ if (!name) {
28295
+ ctx.warn("pm info: name it");
28296
+ return 2;
28297
+ }
28298
+ const local = installedApps(ctx).find((app) => app.name === name);
28299
+ if (local) {
28300
+ ctx.line(`${local.name} ${local.version} (installed ${local.installedAt})`);
28301
+ if (local.summary) ctx.line(` ${local.summary}`);
28302
+ ctx.line(` root: ${local.root}`);
28303
+ if (local.installedBins.length > 0) ctx.line(` commands: ${local.installedBins.join(", ")}`);
28304
+ for (const service of local.services ?? []) {
28305
+ ctx.line(` service: ${service.name} on port ${service.port}${service.path ?? ""}`);
28306
+ }
28307
+ if (local.docs) ctx.line(` docs: ${local.root}/${local.docs}`);
28308
+ return 0;
28309
+ }
28310
+ const release = selectRelease(await fetchIndex(ctx), name);
28311
+ ctx.line(`${release.name} ${release.version} (not installed)`);
28312
+ if (release.summary) ctx.line(` ${release.summary}`);
28313
+ ctx.line(` size: ${Math.round(release.size / 1024)} kB`);
28314
+ if ((release.bins ?? []).length > 0) {
28315
+ ctx.line(` commands: ${(release.bins ?? []).map((b) => b.name).join(", ")}`);
28316
+ }
28317
+ return 0;
28318
+ }
28319
+ async function search(ctx, args) {
28320
+ const term = (args[0] ?? "").toLowerCase();
28321
+ const index = await fetchIndex(ctx);
28322
+ const matches3 = index.apps.filter(
28323
+ (app) => app.name.toLowerCase().includes(term) || (app.summary ?? "").toLowerCase().includes(term)
28324
+ );
28325
+ if (matches3.length === 0) {
28326
+ ctx.line(`nothing matches ${JSON.stringify(args[0] ?? "")}`);
28327
+ return 1;
28328
+ }
28329
+ for (const app of matches3) ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
28330
+ return 0;
28331
+ }
28332
+ async function available(ctx) {
28333
+ const index = await fetchIndex(ctx);
28334
+ for (const app of index.apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
28335
+ ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
28336
+ }
28337
+ return 0;
28338
+ }
28339
+ async function fetchIndex(ctx) {
28340
+ const base2 = registryBase(ctx).replace(/\/$/, "");
28341
+ const url = `${base2}/index.json`;
28342
+ let response;
28343
+ try {
28344
+ response = await fetch(url);
28345
+ } catch (error) {
28346
+ throw new AppRegistryError(
28347
+ `cannot reach the application registry at ${url}: ${error.message}`
28348
+ );
28349
+ }
28350
+ if (!response.ok) {
28351
+ throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
28352
+ }
28353
+ return parseIndex(await response.text(), url);
28354
+ }
28355
+ function installedApps(ctx) {
28356
+ try {
28357
+ const text2 = ctx.vfs.readText(APP_STATE, ctx.cred);
28358
+ const parsed = JSON.parse(text2);
28359
+ return Array.isArray(parsed.apps) ? parsed.apps : [];
28360
+ } catch {
28361
+ return [];
28362
+ }
28363
+ }
28364
+ function record(ctx, app) {
28365
+ const apps = installedApps(ctx).filter((entry) => entry.name !== app.name);
28366
+ apps.push(app);
28367
+ writeState(ctx, apps);
28368
+ }
28369
+ function writeState(ctx, apps) {
28370
+ const directory2 = APP_STATE.slice(0, APP_STATE.lastIndexOf("/"));
28371
+ ctx.vfs.mkdir(directory2, { cred: ctx.cred, recursive: true });
28372
+ ctx.vfs.writeFile(
28373
+ APP_STATE,
28374
+ `${JSON.stringify({ format: "sandboxedjs-installed-apps", apps }, null, 2)}
28375
+ `,
28376
+ { cred: ctx.cred, mode: 420 }
28377
+ );
28378
+ }
28379
+ function appCommands() {
28380
+ return [pm];
28381
+ }
28382
+
27688
28383
  // src/bin/index.ts
27689
28384
  function allCommands() {
27690
28385
  return [
@@ -27704,7 +28399,8 @@ function allCommands() {
27704
28399
  ...ffmpegCommands(),
27705
28400
  ...wasiCommands(),
27706
28401
  ...packageCommands(),
27707
- ...browserCommands()
28402
+ ...browserCommands(),
28403
+ ...appCommands()
27708
28404
  ];
27709
28405
  }
27710
28406
  function installUserland(kernel) {
@@ -31423,7 +32119,7 @@ ${indent(detail, 4)}
31423
32119
  if (detail) host2.write(`${indent(detail, depth * 2 + 2)}
31424
32120
  `);
31425
32121
  };
31426
- const record = (outcome, name, error) => {
32122
+ const record2 = (outcome, name, error) => {
31427
32123
  counts.tests += 1;
31428
32124
  if (outcome === "pass") counts.pass += 1;
31429
32125
  else if (outcome === "fail") {
@@ -31445,13 +32141,13 @@ ${indent(detail, 4)}
31445
32141
  if (!matchesPattern2(display)) return true;
31446
32142
  if (runOnly && !options.only) return true;
31447
32143
  if (options.skip) {
31448
- record("skip", display);
32144
+ record2("skip", display);
31449
32145
  report(depth, "skip", name, started);
31450
32146
  return true;
31451
32147
  }
31452
32148
  if (!fn) {
31453
32149
  const outcome = options.todo ? "todo" : "skip";
31454
- record(outcome, display);
32150
+ record2(outcome, display);
31455
32151
  report(depth, outcome, name, started);
31456
32152
  return true;
31457
32153
  }
@@ -31515,21 +32211,21 @@ ${indent(detail, 4)}
31515
32211
  controller.abort();
31516
32212
  }
31517
32213
  if (skipped) {
31518
- record("skip", display);
32214
+ record2("skip", display);
31519
32215
  report(depth, "skip", name, started);
31520
32216
  return true;
31521
32217
  }
31522
32218
  if (error !== void 0) {
31523
32219
  if (todo) {
31524
- record("todo", display);
32220
+ record2("todo", display);
31525
32221
  report(depth, "todo", name, started);
31526
32222
  return true;
31527
32223
  }
31528
- record("fail", display, error);
32224
+ record2("fail", display, error);
31529
32225
  report(depth, "fail", name, started, errorText(error));
31530
32226
  return false;
31531
32227
  }
31532
- record(todo ? "todo" : "pass", display);
32228
+ record2(todo ? "todo" : "pass", display);
31533
32229
  report(depth, todo ? "todo" : "pass", name, started);
31534
32230
  return true;
31535
32231
  };
@@ -31544,7 +32240,7 @@ ${indent(detail, 4)}
31544
32240
  else host2.write(`${" ".repeat(depth)}\u25B6 ${suite.name}
31545
32241
  `);
31546
32242
  if (suite.options.skip) {
31547
- for (const child of suite.children) if (child.kind === "test") record("skip", fullName(child));
32243
+ for (const child of suite.children) if (child.kind === "test") record2("skip", fullName(child));
31548
32244
  report(depth, "skip", suite.name, started);
31549
32245
  return true;
31550
32246
  }
@@ -31556,7 +32252,7 @@ ${indent(detail, 4)}
31556
32252
  } catch (error) {
31557
32253
  for (const child of suite.children) {
31558
32254
  if (child.kind === "test") {
31559
- record("cancelled", fullName(child), error);
32255
+ record2("cancelled", fullName(child), error);
31560
32256
  report(depth + (isRoot ? 0 : 1), "cancelled", child.name, started, errorText(error));
31561
32257
  }
31562
32258
  }
@@ -37014,6 +37710,14 @@ var Container = class _Container {
37014
37710
  installUserland(kernel);
37015
37711
  const container = new _Container(kernel, pod, opts);
37016
37712
  if (opts.files) container.mountSync(opts.files, { cwd: opts.cwd ?? "/" });
37713
+ if (opts.modules && opts.modules.length > 0) {
37714
+ const result = await container.exec(`pm install ${opts.modules.join(" ")}`);
37715
+ if (result.exitCode !== 0) {
37716
+ throw new Error(
37717
+ `could not install ${opts.modules.join(", ")}: ${(result.stderr || result.stdout).trim() || `pm exited ${result.exitCode}`}`
37718
+ );
37719
+ }
37720
+ }
37017
37721
  return container;
37018
37722
  }
37019
37723
  // ── file seeding ──────────────────────────────────────────────────────────