@openclaw/fs-safe 0.8.4 → 0.8.6

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 CHANGED
@@ -1,23 +1,64 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.8.6 - 2026-09-07
4
+
5
+ **Highlights:** Clearer guidance for atomic writes on Windows exFAT, with a repeatable filesystem compatibility probe.
6
+
7
+ - Document the existing opt-in locked rename policy for Windows exFAT/FAT32 and add a built-package probe for identity drift, substitution handling, and temporary-prefix isolation; require a trusted, quiescent probe parent and verify its cleanup identity. Thanks @dongsheng123132.
8
+ - Refresh the native zstd decoder to 0.14.0 and compatible Rust and JavaScript development dependencies, and update the pnpm setup action to 6.1.0.
9
+
10
+ ## 0.8.5 - 2026-09-07
11
+
12
+ ### Highlights
13
+
14
+ - **Strict moves stay strict:** a move started with hardlink rejection keeps that policy throughout asynchronous preparation and copying, even when the caller reuses or changes its options object.
15
+
16
+ ### Safe moves
17
+
18
+ - Preserve the admitted `sourceHardlinks` policy through staged-copy fallback. A late hardlink is still rejected before destination publication, with the source and its alias preserved, instead of being accepted after an in-flight options change.
19
+ - Document when move policy is captured and how it relates to the existing per-mutation authority checks and destination-publication receipts.
4
20
 
5
21
  ## 0.8.4 - 2026-09-07
6
22
 
23
+ ### Highlights
24
+
25
+ - **Faster reads and writes:** metadata checks avoid unnecessary event-loop round-trips while data I/O stays asynchronous; reconstructible data can explicitly opt out of fsync with `durable: false`.
26
+ - **Inspect TARs without extracting:** the new bounded archive inspection API uses the same native/WASM admission and canonical planner as extraction.
27
+
28
+ ### Filesystem performance and durability
29
+
30
+ - Run metadata checks inside async operations synchronously (microseconds each), cutting event-loop round-trips per read/write while data I/O remains asynchronous and native canonical path spelling is preserved, including Windows short paths.
31
+ - Add `durable` to Root write/create/writeJson/createJson/append options and Root defaults; `durable: false` skips file and parent fsync for reconstructible data (default unchanged: durable).
32
+
33
+ ### Archive inspection and compatibility
34
+
7
35
  - Add bounded `inspectTarArchive` with complete native/WASM admission and the same canonical extraction planner, preserving effective member identities without materializing an output tree.
8
36
  - Preserve inherited and getter-backed extraction options on the WASM TAR path, matching native and ZIP handling for destinations, filters, stripping, and modes.
37
+
38
+ ### Validation
39
+
9
40
  - Apply the existing filesystem-test worker cap locally as well as in CI, and drain archive publication fixtures before resetting hooks or cleaning restricted directories after timeouts.
10
- - Add `durable` to Root write/create/writeJson/createJson/append options and Root defaults; `durable: false` skips file and parent fsync for reconstructible data (default unchanged: durable).
11
- - Run metadata checks inside async operations synchronously (microseconds each), cutting event-loop round-trips per read/write while data I/O remains asynchronous and native canonical path spelling is preserved, including Windows short paths.
12
41
 
13
42
  ## 0.8.3 - 2026-09-06
14
43
 
44
+ ### Highlights
45
+
46
+ - **Safer cancellable moves:** callers can guard every source removal and retain an exact destination-publication receipt for recovery after later failures, including Windows and cross-device fallbacks.
47
+ - **Less filesystem overhead:** reads and writes make fewer calls while preserving their existing boundary and identity checks.
48
+
49
+ ### Move authority and recovery
50
+
15
51
  - Add optional synchronous move authority checks before renames and each copied-source removal, plus an exact bigint destination-publication receipt for caller-owned recovery after later failure; retain cross-device and Windows `EPERM` copy fallbacks and the existing `Promise<void>` contract.
16
52
 
17
- - Simplify internals without changing public behavior: remove unused string/home helpers, `resolveUserPath`, `createBoundedReadStream`, `sidecarLockPayloadIsStale`, and `tarManifestEntryCost`; share filesystem utilities and merge private modules.
53
+ ### Filesystem performance and durability
54
+
18
55
  - Reduce filesystem calls per read and write while retaining boundary, hardlink, hook, retry, and post-mutation identity checks.
19
56
  - Skip native publication's extra mode-only file fsync for modes that retain owner read/write when staging permissions have not widened; after a crash, a file may retain staged `0o600` instead of the wider requested mode, while restrictive modes retain the extra fsync.
57
+
58
+ ### Tooling and maintenance
59
+
20
60
  - Add 1 MiB read/write and existing-mode inheritance benchmarks, with native-mode metadata and per-case iteration counts.
61
+ - Simplify internals without changing public behavior: remove unused string/home helpers, `resolveUserPath`, `createBoundedReadStream`, `sidecarLockPayloadIsStale`, and `tarManifestEntryCost`; share filesystem utilities and merge private modules.
21
62
 
22
63
  ## 0.8.2 - 2026-09-05
23
64
 
