@poe-platform/safe-bash 0.1.423 → 0.1.425

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.
Files changed (34) hide show
  1. package/README.md +2 -2
  2. package/dist/safe-bash/browser.js +123 -21
  3. package/dist/safe-bash/browser.js.map +4 -4
  4. package/dist/safe-bash/commands/network/private-address.d.ts.map +1 -1
  5. package/dist/safe-bash/commands/network/private-address.js +10 -3
  6. package/dist/safe-bash/commands/network/private-address.js.map +1 -1
  7. package/dist/safe-bash/commands/text-programs/awk-runtime.d.ts.map +1 -1
  8. package/dist/safe-bash/commands/text-programs/awk-runtime.js +9 -2
  9. package/dist/safe-bash/commands/text-programs/awk-runtime.js.map +1 -1
  10. package/dist/safe-bash/commands/text-programs/sed.d.ts.map +1 -1
  11. package/dist/safe-bash/commands/text-programs/sed.js +7 -2
  12. package/dist/safe-bash/commands/text-programs/sed.js.map +1 -1
  13. package/dist/safe-bash/commands/text-programs/shared.d.ts +1 -0
  14. package/dist/safe-bash/commands/text-programs/shared.d.ts.map +1 -1
  15. package/dist/safe-bash/commands/text-programs/shared.js.map +1 -1
  16. package/dist/safe-bash/shell/cleanup.d.ts +1 -0
  17. package/dist/safe-bash/shell/cleanup.d.ts.map +1 -1
  18. package/dist/safe-bash/shell/cleanup.js +13 -0
  19. package/dist/safe-bash/shell/cleanup.js.map +1 -1
  20. package/dist/safe-bash/shell/path-lookup.d.ts +11 -0
  21. package/dist/safe-bash/shell/path-lookup.d.ts.map +1 -0
  22. package/dist/safe-bash/shell/path-lookup.js +91 -0
  23. package/dist/safe-bash/shell/path-lookup.js.map +1 -0
  24. package/dist/safe-bash/shell/runtime.d.ts +2 -0
  25. package/dist/safe-bash/shell/runtime.d.ts.map +1 -1
  26. package/dist/safe-bash/shell/runtime.js +18 -20
  27. package/dist/safe-bash/shell/runtime.js.map +1 -1
  28. package/dist/safe-bash/shell/types.d.ts +1 -0
  29. package/dist/safe-bash/shell/types.d.ts.map +1 -1
  30. package/dist/safe-bash/shell/types.js.map +1 -1
  31. package/dist/safe-bash/shell/worker-limits.d.ts.map +1 -1
  32. package/dist/safe-bash/shell/worker-limits.js +1 -0
  33. package/dist/safe-bash/shell/worker-limits.js.map +1 -1
  34. package/package.json +2 -2
package/README.md CHANGED
@@ -270,7 +270,7 @@ For SafeJS host integration, `makeSafeJsShellModule` exposes shell execution and
270
270
  | --- | --- |
271
271
  | `fs` | Required filesystem; no implicit host access. |
272
272
  | `cwd` | Initial virtual directory; defaults to `/`. |
273
- | `env` | Initial exported variables; defaults to an empty map, with `PWD` set from `cwd`. No host environment inheritance. Never pass host `process.env` or any secret-bearing object: everything in `env` is readable by executed scripts (`env`, `printenv`, `$VAR`), and on Cloudflare Workers with `nodejs_compat` `process.env` contains the Worker's secret bindings. The shell logs a warning when it detects this. |
273
+ | `env` | Initial exported variables; defaults to an empty map, with `PWD` set from `cwd`. No host environment inheritance. Never pass host `process.env` or any secret-bearing object: everything in `env` is readable by executed scripts (`env`, `printenv`, `$VAR`), and on Cloudflare Workers with `nodejs_compat` `process.env` contains the Worker's secret bindings. The shell warns only for the identical host `process.env` object, not copies, and does not filter values. |
274
274
  | `commands` | Existing `CommandRegistry`; defaults to an empty registry. |
