@poe-platform/safe-js 0.1.800 → 0.1.802

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/README.md CHANGED
@@ -551,7 +551,7 @@ Factories return exports to register in `modules`; calling a factory alone does
551
551
 
552
552
  `spawn.retry(definition, options, retryOptions)` and `defaultRetry` use `{ maxAttempts, backoffMs, isErrorRetryable?, isRetryable? }`. `spawn.parallel(calls, options?)` accepts definition/options tuples or spawn-handle factories; options are `check`, `maxConcurrent`, `failFast`, and `signal`. Usage helpers are `createSpawnUsageAccumulator()` and `runWithSpawnUsageAccumulator(accumulator, operation)`.
553
553
 
554
- **Config files.** `parseMcpConfig(json, directory)` accepts `servers` and the three numeric MCP limits, resolving stdio paths against `directory`. `parseFsConfig(json)` accepts `{ adapter: { type, options }, root?, cwd?, readFileMaxBytes?, hostReadMemoryLimit? }`; `resolveFsConfig(config, { registry? })` constructs the adapter for `makeFsModule`. Built-in types are `memory` and `real`; `real` requires an absolute host `options.root`. Outer `root`/`cwd` are absolute virtual paths. Custom registry descriptors supply `validateOptions` and `create`. Signals and injected functions are SDK options, not JSON fields.
554
+ **Config files.** `parseMcpConfig(json, directory)` accepts `servers` and the three numeric MCP limits, resolving stdio paths against `directory`. `parseFsConfig(json)` accepts `{ adapter: { type, options }, root?, cwd?, readFileMaxBytes?, hostReadMemoryLimit? }`; `resolveFsConfig(config, { registry? })` constructs the adapter for `makeFsModule`. Built-in types are `memory` and `real`; `real` requires an absolute host `options.root`. Outer `root`/`cwd` are absolute virtual paths. Custom registry descriptors supply `validateOptions` and `create`. `maxSymlinkFollows` optionally bounds dangling-symlink canonicalization for rooted filesystem modules; it defaults to `Infinity` and accepts the same finite values through filesystem JSON configuration. Actual symlink cycles still fail. Signals and injected functions are SDK options, not JSON fields.
555
555
 
556
556
  </details>
557
557
 
@@ -2532,7 +2532,7 @@ import {
2532
2532
  validateFileSystemConfig
2533
2533
  } from "@poe-platform/safe-fs";
