@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.
Files changed (180) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +8 -1
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +2 -8
  5. package/dist/advanced.d.ts +2 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +2 -0
  8. package/dist/archive-deadline.d.ts.map +1 -1
  9. package/dist/archive-deadline.js +22 -23
  10. package/dist/archive-merge.d.ts +1 -0
  11. package/dist/archive-merge.d.ts.map +1 -1
  12. package/dist/archive-merge.js +4 -4
  13. package/dist/archive-native.d.ts +1 -0
  14. package/dist/archive-native.d.ts.map +1 -1
  15. package/dist/archive-native.js +1 -0
  16. package/dist/archive-options.d.ts +2 -0
  17. package/dist/archive-options.d.ts.map +1 -1
  18. package/dist/archive-parser.wasm +0 -0
  19. package/dist/archive-zip-count.d.ts.map +1 -1
  20. package/dist/archive-zip-count.js +21 -1
  21. package/dist/archive-zip-directory.d.ts.map +1 -1
  22. package/dist/archive-zip-directory.js +23 -1
  23. package/dist/archive-zip-loader.d.ts +2 -0
  24. package/dist/archive-zip-loader.d.ts.map +1 -1
  25. package/dist/archive-zip-loader.js +7 -0
  26. package/dist/archive-zip-names.d.ts.map +1 -1
  27. package/dist/archive-zip-names.js +7 -2
  28. package/dist/archive.d.ts.map +1 -1
  29. package/dist/archive.js +9 -3
  30. package/dist/byte-view.d.ts +3 -0
  31. package/dist/byte-view.d.ts.map +1 -0
  32. package/dist/byte-view.js +13 -0
  33. package/dist/creation-darwin.d.ts +0 -1
  34. package/dist/creation-darwin.d.ts.map +1 -1
  35. package/dist/creation-darwin.js +0 -9
  36. package/dist/directory-durability.d.ts +6 -6
  37. package/dist/directory-durability.d.ts.map +1 -1
  38. package/dist/directory-receipt.d.ts +2 -2
  39. package/dist/directory-receipt.d.ts.map +1 -1
  40. package/dist/directory-receipt.js +15 -19
  41. package/dist/file-cleanup.d.ts +1 -0
  42. package/dist/file-cleanup.d.ts.map +1 -1
  43. package/dist/file-cleanup.js +7 -4
  44. package/dist/file-contents.d.ts +6 -0
  45. package/dist/file-contents.d.ts.map +1 -0
  46. package/dist/file-contents.js +40 -0
  47. package/dist/file-hash.d.ts.map +1 -1
  48. package/dist/file-hash.js +16 -4
  49. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -1
  50. package/dist/file-lock-sync-root-acquire.js +3 -0
  51. package/dist/file-lock-sync-root-held.d.ts +1 -2
  52. package/dist/file-lock-sync-root-held.d.ts.map +1 -1
  53. package/dist/file-lock-sync-root-held.js +7 -5
  54. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -1
  55. package/dist/file-lock-sync-stale-admission.js +3 -0
  56. package/dist/file-lock-sync.d.ts.map +1 -1
  57. package/dist/file-lock-sync.js +8 -11
  58. package/dist/file-store.js +3 -3
  59. package/dist/guarded-mkdir.d.ts.map +1 -1
  60. package/dist/guarded-mkdir.js +6 -27
  61. package/dist/install-path.d.ts.map +1 -1
  62. package/dist/install-path.js +2 -5
  63. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  64. package/dist/json-durable-queue-ownership.js +2 -6
  65. package/dist/json-durable-queue-paths.d.ts.map +1 -1
  66. package/dist/json-durable-queue-paths.js +2 -24
  67. package/dist/json-durable-queue.d.ts.map +1 -1
  68. package/dist/json-durable-queue.js +10 -9
  69. package/dist/json.d.ts.map +1 -1
  70. package/dist/json.js +32 -75
  71. package/dist/local-roots.d.ts.map +1 -1
  72. package/dist/local-roots.js +19 -21
  73. package/dist/move-path-cleanup.d.ts +5 -19
  74. package/dist/move-path-cleanup.d.ts.map +1 -1
  75. package/dist/move-path-cleanup.js +57 -21
  76. package/dist/move-path.d.ts.map +1 -1
  77. package/dist/move-path.js +62 -39
  78. package/dist/native-staged-file.d.ts +2 -1
  79. package/dist/native-staged-file.d.ts.map +1 -1
  80. package/dist/native-staged-file.js +8 -5
  81. package/dist/native.js +2 -2
  82. package/dist/opened-realpath.d.ts.map +1 -1
  83. package/dist/opened-realpath.js +11 -2
  84. package/dist/path.d.ts.map +1 -1
  85. package/dist/path.js +2 -1
  86. package/dist/permissions.d.ts.map +1 -1
  87. package/dist/permissions.js +3 -17
  88. package/dist/pinned-write-input.d.ts.map +1 -1
  89. package/dist/pinned-write-input.js +11 -1
  90. package/dist/pinned-write-mode.d.ts +3 -3
  91. package/dist/pinned-write-mode.d.ts.map +1 -1
  92. package/dist/pinned-write-mode.js +15 -8
  93. package/dist/pinned-write-staged.d.ts.map +1 -1
  94. package/dist/pinned-write-staged.js +10 -11
  95. package/dist/pinned-write.d.ts.map +1 -1
  96. package/dist/pinned-write.js +17 -13
  97. package/dist/publish-file.d.ts +2 -2
  98. package/dist/publish-file.d.ts.map +1 -1
  99. package/dist/publish-file.js +56 -96
  100. package/dist/regular-file.d.ts.map +1 -1
  101. package/dist/regular-file.js +35 -44
  102. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  103. package/dist/replace-file-copy-fallback.js +28 -26
  104. package/dist/replace-file-copy-source.d.ts.map +1 -1
  105. package/dist/replace-file-copy-source.js +13 -22
  106. package/dist/replace-file-descriptor.d.ts.map +1 -1
  107. package/dist/replace-file-descriptor.js +10 -16
  108. package/dist/replace-file-temp-owner.js +6 -6
  109. package/dist/replace-file.d.ts.map +1 -1
  110. package/dist/replace-file.js +9 -13
  111. package/dist/root-directory-list.d.ts.map +1 -1
  112. package/dist/root-directory-list.js +20 -3
  113. package/dist/root-file-final-admission.d.ts +1 -1
  114. package/dist/root-file-final-admission.d.ts.map +1 -1
  115. package/dist/root-file-final-admission.js +5 -2
  116. package/dist/root-file.d.ts.map +1 -1
  117. package/dist/root-file.js +3 -2
  118. package/dist/root-impl.d.ts.map +1 -1
  119. package/dist/root-impl.js +50 -20
  120. package/dist/root-move-noreplace.d.ts +2 -0
  121. package/dist/root-move-noreplace.d.ts.map +1 -1
  122. package/dist/root-move-noreplace.js +2 -2
  123. package/dist/root-read-admission.d.ts.map +1 -1
  124. package/dist/root-read-admission.js +7 -2
  125. package/dist/root-remove.d.ts.map +1 -1
  126. package/dist/root-remove.js +15 -1
  127. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  128. package/dist/sidecar-lock-acquire.js +4 -6
  129. package/dist/sidecar-lock-handle.d.ts +3 -0
  130. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  131. package/dist/sidecar-lock-handle.js +6 -0
  132. package/dist/sidecar-lock-reclaim.d.ts +1 -1
  133. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  134. package/dist/sidecar-lock-reclaim.js +11 -8
  135. package/dist/sidecar-lock.d.ts.map +1 -1
  136. package/dist/sidecar-lock.js +3 -5
  137. package/dist/staged-directory.d.ts +2 -2
  138. package/dist/staged-directory.d.ts.map +1 -1
  139. package/dist/strict-file-identity.d.ts +1 -1
  140. package/dist/strict-file-identity.d.ts.map +1 -1
  141. package/dist/strict-file-identity.js +9 -9
  142. package/dist/symlink-parents.d.ts.map +1 -1
  143. package/dist/symlink-parents.js +2 -27
  144. package/dist/temp-workspace-owner.js +4 -4
  145. package/dist/unicode-path.d.ts.map +1 -1
  146. package/dist/unicode-path.js +3 -0
  147. package/dist/walk.d.ts.map +1 -1
  148. package/dist/walk.js +4 -2
  149. package/dist/write-file-handle.d.ts +7 -0
  150. package/dist/write-file-handle.d.ts.map +1 -1
  151. package/dist/write-file-handle.js +23 -0
  152. package/dist/write-open-flags.d.ts.map +1 -1
  153. package/dist/write-open-flags.js +1 -8
  154. package/dist/write-queue.d.ts.map +1 -1
  155. package/dist/write-queue.js +1 -4
  156. package/docs/advanced.md +68 -1
  157. package/docs/archive.md +41 -2
  158. package/docs/atomic.md +29 -5
  159. package/docs/contributing.md +4 -0
  160. package/docs/creation.md +8 -4
  161. package/docs/durability.md +35 -0
  162. package/docs/file-contents.md +68 -0
  163. package/docs/json.md +5 -4
  164. package/docs/local-roots.md +2 -0
  165. package/docs/mutation-policy-proof.md +5 -3
  166. package/docs/native.md +9 -8
  167. package/docs/path.md +4 -4
  168. package/docs/public-api.md +5 -0
  169. package/docs/quickstart.md +1 -1
  170. package/docs/reading.md +2 -2
  171. package/docs/regular-file.md +3 -0
  172. package/docs/root.md +9 -0
  173. package/docs/sidecar-lock.md +9 -1
  174. package/docs/staged-file.md +4 -3
  175. package/docs/store.md +3 -1
  176. package/docs/temp.md +4 -1
  177. package/docs/types.md +18 -2
  178. package/docs/walk.md +7 -0
  179. package/docs/writing.md +9 -2
  180. package/package.json +8 -8
