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.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.95"};
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
  }
@@ -26401,9 +26401,9 @@ function parsePax(data) {
26401
26401
  if (space < 0) break;
26402
26402
  const length = Number(value.slice(offset, space));
26403
26403
  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);
26404
+ const record2 = value.slice(space + 1, offset + length - 1);
26405
+ const equal2 = record2.indexOf("=");
26406
+ if (equal2 > 0) result[record2.slice(0, equal2)] = record2.slice(equal2 + 1);
26407
26407
  offset += length;
26408
26408
  }
26409
26409
  return result;
@@ -27183,27 +27183,27 @@ var apt = defineCommand({
27183
27183
  switch (subcommand) {
27184
27184
  case "list": {
27185
27185
  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]" : ""}`);
27186
+ for (const [name, info2] of Object.entries(provided)) {
27187
+ const available2 = ctx.kernel.which(name === "python3-pip" ? "pip" : name === "gawk" ? "awk" : name, ctx.cwd, ctx.env, ctx.cred) !== null;
27188
+ if (installedOnly && !available2) continue;
27189
+ ctx.line(`${name}/stable ${info2.version} amd64${available2 ? " [installed]" : ""}`);
27190
27190
  }
27191
27191
  return 0;
27192
27192
  }
27193
27193
  case "show": {
27194
27194
  let status = 0;
27195
27195
  for (const name of rest) {
27196
- const info = provided[name];
27197
- if (!info) {
27196
+ const info2 = provided[name];
27197
+ if (!info2) {
27198
27198
  ctx.warn(`E: No packages found matching ${name}`);
27199
27199
  status = 1;
27200
27200
  continue;
27201
27201
  }
27202
27202
  ctx.line(`Package: ${name}`);
27203
- ctx.line(`Version: ${info.version}`);
27203
+ ctx.line(`Version: ${info2.version}`);
27204
27204
  ctx.line(`Priority: optional`);
27205
27205
  ctx.line(`Architecture: amd64`);
27206
- ctx.line(`Description: ${info.description}`);
27206
+ ctx.line(`Description: ${info2.description}`);
27207
27207
  ctx.line("");
27208
27208
  }
27209
27209
  return status;
@@ -27234,10 +27234,10 @@ var apt = defineCommand({
27234
27234
  return 100;
27235
27235
  case "search": {
27236
27236
  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}`);
27237
+ for (const [name, info2] of Object.entries(provided)) {
27238
+ if (pattern.test(name) || pattern.test(info2.description)) {
27239
+ ctx.line(`${name}/stable ${info2.version} amd64`);
27240
+ ctx.line(` ${info2.description}`);
27241
27241
  ctx.line("");
27242
27242
  }
27243
27243
  }
@@ -27702,6 +27702,701 @@ function browserCommands() {
27702
27702
  ];
27703
27703
  }
27704
27704
 
27705
+ // src/apps/pm.ts
27706
+ init_binary();
27707
+ init_zip();
27708
+
27709
+ // src/apps/registry.ts
27710
+ var APPS_ROOT = "/opt";
27711
+ var APP_BIN_DIR = "/usr/local/bin";
27712
+ var APP_STATE = "/var/lib/pm/installed.json";
27713
+ var DEFAULT_REGISTRY = "https://sandboxedjswheels.pages.dev/apps";
27714
+ var BUILT_IN_PUBLISHERS = [
27715
+ {
27716
+ publisher: "SandboxedJs",
27717
+ publicKey: "MCowBQYDK2VwAyEAuyxKMeyeO/05mr5Qu8vc+Dd9yMG2/oKDwt7KA+C9Mj4="
27718
+ }
27719
+ ];
27720
+ var AppRegistryError = class extends Error {
27721
+ };
27722
+ function parseIndex(text2, source) {
27723
+ let parsed;
27724
+ try {
27725
+ parsed = JSON.parse(text2);
27726
+ } catch (error) {
27727
+ throw new AppRegistryError(`${source} is not JSON: ${error.message}`);
27728
+ }
27729
+ const index = parsed;
27730
+ if (index?.format !== "sandboxedjs-app-index") {
27731
+ throw new AppRegistryError(
27732
+ `${source} is not an application index (its format is ${JSON.stringify(index?.format ?? null)})`
27733
+ );
27734
+ }
27735
+ if (!Array.isArray(index.apps)) {
27736
+ throw new AppRegistryError(`${source} has no apps`);
27737
+ }
27738
+ for (const app of index.apps) {
27739
+ for (const field of ["name", "version", "filename", "sha256"]) {
27740
+ if (typeof app[field] !== "string" || !app[field]) {
27741
+ throw new AppRegistryError(`${source}: an entry is missing ${field}`);
27742
+ }
27743
+ }
27744
+ }
27745
+ return index;
27746
+ }
27747
+ function selectRelease(index, name, version) {
27748
+ const matches3 = index.apps.filter((app) => app.name === name);
27749
+ if (matches3.length === 0) {
27750
+ const names = index.apps.map((app) => app.name).sort();
27751
+ throw new AppRegistryError(
27752
+ `no application named ${name}. The registry has: ${names.join(", ") || "nothing"}`
27753
+ );
27754
+ }
27755
+ if (version) {
27756
+ const exact = matches3.find((app) => app.version === version);
27757
+ if (!exact) {
27758
+ const versions = matches3.map((app) => app.version).join(", ");
27759
+ throw new AppRegistryError(`${name} has no version ${version}; it has ${versions}`);
27760
+ }
27761
+ return exact;
27762
+ }
27763
+ return matches3.slice().sort((a, b) => compareVersions2(b.version, a.version))[0];
27764
+ }
27765
+ function compareVersions2(a, b) {
27766
+ const left = a.split(/[.-]/);
27767
+ const right = b.split(/[.-]/);
27768
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
27769
+ const x = left[index] ?? "";
27770
+ const y = right[index] ?? "";
27771
+ const nx = Number(x);
27772
+ const ny = Number(y);
27773
+ if (Number.isFinite(nx) && Number.isFinite(ny) && x !== "" && y !== "") {
27774
+ if (nx !== ny) return nx - ny;
27775
+ continue;
27776
+ }
27777
+ if (x !== y) return x < y ? -1 : 1;
27778
+ }
27779
+ return 0;
27780
+ }
27781
+ function parseSpecifier(specifier) {
27782
+ const at = specifier.lastIndexOf("@");
27783
+ if (at <= 0) return { name: specifier };
27784
+ return { name: specifier.slice(0, at), version: specifier.slice(at + 1) };
27785
+ }
27786
+ function validateManifest2(manifest, source) {
27787
+ const app = manifest;
27788
+ if (typeof app?.name !== "string" || typeof app?.version !== "string") {
27789
+ throw new AppRegistryError(`${source} is not an application manifest`);
27790
+ }
27791
+ for (const binary of app.bins ?? []) {
27792
+ if (typeof binary?.name !== "string" || typeof binary?.path !== "string") {
27793
+ throw new AppRegistryError(`${source}: a bins entry needs a name and a path`);
27794
+ }
27795
+ if (binary.name.includes("/")) {
27796
+ throw new AppRegistryError(`${source}: a command name cannot contain a slash`);
27797
+ }
27798
+ }
27799
+ for (const service of app.services ?? []) {
27800
+ if (typeof service?.port !== "number" || !Number.isInteger(service.port)) {
27801
+ throw new AppRegistryError(`${source}: a service needs an integer port`);
27802
+ }
27803
+ }
27804
+ return app;
27805
+ }
27806
+
27807
+ // src/apps/sbjs.ts
27808
+ init_binary();
27809
+ init_zip();
27810
+ var MANIFEST_PATH = "META/manifest.json";
27811
+ var SIGNATURE_PATH = "META/signature.json";
27812
+ var TRUSTED_KEYS_PATH = "/etc/pm/trusted-keys.json";
27813
+ var PackageError = class extends Error {
27814
+ };
27815
+ async function openPackage(bytes2, trusted) {
27816
+ const entries = readZip(bytes2);
27817
+ const byName = new Map(entries.map((entry) => [entry.name, entry]));
27818
+ const manifestEntry = byName.get(MANIFEST_PATH);
27819
+ if (!manifestEntry) {
27820
+ throw new PackageError(
27821
+ `not a SandboxedJs package: it has no ${MANIFEST_PATH}. A plain zip can be installed with --allow-unsigned, but it carries no publisher.`
27822
+ );
27823
+ }
27824
+ const manifestBytes = manifestEntry.data();
27825
+ const manifest = parseManifest(manifestBytes);
27826
+ validateManifest2(manifest.app, `${MANIFEST_PATH}'s app`);
27827
+ for (const entry of entries) {
27828
+ if (entry.isDirectory || entry.name.startsWith("META/")) continue;
27829
+ const expected = manifest.files[entry.name];
27830
+ if (!expected) {
27831
+ throw new PackageError(`${entry.name} is in the package but not in its manifest`);
27832
+ }
27833
+ const actual = await digestHex("sha256", entry.data());
27834
+ if (actual !== expected) {
27835
+ throw new PackageError(
27836
+ `${entry.name} does not match the manifest (expected ${expected}, found ${actual})`
27837
+ );
27838
+ }
27839
+ }
27840
+ for (const name of Object.keys(manifest.files)) {
27841
+ if (!byName.has(name)) {
27842
+ throw new PackageError(`${name} is in the manifest but missing from the package`);
27843
+ }
27844
+ }
27845
+ const signatureEntry = byName.get(SIGNATURE_PATH);
27846
+ if (!signatureEntry) {
27847
+ return { manifest, signature: null, entries, trustedBy: null };
27848
+ }
27849
+ const signature = parseSignature(signatureEntry.data());
27850
+ const valid = await verifySignature(manifestBytes, signature);
27851
+ if (!valid) {
27852
+ throw new PackageError(
27853
+ `the signature on ${manifest.app.name} does not match its manifest; the package has been altered since it was signed`
27854
+ );
27855
+ }
27856
+ const trustedBy = trusted.find((key) => key.publicKey === signature.publicKey) ?? null;
27857
+ return { manifest, signature, entries, trustedBy };
27858
+ }
27859
+ function parseManifest(bytes2) {
27860
+ let parsed;
27861
+ try {
27862
+ parsed = JSON.parse(new TextDecoder().decode(bytes2));
27863
+ } catch (error) {
27864
+ throw new PackageError(`${MANIFEST_PATH} is not JSON: ${error.message}`);
27865
+ }
27866
+ const manifest = parsed;
27867
+ if (manifest?.format !== "sandboxedjs-package") {
27868
+ throw new PackageError(`${MANIFEST_PATH} is not a package manifest`);
27869
+ }
27870
+ if (!manifest.files || typeof manifest.files !== "object") {
27871
+ throw new PackageError(`${MANIFEST_PATH} lists no files`);
27872
+ }
27873
+ if (!manifest.app) {
27874
+ throw new PackageError(`${MANIFEST_PATH} carries no application manifest`);
27875
+ }
27876
+ return manifest;
27877
+ }
27878
+ function parseSignature(bytes2) {
27879
+ let parsed;
27880
+ try {
27881
+ parsed = JSON.parse(new TextDecoder().decode(bytes2));
27882
+ } catch (error) {
27883
+ throw new PackageError(`${SIGNATURE_PATH} is not JSON: ${error.message}`);
27884
+ }
27885
+ const signature = parsed;
27886
+ if (signature?.format !== "sandboxedjs-signature") {
27887
+ throw new PackageError(`${SIGNATURE_PATH} is not a signature`);
27888
+ }
27889
+ if (signature.algorithm !== "ed25519") {
27890
+ throw new PackageError(
27891
+ `${signature.algorithm} signatures are not supported by this runtime; only ed25519 is`
27892
+ );
27893
+ }
27894
+ for (const field of ["publicKey", "signature"]) {
27895
+ if (typeof signature[field] !== "string" || !signature[field]) {
27896
+ throw new PackageError(`${SIGNATURE_PATH} has no ${field}`);
27897
+ }
27898
+ }
27899
+ return signature;
27900
+ }
27901
+ async function verifySignature(manifestBytes, signature) {
27902
+ const subtle = globalThis.crypto?.subtle;
27903
+ if (!subtle) {
27904
+ throw new PackageError(
27905
+ "this runtime has no WebCrypto, so a package signature cannot be checked"
27906
+ );
27907
+ }
27908
+ let key;
27909
+ try {
27910
+ key = await subtle.importKey(
27911
+ "spki",
27912
+ intoBuffer(base64ToBytes(signature.publicKey)),
27913
+ { name: "Ed25519" },
27914
+ false,
27915
+ ["verify"]
27916
+ );
27917
+ } catch (error) {
27918
+ throw new PackageError(
27919
+ `cannot use the package's public key: ${error.message}. Ed25519 needs Node 20 or a browser that implements it.`
27920
+ );
27921
+ }
27922
+ return subtle.verify(
27923
+ { name: "Ed25519" },
27924
+ key,
27925
+ intoBuffer(base64ToBytes(signature.signature)),
27926
+ intoBuffer(manifestBytes)
27927
+ );
27928
+ }
27929
+ function intoBuffer(bytes2) {
27930
+ return bytes2.slice().buffer;
27931
+ }
27932
+ function base64ToBytes(text2) {
27933
+ const binary = atob(text2);
27934
+ const bytes2 = new Uint8Array(binary.length);
27935
+ for (let index = 0; index < binary.length; index += 1) {
27936
+ bytes2[index] = binary.charCodeAt(index);
27937
+ }
27938
+ return bytes2;
27939
+ }
27940
+ async function fingerprint(publicKey) {
27941
+ const digest2 = await digestHex("sha256", base64ToBytes(publicKey));
27942
+ return digest2.slice(0, 32).replace(/(.{4})(?=.)/g, "$1-");
27943
+ }
27944
+ function asRegistryError(error) {
27945
+ return new AppRegistryError(error.message);
27946
+ }
27947
+
27948
+ // src/apps/pm.ts
27949
+ function registryBase(ctx) {
27950
+ const configured = ctx.env.SBX_PM_REGISTRY ?? ctx.env.PM_REGISTRY;
27951
+ return configured && configured.trim() || DEFAULT_REGISTRY;
27952
+ }
27953
+ var pm = defineCommand({
27954
+ name: "pm",
27955
+ path: "/usr/bin/pm",
27956
+ summary: "install and manage applications",
27957
+ usage: "pm <install|remove|list|info|search|available> [name[@version]]",
27958
+ manual: `Applications are programs the container runs, packaged with what
27959
+ they need. They are not Python or Node packages: use pip or npm for those.
27960
+
27961
+ pm install NAME[@VERSION] download, verify and install
27962
+ pm remove NAME remove it and the commands it installed
27963
+ pm list what is installed here
27964
+ pm info NAME what a package provides
27965
+ pm search TEXT search the registry
27966
+ pm available everything the registry offers
27967
+ pm trust list|add|remove publisher keys this container trusts
27968
+
27969
+ Packages come from ${DEFAULT_REGISTRY} unless SBX_PM_REGISTRY names another.
27970
+
27971
+ A package is a .sbjs file: a zip carrying META/manifest.json, which lists a
27972
+ sha256 for every file in it, and META/signature.json, an Ed25519 signature
27973
+ over that manifest. Installing checks both, and refuses a package signed by a
27974
+ key this container does not trust. --allow-untrusted and --allow-unsigned
27975
+ override that, one install at a time.
27976
+
27977
+ A signature says who published a package and that nothing in it has changed
27978
+ since. It does not say the program is safe to run.`,
27979
+ async run(ctx) {
27980
+ const [subcommand, ...rest] = ctx.args;
27981
+ try {
27982
+ switch (subcommand) {
27983
+ case "install":
27984
+ return await install2(ctx, rest);
27985
+ case "remove":
27986
+ case "uninstall":
27987
+ return await remove(ctx, rest);
27988
+ case "list":
27989
+ return listInstalled2(ctx);
27990
+ case "info":
27991
+ return await info(ctx, rest);
27992
+ case "search":
27993
+ return await search(ctx, rest);
27994
+ case "available":
27995
+ return await available(ctx);
27996
+ case "trust":
27997
+ return await trust(ctx, rest);
27998
+ case void 0:
27999
+ case "--help":
28000
+ case "-h":
28001
+ ctx.line(pm.usage);
28002
+ return 0;
28003
+ default:
28004
+ ctx.warn(`pm: unknown command ${subcommand}`);
28005
+ return 2;
28006
+ }
28007
+ } catch (error) {
28008
+ ctx.warn(`pm: ${error.message}`);
28009
+ return error instanceof AppRegistryError ? 2 : 1;
28010
+ }
28011
+ }
28012
+ });
28013
+ async function install2(ctx, args) {
28014
+ if (args.length === 0) {
28015
+ ctx.warn("pm install: name it, as `pm install officesuite`");
28016
+ return 2;
28017
+ }
28018
+ const options = {
28019
+ allowUnsigned: args.includes("--allow-unsigned"),
28020
+ allowUntrusted: args.includes("--allow-untrusted") || args.includes("--allow-unsigned")
28021
+ };
28022
+ const names = args.filter((argument) => !argument.startsWith("--"));
28023
+ if (names.length === 0) {
28024
+ ctx.warn("pm install: name it, as `pm install officesuite`");
28025
+ return 2;
28026
+ }
28027
+ const index = await fetchIndex(ctx);
28028
+ for (const specifier of names) {
28029
+ const { name, version } = parseSpecifier(specifier);
28030
+ const release = selectRelease(index, name, version);
28031
+ await installRelease(ctx, index, release, /* @__PURE__ */ new Set(), options);
28032
+ }
28033
+ return 0;
28034
+ }
28035
+ async function installRelease(ctx, index, release, seen, options) {
28036
+ if (seen.has(release.name)) return;
28037
+ seen.add(release.name);
28038
+ for (const required of release.requires ?? []) {
28039
+ const { name, version } = parseSpecifier(required);
28040
+ if (installedApps(ctx).some((app) => app.name === name)) continue;
28041
+ await installRelease(ctx, index, selectRelease(index, name, version), seen, options);
28042
+ }
28043
+ const base2 = registryBase(ctx).replace(/\/$/, "");
28044
+ const url = `${base2}/${release.filename}`;
28045
+ ctx.line(`Fetching ${release.name} ${release.version}`);
28046
+ const archive = await download(url);
28047
+ const digest2 = await digestHex("sha256", archive);
28048
+ if (digest2 !== release.sha256) {
28049
+ throw new AppRegistryError(
28050
+ `${release.filename} failed verification: the registry publishes sha256 ${release.sha256} but the download hashes to ${digest2}`
28051
+ );
28052
+ }
28053
+ const verified = await verify(ctx, archive, release, options);
28054
+ const root = `${APPS_ROOT}/${release.name}`;
28055
+ removeInstalled(ctx, release.name, { quiet: true });
28056
+ const manifest = unpack(ctx, archive, root, release, verified);
28057
+ const bins = linkBinaries(ctx, manifest, root);
28058
+ record(ctx, {
28059
+ ...manifest,
28060
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
28061
+ root,
28062
+ installedBins: bins,
28063
+ sha256: release.sha256,
28064
+ ...verified?.signature ? {
28065
+ publisher: verified.signature.publisher ?? verified.trustedBy?.publisher ?? "unknown",
28066
+ publicKey: verified.signature.publicKey
28067
+ } : {}
28068
+ });
28069
+ ctx.line(`Installed ${manifest.name} ${manifest.version} in ${root}`);
28070
+ if (bins.length > 0) ctx.line(` commands: ${bins.join(", ")}`);
28071
+ for (const service of manifest.services ?? []) {
28072
+ ctx.line(
28073
+ ` service: ${service.name} on port ${service.port}` + (service.path ? ` at ${service.path}` : "") + ` (start it with \`${service.command}\`)`
28074
+ );
28075
+ }
28076
+ if (manifest.docs) ctx.line(` docs: ${root}/${manifest.docs}`);
28077
+ }
28078
+ async function download(url) {
28079
+ const response = await fetch(url);
28080
+ if (!response.ok) {
28081
+ throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
28082
+ }
28083
+ return new Uint8Array(await response.arrayBuffer());
28084
+ }
28085
+ async function verify(ctx, archive, release, options) {
28086
+ let opened;
28087
+ try {
28088
+ opened = await openPackage(archive, trustedKeys(ctx));
28089
+ } catch (error) {
28090
+ if (options.allowUnsigned && /has no META\/manifest\.json/.test(error.message)) {
28091
+ ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
28092
+ return null;
28093
+ }
28094
+ throw asRegistryError(error);
28095
+ }
28096
+ if (!opened.signature) {
28097
+ if (!options.allowUnsigned) {
28098
+ throw new AppRegistryError(
28099
+ `${release.name} carries no signature, so its publisher cannot be established. Install it with --allow-unsigned if you got it from somewhere you trust.`
28100
+ );
28101
+ }
28102
+ ctx.warn(`pm: ${release.name} is unsigned and its publisher is unknown`);
28103
+ return opened;
28104
+ }
28105
+ const print = await fingerprint(opened.signature.publicKey);
28106
+ if (!opened.trustedBy && !options.allowUntrusted) {
28107
+ throw new AppRegistryError(
28108
+ `${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.`
28109
+ );
28110
+ }
28111
+ ctx.line(
28112
+ opened.trustedBy ? ` signed by ${opened.trustedBy.publisher} (${print})` : ` signed by an untrusted key (${opened.signature.publisher ?? "unnamed"}, ${print})`
28113
+ );
28114
+ return opened;
28115
+ }
28116
+ function trustedKeys(ctx) {
28117
+ let stored = [];
28118
+ try {
28119
+ const parsed = JSON.parse(ctx.vfs.readText(TRUSTED_KEYS_PATH, ctx.cred));
28120
+ stored = Array.isArray(parsed.keys) ? parsed.keys : [];
28121
+ } catch {
28122
+ }
28123
+ const known = new Set(stored.map((key) => key.publicKey));
28124
+ return [...stored, ...BUILT_IN_PUBLISHERS.filter((key) => !known.has(key.publicKey))];
28125
+ }
28126
+ function writeTrustedKeys(ctx, keys) {
28127
+ ctx.vfs.mkdir("/etc/pm", { cred: ctx.cred, recursive: true });
28128
+ ctx.vfs.writeFile(
28129
+ TRUSTED_KEYS_PATH,
28130
+ `${JSON.stringify({ format: "sandboxedjs-trusted-keys", keys }, null, 2)}
28131
+ `,
28132
+ { cred: ctx.cred, mode: 420 }
28133
+ );
28134
+ }
28135
+ async function trust(ctx, args) {
28136
+ const [action, ...rest] = args;
28137
+ const keys = trustedKeys(ctx);
28138
+ switch (action) {
28139
+ case "list":
28140
+ case void 0: {
28141
+ if (keys.length === 0) {
28142
+ ctx.line("No publisher keys are trusted; only unsigned installs are possible.");
28143
+ return 0;
28144
+ }
28145
+ for (const key of keys) {
28146
+ ctx.line(`${key.publisher} ${await fingerprint(key.publicKey)}`);
28147
+ }
28148
+ return 0;
28149
+ }
28150
+ case "add": {
28151
+ const [publisher, publicKey] = rest;
28152
+ if (!publisher || !publicKey) {
28153
+ ctx.warn("pm trust add: give a publisher name and its base64 public key");
28154
+ return 2;
28155
+ }
28156
+ const next = keys.filter((key) => key.publicKey !== publicKey);
28157
+ next.push({ publisher, publicKey, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
28158
+ writeTrustedKeys(ctx, next);
28159
+ ctx.line(`Trusting ${publisher} (${await fingerprint(publicKey)})`);
28160
+ return 0;
28161
+ }
28162
+ case "remove": {
28163
+ const [publisher] = rest;
28164
+ const next = keys.filter((key) => key.publisher !== publisher);
28165
+ if (next.length === keys.length) {
28166
+ ctx.warn(`pm: no trusted key named ${publisher}`);
28167
+ return 1;
28168
+ }
28169
+ writeTrustedKeys(ctx, next);
28170
+ ctx.line(`Removed ${publisher}`);
28171
+ return 0;
28172
+ }
28173
+ default:
28174
+ ctx.warn(`pm trust: unknown action ${action}`);
28175
+ return 2;
28176
+ }
28177
+ }
28178
+ function unpack(ctx, archive, root, release, verified) {
28179
+ const entries = verified?.entries ?? readZip(archive);
28180
+ const manifest = verified?.manifest.app ?? readAppManifest(entries, release);
28181
+ if (manifest.name !== release.name || manifest.version !== release.version) {
28182
+ throw new AppRegistryError(
28183
+ `${release.filename} contains ${manifest.name} ${manifest.version}, but the registry lists it as ${release.name} ${release.version}`
28184
+ );
28185
+ }
28186
+ ctx.vfs.mkdir(root, { cred: ctx.cred, recursive: true });
28187
+ for (const entry of entries) {
28188
+ if (entry.name.startsWith("META/")) continue;
28189
+ const target = resolveInside(root, entry.name);
28190
+ if (entry.isDirectory) {
28191
+ ctx.vfs.mkdir(target, { cred: ctx.cred, recursive: true });
28192
+ continue;
28193
+ }
28194
+ const parent = target.slice(0, target.lastIndexOf("/"));
28195
+ if (parent) ctx.vfs.mkdir(parent, { cred: ctx.cred, recursive: true });
28196
+ const executable = (entry.mode & 73) !== 0 || (manifest.bins ?? []).some((binary) => binary.path === entry.name);
28197
+ ctx.vfs.writeFile(target, entry.data(), {
28198
+ cred: ctx.cred,
28199
+ mode: executable ? 493 : 420
28200
+ });
28201
+ }
28202
+ return manifest;
28203
+ }
28204
+ function readAppManifest(entries, release) {
28205
+ const entry = entries.find((candidate) => candidate.name === "app.json");
28206
+ if (!entry) throw new AppRegistryError(`${release.filename} has no app.json`);
28207
+ return validateManifest2(
28208
+ JSON.parse(new TextDecoder().decode(entry.data())),
28209
+ `${release.name}'s app.json`
28210
+ );
28211
+ }
28212
+ function resolveInside(root, name) {
28213
+ const clean2 = name.replace(/\\/g, "/");
28214
+ if (clean2.startsWith("/")) {
28215
+ throw new AppRegistryError(`archive entry ${name} is an absolute path`);
28216
+ }
28217
+ const parts = [];
28218
+ for (const part of clean2.split("/")) {
28219
+ if (part === "" || part === ".") continue;
28220
+ if (part === "..") {
28221
+ throw new AppRegistryError(`archive entry ${name} escapes the package directory`);
28222
+ }
28223
+ parts.push(part);
28224
+ }
28225
+ return `${root}/${parts.join("/")}`;
28226
+ }
28227
+ function linkBinaries(ctx, manifest, root) {
28228
+ const installed3 = [];
28229
+ if ((manifest.bins ?? []).length === 0) return installed3;
28230
+ ctx.vfs.mkdir(APP_BIN_DIR, { cred: ctx.cred, recursive: true });
28231
+ for (const binary of manifest.bins ?? []) {
28232
+ const target = resolveInside(root, binary.path);
28233
+ const wrapper = `${APP_BIN_DIR}/${binary.name}`;
28234
+ ctx.vfs.writeFile(
28235
+ wrapper,
28236
+ `#!/bin/sh
28237
+ # installed by pm for ${manifest.name} ${manifest.version}
28238
+ exec ${target} "$@"
28239
+ `,
28240
+ { cred: ctx.cred, mode: 493 }
28241
+ );
28242
+ installed3.push(binary.name);
28243
+ }
28244
+ return installed3;
28245
+ }
28246
+ async function remove(ctx, args) {
28247
+ if (args.length === 0) {
28248
+ ctx.warn("pm remove: name it");
28249
+ return 2;
28250
+ }
28251
+ let code = 0;
28252
+ for (const specifier of args) {
28253
+ const { name } = parseSpecifier(specifier);
28254
+ if (!removeInstalled(ctx, name, { quiet: false })) {
28255
+ ctx.warn(`pm: ${name} is not installed`);
28256
+ code = 1;
28257
+ } else {
28258
+ ctx.line(`Removed ${name}`);
28259
+ }
28260
+ }
28261
+ return code;
28262
+ }
28263
+ function removeInstalled(ctx, name, options) {
28264
+ const apps = installedApps(ctx);
28265
+ const app = apps.find((entry) => entry.name === name);
28266
+ if (!app) return false;
28267
+ for (const binary of app.installedBins) {
28268
+ try {
28269
+ ctx.vfs.unlink(`${APP_BIN_DIR}/${binary}`, ctx.cred);
28270
+ } catch {
28271
+ }
28272
+ }
28273
+ try {
28274
+ removeTree(ctx, app.root);
28275
+ } catch (error) {
28276
+ if (!options.quiet) ctx.warn(`pm: could not remove ${app.root}: ${error.message}`);
28277
+ }
28278
+ writeState(ctx, apps.filter((entry) => entry.name !== name));
28279
+ return true;
28280
+ }
28281
+ function removeTree(ctx, path) {
28282
+ let stats;
28283
+ try {
28284
+ stats = ctx.vfs.stat(path, { cred: ctx.cred });
28285
+ } catch {
28286
+ return;
28287
+ }
28288
+ if (stats.isDirectory()) {
28289
+ for (const entry of ctx.vfs.readdir(path, ctx.cred)) {
28290
+ removeTree(ctx, `${path}/${entry}`);
28291
+ }
28292
+ ctx.vfs.rmdir(path, ctx.cred);
28293
+ return;
28294
+ }
28295
+ ctx.vfs.unlink(path, ctx.cred);
28296
+ }
28297
+ function listInstalled2(ctx) {
28298
+ const apps = installedApps(ctx);
28299
+ if (apps.length === 0) {
28300
+ ctx.line("No applications installed. `pm available` lists what there is.");
28301
+ return 0;
28302
+ }
28303
+ for (const app of apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
28304
+ const commands12 = app.installedBins.length > 0 ? ` (${app.installedBins.join(", ")})` : "";
28305
+ ctx.line(`${app.name} ${app.version}${commands12}`);
28306
+ }
28307
+ return 0;
28308
+ }
28309
+ async function info(ctx, args) {
28310
+ const name = args[0];
28311
+ if (!name) {
28312
+ ctx.warn("pm info: name it");
28313
+ return 2;
28314
+ }
28315
+ const local = installedApps(ctx).find((app) => app.name === name);
28316
+ if (local) {
28317
+ ctx.line(`${local.name} ${local.version} (installed ${local.installedAt})`);
28318
+ if (local.summary) ctx.line(` ${local.summary}`);
28319
+ ctx.line(` root: ${local.root}`);
28320
+ if (local.installedBins.length > 0) ctx.line(` commands: ${local.installedBins.join(", ")}`);
28321
+ for (const service of local.services ?? []) {
28322
+ ctx.line(` service: ${service.name} on port ${service.port}${service.path ?? ""}`);
28323
+ }
28324
+ if (local.docs) ctx.line(` docs: ${local.root}/${local.docs}`);
28325
+ return 0;
28326
+ }
28327
+ const release = selectRelease(await fetchIndex(ctx), name);
28328
+ ctx.line(`${release.name} ${release.version} (not installed)`);
28329
+ if (release.summary) ctx.line(` ${release.summary}`);
28330
+ ctx.line(` size: ${Math.round(release.size / 1024)} kB`);
28331
+ if ((release.bins ?? []).length > 0) {
28332
+ ctx.line(` commands: ${(release.bins ?? []).map((b) => b.name).join(", ")}`);
28333
+ }
28334
+ return 0;
28335
+ }
28336
+ async function search(ctx, args) {
28337
+ const term = (args[0] ?? "").toLowerCase();
28338
+ const index = await fetchIndex(ctx);
28339
+ const matches3 = index.apps.filter(
28340
+ (app) => app.name.toLowerCase().includes(term) || (app.summary ?? "").toLowerCase().includes(term)
28341
+ );
28342
+ if (matches3.length === 0) {
28343
+ ctx.line(`nothing matches ${JSON.stringify(args[0] ?? "")}`);
28344
+ return 1;
28345
+ }
28346
+ for (const app of matches3) ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
28347
+ return 0;
28348
+ }
28349
+ async function available(ctx) {
28350
+ const index = await fetchIndex(ctx);
28351
+ for (const app of index.apps.slice().sort((a, b) => a.name.localeCompare(b.name))) {
28352
+ ctx.line(`${app.name} ${app.version} ${app.summary ?? ""}`);
28353
+ }
28354
+ return 0;
28355
+ }
28356
+ async function fetchIndex(ctx) {
28357
+ const base2 = registryBase(ctx).replace(/\/$/, "");
28358
+ const url = `${base2}/index.json`;
28359
+ let response;
28360
+ try {
28361
+ response = await fetch(url);
28362
+ } catch (error) {
28363
+ throw new AppRegistryError(
28364
+ `cannot reach the application registry at ${url}: ${error.message}`
28365
+ );
28366
+ }
28367
+ if (!response.ok) {
28368
+ throw new AppRegistryError(`${url} answered ${response.status} ${response.statusText}`);
28369
+ }
28370
+ return parseIndex(await response.text(), url);
28371
+ }
28372
+ function installedApps(ctx) {
28373
+ try {
28374
+ const text2 = ctx.vfs.readText(APP_STATE, ctx.cred);
28375
+ const parsed = JSON.parse(text2);
28376
+ return Array.isArray(parsed.apps) ? parsed.apps : [];
28377
+ } catch {
28378
+ return [];
28379
+ }
28380
+ }
28381
+ function record(ctx, app) {
28382
+ const apps = installedApps(ctx).filter((entry) => entry.name !== app.name);
28383
+ apps.push(app);
28384
+ writeState(ctx, apps);
28385
+ }
28386
+ function writeState(ctx, apps) {
28387
+ const directory2 = APP_STATE.slice(0, APP_STATE.lastIndexOf("/"));
28388
+ ctx.vfs.mkdir(directory2, { cred: ctx.cred, recursive: true });
28389
+ ctx.vfs.writeFile(
28390
+ APP_STATE,
28391
+ `${JSON.stringify({ format: "sandboxedjs-installed-apps", apps }, null, 2)}
28392
+ `,
28393
+ { cred: ctx.cred, mode: 420 }
28394
+ );
28395
+ }
28396
+ function appCommands() {
28397
+ return [pm];
28398
+ }
28399
+
27705
28400
  // src/bin/index.ts
27706
28401
  function allCommands() {
27707
28402
  return [
@@ -27721,7 +28416,8 @@ function allCommands() {
27721
28416
  ...ffmpegCommands(),
27722
28417
  ...wasiCommands(),
27723
28418
  ...packageCommands(),
27724
- ...browserCommands()
28419
+ ...browserCommands(),
28420
+ ...appCommands()
27725
28421
  ];
27726
28422
  }
27727
28423
  function installUserland(kernel) {
@@ -31440,7 +32136,7 @@ ${indent(detail, 4)}
31440
32136
  if (detail) host2.write(`${indent(detail, depth * 2 + 2)}
31441
32137
  `);
31442
32138
  };
31443
- const record = (outcome, name, error) => {
32139
+ const record2 = (outcome, name, error) => {
31444
32140
  counts.tests += 1;
31445
32141
  if (outcome === "pass") counts.pass += 1;
31446
32142
  else if (outcome === "fail") {
@@ -31462,13 +32158,13 @@ ${indent(detail, 4)}
31462
32158
  if (!matchesPattern2(display)) return true;
31463
32159
  if (runOnly && !options.only) return true;
31464
32160
  if (options.skip) {
31465
- record("skip", display);
32161
+ record2("skip", display);
31466
32162
  report(depth, "skip", name, started);
31467
32163
  return true;
31468
32164
  }
31469
32165
  if (!fn) {
31470
32166
  const outcome = options.todo ? "todo" : "skip";
31471
- record(outcome, display);
32167
+ record2(outcome, display);
31472
32168
  report(depth, outcome, name, started);
31473
32169
  return true;
31474
32170
  }
@@ -31532,21 +32228,21 @@ ${indent(detail, 4)}
31532
32228
  controller.abort();
31533
32229
  }
31534
32230
  if (skipped) {
31535
- record("skip", display);
32231
+ record2("skip", display);
31536
32232
  report(depth, "skip", name, started);
31537
32233
  return true;
31538
32234
  }
31539
32235
  if (error !== void 0) {
31540
32236
  if (todo) {
31541
- record("todo", display);
32237
+ record2("todo", display);
31542
32238
  report(depth, "todo", name, started);
31543
32239
  return true;
31544
32240
  }
31545
- record("fail", display, error);
32241
+ record2("fail", display, error);
31546
32242
  report(depth, "fail", name, started, errorText(error));
31547
32243
  return false;
31548
32244
  }
31549
- record(todo ? "todo" : "pass", display);
32245
+ record2(todo ? "todo" : "pass", display);
31550
32246
  report(depth, todo ? "todo" : "pass", name, started);
31551
32247
  return true;
31552
32248
  };
@@ -31561,7 +32257,7 @@ ${indent(detail, 4)}
31561
32257
  else host2.write(`${" ".repeat(depth)}\u25B6 ${suite.name}
31562
32258
  `);
31563
32259
  if (suite.options.skip) {
31564
- for (const child of suite.children) if (child.kind === "test") record("skip", fullName(child));
32260
+ for (const child of suite.children) if (child.kind === "test") record2("skip", fullName(child));
31565
32261
  report(depth, "skip", suite.name, started);
31566
32262
  return true;
31567
32263
  }
@@ -31573,7 +32269,7 @@ ${indent(detail, 4)}
31573
32269
  } catch (error) {
31574
32270
  for (const child of suite.children) {
31575
32271
  if (child.kind === "test") {
31576
- record("cancelled", fullName(child), error);
32272
+ record2("cancelled", fullName(child), error);
31577
32273
  report(depth + (isRoot ? 0 : 1), "cancelled", child.name, started, errorText(error));
31578
32274
  }
31579
32275
  }
@@ -37031,6 +37727,14 @@ var Container = class _Container {
37031
37727
  installUserland(kernel);
37032
37728
  const container = new _Container(kernel, pod, opts);
37033
37729
  if (opts.files) container.mountSync(opts.files, { cwd: opts.cwd ?? "/" });
37730
+ if (opts.modules && opts.modules.length > 0) {
37731
+ const result = await container.exec(`pm install ${opts.modules.join(" ")}`);
37732
+ if (result.exitCode !== 0) {
37733
+ throw new Error(
37734
+ `could not install ${opts.modules.join(", ")}: ${(result.stderr || result.stdout).trim() || `pm exited ${result.exitCode}`}`
37735
+ );
37736
+ }
37737
+ }
37034
37738
  return container;
37035
37739
  }
37036
37740
  // ── file seeding ──────────────────────────────────────────────────────────