2534
2534
  function validateFsConfig(value) {
2535
- const config = readConfigRecord(value, "fs config", ["adapter", "root", "cwd", "readFileMaxBytes", "hostReadMemoryLimit"]);
2535
+ const config = readConfigRecord(value, "fs config", ["adapter", "root", "cwd", "readFileMaxBytes", "hostReadMemoryLimit", "maxSymlinkFollows"]);
2536
2536
  const paths = {};
2537
2537
  for (const key of ["root", "cwd"]) {
2538
2538
  const path = config[key];
@@ -2543,7 +2543,7 @@ function validateFsConfig(value) {
2543
2543
  paths[key] = path;
2544
2544
  }
2545
2545
  const limits = {};
2546
- for (const key of ["readFileMaxBytes", "hostReadMemoryLimit"]) {
2546
+ for (const key of ["readFileMaxBytes", "hostReadMemoryLimit", "maxSymlinkFollows"]) {
2547
2547
  const value2 = config[key];
2548
2548
  if (value2 === void 0) continue;
2549
2549
  if (value2 !== Infinity && (typeof value2 !== "number" || !Number.isSafeInteger(value2) || value2 < 0))
@@ -3686,11 +3686,11 @@ function copyBoundedOAuthJson(value, message) {
3686
3686
  import http from "node:http";
3687
3687
 
3688
3688
  // packages/mcp-oauth/src/client/authorization-state.ts
3689
- import crypto from "node:crypto";
3689
+ import crypto2 from "node:crypto";
3690
3690
  function createAuthorizationState(input) {
3691
3691
  const payload = {
3692
3692
  v: 1,
3693
- n: crypto.randomBytes(16).toString("base64url"),
3693
+ n: crypto2.randomBytes(16).toString("base64url"),
3694
3694
  i: input.issuer,
3695
3695
  r: input.requireIssuer
3696
3696
  };
@@ -4198,7 +4198,7 @@ function registrationMatchesRedirect(client, requestedUri, fresh = false) {
4198
4198
  }
4199
4199
 
4200
4200
  // packages/mcp-oauth/src/client/auth-store-session-store.ts
4201
- import crypto2 from "node:crypto";
4201
+ import crypto3 from "node:crypto";
4202
4202
  import path4 from "node:path";
4203
4203
 
4204
4204
  // packages/auth-store/src/encrypted-file-store.ts
@@ -4956,7 +4956,7 @@ function snapshotPersistenceOptions(options) {
4956
4956
  }
4957
4957
  function createNamedSecretStore(key2, options, defaults, namespace) {
4958
4958
  options = snapshotOAuthPersistenceOptions(options);
4959
- const hash = crypto2.createHash("sha256").update(namespace === void 0 ? key2 : JSON.stringify([namespace, key2])).digest("hex");
4959
+ const hash = crypto3.createHash("sha256").update(namespace === void 0 ? key2 : JSON.stringify([namespace, key2])).digest("hex");
4960
4960
  const configuredFilePath = options.fileStore?.filePath;
4961
4961
  const parsedFilePath = configuredFilePath === void 0 ? null : path4.parse(configuredFilePath);
4962
4962
  const fileStore = {
@@ -5256,12 +5256,12 @@ async function fetchMcpResponse(fetchImplementation, input, init = {}) {
5256
5256
  import { URL as URL2 } from "node:url";
5257
5257
 
5258
5258
  // packages/mcp-oauth/src/client/pkce.ts
5259
- import crypto3 from "node:crypto";
5259
+ import crypto4 from "node:crypto";
5260
5260
  function generateCodeVerifier() {
5261
- return crypto3.randomBytes(32).toString("base64url");
5261
+ return crypto4.randomBytes(32).toString("base64url");
5262
5262
  }
5263
5263
  function generateCodeChallenge(verifier) {
5264
- return crypto3.createHash("sha256").update(verifier).digest("base64url");
5264
+ return crypto4.createHash("sha256").update(verifier).digest("base64url");
5265
5265
  }
5266
5266
 
5267
5267
  // packages/mcp-oauth/src/http-response.ts
@@ -5390,7 +5390,7 @@ async function requestTokens(input) {
5390
5390
  if (method === "client_secret_basic") {
5391
5391
  const encoded = new URLSearchParams({ credential: input.clientId }).toString().slice("credential=".length);
5392
5392
  const encodedSecret = new URLSearchParams({ credential: input.clientSecret }).toString().slice("credential=".length);
5393
- headers.set("Authorization", `Basic ${Buffer.from(`${encoded}:${encodedSecret}`).toString("base64")}`);
5393
+ headers.set("Authorization", `Basic ${btoa(`${encoded}:${encodedSecret}`)}`);
5394
5394
  } else {
5395
5395
  body.set("client_id", input.clientId);
5396
5396
  if (method === "client_secret_post") body.set("client_secret", input.clientSecret);
@@ -1217,7 +1217,7 @@ var RealmState = class {
1217
1217
  this.assertOpen();
1218
1218
  const closure = readGuestCallback(callback, this);
1219
1219
  this.checkCollection(this.pendingCallbacks.size + 1, this.limits.callbacks, "pending callback");
1220
- const scheduled = this.options.callbackScheduling === "after-prefix";
1220
+ const scheduled = this.options.callbackScheduling === "after-prefix" || this.queuedCallbacks.has(callback);
1221
1221
  const record = {
1222
1222
  closure,
1223
1223
  prefixComplete: false
@@ -1599,7 +1599,7 @@ var RealmState = class {
1599
1599
  await this.dispose();
1600
1600
  this.assertOpen();
1601
1601
  if (this.active !== void 0) throw new SandboxError("reentry");
1602
- const scheduled = this.options.callbackScheduling === "after-prefix";
1602
+ const scheduled = this.options.callbackScheduling === "after-prefix" || this.releaseReconciliation !== void 0 || this.pendingWork.size > 0;
1603
1603
  if (scheduled && (this.phase.getStore()?.active || [...this.pendingCallbacks].some((record) => !record.prefixComplete)))
1604
1604
  throw new SandboxError("reentry");
1605
1605
  const rejectionOwner = scheduled ? this.tracker.startOperation() : void 0;
@@ -1649,13 +1649,13 @@ var RealmState = class {
1649
1649
  } finally {
1650
1650
  this.tracker.finishOperation(rejectionOwner);
1651
1651
  if (this.active === pending) this.active = void 0;
1652
- if (scheduled) this.reconcileWhenIdle();
1652
+ if (scheduled || this.releaseReconciliation !== void 0) this.reconcileWhenIdle();
1653
1653
  }
1654
1654
  }
1655
1655
  close = () => {
1656
1656
  this.closed = true;
1657
1657
  this.controller.abort(new Error("SafeJS realm is closed."));
1658
- if (this.options.callbackScheduling === "after-prefix") {
1658
+ if (this.options.callbackScheduling === "after-prefix" || this.releaseReconciliation !== void 0 || this.pendingWork.size > 0) {
1659
1659
  const disposal = this.dispose();
1660
1660
  if (this.phase.getStore()?.active) {
1661
1661
  void disposal.catch(() => void 0);
@@ -1674,7 +1674,7 @@ var RealmState = class {
1674
1674
  this.closed = true;
1675
1675
  this.controller.abort(new Error("SafeJS realm is closed."));
1676
1676
  this.options.signal?.removeEventListener("abort", this.abort);
1677
- if (this.options.callbackScheduling === "after-prefix") {
1677
+ if (this.options.callbackScheduling === "after-prefix" || this.releaseReconciliation !== void 0 || this.pendingWork.size > 0) {
1678
1678
  this.disposal = Promise.resolve().then(async () => {
1679
1679
  while (this.pendingWork.size > 0) await Promise.allSettled([...this.pendingWork]);
1680
1680
  await this.sourceGraph?.settle();
@@ -1836,16 +1836,24 @@ import { createFsBridge } from "@poe-platform/safe-fs/core";
1836
1836
  var DEFAULT_WRITE_MAX_ATTEMPTS = 3;
1837
1837
  var DEFAULT_WRITE_RETRY_DELAY_MS = 100;
1838
1838
  var LOCKED_FILE_ERROR_CODES = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
1839
- var pendingOperations = /* @__PURE__ */ new Map();
1839
+ var adapterOperations = /* @__PURE__ */ new WeakMap();
1840
1840
  var FileSnapshotBackend = class {
1841
1841
  constructor(path, options = {}) {
1842
1842
  this.path = path;
1843
+ const adapter = options.adapter ?? hostFs;
1844
+ let pending = adapterOperations.get(adapter);
1845
+ if (pending === void 0) {
1846
+ pending = /* @__PURE__ */ new Map();
1847
+ adapterOperations.set(adapter, pending);
1848
+ }
1849
+ this.#pendingOperations = pending;
1843
1850
  this.#io = options.adapter === void 0 ? hostFs : createFsBridge(options.adapter, { codec: fsCodec });
1844
1851
  this.#writeMaxAttempts = options.writeMaxAttempts ?? DEFAULT_WRITE_MAX_ATTEMPTS;
1845
1852
  this.#writeRetryDelayMs = options.writeRetryDelayMs ?? DEFAULT_WRITE_RETRY_DELAY_MS;
1846
1853
  }
1847
1854
  path;
1848
1855
  #io;
1856
+ #pendingOperations;
1849
1857
  #writeMaxAttempts;
1850
1858
  #writeRetryDelayMs;
1851
1859
  async read() {
@@ -1863,6 +1871,7 @@ var FileSnapshotBackend = class {
1863
1871
  }
1864
1872
  async write(snapshot) {
1865
1873
  await enqueueOperation(
1874
+ this.#pendingOperations,
1866
1875
  this.path,
1867
1876
  () => writeSnapshotAtomically(this.#io, this.path, snapshot, {
1868
1877
  maxAttempts: this.#writeMaxAttempts,
@@ -1871,7 +1880,7 @@ var FileSnapshotBackend = class {
1871
1880
  );
1872
1881
  }
1873
1882
  async remove() {
1874
- await enqueueOperation(this.path, async () => {
1883
+ await enqueueOperation(this.#pendingOperations, this.path, async () => {
1875
1884
  try {
1876
1885
  await this.#io.unlink(this.path);
1877
1886
  } catch (error) {
@@ -1959,7 +1968,7 @@ async function writeSnapshotOnce(io, temporaryPath, snapshotPath, contents) {
1959
1968
  }
1960
1969
  }
1961
1970
  }
1962
- async function enqueueOperation(path, operation) {
1971
+ async function enqueueOperation(pendingOperations, path, operation) {
1963
1972
  const previous = pendingOperations.get(path) ?? Promise.resolve();
1964
1973
  const pending = previous.catch(() => void 0).then(operation);
1965
1974
  const queued = pending.catch(() => void 0);
@@ -2551,10 +2560,9 @@ function run(source, options = {}) {
2551
2560
  migration: restoredSnapshot?.migration,
2552
2561
  bindings: executionScope.snapshot().bindings,
2553
2562
  clock: options.clock,
2554
- hostCalls: hostCalls.snapshot(),
2555
- replay: hostCalls.snapshotReplay(),
2563
+ hostCalls,
2556
2564
  initialInputs: initialInputs.snapshot,
2557
- promiseReplay: promiseReplay.snapshot(),
2565
+ promiseReplay,
2558
2566
  random,
2559
2567
  sourceHash
2560
2568
  });
@@ -2581,12 +2589,11 @@ function run(source, options = {}) {
2581
2589
  migration: restoredSnapshot?.migration,
2582
2590
  bindings: interpreterSnapshot2.bindings,
2583
2591
  clock: options.clock,
2584
- hostCalls: hostCalls.snapshot(),
2592
+ hostCalls,
2585
2593
  loopIterations: interpreterSnapshot2.loopIterations,
2586
2594
  pendingAwaits: [createPendingAwaitSnapshot(yieldPoint)],
2587
- replay: hostCalls.snapshotReplay(),
2588
2595
  initialInputs: initialInputs.snapshot,
2589
- promiseReplay: promiseReplay.snapshot(),
2596
+ promiseReplay,
2590
2597
  random,
2591
2598
  randomResumeState: typeof yieldPoint.replayState === "number" ? yieldPoint.replayState : void 0,
2592
2599
  sourceHash
@@ -2626,12 +2633,11 @@ function run(source, options = {}) {
2626
2633
  migration: restoredSnapshot?.migration,
2627
2634
  bindings: interpreterSnapshot2.bindings,
2628
2635
  clock: options.clock,
2629
- hostCalls: hostCalls.snapshot(),
2636
+ hostCalls,
2630
2637
  loopIterations: interpreterSnapshot2.loopIterations,
2631
2638
  pendingAwaits: [createPendingAwaitSnapshot(yieldPoint)],
2632
- replay: hostCalls.snapshotReplay(),
2633
2639
  initialInputs: initialInputs.snapshot,
2634
- promiseReplay: promiseReplay.snapshot(),
2640
+ promiseReplay,
2635
2641
  random,
2636
2642
  randomResumeState: typeof yieldPoint.replayState === "number" ? yieldPoint.replayState : void 0,
2637
2643
  sourceHash
@@ -2648,27 +2654,17 @@ function run(source, options = {}) {
2648
2654
  await throwIfReturnedPromiseRejected(result2);
2649
2655
  await throwIfUnhandledPromiseRejected(promiseTracker);
2650
2656
  await activeSnapshotScheduler.finish();
2651
- let replay;
2652
- let replayError;
2653
- try {
2654
- replay = hostCalls.snapshotReplay();
2655
- } catch (error) {
2656
- if (!(error instanceof MissingReplayCapabilityError)) throw error;
2657
- replayError = error.message;
2658
- }
2659
2657
  const snapshot = createRunSnapshot({
2660
2658
  executionSemantics,
2661
2659
  migration: restoredSnapshot?.migration,
2662
2660
  bindings: executionScope.snapshot().bindings,
2663
2661
  clock: options.clock,
2664
- hostCalls: hostCalls.snapshot(),
2665
- replay,
2662
+ hostCalls,
2666
2663
  initialInputs: initialInputs.snapshot,
2667
- promiseReplay: promiseReplay.snapshot(),
2664
+ promiseReplay,
2668
2665
  random,
2669
2666
  sourceHash
2670
2667
  });
2671
- if (replayError !== void 0) snapshot.replayError = replayError;
2672
2668
  dumpController.finalize(snapshot);
2673
2669
  completedSnapshot = snapshot;
2674
2670
  reconcileCompiledValues(
@@ -2845,6 +2841,15 @@ function createExecutableNode(module) {
2845
2841
  };
2846
2842
  }
2847
2843
  function createRunSnapshot(input) {
2844
+ let replay;
2845
+ let replayError;
2846
+ try {
2847
+ replay = input.hostCalls.snapshotReplay();
2848
+ } catch (error) {
2849
+ if (!(error instanceof MissingReplayCapabilityError)) throw error;
2850
+ replayError = error.message;
2851
+ }
2852
+ const hostCalls = input.hostCalls.snapshot();
2848
2853
  const snapshot = {
2849
2854
  version: DUMP_FORMAT_VERSION,
2850
2855
  executionSemantics: input.executionSemantics,
@@ -2852,11 +2857,12 @@ function createRunSnapshot(input) {
2852
2857
  ...input.migration === void 0 ? {} : { migration: structuredClone(input.migration) },
2853
2858
  bindings: input.bindings,
2854
2859
  clock: input.clock?.snapshot(),
2855
- ...input.hostCalls === void 0 || input.hostCalls.length === 0 ? {} : { hostCalls: input.hostCalls },
2860
+ ...hostCalls.length === 0 ? {} : { hostCalls },
2856
2861
  ...input.loopIterations === void 0 ? {} : { loopIterations: input.loopIterations },
2857
- ...input.replay === void 0 ? {} : { replay: input.replay },
2862
+ ...replay === void 0 ? {} : { replay },
2863
+ ...replayError === void 0 ? {} : { replayError },
2858
2864
  ...input.initialInputs === void 0 ? {} : { initialInputs: structuredClone(input.initialInputs) },
2859
- ...input.promiseReplay === void 0 ? {} : { promiseReplay: input.promiseReplay },
2865
+ promiseReplay: input.promiseReplay.snapshot(),
2860
2866
  ...input.pendingAwaits === void 0 || input.pendingAwaits.length === 0 ? {} : { pendingAwaits: input.pendingAwaits },
2861
2867
  random: input.random === void 0 ? void 0 : {
2862
2868
  seed: input.random.seed,
@@ -2,7 +2,7 @@ import {
2
2
  McpError,
3
3
  connectMcpTransport,
4
4
  normalizeMcpOptions
5
- } from "./chunk-SHMRT4QX.js";
5
+ } from "./chunk-6H5VC5W5.js";
6
6
  import {
7
7
  declareHostOperation,
8
8
  runResources
@@ -34,11 +34,14 @@ import {
34
34
  // packages/safe-js/src/modules/canonical-path.ts
35
35
  var MISSING_PATH_CODES = ["ENOENT", "ENOTDIR"];
36
36
  var NOT_A_SYMLINK_CODES = [...MISSING_PATH_CODES, "EINVAL"];
37
- var MAX_SYMLINK_FOLLOWS = 32;
38
- async function resolveCanonicalPath({ realpath, readlink }, path) {
37
+ async function resolveCanonicalPath({ realpath, readlink }, path, maxSymlinkFollows = Infinity) {
38
+ if (maxSymlinkFollows !== Infinity && (!Number.isSafeInteger(maxSymlinkFollows) || maxSymlinkFollows < 0)) {
39
+ throw new RangeError("maxSymlinkFollows must be a nonnegative safe integer or Infinity");
40
+ }
39
41
  const missingSegments = [];
40
42
  let current = path;
41
43
  let follows = 0;
44
+ const visited = /* @__PURE__ */ new Set();
42
45
  while (true) {
43
46
  try {
44
47
  const canonicalCurrent = await realpath(current);
@@ -49,9 +52,10 @@ async function resolveCanonicalPath({ realpath, readlink }, path) {
49
52
  }
50
53
  const target = await readSymlinkTarget(readlink, current);
51
54
  if (target !== void 0) {
52
- if (++follows > MAX_SYMLINK_FOLLOWS) {
55
+ if (++follows > maxSymlinkFollows || visited.has(current)) {
53
56
  throw createLoopError(path);
54
57
  }
58
+ visited.add(current);
55
59
  current = isAbsolutePath(target) ? target : `${dirname(current)}${sep}${target}`;
56
60
  continue;
57
61
  }
@@ -264,9 +268,10 @@ var NULL_BYTE = "\0";
264
268
  var [ACCESS_DENIED_ERRNO, ACCESS_DENIED_MESSAGE] = accessDeniedSystemError;
265
269
  function makeFsModule(options = {}) {
266
270
  assertSupportedPlatform();
271
+ const maxSymlinkFollows = options.maxSymlinkFollows ?? Infinity;
267
272
  const hostReadMemoryLimit = options.hostReadMemoryLimit ?? Infinity;
268
273
  const readFileMaxBytes = options.readFileMaxBytes ?? Infinity;
269
- for (const limit of [hostReadMemoryLimit, readFileMaxBytes]) {
274
+ for (const limit of [hostReadMemoryLimit, readFileMaxBytes, maxSymlinkFollows]) {
270
275
  if (limit !== Infinity && (!Number.isSafeInteger(limit) || limit < 0))
271
276
  throw new TypeError("Filesystem read limits must be non-negative safe integers or Infinity.");
272
277
  }
@@ -288,7 +293,7 @@ function makeFsModule(options = {}) {
288
293
  cwd: options.root === void 0 ? options.cwd : void 0,
289
294
  signal: options.signal
290
295
  });
291
- const fs = options.root === void 0 ? implementation : makeRootedFs(implementation, options.root, options.adapter, options.cwd, options.signal);
296
+ const fs = options.root === void 0 ? implementation : makeRootedFs(implementation, options.root, options.adapter, options.cwd, options.signal, maxSymlinkFollows);
292
297
  let readFile = bindStringResult(fs, "readFile");
293
298
  if (options.adapter !== void 0) {
294
299
  const boundedRead = async (args, budget) => {
@@ -316,7 +321,7 @@ function makeFsModule(options = {}) {
316
321
  }
317
322
  });
318
323
  const portable = bridge;
319
- const boundedFs = options.root === void 0 ? portable : makeRootedFs(portable, options.root, options.adapter, options.cwd, options.signal);
324
+ const boundedFs = options.root === void 0 ? portable : makeRootedFs(portable, options.root, options.adapter, options.cwd, options.signal, maxSymlinkFollows);
320
325
  return await invoke(boundedFs, "readFile", args);
321
326
  };
322
327
  const directRead = declare("readFile", "re-issue", (...args) => boundedRead(args));
@@ -408,7 +413,7 @@ function invoke(fs, name, args) {
408
413
  return Reflect.apply(fs[name], fs, args);
409
414
  }
410
415
  var rootedFsOperations = /* @__PURE__ */ new WeakMap();
411
- function makeRootedFs(fs, root, adapter, cwd, signal) {
416
+ function makeRootedFs(fs, root, adapter, cwd, signal, maxSymlinkFollows = Infinity) {
412
417
  if (root.trim().length === 0) {
413
418
  throw new Error("fs module root must be a non-empty string.");
414
419
  }
@@ -429,7 +434,7 @@ function makeRootedFs(fs, root, adapter, cwd, signal) {
429
434
  return await invoke(
430
435
  fs,
431
436
  name,
432
- await resolvePathArguments(fs, resolvedRoot, name, args, adapter, cwd, signal)
437
+ await resolvePathArguments(fs, resolvedRoot, name, args, adapter, cwd, signal, maxSymlinkFollows)
433
438
  );
434
439
  } finally {
435
440
  release();
@@ -450,8 +455,8 @@ function assertRootCanConfineOptions(name, args) {
450
455
  throw createUnsupportedOptionError("cp", "dereference", ROOT_REFUSED_DEREFERENCE_REASON);
451
456
  }
452
457
  }
453
- async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal) {
454
- const canonicalRoot = await resolveCanonicalPath(readCanonicalPathFs(fs), resolve(root));
458
+ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal, maxSymlinkFollows = Infinity) {
459
+ const canonicalRoot = await resolveCanonicalPath(readCanonicalPathFs(fs), resolve(root), maxSymlinkFollows);
455
460
  const base = cwd ?? canonicalRoot;
456
461
  const resolved = [...args];
457
462
  if (name === "symlink") {
@@ -475,7 +480,7 @@ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal)
475
480
  await assertInsideRoot(fs, canonicalRoot, name, [checkedTarget, linkPath], adapter, signal, [
476
481
  targetPath,
477
482
  linkPath
478
- ]);
483
+ ], maxSymlinkFollows);
479
484
  return resolved;
480
485
  }
481
486
  const paths = FS_PATH_ARGUMENTS[name].map((_, index) => {
@@ -483,12 +488,12 @@ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal)
483
488
  resolved[index] = path;
484
489
  return path;
485
490
  });
486
- await assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal);
491
+ await assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal, paths, maxSymlinkFollows);
487
492
  if (name === "mkdtemp") {
488
493
  const prefix = args[0];
489
494
  const absolutePrefix = isAbsolutePath(prefix) ? prefix : `${base}${sep}${prefix}`;
490
495
  const parent = absolutePrefix.endsWith(sep) ? absolutePrefix : dirname(absolutePrefix);
491
- await assertInsideRoot(fs, canonicalRoot, name, [parent], adapter, signal, paths);
496
+ await assertInsideRoot(fs, canonicalRoot, name, [parent], adapter, signal, paths, maxSymlinkFollows);
492
497
  resolved[0] = absolutePrefix;
493
498
  }
494
499
  if (adapter !== void 0 && name === "mkdir") {
@@ -506,15 +511,15 @@ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal)
506
511
  }
507
512
  return resolved;
508
513
  }
509
- async function assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal, reportedPaths = paths) {
514
+ async function assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal, reportedPaths = paths, maxSymlinkFollows = Infinity) {
510
515
  for (const path of paths) {
511
- if (await escapesRoot(fs, canonicalRoot, path, adapter, signal)) {
516
+ if (await escapesRoot(fs, canonicalRoot, path, adapter, signal, maxSymlinkFollows)) {
512
517
  throw createAccessDeniedError(name, reportedPaths[0], reportedPaths[1]);
513
518
  }
514
519
  }
515
520
  }
516
- async function escapesRoot(fs, canonicalRoot, path, adapter, signal) {
517
- const canonicalPath = await resolveCanonicalPath(readCanonicalPathFs(fs), path);
521
+ async function escapesRoot(fs, canonicalRoot, path, adapter, signal, maxSymlinkFollows = Infinity) {
522
+ const canonicalPath = await resolveCanonicalPath(readCanonicalPathFs(fs), path, maxSymlinkFollows);
518
523
  return !await (adapter === void 0 ? containsPath(readStat(fs), canonicalRoot, canonicalPath) : containsAdapterPath(adapter, canonicalRoot, canonicalPath, signal));
519
524
  }
520
525
  async function containsAdapterPath(adapter, canonicalRoot, canonicalPath, signal) {
@@ -8,10 +8,10 @@ import {
8
8
  parseFsConfig,
9
9
  resolveFsConfig,
10
10
  splitFrontmatter
11
- } from "./chunk-XAV2RKLO.js";
11
+ } from "./chunk-4KMUWNZD.js";
12
12
  import {
13
13
  parseMcpConfig
14
- } from "./chunk-SHMRT4QX.js";
14
+ } from "./chunk-6H5VC5W5.js";
15
15
  import {
16
16
  lint,
17
17
  makeEnvModule,
@@ -291,12 +291,12 @@ async function assertHarnessFile(input) {
291
291
  }
292
292
  async function runScriptFile(filepath, parsed, options) {
293
293
  const [{ run }, { restore }, { dump, dumpCurrent }, { createLintModulesFromRuntimeRegistry }, { makeFsModule }, { makeMcpModule }] = await Promise.all([
294
- import("./run-3X6J7FPV.js"),
294
+ import("./run-M6N5KCPF.js"),
295
295
  import("./restore-GDHBT5ZD.js"),
296
296
  import("./dump-KUPBSXSL.js"),
297
297
  import("./runtime-modules-MGSXY4BN.js"),
298
298
  import("../modules/fs.js"),
299
- import("./mcp-4D3WWY7U.js")
299
+ import("./mcp-5TMFK3UE.js")
300
300
  ]);
301
301
  const loaded = loadExecutableSource(filepath, await options.readFile(filepath, "utf8"));
302
302
  const meta = {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  makeMcpModule
3
- } from "./chunk-LWHXSNF6.js";
4
- import "./chunk-SHMRT4QX.js";
3
+ } from "./chunk-IB6WAQAE.js";
4
+ import "./chunk-6H5VC5W5.js";
5
5
  import "./chunk-5RLD4TVI.js";
6
6
  import "./chunk-A3LXCGMJ.js";
7
7
  import "./chunk-JCHJGIQA.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  UnhandledRejectionError,
3
3
  run
4
- } from "./chunk-QEDOBN2G.js";
4
+ } from "./chunk-HFRBLE6Y.js";
5
5
  import "./chunk-DHGJSEFX.js";
6
6
  import "./chunk-DIG26TEI.js";
7
7
  import "./chunk-ONWFPHUI.js";
@@ -13,7 +13,7 @@ import path from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
  async function runCli(argv, options = {}) {
15
15
  if (!argv.includes("--help") && !argv.includes("-h")) {
16
- const runtime = await import("./chunks/cli-runtime-OCWFX4TM.js");
16
+ const runtime = await import("./chunks/cli-runtime-L4IABN2W.js");
17
17
  return runtime.runCli(argv, options, createUsage());
18
18
  }
19
19
  const brokenPipe = createBrokenPipeState();
@@ -32,7 +32,7 @@ async function runCli(argv, options = {}) {
32
32
  stderr.write(`${error instanceof Error ? error.message : String(error)}
33
33
  `);
34
34
  if (brokenPipe.closed) return 0;
35
- const { exitCodeForError } = await import("./chunks/cli-runtime-OCWFX4TM.js");
35
+ const { exitCodeForError } = await import("./chunks/cli-runtime-L4IABN2W.js");
36
36
  return exitCodeForError(error);
37
37
  }
38
38
  }
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  FS_OPTION_SURFACE,
18
18
  makeFsModule
19
- } from "./chunks/chunk-YAHDXC7P.js";
19
+ } from "./chunks/chunk-YCFOANSU.js";
20
20
  import {
21
21
  createRootedSourceResolver
22
22
  } from "./chunks/chunk-3NFTBEZR.js";
@@ -24,7 +24,7 @@ import {
24
24
  createRealm,
25
25
  createReplayableRandom,
26
26
  run
27
- } from "./chunks/chunk-QEDOBN2G.js";
27
+ } from "./chunks/chunk-HFRBLE6Y.js";
28
28
  import "./chunks/chunk-DHGJSEFX.js";
29
29
  import "./chunks/chunk-DIG26TEI.js";
30
30
  import "./chunks/chunk-ONWFPHUI.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunks/chunk-7THYP46K.js";
9
9
  import {
10
10
  makeMcpModule
11
- } from "./chunks/chunk-LWHXSNF6.js";
11
+ } from "./chunks/chunk-IB6WAQAE.js";
12
12
  import {
13
13
  AgentSpawnError,
14
14
  SPAWN_MODES,
@@ -24,10 +24,10 @@ import {
24
24
  runWithSpawnUsageAccumulator,
25
25
  splitFrontmatter,
26
26
  supportsSpawnMode
27
- } from "./chunks/chunk-XAV2RKLO.js";
27
+ } from "./chunks/chunk-4KMUWNZD.js";
28
28
  import {
29
29
  parseMcpConfig
30
- } from "./chunks/chunk-SHMRT4QX.js";
30
+ } from "./chunks/chunk-6H5VC5W5.js";
31
31
  import {
32
32
  makeTimeModule
33
33
  } from "./chunks/chunk-QRWRR6IA.js";
@@ -45,7 +45,7 @@ import {
45
45
  import "./chunks/chunk-X3YCE3ZH.js";
46
46
  import {
47
47
  makeFsModule
48
- } from "./chunks/chunk-YAHDXC7P.js";
48
+ } from "./chunks/chunk-YCFOANSU.js";
49
49
  import {
50
50
  createRootedSourceResolver
51
51
  } from "./chunks/chunk-3NFTBEZR.js";
@@ -54,7 +54,7 @@ import {
54
54
  createRealm,
55
55
  createReplayableRandom,
56
56
  run
57
- } from "./chunks/chunk-QEDOBN2G.js";
57
+ } from "./chunks/chunk-HFRBLE6Y.js";
58
58
  import "./chunks/chunk-DHGJSEFX.js";
59
59
  import {
60
60
  fsCodec
@@ -6,6 +6,7 @@ export type FsConfig = {
6
6
  readonly cwd?: string;
7
7
  readonly readFileMaxBytes?: number;
8
8
  readonly hostReadMemoryLimit?: number;
9
+ readonly maxSymlinkFollows?: number;
9
10
  };
10
11
  export type ResolveFsConfigOptions = {
11
12
  readonly registry?: FileSystemAdapterRegistry;
@@ -44,6 +44,8 @@ type StatOptions = {
44
44
  bigint?: false;
45
45
  };
46
46
  export type FsModuleOptions = {
47
+ /** Dangling-symlink canonicalization quota; unlimited by default. */
48
+ maxSymlinkFollows?: number;
47
49
  hostReadMemoryLimit?: number;
48
50
  readFileMaxBytes?: number;
49
51
  } & ({
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  FS_OPTION_SURFACE,
3
3
  makeFsModule
4
- } from "../chunks/chunk-YAHDXC7P.js";
4
+ } from "../chunks/chunk-YCFOANSU.js";
5
5
  import "../chunks/chunk-DIG26TEI.js";
6
6
  import "../chunks/chunk-ONWFPHUI.js";
7
7
  import "../chunks/chunk-5RLD4TVI.js";
@@ -57798,11 +57798,14 @@ function describeValue2(value) {
57798
57798
  // packages/safe-js/src/modules/canonical-path.ts
57799
57799
  var MISSING_PATH_CODES = ["ENOENT", "ENOTDIR"];
57800
57800
  var NOT_A_SYMLINK_CODES = [...MISSING_PATH_CODES, "EINVAL"];
57801
- var MAX_SYMLINK_FOLLOWS = 32;
57802
- async function resolveCanonicalPath({ realpath, readlink }, path) {
57801
+ async function resolveCanonicalPath({ realpath, readlink }, path, maxSymlinkFollows = Infinity) {
57802
+ if (maxSymlinkFollows !== Infinity && (!Number.isSafeInteger(maxSymlinkFollows) || maxSymlinkFollows < 0)) {
57803
+ throw new RangeError("maxSymlinkFollows must be a nonnegative safe integer or Infinity");
57804
+ }
57803
57805
  const missingSegments = [];
57804
57806
  let current = path;
57805
57807
  let follows = 0;
57808
+ const visited = /* @__PURE__ */ new Set();
57806
57809
  while (true) {
57807
57810
  try {
57808
57811
  const canonicalCurrent = await realpath(current);
@@ -57813,9 +57816,10 @@ async function resolveCanonicalPath({ realpath, readlink }, path) {
57813
57816
  }
57814
57817
  const target = await readSymlinkTarget(readlink, current);
57815
57818
  if (target !== void 0) {
57816
- if (++follows > MAX_SYMLINK_FOLLOWS) {
57819
+ if (++follows > maxSymlinkFollows || visited.has(current)) {
57817
57820
  throw createLoopError(path);
57818
57821
  }
57822
+ visited.add(current);
57819
57823
  current = isAbsolutePath(target) ? target : `${dirname(current)}${sep}${target}`;
57820
57824
  continue;
57821
57825
  }
@@ -58028,9 +58032,10 @@ var NULL_BYTE = "\0";
58028
58032
  var [ACCESS_DENIED_ERRNO, ACCESS_DENIED_MESSAGE] = accessDeniedSystemError;
58029
58033
  function makeFsModule(options = {}) {
58030
58034
  assertSupportedPlatform();
58035
+ const maxSymlinkFollows = options.maxSymlinkFollows ?? Infinity;
58031
58036
  const hostReadMemoryLimit = options.hostReadMemoryLimit ?? Infinity;
58032
58037
  const readFileMaxBytes = options.readFileMaxBytes ?? Infinity;
58033
- for (const limit of [hostReadMemoryLimit, readFileMaxBytes]) {
58038
+ for (const limit of [hostReadMemoryLimit, readFileMaxBytes, maxSymlinkFollows]) {
58034
58039
  if (limit !== Infinity && (!Number.isSafeInteger(limit) || limit < 0))
58035
58040
  throw new TypeError("Filesystem read limits must be non-negative safe integers or Infinity.");
58036
58041
  }
@@ -58052,7 +58057,7 @@ function makeFsModule(options = {}) {
58052
58057
  cwd: options.root === void 0 ? options.cwd : void 0,
58053
58058
  signal: options.signal
58054
58059
  });
58055
- const fs = options.root === void 0 ? implementation : makeRootedFs(implementation, options.root, options.adapter, options.cwd, options.signal);
58060
+ const fs = options.root === void 0 ? implementation : makeRootedFs(implementation, options.root, options.adapter, options.cwd, options.signal, maxSymlinkFollows);
58056
58061
  let readFile = bindStringResult(fs, "readFile");
58057
58062
  if (options.adapter !== void 0) {
58058
58063
  const boundedRead = async (args, budget) => {
@@ -58080,7 +58085,7 @@ function makeFsModule(options = {}) {
58080
58085
  }
58081
58086
  });
58082
58087
  const portable = bridge;
58083
- const boundedFs = options.root === void 0 ? portable : makeRootedFs(portable, options.root, options.adapter, options.cwd, options.signal);
58088
+ const boundedFs = options.root === void 0 ? portable : makeRootedFs(portable, options.root, options.adapter, options.cwd, options.signal, maxSymlinkFollows);
58084
58089
  return await invoke(boundedFs, "readFile", args);
58085
58090
  };
58086
58091
  const directRead = declare("readFile", "re-issue", (...args) => boundedRead(args));
@@ -58172,7 +58177,7 @@ function invoke(fs, name, args) {
58172
58177
  return Reflect.apply(fs[name], fs, args);
58173
58178
  }
58174
58179
  var rootedFsOperations = /* @__PURE__ */ new WeakMap();
58175
- function makeRootedFs(fs, root, adapter, cwd, signal) {
58180
+ function makeRootedFs(fs, root, adapter, cwd, signal, maxSymlinkFollows = Infinity) {
58176
58181
  if (root.trim().length === 0) {
58177
58182
  throw new Error("fs module root must be a non-empty string.");
58178
58183
  }
@@ -58193,7 +58198,7 @@ function makeRootedFs(fs, root, adapter, cwd, signal) {
58193
58198
  return await invoke(
58194
58199
  fs,
58195
58200
  name,
58196
- await resolvePathArguments(fs, resolvedRoot, name, args, adapter, cwd, signal)
58201
+ await resolvePathArguments(fs, resolvedRoot, name, args, adapter, cwd, signal, maxSymlinkFollows)
58197
58202
  );
58198
58203
  } finally {
58199
58204
  release();
@@ -58214,8 +58219,8 @@ function assertRootCanConfineOptions(name, args) {
58214
58219
  throw createUnsupportedOptionError("cp", "dereference", ROOT_REFUSED_DEREFERENCE_REASON);
58215
58220
  }
58216
58221
  }
58217
- async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal) {
58218
- const canonicalRoot = await resolveCanonicalPath(readCanonicalPathFs(fs), resolve(root));
58222
+ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal, maxSymlinkFollows = Infinity) {
58223
+ const canonicalRoot = await resolveCanonicalPath(readCanonicalPathFs(fs), resolve(root), maxSymlinkFollows);
58219
58224
  const base = cwd ?? canonicalRoot;
58220
58225
  const resolved = [...args];
58221
58226
  if (name === "symlink") {
@@ -58239,7 +58244,7 @@ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal)
58239
58244
  await assertInsideRoot(fs, canonicalRoot, name, [checkedTarget, linkPath], adapter, signal, [
58240
58245
  targetPath,
58241
58246
  linkPath
58242
- ]);
58247
+ ], maxSymlinkFollows);
58243
58248
  return resolved;
58244
58249
  }
58245
58250
  const paths = FS_PATH_ARGUMENTS[name].map((_, index) => {
@@ -58247,12 +58252,12 @@ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal)
58247
58252
  resolved[index] = path;
58248
58253
  return path;
58249
58254
  });
58250
- await assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal);
58255
+ await assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal, paths, maxSymlinkFollows);
58251
58256
  if (name === "mkdtemp") {
58252
58257
  const prefix = args[0];
58253
58258
  const absolutePrefix = isAbsolutePath(prefix) ? prefix : `${base}${sep}${prefix}`;
58254
58259
  const parent = absolutePrefix.endsWith(sep) ? absolutePrefix : dirname(absolutePrefix);
58255
- await assertInsideRoot(fs, canonicalRoot, name, [parent], adapter, signal, paths);
58260
+ await assertInsideRoot(fs, canonicalRoot, name, [parent], adapter, signal, paths, maxSymlinkFollows);
58256
58261
  resolved[0] = absolutePrefix;
58257
58262
  }
58258
58263
  if (adapter !== void 0 && name === "mkdir") {
@@ -58270,15 +58275,15 @@ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal)
58270
58275
  }
58271
58276
  return resolved;
58272
58277
  }
58273
- async function assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal, reportedPaths = paths) {
58278
+ async function assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal, reportedPaths = paths, maxSymlinkFollows = Infinity) {
58274
58279
  for (const path of paths) {
58275
- if (await escapesRoot(fs, canonicalRoot, path, adapter, signal)) {
58280
+ if (await escapesRoot(fs, canonicalRoot, path, adapter, signal, maxSymlinkFollows)) {
58276
58281
  throw createAccessDeniedError(name, reportedPaths[0], reportedPaths[1]);
58277
58282
  }
58278
58283
  }
58279
58284
  }
58280
- async function escapesRoot(fs, canonicalRoot, path, adapter, signal) {
58281
- const canonicalPath = await resolveCanonicalPath(readCanonicalPathFs(fs), path);
58285
+ async function escapesRoot(fs, canonicalRoot, path, adapter, signal, maxSymlinkFollows = Infinity) {
58286
+ const canonicalPath = await resolveCanonicalPath(readCanonicalPathFs(fs), path, maxSymlinkFollows);
58282
58287
  return !await (adapter === void 0 ? containsPath(readStat(fs), canonicalRoot, canonicalPath) : containsAdapterPath(adapter, canonicalRoot, canonicalPath, signal));
58283
58288
  }
58284
58289
  async function containsAdapterPath(adapter, canonicalRoot, canonicalPath, signal) {
@@ -119,7 +119,7 @@ import {
119
119
  wrapCallerInjectedBindings,
120
120
  wrapCancelableBindings,
121
121
  wrapHostConstructor
122
- } from "./chunks/chunk-KYJVK33V.js";
122
+ } from "./chunks/chunk-2FNMGWDK.js";
123
123
  import {
124
124
  createRootedSourceResolver
125
125
  } from "./chunks/chunk-XCTU25SU.js";
@@ -12489,7 +12489,7 @@ var RealmState = class {
12489
12489
  this.assertOpen();
12490
12490
  const closure = readGuestCallback(callback, this);
12491
12491
  this.checkCollection(this.pendingCallbacks.size + 1, this.limits.callbacks, "pending callback");
12492
- const scheduled = this.options.callbackScheduling === "after-prefix";
12492
+ const scheduled = this.options.callbackScheduling === "after-prefix" || this.queuedCallbacks.has(callback);
12493
12493
  const record = {
12494
12494
  closure,
12495
12495
  prefixComplete: false
@@ -12871,7 +12871,7 @@ var RealmState = class {
12871
12871
  await this.dispose();
12872
12872
  this.assertOpen();
12873
12873
  if (this.active !== void 0) throw new SandboxError("reentry");
12874
- const scheduled = this.options.callbackScheduling === "after-prefix";
12874
+ const scheduled = this.options.callbackScheduling === "after-prefix" || this.releaseReconciliation !== void 0 || this.pendingWork.size > 0;
12875
12875
  if (scheduled && (this.phase.getStore()?.active || [...this.pendingCallbacks].some((record) => !record.prefixComplete)))
12876
12876
  throw new SandboxError("reentry");
12877
12877
  const rejectionOwner = scheduled ? this.tracker.startOperation() : void 0;
@@ -12921,13 +12921,13 @@ var RealmState = class {
12921
12921
  } finally {
12922
12922
  this.tracker.finishOperation(rejectionOwner);
12923
12923
  if (this.active === pending) this.active = void 0;
12924
- if (scheduled) this.reconcileWhenIdle();
12924
+ if (scheduled || this.releaseReconciliation !== void 0) this.reconcileWhenIdle();
12925
12925
  }
12926
12926
  }
12927
12927
  close = () => {
12928
12928
  this.closed = true;
12929
12929
  this.controller.abort(new Error("SafeJS realm is closed."));
12930
- if (this.options.callbackScheduling === "after-prefix") {
12930
+ if (this.options.callbackScheduling === "after-prefix" || this.releaseReconciliation !== void 0 || this.pendingWork.size > 0) {
12931
12931
  const disposal = this.dispose();
12932
12932
  if (this.phase.getStore()?.active) {
12933
12933
  void disposal.catch(() => void 0);
@@ -12946,7 +12946,7 @@ var RealmState = class {
12946
12946
  this.closed = true;
12947
12947
  this.controller.abort(new Error("SafeJS realm is closed."));
12948
12948
  this.options.signal?.removeEventListener("abort", this.abort);
12949
- if (this.options.callbackScheduling === "after-prefix") {
12949
+ if (this.options.callbackScheduling === "after-prefix" || this.releaseReconciliation !== void 0 || this.pendingWork.size > 0) {
12950
12950
  this.disposal = Promise.resolve().then(async () => {
12951
12951
  while (this.pendingWork.size > 0) await Promise.allSettled([...this.pendingWork]);
12952
12952
  await this.sourceGraph?.settle();
@@ -13393,16 +13393,24 @@ import { createFsBridge } from "@poe-platform/safe-fs/core";
13393
13393
  var DEFAULT_WRITE_MAX_ATTEMPTS = 3;
13394
13394
  var DEFAULT_WRITE_RETRY_DELAY_MS = 100;
13395
13395
  var LOCKED_FILE_ERROR_CODES = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
13396
- var pendingOperations = /* @__PURE__ */ new Map();
13396
+ var adapterOperations = /* @__PURE__ */ new WeakMap();
13397
13397
  var FileSnapshotBackend = class {
13398
13398
  constructor(path, options = {}) {
13399
13399
  this.path = path;
13400
+ const adapter = options.adapter ?? hostFs;
13401
+ let pending = adapterOperations.get(adapter);
13402
+ if (pending === void 0) {
13403
+ pending = /* @__PURE__ */ new Map();
13404
+ adapterOperations.set(adapter, pending);
13405
+ }
13406
+ this.#pendingOperations = pending;
13400
13407
  this.#io = options.adapter === void 0 ? hostFs : createFsBridge(options.adapter, { codec: fsCodec });
13401
13408
  this.#writeMaxAttempts = options.writeMaxAttempts ?? DEFAULT_WRITE_MAX_ATTEMPTS;
13402
13409
  this.#writeRetryDelayMs = options.writeRetryDelayMs ?? DEFAULT_WRITE_RETRY_DELAY_MS;
13403
13410
  }
13404
13411
  path;
13405
13412
  #io;
13413
+ #pendingOperations;
13406
13414
  #writeMaxAttempts;
13407
13415
  #writeRetryDelayMs;
13408
13416
  async read() {
@@ -13420,6 +13428,7 @@ var FileSnapshotBackend = class {
13420
13428
  }
13421
13429
  async write(snapshot) {
13422
13430
  await enqueueOperation(
13431
+ this.#pendingOperations,
13423
13432
  this.path,
13424
13433
  () => writeSnapshotAtomically(this.#io, this.path, snapshot, {
13425
13434
  maxAttempts: this.#writeMaxAttempts,
@@ -13428,7 +13437,7 @@ var FileSnapshotBackend = class {
13428
13437
  );
13429
13438
  }
13430
13439
  async remove() {
13431
- await enqueueOperation(this.path, async () => {
13440
+ await enqueueOperation(this.#pendingOperations, this.path, async () => {
13432
13441
  try {
13433
13442
  await this.#io.unlink(this.path);
13434
13443
  } catch (error) {
@@ -13516,7 +13525,7 @@ async function writeSnapshotOnce(io, temporaryPath, snapshotPath, contents) {
13516
13525
  }
13517
13526
  }
13518
13527
  }
13519
- async function enqueueOperation(path, operation) {
13528
+ async function enqueueOperation(pendingOperations, path, operation) {
13520
13529
  const previous = pendingOperations.get(path) ?? Promise.resolve();
13521
13530
  const pending = previous.catch(() => void 0).then(operation);
13522
13531
  const queued = pending.catch(() => void 0);
@@ -14108,10 +14117,9 @@ function run(source, options = {}) {
14108
14117
  migration: restoredSnapshot?.migration,
14109
14118
  bindings: executionScope.snapshot().bindings,
14110
14119
  clock: options.clock,
14111
- hostCalls: hostCalls.snapshot(),
14112
- replay: hostCalls.snapshotReplay(),
14120
+ hostCalls,
14113
14121
  initialInputs: initialInputs.snapshot,
14114
- promiseReplay: promiseReplay.snapshot(),
14122
+ promiseReplay,
14115
14123
  random,
14116
14124
  sourceHash
14117
14125
  });
@@ -14138,12 +14146,11 @@ function run(source, options = {}) {
14138
14146
  migration: restoredSnapshot?.migration,
14139
14147
  bindings: interpreterSnapshot2.bindings,
14140
14148
  clock: options.clock,
14141
- hostCalls: hostCalls.snapshot(),
14149
+ hostCalls,
14142
14150
  loopIterations: interpreterSnapshot2.loopIterations,
14143
14151
  pendingAwaits: [createPendingAwaitSnapshot(yieldPoint)],
14144
- replay: hostCalls.snapshotReplay(),
14145
14152
  initialInputs: initialInputs.snapshot,
14146
- promiseReplay: promiseReplay.snapshot(),
14153
+ promiseReplay,
14147
14154
  random,
14148
14155
  randomResumeState: typeof yieldPoint.replayState === "number" ? yieldPoint.replayState : void 0,
14149
14156
  sourceHash
@@ -14183,12 +14190,11 @@ function run(source, options = {}) {
14183
14190
  migration: restoredSnapshot?.migration,
14184
14191
  bindings: interpreterSnapshot2.bindings,
14185
14192
  clock: options.clock,
14186
- hostCalls: hostCalls.snapshot(),
14193
+ hostCalls,
14187
14194
  loopIterations: interpreterSnapshot2.loopIterations,
14188
14195
  pendingAwaits: [createPendingAwaitSnapshot(yieldPoint)],
14189
- replay: hostCalls.snapshotReplay(),
14190
14196
  initialInputs: initialInputs.snapshot,
14191
- promiseReplay: promiseReplay.snapshot(),
14197
+ promiseReplay,
14192
14198
  random,
14193
14199
  randomResumeState: typeof yieldPoint.replayState === "number" ? yieldPoint.replayState : void 0,
14194
14200
  sourceHash
@@ -14205,27 +14211,17 @@ function run(source, options = {}) {
14205
14211
  await throwIfReturnedPromiseRejected(result2);
14206
14212
  await throwIfUnhandledPromiseRejected(promiseTracker);
14207
14213
  await activeSnapshotScheduler.finish();
14208
- let replay;
14209
- let replayError;
14210
- try {
14211
- replay = hostCalls.snapshotReplay();
14212
- } catch (error) {
14213
- if (!(error instanceof MissingReplayCapabilityError)) throw error;
14214
- replayError = error.message;
14215
- }
14216
14214
  const snapshot = createRunSnapshot({
14217
14215
  executionSemantics,
14218
14216
  migration: restoredSnapshot?.migration,
14219
14217
  bindings: executionScope.snapshot().bindings,
14220
14218
  clock: options.clock,
14221
- hostCalls: hostCalls.snapshot(),
14222
- replay,
14219
+ hostCalls,
14223
14220
  initialInputs: initialInputs.snapshot,
14224
- promiseReplay: promiseReplay.snapshot(),
14221
+ promiseReplay,
14225
14222
  random,
14226
14223
  sourceHash
14227
14224
  });
14228
- if (replayError !== void 0) snapshot.replayError = replayError;
14229
14225
  dumpController.finalize(snapshot);
14230
14226
  completedSnapshot = snapshot;
14231
14227
  reconcileCompiledValues(
@@ -14402,6 +14398,15 @@ function createExecutableNode(module) {
14402
14398
  };
14403
14399
  }
14404
14400
  function createRunSnapshot(input) {
14401
+ let replay;
14402
+ let replayError;
14403
+ try {
14404
+ replay = input.hostCalls.snapshotReplay();
14405
+ } catch (error) {
14406
+ if (!(error instanceof MissingReplayCapabilityError)) throw error;
14407
+ replayError = error.message;
14408
+ }
14409
+ const hostCalls = input.hostCalls.snapshot();
14405
14410
  const snapshot = {
14406
14411
  version: DUMP_FORMAT_VERSION,
14407
14412
  executionSemantics: input.executionSemantics,
@@ -14409,11 +14414,12 @@ function createRunSnapshot(input) {
14409
14414
  ...input.migration === void 0 ? {} : { migration: structuredClone(input.migration) },
14410
14415
  bindings: input.bindings,
14411
14416
  clock: input.clock?.snapshot(),
14412
- ...input.hostCalls === void 0 || input.hostCalls.length === 0 ? {} : { hostCalls: input.hostCalls },
14417
+ ...hostCalls.length === 0 ? {} : { hostCalls },
14413
14418
  ...input.loopIterations === void 0 ? {} : { loopIterations: input.loopIterations },
14414
- ...input.replay === void 0 ? {} : { replay: input.replay },
14419
+ ...replay === void 0 ? {} : { replay },
14420
+ ...replayError === void 0 ? {} : { replayError },
14415
14421
  ...input.initialInputs === void 0 ? {} : { initialInputs: structuredClone(input.initialInputs) },
14416
- ...input.promiseReplay === void 0 ? {} : { promiseReplay: input.promiseReplay },
14422
+ promiseReplay: input.promiseReplay.snapshot(),
14417
14423
  ...input.pendingAwaits === void 0 || input.pendingAwaits.length === 0 ? {} : { pendingAwaits: input.pendingAwaits },
14418
14424
  random: input.random === void 0 ? void 0 : {
14419
14425
  seed: input.random.seed,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  FS_OPTION_SURFACE,
3
3
  makeFsModule
4
- } from "../chunks/chunk-KYJVK33V.js";
4
+ } from "../chunks/chunk-2FNMGWDK.js";
5
5
  import "../chunks/chunk-H5X3T5VP.js";
6
6
  export {
7
7
  FS_OPTION_SURFACE,
@@ -71259,7 +71259,7 @@ var RealmState = class {
71259
71259
  this.assertOpen();
71260
71260
  const closure = readGuestCallback(callback, this);
71261
71261
  this.checkCollection(this.pendingCallbacks.size + 1, this.limits.callbacks, "pending callback");
71262
- const scheduled = this.options.callbackScheduling === "after-prefix";
71262
+ const scheduled = this.options.callbackScheduling === "after-prefix" || this.queuedCallbacks.has(callback);
71263
71263
  const record5 = {
71264
71264
  closure,
71265
71265
  prefixComplete: false
@@ -71641,7 +71641,7 @@ var RealmState = class {
71641
71641
  await this.dispose();
71642
71642
  this.assertOpen();
71643
71643
  if (this.active !== void 0) throw new SandboxError("reentry");
71644
- const scheduled = this.options.callbackScheduling === "after-prefix";
71644
+ const scheduled = this.options.callbackScheduling === "after-prefix" || this.releaseReconciliation !== void 0 || this.pendingWork.size > 0;
71645
71645
  if (scheduled && (this.phase.getStore()?.active || [...this.pendingCallbacks].some((record5) => !record5.prefixComplete)))
71646
71646
  throw new SandboxError("reentry");
71647
71647
  const rejectionOwner = scheduled ? this.tracker.startOperation() : void 0;
@@ -71691,13 +71691,13 @@ var RealmState = class {
71691
71691
  } finally {
71692
71692
  this.tracker.finishOperation(rejectionOwner);
71693
71693
  if (this.active === pending) this.active = void 0;
71694
- if (scheduled) this.reconcileWhenIdle();
71694
+ if (scheduled || this.releaseReconciliation !== void 0) this.reconcileWhenIdle();
71695
71695
  }
71696
71696
  }
71697
71697
  close = () => {
71698
71698
  this.closed = true;
71699
71699
  this.controller.abort(new Error("SafeJS realm is closed."));
71700
- if (this.options.callbackScheduling === "after-prefix") {
71700
+ if (this.options.callbackScheduling === "after-prefix" || this.releaseReconciliation !== void 0 || this.pendingWork.size > 0) {
71701
71701
  const disposal = this.dispose();
71702
71702
  if (this.phase.getStore()?.active) {
71703
71703
  void disposal.catch(() => void 0);
@@ -71716,7 +71716,7 @@ var RealmState = class {
71716
71716
  this.closed = true;
71717
71717
  this.controller.abort(new Error("SafeJS realm is closed."));
71718
71718
  this.options.signal?.removeEventListener("abort", this.abort);
71719
- if (this.options.callbackScheduling === "after-prefix") {
71719
+ if (this.options.callbackScheduling === "after-prefix" || this.releaseReconciliation !== void 0 || this.pendingWork.size > 0) {
71720
71720
  this.disposal = Promise.resolve().then(async () => {
71721
71721
  while (this.pendingWork.size > 0) await Promise.allSettled([...this.pendingWork]);
71722
71722
  await this.sourceGraph?.settle();
@@ -72264,16 +72264,24 @@ function hasOwnErrorCode(error, code) {
72264
72264
  var DEFAULT_WRITE_MAX_ATTEMPTS = 3;
72265
72265
  var DEFAULT_WRITE_RETRY_DELAY_MS = 100;
72266
72266
  var LOCKED_FILE_ERROR_CODES = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
72267
- var pendingOperations = /* @__PURE__ */ new Map();
72267
+ var adapterOperations = /* @__PURE__ */ new WeakMap();
72268
72268
  var FileSnapshotBackend = class {
72269
72269
  constructor(path, options = {}) {
72270
72270
  this.path = path;
72271
+ const adapter = options.adapter ?? hostFs;
72272
+ let pending = adapterOperations.get(adapter);
72273
+ if (pending === void 0) {
72274
+ pending = /* @__PURE__ */ new Map();
72275
+ adapterOperations.set(adapter, pending);
72276
+ }
72277
+ this.#pendingOperations = pending;
72271
72278
  this.#io = options.adapter === void 0 ? hostFs : createFsBridge(options.adapter, { codec: fsCodec });
72272
72279
  this.#writeMaxAttempts = options.writeMaxAttempts ?? DEFAULT_WRITE_MAX_ATTEMPTS;
72273
72280
  this.#writeRetryDelayMs = options.writeRetryDelayMs ?? DEFAULT_WRITE_RETRY_DELAY_MS;
72274
72281
  }
72275
72282
  path;
72276
72283
  #io;
72284
+ #pendingOperations;
72277
72285
  #writeMaxAttempts;
72278
72286
  #writeRetryDelayMs;
72279
72287
  async read() {
@@ -72291,6 +72299,7 @@ var FileSnapshotBackend = class {
72291
72299
  }
72292
72300
  async write(snapshot) {
72293
72301
  await enqueueOperation(
72302
+ this.#pendingOperations,
72294
72303
  this.path,
72295
72304
  () => writeSnapshotAtomically(this.#io, this.path, snapshot, {
72296
72305
  maxAttempts: this.#writeMaxAttempts,
@@ -72299,7 +72308,7 @@ var FileSnapshotBackend = class {
72299
72308
  );
72300
72309
  }
72301
72310
  async remove() {
72302
- await enqueueOperation(this.path, async () => {
72311
+ await enqueueOperation(this.#pendingOperations, this.path, async () => {
72303
72312
  try {
72304
72313
  await this.#io.unlink(this.path);
72305
72314
  } catch (error) {
@@ -72387,7 +72396,7 @@ async function writeSnapshotOnce(io, temporaryPath, snapshotPath, contents) {
72387
72396
  }
72388
72397
  }
72389
72398
  }
72390
- async function enqueueOperation(path, operation) {
72399
+ async function enqueueOperation(pendingOperations, path, operation) {
72391
72400
  const previous = pendingOperations.get(path) ?? Promise.resolve();
72392
72401
  const pending = previous.catch(() => void 0).then(operation);
72393
72402
  const queued = pending.catch(() => void 0);
@@ -72980,10 +72989,9 @@ function run(source, options = {}) {
72980
72989
  migration: restoredSnapshot?.migration,
72981
72990
  bindings: executionScope.snapshot().bindings,
72982
72991
  clock: options.clock,
72983
- hostCalls: hostCalls.snapshot(),
72984
- replay: hostCalls.snapshotReplay(),
72992
+ hostCalls,
72985
72993
  initialInputs: initialInputs.snapshot,
72986
- promiseReplay: promiseReplay.snapshot(),
72994
+ promiseReplay,
72987
72995
  random,
72988
72996
  sourceHash
72989
72997
  });
@@ -73010,12 +73018,11 @@ function run(source, options = {}) {
73010
73018
  migration: restoredSnapshot?.migration,
73011
73019
  bindings: interpreterSnapshot2.bindings,
73012
73020
  clock: options.clock,
73013
- hostCalls: hostCalls.snapshot(),
73021
+ hostCalls,
73014
73022
  loopIterations: interpreterSnapshot2.loopIterations,
73015
73023
  pendingAwaits: [createPendingAwaitSnapshot(yieldPoint)],
73016
- replay: hostCalls.snapshotReplay(),
73017
73024
  initialInputs: initialInputs.snapshot,
73018
- promiseReplay: promiseReplay.snapshot(),
73025
+ promiseReplay,
73019
73026
  random,
73020
73027
  randomResumeState: typeof yieldPoint.replayState === "number" ? yieldPoint.replayState : void 0,
73021
73028
  sourceHash
@@ -73055,12 +73062,11 @@ function run(source, options = {}) {
73055
73062
  migration: restoredSnapshot?.migration,
73056
73063
  bindings: interpreterSnapshot2.bindings,
73057
73064
  clock: options.clock,
73058
- hostCalls: hostCalls.snapshot(),
73065
+ hostCalls,
73059
73066
  loopIterations: interpreterSnapshot2.loopIterations,
73060
73067
  pendingAwaits: [createPendingAwaitSnapshot(yieldPoint)],
73061
- replay: hostCalls.snapshotReplay(),
73062
73068
  initialInputs: initialInputs.snapshot,
73063
- promiseReplay: promiseReplay.snapshot(),
73069
+ promiseReplay,
73064
73070
  random,
73065
73071
  randomResumeState: typeof yieldPoint.replayState === "number" ? yieldPoint.replayState : void 0,
73066
73072
  sourceHash
@@ -73077,27 +73083,17 @@ function run(source, options = {}) {
73077
73083
  await throwIfReturnedPromiseRejected(result2);
73078
73084
  await throwIfUnhandledPromiseRejected(promiseTracker);
73079
73085
  await activeSnapshotScheduler.finish();
73080
- let replay;
73081
- let replayError;
73082
- try {
73083
- replay = hostCalls.snapshotReplay();
73084
- } catch (error) {
73085
- if (!(error instanceof MissingReplayCapabilityError)) throw error;
73086
- replayError = error.message;
73087
- }
73088
73086
  const snapshot = createRunSnapshot({
73089
73087
  executionSemantics,
73090
73088
  migration: restoredSnapshot?.migration,
73091
73089
  bindings: executionScope.snapshot().bindings,
73092
73090
  clock: options.clock,
73093
- hostCalls: hostCalls.snapshot(),
73094
- replay,
73091
+ hostCalls,
73095
73092
  initialInputs: initialInputs.snapshot,
73096
- promiseReplay: promiseReplay.snapshot(),
73093
+ promiseReplay,
73097
73094
  random,
73098
73095
  sourceHash
73099
73096
  });
73100
- if (replayError !== void 0) snapshot.replayError = replayError;
73101
73097
  dumpController.finalize(snapshot);
73102
73098
  completedSnapshot = snapshot;
73103
73099
  reconcileCompiledValues(
@@ -73274,6 +73270,15 @@ function createExecutableNode(module) {
73274
73270
  };
73275
73271
  }
73276
73272
  function createRunSnapshot(input) {
73273
+ let replay;
73274
+ let replayError;
73275
+ try {
73276
+ replay = input.hostCalls.snapshotReplay();
73277
+ } catch (error) {
73278
+ if (!(error instanceof MissingReplayCapabilityError)) throw error;
73279
+ replayError = error.message;
73280
+ }
73281
+ const hostCalls = input.hostCalls.snapshot();
73277
73282
  const snapshot = {
73278
73283
  version: DUMP_FORMAT_VERSION,
73279
73284
  executionSemantics: input.executionSemantics,
@@ -73281,11 +73286,12 @@ function createRunSnapshot(input) {
73281
73286
  ...input.migration === void 0 ? {} : { migration: structuredClone(input.migration) },
73282
73287
  bindings: input.bindings,
73283
73288
  clock: input.clock?.snapshot(),
73284
- ...input.hostCalls === void 0 || input.hostCalls.length === 0 ? {} : { hostCalls: input.hostCalls },
73289
+ ...hostCalls.length === 0 ? {} : { hostCalls },
73285
73290
  ...input.loopIterations === void 0 ? {} : { loopIterations: input.loopIterations },
73286
- ...input.replay === void 0 ? {} : { replay: input.replay },
73291
+ ...replay === void 0 ? {} : { replay },
73292
+ ...replayError === void 0 ? {} : { replayError },
73287
73293
  ...input.initialInputs === void 0 ? {} : { initialInputs: structuredClone(input.initialInputs) },
73288
- ...input.promiseReplay === void 0 ? {} : { promiseReplay: input.promiseReplay },
73294
+ promiseReplay: input.promiseReplay.snapshot(),
73289
73295
  ...input.pendingAwaits === void 0 || input.pendingAwaits.length === 0 ? {} : { pendingAwaits: input.pendingAwaits },
73290
73296
  random: input.random === void 0 ? void 0 : {
73291
73297
  seed: input.random.seed,
@@ -73472,11 +73478,14 @@ import {
73472
73478
  init_paths();
73473
73479
  var MISSING_PATH_CODES = ["ENOENT", "ENOTDIR"];
73474
73480
  var NOT_A_SYMLINK_CODES = [...MISSING_PATH_CODES, "EINVAL"];
73475
- var MAX_SYMLINK_FOLLOWS = 32;
73476
- async function resolveCanonicalPath({ realpath, readlink }, path) {
73481
+ async function resolveCanonicalPath({ realpath, readlink }, path, maxSymlinkFollows = Infinity) {
73482
+ if (maxSymlinkFollows !== Infinity && (!Number.isSafeInteger(maxSymlinkFollows) || maxSymlinkFollows < 0)) {
73483
+ throw new RangeError("maxSymlinkFollows must be a nonnegative safe integer or Infinity");
73484
+ }
73477
73485
  const missingSegments = [];
73478
73486
  let current = path;
73479
73487
  let follows = 0;
73488
+ const visited = /* @__PURE__ */ new Set();
73480
73489
  while (true) {
73481
73490
  try {
73482
73491
  const canonicalCurrent = await realpath(current);
@@ -73487,9 +73496,10 @@ async function resolveCanonicalPath({ realpath, readlink }, path) {
73487
73496
  }
73488
73497
  const target = await readSymlinkTarget(readlink, current);
73489
73498
  if (target !== void 0) {
73490
- if (++follows > MAX_SYMLINK_FOLLOWS) {
73499
+ if (++follows > maxSymlinkFollows || visited.has(current)) {
73491
73500
  throw createLoopError(path);
73492
73501
  }
73502
+ visited.add(current);
73493
73503
  current = isAbsolutePath(target) ? target : `${dirname(current)}${sep}${target}`;
73494
73504
  continue;
73495
73505
  }
@@ -73702,9 +73712,10 @@ var NULL_BYTE = "\0";
73702
73712
  var [ACCESS_DENIED_ERRNO, ACCESS_DENIED_MESSAGE] = accessDeniedSystemError;
73703
73713
  function makeFsModule(options = {}) {
73704
73714
  assertSupportedPlatform();
73715
+ const maxSymlinkFollows = options.maxSymlinkFollows ?? Infinity;
73705
73716
  const hostReadMemoryLimit = options.hostReadMemoryLimit ?? Infinity;
73706
73717
  const readFileMaxBytes = options.readFileMaxBytes ?? Infinity;
73707
- for (const limit of [hostReadMemoryLimit, readFileMaxBytes]) {
73718
+ for (const limit of [hostReadMemoryLimit, readFileMaxBytes, maxSymlinkFollows]) {
73708
73719
  if (limit !== Infinity && (!Number.isSafeInteger(limit) || limit < 0))
73709
73720
  throw new TypeError("Filesystem read limits must be non-negative safe integers or Infinity.");
73710
73721
  }
@@ -73726,7 +73737,7 @@ function makeFsModule(options = {}) {
73726
73737
  cwd: options.root === void 0 ? options.cwd : void 0,
73727
73738
  signal: options.signal
73728
73739
  });
73729
- const fs = options.root === void 0 ? implementation : makeRootedFs(implementation, options.root, options.adapter, options.cwd, options.signal);
73740
+ const fs = options.root === void 0 ? implementation : makeRootedFs(implementation, options.root, options.adapter, options.cwd, options.signal, maxSymlinkFollows);
73730
73741
  let readFile = bindStringResult(fs, "readFile");
73731
73742
  if (options.adapter !== void 0) {
73732
73743
  const boundedRead = async (args, budget) => {
@@ -73754,7 +73765,7 @@ function makeFsModule(options = {}) {
73754
73765
  }
73755
73766
  });
73756
73767
  const portable = bridge;
73757
- const boundedFs = options.root === void 0 ? portable : makeRootedFs(portable, options.root, options.adapter, options.cwd, options.signal);
73768
+ const boundedFs = options.root === void 0 ? portable : makeRootedFs(portable, options.root, options.adapter, options.cwd, options.signal, maxSymlinkFollows);
73758
73769
  return await invoke(boundedFs, "readFile", args);
73759
73770
  };
73760
73771
  const directRead = declare("readFile", "re-issue", (...args) => boundedRead(args));
@@ -73846,7 +73857,7 @@ function invoke(fs, name, args) {
73846
73857
  return Reflect.apply(fs[name], fs, args);
73847
73858
  }
73848
73859
  var rootedFsOperations = /* @__PURE__ */ new WeakMap();
73849
- function makeRootedFs(fs, root, adapter, cwd, signal) {
73860
+ function makeRootedFs(fs, root, adapter, cwd, signal, maxSymlinkFollows = Infinity) {
73850
73861
  if (root.trim().length === 0) {
73851
73862
  throw new Error("fs module root must be a non-empty string.");
73852
73863
  }
@@ -73867,7 +73878,7 @@ function makeRootedFs(fs, root, adapter, cwd, signal) {
73867
73878
  return await invoke(
73868
73879
  fs,
73869
73880
  name,
73870
- await resolvePathArguments(fs, resolvedRoot, name, args, adapter, cwd, signal)
73881
+ await resolvePathArguments(fs, resolvedRoot, name, args, adapter, cwd, signal, maxSymlinkFollows)
73871
73882
  );
73872
73883
  } finally {
73873
73884
  release();
@@ -73888,8 +73899,8 @@ function assertRootCanConfineOptions(name, args) {
73888
73899
  throw createUnsupportedOptionError("cp", "dereference", ROOT_REFUSED_DEREFERENCE_REASON);
73889
73900
  }
73890
73901
  }
73891
- async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal) {
73892
- const canonicalRoot = await resolveCanonicalPath(readCanonicalPathFs(fs), resolve(root));
73902
+ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal, maxSymlinkFollows = Infinity) {
73903
+ const canonicalRoot = await resolveCanonicalPath(readCanonicalPathFs(fs), resolve(root), maxSymlinkFollows);
73893
73904
  const base = cwd ?? canonicalRoot;
73894
73905
  const resolved = [...args];
73895
73906
  if (name === "symlink") {
@@ -73913,7 +73924,7 @@ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal)
73913
73924
  await assertInsideRoot(fs, canonicalRoot, name, [checkedTarget, linkPath], adapter, signal, [
73914
73925
  targetPath,
73915
73926
  linkPath
73916
- ]);
73927
+ ], maxSymlinkFollows);
73917
73928
  return resolved;
73918
73929
  }
73919
73930
  const paths = FS_PATH_ARGUMENTS[name].map((_, index) => {
@@ -73921,12 +73932,12 @@ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal)
73921
73932
  resolved[index] = path;
73922
73933
  return path;
73923
73934
  });
73924
- await assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal);
73935
+ await assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal, paths, maxSymlinkFollows);
73925
73936
  if (name === "mkdtemp") {
73926
73937
  const prefix = args[0];
73927
73938
  const absolutePrefix = isAbsolutePath(prefix) ? prefix : `${base}${sep}${prefix}`;
73928
73939
  const parent = absolutePrefix.endsWith(sep) ? absolutePrefix : dirname(absolutePrefix);
73929
- await assertInsideRoot(fs, canonicalRoot, name, [parent], adapter, signal, paths);
73940
+ await assertInsideRoot(fs, canonicalRoot, name, [parent], adapter, signal, paths, maxSymlinkFollows);
73930
73941
  resolved[0] = absolutePrefix;
73931
73942
  }
73932
73943
  if (adapter !== void 0 && name === "mkdir") {
@@ -73944,15 +73955,15 @@ async function resolvePathArguments(fs, root, name, args, adapter, cwd, signal)
73944
73955
  }
73945
73956
  return resolved;
73946
73957
  }
73947
- async function assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal, reportedPaths = paths) {
73958
+ async function assertInsideRoot(fs, canonicalRoot, name, paths, adapter, signal, reportedPaths = paths, maxSymlinkFollows = Infinity) {
73948
73959
  for (const path of paths) {
73949
- if (await escapesRoot(fs, canonicalRoot, path, adapter, signal)) {
73960
+ if (await escapesRoot(fs, canonicalRoot, path, adapter, signal, maxSymlinkFollows)) {
73950
73961
  throw createAccessDeniedError(name, reportedPaths[0], reportedPaths[1]);
73951
73962
  }
73952
73963
  }
73953
73964
  }
73954
- async function escapesRoot(fs, canonicalRoot, path, adapter, signal) {
73955
- const canonicalPath = await resolveCanonicalPath(readCanonicalPathFs(fs), path);
73965
+ async function escapesRoot(fs, canonicalRoot, path, adapter, signal, maxSymlinkFollows = Infinity) {
73966
+ const canonicalPath = await resolveCanonicalPath(readCanonicalPathFs(fs), path, maxSymlinkFollows);
73956
73967
  return !await (adapter === void 0 ? containsPath(readStat(fs), canonicalRoot, canonicalPath) : containsAdapterPath(adapter, canonicalRoot, canonicalPath, signal));
73957
73968
  }
73958
73969
  async function containsAdapterPath(adapter, canonicalRoot, canonicalPath, signal) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-js",
3
- "version": "0.1.800",
3
+ "version": "0.1.802",
4
4
  "description": "Budgeted JavaScript interpreter with explicit host capabilities and resumable execution",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -82,7 +82,7 @@
82
82
  "access": "public"
83
83
  },
84
84
  "dependencies": {
85
- "@poe-platform/safe-fs": "0.1.800",
85
+ "@poe-platform/safe-fs": "0.1.802",
86
86
  "@petamoriken/float16": "^3.9.3",
87
87
  "@noble/hashes": "2.4.0",
88
88
  "@formatjs/intl-localematcher": "0.8.13",