package/docs/root.md CHANGED
@@ -167,6 +167,8 @@ cleanup, and filesystem requirements.
167
167
  `append` accepts `prependNewlineIfNeeded: true` to separate text from existing
168
168
  content when neither side supplies a newline. String data uses its `encoding`
169
169
  for the newline check, including UTF-16LE; Buffer data uses a single LF byte.
170
+ Empty strings and Buffers add no separator; an empty append still creates a
171
+ missing file.
170
172
 
171
173
  These five methods and `copyIn` also accept `durable?: boolean`: the per-call value overrides
172
174
  `Root.defaults.durable`, which defaults to `true` when omitted. An explicitly
@@ -308,6 +310,13 @@ from that dispatch. A thrown value rejects the operation unchanged; an async
308
310
  or thenable-returning callback rejects with `TypeError` before that mutation.
309
311
  Synchronous return values are ignored. Callbacks can run multiple times and
310
312
  must inspect current authority each time.
313
+ Directory creation rechecks the retained parent after the callback and before
314
+ submitting mkdir, so a replacement is rejected before creating that component.
315
+ Overwrite moves recheck the retained root, parents, source identity and both
316
+ routes after the callback, including destination parents that were missing
317
+ during preparation. Removals recheck cancellation, retained ancestry and exact
318
+ leaf identity before dispatch; `force` tolerates a missing leaf, not replaced
319
+ ancestry. Removing an admitted hardlink still leaves its other names intact.
311
320
 
