@poe-platform/safe-fs 0.1.716 → 0.1.718

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.
@@ -8,6 +8,7 @@ declare const descriptions: {
8
8
  readonly ECANCELED: "operation canceled";
9
9
  readonly EEXIST: "file already exists";
10
10
  readonly EFBIG: "file too large";
11
+ readonly EILSEQ: "invalid or incomplete multibyte or wide character";
11
12
  readonly EINTR: "interrupted system call";
12
13
  readonly EINVAL: "invalid argument";
13
14
  readonly EIO: "input/output error";
@@ -7,6 +7,7 @@ const descriptions = {
7
7
  ECANCELED: "operation canceled",
8
8
  EEXIST: "file already exists",
9
9
  EFBIG: "file too large",
10
+ EILSEQ: "invalid or incomplete multibyte or wide character",
10
11
  EINTR: "interrupted system call",
11
12
  EINVAL: "invalid argument",
12
13
  EIO: "input/output error",
@@ -1,3 +1,4 @@
1
+ import type { ExactFileReadHandle, ExactFileResizeHandle, ObjectFileSystem } from "./object.js";
1
2
  import type { ByteSource } from "./io.js";
2
3
  import type { FileDescriptor, OpenFileOptions } from "./descriptor.js";
3
4
  export type { FileDescriptor, FileDescriptorCapabilities, OpenFileOptions } from "./descriptor.js";
@@ -90,6 +91,8 @@ export interface CapabilityQueryOptions extends OpenReadFileOptions {
90
91
  readonly create?: boolean;
91
92
  }
92
93
  export interface FileReadHandle {
94
+ /** Exact operations on the same retained object; close owns both facets. */
95
+ readonly exact?: ExactFileReadHandle;
93
96
  stat(options?: FsOptions): Promise<FileStat>;
94
97
  read(position: number, maxBytes: number, options?: FsOptions): Promise<Uint8Array>;
95
98
  seekEnd?: ((options?: FsOptions) => Promise<bigint>) | undefined;
@@ -116,6 +119,8 @@ export interface FileResizeOptions extends FsOptions {
116
119
  readonly mode?: number;
117
120
  }
118
121
  export interface FileResizeHandle {
122
+ /** Exact operations on the same retained object; close owns both facets. */
123
+ readonly exact?: ExactFileResizeHandle;
119
124
  stat(options?: FsOptions): Promise<FileStat>;
120
125
  truncate(length: number, options?: FsOptions): Promise<void>;
121
126
  seekEnd?: ((options?: FsOptions) => Promise<bigint>) | undefined;
@@ -212,6 +217,8 @@ export interface ConditionalFilePublicationOptions extends FsOptions {
212
217
  readonly mtimeMs?: number;
213
218
  }
214
219
  export interface FileSystem {
220
+ /** Explicit retained-object capability for qualified byte-path backends. */
221
+ readonly objects?: ObjectFileSystem;
215
222
  /** Consume the complete source privately, then atomically compare/publish.
216
223
  * Failures before commit preserve the destination. No stat/write fallback. */
217
224
  publishFileConditional?(path: string, source: ByteSource, options: ConditionalFilePublicationOptions): Promise<FileStat>;
@@ -2,3 +2,4 @@ export * from "./errors.js";
2
2
  export * from "./filesystem.js";
3
3
  export * from "./io.js";
4
4
  export * from "./path.js";
5
+ export * from "./object.js";
@@ -2,3 +2,4 @@ export * from "./errors.js";
2
2
  export * from "./filesystem.js";
3
3
  export * from "./io.js";
4
4
  export * from "./path.js";
5
+ export * from "./object.js";
@@ -0,0 +1,134 @@
1
+ import type { FileType, FsOptions, ReadDirectoryOptions } from "./filesystem.js";
2
+ /** Owned Unix path bytes. No decoding, normalization, or namespace authorization. */
3
+ export declare class BytePath {
4
+ #private;
5
+ constructor(value: Uint8Array);
6
+ bytes(): Uint8Array;
7
+ }
8
+ /** JSON representation: octets, never a UTF-8 replacement string. */
9
+ export declare function encodeBytePath(path: BytePath): number[];
10
+ export declare function decodeBytePath(value: unknown): BytePath;
11
+ /** Admit legacy numbers before conversion; native off_t profile is signed 64-bit. */
12
+ export declare function fileOffset(value: number | bigint): bigint;
13
+ export declare function encodeFileOffset(value: bigint): string;
14
+ export declare function decodeFileOffset(value: unknown): bigint;
15
+ export type SpecialFileType = "character" | "fifo" | "socket";
16
+ export type ObjectFileType = FileType | SpecialFileType;
17
+ export interface ExactFileStat {
18
+ readonly type: ObjectFileType;
19
+ readonly size: bigint;
20
+ readonly allocatedBytes?: bigint;
21
+ /** Observed link count, including zero for an unlinked retained object; not identity. */
22
+ readonly nlink?: bigint;
23
+ readonly mode?: number;
24
+ readonly uid?: number;
25
+ readonly gid?: number;
26
+ readonly atimeNs?: bigint;
27
+ readonly mtimeNs?: bigint;
28
+ readonly ctimeNs?: bigint;
29
+ }
30
+ /** Metadata updates affect the retained object. No implicit recursive/path fallback. */
31
+ export interface ObjectMetadata {
32
+ readonly mode?: number;
33
+ readonly uid?: number;
34
+ readonly gid?: number;
35
+ readonly atimeNs?: bigint;
36
+ readonly mtimeNs?: bigint;
37
+ }
38
+ export interface ExactFileReadHandle {
39
+ stat(options?: FsOptions): Promise<ExactFileStat>;
40
+ read(position: bigint, maxBytes: number, options?: FsOptions): Promise<Uint8Array>;
41
+ }
42
+ export interface ExactFileResizeHandle {
43
+ stat(options?: FsOptions): Promise<ExactFileStat>;
44
+ truncate(length: bigint, options?: FsOptions): Promise<void>;
45
+ }
46
+ /** Cursor operation on the same retained open description. Aliases share its
47
+ * cursor; close ownership remains with the enclosing handle. No numeric fallback. */
48
+ export interface ExactFileSeekHandle {
49
+ seek(position: bigint, options?: FsOptions): Promise<void>;
50
+ }
51
+ export interface OpenFileObjectOptions extends FsOptions {
52
+ /** Default read; retained handle access is independent of hardlink identity. */
53
+ readonly access?: "read" | "write" | "readwrite";
54
+ /** Separately authorized native endpoint; omission never admits a special file. */
55
+ readonly special?: SpecialFileType;
56
+ }
57
+ /** Native creation admission on the same object that is returned. Exclusive
58
+ * flags refuse every existing entry, including a symlink, without mutation.
59
+ * w truncates during acquisition; a preserves existing bytes. Failure after a
60
+ * completed truncation does not roll it back. Backend root/mount/readonly/quota
61
+ * policy applies before acquisition; no caller-side existence or stat probe. */
62
+ export interface CreateFileObjectOptions extends FsOptions {
63
+ readonly flag: "w" | "wx" | "a" | "ax";
64
+ readonly access?: "read" | "write" | "readwrite";
65
+ readonly mode?: number;
66
+ }
67
+ export interface RetainedFileObject {
68
+ /** Qualified retained identity: aliases share this token while any retain exists.
69
+ * Tokens cannot collide across unrelated backend/mount authorities and are never
70
+ * derived from paths or unqualified stat tuples. They are NOT wire identifiers. */
71
+ readonly identity: object | symbol;
72
+ readonly type: ObjectFileType;
73
+ stat(options?: FsOptions): Promise<ExactFileStat>;
74
+ read?(position: bigint, maxBytes: number, options?: FsOptions): Promise<Uint8Array>;
75
+ write?(position: bigint, bytes: Uint8Array, options?: FsOptions): Promise<number>;
76
+ /** Choose EOF and append on this retained object as one backend operation.
77
+ * Return actual settled byte progress; never implement with caller-side stat/write. */
78
+ append?(bytes: Uint8Array, options?: FsOptions): Promise<number>;
79
+ truncate?(length: bigint, options?: FsOptions): Promise<void>;
80
+ /** Each field requires backend support; unsupported fields reject ENOTSUP. */
81
+ metadata?(changes: ObjectMetadata, options?: FsOptions): Promise<void>;
82
+ /** Link the retained object, not its former pathname. Preserve EXDEV/EPERM. */
83
+ link?(destination: BytePath, options?: FsOptions): Promise<void>;
84
+ close(): Promise<void>;
85
+ }
86
+ export interface CreatedFileObject extends RetainedFileObject {
87
+ /** Receipt from acquisition, never inferred from a later pathname probe. */
88
+ readonly creation: "created" | "opened" | "truncated";
89
+ }
90
+ /** Optional stronger byte namespace. Absence is unsupported, not string fallback.
91
+ * Implementations retain objects atomically, enforce mount/root permissions and
92
+ * validate requested endpoint kinds before any potentially blocking acquisition. */
93
+ export interface ObjectFileSystem {
94
+ readonly specialFiles?: Readonly<Partial<Record<SpecialFileType, boolean>>>;
95
+ open(path: BytePath, options?: OpenFileObjectOptions): Promise<RetainedFileObject>;
96
+ /** Optional qualified create/open/truncate primitive. Mode is used for newly
97
+ * created files only. Defaults for access/mode belong to the backend. a/ax
98
+ * do not synthesize retained append support or an append cursor. */
99
+ create?(path: BytePath, options: CreateFileObjectOptions): Promise<CreatedFileObject>;
100
+ /** A receipt describes this operation's actual namespace effect, not a later
101
+ * pathname probe. Omission means unknown; identical-object renames may be no-ops. */
102
+ rename?(source: BytePath, destination: BytePath, options?: FsOptions): Promise<void | {
103
+ readonly moved: boolean;
104
+ }>;
105
+ unlink?(path: BytePath, options?: FsOptions): Promise<void>;
106
+ readdir?(path: BytePath, options?: ReadDirectoryOptions): Promise<readonly {
107
+ readonly name: BytePath;
108
+ readonly type: ObjectFileType;
109
+ }[]>;
110
+ }
111
+ export interface WireObjectDirectoryEntry {
112
+ readonly name: readonly number[];
113
+ readonly type: ObjectFileType;
114
+ }
115
+ export interface WireObjectHandle {
116
+ readonly handle: string;
117
+ readonly object: string;
118
+ }
119
+ export type WireExactFileStat = Omit<ExactFileStat, "size" | "allocatedBytes" | "nlink" | "atimeNs" | "mtimeNs" | "ctimeNs"> & {
120
+ readonly size: string;
121
+ readonly allocatedBytes?: string;
122
+ readonly nlink?: string;
123
+ readonly atimeNs?: string;
124
+ readonly mtimeNs?: string;
125
+ readonly ctimeNs?: string;
126
+ };
127
+ export type WireObjectMetadata = Omit<ObjectMetadata, "atimeNs" | "mtimeNs"> & {
128
+ readonly atimeNs?: string;
129
+ readonly mtimeNs?: string;
130
+ };
131
+ export declare function encodeObjectMetadata(value: ObjectMetadata): WireObjectMetadata;
132
+ export declare function decodeObjectMetadata(value: unknown): ObjectMetadata;
133
+ /** Signed nanoseconds since Unix epoch; admit exact values before serialization. */
134
+ export declare function encodeFileTimestamp(value: bigint): string;
@@ -0,0 +1,97 @@
1
+ import { FsError } from "./errors.js";
2
+ /** Owned Unix path bytes. No decoding, normalization, or namespace authorization. */
3
+ export class BytePath {
4
+ #value;
5
+ constructor(value) {
6
+ if (!(value instanceof Uint8Array)) {
7
+ throw new FsError("EINVAL", { syscall: "bytePath" });
8
+ }
9
+ this.#value = new Uint8Array(value);
10
+ if (this.#value.length === 0 || this.#value.includes(0)) {
11
+ throw new FsError("EINVAL", { syscall: "bytePath" });
12
+ }
13
+ }
14
+ bytes() { return this.#value.slice(); }
15
+ }
16
+ /** JSON representation: octets, never a UTF-8 replacement string. */
17
+ export function encodeBytePath(path) {
18
+ return Array.from(BytePath.prototype.bytes.call(path));
19
+ }
20
+ export function decodeBytePath(value) {
21
+ if (!Array.isArray(value))
22
+ throw new FsError("EINVAL", { syscall: "bytePath" });
23
+ const octets = Array.from({ length: value.length }, (_, index) => {
24
+ if (!Object.hasOwn(value, index))
25
+ throw new FsError("EINVAL", { syscall: "bytePath" });
26
+ return value[index];
27
+ });
28
+ if (octets.some(byte => !Number.isInteger(byte) || byte < 0 || byte > 255)) {
29
+ throw new FsError("EINVAL", { syscall: "bytePath" });
30
+ }
31
+ return new BytePath(Uint8Array.from(octets));
32
+ }
33
+ /** Admit legacy numbers before conversion; native off_t profile is signed 64-bit. */
34
+ export function fileOffset(value) {
35
+ if (typeof value !== "bigint" && (typeof value !== "number" || !Number.isSafeInteger(value))) {
36
+ throw new FsError("EINVAL", { syscall: "offset" });
37
+ }
38
+ const exact = BigInt(value);
39
+ if (exact < 0n || exact > 9223372036854775807n)
40
+ throw new FsError("EINVAL", { syscall: "offset" });
41
+ return exact;
42
+ }
43
+ export function encodeFileOffset(value) {
44
+ if (typeof value !== "bigint")
45
+ throw new FsError("EINVAL", { syscall: "offset" });
46
+ return fileOffset(value).toString();
47
+ }
48
+ export function decodeFileOffset(value) {
49
+ if (typeof value !== "string" || value.length === 0 || value.length > 19 ||
50
+ (value.length > 1 && value[0] === "0") || [...value].some(char => char < "0" || char > "9")) {
51
+ throw new FsError("EINVAL", { syscall: "offset" });
52
+ }
53
+ return fileOffset(BigInt(value));
54
+ }
55
+ export function encodeObjectMetadata(value) {
56
+ const { atimeNs, mtimeNs, ...rest } = value;
57
+ const wire = {
58
+ ...rest,
59
+ ...(atimeNs === undefined ? {} : { atimeNs: encodeFileTimestamp(atimeNs) }),
60
+ ...(mtimeNs === undefined ? {} : { mtimeNs: encodeFileTimestamp(mtimeNs) }),
61
+ };
62
+ decodeObjectMetadata(wire);
63
+ return wire;
64
+ }
65
+ export function decodeObjectMetadata(value) {
66
+ if (typeof value !== "object" || value === null || Array.isArray(value))
67
+ throw new FsError("EINVAL");
68
+ const result = {};
69
+ for (const [key, field] of Object.entries(value)) {
70
+ if (key === "atimeNs" || key === "mtimeNs") {
71
+ if (typeof field !== "string" || field.length === 0 || field.length > 20)
72
+ throw new FsError("EINVAL");
73
+ const digits = field.startsWith("-") ? field.slice(1) : field;
74
+ if (digits.length === 0 || [...digits].some(char => char < "0" || char > "9"))
75
+ throw new FsError("EINVAL");
76
+ const exact = BigInt(field);
77
+ if (exact.toString() !== field || exact < -9223372036854775808n || exact > 9223372036854775807n)
78
+ throw new FsError("EINVAL");
79
+ result[key] = exact;
80
+ }
81
+ else if (key === "mode" || key === "uid" || key === "gid") {
82
+ if (typeof field !== "number" || !Number.isSafeInteger(field) || field < 0 || field > (key === "mode" ? 0o7777 : 4294967294))
83
+ throw new FsError("EINVAL");
84
+ result[key] = field;
85
+ }
86
+ else {
87
+ throw new FsError("ENOTSUP");
88
+ }
89
+ }
90
+ return result;
91
+ }
92
+ /** Signed nanoseconds since Unix epoch; admit exact values before serialization. */
93
+ export function encodeFileTimestamp(value) {
94
+ if (typeof value !== "bigint" || value < -9223372036854775808n || value > 9223372036854775807n)
95
+ throw new FsError("EINVAL");
96
+ return value.toString();
97
+ }
@@ -20,3 +20,5 @@ export * from "./python/index.js";
20
20
  export { compareEntries } from "./fs/mount/comparison.js";
21
21
  export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
22
22
  export type { XmlName, XmlAttribute, XmlContent, XmlElement, XmlLimits } from "./xml.js";
23
+ export * from "./contracts/object.js";
24
+ export { ObjectAuthority } from "./fs/object-authority.js";
@@ -17,3 +17,5 @@ export * from "./bridge/index.js";
17
17
  export * from "./python/index.js";
18
18
  export { compareEntries } from "./fs/mount/comparison.js";
19
19
  export { parseXml, parseXmlSteps, XmlLimitError } from "./xml.js";
20
+ export * from "./contracts/object.js";
21
+ export { ObjectAuthority } from "./fs/object-authority.js";
@@ -0,0 +1,27 @@
1
+ import type { FileSystem } from "../contracts/filesystem.js";
2
+ import { BytePath } from "../contracts/object.js";
3
+ import type { WireObjectMetadata, OpenFileObjectOptions, WireExactFileStat, WireObjectHandle, WireObjectDirectoryEntry } from "../contracts/object.js";
4
+ /** One authenticated job/epoch owns one authority. IDs are routing identifiers,
5
+ * not bearer authorization. All admitted operations drain before disposal. */
6
+ export declare class ObjectAuthority {
7
+ #private;
8
+ constructor(filesystem: FileSystem | Pick<FileSystem, "objects">, options: {
9
+ readonly maxHandles: number;
10
+ readonly maxIoBytes?: number;
11
+ });
12
+ open(path: BytePath, options?: OpenFileObjectOptions): Promise<WireObjectHandle>;
13
+ read(handle: string, position: string, maxBytes: number): Promise<Uint8Array>;
14
+ write(handle: string, position: string, bytes: Uint8Array): Promise<number>;
15
+ append(handle: string, bytes: Uint8Array): Promise<number>;
16
+ truncate(handle: string, length: string): Promise<void>;
17
+ metadata(handle: string, changes: WireObjectMetadata): Promise<void>;
18
+ link(handle: string, destination: BytePath): Promise<void>;
19
+ stat(handle: string): Promise<WireExactFileStat>;
20
+ rename(source: BytePath, destination: BytePath): Promise<void | {
21
+ readonly moved: boolean;
22
+ }>;
23
+ unlink(path: BytePath): Promise<void>;
24
+ readdir(path: BytePath, maxEntries: number): Promise<readonly WireObjectDirectoryEntry[]>;
25
+ close(handle: string): Promise<void>;
26
+ dispose(): Promise<void>;
27
+ }
@@ -0,0 +1,286 @@
1
+ import { platform } from "#safe-fs-platform";
2
+ import { FsError } from "../contracts/errors.js";
3
+ import { BytePath, decodeFileOffset, encodeFileOffset, decodeObjectMetadata, encodeFileTimestamp } from "../contracts/object.js";
4
+ /** One authenticated job/epoch owns one authority. IDs are routing identifiers,
5
+ * not bearer authorization. All admitted operations drain before disposal. */
6
+ export class ObjectAuthority {
7
+ #backend;
8
+ #maxHandles;
9
+ #maxIoBytes;
10
+ #prefix = platform.randomUUID();
11
+ #handles = new Map();
12
+ #identities = new Map();
13
+ #sequence = 0;
14
+ #tail = Promise.resolve();
15
+ #disposal;
16
+ constructor(filesystem, options) {
17
+ const backend = filesystem.objects;
18
+ // One issued authority keeps its qualified namespace. Bound operations
19
+ // observe live backend state without accepting replacement capabilities.
20
+ if (backend !== undefined) {
21
+ const { open, rename, unlink, readdir, specialFiles } = backend;
22
+ this.#backend = Object.freeze({
23
+ open: open.bind(backend),
24
+ ...(rename === undefined ? {} : { rename: rename.bind(backend) }),
25
+ ...(unlink === undefined ? {} : { unlink: unlink.bind(backend) }),
26
+ ...(readdir === undefined ? {} : { readdir: readdir.bind(backend) }),
27
+ specialFiles: Object.freeze({ ...specialFiles }),
28
+ });
29
+ }
30
+ this.#maxHandles = options.maxHandles;
31
+ this.#maxIoBytes = options.maxIoBytes ?? 1048576;
32
+ if (!Number.isSafeInteger(this.#maxHandles) || this.#maxHandles < 1 ||
33
+ !Number.isSafeInteger(this.#maxIoBytes) || this.#maxIoBytes < 1)
34
+ throw new FsError("EINVAL");
35
+ }
36
+ #run(operation) {
37
+ if (this.#disposal)
38
+ return Promise.reject(new FsError("EBADF"));
39
+ const result = this.#tail.then(operation);
40
+ this.#tail = result.catch(() => { });
41
+ return result;
42
+ }
43
+ #lookup(handle, access) {
44
+ const retained = this.#handles.get(handle);
45
+ if (!retained || (access && retained.access !== access && retained.access !== "readwrite"))
46
+ throw new FsError("EBADF");
47
+ return retained.object;
48
+ }
49
+ async open(path, options = {}) {
50
+ const ownedPath = new BytePath(BytePath.prototype.bytes.call(path));
51
+ const admission = Object.freeze({ ...options });
52
+ return this.#run(async () => {
53
+ if ((admission.access !== undefined && !["read", "write", "readwrite"].includes(admission.access)) ||
54
+ (admission.special !== undefined && !["character", "fifo", "socket"].includes(admission.special)))
55
+ throw new FsError("EINVAL");
56
+ const backend = this.#backend;
57
+ if (!backend || (admission.special && backend.specialFiles?.[admission.special] !== true))
58
+ throw new FsError("ENOTSUP");
59
+ if (this.#handles.size >= this.#maxHandles)
60
+ throw new FsError("EMFILE");
61
+ const acquired = await backend.open(ownedPath, admission);
62
+ // Own cleanup before inspecting admission properties. Keep the acquired
63
+ // receiver and operations even if its public method table later changes.
64
+ const close = acquired.close.bind(acquired);
65
+ try {
66
+ const token = acquired.identity;
67
+ const type = acquired.type;
68
+ if ((typeof token !== "object" && typeof token !== "symbol") || token === null)
69
+ throw new FsError("ENOTSUP");
70
+ if (admission.special ? type !== admission.special : !["file", "directory", "symlink"].includes(type))
71
+ throw new FsError("ENOTSUP");
72
+ const { read, write, append, truncate, metadata, link } = acquired;
73
+ const object = Object.freeze({
74
+ identity: token, type, close,
75
+ stat: acquired.stat.bind(acquired),
76
+ ...(read === undefined ? {} : { read: read.bind(acquired) }),
77
+ ...(write === undefined ? {} : { write: write.bind(acquired) }),
78
+ ...(append === undefined ? {} : { append: append.bind(acquired) }),
79
+ ...(truncate === undefined ? {} : { truncate: truncate.bind(acquired) }),
80
+ ...(metadata === undefined ? {} : { metadata: metadata.bind(acquired) }),
81
+ ...(link === undefined ? {} : { link: link.bind(acquired) }),
82
+ });
83
+ let identity = this.#identities.get(token);
84
+ if (!identity) {
85
+ identity = { id: `${this.#prefix}:o${++this.#sequence}`, references: 0 };
86
+ this.#identities.set(token, identity);
87
+ }
88
+ identity.references++;
89
+ const handle = `${this.#prefix}:h${++this.#sequence}`;
90
+ this.#handles.set(handle, { object, identity: token, type, access: admission.access ?? "read" });
91
+ return { handle, object: identity.id };
92
+ }
93
+ catch (error) {
94
+ try {
95
+ await close();
96
+ }
97
+ catch (closeError) {
98
+ throw new AggregateError([error, closeError]);
99
+ }
100
+ throw error;
101
+ }
102
+ });
103
+ }
104
+ read(handle, position, maxBytes) {
105
+ return this.#run(async () => {
106
+ const object = this.#lookup(handle, "read");
107
+ const offset = decodeFileOffset(position);
108
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0 || maxBytes > this.#maxIoBytes)
109
+ throw new FsError("EINVAL");
110
+ if (!object.read)
111
+ throw new FsError("ENOTSUP");
112
+ const bytes = await object.read(offset, maxBytes);
113
+ if (!(bytes instanceof Uint8Array))
114
+ throw new FsError("EIO");
115
+ const length = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), "byteLength").get.call(bytes);
116
+ if (length > maxBytes)
117
+ throw new FsError("EIO");
118
+ return new Uint8Array(bytes);
119
+ });
120
+ }
121
+ #ownedWriteBytes(bytes) {
122
+ if (!(bytes instanceof Uint8Array))
123
+ throw new FsError("EINVAL");
124
+ // Caller properties cannot redefine the span admitted before allocation.
125
+ const length = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(Uint8Array.prototype), "byteLength").get.call(bytes);
126
+ if (length > this.#maxIoBytes)
127
+ throw new FsError("EINVAL");
128
+ return new Uint8Array(bytes);
129
+ }
130
+ async write(handle, position, bytes) {
131
+ const owned = this.#ownedWriteBytes(bytes);
132
+ return this.#run(async () => {
133
+ const object = this.#lookup(handle, "write");
134
+ const offset = decodeFileOffset(position);
135
+ if (!object.write)
136
+ throw new FsError("ENOTSUP");
137
+ const count = await object.write(offset, owned);
138
+ if (!Number.isSafeInteger(count) || count < 0 || count > owned.byteLength)
139
+ throw new FsError("EIO");
140
+ return count;
141
+ });
142
+ }
143
+ async append(handle, bytes) {
144
+ const owned = this.#ownedWriteBytes(bytes);
145
+ return this.#run(async () => {
146
+ const object = this.#lookup(handle, "write");
147
+ if (!object.append)
148
+ throw new FsError("ENOTSUP", { syscall: "append" });
149
+ const count = await object.append(owned);
150
+ if (!Number.isSafeInteger(count) || count < 0 || count > owned.byteLength)
151
+ throw new FsError("EIO");
152
+ return count;
153
+ });
154
+ }
155
+ truncate(handle, length) {
156
+ return this.#run(async () => {
157
+ const object = this.#lookup(handle, "write");
158
+ const size = decodeFileOffset(length);
159
+ if (!object.truncate)
160
+ throw new FsError("ENOTSUP");
161
+ await object.truncate(size);
162
+ });
163
+ }
164
+ metadata(handle, changes) {
165
+ if (typeof changes !== "object" || changes === null || Array.isArray(changes))
166
+ return Promise.reject(new FsError("EINVAL"));
167
+ const owned = { ...changes };
168
+ return this.#run(async () => {
169
+ const object = this.#lookup(handle);
170
+ if (!object.metadata)
171
+ throw new FsError("ENOTSUP");
172
+ await object.metadata(decodeObjectMetadata(owned));
173
+ });
174
+ }
175
+ async link(handle, destination) {
176
+ const ownedDestination = new BytePath(BytePath.prototype.bytes.call(destination));
177
+ return this.#run(async () => {
178
+ const object = this.#lookup(handle);
179
+ if (!object.link)
180
+ throw new FsError("ENOTSUP");
181
+ await object.link(ownedDestination);
182
+ });
183
+ }
184
+ stat(handle) {
185
+ return this.#run(async () => {
186
+ const object = this.#lookup(handle);
187
+ const value = await object.stat();
188
+ const { type, size, allocatedBytes, nlink, atimeNs, mtimeNs, ctimeNs, mode, uid, gid } = value;
189
+ if (type !== this.#handles.get(handle).type)
190
+ throw new FsError("EIO");
191
+ for (const [field, maximum] of [[mode, 65535], [uid, 4294967295], [gid, 4294967295]]) {
192
+ if (field !== undefined && (!Number.isSafeInteger(field) || field < 0 || field > maximum))
193
+ throw new FsError("EINVAL");
194
+ }
195
+ return {
196
+ type, size: encodeFileOffset(size),
197
+ ...(mode === undefined ? {} : { mode }),
198
+ ...(uid === undefined ? {} : { uid }),
199
+ ...(gid === undefined ? {} : { gid }),
200
+ ...(allocatedBytes === undefined ? {} : { allocatedBytes: encodeFileOffset(allocatedBytes) }),
201
+ ...(nlink === undefined ? {} : { nlink: encodeFileOffset(nlink) }),
202
+ ...(atimeNs === undefined ? {} : { atimeNs: encodeFileTimestamp(atimeNs) }),
203
+ ...(mtimeNs === undefined ? {} : { mtimeNs: encodeFileTimestamp(mtimeNs) }),
204
+ ...(ctimeNs === undefined ? {} : { ctimeNs: encodeFileTimestamp(ctimeNs) }),
205
+ };
206
+ });
207
+ }
208
+ async rename(source, destination) {
209
+ const ownedSource = new BytePath(BytePath.prototype.bytes.call(source));
210
+ const ownedDestination = new BytePath(BytePath.prototype.bytes.call(destination));
211
+ return this.#run(async () => {
212
+ const backend = this.#backend;
213
+ if (!backend?.rename)
214
+ throw new FsError("ENOTSUP");
215
+ return await backend.rename(ownedSource, ownedDestination);
216
+ });
217
+ }
218
+ async unlink(path) {
219
+ const ownedPath = new BytePath(BytePath.prototype.bytes.call(path));
220
+ return this.#run(async () => {
221
+ const backend = this.#backend;
222
+ if (!backend?.unlink)
223
+ throw new FsError("ENOTSUP");
224
+ await backend.unlink(ownedPath);
225
+ });
226
+ }
227
+ async readdir(path, maxEntries) {
228
+ const ownedPath = new BytePath(BytePath.prototype.bytes.call(path));
229
+ return this.#run(async () => {
230
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 0)
231
+ throw new FsError("EINVAL");
232
+ const backend = this.#backend;
233
+ if (!backend?.readdir)
234
+ throw new FsError("ENOTSUP");
235
+ const entries = await backend.readdir(ownedPath, { maxEntries });
236
+ if (!Array.isArray(entries))
237
+ throw new FsError("EIO");
238
+ const length = entries.length;
239
+ if (length > maxEntries)
240
+ throw new FsError("EFBIG");
241
+ return Array.from({ length }, (_, index) => {
242
+ if (!Object.hasOwn(entries, index))
243
+ throw new FsError("EIO");
244
+ const entry = entries[index];
245
+ if (!entry)
246
+ throw new FsError("EIO");
247
+ const { name: component, type } = entry;
248
+ if (typeof component?.bytes !== "function" ||
249
+ !["file", "directory", "symlink", "character", "fifo", "socket"].includes(type))
250
+ throw new FsError("EIO");
251
+ const carrier = component.bytes();
252
+ if (!(carrier instanceof Uint8Array))
253
+ throw new FsError("EIO");
254
+ const bytes = new Uint8Array(carrier);
255
+ if (!bytes.length || bytes.includes(0) || bytes.includes(47) ||
256
+ (bytes[0] === 46 && (bytes.length === 1 || (bytes.length === 2 && bytes[1] === 46))))
257
+ throw new FsError("EIO");
258
+ const name = Array.from(bytes);
259
+ return { name, type };
260
+ });
261
+ });
262
+ }
263
+ close(handle) {
264
+ return this.#run(async () => {
265
+ const object = this.#lookup(handle);
266
+ const token = this.#handles.get(handle).identity;
267
+ this.#handles.delete(handle);
268
+ const identity = this.#identities.get(token);
269
+ if (--identity.references === 0)
270
+ this.#identities.delete(token);
271
+ await object.close();
272
+ });
273
+ }
274
+ dispose() {
275
+ this.#disposal ??= this.#tail.then(async () => {
276
+ const objects = [...this.#handles.values()].map(retained => retained.object);
277
+ this.#handles.clear();
278
+ this.#identities.clear();
279
+ const results = await Promise.allSettled(objects.map(async (object) => { await object.close(); }));
280
+ const failures = results.filter(result => result.status === "rejected");
281
+ if (failures.length)
282
+ throw new AggregateError(failures.map(result => result.reason), "Object cleanup failed");
283
+ });
284
+ return this.#disposal;
285
+ }
286
+ }
@@ -1,5 +1,7 @@
1
1
  import type { FileSystem, FsOptions } from "../contracts/filesystem.js";
