@openclaw/fs-safe 0.16.0 → 0.17.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 +48 -0
- package/README.md +8 -1
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +2 -8
- package/dist/advanced.d.ts +2 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +22 -23
- package/dist/archive-merge.d.ts +1 -0
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +4 -4
- package/dist/archive-native.d.ts +1 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +1 -0
- package/dist/archive-options.d.ts +2 -0
- package/dist/archive-options.d.ts.map +1 -1
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-zip-count.d.ts.map +1 -1
- package/dist/archive-zip-count.js +21 -1
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +23 -1
- package/dist/archive-zip-loader.d.ts +2 -0
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +7 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +7 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -3
- package/dist/byte-view.d.ts +3 -0
- package/dist/byte-view.d.ts.map +1 -0
- package/dist/byte-view.js +13 -0
- package/dist/creation-darwin.d.ts +0 -1
- package/dist/creation-darwin.d.ts.map +1 -1
- package/dist/creation-darwin.js +0 -9
- package/dist/directory-durability.d.ts +6 -6
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-receipt.d.ts +2 -2
- package/dist/directory-receipt.d.ts.map +1 -1
- package/dist/directory-receipt.js +15 -19
- package/dist/file-cleanup.d.ts +1 -0
- package/dist/file-cleanup.d.ts.map +1 -1
- package/dist/file-cleanup.js +7 -4
- package/dist/file-contents.d.ts +6 -0
- package/dist/file-contents.d.ts.map +1 -0
- package/dist/file-contents.js +40 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +16 -4
- package/dist/file-lock-sync-root-acquire.d.ts.map +1 -1
- package/dist/file-lock-sync-root-acquire.js +3 -0
- package/dist/file-lock-sync-root-held.d.ts +1 -2
- package/dist/file-lock-sync-root-held.d.ts.map +1 -1
- package/dist/file-lock-sync-root-held.js +7 -5
- package/dist/file-lock-sync-stale-admission.d.ts.map +1 -1
- package/dist/file-lock-sync-stale-admission.js +3 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -11
- package/dist/file-store.js +3 -3
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +6 -27
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +2 -5
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +2 -6
- package/dist/json-durable-queue-paths.d.ts.map +1 -1
- package/dist/json-durable-queue-paths.js +2 -24
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +10 -9
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +32 -75
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +19 -21
- package/dist/move-path-cleanup.d.ts +5 -19
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +57 -21
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +62 -39
- package/dist/native-staged-file.d.ts +2 -1
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +8 -5
- package/dist/native.js +2 -2
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +11 -2
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +2 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +3 -17
- package/dist/pinned-write-input.d.ts.map +1 -1
- package/dist/pinned-write-input.js +11 -1
- package/dist/pinned-write-mode.d.ts +3 -3
- package/dist/pinned-write-mode.d.ts.map +1 -1
- package/dist/pinned-write-mode.js +15 -8
- package/dist/pinned-write-staged.d.ts.map +1 -1
- package/dist/pinned-write-staged.js +10 -11
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +17 -13
- package/dist/publish-file.d.ts +2 -2
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +56 -96
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +35 -44
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +28 -26
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +13 -22
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +10 -16
- package/dist/replace-file-temp-owner.js +6 -6
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +9 -13
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +20 -3
- package/dist/root-file-final-admission.d.ts +1 -1
- package/dist/root-file-final-admission.d.ts.map +1 -1
- package/dist/root-file-final-admission.js +5 -2
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +3 -2
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +50 -20
- package/dist/root-move-noreplace.d.ts +2 -0
- package/dist/root-move-noreplace.d.ts.map +1 -1
- package/dist/root-move-noreplace.js +2 -2
- package/dist/root-read-admission.d.ts.map +1 -1
- package/dist/root-read-admission.js +7 -2
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +15 -1
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +4 -6
- package/dist/sidecar-lock-handle.d.ts +3 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +6 -0
- package/dist/sidecar-lock-reclaim.d.ts +1 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +11 -8
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +3 -5
- package/dist/staged-directory.d.ts +2 -2
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/strict-file-identity.d.ts +1 -1
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +9 -9
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +2 -27
- package/dist/temp-workspace-owner.js +4 -4
- package/dist/unicode-path.d.ts.map +1 -1
- package/dist/unicode-path.js +3 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +4 -2
- package/dist/write-file-handle.d.ts +7 -0
- package/dist/write-file-handle.d.ts.map +1 -1
- package/dist/write-file-handle.js +23 -0
- package/dist/write-open-flags.d.ts.map +1 -1
- package/dist/write-open-flags.js +1 -8
- package/dist/write-queue.d.ts.map +1 -1
- package/dist/write-queue.js +1 -4
- package/docs/advanced.md +68 -1
- package/docs/archive.md +41 -2
- package/docs/atomic.md +29 -5
- package/docs/contributing.md +4 -0
- package/docs/creation.md +8 -4
- package/docs/durability.md +35 -0
- package/docs/file-contents.md +68 -0
- package/docs/json.md +5 -4
- package/docs/local-roots.md +2 -0
- package/docs/mutation-policy-proof.md +5 -3
- package/docs/native.md +9 -8
- package/docs/path.md +4 -4
- package/docs/public-api.md +5 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +2 -2
- package/docs/regular-file.md +3 -0
- package/docs/root.md +9 -0
- package/docs/sidecar-lock.md +9 -1
- package/docs/staged-file.md +4 -3
- package/docs/store.md +3 -1
- package/docs/temp.md +4 -1
- package/docs/types.md +18 -2
- package/docs/walk.md +7 -0
- package/docs/writing.md +9 -2
- package/package.json +8 -8
package/docs/advanced.md
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
|
|
20
20
|
## What lives here
|
|
21
21
|
|
|
22
|
-
The exports group into a handful of themes.
|
|
22
|
+
The exports group into a handful of themes. Documented helpers link to their contract below or a dedicated page; everything else is reference-only and tracked here.
|
|
23
23
|
|
|
24
24
|
### Path scopes and root paths
|
|
25
25
|
|
|
@@ -71,7 +71,9 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
|
|
|
71
71
|
| `readFileDescriptorBounded`, `readFileDescriptorBoundedSync`, `readFileHandleBounded` | – | Incremental whole-file reads for already-open descriptors/handles. They consume at most `maxBytes + 1`, do not close the input, and throw `FsSafeError("too-large")` on overflow. |
|
|
72
72
|
| `createDirectory`, `createDirectorySync`, `createFileSync` | [Exclusive leaf creation](creation.md) | Create one exclusive entry under an existing trusted parent, optionally with private permissions; file creation returns an owned disposable descriptor. |
|
|
73
73
|
| `readFileWindowFully`, `readFileWindowFullySync`, `ReadFileWindowOptions` | [positional-read.md](positional-read.md) | Fill a caller-owned buffer at an explicit file position, completing short reads and returning the EOF count without moving or closing the descriptor. |
|
|
74
|
+
| `writeFileWindowFully`, `WriteFileWindowOptions` | [Borrowed-handle writes](#borrowed-handle-writes) | Write all supplied bytes at an explicit position or the current cursor, completing short writes with cancellation and per-write authority checks. |
|
|
74
75
|
| `copyFileHandle`, `copyFileDescriptorSync`, `CopyFileHandleOptions` | [copy.md](copy.md#borrowed-filehandle-transfers) | Copy caller-owned regular files through async handles or sync descriptors from position zero with byte limits and synchronous callbacks; preserves cursors and leaves publication and cleanup to the caller. |
|
|
76
|
+
| `sameFileContentsSync`, `SameFileContentsOptions` | [Exact file comparison](file-contents.md) | Compare borrowed regular-file descriptors byte for byte through EOF with bounded memory and an optional per-file byte limit, preserving both cursors and lifetimes. |
|
|
75
77
|
| `overwriteFileHandle`, `OverwriteFileHandleOptions` | [in-place-write.md](in-place-write.md) | Overwrite a borrowed read/write handle with prefix-only preparation and best-effort rollback; preserves its inode, cursor, and caller-owned lifetime. |
|
|
76
78
|
| `openRootFile`, `openRootFileSync`, `canUseRootFileOpen`, `matchRootFileOpenFailure`, related types | – | Low-level root-bounded open; rejects every symlink component by default, with `symlinks: "follow-parents-within-root"` for contained parent aliases or `"follow-within-root"` for final links too. |
|
|
77
79
|
| `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
|
|
@@ -151,6 +153,71 @@ component is followed by another segment, both helpers throw
|
|
|
151
153
|
`FsSafeError("not-file")` before the platform can expose that state as POSIX
|
|
152
154
|
`ENOTDIR` or Windows `ENOENT`.
|
|
153
155
|
|
|
156
|
+
#### Borrowed-handle writes
|
|
157
|
+
|
|
158
|
+
Use `writeFileWindowFully()` when you already own a writable file handle and
|
|
159
|
+
need to complete a byte-window write, including positive short writes.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { root } from "@openclaw/fs-safe";
|
|
163
|
+
import { writeFileWindowFully } from "@openclaw/fs-safe/advanced";
|
|
164
|
+
|
|
165
|
+
const workspace = await root("/srv/workspace");
|
|
166
|
+
await using opened = await workspace.openWritable("record.bin", { writeMode: "update" });
|
|
167
|
+
await writeFileWindowFully(opened.handle, Buffer.from([1, 2, 3]), 16);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
type WriteFileWindowOptions = {
|
|
172
|
+
signal?: AbortSignal;
|
|
173
|
+
assertBeforeMutation?: () => void;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function writeFileWindowFully(
|
|
177
|
+
handle: import("node:fs/promises").FileHandle,
|
|
178
|
+
bytes: Uint8Array,
|
|
179
|
+
position: number | null,
|
|
180
|
+
options?: WriteFileWindowOptions,
|
|
181
|
+
): Promise<void>;
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A numeric `position` writes at that offset without moving the handle's cursor.
|
|
185
|
+
It and the exclusive window end (`position + bytes.byteLength`) must be
|
|
186
|
+
non-negative safe integers; invalid ranges throw `RangeError` before mutation.
|
|
187
|
+
Bounds come from the intrinsic byte view, ignoring shadowed metadata properties.
|
|
188
|
+
Pass `null` to write at and advance the current cursor. Each syscall writes at
|
|
189
|
+
most 512 KiB. A write that makes no progress throws
|
|
190
|
+
`FsSafeError("helper-failed")`; filesystem errors propagate unchanged.
|
|
191
|
+
Empty input still validates the range and checks cancellation, but performs no
|
|
192
|
+
I/O and does not call `assertBeforeMutation`.
|
|
193
|
+
|
|
194
|
+
The caller must supply a writable regular-file handle, opened **without append
|
|
195
|
+
mode** for numeric positions. Some operating systems ignore positioned-write
|
|
196
|
+
offsets on append handles, and this helper does not inspect file type or open
|
|
197
|
+
flags. Opening, path admission, identity checks, and closing remain the caller's
|
|
198
|
+
responsibility. Keep the handle open and the borrowed bytes unchanged, attached,
|
|
199
|
+
and accessible until the promise settles; avoid concurrent I/O when it can change
|
|
200
|
+
the intended contents or shared cursor. The helper does not acquire a lock.
|
|
201
|
+
|
|
202
|
+
`assertBeforeMutation` runs synchronously immediately before every write,
|
|
203
|
+
including short-write retries. A thrown value propagates unchanged; a Promise or
|
|
204
|
+
thenable return rejects with `TypeError` before that write. The callback must not
|
|
205
|
+
modify the payload or handle. It does not run as a final completion check; the
|
|
206
|
+
caller owns any authority check before later publication or other mutations.
|
|
207
|
+
|
|
208
|
+
`signal` is checked at admission, before and after each authority callback, and
|
|
209
|
+
after each pending write settles. Cancellation waits for an in-flight write and
|
|
210
|
+
then rejects with the signal's reason without starting another syscall. If that
|
|
211
|
+
write fails, its filesystem error or zero-progress failure takes precedence over cancellation. Already
|
|
212
|
+
written bytes remain changed; there is no rollback or hidden write after the
|
|
213
|
+
promise settles.
|
|
214
|
+
|
|
215
|
+
The helper neither truncates an existing suffix nor changes permissions,
|
|
216
|
+
synchronizes, or closes the handle. Callers retain those responsibilities and
|
|
217
|
+
any wider transaction policy. For complete replacement with best-effort
|
|
218
|
+
rollback, use [`overwriteFileHandle()`](in-place-write.md); for root-bounded
|
|
219
|
+
atomic replacement, use [`Root.write()`](writing.md).
|
|
220
|
+
|
|
154
221
|
### Local roots and file URLs
|
|
155
222
|
|
|
156
223
|
| Export | Page | Notes |
|
package/docs/archive.md
CHANGED
|
@@ -11,6 +11,14 @@ These TAR routes work with all optional dependencies omitted and need no
|
|
|
11
11
|
runtime interpreter, download, install script, or consumer compiler toolchain.
|
|
12
12
|
ZIP fallback still requires optional `jszip`.
|
|
13
13
|
|
|
14
|
+
The shared TAR parser reuses the already-validated owned path for ordinary
|
|
15
|
+
members. Original header names and USTAR prefixes still undergo validation
|
|
16
|
+
even when PAX or GNU metadata supplies an override; effective override paths
|
|
17
|
+
retain their separate checks. Empty USTAR prefixes retain field decoding and
|
|
18
|
+
padding checks; path validation applies to nonempty prefixes. Joining an
|
|
19
|
+
admitted prefix and name with a separator preserves their checked components,
|
|
20
|
+
so the parser does not repeat the same component validation on the joined path.
|
|
21
|
+
|
|
14
22
|
`auto` prefers an available native binding; a native operation failure is
|
|
15
23
|
terminal and never retries through WASM. `require` rejects a missing binding
|
|
16
24
|
with `FsSafeError("helper-unavailable")`, including for zstd/bzip2 suffix
|
|
@@ -31,6 +39,7 @@ await extractArchive({
|
|
|
31
39
|
timeoutMs: 15_000, // hard budget; active destination mutation is joined
|
|
32
40
|
stripComponents: 0, // tar-style strip-leading-dirs
|
|
33
41
|
entryModes: "clamp", // default; use "preserve" for archive rwx bits
|
|
42
|
+
entryUmask: 0, // default; remove these bits from final modes
|
|
34
43
|
entryFilter: ({ path, kind, size }) => "extract",
|
|
35
44
|
onFiltered: "reject-archive", // default; opt into "skip-entry" explicitly
|
|
36
45
|
limits: {
|
|
@@ -50,7 +59,7 @@ await extractArchive({
|
|
|
50
59
|
type ExtractArchiveOptions = {
|
|
51
60
|
archivePath: string; // absolute path to the archive
|
|
52
61
|
destDir: string; // absolute destination directory; must already exist
|
|
53
|
-
timeoutMs: number; // positive
|
|
62
|
+
timeoutMs: number; // positive elapsed-time budget; <= 0/non-finite disables it
|
|
54
63
|
durable?: boolean; // false; opt into syncing published files and directories before completion
|
|
55
64
|
kind?: ArchiveKind; // "zip" | "tar" | "tar-zstd" | "tar-bzip2"
|
|
56
65
|
stripComponents?: number; // strip N leading dirs from entry paths
|
|
@@ -58,6 +67,7 @@ type ExtractArchiveOptions = {
|
|
|
58
67
|
limits?: ArchiveExtractLimits;
|
|
59
68
|
logger?: ArchiveLogger; // { info?, warn? }
|
|
60
69
|
entryModes?: "clamp" | "preserve";
|
|
70
|
+
entryUmask?: number; // integer 0..0o777; defaults to 0
|
|
61
71
|
entryFilter?: (entry: { path: string; kind: ArchiveEntryKind; size: number }) =>
|
|
62
72
|
"extract" | "skip";
|
|
63
73
|
onFiltered?: "reject-archive" | "skip-entry";
|
|
@@ -71,6 +81,12 @@ once, deepest first, and finally the destination directory. All work stays insid
|
|
|
71
81
|
the extraction deadline; active syncs are joined before rejection. File sync
|
|
72
82
|
failures use the same error surface as `Root.copyIn()`; directory I/O failures
|
|
73
83
|
also reject, with the existing platform limitations on directory flushing.
|
|
84
|
+
|
|
85
|
+
Deadline checks use a monotonic clock, including before queued mutations start
|
|
86
|
+
and before reporting success. Synchronous caller code can delay the timer, but
|
|
87
|
+
cannot permit the next operation after the budget expires. This does not
|
|
88
|
+
interrupt a callback halfway through execution or replace its own thrown error;
|
|
89
|
+
active destination mutations are still joined before timeout rejection.
|
|
74
90
|
Files whose final mode prevents reading, including `0o000` and write-only files,
|
|
75
91
|
sync once through the copy's retained descriptor during publication. Permissions
|
|
76
92
|
are never widened to reopen them. Directory modes are finalized after the file
|
|
@@ -103,6 +119,15 @@ including a mode containing only stripped special bits, stays zero under
|
|
|
103
119
|
directories; ZIP UNIX creator records with zero attributes are explicit zero,
|
|
104
120
|
while non-UNIX ZIP records use the absent-metadata defaults.
|
|
105
121
|
|
|
122
|
+
`entryUmask` removes permission bits after the selected mode policy: final modes
|
|
123
|
+
are the policy result `& ~entryUmask`. It applies to files, explicit directories,
|
|
124
|
+
and implicit parent directories, including existing destination directories.
|
|
125
|
+
The destination root and private staging modes are unchanged. The default `0`
|
|
126
|
+
preserves existing behavior; invalid masks reject before extraction begins.
|
|
127
|
+
fs-safe neither reads nor changes the process umask. Pass
|
|
128
|
+
`entryUmask: process.umask()` explicitly when that is the caller's policy.
|
|
129
|
+
Windows retains the POSIX-mode limitations described below.
|
|
130
|
+
|
|
106
131
|
TAR mode fields containing only NUL/ASCII-space padding use absent defaults.
|
|
107
132
|
Both backends recognize GNU binary modes, including signed values,
|
|
108
133
|
within JavaScript's safe-integer range before masking permission bits.
|
|
@@ -116,7 +141,7 @@ stay `0o600` and directories `0o700` until publication. Files receive their fina
|
|
|
116
141
|
mode through the guarded copy's owned writer descriptor. Directories are pinned
|
|
117
142
|
before descending and finalized after their children, including empty and
|
|
118
143
|
restrictive directories. Explicit accepted directory modes win regardless of
|
|
119
|
-
archive order; implicit parents receive `0o755`. Existing destination directories
|
|
144
|
+
archive order; implicit parents receive `0o755 & ~entryUmask`. Existing destination directories
|
|
120
145
|
also receive the requested final mode. They are never temporarily widened to
|
|
121
146
|
allow child writes; insufficient write/search access still rejects.
|
|
122
147
|
|
|
@@ -154,6 +179,13 @@ between native and JavaScript paths rather than reimplementing it in Rust.
|
|
|
154
179
|
|
|
155
180
|
ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless internal separator and dot-component equivalence is allowed only after validation; every raw and Unicode interpretation must also agree on whether its name ends in `/` or `\`. Ordinary legacy filename decoding remains backend-selected. Native ZIP extraction groups nearby metadata reads into at most two 4 KiB read-ahead buffers per admission pass; larger records retain separately bounded reads. Buffered record views remain stable across eviction, and cached work periodically yields for deadline checks.
|
|
156
181
|
|
|
182
|
+
ZIP end-record admission searches the bounded comment window for signatures
|
|
183
|
+
while retaining complete comment-length and ambiguity checks. Dense signature
|
|
184
|
+
sequences fall back to the bounded byte scan.
|
|
185
|
+
The separate `readZipCentralDirectoryEntryCount(buffer)` hint uses bounded
|
|
186
|
+
reverse searches for comments and retains its latest-valid-record selection;
|
|
187
|
+
it does not replace strict archive admission.
|
|
188
|
+
|
|
157
189
|
ZIP admission establishes each entry's kind before callbacks: a high-word UNIX
|
|
158
190
|
symlink type takes precedence regardless of creator, followed by the DOS directory
|
|
159
191
|
bit, the exact UNIX directory type, or a terminal slash/backslash. Native manifests
|
|
@@ -185,6 +217,10 @@ decoded validation. Unicode Path admission is shared only when both the raw name
|
|
|
185
217
|
and the complete Unicode fields match; different fields still verify their own
|
|
186
218
|
CRC and interpretation. Shared backing memory is checked independently. Decoded
|
|
187
219
|
name validation is not reused across entries or archives.
|
|
220
|
+
UTF-8-flagged ASCII names in nonshared backing memory reuse their raw-path
|
|
221
|
+
validation, and an identical decoded spelling reuses its canonical key. Shared
|
|
222
|
+
name bytes still undergo independent decoding and validation; Unicode Path
|
|
223
|
+
fields retain their own CRC and interpretation checks.
|
|
188
224
|
|
|
189
225
|
`stripComponents` removes leading nonempty, non-`.` path components after
|
|
190
226
|
normalizing separators. For example, `./pkg/hello.txt` with
|
|
@@ -202,6 +238,9 @@ directory paths. For example, `./pkg//state\cache/value` is presented as
|
|
|
202
238
|
`pkg/state/cache/value`, even with `stripComponents: 1`. Case and Unicode
|
|
203
239
|
spelling are preserved. Local PAX `path`, GNU long-name, and supported ZIP
|
|
204
240
|
Unicode Path names use the same canonicalization.
|
|
241
|
+
Callbacks follow physical archive order, including ZIP names that look like
|
|
242
|
+
integer object keys. The public ZIP loader's `files` object retains ordinary
|
|
243
|
+
JavaScript object enumeration and mutation behavior.
|
|
205
244
|
|
|
206
245
|
Raw paths undergo traversal, absolute/drive-path, and NUL validation **before**
|
|
207
246
|
canonicalization; normalization cannot turn an unsafe path into an accepted
|
package/docs/atomic.md
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
|
|
17
17
|
Write `content` to a sibling temp file in the destination directory, apply the parent-directory and final file modes through verified descriptors, optionally `fsync` the file descriptor, optionally `fsync` the parent directory after rename, then atomically rename over the destination. No permission change follows a caller-supplied pathname.
|
|
18
18
|
|
|
19
|
-
On POSIX, the parent is opened with no-follow and directory-only flags, checked against its pre-open identity, and mode-adjusted through that descriptor. A replacement symlink is rejected rather than followed. If the directory cannot be opened for descriptor access, the operation fails closed instead of retrying by pathname. Windows does not enforce POSIX directory modes and Node cannot consistently open directory descriptors there, so `dirMode` is passed only to `mkdir`; no pathname `chmod` fallback is attempted.
|
|
19
|
+
On POSIX, the parent is opened with no-follow and directory-only flags, checked against its exact pre-open device/inode identity, and mode-adjusted through that descriptor. A replacement symlink is rejected rather than followed. If the directory cannot be opened for descriptor access, the operation fails closed instead of retrying by pathname. Windows does not enforce POSIX directory modes and Node cannot consistently open directory descriptors there, so `dirMode` is passed only to `mkdir`; no pathname `chmod` fallback is attempted.
|
|
20
20
|
|
|
21
21
|
Async replacements to the same destination are serialized inside the current process, so two overlapping `replaceFileAtomic()` calls do not interleave their temp-write/rename phases. Use a sidecar lock when multiple processes may write the same target.
|
|
22
22
|
|
|
@@ -92,10 +92,13 @@ await replaceFileAtomic({
|
|
|
92
92
|
|
|
93
93
|
If `beforeRename` throws, the rename is skipped and the owned temp file is removed — the destination is unchanged. Cleanup unlinks only the exact admitted single-link file; a substitute observed at the temp name is preserved and removed from cleanup authority. The same identity is rechecked before every rename retry, when entering copy fallback, and at the final name after rename. A post-rename verification failure reports the race without rolling back or deleting the published name.
|
|
94
94
|
|
|
95
|
-
JavaScript permits `beforeRename` callbacks to throw any value, including
|
|
95
|
+
JavaScript permits `beforeRename` callbacks and filesystem adapters to throw any value, including
|
|
96
96
|
`undefined`, `null`, `false`, signed zero, `0n`, an empty string, and `NaN`.
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
Atomic replacement preserves such operational failures when cleanup and close
|
|
98
|
+
succeed, including rename and post-rename verification failures. Rename retry
|
|
99
|
+
and copy-fallback classification reads the error code once without coercion;
|
|
100
|
+
missing or unreadable codes preserve the original failure. A rejected call has
|
|
101
|
+
no success receipt even if an adapter committed its rename before throwing. With
|
|
99
102
|
`throwOnCleanupError: true`, an additional owned-temp cleanup failure keeps the
|
|
100
103
|
existing cleanup wrapper whose `cause` is the original thrown value. A later
|
|
101
104
|
descriptor-close failure is reported in an `AggregateError`, in operation/cleanup
|
|
@@ -150,6 +153,14 @@ must not have aliases. The policy reads `nlink` from a pinned destination
|
|
|
150
153
|
descriptor, not pathname metadata, before rename and rechecks it in the copy
|
|
151
154
|
fallback.
|
|
152
155
|
|
|
156
|
+
Source and pinned destination admission compare exact bigint device/inode
|
|
157
|
+
observations, so distinct identities that round to the same JavaScript number
|
|
158
|
+
cannot authorize a copy. Unknown Windows identities get one bounded reinspection
|
|
159
|
+
of the same descriptor or path; incomplete or inconsistent observations fail
|
|
160
|
+
closed without reopening. Injected filesystem adapters must honor the
|
|
161
|
+
`{ bigint: true }` stat option. Source admission reuses that exact pair instead
|
|
162
|
+
of immediately repeating it with numeric metadata.
|
|
163
|
+
|
|
153
164
|
The default `copyFallbackRestore: "none"` preserves the existing fallback
|
|
154
165
|
contract: a failed copy can leave a partial destination. For state files where
|
|
155
166
|
preserving the old bytes is more important, choose `"restore-original"` and set
|
|
@@ -349,7 +360,10 @@ the preflight cap fails with `FsSafeError("too-large")`.
|
|
|
349
360
|
If another writer changes source entries during the fallback, the staged copy
|
|
350
361
|
throws `ESTALE` before commit when possible. If the destination has already
|
|
351
362
|
been committed, cleanup still preserves the changed source entries and throws
|
|
352
|
-
`ESTALE`.
|
|
363
|
+
`ESTALE`. Copied file and symlink manifests retain exact bigint identities and
|
|
364
|
+
nanosecond timestamps, so rounded file IDs cannot authorize copying or removal
|
|
365
|
+
of a different entry. Hardlink groups also use exact identities. Directory
|
|
366
|
+
manifests retain an exact bigint device/inode receipt from
|
|
353
367
|
copy admission. Each directory is rechecked after traversal, and the source root
|
|
354
368
|
is checked again before publication. Cleanup checks the same receipt before
|
|
355
369
|
removing children, then invokes mutation authority and rechecks the receipt and
|
|
@@ -366,6 +380,11 @@ unlink is verified through a remaining manifested alias and its exact resulting
|
|
|
366
380
|
identity becomes the next cleanup receipt. This accounts for the operation's
|
|
367
381
|
own link-count and ctime changes without suppressing unexpected external
|
|
368
382
|
mutations.
|
|
383
|
+
On Windows, opening a regular source may advance its ctime while all other
|
|
384
|
+
fingerprint fields match. That exception applies only to opening; post-copy
|
|
385
|
+
verification and cleanup retain their full fingerprint checks.
|
|
386
|
+
Copied aliases share each verified open-time update. Changes observed between
|
|
387
|
+
copies still reject instead of being mistaken for an owned open transition.
|
|
369
388
|
|
|
370
389
|
### Mutation authority and publication receipts
|
|
371
390
|
|
|
@@ -404,6 +423,11 @@ thenable, or any other value fails with a `TypeError`; rejected asynchronous
|
|
|
404
423
|
results are consumed. Perform asynchronous policy checks before calling the
|
|
405
424
|
helper and use the authority callback to recheck the current owner at each
|
|
406
425
|
mutation boundary. All callbacks are captured before the first await.
|
|
426
|
+
Copied source leaves are checked again immediately after authority returns and
|
|
427
|
+
before unlink is submitted. Supplying any of the three callbacks also
|
|
428
|
+
retains the original source-parent route and renews copied-directory ancestry
|
|
429
|
+
before cleanup. Substituted entries are preserved; pathname checks and unlink
|
|
430
|
+
remain a best-effort sequence, not atomic.
|
|
407
431
|
|
|
408
432
|
`onDestinationPublished` runs exactly once after a successful rename resolves,
|
|
409
433
|
before awaited post-rename directory checks or source cleanup. It receives a
|
package/docs/contributing.md
CHANGED
|
@@ -157,6 +157,10 @@ pnpm archive:producer-smoke ./consumer require
|
|
|
157
157
|
This uses a child bound to canonical cwd/device/inode running `/usr/bin/tar -czf - .`
|
|
158
158
|
with unchanged stdout, and npm tar, on synthetic Unicode/newline/long-name files,
|
|
159
159
|
then the installed package API for exact payload hashes and bounded reads.
|
|
160
|
+
The consumer must be separate from the source checkout; package resolution must
|
|
161
|
+
stay within its own `node_modules`, including pnpm's local `.pnpm` layout.
|
|
162
|
+
Workspace self-resolution, upward resolution, and external package links reject
|
|
163
|
+
before package imports or archive fixture creation.
|
|
160
164
|
It also rejects a valid PAX override attached to an invalid raw UTF-8 field.
|
|
161
165
|
The `require` command must resolve the freshly packed native binding; the
|
|
162
166
|
`off` command uses the installed WASM asset. No live user files are read.
|
package/docs/creation.md
CHANGED
|
@@ -42,12 +42,16 @@ POSIX creation requests `0700` for directories and `0600` for files by default;
|
|
|
42
42
|
the umask may restrict those permissions further. Existing directory privacy
|
|
43
43
|
checks never broaden permissions.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Private POSIX `Root.create()` and `createJson()` writes check the retained descriptor's actual owner and
|
|
46
46
|
permissions before writing payload bytes, after producer and authority callbacks,
|
|
47
|
-
and at publication
|
|
47
|
+
and at publication, including JavaScript fallback writes. Payload writes require
|
|
48
|
+
the temporary `0600` mode even when the requested final mode differs.
|
|
49
|
+
Private ownership, permissions and ACLs are checked before preparing that mode,
|
|
50
|
+
including after authority callbacks; valid restrictive initial modes remain supported.
|
|
51
|
+
A successful `chmod` is insufficient: filesystems that do not
|
|
48
52
|
enforce owner-only permissions reject before payload writes. The requested final
|
|
49
53
|
mode is verified too; a failure after publication preserves the completed file
|
|
50
|
-
and reports its published outcome.
|
|
54
|
+
and staged creation reports its published outcome.
|
|
51
55
|
|
|
52
56
|
On macOS (Darwin), private creation also requires an ACL-free result. The native
|
|
53
57
|
helper must provide `inspectDarwinAcl`; native `off`, a missing helper, or an
|
|
@@ -113,7 +117,7 @@ thenables reject before mutation. Parent and file identity checks are repeated
|
|
|
113
117
|
after the callback. Final permission checks, descriptor settlement and cleanup
|
|
114
118
|
retain the operation's cleanup ownership after publication.
|
|
115
119
|
|
|
116
|
-
Failure does not always mean the final path is absent.
|
|
120
|
+
Failure does not always mean the final path is absent. Staged private-file errors
|
|
117
121
|
after publication or ambiguous publication preserve the destination and carry
|
|
118
122
|
`details.publication.status` (`published` or `indeterminate`), the target path and
|
|
119
123
|
staging cleanup outcome. Cleanup and close failures retain the original error
|
package/docs/durability.md
CHANGED
|
@@ -89,6 +89,29 @@ or reconstructed numeric identity is accepted only when both components are
|
|
|
89
89
|
safe integers and, on Windows, nonzero. Rounded or unknown caller identities
|
|
90
90
|
fail with `path-mismatch` rather than authorizing a different directory.
|
|
91
91
|
|
|
92
|
+
Caller-supplied receipts may use `DirectoryReceipt<BigIntStats>` with the result
|
|
93
|
+
of `lstat(path, { bigint: true })`. `pinDirectory()`, `syncDirectory()`,
|
|
94
|
+
`syncDirectorySync()`, `publishFileExclusive()`'s `parentReceipt`, and
|
|
95
|
+
`stageFileInDirectory()` accept both numeric and bigint receipt inputs.
|
|
96
|
+
`DirectoryReceipt` without a type argument and all returned durability receipts
|
|
97
|
+
still expose numeric `Stats`, including working type predicates and Date
|
|
98
|
+
properties. Bigint metadata is projected from the supplied observation, retaining
|
|
99
|
+
fractional timestamps and the private exact device/inode identity.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { lstatSync, realpathSync, type BigIntStats } from "node:fs";
|
|
103
|
+
import { syncDirectorySync, type DirectoryReceipt } from "@openclaw/fs-safe/durability";
|
|
104
|
+
|
|
105
|
+
const directoryPath = "/srv/backups/sqlite";
|
|
106
|
+
const receipt: DirectoryReceipt<BigIntStats> = {
|
|
107
|
+
path: directoryPath,
|
|
108
|
+
realPath: realpathSync(directoryPath),
|
|
109
|
+
identity: lstatSync(directoryPath, { bigint: true }),
|
|
110
|
+
};
|
|
111
|
+
// Keep this receipt across the application's publication operation.
|
|
112
|
+
const outcome = syncDirectorySync(receipt);
|
|
113
|
+
```
|
|
114
|
+
|
|
92
115
|
Call `close()` in `finally`. Closing is idempotent; using a closed pin fails.
|
|
93
116
|
|
|
94
117
|
These checks intentionally reject a moved or replaced pathname. For one file's
|
|
@@ -276,6 +299,11 @@ pathname without reopening the file and repeat the symlink and file-type checks.
|
|
|
276
299
|
POSIX opens are nonblocking, so a raced FIFO or device is rejected after
|
|
277
300
|
descriptor inspection rather than waiting for a writer.
|
|
278
301
|
|
|
302
|
+
A pathname hash reports failure to close its owned descriptor after successful
|
|
303
|
+
hashing. If hashing, admission, or cancellation already failed, that original
|
|
304
|
+
failure remains primary even when close also fails. This also applies to
|
|
305
|
+
`sha256FileSync()`; borrowed handles and descriptors remain caller-owned.
|
|
306
|
+
|
|
279
307
|
When the optional binding is active, hashing runs as an async native task and
|
|
280
308
|
does not occupy the JavaScript event loop with digest updates. With native mode
|
|
281
309
|
`off`, or in `auto` when no binding loads, the fallback performs asynchronous
|
|
@@ -347,6 +375,13 @@ this receipt instead of inferring ownership from path existence. The original
|
|
|
347
375
|
failure remains available as `cause`. Failures before target creation retain
|
|
348
376
|
their existing error shape and do not claim a cleanup result.
|
|
349
377
|
|
|
378
|
+
Source and target identities are checked again after successful or unsupported
|
|
379
|
+
directory synchronization, while their descriptors remain owned. A late
|
|
380
|
+
verification failure retains its strategy's verification phase and is not a
|
|
381
|
+
directory-sync failure. Completed copied targets stay pinned during conditional
|
|
382
|
+
cleanup; substituted entries remain untouched. Returned numeric metadata comes
|
|
383
|
+
from the retained target descriptor and grants no continuing pathname authority.
|
|
384
|
+
|
|
350
385
|
### Directory-sync failure policy
|
|
351
386
|
|
|
352
387
|
`onSyncFailure` applies only after target creation and content/identity fencing
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Exact file comparison
|
|
2
|
+
|
|
3
|
+
`sameFileContentsSync()` compares the bytes of two already-open regular files,
|
|
4
|
+
starting at offset zero. It uses bounded buffers and completes positive short
|
|
5
|
+
reads independently on each descriptor. A `true` result requires matching bytes
|
|
6
|
+
and observed EOF on both inputs; a difference can return `false` immediately.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import { sameFileContentsSync } from "@openclaw/fs-safe/advanced";
|
|
11
|
+
|
|
12
|
+
const source = fs.openSync("/trusted/source.sqlite", "r");
|
|
13
|
+
try {
|
|
14
|
+
const copy = fs.openSync("/trusted/copy.sqlite", "r");
|
|
15
|
+
try {
|
|
16
|
+
console.log(sameFileContentsSync(source, copy, { maxBytes: 256 * 1024 * 1024 }));
|
|
17
|
+
} finally {
|
|
18
|
+
fs.closeSync(copy);
|
|
19
|
+
}
|
|
20
|
+
} finally {
|
|
21
|
+
fs.closeSync(source);
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Signature
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
type SameFileContentsOptions = { maxBytes?: number };
|
|
29
|
+
|
|
30
|
+
function sameFileContentsSync(
|
|
31
|
+
leftFd: number,
|
|
32
|
+
rightFd: number,
|
|
33
|
+
options?: SameFileContentsOptions,
|
|
34
|
+
): boolean;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Both descriptors must be open regular files. Nonregular inputs throw
|
|
38
|
+
`FsSafeError("not-file")`; underlying filesystem errors propagate unchanged.
|
|
39
|
+
Passing the same descriptor twice is allowed, but does not bypass validation,
|
|
40
|
+
the byte limit, or reads. File sizes are checked against the limit, but are not
|
|
41
|
+
used as proof that contents match or that EOF has been reached.
|
|
42
|
+
|
|
43
|
+
## Bounds and ownership
|
|
44
|
+
|
|
45
|
+
`maxBytes` is a limit for each file, not their combined size. It accepts a
|
|
46
|
+
non-negative safe integer or `Infinity`; omission imposes no caller-selected
|
|
47
|
+
limit. Invalid limits throw `RangeError` before filesystem work. Comparisons
|
|
48
|
+
cannot exceed `Number.MAX_SAFE_INTEGER` bytes because positions must remain
|
|
49
|
+
exactly representable.
|
|
50
|
+
|
|
51
|
+
A reported file size above the limit throws `FsSafeError("too-large")` before
|
|
52
|
+
reading. At the limit, the comparison reads at most one additional byte from
|
|
53
|
+
each input to prove EOF; any observed overflow throws the same error. A
|
|
54
|
+
matching prefix is never reported as complete equality. An early mismatch
|
|
55
|
+
does not scan the remaining bytes or promise to detect later errors or growth.
|
|
56
|
+
A zero-byte limit admits two empty files. Memory use is at most two 1 MiB
|
|
57
|
+
payload buffers, regardless of file size.
|
|
58
|
+
|
|
59
|
+
The operation neither changes the descriptors' current offsets nor closes
|
|
60
|
+
them, including on failure. It performs no writes, hashing, pathname lookup,
|
|
61
|
+
or identity comparison. Callers retain path admission, hardlink policy,
|
|
62
|
+
descriptor lifetime, and any before/after mutation-fingerprint checks. A
|
|
63
|
+
comparison is not a snapshot of concurrently modified files; applications
|
|
64
|
+
requiring stable contents must retain their existing coordination and checks.
|
|
65
|
+
|
|
66
|
+
Use [`readFileWindowFullySync()`](positional-read.md) for a selected byte
|
|
67
|
+
window and [bounded descriptor reads](advanced.md#files-and-identity) when the
|
|
68
|
+
caller needs the file contents in memory.
|
package/docs/json.md
CHANGED
|
@@ -146,10 +146,11 @@ where lower latency matters more than crash-durability.
|
|
|
146
146
|
|
|
147
147
|
Synchronous variant. It pretty-prints with two spaces, appends a newline,
|
|
148
148
|
creates parents at `0o700`, writes at `0o600`, and synchronizes the parent
|
|
149
|
-
directory best-effort.
|
|
150
|
-
through rename and
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
directory best-effort. A retained staging descriptor carries the exact bigint
|
|
150
|
+
identity through rename and file-mode tightening. Publication and cleanup never
|
|
151
|
+
adopt a substituted temporary file: a changed identity, type or link count rejects
|
|
152
|
+
the write and leaves the replacement untouched. A swap detected after publication
|
|
153
|
+
also rejects without deleting or changing the replacement. It has no options bag. On `EPERM`/`EEXIST`, its legacy
|
|
153
154
|
compatibility path removes the existing destination and retries the staged-file
|
|
154
155
|
rename, so that fallback is temporarily non-atomic while retaining the staged
|
|
155
156
|
file's `0600` mode; use the async `writeJson()`/`replaceFileAtomic()` surfaces
|
package/docs/local-roots.md
CHANGED
|
@@ -83,6 +83,8 @@ An existing non-directory component cannot be traversed further, including by
|
|
|
83
83
|
The asynchronous helper opens the candidate through the matched [`Root`](root.md),
|
|
84
84
|
so no-follow, identity, hardlink, device-path, and byte-limit checks happen at
|
|
85
85
|
the read itself.
|
|
86
|
+
Link policies are captured once before root initialization and reused for every
|
|
87
|
+
candidate, so replacing options while a read is pending cannot weaken admission.
|
|
86
88
|
|
|
87
89
|
```ts
|
|
88
90
|
type ReadLocalFileFromRootsOptions = LocalRootsInputOptions & {
|
|
@@ -44,9 +44,11 @@ new placeholder, and before publishing to an existing symlink-selected destinati
|
|
|
44
44
|
They verify alias binding, destination preservation, observed placeholder/stage
|
|
45
45
|
states, and cleanup before fixture teardown. The default native-off and explicit
|
|
46
46
|
`verify-content-with-lock` native-require configurations both select the existing
|
|
47
|
-
Windows JS buffer writer.
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
Windows JS buffer writer. In native-require mode, the compatibility route loads
|
|
48
|
+
the addon to publish its retained sidecar lock through `Root.create`; the payload
|
|
49
|
+
writer remains JS. The receipt does not mislabel this as native payload
|
|
50
|
+
publication or evidence that content-verification fallback or lock contention
|
|
51
|
+
was exercised.
|
|
50
52
|
|
|
51
53
|
## Bounds and interpretation
|
|
52
54
|
|
package/docs/native.md
CHANGED
|
@@ -76,14 +76,14 @@ whether a path, archive entry, mode, owner, or cleanup policy is acceptable.
|
|
|
76
76
|
or add-on CRT descriptor namespace.
|
|
77
77
|
|
|
78
78
|
The internal macOS `inspectDarwinAcl(fd)` capability reports `absent`, `empty`,
|
|
79
|
-
or `present` for the opened object's extended ACL. It synchronously
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
or `present` for the opened object's extended ACL. It synchronously borrows the
|
|
80
|
+
caller's descriptor, preserving its file position and POSIX record locks, and
|
|
81
|
+
never reopens a pathname. Keep the descriptor open until inspection returns.
|
|
82
|
+
Darwin's `acl_get_entry` returns
|
|
82
83
|
zero for an entry; end-of-list is accepted only for the first entry of a valid,
|
|
83
84
|
privately owned empty ACL. Unsupported, malformed, and failed inspection is not
|
|
84
|
-
reported as absence. These facts do not classify individual ACE permissions
|
|
85
|
-
prove volume ownership enforcement
|
|
86
|
-
and secure readers outside the clone path.
|
|
85
|
+
reported as absence. These facts do not classify individual ACE permissions or
|
|
86
|
+
prove volume ownership enforcement; each caller applies its own security policy.
|
|
87
87
|
|
|
88
88
|
## Archives
|
|
89
89
|
|
|
@@ -267,8 +267,9 @@ infer native loading from timing.
|
|
|
267
267
|
## Loader security
|
|
268
268
|
|
|
269
269
|
Importing fs-safe never executes a child process. Linux libc selection uses
|
|
270
|
-
the Node process report,
|
|
271
|
-
|
|
270
|
+
the Node process report, then the ELF `PT_INTERP` field of `process.execPath`,
|
|
271
|
+
then conventional musl library filenames. An installed compatibility loader
|
|
272
|
+
does not override the running executable's interpreter. If all probes are inconclusive, the
|
|
272
273
|
loader conservatively attempts the glibc package and lets normal module loading
|
|
273
274
|
fail into `auto` fallback. The loader requires only the package selected from
|
|
274
275
|
the detected target; it never probes unrelated packages, downloads code, or
|
package/docs/path.md
CHANGED
|
@@ -44,7 +44,7 @@ opened or mutated.
|
|
|
44
44
|
|
|
45
45
|
### `isPathInsideWithRealpath(rootDir, target, opts?)`
|
|
46
46
|
|
|
47
|
-
Synchronous.
|
|
47
|
+
Synchronous. First requires lexical containment with `isPathInside`, then resolves both inputs through `realpath` and checks containment again. A lexically outside path is rejected even if its resolved target is inside the root.
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
50
|
isPathInsideWithRealpath("/srv/uploads", "/srv/symlink-to-elsewhere"); // false
|
|
@@ -121,7 +121,7 @@ The check is intentionally not a normal consumer policy knob. Safe read APIs rej
|
|
|
121
121
|
|
|
122
122
|
### `isNotFoundPathError(err)`
|
|
123
123
|
|
|
124
|
-
`true` if the error
|
|
124
|
+
`true` if the error has code `ENOENT` (file or directory missing) or `ENOTDIR` (a path component is not a directory).
|
|
125
125
|
|
|
126
126
|
```ts
|
|
127
127
|
try {
|
|
@@ -207,8 +207,8 @@ import {
|
|
|
207
207
|
} from "@openclaw/fs-safe/advanced";
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
- `assertNoPathAliasEscape({
|
|
211
|
-
- `assertNoHardlinkedFinalPath({ filePath })` — async.
|
|
210
|
+
- `assertNoPathAliasEscape({ absolutePath, rootPath, boundaryLabel, policy? })` — async. Applies root path resolution and final hardlink checks. `policy` defaults to `PATH_ALIAS_POLICIES.strict`; `PATH_ALIAS_POLICIES.unlinkTarget` permits final symlink and hardlink aliases for unlink operations.
|
|
211
|
+
- `assertNoHardlinkedFinalPath({ filePath, root, boundaryLabel, allowFinalHardlinkForUnlink? })` — async. Rejects a regular file with `nlink > 1`; missing paths and nonregular resolved entries are ignored. Setting `allowFinalHardlinkForUnlink: true` skips this check for unlink operations.
|
|
212
212
|
|
|
213
213
|
Use these when writing a custom helper that wants the same guards `root()` uses but with different surrounding logic.
|
|
214
214
|
|
package/docs/public-api.md
CHANGED
|
@@ -20,6 +20,9 @@ The root subpath also exports `openLocalFileSafely`, `readLocalFileSafely`, and
|
|
|
20
20
|
`resolveOpenedFileRealPathForHandle` for trusted absolute-file composition.
|
|
21
21
|
They do not create a root boundary around arbitrary caller input; prefer
|
|
22
22
|
`root()` for untrusted paths.
|
|
23
|
+
The handle resolver verifies exact descriptor and pathname identities, with one
|
|
24
|
+
bounded retry for unknown Windows observations. It borrows the handle without
|
|
25
|
+
reading, reopening, closing it, or changing its cursor.
|
|
23
26
|
|
|
24
27
|
The error helpers are `categorizeFsSafeError` and `FsSafeErrorDetails`. The
|
|
25
28
|
deprecated native-configuration bridge retains the `FsSafePythonConfig` type.
|
|
@@ -135,6 +138,8 @@ The durability surface also exports the synchronous strict
|
|
|
135
138
|
`Sha256FileSyncInput`, `Sha256FileOptions`, and `Sha256FileResult`.
|
|
136
139
|
`sha256FileSync()` provides synchronous pathname or borrowed-descriptor hashing
|
|
137
140
|
with the same byte-budget and digest-result contracts as `sha256File()`.
|
|
141
|
+
`DirectoryReceipt<T>` accepts `Stats` or `BigIntStats` input metadata; its default
|
|
142
|
+
type argument and returned durability receipts remain numeric `Stats`.
|
|
138
143
|
|
|
139
144
|
## Archives
|
|
140
145
|
|
package/docs/quickstart.md
CHANGED
|
@@ -60,7 +60,7 @@ await fs.move("notes/today.txt", "notes/archive/today.txt", { overwrite: true })
|
|
|
60
60
|
await fs.remove("notes/archive/today.txt");
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
`move()` defaults to no clobber. That mode requires the native helper so a concurrent target cannot be replaced between an absence check and the rename; without it, the call fails with `helper-unavailable`. Pass `{ overwrite: true }` when replacing the target is intentional. `remove()`
|
|
63
|
+
`move()` defaults to no clobber. That mode requires the native helper so a concurrent target cannot be replaced between an absence check and the rename; without it, the call fails with `helper-unavailable`. Pass `{ overwrite: true }` when replacing the target is intentional. `remove()` removes files and empty directories by default. To remove a non-empty directory, pass `{ recursive: true }`; use `maxEntries`, `maxDepth`, and `signal` to bound the work. See [`root()`](root.md) for removal ordering, limits, and partial-removal semantics.
|
|
64
64
|
|
|
65
65
|
## 5. Inspect
|
|
66
66
|
|
package/docs/reading.md
CHANGED
|
@@ -28,7 +28,7 @@ Regardless of shape, every read goes through the same boundary checks:
|
|
|
28
28
|
4. Reject `..` traversal and absolute spellings when they resolve outside the root. In-root absolute spellings remain accepted; `readAbsolute` makes that intent explicit.
|
|
29
29
|
5. Open with `O_NOFOLLOW` where available. Any remaining symlink in the path triggers `symlink` unless the call's `symlinks` policy is `follow-within-root`.
|
|
30
30
|
6. Compare the pre-open bigint path identity with the open fd, then perform one best-effort final admission: check the captured root identity, compare the policy-aware pathname with the fd, freshly canonicalize and re-admit that target inside the captured root, compare its exact bigint identity without following a final symlink with the fd, and check the root again. Both final pathname observations run even when their spellings match. An observed swap triggers `path-mismatch` or `outside-workspace`; a missing final path triggers `not-found`.
|
|
31
|
-
7. If `hardlinks: "reject"`, refuse files with `nlink > 1` (`hardlink`).
|
|
31
|
+
7. If `hardlinks: "reject"`, refuse files with `nlink > 1` (`hardlink`), including links introduced before either fresh final pathname observation. Root-file helpers apply the same final check when `rejectHardlinks` is enabled; directory admission is unaffected.
|
|
32
32
|
8. If `maxBytes` is set, refuse reads larger than the cap (`too-large`).
|
|
33
33
|
|
|
34
34
|
The final fence closes a rejected descriptor before any Root read consumes bytes or
|
|
@@ -100,7 +100,7 @@ type RootReadOptions = {
|
|
|
100
100
|
hardlinks?: "reject" | "allow"; // override defaults.hardlinks
|
|
101
101
|
maxBytes?: number; // refuse reads larger than this many bytes
|
|
102
102
|
nonBlockingRead?: boolean; // compatibility hint; safe opens are already nonblocking where supported
|
|
103
|
-
symlinks?: "reject" | "follow-within-root"; // override defaults.symlinks
|
|
103
|
+
symlinks?: "reject" | "follow-within-root" | "follow-parents-within-root"; // override defaults.symlinks
|
|
104
104
|
};
|
|
105
105
|
```
|
|
106
106
|
|
package/docs/regular-file.md
CHANGED
|
@@ -110,6 +110,9 @@ descriptor, and current pathname identities remain exact bigints through the
|
|
|
110
110
|
append boundary; rounded-equal replacements and persistent unknown Windows
|
|
111
111
|
identities reject before chmod or writing bytes. With
|
|
112
112
|
`rejectSymlinkParents: true`, it also rejects symlinked ancestor directories.
|
|
113
|
+
Supported option values are captured once before filesystem work, so replacing
|
|
114
|
+
the content, encoding, mode or cap cannot change an in-flight append. Byte-array
|
|
115
|
+
contents remain caller-owned; leave them unchanged until the append completes.
|
|
113
116
|
|
|
114
117
|
On POSIX, `O_NONBLOCK` prevents a no-reader FIFO substituted before open from
|
|
115
118
|
stalling admission. A confirmed non-regular target is refused before chmod or
|