312
321
  Already dispatched I/O cannot be revoked. Identity-checked cleanup, final
313
322
  permissions, and durability finish under the existing operation owner even
@@ -55,6 +55,7 @@ invocation is not mutation authority. A failing final parser keeps its error
55
55
  even if guard ownership has also changed.
56
56
  `manager.reset()` invalidates admission bookkeeping but preserves a pending
57
57
  Root guard; let its original attempt settle before retrying that guarded path.
58
+ It stops compromise monitoring for forgotten holders, including callbacks from checks already in flight.
58
59
 
59
60
  Always release locks in a `finally` block. Application-managed graceful shutdown can await `release()` or `manager.drain()` before terminating. Explicit `process.exit()`, uncaught failures, crashes, default signal handling, and fatal termination (including `SIGKILL`) do not reliably run asynchronous Root cleanup and may leave sidecars. Recover only after an application-owned liveness policy proves the holder cannot still be writing.
60
61
 
@@ -64,7 +65,7 @@ Each new sidecar also carries an internal random ownership token encoded as JSON
64
65
 
65
66
  The raw sidecar bytes are not a canonical JSON representation: tools that trim or rewrite the trailing whitespace invalidate the ownership token, so release leaves the changed sidecar in place and fails closed. The token distinguishes cooperating acquisitions; it is not a secret and does not make pathname compare-and-remove atomic against a hostile process that can replace files outside the lock protocol.
66
67
 