2
- export declare function scopeFileSystem(filesystem: FileSystem, charge: () => void, signal: AbortSignal, cleanupCharge?: () => void): FileSystem;
2
+ export declare function scopeFileSystem(filesystem: FileSystem, charge: () => void, signal: AbortSignal, cleanupCharge?: () => void, options?: {
3
+ readonly preserveDescriptorWriteReceipt?: boolean;
4
+ }): FileSystem;
3
5
  export interface RetainedFileSystemCleanupView {
4
6
  readonly removeFileConditional?: NonNullable<FileSystem["removeFileConditional"]>;
5
7
  readonly removeStagedFile?: NonNullable<FileSystem["removeStagedFile"]>;
@@ -10,7 +10,7 @@ const operations = new Set([
10
10
  "readlink", "realpath", "rename", "resizeFile", "rm", "rmdir", "unlink", "stat", "symlink", "truncate", "utimes",
11
11
  "writeFile", "writeStream",
12
12
  ]);
13
- export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = charge) {
13
+ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = charge, options = {}) {
14
14
  const original = originals.get(filesystem)?.filesystem ?? filesystem;
15
15
  const methods = new Map();
16
16
  const assertOpen = (options) => {
@@ -115,7 +115,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
115
115
  };
116
116
  const wrapDescriptor = (descriptor) => {
117
117
  let closing;
118
- const invoke = async (options, action) => {
118
+ const invoke = async (options, action, preserveReceipt = false) => {
119
119
  assertOpen(options);
120
120
  if (closing)
121
121
  throw new FsError("EBADF");
@@ -125,7 +125,8 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
125
125
  throw new FsError("EBADF");
126
126
  try {
127
127
  const result = await action(resizeOptions(options));
128
- assertOpen(options);
128
+ if (!preserveReceipt)
129
+ assertOpen(options);
129
130
  return result;
130
131
  }
131
132
  catch (error) {
@@ -141,7 +142,7 @@ export function scopeFileSystem(filesystem, charge, signal, cleanupCharge = char
141
142
  ...(probeRead === undefined ? {} : { probeRead: (options = {}) => invoke(options, scoped => probeRead.call(descriptor, scoped)) }),
142
143
  stat: (options = {}) => invoke(options, scoped => descriptor.stat(scoped)),
143
144
  read: (buffer, position, options = {}) => invoke(options, scoped => descriptor.read(buffer, position, scoped)),
144
- write: (buffer, position, options = {}) => invoke(options, scoped => descriptor.write(buffer, position, scoped)),
145
+ write: (buffer, position, forwarded = {}) => invoke(forwarded, scoped => descriptor.write(buffer, position, scoped), options.preserveDescriptorWriteReceipt === true),
145
146
  truncate: (length, options = {}) => invoke(options, scoped => descriptor.truncate(length, scoped)),
146
147
  sync: (dataOnly, options = {}) => invoke(options, scoped => descriptor.sync(dataOnly, scoped)),
147
148
  close: () => closing ??= Promise.resolve().then(() => descriptor.close()),
@@ -1,4 +1,5 @@
1
1
  export * from "./contracts/index.js";
2
+ export { ObjectAuthority } from "./fs/object-authority.js";
2
3
  export * from "./python/index.js";
3
4
  export { openFileDescriptor } from "./fs/descriptor.js";
4
5
  export type { DescriptorBackend, DescriptorOpenOptions } from "./fs/descriptor.js";
@@ -1,4 +1,5 @@
1
1
  export * from "./contracts/index.js";
2
+ export { ObjectAuthority } from "./fs/object-authority.js";
2
3
  export * from "./python/index.js";
3
4
  export { openFileDescriptor } from "./fs/descriptor.js";
4
5
  export * from "./fs/memory/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.716",
3
+ "version": "0.1.718",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -110,6 +110,14 @@
110
110
  "./fs/quota": {
111
111
  "types": "./dist/safe-fs/fs/quota/index.d.ts",
112
112
  "import": "./dist/safe-fs/fs/quota/index.js"
113
+ },
114
+ "./contracts/object": {
115
+ "types": "./dist/safe-fs/contracts/object.d.ts",
116
+ "import": "./dist/safe-fs/contracts/object.js"
117
+ },
118
+ "./contracts/errors": {
119
+ "types": "./dist/safe-fs/contracts/errors.d.ts",
120
+ "import": "./dist/safe-fs/contracts/errors.js"
113
121
  }
114
122
  },
115
123
  "repository": {