@openclaw/fs-safe 0.9.0 → 0.11.0
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/CHANGELOG.md +134 -0
- package/LICENSE +1 -0
- package/README.md +52 -4
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -0
- package/dist/archive-crc32.d.ts.map +1 -1
- package/dist/archive-crc32.js +6 -1
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +3 -4
- package/dist/archive-durability.d.ts +6 -6
- package/dist/archive-durability.d.ts.map +1 -1
- package/dist/archive-durability.js +1 -1
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +4 -5
- package/dist/archive-gzip-tail.d.ts +3 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +23 -3
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +4 -2
- package/dist/archive-merge.d.ts +5 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +16 -13
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +7 -6
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +83 -74
- package/dist/archive-staging.d.ts +6 -3
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +42 -22
- package/dist/archive-tar-stream.d.ts +11 -4
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +23 -10
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +18 -14
- package/dist/archive-zip-admission.d.ts +1 -1
- package/dist/archive-zip-admission.d.ts.map +1 -1
- package/dist/archive-zip-admission.js +48 -12
- package/dist/archive-zip-loader.d.ts +6 -0
- package/dist/archive-zip-loader.d.ts.map +1 -0
- package/dist/archive-zip-loader.js +38 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +13 -8
- package/dist/archive-zip-preflight.d.ts +2 -3
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +2 -34
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +12 -10
- package/dist/bounded-read.d.ts +12 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +82 -45
- package/dist/clone-metadata.d.ts +19 -0
- package/dist/clone-metadata.d.ts.map +1 -0
- package/dist/clone-metadata.js +32 -0
- package/dist/copy-file-input.d.ts +22 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +69 -0
- package/dist/copy-policy.d.ts +3 -0
- package/dist/copy-policy.d.ts.map +1 -0
- package/dist/copy-policy.js +8 -0
- package/dist/copy-publication.d.ts +10 -1
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +27 -0
- package/dist/copy-tree-portable.d.ts +9 -0
- package/dist/copy-tree-portable.d.ts.map +1 -0
- package/dist/copy-tree-portable.js +222 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +125 -0
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +5 -4
- package/dist/directory-guard.d.ts +11 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +53 -11
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +1 -1
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -1
- package/dist/file-handle-transfer.d.ts +14 -0
- package/dist/file-handle-transfer.d.ts.map +1 -0
- package/dist/file-handle-transfer.js +64 -0
- package/dist/file-hash.d.ts +9 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +135 -39
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -4
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +7 -5
- package/dist/file-store-path.d.ts +3 -0
- package/dist/file-store-path.d.ts.map +1 -0
- package/dist/file-store-path.js +27 -0
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +6 -4
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +56 -44
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +2 -18
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -13
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +4 -2
- package/dist/guarded-mutation.d.ts +2 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +8 -4
- package/dist/guest-dispatch-python.d.ts +2 -0
- package/dist/guest-dispatch-python.d.ts.map +1 -0
- package/dist/guest-dispatch-python.js +117 -0
- package/dist/guest-native-python.d.ts +4 -0
- package/dist/guest-native-python.d.ts.map +1 -0
- package/dist/guest-native-python.js +135 -0
- package/dist/guest.d.ts +9 -0
- package/dist/guest.d.ts.map +1 -0
- package/dist/guest.js +413 -0
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +9 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +5 -8
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +19 -18
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +2 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +24 -25
- package/dist/move-path-stage.d.ts +8 -0
- package/dist/move-path-stage.d.ts.map +1 -0
- package/dist/move-path-stage.js +56 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +30 -34
- package/dist/mutation-authority.d.ts +9 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +29 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +11 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +19 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +3 -1
- package/dist/native-staged-file.d.ts +3 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +35 -8
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +5 -4
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +2 -0
- package/dist/overwrite-file-handle.d.ts +8 -0
- package/dist/overwrite-file-handle.d.ts.map +1 -0
- package/dist/overwrite-file-handle.js +42 -0
- package/dist/path-case.d.ts +7 -0
- package/dist/path-case.d.ts.map +1 -0
- package/dist/path-case.js +136 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +5 -2
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +85 -53
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +7 -3
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +51 -30
- package/dist/positional-read.d.ts +9 -0
- package/dist/positional-read.d.ts.map +1 -0
- package/dist/positional-read.js +36 -0
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +6 -4
- package/dist/publish-copy-stage.d.ts +13 -0
- package/dist/publish-copy-stage.d.ts.map +1 -0
- package/dist/publish-copy-stage.js +47 -0
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +6 -5
- package/dist/realpath.d.ts +4 -0
- package/dist/realpath.d.ts.map +1 -0
- package/dist/realpath.js +43 -0
- package/dist/recursive-mkdir-path.d.ts +3 -0
- package/dist/recursive-mkdir-path.d.ts.map +1 -0
- package/dist/recursive-mkdir-path.js +8 -0
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +2 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +23 -31
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +7 -12
- package/dist/replace-file-mode.d.ts +3 -0
- package/dist/replace-file-mode.d.ts.map +1 -0
- package/dist/replace-file-mode.js +10 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +6 -2
- package/dist/replace-file.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +14 -8
- package/dist/root-context.d.ts +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +53 -28
- package/dist/root-create-input.d.ts +10 -0
- package/dist/root-create-input.d.ts.map +1 -0
- package/dist/root-create-input.js +80 -0
- package/dist/root-directory-list.d.ts +26 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +219 -0
- package/dist/root-entries.d.ts +11 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +61 -0
- package/dist/root-errors.d.ts +6 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +16 -12
- package/dist/root-file.d.ts +2 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +12 -4
- package/dist/root-impl.d.ts +17 -51
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +358 -234
- package/dist/root-options.d.ts +77 -0
- package/dist/root-options.d.ts.map +1 -0
- package/dist/root-options.js +18 -0
- package/dist/root-path-existing.d.ts +5 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +59 -3
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +74 -162
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +13 -9
- package/dist/root-remove.d.ts +5 -0
- package/dist/root-remove.d.ts.map +1 -0
- package/dist/root-remove.js +286 -0
- package/dist/root-symlink-policy.d.ts +14 -0
- package/dist/root-symlink-policy.d.ts.map +1 -0
- package/dist/root-symlink-policy.js +34 -0
- package/dist/root-walk.d.ts +5 -4
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +99 -50
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +3 -5
- package/dist/root.d.ts +4 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +25 -8
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +2 -1
- package/dist/sibling-staged-file.d.ts +1 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +42 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +6 -4
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +17 -5
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -1
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/temp-target.d.ts +14 -12
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +12 -6
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +9 -7
- package/dist/unicode-path.d.ts +3 -0
- package/dist/unicode-path.d.ts.map +1 -0
- package/dist/unicode-path.js +13 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +9 -28
- package/dist/windows-owner.d.ts +9 -12
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +24 -58
- package/dist/write-file-handle.d.ts +7 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +26 -0
- package/docs/advanced.md +22 -4
- package/docs/archive.md +44 -9
- package/docs/atomic.md +24 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +36 -1
- package/docs/copy.md +155 -0
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +53 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +4 -4
- package/docs/file-store.md +15 -0
- package/docs/guest.md +141 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install.md +31 -0
- package/docs/local-roots.md +8 -1
- package/docs/native-helper.md +10 -3
- package/docs/native.md +18 -1
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/permissions.md +29 -10
- package/docs/positional-read.md +63 -0
- package/docs/public-api.md +31 -2
- package/docs/root.md +196 -6
- package/docs/secure-file.md +2 -0
- package/docs/security-model.md +14 -0
- package/docs/sidecar-lock.md +12 -3
- package/docs/temp.md +35 -6
- package/docs/timing.md +2 -0
- package/docs/types.md +19 -12
- package/docs/walk.md +54 -5
- package/docs/writing.md +173 -5
- package/package.json +19 -8
package/docs/root.md
CHANGED
|
@@ -18,6 +18,7 @@ const fs = await root("/srv/workspace", {
|
|
|
18
18
|
function root(rootDir: string, defaults?: RootDefaults): Promise<Root>;
|
|
19
19
|
|
|
20
20
|
type RootDefaults = {
|
|
21
|
+
assertBeforeMutation?: () => void; // synchronous caller authority check at mutation dispatch
|
|
21
22
|
durable?: boolean; // fsync write/create/writeJson/createJson/append/copyIn; default true
|
|
22
23
|
hardlinks?: "reject" | "allow"; // refuse files with nlink > 1 on read; defaults to "reject"
|
|
23
24
|
denyMutations?: DenyMutationPolicy; // absolute paths/prefixes mutation methods may not change
|
|
@@ -26,7 +27,8 @@ type RootDefaults = {
|
|
|
26
27
|
mode?: number; // file mode applied to new writes; per-call override available
|
|
27
28
|
nonBlockingRead?: boolean; // compatibility hint; safe opens are already nonblocking where supported
|
|
28
29
|
renameIdentity?: "strict" | "verify-content-with-lock"; // default "strict"
|
|
29
|
-
symlinks?: "reject" | "follow-within-root"; // policy
|
|
30
|
+
symlinks?: "reject" | "follow-within-root" | "follow-parents-within-root"; // read policy
|
|
31
|
+
mutationSymlinks?: "reject" | "follow-parents-within-root"; // opt-in mutation policy
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
type DenyMutationPolicy = {
|
|
@@ -37,7 +39,9 @@ type DenyMutationPolicy = {
|
|
|
37
39
|
|
|
38
40
|
`root()` resolves the directory through the real filesystem. A symlinked input becomes the canonical path; a non-existent root throws `FsSafeError` with code `not-found`, and malformed or non-directory roots throw `invalid-path`.
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
The root directory is pinned with exact bigint device/inode identities. A changed root rejects subsequent operations; an unknown Windows identity that remains unverifiable after bounded reinspection rejects construction with `path-mismatch`.
|
|
43
|
+
|
|
44
|
+
`defaults` apply to every method on the returned handle. Per-call options on individual methods override the defaults for that call only, except `denyMutations` and `assertBeforeMutation`: deny entries are merged, and the root assertion runs before the per-call assertion. A call cannot clear either root-level restriction.
|
|
41
45
|
|
|
42
46
|
Every `maxBytes` value must be a non-negative safe integer or positive `Infinity`. Zero is an active zero-byte cap; `Infinity` explicitly disables the cap. An omitted or explicitly `undefined` per-call value preserves the configured Root default instead of clearing it.
|
|
43
47
|
|
|
@@ -60,7 +64,12 @@ fs.walk(rel, options) // root-bounded AsyncIterable<{ relativePath, kin
|
|
|
60
64
|
|
|
61
65
|
`walk()` is the incremental, root-bounded recursive scan. It supports entry and
|
|
62
66
|
depth budgets, cancellation, and `symlinkPolicy: "skip" |
|
|
63
|
-
"follow-within-root"`.
|
|
67
|
+
"follow-within-root"`. With an entry budget, sorted walks prepare small metadata
|
|
68
|
+
batches within the remaining budget; unbounded sorted walks reuse the full
|
|
69
|
+
directory snapshot.
|
|
70
|
+
The default `order: "sorted"` enumerates and sorts each directory's names;
|
|
71
|
+
`order: "filesystem"` streams names in filesystem order for bounded work in
|
|
72
|
+
wide directories. Budget exhaustion yields a `"truncated"` marker by
|
|
64
73
|
default or throws `FsSafeError("too-large")` with `limitBehavior: "throw"`.
|
|
65
74
|
Use `entryFilter(entry)` to return `"include"`, `"skip"`, or
|
|
66
75
|
`"skip-subtree"`. `"skip"` omits the current entry but still descends into a
|
|
@@ -104,13 +113,25 @@ fs.append(rel, data, options?) // append text/buffer; syncs before clo
|
|
|
104
113
|
fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
|
|
105
114
|
fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
|
|
106
115
|
fs.move(from, to, options?) // rename within the root; defaults to no clobber
|
|
107
|
-
fs.remove(rel, options?) // unlink file or
|
|
116
|
+
fs.remove(rel, options?) // unlink file, rmdir, or bounded recursive removal
|
|
108
117
|
fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
|
|
109
118
|
fs.ensureRoot(options?) // accepts "" / "." as the root itself
|
|
110
119
|
```
|
|
111
120
|
|
|
112
121
|
`write`, `create`, `append`, `writeJson`, and `createJson` accept `mode?: number`; use `0o600` for credentials and other private state. `writeJson` also accepts the same options as `JSON.stringify` plus `trailingNewline?: boolean` (defaults `true` so the file ends in `\n`).
|
|
113
122
|
|
|
123
|
+
`create` also accepts `AsyncIterable<Uint8Array>` with `RootCreateStreamOptions`:
|
|
124
|
+
the same path, authority, mode, and durability options, plus `maxBytes` and
|
|
125
|
+
`signal`, without `encoding` or `renameIdentity`. It consumes one chunk at a
|
|
126
|
+
time and publishes the completed file exclusively. The byte cap inherits an
|
|
127
|
+
explicit `Root.defaults.maxBytes`; without either cap, consumption is unlimited.
|
|
128
|
+
See [streamed creation](writing.md#streamed-creation) for cancellation,
|
|
129
|
+
cleanup, and filesystem requirements.
|
|
130
|
+
|
|
131
|
+
`append` accepts `prependNewlineIfNeeded: true` to separate text from existing
|
|
132
|
+
content when neither side supplies a newline. String data uses its `encoding`
|
|
133
|
+
for the newline check, including UTF-16LE; Buffer data uses a single LF byte.
|
|
134
|
+
|
|
114
135
|
These five methods and `copyIn` also accept `durable?: boolean`: the per-call value overrides
|
|
115
136
|
`Root.defaults.durable`, which defaults to `true` when omitted. An explicitly
|
|
116
137
|
`undefined` per-call value preserves the root default. `durable: false` keeps
|
|
@@ -119,7 +140,76 @@ and parent-directory fsync calls. Use it only for reconstructible data: a crash
|
|
|
119
140
|
may lose the write or leave the previous file. See [Writing](writing.md#write-options)
|
|
120
141
|
for platform details.
|
|
121
142
|
|
|
122
|
-
`copyIn`
|
|
143
|
+
`copyIn` accepts a `RootCopySource`: a trusted absolute source path or a file
|
|
144
|
+
within another Root. The guarded form supplies `root` with only its `open` and
|
|
145
|
+
`stat` read capabilities, plus `relativePath`:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const source = await root("/srv/templates");
|
|
149
|
+
const destination = await root("/srv/workspace");
|
|
150
|
+
await destination.copyIn("config/settings.json", {
|
|
151
|
+
root: source,
|
|
152
|
+
relativePath: "config/settings.json",
|
|
153
|
+
}, {
|
|
154
|
+
overwrite: false,
|
|
155
|
+
clone: "auto",
|
|
156
|
+
mode: 0o600,
|
|
157
|
+
signal: AbortSignal.timeout(30_000),
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The source Root applies its read policies, including confinement and symlink
|
|
162
|
+
handling. `sourceHardlinks` overrides its hardlink policy only when supplied;
|
|
163
|
+
otherwise the source Root default is retained. The admitted source
|
|
164
|
+
descriptor stays open through copying and source-identity verification; copying
|
|
165
|
+
does not consume its current file position. Both forms enforce `maxBytes` while
|
|
166
|
+
reading, including when a file grows after admission, and use bounded buffers.
|
|
167
|
+
Copies have independent file data; changing either file cannot change the other.
|
|
168
|
+
Set `preserveSourceMode: true` to select the mode from the admitted source
|
|
169
|
+
descriptor. An explicit numeric `mode`, including `Root.defaults.mode`, takes
|
|
170
|
+
precedence. By default, copying retains the existing destination-mode rules.
|
|
171
|
+
The operation verifies source identity, not a coherent snapshot of concurrent
|
|
172
|
+
in-place edits. Keep the source unchanged when snapshot consistency is required.
|
|
173
|
+
|
|
174
|
+
`overwrite` defaults to `true`, preserving the existing replacement behavior.
|
|
175
|
+
With `overwrite: false`, an existing destination produces `already-exists` and
|
|
176
|
+
is never altered. Copying prepares a private sibling file before publishing its
|
|
177
|
+
completed contents. Native mode uses no-replace rename. The guarded JavaScript
|
|
178
|
+
fallback links the completed stage and removes its temporary name in the same
|
|
179
|
+
JavaScript turn; the filesystem must support hardlinks. Other processes can
|
|
180
|
+
briefly observe both names. The source is never hardlinked to the destination.
|
|
181
|
+
|
|
182
|
+
`clone` chooses the file-data transfer strategy through `CopyCloneMode`, shared
|
|
183
|
+
with [`copyTree`](copy.md#api). File copies default to `"never"`; tree copies
|
|
184
|
+
default to `"auto"`:
|
|
185
|
+
|
|
186
|
+
| Value | Behavior |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `never` | Copy regular file bytes using reads and writes, without explicit cloning or copy offload. |
|
|
189
|
+
| `auto` | Try native file cloning, then copy offload or ordinary byte copying when cloning is unavailable. |
|
|
190
|
+
| `always` | Require native cloning; fail when the binding or filesystem cannot provide it. |
|
|
191
|
+
|
|
192
|
+
Native file cloning supports APFS and supported Linux filesystems. Windows
|
|
193
|
+
currently uses byte copying for `never` and `auto`; `always` fails. Clone choice
|
|
194
|
+
does not change modes, durability, root confinement, or source and publication
|
|
195
|
+
identity checks. The shared strategy does not replace Root's guarded regular-file
|
|
196
|
+
contract with `copyTree`'s caller-owned immutable-tree and metadata contract.
|
|
197
|
+
|
|
198
|
+
An already aborted `signal` prevents I/O. Cancellation during copying waits for
|
|
199
|
+
admitted reads and native work to settle, then cleans only the owned unpublished
|
|
200
|
+
stage. The final authority check runs before publication. Once publication has
|
|
201
|
+
occurred, later cancellation or verification failure preserves the destination.
|
|
202
|
+
The synchronous optional `onDestinationPublished` callback receives a frozen
|
|
203
|
+
`RootCopyPublicationReceipt` containing `{ path, dev, ino }`, with exact bigint identity immediately after
|
|
204
|
+
publication, before later checks can fail. Callback errors also preserve the
|
|
205
|
+
published file. This receipt records an outcome; it does not authorize removing
|
|
206
|
+
a file that another actor may have edited. Application recovery and cooperative
|
|
207
|
+
locking remain caller-owned.
|
|
208
|
+
|
|
209
|
+
Existing `copyIn` callers must account for completed destinations retained after
|
|
210
|
+
a post-publication source-verification failure, even without the new options.
|
|
211
|
+
Recovery must inspect current destination state rather than assume a rejected
|
|
212
|
+
copy left no file.
|
|
123
213
|
|
|
124
214
|
Root operations that choose a new destination reject a leading Windows
|
|
125
215
|
drive-relative spelling such as `C:name` on every platform. This applies to
|
|
@@ -131,8 +221,71 @@ basename first.
|
|
|
131
221
|
|
|
132
222
|
`openWritable` opens a writable file with options `mode?: number` and `writeMode?: "replace" | "append" | "update"`. `replace` truncates existing files and is the default; `update` keeps existing contents. Use it for streaming output. Prefer `await using` for cleanup.
|
|
133
223
|
|
|
224
|
+
`remove` leaves non-empty directories unchanged unless `recursive: true` is
|
|
225
|
+
provided. Recursive removal defaults to streaming entries in filesystem order;
|
|
226
|
+
`order: "sorted"` processes each directory's children lexicographically. The
|
|
227
|
+
`maxEntries` (100,000 by default) and `maxDepth` (64 by default) budgets accept
|
|
228
|
+
explicit `Infinity` when the caller needs unlimited traversal. It never
|
|
229
|
+
follows discovered symlinks; an explicit `mutationSymlinks` policy rejects them,
|
|
230
|
+
while the omitted policy unlinks them. `force: true` ignores missing targets,
|
|
231
|
+
and `signal` stops further work after admitted I/O and resource cleanup settle.
|
|
232
|
+
Removal is not transactional: a budget, cancellation, policy, or identity
|
|
233
|
+
failure can leave a partially removed tree. See [removal](writing.md)
|
|
234
|
+
for the full counting and failure contract.
|
|
235
|
+
|
|
236
|
+
### Live mutation authority
|
|
237
|
+
|
|
238
|
+
All mutation methods accept `assertBeforeMutation?: () => void`. Use it when a
|
|
239
|
+
lease, operation owner, or cancellation state can expire while filesystem
|
|
240
|
+
preparation is awaiting I/O:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const controller = new AbortController();
|
|
244
|
+
await fs.write("config.json", "{}\n", {
|
|
245
|
+
assertBeforeMutation: () => controller.signal.throwIfAborted(),
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The callback runs synchronously after awaited preparation, immediately before
|
|
250
|
+
each Root-owned mutation is dispatched: parent creation, file creation and
|
|
251
|
+
content writes (including private staging and streamed chunks), publication,
|
|
252
|
+
truncation, append, move, and removal. Buffered writes use bounded chunks and
|
|
253
|
+
recheck before every partial-write submission; file removal submits a direct
|
|
254
|
+
unlink request. Native calls that perform multiple filesystem steps are one
|
|
255
|
+
dispatch. No asynchronous wait separates the check
|
|
256
|
+
from that dispatch. A thrown value rejects the operation unchanged; an async
|
|
257
|
+
or thenable-returning callback rejects with `TypeError` before that mutation.
|
|
258
|
+
Synchronous return values are ignored. Callbacks can run multiple times and
|
|
259
|
+
must inspect current authority each time.
|
|
260
|
+
|
|
261
|
+
Already dispatched I/O cannot be revoked. Identity-checked cleanup, final
|
|
262
|
+
permissions, and durability finish under the existing operation owner even
|
|
263
|
+
after authority expires. Sidecar lock acquisition, recovery, and release for
|
|
264
|
+
`renameIdentity: "verify-content-with-lock"` are lock bookkeeping outside this
|
|
265
|
+
callback; content mutations still recheck after the lock is acquired. An
|
|
266
|
+
operation may leave already-created parent directories when a later check
|
|
267
|
+
rejects. A no-op such as `ensureRoot()` on the existing root does not require a
|
|
268
|
+
callback invocation. This is a dispatch fence, not a filesystem transaction or
|
|
269
|
+
a replacement for root confinement.
|
|
270
|
+
|
|
271
|
+
If cleanup also fails or cannot prove ownership of an entry, the existing
|
|
272
|
+
structured cleanup error takes precedence and retains the authority refusal
|
|
273
|
+
as its cause.
|
|
274
|
+
|
|
275
|
+
For `openWritable()`, the callback covers the library's parent creation,
|
|
276
|
+
exclusive creation, and truncation. The returned raw `FileHandle` belongs to
|
|
277
|
+
the caller, which must check authority before its own later writes.
|
|
278
|
+
|
|
134
279
|
All mutation methods accept `denyMutations?: { paths?: string[]; prefixes?: string[] }`. Entries must be absolute paths. `paths` blocks those exact paths; `prefixes` blocks those paths and their descendants. fs-safe preserves path strings exactly and canonicalizes through existing ancestors before comparing, so a symlinked ancestor to a denied location is still denied. Denied mutations throw `FsSafeError` with code `denied-path`. Use this for caller-specific sensitive paths, not as a replacement for the root boundary, symlink, or hardlink checks.
|
|
135
280
|
|
|
281
|
+
All mutation methods also accept `mutationSymlinks`. `"reject"` rejects symlink
|
|
282
|
+
components; `"follow-parents-within-root"` resolves contained parent directory
|
|
283
|
+
aliases but rejects the final component if it is a symlink, including a dangling
|
|
284
|
+
link. Missing parent directories can still be created through a contained alias.
|
|
285
|
+
`move()` applies the policy to both source and destination. An omitted value
|
|
286
|
+
preserves existing behavior, including `remove()` unlinking a final symlink.
|
|
287
|
+
The read-only `symlinks` default does not change mutation behavior.
|
|
288
|
+
|
|
136
289
|
### Inspection (advisory)
|
|
137
290
|
|
|
138
291
|
```ts
|
|
@@ -140,14 +293,22 @@ fs.exists(rel) // boolean
|
|
|
140
293
|
fs.stat(rel) // PathStat
|
|
141
294
|
fs.list(rel) // string[]
|
|
142
295
|
fs.list(rel, { withFileTypes }) // DirEntry[]
|
|
296
|
+
fs.entries(rel, options?) // nonrecursive AsyncIterable<DirEntry>, including symlinks
|
|
143
297
|
fs.resolve(rel) // absolute path inside the root, after canonicalization
|
|
144
298
|
```
|
|
145
299
|
|
|
146
300
|
These do not pin a later operation. They are safe to expose to UIs and decision points; for the actual read or write, use the verb methods so the operation pins identity at the point of use.
|
|
147
301
|
|
|
302
|
+
`entries()` streams immediate children in filesystem order by default. It
|
|
303
|
+
supports cancellation, a physical-entry limit that throws on overflow, and
|
|
304
|
+
bounded sorted-name collection. It reports child symlinks without following
|
|
305
|
+
them; its `symlinks` option applies only to the selected directory path.
|
|
306
|
+
See [Directory entries](entries.md) for ordering, identity, and partial-result
|
|
307
|
+
semantics.
|
|
308
|
+
|
|
148
309
|
`resolve()` is the exception to the existing-object rule: because it selects a
|
|
149
310
|
location for later use, it rejects a leading drive-relative spelling. Reads,
|
|
150
|
-
`stat`, `exists`, `list`, `walk`, `remove`, and the source argument of `move`
|
|
311
|
+
`stat`, `exists`, `list`, `entries`, `walk`, `remove`, and the source argument of `move`
|
|
151
312
|
accept an existing POSIX filename such as `c:notes.txt`. For `move`, only the
|
|
152
313
|
new destination name is subject to the portable guard.
|
|
153
314
|
|
|
@@ -218,6 +379,35 @@ await fs.readText("config.toml");
|
|
|
218
379
|
await fs.readText("links/current.log", { symlinks: "follow-within-root" });
|
|
219
380
|
```
|
|
220
381
|
|
|
382
|
+
With `follow-within-root`, parent components after a symlink are applied to the
|
|
383
|
+
symlink's resolved target. Reads use that checked canonical path, including
|
|
384
|
+
after home expansion and through `readAbsolute` and `reader`; the default policy still rejects a symlink
|
|
385
|
+
even when a later `..` would hide it in a purely lexical normalization.
|
|
386
|
+
|
|
387
|
+
Use `follow-parents-within-root` when directory aliases are allowed but a final
|
|
388
|
+
file symlink should fail. Set each policy at the root to share that contract
|
|
389
|
+
between reads and mutations:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
const workspace = await root("/srv/workspace", {
|
|
393
|
+
symlinks: "follow-parents-within-root",
|
|
394
|
+
mutationSymlinks: "follow-parents-within-root",
|
|
395
|
+
});
|
|
396
|
+
await workspace.readText("directory-alias/notes.txt");
|
|
397
|
+
await workspace.write("directory-alias/notes.txt", "updated\n");
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The library uses the resolved parent for the operation and checks the final
|
|
401
|
+
component again before publication or removal. These checks preserve the existing
|
|
402
|
+
[platform containment guarantees](security-model.md#symlinks-write-side);
|
|
403
|
+
they do not make check-and-rename atomic against another process. Callers do not
|
|
404
|
+
need a separate `realpath()` or final `lstat()` preflight.
|
|
405
|
+
|
|
406
|
+
For methods that accept absolute paths, the same final-component rule applies
|
|
407
|
+
when a path enters the root through an alias outside its lexical spelling. A directory alias may
|
|
408
|
+
lead to a regular file inside the root; an absolute final file or directory
|
|
409
|
+
symlink is rejected before its canonical target replaces the original path.
|
|
410
|
+
|
|
221
411
|
Text helpers default to UTF-8. Pass `encoding` per call to `readText`, `readJson`, `write`, `create`, or `append` when you need another encoding.
|
|
222
412
|
|
|
223
413
|
## Common patterns
|
package/docs/secure-file.md
CHANGED
|
@@ -21,6 +21,7 @@ The helper:
|
|
|
21
21
|
- rejects every non-regular preview and, by default, symlink paths
|
|
22
22
|
- opens POSIX paths no-follow and nonblocking before reading, then verifies the opened fd still matches the path and realpath; a FIFO swap cannot block before `timeoutMs` owns the byte read
|
|
23
23
|
- optionally requires the real path to live under one of `trust.trustedDirs`
|
|
24
|
+
- rejects hardlink aliases using descriptor, pathname, and realpath link counts, then rechecks the descriptor after reading before returning bytes
|
|
24
25
|
- rejects hard-to-verify or unsafe permissions unless `permissions.allowInsecure` is set
|
|
25
26
|
- rejects files owned by another POSIX uid
|
|
26
27
|
- enforces `maxBytes` before and after reading
|
|
@@ -73,6 +74,7 @@ type SecureFileReadOptions = {
|
|
|
73
74
|
| `not-found` | The path could not be stat'd before open. |
|
|
74
75
|
| `not-file` | The opened target is not a regular file. |
|
|
75
76
|
| `symlink` | The path is a symlink and `trust.allowSymlink` is false. |
|
|
77
|
+
| `hardlink` | The descriptor, pathname, or realpath has more than one link. |
|
|
76
78
|
| `path-mismatch` | The path or realpath changed between open and verification, or filesystem identity could not be verified after bounded re-inspection. |
|
|
77
79
|
| `outside-workspace` | `realPath` is outside `trust.trustedDirs`. |
|
|
78
80
|
| `permission-unverified` | Required mode/ACL checks could not be completed. |
|
package/docs/security-model.md
CHANGED
|
@@ -53,6 +53,11 @@ Every path is resolved against the canonicalized real path of the root, then che
|
|
|
53
53
|
|
|
54
54
|
Per-call `symlinks: "follow-within-root"` allows symlinks whose final target is still inside the root. The default is `"reject"`.
|
|
55
55
|
|
|
56
|
+
`symlinks: "follow-parents-within-root"` allows contained parent directory aliases
|
|
57
|
+
while rejecting a final symlink, including dangling links. Reads open the checked
|
|
58
|
+
canonical parent plus the final basename with the usual no-follow and identity
|
|
59
|
+
checks, so callers do not need their own parent canonicalization.
|
|
60
|
+
|
|
56
61
|
Guarded root reads compare lossless bigint identities from before open, the opened
|
|
57
62
|
descriptor, the input path, and the canonical target; numeric public `Stats`
|
|
58
63
|
receipts are not used as identity evidence. Unknown Windows device/inode values
|
|
@@ -70,6 +75,15 @@ directory descriptors. Replacement uses descriptor-relative rename just like
|
|
|
70
75
|
no-replace publication, so replacing the parent pathname does not divert the
|
|
71
76
|
mutation.
|
|
72
77
|
|
|
78
|
+
The opt-in `mutationSymlinks` policy applies independently of read policy.
|
|
79
|
+
`"reject"` rejects symlink components; `"follow-parents-within-root"` resolves
|
|
80
|
+
contained directory aliases and rejects final symlinks. Publication checks the
|
|
81
|
+
final component again after awaited staging and parent fences, immediately before
|
|
82
|
+
the rename or exclusive open. These are best-effort symlink checks, not an atomic
|
|
83
|
+
expected-entry/CAS replacement: a concurrent process can still replace the final
|
|
84
|
+
entry between its check and rename. Existing parent containment guarantees remain
|
|
85
|
+
as described below. Omitting `mutationSymlinks` preserves existing mutation behavior.
|
|
86
|
+
|
|
73
87
|
The JavaScript fallback used by `off`, by `auto` when no binding loads, and by
|
|
74
88
|
the explicit `renameIdentity: "verify-content-with-lock"` compatibility policy
|
|
75
89
|
cannot provide that guarantee because Node exposes no `mkdirat` or `renameat`.
|
package/docs/sidecar-lock.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`acquireFileLock()` and `withFileLock()` provide a cross-process file lock with retry and process-exit cleanup. The lock is implemented as a sidecar file (e.g. `state.json` ↔ `state.json.lock`) — only one acquirer can create the sidecar with `O_CREAT | O_EXCL` at a time.
|
|
4
4
|
|
|
5
|
+
JavaScript raw-sidecar creation passes mode `0o600` to that exclusive open. On POSIX, the process umask may further restrict the new file but cannot add group or other access. No pathname `chmod` fallback is used.
|
|
6
|
+
|
|
5
7
|
```ts
|
|
6
8
|
import { acquireFileLock } from "@openclaw/fs-safe/file-lock";
|
|
7
9
|
|
|
@@ -61,7 +63,7 @@ function withFileLockSync<T, TPayload>(targetPath: string, options: FileLockSync
|
|
|
61
63
|
type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
62
64
|
managerKey?: string; // optional in-process manager namespace
|
|
63
65
|
lockPath?: string; // override; defaults to `${targetPath}.lock`
|
|
64
|
-
staleMs?: number; // default 30_000
|
|
66
|
+
staleMs?: number; // non-negative or Infinity; default 30_000
|
|
65
67
|
timeoutMs?: number; // overall acquire deadline; default unbounded
|
|
66
68
|
retry?: FileLockRetryOptions;
|
|
67
69
|
staleRecovery?: "fail-closed" | "remove-if-unchanged"; // default "fail-closed"
|
|
@@ -86,7 +88,7 @@ type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = {
|
|
|
86
88
|
lockRoot?: Root;
|
|
87
89
|
retainOnExit?: boolean; // keep the sidecar across process exit (default false)
|
|
88
90
|
onCompromised?: (info: { lockPath: string; normalizedTargetPath: string }) => void;
|
|
89
|
-
compromiseCheckIntervalMs?: number;
|
|
91
|
+
compromiseCheckIntervalMs?: number; // 0/omitted disables; otherwise 1..2_147_483_647
|
|
90
92
|
};
|
|
91
93
|
|
|
92
94
|
type FileLockRetryOptions = {
|
|
@@ -245,7 +247,14 @@ type FileLockHandle = {
|
|
|
245
247
|
captured at acquisition. Set `compromiseCheckIntervalMs` together with
|
|
246
248
|
`onCompromised` for a cheap periodic check; the callback fires once after the
|
|
247
249
|
sidecar no longer matches or after a verification I/O failure. This is
|
|
248
|
-
detection, not revocation of work already in progress.
|
|
250
|
+
detection, not revocation of work already in progress. Asynchronous checks are
|
|
251
|
+
serialized, so a slow verification never overlaps the next timer tick.
|
|
252
|
+
|
|
253
|
+
The compromise-check interval is validated before payload evaluation or
|
|
254
|
+
filesystem acquisition. Omit it or pass `0` to disable monitoring; enabled
|
|
255
|
+
intervals must be finite and between 1 and 2,147,483,647 milliseconds. Values
|
|
256
|
+
outside that range are rejected instead of being clamped by Node.js to an
|
|
257
|
+
unexpectedly tight polling loop.
|
|
249
258
|
|
|
250
259
|
## Synchronous locks
|
|
251
260
|
|
package/docs/temp.md
CHANGED
|
@@ -262,8 +262,8 @@ const result = await writeSiblingTempFile<string>({
|
|
|
262
262
|
// result.filePath, result.result (returned by writeTemp)
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
-
`writeSiblingTempFile` chooses a random, initially absent sibling
|
|
266
|
-
and calls `writeTemp()`. After the callback succeeds, it validates the produced
|
|
265
|
+
By default, `writeSiblingTempFile` chooses a random, initially absent sibling
|
|
266
|
+
name in `dir` and calls `writeTemp()`. After the callback succeeds, it validates the produced
|
|
267
267
|
regular file before taking ownership: symlinks, directories, other non-regular
|
|
268
268
|
files, hardlinks, and changes between the pre-open pathname, opened descriptor,
|
|
269
269
|
and current pathname are rejected. The callback must finish and close its
|
|
@@ -292,12 +292,40 @@ Omitting either option or passing `false` skips that sync, never the identity
|
|
|
292
292
|
checks. Parent synchronization can be unsupported or fail without rejecting
|
|
293
293
|
the write, so success is not a strict crash-durability receipt.
|
|
294
294
|
|
|
295
|
-
|
|
296
|
-
single-link regular-file checks still agree.
|
|
295
|
+
Without producer isolation, cleanup only unlinks an admitted file while the
|
|
296
|
+
parent, pathname identity, and single-link regular-file checks still agree.
|
|
297
|
+
Observed substitutes are preserved,
|
|
297
298
|
including during process-exit cleanup. Operational cleanup failures retain an
|
|
298
299
|
identity-bound exit retry. If the callback throws or admission fails, no file
|
|
299
300
|
has been adopted: even a regular partial file is left for caller-directed
|
|
300
|
-
recovery. The helper never recursively removes a sibling temp.
|
|
301
|
+
recovery. The helper never recursively removes a sibling temp file path.
|
|
302
|
+
|
|
303
|
+
Set `producerIsolation: "private-directory"` in `WriteSiblingTempFileOptions`
|
|
304
|
+
when the producer can leave partial output before throwing. The callback then
|
|
305
|
+
receives an initially absent file path inside a private child workspace under
|
|
306
|
+
`dir`, on the same filesystem as the final target. fs-safe captures directory
|
|
307
|
+
cleanup ownership before invoking the callback. A callback exception triggers
|
|
308
|
+
owned workspace cleanup, including partial output, subject to directory
|
|
309
|
+
identity checks and I/O failures. The callback must still finish and close its
|
|
310
|
+
writer before returning.
|
|
311
|
+
|
|
312
|
+
After the callback succeeds, `Root.move` checks source aliases and moves the
|
|
313
|
+
output to the ordinary sibling path before file admission. An escaping symlink
|
|
314
|
+
can fail with `path-alias` at this step. Rejected output still inside the owned
|
|
315
|
+
workspace follows its cleanup contract. Once output moves to the sibling path,
|
|
316
|
+
failures before file adoption retain it for caller-directed recovery, as above.
|
|
317
|
+
File admission, requested modes, sync options, and final rename keep their
|
|
318
|
+
existing contracts; `resolveFinalPath(result)` still names a direct child of `dir`.
|
|
319
|
+
|
|
320
|
+
The isolated path retains exact bigint identities for both the parent and the
|
|
321
|
+
workspace and rechecks them before moving output to the sibling path. An
|
|
322
|
+
observed replacement is rejected. Cleanup uses the existing
|
|
323
|
+
[`withTempFile` ownership contract](#withtempfile), backed by [`tempFile`](#tempfile):
|
|
324
|
+
moving or replacing the parent or workspace can leave the original or
|
|
325
|
+
replacement paths in place. This option does not promise cleanup through a
|
|
326
|
+
retained directory after a rename, stronger permissions, or additional crash
|
|
327
|
+
durability. Omitting it preserves the direct sibling callback path and
|
|
328
|
+
unadmitted partial-file retention.
|
|
301
329
|
|
|
302
330
|
On POSIX, admission uses no-follow and nonblocking open flags, so a FIFO swap
|
|
303
331
|
does not block the helper. Windows retains Node's guarded pathname-open behavior
|
|
@@ -345,7 +373,7 @@ If `replaceFileAtomic` does what you need, prefer that. Use
|
|
|
345
373
|
the final destination still needs root-boundary checks.
|
|
346
374
|
Its private workspace uses the same identity-aware directory cleanup as
|
|
347
375
|
`tempFile()`: moving and replacing the workspace preserves the replacement.
|
|
348
|
-
The callback staging component is capped at 255 bytes under NFC and NFD by
|
|
376
|
+
The callback staging component is capped at 255 bytes as written and under NFC and NFD by
|
|
349
377
|
shortening only an overlong embedded destination tail, while preserving an
|
|
350
378
|
extension when possible. Short callback paths and the final target stay
|
|
351
379
|
unchanged. This workspace owns its contents, unlike the unadmitted sibling
|
|
@@ -440,6 +468,7 @@ import fs from "node:fs/promises";
|
|
|
440
468
|
|
|
441
469
|
const r = await writeSiblingTempFile({
|
|
442
470
|
dir: "/srv/cache",
|
|
471
|
+
producerIsolation: "private-directory",
|
|
443
472
|
writeTemp: async (tempPath) => {
|
|
444
473
|
const handle = await fs.open(tempPath, "w");
|
|
445
474
|
try {
|
package/docs/timing.md
CHANGED
|
@@ -22,6 +22,8 @@ function withTimeout<T>(
|
|
|
22
22
|
|
|
23
23
|
If `timeoutMs` is `0`, negative, `Infinity`, or `NaN`, the helper is a no-op and simply awaits the original promise.
|
|
24
24
|
|
|
25
|
+
Finite delays above Node's single-timer limit (2,147,483,647 ms, about 24.9 days) are scheduled in bounded intervals without expiring early. The timer is still cleared if the wrapped promise settles first.
|
|
26
|
+
|
|
25
27
|
## Examples
|
|
26
28
|
|
|
27
29
|
### Simple ceiling
|
package/docs/types.md
CHANGED
|
@@ -45,7 +45,7 @@ type DirEntry = PathStat & {
|
|
|
45
45
|
};
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
Returned by `Root.list(rel, { withFileTypes: true })
|
|
48
|
+
Returned by `Root.list(rel, { withFileTypes: true })` and [`Root.entries()`](entries.md). Includes every
|
|
49
49
|
`PathStat` field plus the entry's `name`.
|
|
50
50
|
|
|
51
51
|
## `BasePathOptions`
|
|
@@ -99,6 +99,7 @@ type ReadResult = {
|
|
|
99
99
|
type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
|
|
100
100
|
|
|
101
101
|
type RootDefaults = {
|
|
102
|
+
assertBeforeMutation?: () => void;
|
|
102
103
|
denyMutations?: DenyMutationPolicy;
|
|
103
104
|
durable?: boolean; // default true for write/create/writeJson/createJson/append
|
|
104
105
|
hardlinks?: "reject" | "allow";
|
|
@@ -107,7 +108,8 @@ type RootDefaults = {
|
|
|
107
108
|
mode?: number;
|
|
108
109
|
nonBlockingRead?: boolean;
|
|
109
110
|
renameIdentity?: RenameIdentityPolicy;
|
|
110
|
-
symlinks?: "reject" | "follow-within-root";
|
|
111
|
+
symlinks?: "reject" | "follow-within-root" | "follow-parents-within-root";
|
|
112
|
+
mutationSymlinks?: MutationSymlinkPolicy;
|
|
111
113
|
};
|
|
112
114
|
|
|
113
115
|
type DenyMutationPolicy = {
|
|
@@ -121,20 +123,20 @@ type RootOptions = {
|
|
|
121
123
|
};
|
|
122
124
|
```
|
|
123
125
|
|
|
124
|
-
`RootDefaults` is what `root(rootDir, defaults)` accepts. See [`root()`](root.md) for the per-method options that override these. `denyMutations`
|
|
126
|
+
`RootDefaults` is what `root(rootDir, defaults)` accepts. See [`root()`](root.md) for the per-method options that override these. `denyMutations` and `assertBeforeMutation` are exceptions: deny entries are merged, and the root authority assertion runs before the per-call assertion.
|
|
125
127
|
|
|
126
128
|
## `RootReadOptions` / `RootWriteOptions` / `RootCopyOptions`
|
|
127
129
|
|
|
128
130
|
```ts
|
|
129
131
|
type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
|
|
130
|
-
type RootWriteOptions = Pick<RootDefaults, "denyMutations" | "durable" | "mkdir" | "mode" | "renameIdentity"> & {
|
|
132
|
+
type RootWriteOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "mkdir" | "mode" | "renameIdentity" | "mutationSymlinks"> & {
|
|
131
133
|
encoding?: BufferEncoding;
|
|
132
134
|
overwrite?: boolean;
|
|
133
135
|
};
|
|
134
|
-
type RootCopyOptions = Pick<RootDefaults, "denyMutations" | "maxBytes" | "mkdir" | "mode"> & {
|
|
136
|
+
type RootCopyOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "maxBytes" | "mkdir" | "mode" | "mutationSymlinks"> & {
|
|
135
137
|
sourceHardlinks?: "reject" | "allow";
|
|
136
138
|
};
|
|
137
|
-
type RootOpenWritableOptions = Pick<RootDefaults, "denyMutations" | "mkdir" | "mode"> & {
|
|
139
|
+
type RootOpenWritableOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mkdir" | "mode" | "mutationSymlinks"> & {
|
|
138
140
|
writeMode?: "replace" | "append" | "update";
|
|
139
141
|
};
|
|
140
142
|
type RootWriteJsonOptions = RootWriteOptions & {
|
|
@@ -145,23 +147,28 @@ type RootWriteJsonOptions = RootWriteOptions & {
|
|
|
145
147
|
type RootAppendOptions = RootWriteOptions & {
|
|
146
148
|
prependNewlineIfNeeded?: boolean;
|
|
147
149
|
};
|
|
148
|
-
type RootMoveOptions = Pick<RootDefaults, "denyMutations"> & {
|
|
150
|
+
type RootMoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
|
|
149
151
|
overwrite?: boolean;
|
|
150
152
|
};
|
|
151
|
-
type RootRemoveOptions = Pick<RootDefaults, "denyMutations">;
|
|
152
|
-
type RootMkdirOptions = Pick<RootDefaults, "denyMutations">;
|
|
153
|
+
type RootRemoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks">;
|
|
154
|
+
type RootMkdirOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks">;
|
|
153
155
|
```
|
|
154
156
|
|
|
155
157
|
Per-method option shapes. Each picks the `RootDefaults` keys that apply, plus method-specific extras.
|
|
156
158
|
|
|
157
|
-
## `SymlinkPolicy` / `HardlinkPolicy`
|
|
159
|
+
## `SymlinkPolicy` / `MutationSymlinkPolicy` / `HardlinkPolicy`
|
|
158
160
|
|
|
159
161
|
```ts
|
|
160
|
-
type SymlinkPolicy = "reject" | "follow-within-root";
|
|
162
|
+
type SymlinkPolicy = "reject" | "follow-within-root" | "follow-parents-within-root";
|
|
163
|
+
type MutationSymlinkPolicy = "reject" | "follow-parents-within-root";
|
|
161
164
|
type HardlinkPolicy = "reject" | "allow";
|
|
162
165
|
```
|
|
163
166
|
|
|
164
|
-
|
|
167
|
+
`"reject"` is conservative; `"follow-within-root"` allows symlinks whose final target is still inside the root; `"allow"` (hardlinks only) is permissive. Defaults for read symlinks and hardlinks are `"reject"`; switch hardlinks to `"allow"` only when you intentionally accept hardlink aliases.
|
|
168
|
+
|
|
169
|
+
`"follow-parents-within-root"` allows contained parent directory aliases while
|
|
170
|
+
rejecting final symlinks. Mutation policy is opt-in and independent of read
|
|
171
|
+
policy; omission preserves each mutation method's existing behavior.
|
|
165
172
|
|
|
166
173
|
## `FsSafeErrorCode` / `FsSafeErrorCategory`
|
|
167
174
|
|
package/docs/walk.md
CHANGED
|
@@ -72,10 +72,57 @@ Unreadable directories are skipped rather than throwing, but every skipped direc
|
|
|
72
72
|
`Root.walk(rel, options)` is the root-bounded counterpart to these standalone
|
|
73
73
|
inventory helpers. It yields `{ relativePath, kind, size }` incrementally and
|
|
74
74
|
accepts `maxDepth`, `maxEntries`, `symlinkPolicy: "skip" |
|
|
75
|
-
"follow-within-root"`, and an `AbortSignal`. The default budget behavior yields
|
|
75
|
+
"follow-within-root"`, `order: "sorted" | "filesystem"`, and an `AbortSignal`. The default budget behavior yields
|
|
76
76
|
one `kind: "truncated"` marker and ends; pass `limitBehavior: "throw"` for a
|
|
77
77
|
typed `FsSafeError("too-large")` instead.
|
|
78
78
|
|
|
79
|
+
For followed symlinks, both `kind` and `size` describe the resolved target.
|
|
80
|
+
|
|
81
|
+
The default `order: "sorted"` visits each directory's names in lexicographic
|
|
82
|
+
order before descending depth first. It reads and sorts all names in each
|
|
83
|
+
visited directory. With `maxEntries`, it prepares small metadata batches capped
|
|
84
|
+
by the remaining global entry budget. Every batch stops at the first directory
|
|
85
|
+
or symlink, so recursive descent cannot spend a budget already used by later
|
|
86
|
+
siblings. An early `break` may leave metadata from the current batch unused;
|
|
87
|
+
the total still stays within `maxEntries`. Filtering requires metadata and
|
|
88
|
+
consumes the entry budget, including entries skipped by the filter.
|
|
89
|
+
|
|
90
|
+
Without `maxEntries`, sorted walks reuse a full directory metadata snapshot
|
|
91
|
+
from the `Root.list()` owner. This preserves the existing fast complete-scan
|
|
92
|
+
behavior and its snapshot semantics: changes made after a directory is listed
|
|
93
|
+
do not alter its already-captured entries. Supply an entry budget or use
|
|
94
|
+
filesystem order when metadata work must remain incremental. Sorted entries
|
|
95
|
+
describe the observations captured in their directory snapshot or batch.
|
|
96
|
+
|
|
97
|
+
Use `order: "filesystem"` when a wide directory must not be fully enumerated:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
for await (const entry of capability.walk("", {
|
|
101
|
+
order: "filesystem",
|
|
102
|
+
maxEntries: 128,
|
|
103
|
+
symlinkPolicy: "skip",
|
|
104
|
+
})) {
|
|
105
|
+
consume(entry);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This order follows the filesystem's directory stream and is not deterministic.
|
|
110
|
+
It reads one entry at a time, including one name of lookahead to distinguish an
|
|
111
|
+
exactly exhausted budget from truncation. The lookahead does not request full
|
|
112
|
+
entry metadata from fs-safe, and an early `break` does not prefetch later child
|
|
113
|
+
metadata. If a filesystem does not supply directory-entry
|
|
114
|
+
types, Node may classify that one extra entry with a synchronous `lstat`.
|
|
115
|
+
Handles close on completion, truncation, cancellation, errors, or an
|
|
116
|
+
early `break`. Both orders keep the same depth-first traversal, entry filtering,
|
|
117
|
+
and truncation rules. Cancellation is checked between entries, with event-loop
|
|
118
|
+
handoffs between budgeted sorted batches. Root and directory checks and admitted
|
|
119
|
+
child metadata reads are synchronous; no mode can interrupt a filesystem
|
|
120
|
+
syscall already in progress or the sorted mode's name sorting.
|
|
121
|
+
|
|
122
|
+
If a thrown walk failure and directory close both fail, disposal throws a
|
|
123
|
+
`SuppressedError` with the close failure in `error` and the original failure in
|
|
124
|
+
`suppressed`, preserving both causes.
|
|
125
|
+
|
|
79
126
|
`entryFilter` is evaluated for each resolved file, directory, or other entry:
|
|
80
127
|
|
|
81
128
|
```ts
|
|
@@ -109,10 +156,12 @@ Every examined directory entry consumes `maxEntries` before filtering, so
|
|
|
109
156
|
`"truncated"` markers describe already-reached state and do not authorize
|
|
110
157
|
further descent.
|
|
111
158
|
|
|
112
|
-
The pure-Node path validates every directory
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
159
|
+
The pure-Node path validates every directory through the Root boundary, pins
|
|
160
|
+
its exact identity, and rechecks it and the Root identity around each metadata
|
|
161
|
+
batch or individual filesystem-order observation. Sorted batches contain no
|
|
162
|
+
await or caller code between their before/after checks. It tracks canonical
|
|
163
|
+
directories to stop symlink cycles.
|
|
164
|
+
Neither mode holds a descriptor for every path component, so it is not a process sandbox against a hostile peer that
|
|
116
165
|
can continuously swap and restore directories. Each individual lookup retains
|
|
117
166
|
the documented Node `Root` boundary checks.
|
|
118
167
|
|