67
- `release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. The handle and manager retain the exact cleanup receipt after a failure, so the same handle can retry `release()` and `manager.drain()` can retry retained cleanup. A changed sidecar remains an ownership mismatch rather than a deletion failure and is left untouched. If both a `withFileLock()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed asynchronous acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
68
+ `release()` propagates an I/O failure that prevents deletion of an unchanged, owned sidecar; it never reports successful cleanup while leaving that lock behind. The handle and manager retain the exact cleanup receipt after a failure, so the same handle can retry `release()` and `manager.drain()` can retry retained cleanup. A changed sidecar remains an ownership mismatch rather than a deletion failure and is left untouched. If both a `withFileLock()` or `withFileLockSync()` callback and release fail, the release error is the primary `SuppressedError.error` and the callback failure remains available as `SuppressedError.suppressed`. Failed asynchronous acquisition cleanup uses the same shape, with the cleanup error primary and the acquisition failure suppressed. On Node runtimes without the global `SuppressedError` constructor, fs-safe returns the equivalent `Error` shape with the same name and properties.
68
69
 
69
70
  ## API
70
71
 
@@ -336,6 +337,12 @@ sidecar no longer matches or after a verification I/O failure. This is
336
337
  detection, not revocation of work already in progress. Asynchronous checks are
337
338
  serialized, so a slow verification never overlaps the next timer tick.
338
339
 
340
+ Ownership-only checks compare serialized bytes, tokens, and file identities
341
+ without decoding an unused default JSON payload. Stale-policy reads still
342
+ decode the payload. Explicit `parsePayload` callbacks keep their existing
343
+ verification and asynchronous-cleanup calls, receivers, and errors;
344
+ synchronous release continues without invoking a custom parser.
345
+
339
346
  The compromise-check interval is validated before payload evaluation or
340
347
  filesystem acquisition. Omit it or pass `0` to disable monitoring; enabled
341
348
  intervals must be finite and between 1 and 2,147,483,647 milliseconds. Values
@@ -438,6 +445,7 @@ error.
438
445
  The sync payload, reclaim, and parsing callbacks must also be synchronous. This
439
446
  shape is appropriate for a short boot migration; it is a poor fit for a server
440
447
  request because retry backoff uses a blocking wait.
448
+ Synchronous `shouldReclaim` and `shouldRemoveStaleLock` reject Promise or thenable results with `TypeError` before deleting the observed sidecar; an asynchronous result is never approval.
441
449
 
442
450
  If termination skips the relevant cleanup handler or cleanup fails, the sidecar remains. In particular, `process.exit()` skips asynchronous Root cleanup; await explicit release or drain during application-managed graceful shutdown. Once `staleMs` elapses (or your `shouldReclaim` returns true), acquisition fails closed by default instead of deleting by path.
443
451
 
@@ -49,7 +49,7 @@ remain with the caller.
49
49
 
