@poe-platform/safe-fs 0.1.808 → 0.1.809

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
@@ -10,6 +10,10 @@ working data. Supply `{fs, cwd, env, signal}` and an optional page-cache count
10
10
  `env.TMPDIR` or `cwd`, with conditional unlink and cleanup through `close()`.
11
11
  Providers must support those capabilities; there is no host fallback. Use an
12
12
  external backend for large data because memory filesystems retain spilled bytes.
13
+ `IntegerTable.entries()` replays records in unsigned key order with a fixed
14
+ 16-level node stack and batches of at most 128 records. It accepts zero-filled
15
+ caller storage with `allocate`, `read` and `write`; mutation during traversal
16
+ is rejected. Branch pointers use a separate fixed 128-entry cache.
13
17
 
14
18
  ## Quickstart
15
19
 
@@ -133,7 +137,11 @@ For temporary staging that must be cleaned up after its parent moves, adapters
133
137
  advertising `retainedStagingCleanup` accept `createStagedFile(..., { parent,
134
138
  retainCleanup: true })`. Use the returned `staging.cleanup.remove()` and always
135
139
  call `staging.cleanup.close()` in `finally`. Memory and its supported wrappers
136
- retain only the owned staging entries; replacement entries remain protected.
140
+ retain only the owned staging entries; replacement entries remain protected. When
141
+ `atomicStagedFileMutation` is available, `publishStagedFile(..., { preserveIdentity: true,
142
+ parent, destination })` updates an existing file without breaking hardlink aliases.
143
+ Memory and its supported wrappers adopt staged bytes and timestamps while keeping
144
+ the destination identity and mode; unsupported adapters reject this option.
137
145
  Overlays backed by stock Memory filesystems support conditional file mutations,
138
146
  directory preparation, and confined staging. Publication may omit `ancestors`
139
147
  when the destination shares the staging parent; the overlay checks the ancestry
