@openclaw/fs-safe 0.10.0 → 0.12.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 +114 -0
- package/LICENSE +1 -0
- package/README.md +39 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +3 -2
- package/dist/advanced.d.ts +5 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +5 -1
- 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 +8 -8
- package/dist/archive-gzip-tail.d.ts +1 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +16 -9
- 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 +15 -12
- package/dist/archive-native.js +4 -4
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +54 -42
- 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.map +1 -1
- package/dist/archive-tar-stream.js +7 -6
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +16 -13
- 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 +26 -9
- 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 +5 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +18 -11
- package/dist/copy-file-input.d.ts +0 -1
- package/dist/copy-file-input.d.ts.map +1 -1
- package/dist/copy-file-input.js +4 -26
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +57 -26
- package/dist/copy.d.ts +1 -1
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +3 -1
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -3
- 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/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 +3 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +99 -32
- 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 +15 -5
- 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 +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +32 -14
- package/dist/guarded-mkdir.d.ts +2 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +52 -16
- 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 +421 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts +6 -0
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +16 -2
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue-ownership.d.ts +2 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +47 -3
- package/dist/json-durable-queue-read.d.ts +6 -0
- package/dist/json-durable-queue-read.d.ts.map +1 -0
- package/dist/json-durable-queue-read.js +59 -0
- package/dist/json-durable-queue.d.ts +1 -1
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +43 -84
- 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 +2 -1
- package/dist/move-path-stage.d.ts.map +1 -1
- package/dist/move-path-stage.js +2 -1
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +4 -3
- package/dist/mutation-authority.d.ts +1 -0
- package/dist/mutation-authority.d.ts.map +1 -1
- package/dist/mutation-authority.js +4 -4
- package/dist/native-binding.d.ts +14 -2
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +3 -2
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +2 -1
- 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-scope-lexical.d.ts +14 -0
- package/dist/path-scope-lexical.d.ts.map +1 -0
- package/dist/path-scope-lexical.js +27 -0
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +22 -3
- package/dist/permissions-windows.d.ts +1 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +48 -6
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +3 -1
- package/dist/pinned-write.d.ts +2 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +6 -4
- 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.d.ts +1 -0
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +14 -8
- package/dist/root-boundary.d.ts +25 -0
- package/dist/root-boundary.d.ts.map +1 -0
- package/dist/root-boundary.js +177 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +40 -13
- 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 +3 -1
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +31 -5
- 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 +5 -5
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +13 -12
- package/dist/root-impl.d.ts +13 -3
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +93 -70
- package/dist/root-move-preflight.d.ts +8 -0
- package/dist/root-move-preflight.d.ts.map +1 -0
- package/dist/root-move-preflight.js +16 -0
- package/dist/root-options.d.ts +12 -1
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-existing.d.ts +2 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +14 -5
- package/dist/root-path-symlink.d.ts.map +1 -1
- package/dist/root-path-symlink.js +3 -2
- package/dist/root-path.d.ts +2 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +54 -26
- package/dist/root-paths.d.ts +2 -6
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +24 -32
- 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 +2 -1
- package/dist/root-symlink-policy.d.ts.map +1 -1
- package/dist/root-symlink-policy.js +2 -2
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +2 -1
- package/dist/root-write-mode.d.ts +2 -0
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +21 -7
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +12 -3
- package/dist/root.d.ts +2 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +3 -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-windows.d.ts +10 -0
- package/dist/secure-file-windows.d.ts.map +1 -0
- package/dist/secure-file-windows.js +186 -0
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +27 -7
- 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 +2 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +49 -8
- package/dist/sibling-temp.d.ts +2 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +8 -5
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +16 -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 +17 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -3
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +4 -3
- package/dist/temp-target.d.ts +14 -12
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -7
- package/dist/trash.d.ts.map +1 -1
- package/dist/trash.js +7 -5
- 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 +14 -12
- package/dist/write-file-handle.d.ts +1 -0
- package/dist/write-file-handle.d.ts.map +1 -1
- package/dist/write-file-handle.js +3 -2
- package/docs/advanced.md +7 -1
- package/docs/archive.md +31 -5
- package/docs/atomic.md +17 -1
- package/docs/config.md +1 -0
- package/docs/contributing.md +33 -2
- package/docs/copy.md +75 -6
- package/docs/directory-identity.md +85 -0
- package/docs/durability.md +40 -3
- package/docs/entries.md +109 -0
- package/docs/errors.md +3 -3
- package/docs/file-store.md +21 -0
- package/docs/filename.md +9 -2
- package/docs/guest.md +146 -0
- package/docs/in-place-write.md +81 -0
- package/docs/index.md +2 -0
- package/docs/install-path.md +59 -13
- package/docs/install.md +34 -0
- package/docs/native-helper.md +14 -5
- package/docs/native.md +18 -2
- package/docs/output.md +32 -6
- package/docs/path-case.md +64 -0
- package/docs/path-scope.md +1 -1
- package/docs/path.md +1 -1
- package/docs/permissions.md +37 -3
- package/docs/public-api.md +36 -2
- package/docs/root.md +33 -3
- package/docs/secure-file.md +17 -14
- package/docs/security-model.md +1 -1
- package/docs/sidecar-lock.md +12 -3
- package/docs/store.md +22 -1
- package/docs/temp.md +39 -6
- package/docs/types.md +1 -1
- package/docs/writing.md +153 -3
- package/package.json +14 -8
package/docs/root.md
CHANGED
|
@@ -100,7 +100,9 @@ The read methods also accept an absolute spelling that already resolves inside
|
|
|
100
100
|
the root. `readAbsolute()` and `reader()` make that intent explicit and accept
|
|
101
101
|
both the configured root spelling and its canonical real path when the Root was
|
|
102
102
|
created through a directory symlink or Windows junction. An absolute path
|
|
103
|
-
outside the root is still rejected.
|
|
103
|
+
outside the root is still rejected. On Windows, alternate casing is accepted
|
|
104
|
+
only when the differently cased Root prefix has the Root's exact directory
|
|
105
|
+
identity; the operation then continues under the trusted Root spelling.
|
|
104
106
|
|
|
105
107
|
### Writes
|
|
106
108
|
|
|
@@ -113,13 +115,21 @@ fs.append(rel, data, options?) // append text/buffer; syncs before clo
|
|
|
113
115
|
fs.copyIn(rel, sourceAbsPath, options?) // copy from outside the root, atomically, with size cap
|
|
114
116
|
fs.openWritable(rel, options?) // FileHandle for streaming writes; supports await using
|
|
115
117
|
fs.move(from, to, options?) // rename within the root; defaults to no clobber
|
|
116
|
-
fs.remove(rel, options?) // unlink file or
|
|
118
|
+
fs.remove(rel, options?) // unlink file, rmdir, or bounded recursive removal
|
|
117
119
|
fs.mkdir(rel, options?) // mkdir -p (creates missing parents)
|
|
118
120
|
fs.ensureRoot(options?) // accepts "" / "." as the root itself
|
|
119
121
|
```
|
|
120
122
|
|
|
121
123
|
`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`).
|
|
122
124
|
|
|
125
|
+
`create` also accepts `AsyncIterable<Uint8Array>` with `RootCreateStreamOptions`:
|
|
126
|
+
the same path, authority, mode, and durability options, plus `maxBytes` and
|
|
127
|
+
`signal`, without `encoding` or `renameIdentity`. It consumes one chunk at a
|
|
128
|
+
time and publishes the completed file exclusively. The byte cap inherits an
|
|
129
|
+
explicit `Root.defaults.maxBytes`; without either cap, consumption is unlimited.
|
|
130
|
+
See [streamed creation](writing.md#streamed-creation) for cancellation,
|
|
131
|
+
cleanup, and filesystem requirements.
|
|
132
|
+
|
|
123
133
|
`append` accepts `prependNewlineIfNeeded: true` to separate text from existing
|
|
124
134
|
content when neither side supplies a newline. String data uses its `encoding`
|
|
125
135
|
for the newline check, including UTF-16LE; Buffer data uses a single LF byte.
|
|
@@ -213,6 +223,18 @@ basename first.
|
|
|
213
223
|
|
|
214
224
|
`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.
|
|
215
225
|
|
|
226
|
+
`remove` leaves non-empty directories unchanged unless `recursive: true` is
|
|
227
|
+
provided. Recursive removal defaults to streaming entries in filesystem order;
|
|
228
|
+
`order: "sorted"` processes each directory's children lexicographically. The
|
|
229
|
+
`maxEntries` (100,000 by default) and `maxDepth` (64 by default) budgets accept
|
|
230
|
+
explicit `Infinity` when the caller needs unlimited traversal. It never
|
|
231
|
+
follows discovered symlinks; an explicit `mutationSymlinks` policy rejects them,
|
|
232
|
+
while the omitted policy unlinks them. `force: true` ignores missing targets,
|
|
233
|
+
and `signal` stops further work after admitted I/O and resource cleanup settle.
|
|
234
|
+
Removal is not transactional: a budget, cancellation, policy, or identity
|
|
235
|
+
failure can leave a partially removed tree. See [removal](writing.md)
|
|
236
|
+
for the full counting and failure contract.
|
|
237
|
+
|
|
216
238
|
### Live mutation authority
|
|
217
239
|
|
|
218
240
|
All mutation methods accept `assertBeforeMutation?: () => void`. Use it when a
|
|
@@ -273,14 +295,22 @@ fs.exists(rel) // boolean
|
|
|
273
295
|
fs.stat(rel) // PathStat
|
|
274
296
|
fs.list(rel) // string[]
|
|
275
297
|
fs.list(rel, { withFileTypes }) // DirEntry[]
|
|
298
|
+
fs.entries(rel, options?) // nonrecursive AsyncIterable<DirEntry>, including symlinks
|
|
276
299
|
fs.resolve(rel) // absolute path inside the root, after canonicalization
|
|
277
300
|
```
|
|
278
301
|
|
|
279
302
|
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.
|
|
280
303
|
|
|
304
|
+
`entries()` streams immediate children in filesystem order by default. It
|
|
305
|
+
supports cancellation, a physical-entry limit that throws on overflow, and
|
|
306
|
+
bounded sorted-name collection. It reports child symlinks without following
|
|
307
|
+
them; its `symlinks` option applies only to the selected directory path.
|
|
308
|
+
See [Directory entries](entries.md) for ordering, identity, and partial-result
|
|
309
|
+
semantics.
|
|
310
|
+
|
|
281
311
|
`resolve()` is the exception to the existing-object rule: because it selects a
|
|
282
312
|
location for later use, it rejects a leading drive-relative spelling. Reads,
|
|
283
|
-
`stat`, `exists`, `list`, `walk`, `remove`, and the source argument of `move`
|
|
313
|
+
`stat`, `exists`, `list`, `entries`, `walk`, `remove`, and the source argument of `move`
|
|
284
314
|
accept an existing POSIX filename such as `c:notes.txt`. For `move`, only the
|
|
285
315
|
new destination name is subject to the portable guard.
|
|
286
316
|
|
package/docs/secure-file.md
CHANGED
|
@@ -21,14 +21,17 @@ 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
|
|
27
28
|
- closes the handle on success, error, and timeout
|
|
28
29
|
|
|
29
|
-
On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper
|
|
30
|
+
On POSIX, unsafe permissions mean group/world writable, and group/world readable unless `permissions.allowReadableByOthers` is true. On Windows, the helper queries owner, DACL, and locality from the same open descriptor that supplies the bytes. The native query returns the 32-bit volume serial and 64-bit file-index projection used by Node, which must equal Node's bigint descriptor receipt before its ACL facts are trusted. This avoids JavaScript number rounding but does not represent the full 128-bit file identity available on ReFS. Only the current user, LocalSystem, and built-in Administrators are trusted owner classes.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
Windows secure reads require the matching current optional native package. A missing or stale helper, fd-to-handle conversion failure, denied `READ_CONTROL`, remote handle, incomplete descriptor, or unsupported ACE form rejects with `permission-unverified` before content is read. A malformed or different handle identity rejects with `path-mismatch`. There is no pathname-command fallback for `readSecureFile()`; the standalone reporting APIs in [`permissions`](permissions.md) retain their documented fallbacks. `permissions.allowInsecure` remains the explicit escape hatch and bypasses the ACL query.
|
|
33
|
+
|
|
34
|
+
Descriptor, pathname, and realpath identity checks use bigint stats internally to avoid JavaScript number rounding. The returned `stat` remains a normal Node `Stats` object with numeric fields. A zero Windows device or inode is unverified, never a match: the helper re-inspects that identity once using the same descriptor or pathname, then rejects persistent ambiguity with `path-mismatch`. A definite mismatch rejects immediately; retries retain known identity components and still enforce symlink policy.
|
|
32
35
|
|
|
33
36
|
## Options
|
|
34
37
|
|
|
@@ -61,6 +64,8 @@ type SecureFileReadOptions = {
|
|
|
61
64
|
|
|
62
65
|
`permissions.allowInsecure` is a migration escape hatch. Prefer fixing permissions and using [`formatPermissionRemediation`](permissions.md) to show the user what to run. `trust.allowNetworkPath` is off by default because UNC paths are remote authority, not local filesystem input. `inject` is for tests and platform adapters; production callers usually leave it unset.
|
|
63
66
|
|
|
67
|
+
On an actual Windows process with effective `platform: "win32"`, `inject.env` and `inject.exec` do not replace descriptor inspection. They remain available to simulated Windows checks on non-Windows hosts.
|
|
68
|
+
|
|
64
69
|
`permissions.allowInsecure` bypasses only permission checks. Neither it nor `inject.platform` changes filesystem identity verification, which always uses the actual process platform. `trust.allowSymlink` permits an alias but still requires its target and realpath to match the opened descriptor.
|
|
65
70
|
|
|
66
71
|
## Errors
|
|
@@ -73,25 +78,23 @@ type SecureFileReadOptions = {
|
|
|
73
78
|
| `not-found` | The path could not be stat'd before open. |
|
|
74
79
|
| `not-file` | The opened target is not a regular file. |
|
|
75
80
|
| `symlink` | The path is a symlink and `trust.allowSymlink` is false. |
|
|
81
|
+
| `hardlink` | The descriptor, pathname, or realpath has more than one link. |
|
|
76
82
|
| `path-mismatch` | The path or realpath changed between open and verification, or filesystem identity could not be verified after bounded re-inspection. |
|
|
77
83
|
| `outside-workspace` | `realPath` is outside `trust.trustedDirs`. |
|
|
78
|
-
| `permission-unverified` | Required mode/ACL checks could not be completed. |
|
|
84
|
+
| `permission-unverified` | Required mode/ACL checks could not be completed, including when descriptor-bound Windows inspection is unavailable. |
|
|
79
85
|
| `insecure-permissions` | Mode bits or ACLs grant broader access than allowed. |
|
|
80
86
|
| `not-owned` | POSIX owner uid is not the current process uid. |
|
|
81
87
|
| `too-large` | File size or bytes read exceeded `maxBytes`. |
|
|
82
88
|
| `timeout` | `timeoutMs` elapsed while reading. |
|
|
83
89
|
|
|
84
|
-
Windows inspection failures
|
|
85
|
-
and
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
errors also retain their original execFile exception in the cause chain.
|
|
93
|
-
Treat causes as restricted local diagnostic data. No retries are performed,
|
|
94
|
-
and verification order and rejection conditions are unchanged.
|
|
90
|
+
Windows descriptor-inspection failures are operational `permission-unverified`
|
|
91
|
+
errors and refuse the read. The original native exception is retained as
|
|
92
|
+
`cause`; treat causes as restricted local diagnostic data. No pathname or ACL
|
|
93
|
+
content is copied into the display message. Test adapters that simulate Windows
|
|
94
|
+
on another operating system retain the standalone pathname inspector's
|
|
95
|
+
structured command diagnostics (`ownerError`, `command`, `durationMs`,
|
|
96
|
+
`timedOut`, `exitCode`, `signal`, and bounded escaped `stderr`). Actual Windows
|
|
97
|
+
secure reads do not start those commands. No retries are performed.
|
|
95
98
|
|
|
96
99
|
## See also
|
|
97
100
|
|
package/docs/security-model.md
CHANGED
|
@@ -45,7 +45,7 @@ If you need full sandboxing, run the worker under reduced privileges (uid, conta
|
|
|
45
45
|
|
|
46
46
|
### Path traversal and absolute paths
|
|
47
47
|
|
|
48
|
-
Every path is resolved against the canonicalized real path of the root, then checked
|
|
48
|
+
Every path is resolved against the canonicalized real path of the root, then checked at that boundary. On Windows, an exact-case structural Root prefix stays on the lexical fast path; a prefix accepted only by case folding must have the Root's exact directory identity and is rebased onto the trusted Root spelling before use. Alias resolution walks components before applying a later `..`, so a symlink cannot change what that parent segment means after validation. Parent traversal, an absolute spelling, or any alias whose canonical result is outside the root throws `outside-workspace`; absolute spellings that remain inside the root are accepted.
|
|
49
49
|
|
|
50
50
|
### Symlinks (read side)
|
|
51
51
|
|
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/store.md
CHANGED
|
@@ -72,12 +72,33 @@ Loading serializes consumers for one ID through a sidecar lock, then creates `pr
|
|
|
72
72
|
|
|
73
73
|
Queue and failed directory creation fsyncs every newly-created parent edge from the leaf toward the trusted root. Enqueue and migration writes fsync the temp file and parent; claim, acknowledgement, quarantine, delivered-marker cleanup, and retirement transitions fsync every affected directory and propagate real sync failures. A transition may already be visible when a post-mutation sync fails, so retry the same operation to complete its crash-recovery state. Acknowledgement retries resync the queue directory even when both `.processing` and `.delivered` marker names are already absent, before reporting completion or rejecting a newer pending generation; quarantine retries with only failed evidence resync that destination before repairing the vanished queue source.
|
|
74
74
|
|
|
75
|
-
`writeJsonDurableQueueEntry()` and migrations share strict parent synchronization inside the atomic writer's retained descriptor and per-path serialization lifetime, followed by published-file identity verification. If sync fails after publication, the write rejects without rolling back the published JSON; retrying writes the entry again and must complete its own sync. This is not a rollback, deduplication, or exactly-once guarantee. The generic `replaceFileAtomic({ syncParentDir: true })` option remains best-effort.
|
|
75
|
+
`writeJsonDurableQueueEntry()` and migrations share strict parent synchronization inside the atomic writer's retained descriptor and per-path serialization lifetime, followed by published-file identity verification. If sync fails after publication, the write rejects without rolling back the published JSON; retrying `writeJsonDurableQueueEntry()` writes the entry again and must complete its own sync. Loader retries resync an existing processing claim's parent under the transfer lock before calling `read`, even when a version-dependent callback would no longer request migration. Fresh claims and same-directory source retirement already complete that sync. This is not a rollback, deduplication, or exactly-once guarantee. The generic `replaceFileAtomic({ syncParentDir: true })` option remains best-effort.
|
|
76
76
|
|
|
77
77
|
Batch loading skips invalid entry names, malformed, oversized, or unreadable entry content, and caller `read` callback failures. Initially unowned pending entries (hardlinks or unverifiable identities), symlinks, non-files, and absent pending entries are also skipped. Claim, transfer-lock, retirement, and migration write/publication/durability failures reject the batch with the original error, even if earlier entries succeeded. Migration in both loaders strictly syncs the parent directory after successful publication. Visible transitions and earlier processing claims remain for retry; a rejected batch does not acknowledge or roll them back.
|
|
78
78
|
|
|
79
79
|
Failed destinations are create-only. Quarantine publishes the claimed file by hardlink, so the queue and failed directories must share a filesystem with hardlink support. If `failed/<id>.json` already exists, quarantine rejects while preserving both that earlier evidence and the current claimed entry instead of overwriting either file. The `read` callback continues to receive the logical `.json` path even though bytes are read and migrations are written through the claimed path.
|
|
80
80
|
|
|
81
|
+
Migrations stay bound to the exact processing file opened for that load. The
|
|
82
|
+
read descriptor remains pinned while the callback runs outside the transfer
|
|
83
|
+
lock; after the callback returns, migration reacquires the lock and rechecks the
|
|
84
|
+
claim before publication. If another consumer acknowledged, quarantined, or
|
|
85
|
+
replaced that claim, the migration rejects with `FsSafeError("path-mismatch")`
|
|
86
|
+
and leaves the newer generation or failed evidence intact. A stale migration
|
|
87
|
+
rejects both single and batch loads; ordinary callback failures retain their
|
|
88
|
+
existing single-load rejection and batch-skip behavior.
|
|
89
|
+
|
|
90
|
+
On Windows, migration releases its read pin once at this publication boundary
|
|
91
|
+
because an open target can block replacement. It rechecks the exact pathname
|
|
92
|
+
identity after the asynchronous close while still holding the transfer lock.
|
|
93
|
+
POSIX retains the read pin through publication. Other readers keep ownership
|
|
94
|
+
of their handles; Windows sharing denials still reject and can be retried after
|
|
95
|
+
those readers close.
|
|
96
|
+
|
|
97
|
+
Generation arbitration requires consumers to use the transfer lock. As with
|
|
98
|
+
[atomic writes](atomic.md#beforerename), identity checks and pathname replacement
|
|
99
|
+
are separate operations; use a trusted writable parent or OS isolation against
|
|
100
|
+
processes that ignore the lock and mutate queue paths concurrently.
|
|
101
|
+
|
|
81
102
|
Queue entry reads verify lossless file identities before opening, on the opened
|
|
82
103
|
descriptor, and at the current pathname before reading bytes. POSIX opens are
|
|
83
104
|
nonblocking, so a raced FIFO is rejected rather than stalling a consumer. On Windows, an
|
package/docs/temp.md
CHANGED
|
@@ -262,13 +262,17 @@ 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
|
|
270
270
|
writer before returning. Its return value is preserved as `result`.
|
|
271
271
|
|
|
272
|
+
Generated temp filenames suffix Windows reserved-device basenames on every
|
|
273
|
+
platform. A completed sibling staging component that still resolves as a
|
|
274
|
+
Windows device alias rejects with `invalid-path` before hooks or producers run.
|
|
275
|
+
|
|
272
276
|
The helper retains one descriptor through requested mode application, opt-in
|
|
273
277
|
file synchronization, rename, and publication verification. It opens read-only
|
|
274
278
|
unless file synchronization is requested, so closed read-only producer output
|
|
@@ -292,12 +296,40 @@ Omitting either option or passing `false` skips that sync, never the identity
|
|
|
292
296
|
checks. Parent synchronization can be unsupported or fail without rejecting
|
|
293
297
|
the write, so success is not a strict crash-durability receipt.
|
|
294
298
|
|
|
295
|
-
|
|
296
|
-
single-link regular-file checks still agree.
|
|
299
|
+
Without producer isolation, cleanup only unlinks an admitted file while the
|
|
300
|
+
parent, pathname identity, and single-link regular-file checks still agree.
|
|
301
|
+
Observed substitutes are preserved,
|
|
297
302
|
including during process-exit cleanup. Operational cleanup failures retain an
|
|
298
303
|
identity-bound exit retry. If the callback throws or admission fails, no file
|
|
299
304
|
has been adopted: even a regular partial file is left for caller-directed
|
|
300
|
-
recovery. The helper never recursively removes a sibling temp.
|
|
305
|
+
recovery. The helper never recursively removes a sibling temp file path.
|
|
306
|
+
|
|
307
|
+
Set `producerIsolation: "private-directory"` in `WriteSiblingTempFileOptions`
|
|
308
|
+
when the producer can leave partial output before throwing. The callback then
|
|
309
|
+
receives an initially absent file path inside a private child workspace under
|
|
310
|
+
`dir`, on the same filesystem as the final target. fs-safe captures directory
|
|
311
|
+
cleanup ownership before invoking the callback. A callback exception triggers
|
|
312
|
+
owned workspace cleanup, including partial output, subject to directory
|
|
313
|
+
identity checks and I/O failures. The callback must still finish and close its
|
|
314
|
+
writer before returning.
|
|
315
|
+
|
|
316
|
+
After the callback succeeds, `Root.move` checks source aliases and moves the
|
|
317
|
+
output to the ordinary sibling path before file admission. An escaping symlink
|
|
318
|
+
can fail with `path-alias` at this step. Rejected output still inside the owned
|
|
319
|
+
workspace follows its cleanup contract. Once output moves to the sibling path,
|
|
320
|
+
failures before file adoption retain it for caller-directed recovery, as above.
|
|
321
|
+
File admission, requested modes, sync options, and final rename keep their
|
|
322
|
+
existing contracts; `resolveFinalPath(result)` still names a direct child of `dir`.
|
|
323
|
+
|
|
324
|
+
The isolated path retains exact bigint identities for both the parent and the
|
|
325
|
+
workspace and rechecks them before moving output to the sibling path. An
|
|
326
|
+
observed replacement is rejected. Cleanup uses the existing
|
|
327
|
+
[`withTempFile` ownership contract](#withtempfile), backed by [`tempFile`](#tempfile):
|
|
328
|
+
moving or replacing the parent or workspace can leave the original or
|
|
329
|
+
replacement paths in place. This option does not promise cleanup through a
|
|
330
|
+
retained directory after a rename, stronger permissions, or additional crash
|
|
331
|
+
durability. Omitting it preserves the direct sibling callback path and
|
|
332
|
+
unadmitted partial-file retention.
|
|
301
333
|
|
|
302
334
|
On POSIX, admission uses no-follow and nonblocking open flags, so a FIFO swap
|
|
303
335
|
does not block the helper. Windows retains Node's guarded pathname-open behavior
|
|
@@ -345,7 +377,7 @@ If `replaceFileAtomic` does what you need, prefer that. Use
|
|
|
345
377
|
the final destination still needs root-boundary checks.
|
|
346
378
|
Its private workspace uses the same identity-aware directory cleanup as
|
|
347
379
|
`tempFile()`: moving and replacing the workspace preserves the replacement.
|
|
348
|
-
The callback staging component is capped at 255 bytes under NFC and NFD by
|
|
380
|
+
The callback staging component is capped at 255 bytes as written and under NFC and NFD by
|
|
349
381
|
shortening only an overlong embedded destination tail, while preserving an
|
|
350
382
|
extension when possible. Short callback paths and the final target stay
|
|
351
383
|
unchanged. This workspace owns its contents, unlike the unadmitted sibling
|
|
@@ -440,6 +472,7 @@ import fs from "node:fs/promises";
|
|
|
440
472
|
|
|
441
473
|
const r = await writeSiblingTempFile({
|
|
442
474
|
dir: "/srv/cache",
|
|
475
|
+
producerIsolation: "private-directory",
|
|
443
476
|
writeTemp: async (tempPath) => {
|
|
444
477
|
const handle = await fs.open(tempPath, "w");
|
|
445
478
|
try {
|
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`
|
package/docs/writing.md
CHANGED
|
@@ -154,6 +154,61 @@ try {
|
|
|
154
154
|
}
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
### Streamed creation
|
|
158
|
+
|
|
159
|
+
Pass an `AsyncIterable<Uint8Array>` to `create()` when bytes come from a database,
|
|
160
|
+
network response, or another incremental producer. Buffers are accepted chunks.
|
|
161
|
+
The writer consumes each chunk completely before requesting the next one; it
|
|
162
|
+
does not collect the full input in memory or expose a writable descriptor.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
async function* snapshotChunks(): AsyncGenerator<Uint8Array> {
|
|
166
|
+
yield Buffer.from("first stored chunk\n");
|
|
167
|
+
yield Buffer.from("second stored chunk\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await fs.create("restored/config.txt", snapshotChunks(), {
|
|
171
|
+
mode: 0o600,
|
|
172
|
+
maxBytes: 8 * 1024 * 1024,
|
|
173
|
+
durable: false,
|
|
174
|
+
signal: AbortSignal.timeout(30_000),
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`RootCreateStreamOptions` keeps `mkdir`, `mode`, `durable`,
|
|
179
|
+
`assertBeforeMutation`, `denyMutations`, and `mutationSymlinks` from buffered
|
|
180
|
+
creation and adds `maxBytes` and `signal`. Byte chunks have no encoding option;
|
|
181
|
+
streamed creation uses strict publication identity and does not support
|
|
182
|
+
`renameIdentity: "verify-content-with-lock"`. Existing Root defaults apply,
|
|
183
|
+
including explicit `maxBytes`; with no byte cap at either level, input size is
|
|
184
|
+
unlimited. Zero permits an empty input only. Invalid limits reject before I/O.
|
|
185
|
+
|
|
186
|
+
Unlike buffered creation's JavaScript fallback, streamed creation stages all
|
|
187
|
+
chunks before publishing the final name. Native mode uses no-replace rename;
|
|
188
|
+
the JavaScript fallback hardlinks the completed stage and removes its temporary
|
|
189
|
+
name in the same JavaScript turn. That fallback requires a filesystem supporting
|
|
190
|
+
hardlinks; other processes may briefly observe both names. An existing or
|
|
191
|
+
concurrently created destination is preserved. Existing-target preflight does
|
|
192
|
+
not consume the input. The final mode and durability policy use the same guarded
|
|
193
|
+
writer as other Root operations.
|
|
194
|
+
|
|
195
|
+
Cancellation checks run before and after producer pulls, before content writes,
|
|
196
|
+
and before publication. `assertBeforeMutation` also rechecks current application
|
|
197
|
+
authority after producer waits and before each partial write. The operation
|
|
198
|
+
waits for any pending producer pull or filesystem write, then awaits the
|
|
199
|
+
producer's `return()` and cleans only the owned unpublished stage. Pass the same
|
|
200
|
+
signal into a producer that may stall: an arbitrary async iterator cannot be
|
|
201
|
+
forcibly interrupted, so cancellation waits for its pending work and cleanup to
|
|
202
|
+
settle. Do not mutate a yielded chunk until the next pull. Producer errors retain
|
|
203
|
+
their original value when cleanup succeeds.
|
|
204
|
+
|
|
205
|
+
An aborted or failed operation can leave created parent directories. If a
|
|
206
|
+
stage's identity or parent cannot be verified during cleanup, the existing
|
|
207
|
+
guarded cleanup preserves it. After publication, later verification or cleanup
|
|
208
|
+
failures preserve the destination; rejection does not prove that no file was
|
|
209
|
+
created. Cancellation arriving after publication does not undo the completed
|
|
210
|
+
file. Application recovery remains caller-owned.
|
|
211
|
+
|
|
157
212
|
### `fs.writeJson(rel, value, options?)`
|
|
158
213
|
|
|
159
214
|
`JSON.stringify(value, replacer, space)` + atomic write. Adds a trailing newline by default.
|
|
@@ -230,6 +285,101 @@ await fs.remove("snapshots/empty-dir"); // ok
|
|
|
230
285
|
await fs.remove("snapshots/full-dir"); // throws not-empty
|
|
231
286
|
```
|
|
232
287
|
|
|
288
|
+
For a tree, opt into bounded recursive removal:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
await fs.remove("scratch/finished-job", {
|
|
292
|
+
recursive: true,
|
|
293
|
+
force: true,
|
|
294
|
+
maxEntries: 20_000,
|
|
295
|
+
maxDepth: 32,
|
|
296
|
+
mutationSymlinks: "reject",
|
|
297
|
+
signal: AbortSignal.timeout(30_000),
|
|
298
|
+
assertBeforeMutation: () => assertJobLeaseCurrent(),
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
| Option | Default / behavior |
|
|
303
|
+
| --- | --- |
|
|
304
|
+
| `recursive` | `false`; opt in to removing non-empty directories. |
|
|
305
|
+
| `force` | `false`; `true` tolerates missing targets and vanished child entries. Other errors still reject. |
|
|
306
|
+
| `order` | `"filesystem"`; `"sorted"` collects and sorts child names lexicographically before descending. Requires `recursive: true`. |
|
|
307
|
+
| `maxEntries` | `100_000` in recursive mode; counts the requested target and every encountered child, including directories and symlinks. |
|
|
308
|
+
| `maxDepth` | `64` in recursive mode; the requested target has depth 0 and each child adds one. An empty directory at the limit can be removed. |
|
|
309
|
+
| `signal` | Stops traversal and new mutations when aborted. Already dispatched work settles and directory handles close before rejection. |
|
|
310
|
+
|
|
311
|
+
Budgets must be non-negative safe integers or explicit `Infinity`, and require
|
|
312
|
+
`recursive: true`. Omitted budgets retain their finite defaults. An entry or
|
|
313
|
+
depth limit throws `too-large` before processing the over-budget entry.
|
|
314
|
+
|
|
315
|
+
The default filesystem order streams each directory once, using memory and open
|
|
316
|
+
handles proportional to depth rather than directory width. Sorted order also
|
|
317
|
+
enumerates each directory once, but collects its names before descending and
|
|
318
|
+
deletes directories after their children. It uses JavaScript's default string
|
|
319
|
+
sort, not locale collation. Collected names consume the shared entry budget,
|
|
320
|
+
including sibling names still pending while an earlier directory is traversed.
|
|
321
|
+
With a finite budget, collection overflow rejects before processing that
|
|
322
|
+
directory's children; one extra name distinguishes an exact limit from overflow.
|
|
323
|
+
An `Infinity` entry budget uses a bulk name read while retaining the opened
|
|
324
|
+
directory handle and identity checks. Sorted mode retains collected names, so
|
|
325
|
+
unlimited budgets also permit unlimited name storage.
|
|
326
|
+
|
|
327
|
+
Sorted traversal preserves lexicographic processing, not the incidental syscall
|
|
328
|
+
timing of a caller that repeatedly rescans parent directories. Each child is
|
|
329
|
+
inspected when visited. Newly added entries can make the final `rmdir` fail with
|
|
330
|
+
`not-empty`; the operation does not retry indefinitely.
|
|
331
|
+
|
|
332
|
+
Recursive removal never follows a discovered symlink or junction. With an
|
|
333
|
+
omitted `mutationSymlinks` policy it unlinks that entry, preserving the existing
|
|
334
|
+
nonrecursive behavior. Both explicit mutation policies reject discovered links.
|
|
335
|
+
`follow-parents-within-root` permits aliases only in the requested target's
|
|
336
|
+
parents, and still rejects a final link. Root and per-call `denyMutations` policies
|
|
337
|
+
remain additive; a denied descendant prevents removal of the enclosing requested
|
|
338
|
+
tree before any entry is removed.
|
|
339
|
+
|
|
340
|
+
The operation retains exact identities for the traversal directories and each
|
|
341
|
+
observed target. Swapped or missing ancestors reject even with `force: true`;
|
|
342
|
+
an abort reason or authority refusal carrying `ENOENT` is not treated as absence.
|
|
343
|
+
Authority is rechecked immediately before each direct `unlink` or `rmdir`
|
|
344
|
+
dispatch. Directory handles close before their directories are removed, including
|
|
345
|
+
on Windows. If both traversal and close fail, `SuppressedError` retains both
|
|
346
|
+
failures. Directory-stream filesystem errors use the same removal codes as
|
|
347
|
+
`unlink` and `rmdir`, with the original error in `cause`; caller abort and
|
|
348
|
+
authority refusals retain their original values.
|
|
349
|
+
|
|
350
|
+
When `force: true` encounters a missing directory during an `opendir` or read,
|
|
351
|
+
it closes any open stream and reaches the ordinary final target check. The
|
|
352
|
+
surviving ancestors and original target identity are checked again; a replacement
|
|
353
|
+
is never accepted as a missing directory. An actually vanished child does not
|
|
354
|
+
prevent processing later siblings.
|
|
355
|
+
|
|
356
|
+
Filesystem failures normalized by the recursive removal owner and its direct
|
|
357
|
+
symlink rejections carry additional `FsSafeError.details` context:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
type RemoveFailureDetails = {
|
|
361
|
+
operation: "remove";
|
|
362
|
+
phase: "enumerate" | "inspect" | "remove";
|
|
363
|
+
relativePath: string;
|
|
364
|
+
};
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`relativePath` is relative to the requested removal target, using host path
|
|
368
|
+
separators: `""` identifies that target and `"nested/link"` identifies a child
|
|
369
|
+
on POSIX. It does not replace caller spelling with the canonical Root path.
|
|
370
|
+
`enumerate` covers directory stream operations, `inspect` covers initial child
|
|
371
|
+
observations, and `remove` covers final identity checks and unlink/rmdir failures.
|
|
372
|
+
The existing error codes, messages, and causes remain unchanged. Errors that
|
|
373
|
+
already have their own identity, including caller authority and cancellation
|
|
374
|
+
reasons, are propagated without adding or changing their details. Context is
|
|
375
|
+
diagnostic; it is not permission to retry or mutate an entry.
|
|
376
|
+
|
|
377
|
+
Removal is incremental, not atomic. A later budget, cancellation, identity, or
|
|
378
|
+
filesystem failure does not restore already removed entries. As with existing
|
|
379
|
+
`remove`, this is a guarded JavaScript operation in every native mode: pathname
|
|
380
|
+
checks are best-effort against a hostile concurrent process and do not create
|
|
381
|
+
an atomic check-and-delete syscall. Use OS isolation for that threat model.
|
|
382
|
+
|
|
233
383
|
### `fs.mkdir(rel)`
|
|
234
384
|
|
|
235
385
|
`mkdir -p`. Creates missing parents.
|
|
@@ -265,9 +415,9 @@ try {
|
|
|
265
415
|
Options are `{ denyMutations?, mkdir?, mode?, writeMode? }`, where `writeMode`
|
|
266
416
|
is `"replace"` (default), `"append"`, or `"update"`. `replace` truncates existing
|
|
267
417
|
files; `update` keeps existing contents. Streaming writes go directly to the
|
|
268
|
-
destination — there is no atomic-rename step.
|
|
269
|
-
|
|
270
|
-
[`atomic`](atomic.md) helpers
|
|
418
|
+
destination — there is no atomic-rename step. For exclusive publication of a
|
|
419
|
+
complete stream, use [`create()`](#streamed-creation). For streamed replacement,
|
|
420
|
+
the [`atomic`](atomic.md) helpers provide a staged writer.
|
|
271
421
|
|
|
272
422
|
On POSIX, existing-target opens use `O_NONBLOCK` as an admission safeguard so
|
|
273
423
|
a no-reader FIFO cannot stall regular-file validation. This does not change
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/fs-safe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
"types": "./dist/copy.d.ts",
|
|
51
51
|
"default": "./dist/copy.js"
|
|
52
52
|
},
|
|
53
|
+
"./guest": {
|
|
54
|
+
"types": "./dist/guest.d.ts",
|
|
55
|
+
"default": "./dist/guest.js"
|
|
56
|
+
},
|
|
53
57
|
"./config": {
|
|
54
58
|
"types": "./dist/config.d.ts",
|
|
55
59
|
"default": "./dist/config.js"
|
|
@@ -137,6 +141,8 @@
|
|
|
137
141
|
"lint:fs-boundary": "node scripts/check-fs-boundary-primitives.mjs",
|
|
138
142
|
"prepack": "node scripts/prepack-build.mjs",
|
|
139
143
|
"test": "vitest run",
|
|
144
|
+
"test:bun": "bun node_modules/vitest/vitest.mjs run --config scripts/bun-vitest.config.ts",
|
|
145
|
+
"test:bun:native": "bun scripts/bun-native-proof.mjs && bun node_modules/vitest/vitest.mjs run --config scripts/bun-native-vitest.config.ts",
|
|
140
146
|
"test:coverage": "vitest run --coverage",
|
|
141
147
|
"test:coverage:collect": "pnpm build && vitest run --coverage --coverage.reporter=json --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.statements=0 --coverage.thresholds.branches=0",
|
|
142
148
|
"test:coverage:merge": "node scripts/merge-coverage.mjs",
|
|
@@ -161,13 +167,13 @@
|
|
|
161
167
|
"archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
|
|
162
168
|
},
|
|
163
169
|
"optionalDependencies": {
|
|
164
|
-
"@openclaw/fs-safe-darwin-arm64": "0.
|
|
165
|
-
"@openclaw/fs-safe-darwin-x64": "0.
|
|
166
|
-
"@openclaw/fs-safe-linux-arm64-gnu": "0.
|
|
167
|
-
"@openclaw/fs-safe-linux-arm64-musl": "0.
|
|
168
|
-
"@openclaw/fs-safe-linux-x64-gnu": "0.
|
|
169
|
-
"@openclaw/fs-safe-linux-x64-musl": "0.
|
|
170
|
-
"@openclaw/fs-safe-win32-x64-msvc": "0.
|
|
170
|
+
"@openclaw/fs-safe-darwin-arm64": "0.12.0",
|
|
171
|
+
"@openclaw/fs-safe-darwin-x64": "0.12.0",
|
|
172
|
+
"@openclaw/fs-safe-linux-arm64-gnu": "0.12.0",
|
|
173
|
+
"@openclaw/fs-safe-linux-arm64-musl": "0.12.0",
|
|
174
|
+
"@openclaw/fs-safe-linux-x64-gnu": "0.12.0",
|
|
175
|
+
"@openclaw/fs-safe-linux-x64-musl": "0.12.0",
|
|
176
|
+
"@openclaw/fs-safe-win32-x64-msvc": "0.12.0",
|
|
171
177
|
"jszip": "^3.10.2"
|
|
172
178
|
},
|
|
173
179
|
"devDependencies": {
|