50
50
  ```ts
51
51
  function stageFileInDirectory(options: {
52
- directory: string | DirectoryReceipt;
52
+ directory: string | DirectoryReceipt<Stats | BigIntStats>;
53
53
  content: string | Uint8Array;
54
54
  mode?: number;
55
55
  }): Promise<StagedFile>;
@@ -81,8 +81,9 @@ Creation uses an exclusive, no-follow, close-on-exec open of a generated direct
81
81
  child name. Writes use that descriptor. Inspection uses non-following metadata
82
82
  operations, never a potentially blocking reopen of the leaf.
83
83
 
84
- A supplied directory receipt must still match at admission. Its numeric
85
- identity must be exactly representable; ambiguous identity fails closed.
84
+ A supplied directory receipt must still match at admission. Caller receipts can
85
+ carry numeric `Stats` or exact `BigIntStats`; untracked numeric identities must
86
+ be exactly representable. Ambiguous identity fails closed.
86
87
  Returned receipts are frozen descriptive snapshots, not mutable authority.
87
88
  Changing a supplied receipt after admission cannot retarget the lifecycle.
88
89
 
package/docs/store.md CHANGED
@@ -89,7 +89,9 @@ claim before publication. If another consumer acknowledged, quarantined, or
89
89
  replaced that claim, the migration rejects with `FsSafeError("path-mismatch")`
90
90
  and leaves the newer generation or failed evidence intact. A stale migration
91
91
  rejects both single and batch loads; ordinary callback failures retain their
92
- existing single-load rejection and batch-skip behavior.
92
+ existing single-load rejection and batch-skip behavior. A caller or migration
93
+ error with code `ENOENT` is still a failure, not a missing queue entry; only a
94
+ claim that is absent or disappears before reading returns `null` from a single load.
93
95
 
94
96
  On Windows, migration releases its read pin once at this publication boundary
95
97
  because an open target can block replacement. It rechecks the exact pathname
package/docs/temp.md CHANGED
@@ -155,12 +155,15 @@ uses guarded pathname-recursive removal. This fallback never recursively
155
155
  removes the public workspace name, but it is not atomic conditional deletion: a
156
156
  same-privilege peer that discovers and replaces the private quarantine after
157
157
  verification can still redirect the final pathname removal.
158
+ If admitting a cleanup parent fails and closing its descriptor also fails,
159
+ creation rejects with both failures in an `AggregateError`. This does not select
160
+ compatible fallback or retry the indeterminate descriptor close.
158
161
 
159
162
  Set `cleanupSafety: "require-bounded"` when that concurrent attacker is in scope.
160
163
  Creation then requires native no-replace directory rename, native owned-tree
161
164
  removal, and a readable retained parent descriptor **before** child creation.
162
165
  On POSIX, the final requested `dirMode` must also include owner read
163
- and search (`(dirMode & 0o500) === 0o500`). Preflight failure throws
166
+ and search (`(dirMode & 0o500) === 0o500`). An unavailable capability throws
164
167
  `FsSafeError("helper-unavailable")` without creating a child or calling a scoped
165
168
  callback. The child descriptor is opened
166
169
  while the new directory still has its private creation mode, before an explicit
package/docs/types.md CHANGED
@@ -128,6 +128,8 @@ type RootOptions = {
128
128
  ## `RootReadOptions` / `RootWriteOptions` / `RootCopyOptions`
129
129
 
130
130
  ```ts
131
+ import type { CopyCloneMode, RootCopyPublicationReceipt } from "@openclaw/fs-safe";
132
+
131
133
  type RootReadOptions = Pick<RootDefaults, "hardlinks" | "maxBytes" | "nonBlockingRead" | "symlinks">;
132
134
  type RootWriteOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "mkdir" | "mode" | "renameIdentity" | "mutationSymlinks"> & {
133
135
  encoding?: BufferEncoding;
@@ -135,6 +137,11 @@ type RootWriteOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutatio
135
137
  };
136
138
  type RootCopyOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "durable" | "maxBytes" | "mkdir" | "mode" | "mutationSymlinks"> & {
137
139
  sourceHardlinks?: "reject" | "allow";
140
+ overwrite?: boolean;
141
+ clone?: CopyCloneMode;
142
+ signal?: AbortSignal;
143
+ preserveSourceMode?: boolean;
144
+ onDestinationPublished?: (receipt: RootCopyPublicationReceipt) => void;
138
145
  };
139
146
  type RootOpenWritableOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mkdir" | "mode" | "mutationSymlinks"> & {
140
147
  writeMode?: "replace" | "append" | "update";
@@ -150,8 +157,17 @@ type RootAppendOptions = RootWriteOptions & {
150
157
  type RootMoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
151
158
  overwrite?: boolean;
152
159
  };