275
275
  | `limits` | Resource limits listed below. |
276
276
 
@@ -289,7 +289,7 @@ provided. Pass an `AbortSignal` as `signal` to cancel. [Option types](src/shell/
289
289
  | `maxExpansionFields` | 10,000 |
290
290
  | `maxExpansionBytes` | 16 MiB |
291
291
  | `maxWallClockMs` | 30 seconds |
292
- | `maxCpuMs` | 30 seconds, checked at command and cooperative-yield checkpoints |
292
+ | `maxCpuMs` | 30 seconds elapsed including waits; checkpoint-enforced, not CPU accounting or preemptive enforcement. |
293
293
  | `pipeHighWaterMark` | 64 KiB |
294
294
 
295
295
  Always call `dispose()` when finished. Shell failures normally produce an exit
@@ -9195,6 +9195,7 @@ var InvocationScope = class _InvocationScope {
9195
9195
  parent;
9196
9196
  #children = /* @__PURE__ */ new Set();
9197
9197
  #callbacks = [];
9198
+ #finalizers = [];
9198
9199
  #work = /* @__PURE__ */ new Set();
9199
9200
  #controller = new AbortController();
9200
9201
  #closed = false;
@@ -9202,6 +9203,10 @@ var InvocationScope = class _InvocationScope {
9202
9203
  get signal() {
9203
9204
  return this.#controller.signal;
9204
9205
  }
9206
+ registerFinalizer(finalize) {
9207
+ this.assertOpen();
9208
+ this.#finalizers.push(finalize);
9209
+ }
9205
9210
  assertOpen() {
9206
9211
  this.callerSignal?.throwIfAborted();
9207
9212
  if (this.#closed) throw this.signal.reason;
@@ -9256,6 +9261,13 @@ var InvocationScope = class _InvocationScope {
9256
9261
  ...this.#work
9257
9262
  ]);
9258
9263
  } finally {
9264
+ for (const finalize of this.#finalizers.splice(0)) {
9265
+ try {
9266
+ finalize();
9267
+ } catch (error) {
9268
+ this.failures.push(error);
9269
+ }
9270
+ }
9259
9271
  if (this.parent) this.parent.#children.delete(this);
9260
9272
  }
9261
9273
  });
@@ -11605,6 +11617,89 @@ async function matchEre(program, subject, ledger, signal) {
11605
11617
  return Object.freeze({ matched: false, captures: Object.freeze([]), values: Object.freeze([]) });
11606
11618
  }
11607
11619
 
11620
+ // packages/safe-bash/src/shell/path-lookup.ts
11621
+ init_platform();
11622
+ var maxCacheEntries = 256;
11623
+ var maxCacheBytes = 64 * 1024;
11624
+ var PathLookup = class {
11625
+ #entries = /* @__PURE__ */ new Map();
11626
+ #bytes = 0;
11627
+ #pending = 0;
11628
+ #generation = 0;
11629
+ suspend() {
11630
+ this.#entries.clear();
11631
+ this.#bytes = 0;
11632
+ this.#generation++;
11633
+ this.#pending++;
11634
+ let closed = false;
11635
+ return () => {
11636
+ if (closed) return;
11637
+ closed = true;
11638
+ this.#pending--;
11639
+ this.#generation++;
11640
+ };
11641
+ }
11642
+ suspendUntilClosed(scope) {
11643
+ scope.assertOpen();
11644
+ const resume = this.suspend();
11645
+ scope.registerFinalizer(resume);
11646
+ }
11647
+ async isFile(fs, path, signal) {
11648
+ signal.throwIfAborted();
11649
+ const cached = this.#entries.get(path);
11650
+ if (cached) return cached.file;
11651
+ const generation = this.#generation;
11652
+ let file;
11653
+ try {
11654
+ file = (await fs.stat(path, { signal })).type === "file";
11655
+ } catch (error) {
11656
+ signal.throwIfAborted();
11657
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
11658
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
11659
+ file = false;
11660
+ }
11661
+ signal.throwIfAborted();
11662
+ const bytes2 = import_buffer.Buffer.byteLength(path) + 64;
11663
+ if (!this.#pending && generation === this.#generation && bytes2 <= maxCacheBytes && !this.#entries.has(path)) {
11664
+ while (this.#entries.size >= maxCacheEntries || this.#bytes + bytes2 > maxCacheBytes) {
11665
+ const oldest = this.#entries.entries().next().value;
11666
+ this.#bytes -= oldest[1].bytes;
11667
+ this.#entries.delete(oldest[0]);
11668
+ }
11669
+ this.#entries.set(path, { file, bytes: bytes2 });
11670
+ this.#bytes += bytes2;
11671
+ }
11672
+ return file;
11673
+ }
11674
+ };
11675
+ function* pathTargets(name2, path, limits, signal, fail2) {
11676
+ signal.throwIfAborted();
11677
+ if (path !== void 0 && import_buffer.Buffer.byteLength(path) > limits.maxExpansionBytes) fail2("maxExpansionBytes");
11678
+ if (name2.includes("/") || path === void 0) {
11679
+ if (limits.maxExpansionFields < 1) fail2("maxExpansionFields");
11680
+ yield name2;
11681
+ return;
11682
+ }
11683
+ let fields = 1;
11684
+ for (let separator = path.indexOf(":"); separator >= 0; separator = path.indexOf(":", separator + 1)) {
11685
+ signal.throwIfAborted();
11686
+ if (++fields > limits.maxExpansionFields) fail2("maxExpansionFields");
11687
+ }
11688
+ if (fields > limits.maxExpansionFields) fail2("maxExpansionFields");
11689
+ let start = 0;
11690
+ let consulted = 0;
11691
+ while (start <= path.length) {
11692
+ signal.throwIfAborted();
11693
+ if (consulted >= limits.maxPathComponents) fail2("maxPathComponents");
11694
+ consulted++;
11695
+ const end = path.indexOf(":", start);
11696
+ const component = path.slice(start, end < 0 ? path.length : end);
11697
+ yield `${component || "."}${component.endsWith("/") ? "" : "/"}${name2}`;
11698
+ if (end < 0) return;
11699
+ start = end + 1;
11700
+ }
11701
+ }
11702
+
11608
11703
  // packages/safe-bash/src/shell/runtime.ts
11609
11704
  var defaultLimits = {
11610
11705
  maxParseUnits: defaultMaxParseUnits,
@@ -11612,6 +11707,7 @@ var defaultLimits = {
11612
11707
  maxOutputBytes: 16 * 1024 * 1024,
11613
11708
  maxCommands: 1e4,
11614
11709
  maxFileSystemOperations: 1e5,
11710
+ maxPathComponents: 64,
11615
11711
  maxRedirects: 64,
11616
11712
  maxPipelineStages: 64,
11617
11713
  maxLoopIterations: 1e4,
@@ -11705,6 +11801,7 @@ var Budget = class {
11705
11801
  }
11706
11802
  limits;
11707
11803
  executionScope = Object.freeze({});
11804
+ pathLookup = new PathLookup();
11708
11805
  parsing;
11709
11806
  values;
11710
11807
  commands = 0;
@@ -13914,9 +14011,11 @@ var Runtime = class _Runtime {
13914
14011
  });
13915
14012
  } };