Binary file
package/dist/move-path.js CHANGED
@@ -289,7 +289,7 @@ export async function movePathWithCopyFallback(options) {
289
289
  const unregisterStaged = registerTempPathForExit(staged, { recursive: true });
290
290
  try {
291
291
  const manifest = await copyEntryWithManifest(sourcePath, staged, {
292
- sourceHardlinks: options.sourceHardlinks ?? "allow",
292
+ sourceHardlinks: rejectHardlinks ? "reject" : "allow",
293
293
  ...(rejectHardlinks ? { budget: { discovered: 1 } } : {}),
294
294
  }, sourceIdentity);
295
295
  const cleanupState = createCleanupCopiedEntryState(sourcePath, manifest);
package/docs/atomic.md CHANGED
@@ -75,12 +75,37 @@ If `beforeRename` throws, the rename is skipped and the owned temp file is remov
75
75
 
76
76
  Identity checks and pathname rename/unlink remain separate syscalls, not atomic conditional mutations. Use an approved writable parent plus cooperative locking or OS isolation when arbitrary concurrent namespace mutation is in scope.
77
77
 
78
- ### FUSE mounts and unstable rename identity
78
+ ### FUSE, Windows exFAT/FAT32, and unstable rename identity
79
79
 
80
80
  Strict source-to-destination identity is the default. Some FUSE mounts assign a different inode to the destination during rename even without concurrency. Set `renameIdentity: "verify-content-with-lock"` to accept that boundary only when the re-opened no-follow destination has the exact requested SHA-256 content under an exclusive hashed sidecar lock in the destination parent. The newly accepted descriptor and identity remain pinned through parent sync and final verification. The synchronous helper provides the same policy with the synchronous lock implementation.
81
81
 
82
82
  This is the same explicit weaker contract available on `Root` writes: cooperating writers are serialized, stale locks fail closed, and mismatched content is rejected after publication without rollback. A same-authority actor that ignores the advisory lock can still substitute another file with identical bytes, so do not use this compatibility policy in directories writable by untrusted same-UID processes.
83
83
 
84
+ Windows exFAT/FAT32 volumes can also change file identity during rename, depending
85
+ on the source and destination names. The destination may already contain the
86
+ requested bytes when strict verification reports `path-mismatch`. A short
87
+ temporary name alone does not guarantee stable identity for a longer destination.
88
+ For an application-controlled directory on such a volume, callers can explicitly
89
+ select `renameIdentity: "verify-content-with-lock"` on `replaceFileAtomic` or
90
+ `replaceFileAtomicSync`. Keep strict mode for directories that require the
91
+ stronger identity contract; do not automatically retry every `path-mismatch`
92
+ with the weaker policy. Staging names remain random, including custom prefixes.
93
+
94
+ To verify the built package against an actual volume, run
95
+ `node scripts/atomic-rename-compat-proof.mjs EXISTING_PARENT` after `pnpm build`.
96
+ Use a trusted parent directory that no other process can rename or modify during
97
+ the entire run, including cleanup; do not point the probe at a shared writable
98
+ volume root. Cleanup checks the created directory's identity before recursive
99
+ removal, but the check and removal are not atomic. The probe does not test safety
100
+ against hostile concurrent namespace mutation.
101
+ The probe creates and cleans up its own child directory and emits JSON without
102
+ local paths. It compares default, strict, and locked policies in both async and
103
+ sync calls, checks real rename identities and file contents, injects different-
104
+ and identical-content substitutions, checks lock cleanup, and exercises custom
105
+ prefix isolation and validation. Callback staging remains strict and can still
106
+ report identity drift; the probe records that result separately. Filesystem type
107
+ must be recorded independently; the probe does not infer it from a drive letter.
108
+
84
109
  ### `EPERM` and copy fallback
85
110
 
86
111
  On systems where `rename` fails with `EPERM`/`EEXIST`, pass
@@ -200,6 +225,8 @@ await movePathWithCopyFallback({
200
225
  ```
201
226
 
202
227
  Use it when source and destination might live on different filesystems (containers, tmpfs, separate volumes).
228
+ The hardlink policy is captured when the move starts. Changing or reusing the
229
+ options object later does not change the policy of an in-flight move.
203
230
  `sourceHardlinks: "reject"` performs a recursive preflight capped at 50,000
204
231
  entries before any mutation. Because link count and rename cannot be one atomic
205
232
  portable operation, this mode always commits a fresh inode/tree through the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.8.4",
3
+ "version": "0.8.6",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -156,13 +156,13 @@
156
156
  "archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
157
157
  },
158
158
  "optionalDependencies": {
159
- "@openclaw/fs-safe-darwin-arm64": "0.8.4",
160
- "@openclaw/fs-safe-darwin-x64": "0.8.4",
161
- "@openclaw/fs-safe-linux-arm64-gnu": "0.8.4",
162
- "@openclaw/fs-safe-linux-arm64-musl": "0.8.4",
163
- "@openclaw/fs-safe-linux-x64-gnu": "0.8.4",
164
- "@openclaw/fs-safe-linux-x64-musl": "0.8.4",
165
- "@openclaw/fs-safe-win32-x64-msvc": "0.8.4",
159
+ "@openclaw/fs-safe-darwin-arm64": "0.8.6",
160
+ "@openclaw/fs-safe-darwin-x64": "0.8.6",
161
+ "@openclaw/fs-safe-linux-arm64-gnu": "0.8.6",
162
+ "@openclaw/fs-safe-linux-arm64-musl": "0.8.6",
163
+ "@openclaw/fs-safe-linux-x64-gnu": "0.8.6",
164
+ "@openclaw/fs-safe-linux-x64-musl": "0.8.6",
165
+ "@openclaw/fs-safe-win32-x64-msvc": "0.8.6",
166
166
  "jszip": "^3.10.1"
167
167
  },
168
168
  "devDependencies": {