153
- type RootRemoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks">;
154
- type RootMkdirOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks">;
160
+ type RootRemoveOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
161
+ recursive?: boolean;
162
+ force?: boolean;
163
+ order?: "filesystem" | "sorted";
164
+ maxEntries?: number;
165
+ maxDepth?: number;
166
+ signal?: AbortSignal;
167
+ };
168
+ type RootMkdirOptions = Pick<RootDefaults, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks"> & {
169
+ private?: boolean;
170
+ };
155
171
  ```
156
172
 
157
173
  Per-method option shapes. Each picks the `RootDefaults` keys that apply, plus method-specific extras.
package/docs/walk.md CHANGED
@@ -47,6 +47,10 @@ type WalkDirectoryFailure = {
47
47
 
48
48
  `depth` starts at `1` for direct children of `rootDir`. `relativePath` is always relative to the supplied root. `scannedEntryCount` counts directory entries examined, including entries filtered out by `include`.
49
49
 
50
+ Each entry's `path` is absolute and retains the normalized spelling of the
51
+ supplied root, including followed directory aliases. Paths do not switch to
52
+ the canonical symlink target during descent.
53
+
50
54
  `walkDirectory()` and `walkDirectorySync()` always return `failedDirs`; the property remains optional on the exported `WalkDirectoryResult` type so existing callers that manually construct the legacy result shape remain source-compatible. It lists every directory whose `realpath`/`readdir` threw, so its contents are absent from `entries`. `error` is the thrown value (a `NodeJS.ErrnoException` at runtime), so callers can distinguish a benign missing-directory race (`ENOENT`) from a real read failure (`EACCES`, `EIO`, `ESTALE`, …). The walk-root failure has an empty `relativePath` and `depth: 0`. Failures resolving a symlink's target kind are not reported here.
51
55
 
52
56
  ## Options
@@ -227,6 +231,9 @@ its exact identity, and rechecks it and the Root identity around each metadata
227
231
  batch or individual filesystem-order observation. Sorted batches contain no
228
232
  await or caller code between their before/after checks. It tracks canonical
229
233
  directories to stop symlink cycles.
234
+ Directory rechecks retain exact identities while using ordinary numeric metadata
235
+ when it represents those identities without rounding. Large identities and
236
+ Windows unknown-identity retries keep the bigint inspection path.
230
237
  Neither mode holds a descriptor for every path component, so it is not a process sandbox against a hostile peer that
231
238
  can continuously swap and restore directories. Each individual lookup retains
232
239
  the documented Node `Root` boundary checks.
package/docs/writing.md CHANGED
@@ -154,11 +154,17 @@ claimed exclusively first and content is written afterward, so observers can
154
154
  briefly see an empty file; failure cleanup removes a claimed file only when its
155
155
  identity is unchanged.
156
156
 
157
+ After a successful create-only write, failure to close its owned file handle
158
+ rejects the operation and leaves the complete file present. Ordinary buffered
159
+ creation in the JavaScript fallback preserves an earlier write or verification
160
+ failure if close also fails. Native, atomic, and streamed creation retain their
161
+ existing publication and cleanup diagnostics.
162
+
157
163
  ```ts
158
164
  try {
159
165
  await fs.create("config/seed.json", initial);
160
166
  } catch (err) {
161
- if (err instanceof FsSafeError && err.code !== "already-exists") throw err;
167
+ if (!(err instanceof FsSafeError) || err.code !== "already-exists") throw err;
162
168
  }
163
169
  ```
164
170
 
@@ -201,7 +207,8 @@ already published complete destination. See [receipt meanings](staged-file.md).
201
207
 
202
208
  Pass an `AsyncIterable<Uint8Array>` to `create()` when bytes come from a database,
203
209
  network response, or another incremental producer. Buffers are accepted chunks.
204
- The writer consumes each chunk completely before requesting the next one; it
210
+ The writer borrows `Uint8Array` slices without copying their payloads and counts
211
+ their actual byte bounds. It consumes each chunk completely before requesting the next one; it
205
212
  does not collect the full input in memory or expose a writable descriptor.
206
213
 
207
214
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -170,13 +170,13 @@
170
170
  "archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
171
171
  },
172
172
  "optionalDependencies": {
173
- "@openclaw/fs-safe-darwin-arm64": "0.16.0",
174
- "@openclaw/fs-safe-darwin-x64": "0.16.0",
175
- "@openclaw/fs-safe-linux-arm64-gnu": "0.16.0",
176
- "@openclaw/fs-safe-linux-arm64-musl": "0.16.0",
177
- "@openclaw/fs-safe-linux-x64-gnu": "0.16.0",
178
- "@openclaw/fs-safe-linux-x64-musl": "0.16.0",
179
- "@openclaw/fs-safe-win32-x64-msvc": "0.16.0",
173
+ "@openclaw/fs-safe-darwin-arm64": "0.17.0",
174
+ "@openclaw/fs-safe-darwin-x64": "0.17.0",
175
+ "@openclaw/fs-safe-linux-arm64-gnu": "0.17.0",
176
+ "@openclaw/fs-safe-linux-arm64-musl": "0.17.0",
177
+ "@openclaw/fs-safe-linux-x64-gnu": "0.17.0",
178
+ "@openclaw/fs-safe-linux-x64-musl": "0.17.0",
179
+ "@openclaw/fs-safe-win32-x64-msvc": "0.17.0",
180
180
  "jszip": "^3.10.2"
181
181
  },
182
182
  "devDependencies": {