@@ -76,6 +76,8 @@ export interface FileSystemCapabilities {
76
76
  /** Owned staging serialized within a trusted host; requires external tree isolation. */
77
77
  readonly trustedOwnedStaging?: boolean;
78
78
  readonly atomicFileStaging?: boolean;
79
+ /** Staged payload publication can atomically preserve an existing inode. */
80
+ readonly atomicStagedFileMutation?: boolean;
79
81
  /** Creates a cleanup handle bound to owned staging entries, independent of ancestor paths. */
80
82
  readonly retainedStagingCleanup?: boolean;
81
83
  readonly retainedStagingWrite?: boolean;
@@ -85,6 +87,8 @@ export interface FileSystemCapabilities {
85
87
  readonly synchronousDirectoryValidation?: boolean;
86
88
  /** Captures a followed destination path with a synchronous commit validator. */
87
89
  readonly synchronousStagingResolution?: boolean;
90
+ /** Also binds the final symlink and its target in the synchronous validator. */
91
+ readonly synchronousFollowedStagingResolution?: boolean;
88
92
  /** Invokes commitGuard in the same synchronous section as staged replacement. */
89
93
  readonly guardedStagingPublication?: boolean;
90
94
  readonly atomicFilePublication?: boolean;
@@ -127,6 +131,7 @@ export interface CapabilityQueryOptions extends OpenReadFileOptions {
127
131
  * Omission retains the ordinary per-target query and its acquisition intent. */
128
132
  readonly stagingAncestry?: boolean;
129
133
  readonly stagingResolution?: boolean;
134
+ readonly followFinalSymlink?: boolean;
130
135
  readonly create?: boolean;
131
136
  readonly creation?: OpenFileOptions["creation"];
132
137
  }
@@ -231,6 +236,10 @@ export interface FileStagingEntry {
231
236
  export interface FileResolutionStep extends FileStagingEntry {
232
237
  readonly linkTarget?: string;
233
238
  }
239
+ export interface PrepareStagingResolutionOptions extends FsOptions {
240
+ /** Requires synchronousFollowedStagingResolution; omission preserves final-entry resolution. */
241
+ readonly followFinalSymlink?: boolean;
242
+ }
234
243
  export interface FileStagingResolution {
235
244
  readonly path: string;
236
245
  readonly parent: FileStat;
@@ -277,6 +286,11 @@ export interface CreateStagedFileOptions extends FsOptions {
277
286
  readonly mtimeMs?: number;
278
287
  }
279
288
  export interface PublishStagedFileOptions extends FsOptions {
289
+ /** Requires atomicStagedFileMutation and an existing regular destination.
290
+ * Atomically adopts staged bytes and timestamps while preserving destination
291
+ * identity, links and mode. Consumes the staged pathname as ordinary publication.
292
+ * Unsupported hosts must reject before effects, never silently rename. */
293
+ readonly preserveIdentity?: boolean;
280
294
  readonly ancestors?: readonly FileStagingEntry[];
281
295
  /** Trusted, mutation-free validation. Must return literal true synchronously;
282
296
  * a promise is not acceptance. Requires guardedStagingPublication. */
@@ -337,7 +351,7 @@ export interface FileSystem {
337
351
  * The returned guard checks current identities/search access synchronously;
338
352
  * preparation is not validation, and the guard's result is not an async lease. */
339
353
  prepareDirectoryAncestry?(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
340
- prepareStagingResolution?(path: string, options?: FsOptions): Promise<FileStagingResolution>;
354
+ prepareStagingResolution?(path: string, options?: PrepareStagingResolutionOptions): Promise<FileStagingResolution>;
341
355
  removeStagedFile?(staging: FileStaging, options?: FsOptions): Promise<void>;
342
356
  openReadFile?(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
343
357
  openResizeFile?(path: string, options?: OpenResizeFileOptions): Promise<FileResizeHandle>;
@@ -8,7 +8,7 @@ export * from "./fs/object-publication/index.js";
8
8
  export * from "./fs/webdav/index.js";
9
9
  export * from "./bridge/index.js";
10
10
  export * from "./python/index.js";
11
- export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
11
+ export { parseXml, parseXmlSteps, parseXmlStream, XmlLimitError } from "./xml.js";
12
12
  export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
13
13
  export * from "./contracts/object.js";
14
14
  export { ObjectAuthority } from "./fs/object-authority.js";
@@ -8,7 +8,7 @@ export * from "./fs/object-publication/index.js";
8
8
  export * from "./fs/webdav/index.js";
9
9
  export * from "./bridge/index.js";
10
10
  export * from "./python/index.js";
11
- export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
11
+ export { parseXml, parseXmlSteps, parseXmlStream, XmlLimitError } from "./xml.js";
12
12
  export * from "./contracts/object.js";
13
13
  export { ObjectAuthority } from "./fs/object-authority.js";
14
14
  export * from "./fs/s3/index.js";
@@ -7,4 +7,4 @@ export declare function openRetainedResizeFile(filesystem: FileSystem, path: str
7
7
  export declare function readOnlyCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
8
8
  export declare function quotaCapabilities(capabilities: FileSystemCapabilities): FileSystemCapabilities;
9
9
  export declare function ownedMutationCapabilities(filesystem: FileSystem, capabilities?: FileSystemCapabilities): FileSystemCapabilities;
10
- export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "retainedStagingCleanup" | "atomicFilePublication" | "atomicEntryRemoval" | "atomicEntryRemovalReceipt" | "atomicTreeRemoval" | "atomicFileMutation" | "atomicFileStaging" | "guardedStagingPublication" | "atomicStagingAncestry" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
10
+ export declare function requireOwnedMutation(filesystem: FileSystem, path: string, capability: "retainedStagingCleanup" | "atomicFilePublication" | "atomicEntryRemoval" | "atomicEntryRemovalReceipt" | "atomicTreeRemoval" | "atomicFileMutation" | "atomicFileStaging" | "atomicStagedFileMutation" | "guardedStagingPublication" | "atomicStagingAncestry" | "atomicDirectoryMetadata", options: FsOptions, create?: boolean): Promise<void>;
@@ -132,11 +132,11 @@ export function readOnlyCapabilities(capabilities) {
132
132
  ].filter(name => capabilities[name] !== undefined).map(name => [name, capabilities[name]]));
133
133
  return Object.freeze({
134
134
  ...inspection, retainedStagingCleanup: false, retainedStagingWrite: false, readOnly: true, write: false, append: false, exclusiveCreate: false,
135
- synchronousStagingResolution: false,
135
+ synchronousStagingResolution: false, synchronousFollowedStagingResolution: false,
136
136
  mkdir: false, recursiveMkdir: false, remove: false, removeDirectory: false, recursiveRemove: false,
137
137
  rename: false, copy: false, exclusiveCopy: false, truncate: false, streamingAppend: false,
138
138
  randomAccessWrite: false, hardlinks: false, permissions: false, conditionalChmod: false, timestamps: false,
139
- descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
139
+ descriptorWriteStream: false, atomicResize: false, retainedResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
140
140
  atomicFilePublication: false, atomicRename: false, atomicRenameNoReplace: false, streamingWrite: false,
141
141
  });
142
142
  }
@@ -144,7 +144,7 @@ export function quotaCapabilities(capabilities) {
144
144
  const streamingWrite = requireCapabilities(capabilities.write, capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
145
145
  const streamingAppend = requireCapabilities(capabilities.append, capabilities.streamingAppend, !capabilities.readOnly);
146
146
  const { streamingWrite: ignoredWrite, streamingAppend: ignoredAppend, ...rest } = capabilities;
147
- return Object.freeze({ ...rest, synchronousStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
147
+ return Object.freeze({ ...rest, synchronousStagingResolution: false, synchronousFollowedStagingResolution: false, retainedStagingCleanup: false, retainedStagingWrite: false, atomicStagingAncestry: false, synchronousDirectoryValidation: false, guardedStagingPublication: false, atomicFilePublication: false, descriptorWriteStream: false, atomicResize: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
148
148
  ...(streamingWrite === undefined ? {} : { streamingWrite }),
149
149
  ...(streamingAppend === undefined ? {} : { streamingAppend }),
150
150
  });
@@ -167,6 +167,8 @@ export function ownedMutationCapabilities(filesystem, capabilities = filesystem.
167
167
  if (capabilities.atomicFileStaging === true && (capabilities.readOnly === true
168
168
  || typeof filesystem.createStagedFile !== "function" || typeof filesystem.publishStagedFile !== "function" || typeof filesystem.removeStagedFile !== "function"))
169
169
  (unavailable ??= {}).atomicFileStaging = false;
170
+ if (capabilities.atomicStagedFileMutation === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
171
+ (unavailable ??= {}).atomicStagedFileMutation = false;
170
172
  if (capabilities.retainedStagingCleanup === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
171
173
  (unavailable ??= {}).retainedStagingCleanup = false;
172
174
  if (capabilities.retainedStagingWrite === true && (capabilities.retainedStagingCleanup !== true || unavailable?.retainedStagingCleanup === false))
@@ -177,6 +179,8 @@ export function ownedMutationCapabilities(filesystem, capabilities = filesystem.
177
179
  (unavailable ??= {}).synchronousDirectoryValidation = false;
178
180
  if (capabilities.synchronousStagingResolution === true && (capabilities.readOnly === true || typeof filesystem.prepareStagingResolution !== "function"))
179
181
  (unavailable ??= {}).synchronousStagingResolution = false;
182
+ if (capabilities.synchronousFollowedStagingResolution === true && (capabilities.synchronousStagingResolution !== true || unavailable?.synchronousStagingResolution === false))
183
+ (unavailable ??= {}).synchronousFollowedStagingResolution = false;
180
184
  if (capabilities.guardedStagingPublication === true && (capabilities.atomicFileStaging !== true || unavailable?.atomicFileStaging === false))
181
185
  (unavailable ??= {}).guardedStagingPublication = false;
182
186
  if (capabilities.atomicDirectoryMetadata === true && (capabilities.readOnly === true || typeof filesystem.prepareDirectory !== "function"))
@@ -1,5 +1,5 @@
1
1
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
2
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PrepareStagingResolutionOptions, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
3
3
  import type { ByteSource } from "../../contracts/io.js";
4
4
  import { type EntryLocation } from "../mount/comparison.js";
5
5
  import { pathNamespace } from "../path-namespace.js";
@@ -43,7 +43,7 @@ export declare class DeviceFileSystem implements FileSystem {
43
43
  publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
44
44
  publishStagedFileSet(receipt: FileStaging, destination: string, supplied: PublishStagedFileSetOptions): Promise<FileStat>;
45
45
  prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
46
- prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
46
+ prepareStagingResolution(path: string, options?: PrepareStagingResolutionOptions): Promise<FileStagingResolution>;
47
47
  removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
48
48
  prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
49
49
  rename(source: string, destination: string, options?: RenameOptions): Promise<void>;
@@ -13,14 +13,14 @@ import { openFileDescriptor } from "../descriptor.js";
13
13
  import { normalizePath } from "../../contracts/virtual-path.js";
14
14
  const views = new WeakMap();
15
15
  const deviceCapabilities = Object.freeze({
16
- synchronousStagingResolution: false,
16
+ synchronousStagingResolution: false, synchronousFollowedStagingResolution: false,
17
17
  readOnly: false, read: true, stat: true, realpath: true, access: true, readdir: false,
18
18
  write: true, append: true, exclusiveCreate: true, streamingRead: true, retainedRead: true,
19
19
  streamingWrite: true, streamingAppend: true, independentWriteStreams: true, copy: true, exclusiveCopy: true,
20
20
  remove: false, removeDirectory: false, recursiveRemove: false, rename: false,
21
21
  mkdir: false, recursiveMkdir: false, symlinks: false, hardlinks: false, readlink: false,
22
22
  permissions: false, timestamps: false, truncate: false, randomAccessWrite: false,
23
- open: true, retainedStagingCleanup: false, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
23
+ open: true, retainedStagingCleanup: false, atomicFilePublication: false, atomicFileMutation: false, atomicEntryRemoval: false, atomicEntryRemovalReceipt: false, atomicTreeRemoval: false, atomicStagedFileMutation: false, atomicFileStaging: false, atomicDirectoryMetadata: false, trustedOwnedStaging: false,
24
24
  atomicRename: false, atomicRenameNoReplace: false, descriptorWriteStream: true, retainedResize: true, atomicResize: false,
25
25
  });
26
26
  function globalCapabilities(filesystem) {
@@ -31,6 +31,7 @@ function globalCapabilities(filesystem) {
31
31
  synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
32
32
  synchronousStagingResolution: ["prepareStagingResolution"],
33
33
  trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
34
+ atomicStagedFileMutation: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
34
35
  atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
35
36
  streamingRead: ["readStream"], streamingWrite: ["writeStream"], retainedRead: ["openReadFile"],
36
37
  streamingAppend: ["writeStream"], descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
@@ -178,7 +179,7 @@ export class DeviceFileSystem {
178
179
  let selected;
179
180
  const traversal = options.stagingResolution === true ? { virtual: false } : undefined;
180
181
  try {
181
- resolved = await this.#resolve(path, options, options.stagingResolution !== true && (options.create !== undefined || options.creation !== "exclusive"), options.create, traversal);
182
+ resolved = await this.#resolve(path, options, options.stagingResolution === true ? options.followFinalSymlink === true : options.create !== undefined || options.creation !== "exclusive", options.create, traversal);
182
183
  }
183
184
  catch (error) {
184
185
  options.signal?.throwIfAborted();
@@ -209,7 +210,7 @@ export class DeviceFileSystem {
209
210
  // This intent cannot bind virtual traversal. Do not infer other path
210
211
  // capabilities by resolving the shadowed entry in the backing filesystem.
211
212
  if (traversal?.virtual)
212
- return Object.freeze({ synchronousStagingResolution: false });
213
+ return Object.freeze({ synchronousStagingResolution: false, synchronousFollowedStagingResolution: false });
213
214
  if (selected === undefined) {
214
215
  const query = this.#filesystem.capabilitiesFor;
215
216
  options.signal?.throwIfAborted();
@@ -228,6 +229,7 @@ export class DeviceFileSystem {
228
229
  throw new FsError("EAGAIN", { path });
229
230
  if ([resolution.path, ...resolution.traversed.map(entry => entry.path)].some(entry => entry === deviceDirectory || entry.startsWith(`${deviceDirectory}/`))) {
230
231
  (unavailable ??= {}).synchronousStagingResolution = false;
232
+ unavailable.synchronousFollowedStagingResolution = false;
231
233
  }
232
234
  runStagingGuard(resolution.validate);
233
235
  }
@@ -236,6 +238,7 @@ export class DeviceFileSystem {
236
238
  if (!isFsError(error, "ENOTSUP"))
237
239
  throw error;
238
240
  (unavailable ??= {}).synchronousStagingResolution = false;
241
+ unavailable.synchronousFollowedStagingResolution = false;
239
242
  }
240
243
  }
241
244
  if (resolved.startsWith(`${deviceDirectory}/`)) {
@@ -243,6 +246,7 @@ export class DeviceFileSystem {
243
246
  u.atomicStagingAncestry = false;
244
247
  u.synchronousDirectoryValidation = false;
245
248
  u.synchronousStagingResolution = false;
249
+ u.synchronousFollowedStagingResolution = false;
246
250
  }
247
251
  if (typeof this.#filesystem.open !== "function" && capabilities.open !== false)
248
252
  (unavailable ??= {}).open = false;
@@ -667,6 +671,8 @@ export class DeviceFileSystem {
667
671
  await this.#mutable(path, options, false);
668
672
  await requireOwnedMutation(this.#filesystem, staging.directory.path, "atomicFileStaging", options);
669
673
  await requireOwnedMutation(this.#filesystem, destination, "atomicFileStaging", options, options.destination === null);
674
+ if (options.preserveIdentity)
675
+ await requireOwnedMutation(this.#filesystem, destination, "atomicStagedFileMutation", options);
670
676
  if (!this.#filesystem.publishStagedFile)
671
677
  throw new FsError("ENOTSUP", { path: destination });
672
678
  if (options.commitGuard !== undefined)
@@ -750,19 +756,19 @@ export class DeviceFileSystem {
750
756
  }
751
757
  }
752
758
  async prepareStagingResolution(path, options = {}) {
753
- const controls = options.signal === undefined ? {} : { signal: options.signal };
759
+ const controls = { ...(options.signal === undefined ? {} : { signal: options.signal }), ...(options.followFinalSymlink === true ? { followFinalSymlink: true } : {}) };
754
760
  controls.signal?.throwIfAborted();
755
761
  if (normalizePath(path) !== path || !path.startsWith("/"))
756
762
  throw new FsError("EINVAL", { path });
757
763
  try {
758
764
  const traversal = { virtual: false };
759
- const resolved = await this.#resolve(path, controls, false, undefined, traversal);
765
+ const resolved = await this.#resolve(path, controls, controls.followFinalSymlink === true, undefined, traversal);
760
766
  controls.signal?.throwIfAborted();
761
767
  if (traversal.virtual)
762
768
  throw new FsError("ENOTSUP", { path });
763
769
  const declared = ownedMutationCapabilities(this.#filesystem, await this.#filesystem.capabilitiesFor?.(path, { ...controls, stagingResolution: true }) ?? this.#filesystem.capabilities);
764
770
  controls.signal?.throwIfAborted();
765
- if (declared.synchronousStagingResolution !== true || !this.#filesystem.prepareStagingResolution)
771
+ if (declared.synchronousStagingResolution !== true || controls.followFinalSymlink === true && declared.synchronousFollowedStagingResolution !== true || !this.#filesystem.prepareStagingResolution)
766
772
  throw new FsError("ENOTSUP", { path });
767
773
  const receipt = snapshotStagingResolution(await this.#filesystem.prepareStagingResolution(path, controls));
768
774
  controls.signal?.throwIfAborted();
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, FileDescriptor, OpenFileOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CopyFileOptions, DirectoryEntry, EntryComparison, FileReadHandle, FileResizeHandle, FileStat, FileSystem, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, FileDescriptor, OpenFileOptions, OpenReadFileOptions, OpenResizeFileOptions, WriteFileOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PrepareStagingResolutionOptions, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  import { type MemoryAtomicView } from "./atomic-view.js";
4
4
  import { MemoryAllocation, MemoryLedger } from "./ledger.js";
@@ -116,7 +116,7 @@ export declare class MemoryFileSystem implements FileSystem {
116
116
  createStagedFile(directoryPath: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
117
117
  private retainStagingCleanup;
118
118
  prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
119
- prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
119
+ prepareStagingResolution(path: string, options?: PrepareStagingResolutionOptions): Promise<FileStagingResolution>;
120
120
  private verifyDirectoryAncestry;
121
121
  publishStagedFile(receipt: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
122
122
  publishStagedFileSet(receipt: FileStaging, destination: string, options: PublishStagedFileSetOptions): Promise<FileStat>;
@@ -584,8 +584,8 @@ const SHARED_STOCK_MEMORY_CAPABILITIES = Object.freeze({
584
584
  permissions: true,
585
585
  timestamps: true,
586
586
  atomicRename: true,
587
- atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
588
- synchronousDirectoryValidation: true, synchronousStagingResolution: true, guardedStagingPublication: true,
587
+ atomicStagedFileMutation: true, atomicFileStaging: true, retainedStagingCleanup: true, retainedStagingWrite: true, atomicStagingAncestry: true, atomicFileMutation: true, atomicEntryRemoval: true, atomicEntryRemovalReceipt: true, atomicTreeRemoval: true,
588
+ synchronousDirectoryValidation: true, synchronousStagingResolution: true, synchronousFollowedStagingResolution: true, guardedStagingPublication: true,
589
589
  atomicDirectoryMetadata: true,
590
590
  streamingRead: true,
591
591
  retainedRead: true,
@@ -2186,13 +2186,14 @@ export class MemoryFileSystem {
2186
2186
  return () => this.verifyDirectoryAncestry(entries, controls);
2187
2187
  }
2188
2188
  async prepareStagingResolution(path, options = {}) {
2189
+ const followFinal = options.followFinalSymlink === true;
2189
2190
  const controls = options.signal === undefined ? {} : { signal: options.signal };
2190
2191
  controls.signal?.throwIfAborted();
2191
2192
  this.validatePath(path, "prepareStagingResolution");
2192
2193
  if (!path.startsWith("/") || normalizePath(path) !== path)
2193
2194
  this.fail("EINVAL", "prepareStagingResolution", path);
2194
2195
  const traversed = [];
2195
- const location = this.resolve(path, "prepareStagingResolution", { followFinal: false, allowMissing: true, resolutionSteps: traversed });
2196
+ const location = this.resolve(path, "prepareStagingResolution", { followFinal, allowMissing: true, resolutionSteps: traversed });
2196
2197
  if (location.node && location.node.type !== "file")
2197
2198
  this.fail("ENOTSUP", "prepareStagingResolution", path);
2198
2199
  const parentPath = location.path.slice(0, location.path.lastIndexOf("/")) || "/";
@@ -2207,7 +2208,7 @@ export class MemoryFileSystem {
2207
2208
  const currentSteps = [];
2208
2209
  let current;
2209
2210
  try {
2210
- current = this.resolve(path, "verifyStagingResolution", { followFinal: false, allowMissing: true, resolutionSteps: currentSteps });
2211
+ current = this.resolve(path, "verifyStagingResolution", { followFinal, allowMissing: true, resolutionSteps: currentSteps });
2211
2212
  }
2212
2213
  catch (error) {
2213
2214
  if (error instanceof FsError && ["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code))
@@ -2245,6 +2246,7 @@ export class MemoryFileSystem {
2245
2246
  }
2246
2247
  async publishStagedFile(receipt, destination, options) {
2247
2248
  const signal = options.signal;
2249
+ const preserveIdentity = options.preserveIdentity === true;
2248
2250
  const guard = options.commitGuard;
2249
2251
  const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
2250
2252
  const snapshot = (entry) => ({ path: entry.path, stat: { ...entry.stat } });
@@ -2272,9 +2274,48 @@ export class MemoryFileSystem {
2272
2274
  this.fail("EINVAL", "publishStagedFile", destination);
2273
2275
  if (target.node && target.node.type !== "file")
2274
2276
  this.fail("EAGAIN", "publishStagedFile", destination);
2277
+ if (preserveIdentity) {
2278
+ if (this.capabilities.atomicStagedFileMutation !== true)
2279
+ this.fail("ENOTSUP", "publishStagedFile", destination);
2280
+ if (file.node?.type !== "file" || target.node?.type !== "file" || expected === null)
2281
+ this.fail("EINVAL", "publishStagedFile", destination);
2282
+ this.permission(file.parent, 3, "publishStagedFile", staging.file.path);
2283
+ this.permission(target.parent, 3, "publishStagedFile", destination);
2284
+ this.permission(target.node, 2, "publishStagedFile", destination);
2285
+ // Provider-owned storage is copied directly, without assembling an archive
2286
+ // in caller memory. Preserve retained staging readers and independent writes.
2287
+ this.admitSize(target.node, file.node.byteLength, "publishStagedFile", destination);
2288
+ const allocation = this.allocate(file.node.byteLength, "publishStagedFile", destination);
2289
+ try {
2290
+ for (let offset = 0; offset < file.node.byteLength; offset += 65536) {
2291
+ allocation.data.set(file.node.data.subarray(offset, offset + 65536), offset);
2292
+ }
2293
+ }
2294
+ catch (error) {
2295
+ allocation.release();
2296
+ throw error;
2297
+ }
2298
+ // No awaits or overridable operations occur between admission and commit.
2299
+ const now = Date.now();
2300
+ this.replaceData(target.node, allocation);
2301
+ this.changed(target.node, now);
2302
+ target.node.atimeMs = file.node.atimeMs;
2303
+ target.node.mtimeMs = file.node.mtimeMs;
2304
+ file.parent.entries.delete(file.name);
2305
+ this.ledger.release(file.name.length * 2, 1);
2306
+ file.node.nlink--;
2307
+ file.node.ctimeMs = now;
2308
+ file.node.revision = Math.min(Number.MAX_SAFE_INTEGER + 1, file.node.revision + 1);
2309
+ this.releaseNode(file.node);
2310
+ this.changed(file.parent, now);
2311
+ memoryCaches.get(this.ledger).clearWrites();
2312
+ return;
2313
+ }
2275
2314
  return MemoryFileSystem.prototype.rename.call(this, staging.file.path, destination, { ...controls, noReplace: expected === null });
2276
2315
  }
2277
2316
  async publishStagedFileSet(receipt, destination, options) {
2317
+ if (options.preserveIdentity)
2318
+ this.fail("ENOTSUP", "publishStagedFileSet", destination);
2278
2319
  const signal = options.signal;
2279
2320
  const guard = options.commitGuard;
2280
2321
  const ancestors = options.ancestors === undefined ? undefined : snapshotDirectoryAncestry(options.ancestors);
@@ -1,4 +1,4 @@
1
- import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
1
+ import type { AppendFileOptions, CapabilityQueryOptions, CopyFileOptions, DirectoryEntry, FileReadHandle, FileResizeHandle, FileResizeOperation, FileResizeOptions, FileStat, FileSystem, OpenReadFileOptions, OpenResizeFileOptions, FileSystemCapabilities, FsOptions, ChmodOptions, RenameOptions, MkdirOptions, ReadDirectoryOptions, ReadFileOptions, ReadStreamOptions, RemoveOptions, WriteFileOptions, ConditionalFilePublicationOptions, ConditionalWriteFileOptions, ConditionalRemoveFileOptions, ConditionalRemoveEntryOptions, ConditionalRemoveEntryReceiptOptions, CreateStagedFileOptions, FileStaging, FileStagingEntry, FileStagingResolution, PrepareStagingResolutionOptions, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
3
  import type { FileDescriptor, OpenFileOptions } from "../../contracts/descriptor.js";
4
4
  export interface MountFileSystemOptions {
@@ -18,7 +18,7 @@ export declare class MountFileSystem implements FileSystem {
18
18
  private supportsStagingAncestry;
19
19
  private supportsDirectoryValidation;
20
20
  prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
21
- prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
21
+ prepareStagingResolution(path: string, options?: PrepareStagingResolutionOptions): Promise<FileStagingResolution>;
22
22
  private prepareAncestry;
23
23
  private protected;
24
24
  openReadFile(path: string, options?: OpenReadFileOptions): Promise<FileReadHandle>;
@@ -109,6 +109,7 @@ export class MountFileSystem {
109
109
  retainedStagingCleanup: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
110
110
  synchronousDirectoryValidation: ["prepareDirectoryAncestry"], guardedStagingPublication: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
111
111
  trustedOwnedStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile", "writeFileConditional", "removeFileConditional", "prepareDirectory"],
112
+ atomicStagedFileMutation: ["createStagedFile", "publishStagedFile", "removeStagedFile"],
112
113
  atomicFilePublication: ["publishFileConditional"], atomicEntryRemoval: ["removeEntryConditional"], atomicEntryRemovalReceipt: ["removeEntryConditional"], atomicTreeRemoval: ["removeTreeConditional"], atomicFileMutation: ["writeFileConditional", "removeFileConditional"], atomicFileStaging: ["createStagedFile", "publishStagedFile", "removeStagedFile"], atomicDirectoryMetadata: ["prepareDirectory"],
113
114
  symlinks: ["symlink", "readlink"], hardlinks: ["link"], permissions: ["chmod"], timestamps: ["utimes"], readlink: ["readlink"],
114
115
  descriptorWriteStream: ["writeStream"], retainedResize: ["openResizeFile"], atomicResize: ["resizeFile"],
@@ -128,7 +129,7 @@ export class MountFileSystem {
128
129
  return values.every(value => value === true) ? true : values.every(value => value === false) ? false : undefined;
129
130
  };
130
131
  const semantics = Object.fromEntries([
131
- "guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt",
132
+ "guardedStagingPublication", "retainedStagingCleanup", "atomicEntryRemovalReceipt", "atomicStagedFileMutation",
132
133
  "trustedOwnedStaging", "atomicFilePublication", "atomicEntryRemoval", "atomicTreeRemoval", "atomicFileMutation", "atomicFileStaging", "atomicDirectoryMetadata", "read", "stat", "readdir", "realpath", "access", "open", "versionedDescriptors",
133
134
  "write", "append", "exclusiveCreate", "explicitDirectories", "implicitDirectories", "mkdir", "recursiveMkdir",
134
135
  "remove", "removeDirectory", "recursiveRemove", "rename", "atomicRenameNoReplace", "copy", "exclusiveCopy", "readlink", "truncate",
@@ -190,7 +191,8 @@ export class MountFileSystem {
190
191
  fail("EINVAL");
191
192
  const location = await this.resolve(path, options, {
192
193
  allowMissing: options.create ?? true,
193
- followFinal: options.stagingResolution !== true && (options.create !== undefined || options.creation !== "exclusive"),
194
+ retainedResolution: options.stagingResolution === true && options.followFinalSymlink === true,
195
+ followFinal: options.stagingResolution === true ? options.followFinalSymlink === true : options.create !== undefined || options.creation !== "exclusive",
194
196
  ...(options.create === undefined ? {} : { resizeCreate: options.create }),
195
197
  });
196
198
  const observed = ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(location.local, options)
@@ -211,15 +213,15 @@ export class MountFileSystem {
211
213
  const input = location.mount.path === "/" ? path : path.slice(location.mount.path.length) || "/";
212
214
  const declared = input === location.local ? resize : ownedMutationCapabilities(location.mount.backend, await location.mount.backend.capabilitiesFor?.(input, options) ?? location.mount.backend.capabilities);
213
215
  options.signal?.throwIfAborted();
214
- resolution = declared.synchronousStagingResolution === true && await this.supportsStagingAncestry(location, resize, options);
216
+ resolution = declared.synchronousStagingResolution === true && (options.followFinalSymlink !== true || declared.synchronousFollowedStagingResolution === true) && await this.supportsStagingAncestry(location, resize, options);
215
217
  }
216
218
  }
217
219
  const conditionalChmod = options.conditionalChmod === true
218
220
  ? resize.conditionalChmod === true && typeof location.mount.backend.chmod === "function"
219
221
  && await this.supportsDirectoryValidation(location.path.slice(0, location.path.lastIndexOf("/")) || "/", options, true)
220
222
  : this.capabilities.conditionalChmod;
221
- const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, ...ordinary } = resize;
222
- const withOpen = { ...ordinary, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
223
+ const { synchronousDirectoryValidation: ignoredValidation, synchronousStagingResolution: ignoredResolution, synchronousFollowedStagingResolution: ignoredFollowedResolution, ...ordinary } = resize;
224
+ const withOpen = { ...ordinary, ...(conditionalChmod === undefined ? {} : { conditionalChmod }), ...(resolution === undefined ? {} : { synchronousStagingResolution: resolution, ...(options.followFinalSymlink === true ? { synchronousFollowedStagingResolution: resolution } : {}) }), ...(validation === undefined ? {} : { synchronousDirectoryValidation: validation }), atomicStagingAncestry: ancestry, ...(typeof location.mount.backend.open === "function" ? {} : { open: false }) };
223
225
  const capabilities = location.synthetic ? { ...withOpen, open: false, retainedRead: false }
224
226
  : retainedResizeCapabilities(location.mount.backend, retainedReadCapabilities(location.mount.backend, withOpen));
225
227
  if (location.synthetic)
@@ -263,19 +265,19 @@ export class MountFileSystem {
263
265
  return this.operation("prepareDirectoryAncestry", ancestors.at(-1)?.path ?? "/", options, () => this.prepareAncestry(ancestors, options));
264
266
  }
265
267
  prepareStagingResolution(path, options = {}) {
266
- const controls = options.signal === undefined ? {} : { signal: options.signal };
268
+ const controls = { ...(options.signal === undefined ? {} : { signal: options.signal }), ...(options.followFinalSymlink === true ? { followFinalSymlink: true } : {}) };
267
269
  return this.operation("prepareStagingResolution", path, controls, async () => {
268
270
  if (normalizePath(globalPath(path)) !== path)
269
271
  fail("EINVAL");
270
272
  const observed = [];
271
- const location = await this.resolve(path, controls, { followFinal: false, allowMissing: true, resolutionSteps: observed });
273
+ const location = await this.resolve(path, controls, { followFinal: controls.followFinalSymlink === true, retainedResolution: controls.followFinalSymlink === true, allowMissing: true, resolutionSteps: observed });
272
274
  if (location.synthetic || !within(location.mount.path, path))
273
275
  fail("ENOTSUP");
274
276
  const backend = location.mount.backend;
275
277
  const input = location.mount.path === "/" ? path : path.slice(location.mount.path.length) || "/";
276
278
  const declared = ownedMutationCapabilities(backend, await backend.capabilitiesFor?.(input, { ...controls, stagingResolution: true }) ?? backend.capabilities);
277
279
  controls.signal?.throwIfAborted();
278
- if (declared.synchronousStagingResolution !== true || !backend.prepareStagingResolution)
280
+ if (declared.synchronousStagingResolution !== true || controls.followFinalSymlink === true && declared.synchronousFollowedStagingResolution !== true || !backend.prepareStagingResolution)
279
281
  fail("ENOTSUP");
280
282
  const prefix = directoryAncestryPaths(location.mount.path).map(ancestor => {
281
283
  const entry = observed.find(step => step.path === ancestor);
@@ -545,6 +547,10 @@ export class MountFileSystem {
545
547
  let followedFinalLink = false;
546
548
  const verified = async (location) => {
547
549
  let inspection = !location.stat && settings.resizeCreate === true ? creationVerification : verification;
550
+ // A guarded resolution binds the original link chain and missing target
551
+ // through the authoritative backend receipt; realpath cannot name absence.
552
+ if (settings.retainedResolution && !location.stat && followedFinalLink)
553
+ inspection = undefined;
548
554
  // A missing entry has no realpath. Verify its existing parent while
549
555
  // leaving dangling final symlinks on the ordinary following path.
550
556
  if (inspection && !location.stat && settings.allowMissing && !followedFinalLink && inspection.finalName === undefined) {
@@ -1096,6 +1102,8 @@ export class MountFileSystem {
1096
1102
  await requireOwnedMutation(local.mount.backend, target.local, "atomicFileStaging", options, options.destination === null);
1097
1103
  if (!local.mount.backend.publishStagedFile)
1098
1104
  fail("ENOTSUP");
1105
+ if (options.preserveIdentity)
1106
+ await requireOwnedMutation(local.mount.backend, target.local, "atomicStagedFileMutation", options);
1099
1107
  if (ancestors || callerGuard !== undefined)
1100
1108
  await requireOwnedMutation(local.mount.backend, target.local, "guardedStagingPublication", options, options.destination === null);
1101
1109
  if (!ancestors) {
@@ -43,6 +43,7 @@ export declare class OverlayFileSystem implements FileSystem {
43
43
  private bytes;
44
44
  private cleanGarbage;
45
45
  cleanup(options?: FsOptions): Promise<void>;
46
+ private stagingOptions;
46
47
  private staged;
47
48
  private preserve;
48
49
  private clone;
@@ -554,7 +554,12 @@ export class OverlayFileSystem {
554
554
  async cleanup(options = {}) {
555
555
  return this.run(options, () => this.cleanGarbage(true));
556
556
  }
557
+ // Receipts describe overlay paths and identities, never backing-layer staging paths.
558
+ stagingOptions({ signal }) {
559
+ return signal === undefined ? {} : { signal };
560
+ }
557
561
  async staged(options, operation) {
562
+ options = this.stagingOptions(options);
558
563
  const root = `/.virtual-bash-overlay-${platform.randomUUID()}`;
559
564
  if (await this.maybeStat(this.#upper, root, options) || await this.maybeStat(this.#lower, root, options))
560
565
  fail("EEXIST", root);
@@ -570,6 +575,7 @@ export class OverlayFileSystem {
570
575
  }
571
576
  }
572
577
  async preserve(path, stat, options) {
578
+ options = this.stagingOptions(options);
573
579
  if (this.#upper.chmod)
574
580
  await this.#upper.chmod(path, stat.mode & 0o7777, options);
575
581
  else if (((await this.#upper.lstat(path, options)).mode & 0o7777) !== (stat.mode & 0o7777))
@@ -579,6 +585,7 @@ export class OverlayFileSystem {
579
585
  await this.#upper.utimes(path, stat.atimeMs, stat.mtimeMs, options);
580
586
  }
581
587
  async clone(entry, destination, options) {
588
+ options = this.stagingOptions(options);
582
589
  if (entry.stat.type !== "directory" && ((entry.stat.nlink ?? 1) > 1
583
590
  || (entry.backend.capabilities.hardlinks === true && entry.stat.nlink === undefined))) {
584
591
  fail("ENOTSUP", entry.path, "copy-up cannot preserve hardlink identity");
@@ -608,6 +615,7 @@ export class OverlayFileSystem {
608
615
  }
609
616
  }
610
617
  async ensureDirectory(path, options) {
618
+ options = this.stagingOptions(options);
611
619
  const entry = await this.lookup(path, options);
612
620
  if (!entry)
613
621
  fail("ENOENT", path);
@@ -618,6 +626,7 @@ export class OverlayFileSystem {
618
626
  await this.copyUp(entry, options);
619
627
  }
620
628
  async copyUp(entry, options) {
629
+ options = this.stagingOptions(options);
621
630
  if (entry.backend === this.#upper)
622
631
  return;
623
632
  if (entry.path === "/")
@@ -640,6 +649,7 @@ export class OverlayFileSystem {
640
649
  });
641
650
  }
642
651
  async parent(path, options) {
652
+ options = this.stagingOptions(options);
643
653
  const parent = await this.lookup(dirname(path), options);
644
654
  if (!parent)
645
655
  fail("ENOENT", dirname(path));
@@ -376,6 +376,24 @@ export class OverlayMemoryPublication {
376
376
  const owned = await this.upper.createStagedFile(directory, name, content, { ...options, parent });
377
377
  const staging = Object.freeze({
378
378
  parent: owned.parent, directory: owned.directory, file: owned.file,
379
+ ...(owned.writer ? { writer: Object.freeze({
380
+ write: async (bytes, controls = {}) => {
381
+ controls.signal?.throwIfAborted();
382
+ this.stores();
383
+ guard?.();
384
+ this.check(ancestors);
385
+ await owned.writer.write(bytes, controls);
386
+ },
387
+ finish: async (controls = {}) => {
388
+ controls.signal?.throwIfAborted();
389
+ this.stores();
390
+ guard?.();
391
+ this.check(ancestors);
392
+ const stat = await owned.writer.finish(controls);
393
+ this.stages.set(staging.directory, { ...owned, file: { ...owned.file, stat } });
394
+ return stat;
395
+ },
396
+ }) } : {}),
379
397
  ...(owned.cleanup ? { cleanup: createStagingCleanup(directory, controls => this.cleanup(staging, controls, true), () => owned.cleanup.close()) } : {}),
380
398
  });
381
399
  this.stages.set(staging.directory, owned);
@@ -384,6 +402,8 @@ export class OverlayMemoryPublication {
384
402
  return staging;
385
403
  }
386
404
  async publish(staging, path, options) {
405
+ if (options.preserveIdentity)
406
+ throw new FsError("ENOTSUP", { path });
387
407
  options.signal?.throwIfAborted();
388
408
  this.path(path);
389
409
  const owned = this.stages.get(staging.directory);
@@ -413,6 +413,8 @@ export class RealFileSystem {
413
413
  throw new FsError("EAGAIN");
414
414
  }
415
415
  async publishStagedFile(staging, destination, options) {
416
+ if (options.preserveIdentity)
417
+ throw new FsError("ENOTSUP", { path: destination });
416
418
  return this.operation("publishStagedFile", staging.file.path, options, async () => {
417
419
  options.signal?.throwIfAborted();
418
420
  if (options.ancestors !== undefined || options.commitGuard !== undefined)
@@ -343,6 +343,8 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
343
343
  if (property === "publishStagedFile") {
344
344
  const publishOptions = (options ?? {});
345
345
  await requireOwnedMutation(original, args[1], "atomicFileStaging", publishOptions, publishOptions.destination === null);
346
+ if (publishOptions.preserveIdentity)
347
+ await requireOwnedMutation(original, args[1], "atomicStagedFileMutation", publishOptions);
346
348
  const query = options.destination === null ? { ...options, create: true } : options;
347
349
  const declared = ownedMutationCapabilities(original, await original.capabilitiesFor?.(args[1], query) ?? original.capabilities);
348
350
  assertReady();
@@ -457,7 +459,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
457
459
  try {
458
460
  const declared = ownedMutationCapabilities(original, await original.capabilitiesFor?.(path, { ...controls, stagingResolution: true }) ?? original.capabilities);
459
461
  controls.signal?.throwIfAborted();
460
- if (declared.synchronousStagingResolution !== true)
462
+ if (declared.synchronousStagingResolution !== true || controls.followFinalSymlink === true && declared.synchronousFollowedStagingResolution !== true)
461
463
  throw new FsError("ENOTSUP", { path });
462
464
  const resolution = snapshotStagingResolution(await Reflect.apply(method, original, [path, controls]));
463
465
  controls.signal?.throwIfAborted();
@@ -39,11 +39,19 @@ export declare class IntegerTable {
39
39
  private readonly storage;
40
40
  private readonly maxEntries;
41
41
  private root;
42
+ private revision;
43
+ private readonly branches;
42
44
  private readonly cache;
43
- constructor(storage: PagedStorage, maxEntries?: number);
45
+ constructor(storage: Pick<PagedStorage, "allocate" | "read" | "write">, maxEntries?: number);
44
46
  private retain;
45
47
  get(key: bigint): Promise<bigint | undefined>;
46
48
  set(key: bigint, value: bigint): Promise<void>;
49
+ /** Ordered replay with a fixed 16-level radix stack (at most 2 KiB of
50
+ * owned node bytes) and 128 output records. Batches avoid alternating sparse
51
+ * index reads with consumer writes on every record. Mutation is not permitted. */
52
+ entries(start?: bigint, end?: bigint): AsyncGenerator<readonly [bigint, bigint]>;
53
+ /** Hot radix paths must not evict dirty leaf pages in a one-page backend. */
54
+ private branch;
47
55
  private lookup;
48
56
  private persist;
49
57
  }
@@ -103,14 +103,20 @@ export class PagedStorage {
103
103
  this.pages.set(number, cached);
104
104
  return cached;
105
105
  }
106
+ let reusable;
106
107
  if (this.pages.size === this.maxPages) {
107
108
  if (!this.descriptor)
108
109
  await this.spill();
109
110
  const [oldNumber, oldPage] = this.pages.entries().next().value;
110
111
  await this.flush(oldNumber, oldPage);
111
112
  this.pages.delete(oldNumber);
113
+ reusable = oldPage;
112
114
  }
113
- const page = { bytes: new Uint8Array(pageBytes), dirty: false };
115
+ // Flush owns the bytes until its awaited write completes. Reuse only then;
116
+ // zero new/unwritten ranges so the previous page cannot leak into them.
117
+ const page = reusable ?? { bytes: new Uint8Array(pageBytes), dirty: false };
118
+ if (reusable)
119
+ page.bytes.fill(0);
114
120
  if (number * pageBytes < this.diskLength) {
115
121
  let offset = 0;
116
122
  while (offset < pageBytes) {
@@ -192,6 +198,8 @@ export class IntegerTable {
192
198
  storage;
193
199
  maxEntries;
194
200
  root = 0;
201
+ revision = 0;
202
+ branches = new Map();
195
203
  cache = new Map();
196
204
  constructor(storage, maxEntries = 4096) {
197
205
  this.storage = storage;
@@ -202,8 +210,14 @@ export class IntegerTable {
202
210
  async retain(key, value, dirty) {
203
211
  if (this.cache.size === this.maxEntries && !this.cache.has(key)) {
204
212
  const [oldKey, old] = this.cache.entries().next().value;
213
+ // Flush the bounded dirty window together, before the caller touches other
214
+ // stored data. One dirty leaf per insertion otherwise thrashes a page cache.
205
215
  if (old.dirty)
206
- await this.persist(oldKey, old.value);
216
+ for (const [key, entry] of this.cache)
217
+ if (entry.dirty) {
218
+ await this.persist(key, entry.value);
219
+ entry.dirty = false;
220
+ }
207
221
  this.cache.delete(oldKey);
208
222
  }
209
223
  this.cache.delete(key);
@@ -224,19 +238,98 @@ export class IntegerTable {
224
238
  async set(key, value) {
225
239
  if (key < 0n || key > 0xffffffffffffffffn || value < 0n || value >= 0xffffffffffffffffn)
226
240
  throw new RangeError("Invalid storage integer table value");
241
+ this.revision++;
227
242
  await this.retain(key, value, true);
228
243
  }
244
+ /** Ordered replay with a fixed 16-level radix stack (at most 2 KiB of
245
+ * owned node bytes) and 128 output records. Batches avoid alternating sparse
246
+ * index reads with consumer writes on every record. Mutation is not permitted. */
247
+ async *entries(start = 0n, end = 0x10000000000000000n) {
248
+ if (start < 0n || start > end || end > 0x10000000000000000n)
249
+ throw new RangeError("Invalid integer table range");
250
+ if (start === end)
251
+ return;
252
+ const revision = this.revision;
253
+ const check = () => { if (revision !== this.revision)
254
+ throw new Error("Integer table changed during iteration"); };
255
+ for (const [key, entry] of this.cache)
256
+ if (entry.dirty) {
257
+ check();
258
+ await this.persist(key, entry.value);
259
+ check();
260
+ entry.dirty = false;
261
+ }
262
+ const storage = this.storage;
263
+ async function* visit(position, shift, prefix) {
264
+ check();
265
+ if (!position)
266
+ return;
267
+ const bytes = new Uint8Array(await storage.read(position, 128));
268
+ check();
269
+ if (bytes.length !== 128)
270
+ throw new RangeError("Truncated integer table node");
271
+ const node = new DataView(bytes.buffer);
272
+ for (let slot = 0; slot < 16; slot++) {
273
+ check();
274
+ const value = node.getBigUint64(slot * 8, true);
275
+ if (!value)
276
+ continue;
277
+ const key = prefix | BigInt(slot) << shift;
278
+ if (key >= end || key + (1n << shift) <= start)
279
+ continue;
280
+ if (shift === 0n)
281
+ yield [key, value - 1n];
282
+ else
283
+ yield* visit(Number(value), shift - 4n, key);
284
+ }
285
+ }
286
+ const batch = [];
287
+ for await (const entry of visit(this.root, 60n, 0n)) {
288
+ batch.push(entry);
289
+ if (batch.length === 128) {
290
+ for (const entry of batch) {
291
+ check();
292
+ yield entry;
293
+ }
294
+ batch.length = 0;
295
+ }
296
+ }
297
+ for (const entry of batch) {
298
+ check();
299
+ yield entry;
300
+ }
301
+ check();
302
+ }
303
+ /** Hot radix paths must not evict dirty leaf pages in a one-page backend. */
304
+ branch(key, shift, position) {
305
+ const prefix = key >> shift | 1n << (64n - shift);
306
+ const value = position ?? this.branches.get(prefix);
307
+ if (value !== undefined) {
308
+ this.branches.delete(prefix);
309
+ this.branches.set(prefix, value);
310
+ if (this.branches.size > 128)
311
+ this.branches.delete(this.branches.keys().next().value);
312
+ }
313
+ return value;
314
+ }
229
315
  async lookup(key) {
230
316
  let node = this.root;
231
317
  for (let shift = 60n; shift >= 0n; shift -= 4n) {
232
318
  if (!node)
233
319
  return undefined;
320
+ const cached = shift === 0n ? undefined : this.branch(key, shift);
321
+ if (cached !== undefined) {
322
+ node = cached;
323
+ continue;
324
+ }
234
325
  const slot = node + Number((key >> shift) & 15n) * 8;
235
326
  const bytes = await this.storage.read(slot, 8);
236
327
  const value = new DataView(bytes.buffer, bytes.byteOffset, 8).getBigUint64(0, true);
237
328
  if (shift === 0n)
238
329
  return value === 0n ? undefined : value - 1n;
239
330
  node = Number(value);
331
+ if (node)
332
+ this.branch(key, shift, node);
240
333
  }
241
334
  return undefined;
242
335
  }
@@ -245,6 +338,11 @@ export class IntegerTable {
245
338
  this.root = this.storage.allocate(128);
246
339
  let node = this.root;
247
340
  for (let shift = 60n; shift >= 0n; shift -= 4n) {
341
+ const cached = shift === 0n ? undefined : this.branch(key, shift);
342
+ if (cached !== undefined) {
343
+ node = cached;
344
+ continue;
345
+ }
248
346
  const slot = node + Number((key >> shift) & 15n) * 8;
249
347
  const bytes = await this.storage.read(slot, 8);
250
348
  const view = new DataView(bytes.buffer, bytes.byteOffset, 8);
@@ -260,6 +358,7 @@ export class IntegerTable {
260
358
  await this.storage.write(slot, bytes);
261
359
  }
262
360
  node = child;
361
+ this.branch(key, shift, child);
263
362
  }
264
363
  }
265
364
  }
@@ -0,0 +1,4 @@
1
+ export declare class XmlLimitError extends SyntaxError {
2
+ readonly limit: string;
3
+ constructor(limit: string, message: string);
4
+ }
@@ -0,0 +1,7 @@
1
+ export class XmlLimitError extends SyntaxError {
2
+ limit;
3
+ constructor(limit, message) {
4
+ super(message);
5
+ this.limit = limit;
6
+ }
7
+ }
@@ -1,3 +1,4 @@
1
+ export { XmlLimitError } from "./errors.js";
1
2
  export interface XmlName {
2
3
  readonly name: string;
3
4
  readonly namespace: string;
@@ -39,9 +40,6 @@ export interface XmlLimits {
39
40
  readonly maxTextLength?: number;
40
41
  readonly onElement?: (element: XmlName, parent: XmlName | undefined, depth: number) => void;
41
42
  }
42
- export declare class XmlLimitError extends SyntaxError {
43
- readonly limit: string;
44
- constructor(limit: string, message: string);
45
- }
46
43
  export declare function parseXmlSteps(input: string, limits?: XmlLimits): Generator<number, XmlElement, void>;
47
44
  export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
45
+ export { parseXmlStream, type XmlStreamLimits, type XmlStreamEvent } from "./stream.js";
@@ -1,10 +1,5 @@
1
- export class XmlLimitError extends SyntaxError {
2
- limit;
3
- constructor(limit, message) {
4
- super(message);
5
- this.limit = limit;
6
- }
7
- }
1
+ import { XmlLimitError } from "./errors.js";
2
+ export { XmlLimitError } from "./errors.js";
8
3
  function* find(source, needle, start) {
9
4
  const found = source.indexOf(needle, start);
10
5
  const end = found < 0 ? source.length : found;
@@ -633,3 +628,4 @@ export function parseXml(input, limits = {}) {
633
628
  result = parser.next();
634
629
  return result.value;
635
630
  }
631
+ export { parseXmlStream } from "./stream.js";
@@ -0,0 +1,31 @@
1
+ import type { XmlContent, XmlElement, XmlLimits, XmlName } from "./index.js";
2
+ export type XmlStreamEvent = {
3
+ readonly type: "open" | "close";
4
+ readonly element: XmlElement;
5
+ readonly parent: XmlElement | undefined;
6
+ } | {
7
+ readonly type: "content";
8
+ readonly content: Exclude<XmlContent, XmlElement>;
9
+ readonly parent: XmlElement | undefined;
10
+ };
11
+ export interface XmlStreamLimits extends XmlLimits {
12
+ /** Validate the full document but return only its root name, without a retained tree. */
13
+ readonly retainTree?: boolean;
14
+ /** Ordered node events, awaited after each parser window. Requires retainTree:
15
+ * false; elements carry metadata without child/content arrays. Individual XML
16
+ * tokens remain resident until the underlying parser completes them. */
17
+ readonly events?: (event: XmlStreamEvent) => void | Promise<void>;
18
+ /** Detach selected non-root subtrees and await their consumer before parsing
19
+ * the next 512-unit window. By default nested matches belong to the selected ancestor. */
20
+ readonly streamElements?: {
21
+ /** Also detach preceding sibling content when selecting a subtree, before
22
+ * the parser advances. The final trailing content remains in the parent. */
23
+ readonly captureBefore?: boolean;
24
+ /** Detach nested matches too, consuming descendants before their ancestors. */
25
+ readonly includeNested?: boolean;
26
+ readonly matches: (element: XmlName, parent: XmlName | undefined, depth: number) => boolean;
27
+ readonly consume: (element: XmlElement, parent: XmlElement, before?: readonly XmlContent[]) => void | Promise<void>;
28
+ };
29
+ }
30
+ /** Incremental source consumption. Individual XML tokens and optional trees remain resident. */
31
+ export declare function parseXmlStream(source: AsyncIterable<string> | Iterable<string>, limits?: XmlStreamLimits, checkpoint?: (units: number) => void | Promise<void>): Promise<XmlElement>;
@@ -0,0 +1,179 @@
1
+ import { SaxesParser } from "saxes/saxes.js";
2
+ import { XmlLimitError } from "./errors.js";
3
+ /** Incremental source consumption. Individual XML tokens and optional trees remain resident. */
4
+ export async function parseXmlStream(source, limits = {}, checkpoint) {
5
+ if (limits.recover)
6
+ throw new TypeError("XML stream recovery is unsupported; use parseXmlSteps");
7
+ for (const key of ["maxDepth", "maxNodes", "maxAttributes", "maxAttributesPerElement", "maxNamespaces", "maxContentNodes", "maxTextLength"]) {
8
+ const value = limits[key];
9
+ if (value !== undefined && value !== Infinity && (!Number.isSafeInteger(value) || value < 1))
10
+ throw new RangeError("XML limits must be positive integers");
11
+ }
12
+ const counters = { maxNodes: 0, maxAttributes: 0, maxContentNodes: 0, maxTextLength: 0 };
13
+ function bound(key, value) {
14
+ if (value > (limits[key] ?? Infinity))
15
+ throw new XmlLimitError(key, "XML resource limit exceeded");
16
+ }
17
+ function charge(key, value = 1) { counters[key] += value; bound(key, counters[key]); }
18
+ const retainContent = limits.retainContent !== false, tree = limits.retainTree !== false, retain = tree && retainContent;
19
+ const events = limits.events;
20
+ if (events && tree)
21
+ throw new TypeError("XML events require retainTree: false");
22
+ const describe = retain || !!events;
23
+ const queued = [];
24
+ const streaming = limits.streamElements;
25
+ if (streaming && !retain)
26
+ throw new TypeError("XML subtree streaming requires retained content and tree mode");
27
+ let selectedCount = 0;
28
+ const completed = [];
29
+ const stack = [];
30
+ const prolog = [], epilog = [];
31
+ const emptyNamespaces = new Map();
32
+ let root, declaration;
33
+ const parser = new SaxesParser({ xmlns: true, defaultXMLVersion: "1.0", forceXMLVersion: true });
34
+ parser.on("error", error => { throw new SyntaxError(`Invalid XML: ${error.message}`); });
35
+ parser.on("doctype", () => { throw new SyntaxError("Invalid XML: DTD and entity declarations are forbidden"); });
36
+ let prefix = "", capturing = describe;
37
+ parser.on("xmldecl", value => {
38
+ const encoding = value.encoding?.toUpperCase();
39
+ if (value.version !== "1.0" || encoding !== undefined &&
40
+ (!["UTF-8", "UTF-16", "UTF-16LE", "UTF-16BE"].includes(encoding) ||
41
+ limits.expectedEncoding !== undefined && encoding !== limits.expectedEncoding &&
42
+ !(encoding === "UTF-16" && ["UTF-16LE", "UTF-16BE"].includes(limits.expectedEncoding))))
43
+ throw new SyntaxError("Invalid XML: unsupported XML declaration");
44
+ if (describe)
45
+ declaration = prefix.slice(0, prefix.indexOf("?>") + 2);
46
+ prefix = "";
47
+ capturing = false;
48
+ });
49
+ parser.on("opentag", tag => {
50
+ charge("maxNodes");
51
+ bound("maxDepth", stack.length + 1);
52
+ const attrs = Object.values(tag.attributes);
53
+ charge("maxAttributes", attrs.length);
54
+ bound("maxAttributesPerElement", attrs.length);
55
+ const parent = stack.at(-1);
56
+ let namespaces = parent?.namespaces ?? new Map([["xml", "http://www.w3.org/XML/1998/namespace"]]);
57
+ const bindings = Object.entries(tag.ns);
58
+ if (bindings.length) {
59
+ namespaces = new Map(namespaces);
60
+ for (const [name, uri] of bindings)
61
+ namespaces.set(name, uri);
62
+ bound("maxNamespaces", namespaces.size);
63
+ }
64
+ for (const attr of attrs)
65
+ charge("maxTextLength", attr.value.length);
66
+ const name = { name: tag.name, namespace: tag.uri, localName: tag.local };
67
+ limits.onElement?.(name, parent?.element, stack.length + 1);
68
+ const selected = (!selectedCount || streaming?.includeNested === true) && !!streaming?.matches(name, parent?.element, stack.length + 1);
69
+ if (selected && !parent)
70
+ throw new TypeError("XML subtree streaming cannot select the root");
71
+ let before = [];
72
+ if (selected) {
73
+ selectedCount++;
74
+ if (streaming?.captureBefore) {
75
+ before = parent.element.content.splice(0);
76
+ parent.element.children.length = 0;
77
+ parent.element.text = '';
78
+ }
79
+ }
80
+ charge("maxContentNodes", 1 + (retainContent || events ? attrs.length : 0));
81
+ const element = { kind: "element", ...name, children: [], content: [], text: "",
82
+ attributes: describe ? attrs.map(attr => ({ name: attr.name, localName: attr.local, namespace: attr.uri, value: attr.value })) : [],
83
+ namespaces: describe ? namespaces : emptyNamespaces,
84
+ ...(!root && describe ? { prolog, epilog, ...(declaration === undefined ? {} : { declaration }) } : {}) };
85
+ if (parent && tree && !selected) {
86
+ parent.element.children.push(element);
87
+ if (retain)
88
+ parent.element.content.push(element);
89
+ }
90
+ else if (!parent)
91
+ root = element;
92
+ stack.push({ element, namespaces, selected, before });
93
+ if (events)
94
+ queued.push({ type: "open", element, parent: parent?.element });
95
+ });
96
+ parser.on("closetag", () => {
97
+ const frame = stack.pop();
98
+ if (events)
99
+ queued.push({ type: "close", element: frame.element, parent: stack.at(-1)?.element });
100
+ if (frame.selected) {
101
+ completed.push({ element: frame.element, parent: stack.at(-1).element, before: frame.before });
102
+ selectedCount--;
103
+ }
104
+ });
105
+ async function drain() {
106
+ for (const entry of completed) {
107
+ await checkpoint?.(0);
108
+ await streaming.consume(entry.element, entry.parent, entry.before);
109
+ }
110
+ completed.length = 0;
111
+ for (const event of queued) {
112
+ await checkpoint?.(0);
113
+ await events(event);
114
+ }
115
+ queued.length = 0;
116
+ }
117
+ function append(content) {
118
+ charge("maxTextLength", content.text.length);
119
+ charge("maxContentNodes");
120
+ const parent = stack.at(-1)?.element;
121
+ if (events)
122
+ queued.push({ type: "content", content, parent });
123
+ if (tree && parent && (content.kind === "text" || content.kind === "cdata"))
124
+ parent.text += content.text;
125
+ if (retain)
126
+ (parent ? parent.content : root ? epilog : prolog).push(content);
127
+ }
128
+ parser.on("text", text => { if (text.length)
129
+ append({ kind: "text", text }); });
130
+ parser.on("cdata", text => append({ kind: "cdata", text }));
131
+ parser.on("comment", text => append({ kind: "comment", text }));
132
+ parser.on("processinginstruction", value => append({ kind: "processing-instruction", target: value.target, text: value.body }));
133
+ let cr = false, first = true;
134
+ function write(text) {
135
+ if (!text)
136
+ return;
137
+ if (first) {
138
+ first = false;
139
+ if (text.charCodeAt(0) === 0xfeff) {
140
+ parser.write(text[0]);
141
+ text = text.slice(1);
142
+ }
143
+ }
144
+ if (cr) {
145
+ if (text.startsWith("\n"))
146
+ text = text.slice(1);
147
+ cr = false;
148
+ }
149
+ cr = text.endsWith("\r");
150
+ text = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
151
+ if (capturing) {
152
+ prefix += text;
153
+ if (prefix.length >= 6 && !(prefix.startsWith("<?xml") && " \t\n".includes(prefix[5]))) {
154
+ prefix = "";
155
+ capturing = false;
156
+ }
157
+ }
158
+ parser.write(text);
159
+ }
160
+ await checkpoint?.(0);
161
+ for await (const text of source) {
162
+ if (typeof text !== "string")
163
+ throw new TypeError("XML producer must yield strings");
164
+ for (let i = 0; i < text.length; i += 512) {
165
+ await checkpoint?.(0);
166
+ write(text.slice(i, i + 512));
167
+ if (completed.length || queued.length)
168
+ await drain();
169
+ await checkpoint?.(Math.min(512, text.length - i));
170
+ }
171
+ }
172
+ await checkpoint?.(0);
173
+ parser.close();
174
+ if (completed.length || queued.length)
175
+ await drain();
176
+ if (!root)
177
+ throw new SyntaxError("Invalid XML: incomplete document");
178
+ return root;
179
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.808",
3
+ "version": "0.1.809",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -148,6 +148,7 @@
148
148
  "access": "public"
149
149
  },
150
150
  "dependencies": {
151
+ "saxes": "^6.0.0",
151
152
  "@types/node": "^25.2.2",
152
153
  "@noble/hashes": "2.4.0"
153
154
  },