13916
14013
  };
14014
+ const resumePathCache = this.budget.pathLookup.suspend();
13917
14015
  const release = () => {
13918
14016
  if (closed) return;
13919
14017
  closed = true;
14018
+ resumePathCache();
13920
14019
  if (--file.references === 0 && this.outputFiles.get(path) === file) this.outputFiles.delete(path);
13921
14020
  };
13922
14021
  let target2;
@@ -14191,6 +14290,7 @@ var Runtime = class _Runtime {
14191
14290
  if (argumentValues.values.every((value2) => typeof value2 === "string")) Reflect.deleteProperty(context, "argumentValues");
14192
14291
  const middleware = this.middleware.map((handler) => (context2, next) => {
14193
14292
  scope.assertOpen();
14293
+ this.budget.pathLookup.suspendUntilClosed(scope);
14194
14294
  let downstream;
14195
14295
  const raw = handler(context2, () => {
14196
14296
  downstream = next();
@@ -14384,6 +14484,7 @@ var Runtime = class _Runtime {
14384
14484
  await this.diagnostic({ ...io, ...context2 }, `${context2.command}: command not found`);
14385
14485
  return { exitCode: 127 };
14386
14486
  }
14487
+ this.budget.pathLookup.suspendUntilClosed(scope);
14387
14488
  const raw = definition.execute(forwarded);
14388
14489
  const observed = this.observeRuntimeReturn(raw, runtimeFrame);
14389
14490
  return await interruptible(observed, this.signal);
@@ -14537,25 +14638,20 @@ ${functionDisplay(name2, state.functions.get(name2))}`;
14537
14638
  }
14538
14639
  async searchPaths(name2, state, all = false, discovery = false) {
14539
14640
  if (!name2) return [];
14540
- const path = state.variables.PATH;
14541
- if (path !== void 0 && import_buffer.Buffer.byteLength(path) > this.budget.limits.maxExpansionBytes) this.budget.fail("maxExpansionBytes");
14542
- const components = name2.includes("/") || path === void 0 ? [void 0] : path.split(":");
14543
- if (components.length > this.budget.limits.maxExpansionFields) this.budget.fail("maxExpansionFields");
14544
14641
  let denied;
14545
14642
  const matches = [];
14546
- for (const component of components) {
14547
- this.signal.throwIfAborted();
14548
- const target = component === void 0 ? name2 : `${component || "."}${component?.endsWith("/") ? "" : "/"}${name2}`;
14643
+ for (const target of pathTargets(name2, state.variables.PATH, this.budget.limits, this.signal, (limit) => this.budget.fail(limit))) {
14549
14644
  const resolved = resolvePath(state.cwd, target);
14550
14645
  try {
14551
14646
  const options3 = { signal: this.signal };
14552
- if ((await interruptible(this.fs.stat(resolved, options3), this.signal)).type !== "file") continue;
14647
+ if (!await interruptible(this.budget.pathLookup.isFile(this.fs, resolved, this.signal), this.signal)) continue;
14553
14648
  if (this.fs.capabilities.permissions !== true) throw new CommandFailure(`${target}: execution permissions are not supported by this filesystem`, 126);
14554
14649
  await interruptible(this.fs.access(resolved, ACCESS_MODES.X_OK, options3), this.signal);
14555
14650
  matches.push(target);
14556
14651
  if (!all) return matches;
14557
14652
  } catch (error) {
14558
14653
  this.signal.throwIfAborted();
14654
+ if (error instanceof ShellLimitError) throw error;
14559
14655
  if (error instanceof CommandFailure) {
14560
14656
  if (discovery) continue;
14561
14657
  throw error;
@@ -14805,6 +14901,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14805
14901
  const runtimeFrame = {};
14806
14902
  const middleware = this.middleware.map((handler) => (context2, next) => {
14807
14903
  scope.assertOpen();
14904
+ this.budget.pathLookup.suspendUntilClosed(scope);
14808
14905
  let downstream;
14809
14906
  const raw = handler(context2, () => {
14810
14907
  downstream = next();
@@ -14887,7 +14984,10 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14887
14984
  return { exitCode: await runtime.interpreter(forwarded, state2, childIO, loadedSource) };
14888
14985
  }
14889
14986
  if (direct) return { exitCode: await runtime.scriptFile(forwarded, state2, childIO, command2, forwarded.args, true) };
14890
- if (definition) return definition.execute(forwarded);
14987
+ if (definition) {
14988
+ runtime.budget.pathLookup.suspendUntilClosed(childIO[invocationScope]);
14989
+ return definition.execute(forwarded);
14990
+ }
14891
14991
  await writeDiagnostic(forwarded.stderr, `env: ${command2}: command not found
14892
14992
  `);
14893
14993
  return { exitCode: 127 };
@@ -14907,6 +15007,7 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
14907
15007
  args: argumentValues.args,
14908
15008
  argumentValues
14909
15009
  }, state, io, async (runtime, forwarded) => {
15010
+ runtime.budget.pathLookup.suspendUntilClosed(io[invocationScope]);
14910
15011
  let failed = false;
14911
15012
  let failure;
14912
15013
  let failureReport;
@@ -15079,16 +15180,11 @@ ${prefix2} line ${offset + line}: \`${source.split("\n")[line - 1] ?? ""}'
15079
15180
  try {
15080
15181
  const options3 = { signal: this.signal };
15081
15182
  if (filename && !filename.includes("/") && state.variables.PATH) {
15082
- if (import_buffer.Buffer.byteLength(state.variables.PATH) > this.budget.limits.maxExpansionBytes) this.budget.fail("maxExpansionBytes");
15083
- const components = state.variables.PATH.split(":");
15084
- if (components.length > this.budget.limits.maxExpansionFields) this.budget.fail("maxExpansionFields");
15085
15183
  let found = false;
15086
- for (const component of components) {
15087
- this.signal.throwIfAborted();
15088
- const candidate = `${component || "."}${component.endsWith("/") ? "" : "/"}${filename}`;
15184
+ for (const candidate of pathTargets(filename, state.variables.PATH, this.budget.limits, this.signal, (limit) => this.budget.fail(limit))) {
15089
15185
  const path2 = resolvePath(state.cwd, candidate);
15090
15186
  try {
15091
- if ((await interruptible(this.fs.stat(path2, options3), this.signal)).type !== "file") continue;
15187
+ if (!await interruptible(this.budget.pathLookup.isFile(this.fs, path2, this.signal), this.signal)) continue;
15092
15188
  await interruptible(this.fs.access(path2, ACCESS_MODES.R_OK, options3), this.signal);
15093
15189
  target = candidate;
15094
15190
  found = true;
@@ -17697,6 +17793,7 @@ var cloudflareWorkerLimits = Object.freeze({
17697
17793
  maxOutputBytes: 4 * 1024 * 1024,
17698
17794
  maxCommands: 1e3,
17699
17795
  maxFileSystemOperations: 1e4,
17796
+ maxPathComponents: 64,
17700
17797
  maxRedirects: 64,
17701
17798
  maxPipelineStages: 64,
17702
17799
  maxLoopIterations: 1e3,
@@ -17844,7 +17941,7 @@ init_platform();
17844
17941
  // packages/safe-bash/src/commands/network/private-address.ts
17845
17942
  init_platform();
17846
17943
  function privateIPv4(first, second) {
17847
- return first === 10 || first === 127 || first === 0 || first === 169 && second === 254 || first === 192 && second === 168 || first === 172 && second >= 16 && second <= 31;
17944
+ return first === 10 || first === 127 || first === 0 || first === 169 && second === 254 || first === 192 && second === 168 || first === 172 && second >= 16 && second <= 31 || first === 100 && second >= 64 && second <= 127 || first === 198 && second >= 18 && second <= 19 || first >= 224;
17848
17945
  }
17849
17946
  function privateHostname(input3) {
17850
17947
  const hostname = input3.toLowerCase().replace(/\.$/u, "").replace(/^\[|\]$/gu, "");
@@ -17860,10 +17957,12 @@ function privateHostname(input3) {
17860
17957
  const mappedIPv4 = hextets.slice(0, 5).every((value2) => value2 === 0) && hextets[5] === 65535;
17861
17958
  const translatedIPv4 = hextets.slice(0, 4).every((value2) => value2 === 0) && hextets[4] === 65535 && hextets[5] === 0;
17862
17959
  const nat64IPv4 = hextets[0] === 100 && hextets[1] === 65435 && hextets.slice(2, 6).every((value2) => value2 === 0);
17863
- if (mappedIPv4 || translatedIPv4 || nat64IPv4) {
17960
+ const compatibleIPv4 = hextets.slice(0, 6).every((value2) => value2 === 0);
17961
+ if (mappedIPv4 || translatedIPv4 || nat64IPv4 || compatibleIPv4) {
17864
17962
  return privateIPv4(hextets[6] >>> 8, hextets[6] & 255);
17865
17963
  }
17866
- return (hextets[0] & 65024) === 64512 || (hextets[0] & 65472) === 65152;
17964
+ if (hextets[0] === 8194) return privateIPv4(hextets[1] >>> 8, hextets[1] & 255);
17965
+ return (hextets[0] & 65024) === 64512 || (hextets[0] & 65472) === 65152 || (hextets[0] & 65472) === 65216 || (hextets[0] & 65280) === 65280 || hextets[0] === 100 && hextets[1] === 65435 && hextets[2] === 1;
17867
17966
  }
17868
17967
  const parts2 = hostname.split(".");
17869
17968
  if (parts2.length !== 4 || parts2.some((part) => !part || Array.from(part).some((character) => character < "0" || character > "9"))) return false;
@@ -19047,7 +19146,7 @@ async function substitute(text, pattern, replacement, budget, global, occurrence
19047
19146
  }
19048
19147
 
19049
19148
  // packages/safe-bash/src/commands/text-programs/sed.ts
19050
- function parse(source, extended, separator) {
19149
+ function parse(source, extended, separator, maxProgramInstructions) {
19051
19150
  if (source.length > 1024 * 1024) throw new ProgramError("sed program exceeds 1 MiB");
19052
19151
  const result = [];
19053
19152
  const groups = [];
@@ -19143,6 +19242,7 @@ function parse(source, extended, separator) {
19143
19242
  continue;
19144
19243
  }
19145
19244
  if (offset === source.length) break;
19245
+ if (result.length >= maxProgramInstructions) throw new ProgramError("program instruction limit exceeded");
19146
19246
  const first = address();
19147
19247
  horizontal();
19148
19248
  let second;
@@ -19532,6 +19632,8 @@ async function execute(program, context, files, quiet, budget, separator, output
19532
19632
  }
19533
19633
  function sedCommand(options3 = {}) {
19534
19634
  const definition = command("sed", async (context) => {
19635
+ const maxProgramInstructions = options3.maxProgramInstructions === void 0 ? 1024 : options3.maxProgramInstructions;
19636
+ if (!Number.isSafeInteger(maxProgramInstructions) || maxProgramInstructions < 1) throw new ProgramError("maxProgramInstructions must be a positive safe integer");
19535
19637
  const budget = new Budget3(context, options3);
19536
19638
  const sources = [];
19537
19639
  const files = [];
@@ -19580,7 +19682,7 @@ function sedCommand(options3 = {}) {
19580
19682
  sources.push(byteString(files.shift()));
19581
19683
  }
19582
19684
  if (sources[0]?.startsWith("#n")) quiet = true;
19583
- const program = parse(sources.join("\n"), extended, separator);
19685
+ const program = parse(sources.join("\n"), extended, separator, maxProgramInstructions);
19584
19686
  const outputFiles = program.flatMap((instruction) => instruction.kind !== "r" && instruction.file !== void 0 ? [instruction.file] : []);
19585
19687
  await assertPathRequirements(context, sedRequirements, ["script-output"], outputFiles);
19586
19688
  if (inPlace !== void 0 || outputFiles.length) {