@poe-platform/safe-fs 0.1.804 → 0.1.806

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +5 -5
  2. package/dist/safe-fs/bridge/filesystem.d.ts +1 -0
  3. package/dist/safe-fs/bridge/filesystem.js +3 -0
  4. package/dist/safe-fs/bridge/index.d.ts +1 -0
  5. package/dist/safe-fs/bridge/index.js +1 -0
  6. package/dist/safe-fs/config/registry.d.ts +2 -0
  7. package/dist/safe-fs/config/registry.js +15 -0
  8. package/dist/safe-fs/config.node.js +3 -14
  9. package/dist/safe-fs/config.portable.d.ts +3 -0
  10. package/dist/safe-fs/config.portable.js +9 -0
  11. package/dist/safe-fs/contracts/errors.d.ts +1 -0
  12. package/dist/safe-fs/contracts/errors.js +12 -0
  13. package/dist/safe-fs/contracts/filesystem.d.ts +12 -0
  14. package/dist/safe-fs/contracts/index.d.ts +1 -0
  15. package/dist/safe-fs/contracts/index.js +1 -0
  16. package/dist/safe-fs/contracts/io.js +1 -1
  17. package/dist/safe-fs/core.d.ts +5 -14
  18. package/dist/safe-fs/core.js +4 -11
  19. package/dist/safe-fs/fs/devices/index.d.ts +8 -2
  20. package/dist/safe-fs/fs/devices/index.js +79 -23
  21. package/dist/safe-fs/fs/devices/path.js +1 -1
  22. package/dist/safe-fs/fs/memory/atomic-view.d.ts +3 -0
  23. package/dist/safe-fs/fs/memory/atomic-view.js +5 -0
  24. package/dist/safe-fs/fs/memory/index.d.ts +7 -4
  25. package/dist/safe-fs/fs/memory/index.js +211 -160
  26. package/dist/safe-fs/fs/memory/ledger.d.ts +0 -4
  27. package/dist/safe-fs/fs/memory/ledger.js +0 -15
  28. package/dist/safe-fs/fs/memory/limits.js +3 -0
  29. package/dist/safe-fs/fs/mount/comparison.js +3 -3
  30. package/dist/safe-fs/fs/mount/identity.d.ts +4 -0
  31. package/dist/safe-fs/fs/mount/identity.js +8 -0
  32. package/dist/safe-fs/fs/mount/index.d.ts +1 -0
  33. package/dist/safe-fs/fs/mount/index.js +31 -0
  34. package/dist/safe-fs/fs/object-publication/index.d.ts +3 -0
  35. package/dist/safe-fs/fs/object-publication/index.js +3 -2
  36. package/dist/safe-fs/fs/quota/index.js +1 -1
  37. package/dist/safe-fs/fs/readonly/index.js +1 -1
  38. package/dist/safe-fs/fs/s3/filesystem.js +1 -1
  39. package/dist/safe-fs/fs/s3/http/transport.js +1 -1
  40. package/dist/safe-fs/fs/s3/http/xml.js +2 -2
  41. package/dist/safe-fs/fs/scoped.js +32 -2
  42. package/dist/safe-fs/fs/webdav/webdav.js +1 -1
  43. package/dist/safe-fs/index.d.ts +2 -0
  44. package/dist/safe-fs/index.js +2 -0
  45. package/dist/safe-fs/node/filesystem.d.ts +2 -1
  46. package/dist/safe-fs/node/filesystem.js +1 -5
  47. package/dist/safe-fs/runtime-core.d.ts +27 -0
  48. package/dist/safe-fs/runtime-core.js +57 -0
  49. package/package.json +9 -1
package/README.md CHANGED
@@ -35,7 +35,7 @@ rename nor inode/version identity from placeholder native metadata. Retained
35
35
  reads remain available, with native seek reporting `ENOTSUP`. Ordinary browser
36
36
  bundles cannot import this backend. Node keeps its POSIX host profile.
37
37
 
38
- For host storage, use `await createRealFileSystem({ root: "/absolute/existing/directory" })` instead. The root must already exist; virtual `/` maps to that directory. ZIP creation and updates can use this adapter's private owned staging in an isolated host tree. Unzip extraction requires atomic ancestry verification, currently supported by MemoryFileSystem; the real adapter and mount views refuse it. Its `trustedOwnedStaging` capability checks original entries before publication and cleanup, and preserves foreign staging children. It does not advertise atomic conditional mutations: keep external writers and other in-flight writes away from the tree during these operations. Read the safety boundary below before exposing it to untrusted code.
38
+ For host storage, use `await createRealFileSystem({ root: "/absolute/existing/directory" })` instead. The root must already exist; virtual `/` maps to that directory. ZIP creation and updates can use this adapter's private owned staging in an isolated host tree. Archive extraction requires atomic ancestry verification. MemoryFileSystem supports it, including a writable root composed with read-only mounts such as `/skills/.system`; the real adapter refuses it. Mount extraction delegates confinement to the root backend and keeps other mounts read-only in the extraction view. Extraction roots inside a separate mount are rejected because directory and metadata mutations cannot atomically guard ancestors on another backend. Staged file publication supports nested writable mounts when every ancestor provides authoritative validation. Its `trustedOwnedStaging` capability checks original entries before publication and cleanup, and preserves foreign staging children. It does not advertise atomic conditional mutations: keep external writers and other in-flight writes away from the tree during these operations. Read the safety boundary below before exposing it to untrusted code.
39
39
 
40
40
  To create new outputs with Safe Bash's `dos2unix`, `unix2dos`, or compression commands, host adapters need atomic no-replace publication. Supply `createRealFileSystem({ root, renameNoReplace })` only when that callback binds a qualified native primitive such as Linux `renameat2` with `RENAME_NOREPLACE`. It receives resolved absolute host paths and an optional signal in its third argument. Existing destinations must reject `EEXIST` atomically; existence checks followed by rename and copy/delete are insufficient. Without this binding the adapter explicitly refuses those operations. See the [no-replace contract](src/contracts/filesystem.md#atomic-no-replace-rename).
41
41
 
@@ -148,7 +148,7 @@ Omit `maxOperations` or pass `Infinity` for unlimited cleanup operations.
148
148
  | `createOverlayFileSystem({ upper, lower })` | Reading through to a lower layer and writing changes to an upper layer |
149
149
  | `withFileSystemQuota(filesystem, { maxBytes })` | Enforcing a cumulative logical-byte ceiling across every write path |
150
150
 
151
- `createNodeFsBridge` offers a promises-shaped subset, including recursive `cp` and `mkdtemp`. A trusted `reserveReadFile` callback can reserve host resources and return a release function; cancellation retains the reservation until the backend read settles. The portable `createFsBridge` from `@poe-platform/safe-fs/core` instead requires a caller-supplied text codec and returns `Uint8Array` values.
151
+ `createNodeFsBridge` offers a promises-shaped subset, including recursive `cp` and `mkdtemp`. A trusted `reserveReadFile` callback can reserve host resources and return a release function; cancellation retains the reservation until the backend read settles. The portable `createFsBridge` from `@poe-platform/safe-fs/bridge` instead requires a caller-supplied text codec and returns `Uint8Array` values.
152
152
 
153
153
  ## Write an adapter
154
154
 
@@ -253,7 +253,7 @@ console.log(new TextDecoder().decode(await catalog.readFile("/welcome.txt")));
253
253
 
254
254
  Output: `Hello from the catalog`. `readFile` returns a copy so callers cannot mutate the stored snapshot. Reads check cancellation and `maxBytes`; required mutations always reject with `EROFS`. `stat` and `lstat` share an implementation because this adapter has no symlinks.
255
255
 
256
- `createFileSystem` validates options before calling the factory. The Node registry includes `memory` and `real`, accepts new names, and rejects attempts to replace those defaults. For an explicit allowlist without either default, pass `new Map([["catalog", catalogAdapter]])` directly as `registry`. Names select registered factories; they do not load modules from config.
256
+ `createFileSystem` validates options before calling the factory. For Workers, import `createPortableFileSystemAdapterRegistry` and `createFileSystem` from `@poe-code/safe-fs/core`; the portable registry includes `memory` and caller-supplied adapters, without ambient filesystem access. The Node registry includes `memory` and `real`, accepts new names, and rejects attempts to replace those defaults. For an explicit allowlist without either default, pass `new Map([["catalog", catalogAdapter]])` directly as `registry`. Names select registered factories; they do not load modules from config.
257
257
 
258
258
  For a mutable backend, preserve exclusive-create behavior, return independent byte buffers, handle cancellation and size limits, and declare only capabilities you actually provide. Use `FsError` for filesystem failures. Do not invent entry identity from matching paths or metadata; omit `compareEntry` when identity is unknown.
259
259
 
@@ -357,8 +357,8 @@ Each batch lists from the beginning because previous keys have been deleted; it
357
357
  | `addressingStyle` | `path`; alternative `virtual-hosted` requires a DNS endpoint |
358
358
  | `listUrlEncoding` | `percent`; alternative `form` |
359
359
  | `allowInsecureHttp` | `false`; HTTPS required unless explicitly enabled |
360
- | `maxPutBytes`, `maxGetBytes` | Unlimited unless configured |
361
- | `maxXmlBytes` | Unlimited unless configured |
360
+ | `maxPutBytes`, `maxGetBytes` | Unlimited when omitted or `Infinity`; positive safe integers set byte caps |
361
+ | `maxXmlBytes` | Unlimited when omitted or `Infinity`; positive safe integers set a byte cap |
362
362
  | `requestTimeoutMs` | Unlimited unless configured |
363
363
  | `enableCopy` | `true`; disabling uses a buffered GET/PUT fallback |
364
364
  | `verifiedConditionalOperations` | Optional `put`, `copy`, `delete` booleans, each defaulting to false; enable only after verifying the server's semantics |
@@ -12,6 +12,7 @@ type DirectoryOptions = {
12
12
  };
13
13
  export declare class FileSystemBridge<Binary extends Uint8Array> {
14
14
  #private;
15
+ static getProvider(bridge: object): FileSystem | undefined;
15
16
  constructor(fs: FileSystem, options: {
16
17
  readonly cwd?: string;
17
18
  readonly root?: string;
@@ -40,6 +40,9 @@ function timeValue(value) {
40
40
  return number;
41
41
  }
42
42
  export class FileSystemBridge {
43
+ static getProvider(bridge) {
44
+ return #fs in bridge ? bridge.#fs : undefined;
45
+ }
43
46
  #fs;
44
47
  #cwd;
45
48
  #root;
@@ -4,3 +4,4 @@ import type { FsBridgeOptions } from "./types.js";
4
4
  export type { FsBridgeCodec, FsBridgeDirent, FsBridgeEncoding, FsBridgeFileSystem, FsBridgeOptions, FsBridgeStats } from "./types.js";
5
5
  export type FsBridge = FileSystemBridge<Uint8Array<ArrayBuffer>>;
6
6
  export declare function createFsBridge(fs: FileSystem, options: FsBridgeOptions): FsBridge;
7
+ export declare const getFsBridgeProvider: typeof FileSystemBridge.getProvider;
@@ -16,3 +16,4 @@ export function createFsBridge(fs, options) {
16
16
  }
17
17
  });
18
18
  }
19
+ export const getFsBridgeProvider = FileSystemBridge.getProvider;
@@ -0,0 +1,2 @@
1
+ import type { FileSystemAdapterRegistry } from "../config.js";
2
+ export declare function extendAdapterRegistry(defaults: FileSystemAdapterRegistry, extensions?: FileSystemAdapterRegistry): FileSystemAdapterRegistry;
@@ -0,0 +1,15 @@
1
+ export function extendAdapterRegistry(defaults, extensions) {
2
+ if (extensions !== undefined &&
3
+ (extensions === null ||
4
+ typeof extensions.get !== "function" ||
5
+ typeof extensions[Symbol.iterator] !== "function")) {
6
+ throw new TypeError("registry must be a filesystem adapter map.");
7
+ }
8
+ const registry = new Map(defaults);
9
+ for (const [name, descriptor] of extensions ?? []) {
10
+ if (registry.has(name))
11
+ throw new TypeError(`Filesystem adapter already registered: ${name}`);
12
+ registry.set(name, descriptor);
13
+ }
14
+ return registry;
15
+ }
@@ -1,22 +1,11 @@
1
+ import { extendAdapterRegistry } from "./config/registry.js";
1
2
  import { createMemoryFileSystemAdapter } from "./config/memory.js";
2
3
  import { createRealFileSystemAdapter } from "./config/real.js";
3
4
  import { createMemoryFileSystem } from "./fs/memory/index.js";
4
5
  import { createRealFileSystem } from "./fs/real/index.js";
5
6
  export function createNodeFileSystemAdapterRegistry(extensions) {
6
- if (extensions !== undefined &&
7
- (extensions === null ||
8
- typeof extensions.get !== "function" ||
9
- typeof extensions[Symbol.iterator] !== "function")) {
10
- throw new TypeError("registry must be a filesystem adapter map.");
11
- }
12
- const registry = new Map([
7
+ return extendAdapterRegistry(new Map([
13
8
  ["memory", createMemoryFileSystemAdapter(createMemoryFileSystem)],
14
9
  ["real", createRealFileSystemAdapter(createRealFileSystem)]
15
- ]);
16
- for (const [name, descriptor] of extensions ?? []) {
17
- if (registry.has(name))
18
- throw new TypeError(`Filesystem adapter already registered: ${name}`);
19
- registry.set(name, descriptor);
20
- }
21
- return registry;
10
+ ]), extensions);
22
11
  }
@@ -0,0 +1,3 @@
1
+ import type { FileSystemAdapterRegistry } from "./config.js";
2
+ /** Portable defaults never grant access to the host filesystem. */
3
+ export declare function createPortableFileSystemAdapterRegistry(extensions?: FileSystemAdapterRegistry): FileSystemAdapterRegistry;
@@ -0,0 +1,9 @@
1
+ import { createMemoryFileSystemAdapter } from "./config/memory.js";
2
+ import { extendAdapterRegistry } from "./config/registry.js";
3
+ import { createMemoryFileSystem } from "./fs/memory/index.js";
4
+ /** Portable defaults never grant access to the host filesystem. */
5
+ export function createPortableFileSystemAdapterRegistry(extensions) {
6
+ return extendAdapterRegistry(new Map([
7
+ ["memory", createMemoryFileSystemAdapter(createMemoryFileSystem)]
8
+ ]), extensions);
9
+ }
@@ -40,6 +40,7 @@ export interface FsErrorOptions extends ErrorOptions {
40
40
  readonly message?: string;
41
41
  }
42
42
  export declare class FsError extends Error {
43
+ static [Symbol.hasInstance](value: unknown): boolean;
43
44
  readonly code: ErrnoCode;
44
45
  readonly errno: PlatformErrno;
45
46
  readonly syscall?: string;
@@ -31,7 +31,18 @@ const descriptions = {
31
31
  ETIMEDOUT: "operation timed out",
32
32
  EXDEV: "cross-device link not permitted",
33
33
  };
34
+ const FS_ERROR_BRAND = Symbol.for("@poe-code/safe-fs.FsError");
34
35
  export class FsError extends Error {
36
+ static [Symbol.hasInstance](value) {
37
+ if (typeof value !== "object" || value === null)
38
+ return false;
39
+ if (this !== FsError)
40
+ return Function.prototype[Symbol.hasInstance].call(this, value);
41
+ if (Function.prototype[Symbol.hasInstance].call(FsError, value))
42
+ return true;
43
+ const record = value;
44
+ return record[FS_ERROR_BRAND] === true && isErrnoCode(record.code);
45
+ }
35
46
  code;
36
47
  errno;
37
48
  syscall;
@@ -53,6 +64,7 @@ export class FsError extends Error {
53
64
  finally {
54
65
  errorConstructor.stackTraceLimit = prevStackLimit;
55
66
  }
67
+ Object.defineProperty(this, FS_ERROR_BRAND, { value: true, enumerable: false, configurable: false, writable: false });
56
68
  this.name = "FsError";
57
69
  this.code = code;
58
70
  this.errno = errno;
@@ -284,6 +284,17 @@ export interface PublishStagedFileOptions extends FsOptions {
284
284
  readonly parent: FileStat;
285
285
  readonly destination: FileStat | null;
286
286
  }
287
+ /** Publish one staged regular file and validate/retire sibling source entries in
288
+ * the same indivisible commit. Every expected version (including absence) must
289
+ * match before any mutation. Hosts must not emulate this with sequential calls.
290
+ * Unavailable hosts omit publishStagedFileSet. Sources remain caller-owned. */
291
+ export interface PublishStagedFileSetOptions extends PublishStagedFileOptions {
292
+ readonly companions: readonly {
293
+ readonly path: string;
294
+ readonly expected: FileStat | null;
295
+ readonly remove: boolean;
296
+ }[];
297
+ }
287
298
  export interface PrepareDirectoryOptions extends FsOptions {
288
299
  readonly expected: FileStat | null;
289
300
  readonly parent: FileStat;
@@ -321,6 +332,7 @@ export interface FileSystem {
321
332
  prepareDirectory?(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;
322
333
  createStagedFile?(directoryPath: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
323
334
  publishStagedFile?(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
335
+ publishStagedFileSet?(staging: FileStaging, destination: string, options: PublishStagedFileSetOptions): Promise<FileStat>;
324
336
  /** Admit a complete canonical root-to-directory prefix and prepare a guard.
325
337
  * The returned guard checks current identities/search access synchronously;
326
338
  * preparation is not validation, and the guard's result is not an async lease. */
@@ -3,3 +3,4 @@ export * from "./filesystem.js";
3
3
  export * from "./io.js";
4
4
  export * from "./path.js";
5
5
  export * from "./object.js";
6
+ export { compareIdentity, compareFileVersion } from "../fs/mount/identity.js";
@@ -3,3 +3,4 @@ export * from "./filesystem.js";
3
3
  export * from "./io.js";
4
4
  export * from "./path.js";
5
5
  export * from "./object.js";
6
+ export { compareIdentity, compareFileVersion } from "../fs/mount/identity.js";
@@ -211,7 +211,7 @@ class ReadBytesGenerator {
211
211
  if (!this.finished && this.iterator?.return) {
212
212
  this.finished = true;
213
213
  const it = this.iterator;
214
- const cleanup = Promise.resolve().then(() => it.return());
214
+ const cleanup = new Promise(resolve => { resolve(it.return()); });
215
215
  if (signal?.aborted)
216
216
  void cleanup.catch(noop);
217
217
  else
@@ -1,28 +1,19 @@
1
- export * from "./contracts/errors.js";
2
- export * from "./contracts/filesystem.js";
3
- export * from "./contracts/io.js";
4
- export { openFileDescriptor } from "./fs/descriptor.js";
5
- export type { DescriptorBackend, DescriptorOpenOptions } from "./fs/descriptor.js";
6
- export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./contracts/virtual-path.js";
7
- export { basename, dirname, extname, isAbsolutePath, joinPath, posixPath } from "./contracts/portable-path.js";
8
- export * from "./fs/memory/index.js";
9
- export * from "./fs/devices/index.js";
1
+ export * from "./runtime-core.js";
2
+ export { MemoryFileSystem, MemoryRedirectHandle, createMemoryFileSystem, defaultMemoryFileSystemLimits, bindConditionalMutation, getLastReadMemoryFileSourceRef, isCleanAbsolutePath, tryGetMemoryDirectoryEntryNamesSync, tryMkdirMemorySync, tryOpenMemoryRedirectHandleSync, tryReadMemoryFileViewSync, tryResolveMemoryDevicePath, tryRmRfMemorySync, tryWriteMemoryFileInDirSync, tryWriteMemoryFileSync, utf8ByteLength } from "./fs/memory/index.js";
10
3
  export * from "./fs/readonly/index.js";
11
4
  export * from "./fs/mount/index.js";
12
5
  export * from "./fs/overlay/index.js";
13
6
  export * from "./fs/quota/index.js";
14
7
  export * from "./fs/object-publication/index.js";
15
- export { retargetScopedFileSystem, scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
16
- export type { RetainedFileSystemCleanupView, RetainedFileSystemCleanupOptions } from "./fs/scoped.js";
17
8
  export * from "./fs/webdav/index.js";
18
9
  export * from "./bridge/index.js";
19
10
  export * from "./python/index.js";
20
- export { compareEntries, registerEntryView } from "./fs/mount/comparison.js";
21
- export type { EntryViewResolver } from "./fs/mount/comparison.js";
22
11
  export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
23
12
  export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
24
13
  export * from "./contracts/object.js";
25
14
  export { ObjectAuthority } from "./fs/object-authority.js";
26
- export { bindConditionalMutation, type ConditionalMutationBinding } from "./fs/memory/index.js";
27
15
  export * from "./fs/s3/index.js";
28
16
  export * from "./fs/s3/http/index.js";
17
+ export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
18
+ export type { FileSystemConfig, FileSystemAdapterDescriptor, FileSystemAdapterRegistry } from "./config.js";
19
+ export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
@@ -1,24 +1,17 @@
1
- export * from "./contracts/errors.js";
2
- export * from "./contracts/filesystem.js";
3
- export * from "./contracts/io.js";
4
- export { openFileDescriptor } from "./fs/descriptor.js";
5
- export { assertPathWithin, isPathWithin, normalizePath, relativePath, resolvePath, validatePath } from "./contracts/virtual-path.js";
6
- export { basename, dirname, extname, isAbsolutePath, joinPath, posixPath } from "./contracts/portable-path.js";
7
- export * from "./fs/memory/index.js";
8
- export * from "./fs/devices/index.js";
1
+ export * from "./runtime-core.js";
2
+ export { MemoryFileSystem, MemoryRedirectHandle, createMemoryFileSystem, defaultMemoryFileSystemLimits, bindConditionalMutation, getLastReadMemoryFileSourceRef, isCleanAbsolutePath, tryGetMemoryDirectoryEntryNamesSync, tryMkdirMemorySync, tryOpenMemoryRedirectHandleSync, tryReadMemoryFileViewSync, tryResolveMemoryDevicePath, tryRmRfMemorySync, tryWriteMemoryFileInDirSync, tryWriteMemoryFileSync, utf8ByteLength } from "./fs/memory/index.js";
9
3
  export * from "./fs/readonly/index.js";
10
4
  export * from "./fs/mount/index.js";
11
5
  export * from "./fs/overlay/index.js";
12
6
  export * from "./fs/quota/index.js";
13
7
  export * from "./fs/object-publication/index.js";
14
- export { retargetScopedFileSystem, scopeFileSystem, retainFileSystemCleanup } from "./fs/scoped.js";
15
8
  export * from "./fs/webdav/index.js";
16
9
  export * from "./bridge/index.js";
17
10
  export * from "./python/index.js";
18
- export { compareEntries, registerEntryView } from "./fs/mount/comparison.js";
19
11
  export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
20
12
  export * from "./contracts/object.js";
21
13
  export { ObjectAuthority } from "./fs/object-authority.js";
22
- export { bindConditionalMutation } from "./fs/memory/index.js";
23
14
  export * from "./fs/s3/index.js";
24
15
  export * from "./fs/s3/http/index.js";
16
+ export { createFileSystem, readConfigRecord, validateFileSystemConfig } from "./config.js";
17
+ export { createPortableFileSystemAdapterRegistry } from "./config.portable.js";
@@ -1,15 +1,20 @@
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, 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, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
3
3
  import type { ByteSource } from "../../contracts/io.js";
4
+ import { type EntryLocation } from "../mount/comparison.js";
5
+ import { pathNamespace } from "../path-namespace.js";
4
6
  export declare class DeviceFileSystem implements FileSystem {
5
7
  #private;
6
8
  readonly capabilities: FileSystemCapabilities;
9
+ get [pathNamespace](): any;
10
+ _resolveEntryView: (path: string, options: FsOptions) => Promise<EntryLocation>;
11
+ _entryAuthority: () => Promise<"unknown">;
7
12
  constructor(filesystem: FileSystem);
8
13
  canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
9
14
  capabilitiesFor(path: string, options?: CapabilityQueryOptions): Promise<FileSystemCapabilities>;
10
15
  stat(path: string, options?: FsOptions): Promise<FileStat>;
11
16
  lstat(path: string, options?: FsOptions): Promise<FileStat>;
12
- compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../contracts/filesystem.js").EntryComparison>;
17
+ compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<import("../../runtime-core.js").EntryComparison>;
13
18
  realpath(path: string, options?: FsOptions): Promise<string>;
14
19
  readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array>;
15
20
  readStream(path: string, options?: ReadStreamOptions): ByteSource;
@@ -35,6 +40,7 @@ export declare class DeviceFileSystem implements FileSystem {
35
40
  removeTreeConditional(path: string, options: ConditionalRemoveEntryOptions): Promise<void>;
36
41
  createStagedFile(path: string, name: string, content: StagedFileContent, options: CreateStagedFileOptions): Promise<FileStaging>;
37
42
  publishStagedFile(staging: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
43
+ publishStagedFileSet(receipt: FileStaging, destination: string, supplied: PublishStagedFileSetOptions): Promise<FileStat>;
38
44
  prepareDirectoryAncestry(ancestors: readonly FileStagingEntry[], options?: FsOptions): Promise<() => true>;
39
45
  prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
40
46
  removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
@@ -2,9 +2,9 @@ import { snapshotStagingCreation } from "../staging-cleanup.js";
2
2
  import { FsError, isFsError } from "../../contracts/errors.js";
3
3
  import { finishCleanup } from "../../contracts/cleanup.js";
4
4
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
5
- import { compareEntries, registerEntryAuthority, registerEntryView } from "../mount/comparison.js";
5
+ import { compareEntries } from "../mount/comparison.js";
6
6
  import { deviceDirectory, lexicalDevicePath, nullPath, resolveDevicePath } from "./path.js";
7
- import { tryResolveMemoryDevicePath } from "../memory/index.js";
7
+ import { tryResolveMemoryDevicePath } from "../../runtime-core.js";
8
8
  import { createDeviceYield, deviceReadStream, drainDeviceFile, drainDeviceInput } from "./stream.js";
9
9
  import { openRetainedReadFile, openRetainedResizeFile, retainedResizeCapabilities, ownedMutationCapabilities, requireOwnedMutation } from "../capabilities.js";
10
10
  import { pathNamespace } from "../path-namespace.js";
@@ -71,21 +71,46 @@ function nonnegative(value, path) {
71
71
  if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
72
72
  throw new FsError("EINVAL", { path });
73
73
  }
74
+ const sharedUnknownEntryAuthority = async () => "unknown";
74
75
  export class DeviceFileSystem {
75
76
  #filesystem;
76
77
  capabilities;
77
- #nullStat;
78
- #directoryStat;
78
+ #identityScopeObj;
79
+ #nullStatObj;
80
+ #directoryStatObj;
81
+ get [pathNamespace]() {
82
+ return Reflect.get(this.#filesystem, pathNamespace);
83
+ }
84
+ get #nullStat() {
85
+ if (this.#nullStatObj)
86
+ return this.#nullStatObj;
87
+ const identityScope = (this.#identityScopeObj ??= Object.freeze({}));
88
+ return (this.#nullStatObj = Object.freeze({
89
+ type: "character", mode: 0o020666, size: 0, allocatedBytes: 0, uid: 0, gid: 0,
90
+ mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 1, dev: 0, nlink: 1, preferredIoBlockSize: 4096,
91
+ }));
92
+ }
93
+ get #directoryStat() {
94
+ if (this.#directoryStatObj)
95
+ return this.#directoryStatObj;
96
+ const identityScope = (this.#identityScopeObj ??= Object.freeze({}));
97
+ return (this.#directoryStatObj = Object.freeze({
98
+ type: "directory", mode: 0o040755, size: 0, allocatedBytes: 0, uid: 0, gid: 0,
99
+ mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 2, dev: 0, nlink: 1,
100
+ }));
101
+ }
102
+ _resolveEntryView = async (path, options) => {
103
+ const resolved = await this.#resolve(path, options);
104
+ if (resolved === nullPath || resolved === deviceDirectory) {
105
+ return { filesystem: this, path: resolved, stat: resolved === nullPath ? this.#nullStat : this.#directoryStat, readOnly: false };
106
+ }
107
+ return { filesystem: this.#filesystem, path };
108
+ };
109
+ _entryAuthority = sharedUnknownEntryAuthority;
79
110
  constructor(filesystem) {
80
111
  this.#filesystem = filesystem;
81
- Object.defineProperty(this, pathNamespace, { get: () => Reflect.get(filesystem, pathNamespace) });
82
112
  this.capabilities = globalCapabilities(filesystem);
83
- const identityScope = Object.freeze({});
84
- const stat = { mode: 0o020666, size: 0, allocatedBytes: 0, uid: 0, gid: 0,
85
- mtimeMs: 0, atimeMs: 0, ctimeMs: 0, birthtimeMs: 0, identityScope, ino: 1, dev: 0, nlink: 1 };
86
- this.#nullStat = Object.freeze({ ...stat, type: "character", preferredIoBlockSize: 4096 });
87
- this.#directoryStat = Object.freeze({ ...stat, type: "directory", mode: 0o040755, ino: 2 });
88
- const methods = new Map();
113
+ let methods;
89
114
  const view = new Proxy(this, {
90
115
  set: (_target, property, value) => Reflect.set(filesystem, property, value, filesystem),
91
116
  get(target, property) {
@@ -93,24 +118,14 @@ export class DeviceFileSystem {
93
118
  if (typeof value !== "function" || property === "constructor")
94
119
  return value;
95
120
  const backing = Reflect.get(filesystem, property, filesystem);
96
- const cached = methods.get(property);
121
+ const cached = methods?.get(property);
97
122
  if (cached && cached.backing === backing)
98
123
  return cached.bound;
99
124
  const bound = value.bind(target);
100
- methods.set(property, { backing, bound });
125
+ (methods ??= new Map()).set(property, { backing, bound });
101
126
  return bound;
102
127
  },
103
128
  });
104
- for (const identity of [this, view])
105
- registerEntryView(identity, async (path, options) => {
106
- const resolved = await this.#resolve(path, options);
107
- if (resolved === nullPath || resolved === deviceDirectory)
108
- return { filesystem: this, path: resolved,
109
- stat: resolved === nullPath ? this.#nullStat : this.#directoryStat, readOnly: false };
110
- return { filesystem, path };
111
- });
112
- // This internal view has no identity proof beyond its synthetic stats.
113
- registerEntryAuthority(this, async () => "unknown");
114
129
  return view;
115
130
  }
116
131
  #resolve(path, options, followFinal = true, resizeCreate, traversal) {
@@ -624,6 +639,47 @@ export class DeviceFileSystem {
624
639
  await requireOwnedMutation(this.#filesystem, destination, "guardedStagingPublication", options, options.destination === null);
625
640
  await this.#filesystem.publishStagedFile(staging, destination, options);
626
641
  }
642
+ async publishStagedFileSet(receipt, destination, supplied) {
643
+ const publish = this.#filesystem.publishStagedFileSet;
644
+ if (!publish)
645
+ throw new FsError("ENOTSUP", { path: destination });
646
+ const entry = (value) => ({ path: value.path, stat: { ...value.stat } });
647
+ const staging = { ...receipt, parent: entry(receipt.parent), directory: entry(receipt.directory), file: entry(receipt.file) };
648
+ const options = {
649
+ ...supplied,
650
+ parent: { ...supplied.parent },
651
+ destination: supplied.destination === null ? null : { ...supplied.destination },
652
+ companions: supplied.companions.map(value => ({ ...value, expected: value.expected === null ? null : { ...value.expected } })),
653
+ ...(supplied.ancestors === undefined ? {} : { ancestors: snapshotDirectoryAncestry(supplied.ancestors) }),
654
+ };
655
+ options.signal?.throwIfAborted();
656
+ const capabilities = await this.#filesystem.capabilitiesFor?.(destination, { ...options, create: options.destination === null }) ?? this.#filesystem.capabilities;
657
+ if (capabilities.readOnly === true)
658
+ throw new FsError("EROFS", { path: destination });
659
+ for (const path of [staging.parent.path, staging.directory.path])
660
+ await this.#mutable(path, options, false);
661
+ const guards = [];
662
+ if (options.ancestors)
663
+ guards.push(await this.prepareDirectoryAncestry(options.ancestors, options));
664
+ for (const path of [staging.file.path, destination, ...options.companions.map(value => value.path)]) {
665
+ await this.#mutable(path, options, false);
666
+ const resolution = await this.prepareStagingResolution(path, options);
667
+ guards.push(resolution.validate);
668
+ }
669
+ const callerGuard = options.commitGuard;
670
+ options.signal?.throwIfAborted();
671
+ return publish.call(this.#filesystem, staging, destination, { ...options, commitGuard: () => {
672
+ options.signal?.throwIfAborted();
673
+ if (callerGuard !== undefined)
674
+ runStagingGuard(callerGuard);
675
+ // Recheck every route in the backend's atomic commit, so a source cannot
676
+ // cross into the virtual device namespace after asynchronous admission.
677
+ for (const guard of guards)
678
+ runStagingGuard(guard);
679
+ options.signal?.throwIfAborted();
680
+ return true;
681
+ } });
682
+ }
627
683
  async prepareDirectoryAncestry(ancestors, options = {}) {
628
684
  options.signal?.throwIfAborted();
629
685
  try {
@@ -1,7 +1,7 @@
1
1
  import { FsError, isFsError } from "../../contracts/errors.js";
2
2
  import { pathByteLength, validatePath } from "../../contracts/virtual-path.js";
3
3
  import { capturePathNamespace, pathNamespace } from "../path-namespace.js";
4
- import { isCleanAbsolutePath, tryResolveMemoryDevicePath } from "../memory/index.js";
4
+ import { isCleanAbsolutePath, tryResolveMemoryDevicePath } from "../../runtime-core.js";
5
5
  export const nullPath = "/dev/null";
6
6
  export const deviceDirectory = "/dev";
7
7
  function isNullParts(parts) {
@@ -4,5 +4,8 @@ export interface MemoryAtomicView {
4
4
  stat(path: string): FileStat | undefined;
5
5
  names(path: string): readonly string[];
6
6
  }
7
+ type LazyMemoryAtomicView = (this: FileSystem) => MemoryAtomicView | undefined;
7
8
  export declare function registerMemoryAtomicView(fs: FileSystem, view: MemoryAtomicView, intact: () => boolean): void;
9
+ export declare function registerLazyMemoryAtomicView(fs: FileSystem, create: LazyMemoryAtomicView): void;
8
10
  export declare function memoryAtomicView(fs: FileSystem): MemoryAtomicView | undefined;
11
+ export {};
@@ -2,7 +2,12 @@ const views = new WeakMap();
2
2
  export function registerMemoryAtomicView(fs, view, intact) {
3
3
  views.set(fs, { view, intact });
4
4
  }
5
+ export function registerLazyMemoryAtomicView(fs, create) {
6
+ views.set(fs, create);
7
+ }
5
8
  export function memoryAtomicView(fs) {
6
9
  const registered = views.get(fs);
10
+ if (typeof registered === "function")
11
+ return registered.call(fs);
7
12
  return registered?.intact() ? registered.view : undefined;
8
13
  }
@@ -1,5 +1,6 @@
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, 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, PublishStagedFileOptions, PublishStagedFileSetOptions, PrepareDirectoryOptions, StagedFileContent } from "../../contracts/filesystem.js";
2
2
  import type { ByteSource } from "../../contracts/io.js";
3
+ import { type MemoryAtomicView } from "./atomic-view.js";
3
4
  import { MemoryAllocation, MemoryLedger } from "./ledger.js";
4
5
  import { type MemoryFileSystemOptions } from "./limits.js";
5
6
  export { defaultMemoryFileSystemLimits, type MemoryFileSystemLimits, type MemoryFileSystemOptions } from "./limits.js";
@@ -34,7 +35,6 @@ interface SymlinkNode extends Metadata {
34
35
  }
35
36
  type MemoryNode = FileNode | DirectoryNode | SymlinkNode;
36
37
  declare class MemoryCache {
37
- readonly allocations: MemoryAllocation[];
38
38
  readonly files: FileNode[];
39
39
  readonly directories: DirectoryNode[];
40
40
  readonly removedScratch: MemoryNode[];
@@ -59,9 +59,10 @@ export interface ConditionalMutationBinding {
59
59
  }
60
60
  export declare function bindConditionalMutation<Result>(identityScope: object | symbol | undefined, binding: ConditionalMutationBinding, action: () => Promise<Result>): Promise<Result>;
61
61
  export declare class MemoryFileSystem implements FileSystem {
62
+ #private;
62
63
  capabilitiesFor?: NonNullable<FileSystem["capabilitiesFor"]>;
63
- readonly capabilities: FileSystemCapabilities;
64
- private readonly identityScope;
64
+ private get identityScope();
65
+ get capabilities(): FileSystemCapabilities;
65
66
  private nextInode;
66
67
  private readonly ledger;
67
68
  readonly _cache: MemoryCache;
@@ -70,6 +71,7 @@ export declare class MemoryFileSystem implements FileSystem {
70
71
  private readonly root;
71
72
  private totalBytes;
72
73
  symlinkCount: number;
74
+ _getAtomicView(): MemoryAtomicView | undefined;
73
75
  constructor(options?: MemoryFileSystemOptions);
74
76
  compareEntry(path: string, peer: FileSystem, peerPath: string, options?: FsOptions): Promise<EntryComparison>;
75
77
  canonicalizeMissingTarget(path: string, options?: FsOptions): string | undefined;
@@ -115,6 +117,7 @@ export declare class MemoryFileSystem implements FileSystem {
115
117
  prepareStagingResolution(path: string, options?: FsOptions): Promise<FileStagingResolution>;
116
118
  private verifyDirectoryAncestry;
117
119
  publishStagedFile(receipt: FileStaging, destination: string, options: PublishStagedFileOptions): Promise<void>;
120
+ publishStagedFileSet(receipt: FileStaging, destination: string, options: PublishStagedFileSetOptions): Promise<FileStat>;
118
121
  removeStagedFile(staging: FileStaging, options?: FsOptions): Promise<void>;
119
122
  private removeStagingLocations;
120
123
  prepareDirectory(path: string, options: PrepareDirectoryOptions): Promise<FileStat>;