@openclaw/fs-safe 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/LICENSE +1 -0
  3. package/README.md +52 -4
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +3 -2
  6. package/dist/advanced.d.ts +5 -0
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +5 -0
  9. package/dist/archive-crc32.d.ts.map +1 -1
  10. package/dist/archive-crc32.js +6 -1
  11. package/dist/archive-deadline.d.ts.map +1 -1
  12. package/dist/archive-deadline.js +3 -4
  13. package/dist/archive-durability.d.ts +6 -6
  14. package/dist/archive-durability.d.ts.map +1 -1
  15. package/dist/archive-durability.js +1 -1
  16. package/dist/archive-entry.d.ts.map +1 -1
  17. package/dist/archive-entry.js +4 -5
  18. package/dist/archive-gzip-tail.d.ts +3 -0
  19. package/dist/archive-gzip-tail.d.ts.map +1 -1
  20. package/dist/archive-gzip-tail.js +23 -3
  21. package/dist/archive-input.d.ts.map +1 -1
  22. package/dist/archive-input.js +4 -2
  23. package/dist/archive-merge.d.ts +5 -1
  24. package/dist/archive-merge.d.ts.map +1 -1
  25. package/dist/archive-merge.js +16 -13
  26. package/dist/archive-native.d.ts.map +1 -1
  27. package/dist/archive-native.js +7 -6
  28. package/dist/archive-parser.wasm +0 -0
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +83 -74
  31. package/dist/archive-staging.d.ts +6 -3
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +42 -22
  34. package/dist/archive-tar-stream.d.ts +11 -4
  35. package/dist/archive-tar-stream.d.ts.map +1 -1
  36. package/dist/archive-tar-stream.js +23 -10
  37. package/dist/archive-tar-wasm.d.ts.map +1 -1
  38. package/dist/archive-tar-wasm.js +18 -14
  39. package/dist/archive-zip-admission.d.ts +1 -1
  40. package/dist/archive-zip-admission.d.ts.map +1 -1
  41. package/dist/archive-zip-admission.js +48 -12
  42. package/dist/archive-zip-loader.d.ts +6 -0
  43. package/dist/archive-zip-loader.d.ts.map +1 -0
  44. package/dist/archive-zip-loader.js +38 -0
  45. package/dist/archive-zip-names.d.ts.map +1 -1
  46. package/dist/archive-zip-names.js +13 -8
  47. package/dist/archive-zip-preflight.d.ts +2 -3
  48. package/dist/archive-zip-preflight.d.ts.map +1 -1
  49. package/dist/archive-zip-preflight.js +2 -34
  50. package/dist/archive.d.ts.map +1 -1
  51. package/dist/archive.js +12 -10
  52. package/dist/bounded-read.d.ts +12 -0
  53. package/dist/bounded-read.d.ts.map +1 -1
  54. package/dist/bounded-read.js +82 -45
  55. package/dist/clone-metadata.d.ts +19 -0
  56. package/dist/clone-metadata.d.ts.map +1 -0
  57. package/dist/clone-metadata.js +32 -0
  58. package/dist/copy-file-input.d.ts +22 -0
  59. package/dist/copy-file-input.d.ts.map +1 -0
  60. package/dist/copy-file-input.js +69 -0
  61. package/dist/copy-policy.d.ts +3 -0
  62. package/dist/copy-policy.d.ts.map +1 -0
  63. package/dist/copy-policy.js +8 -0
  64. package/dist/copy-publication.d.ts +10 -1
  65. package/dist/copy-publication.d.ts.map +1 -1
  66. package/dist/copy-publication.js +27 -0
  67. package/dist/copy-tree-portable.d.ts +9 -0
  68. package/dist/copy-tree-portable.d.ts.map +1 -0
  69. package/dist/copy-tree-portable.js +222 -0
  70. package/dist/copy.d.ts +16 -0
  71. package/dist/copy.d.ts.map +1 -0
  72. package/dist/copy.js +125 -0
  73. package/dist/directory-durability.d.ts.map +1 -1
  74. package/dist/directory-durability.js +5 -4
  75. package/dist/directory-guard.d.ts +11 -1
  76. package/dist/directory-guard.d.ts.map +1 -1
  77. package/dist/directory-guard.js +53 -11
  78. package/dist/durability.d.ts +1 -1
  79. package/dist/durability.d.ts.map +1 -1
  80. package/dist/durability.js +1 -1
  81. package/dist/error-detail.d.ts.map +1 -1
  82. package/dist/error-detail.js +4 -1
  83. package/dist/file-handle-transfer.d.ts +14 -0
  84. package/dist/file-handle-transfer.d.ts.map +1 -0
  85. package/dist/file-handle-transfer.js +64 -0
  86. package/dist/file-hash.d.ts +9 -2
  87. package/dist/file-hash.d.ts.map +1 -1
  88. package/dist/file-hash.js +135 -39
  89. package/dist/file-lock-sync.d.ts.map +1 -1
  90. package/dist/file-lock-sync.js +8 -4
  91. package/dist/file-store-boundary.d.ts.map +1 -1
  92. package/dist/file-store-boundary.js +7 -5
  93. package/dist/file-store-path.d.ts +3 -0
  94. package/dist/file-store-path.d.ts.map +1 -0
  95. package/dist/file-store-path.js +27 -0
  96. package/dist/file-store-prune.d.ts.map +1 -1
  97. package/dist/file-store-prune.js +6 -4
  98. package/dist/file-store-sync-write.d.ts.map +1 -1
  99. package/dist/file-store-sync-write.js +56 -44
  100. package/dist/file-store.d.ts.map +1 -1
  101. package/dist/file-store.js +2 -18
  102. package/dist/filename.d.ts.map +1 -1
  103. package/dist/filename.js +4 -13
  104. package/dist/guarded-mkdir.d.ts +1 -0
  105. package/dist/guarded-mkdir.d.ts.map +1 -1
  106. package/dist/guarded-mkdir.js +4 -2
  107. package/dist/guarded-mutation.d.ts +2 -0
  108. package/dist/guarded-mutation.d.ts.map +1 -1
  109. package/dist/guarded-mutation.js +8 -4
  110. package/dist/guest-dispatch-python.d.ts +2 -0
  111. package/dist/guest-dispatch-python.d.ts.map +1 -0
  112. package/dist/guest-dispatch-python.js +117 -0
  113. package/dist/guest-native-python.d.ts +4 -0
  114. package/dist/guest-native-python.d.ts.map +1 -0
  115. package/dist/guest-native-python.js +135 -0
  116. package/dist/guest.d.ts +9 -0
  117. package/dist/guest.d.ts.map +1 -0
  118. package/dist/guest.js +413 -0
  119. package/dist/home-dir.d.ts.map +1 -1
  120. package/dist/home-dir.js +9 -7
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/install-path.d.ts.map +1 -1
  124. package/dist/install-path.js +5 -8
  125. package/dist/json-durable-queue-directory.js +3 -3
  126. package/dist/json-durable-queue.d.ts.map +1 -1
  127. package/dist/json-durable-queue.js +19 -18
  128. package/dist/json.d.ts.map +1 -1
  129. package/dist/json.js +2 -1
  130. package/dist/local-roots.d.ts.map +1 -1
  131. package/dist/local-roots.js +24 -25
  132. package/dist/move-path-stage.d.ts +8 -0
  133. package/dist/move-path-stage.d.ts.map +1 -0
  134. package/dist/move-path-stage.js +56 -0
  135. package/dist/move-path.d.ts.map +1 -1
  136. package/dist/move-path.js +30 -34
  137. package/dist/mutation-authority.d.ts +9 -0
  138. package/dist/mutation-authority.d.ts.map +1 -0
  139. package/dist/mutation-authority.js +36 -0
  140. package/dist/native-binding.d.ts +29 -1
  141. package/dist/native-binding.d.ts.map +1 -1
  142. package/dist/native-operations.d.ts +1 -1
  143. package/dist/native-operations.d.ts.map +1 -1
  144. package/dist/native-operations.js +11 -5
  145. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  146. package/dist/native-pinned-write-windows.js +19 -7
  147. package/dist/native-pinned-write.d.ts.map +1 -1
  148. package/dist/native-pinned-write.js +3 -1
  149. package/dist/native-staged-file.d.ts +3 -3
  150. package/dist/native-staged-file.d.ts.map +1 -1
  151. package/dist/native-staged-file.js +35 -8
  152. package/dist/opened-realpath.d.ts.map +1 -1
  153. package/dist/opened-realpath.js +5 -4
  154. package/dist/output.d.ts +2 -0
  155. package/dist/output.d.ts.map +1 -1
  156. package/dist/output.js +2 -0
  157. package/dist/overwrite-file-handle.d.ts +8 -0
  158. package/dist/overwrite-file-handle.d.ts.map +1 -0
  159. package/dist/overwrite-file-handle.js +42 -0
  160. package/dist/path-case.d.ts +7 -0
  161. package/dist/path-case.d.ts.map +1 -0
  162. package/dist/path-case.js +136 -0
  163. package/dist/path.d.ts.map +1 -1
  164. package/dist/path.js +5 -2
  165. package/dist/permissions-windows.d.ts +1 -1
  166. package/dist/permissions-windows.d.ts.map +1 -1
  167. package/dist/permissions-windows.js +85 -53
  168. package/dist/pinned-open.d.ts.map +1 -1
  169. package/dist/pinned-open.js +3 -1
  170. package/dist/pinned-operation.js +1 -1
  171. package/dist/pinned-write.d.ts +7 -3
  172. package/dist/pinned-write.d.ts.map +1 -1
  173. package/dist/pinned-write.js +51 -30
  174. package/dist/positional-read.d.ts +9 -0
  175. package/dist/positional-read.d.ts.map +1 -0
  176. package/dist/positional-read.js +36 -0
  177. package/dist/private-temp-workspace.d.ts.map +1 -1
  178. package/dist/private-temp-workspace.js +6 -4
  179. package/dist/publish-copy-stage.d.ts +13 -0
  180. package/dist/publish-copy-stage.d.ts.map +1 -0
  181. package/dist/publish-copy-stage.js +47 -0
  182. package/dist/publish-file.d.ts.map +1 -1
  183. package/dist/publish-file.js +6 -5
  184. package/dist/realpath.d.ts +4 -0
  185. package/dist/realpath.d.ts.map +1 -0
  186. package/dist/realpath.js +43 -0
  187. package/dist/recursive-mkdir-path.d.ts +3 -0
  188. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  189. package/dist/recursive-mkdir-path.js +8 -0
  190. package/dist/replace-directory.d.ts.map +1 -1
  191. package/dist/replace-directory.js +2 -1
  192. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  193. package/dist/replace-file-copy-fallback.js +23 -31
  194. package/dist/replace-file-copy-source.d.ts.map +1 -1
  195. package/dist/replace-file-copy-source.js +7 -12
  196. package/dist/replace-file-mode.d.ts +3 -0
  197. package/dist/replace-file-mode.d.ts.map +1 -0
  198. package/dist/replace-file-mode.js +10 -0
  199. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  200. package/dist/replace-file-temp-owner.js +6 -2
  201. package/dist/replace-file.d.ts +1 -0
  202. package/dist/replace-file.d.ts.map +1 -1
  203. package/dist/replace-file.js +14 -8
  204. package/dist/root-context.d.ts +3 -0
  205. package/dist/root-context.d.ts.map +1 -1
  206. package/dist/root-context.js +53 -28
  207. package/dist/root-create-input.d.ts +10 -0
  208. package/dist/root-create-input.d.ts.map +1 -0
  209. package/dist/root-create-input.js +80 -0
  210. package/dist/root-directory-list.d.ts +26 -0
  211. package/dist/root-directory-list.d.ts.map +1 -0
  212. package/dist/root-directory-list.js +219 -0
  213. package/dist/root-entries.d.ts +11 -0
  214. package/dist/root-entries.d.ts.map +1 -0
  215. package/dist/root-entries.js +61 -0
  216. package/dist/root-errors.d.ts +6 -5
  217. package/dist/root-errors.d.ts.map +1 -1
  218. package/dist/root-errors.js +16 -12
  219. package/dist/root-file.d.ts +2 -0
  220. package/dist/root-file.d.ts.map +1 -1
  221. package/dist/root-file.js +12 -4
  222. package/dist/root-impl.d.ts +17 -51
  223. package/dist/root-impl.d.ts.map +1 -1
  224. package/dist/root-impl.js +358 -234
  225. package/dist/root-options.d.ts +77 -0
  226. package/dist/root-options.d.ts.map +1 -0
  227. package/dist/root-options.js +18 -0
  228. package/dist/root-path-existing.d.ts +5 -0
  229. package/dist/root-path-existing.d.ts.map +1 -1
  230. package/dist/root-path-existing.js +59 -3
  231. package/dist/root-path-symlink.d.ts.map +1 -1
  232. package/dist/root-path-symlink.js +3 -2
  233. package/dist/root-path.d.ts +1 -0
  234. package/dist/root-path.d.ts.map +1 -1
  235. package/dist/root-path.js +74 -162
  236. package/dist/root-paths.d.ts.map +1 -1
  237. package/dist/root-paths.js +13 -9
  238. package/dist/root-remove.d.ts +5 -0
  239. package/dist/root-remove.d.ts.map +1 -0
  240. package/dist/root-remove.js +286 -0
  241. package/dist/root-symlink-policy.d.ts +14 -0
  242. package/dist/root-symlink-policy.d.ts.map +1 -0
  243. package/dist/root-symlink-policy.js +34 -0
  244. package/dist/root-walk.d.ts +5 -4
  245. package/dist/root-walk.d.ts.map +1 -1
  246. package/dist/root-walk.js +99 -50
  247. package/dist/root-write-mode.d.ts.map +1 -1
  248. package/dist/root-write-mode.js +3 -5
  249. package/dist/root.d.ts +4 -1
  250. package/dist/root.d.ts.map +1 -1
  251. package/dist/secret-file.d.ts.map +1 -1
  252. package/dist/secret-file.js +2 -1
  253. package/dist/secret-read-async.d.ts.map +1 -1
  254. package/dist/secret-read-async.js +2 -1
  255. package/dist/secure-file.d.ts.map +1 -1
  256. package/dist/secure-file.js +25 -8
  257. package/dist/secure-temp-dir.d.ts.map +1 -1
  258. package/dist/secure-temp-dir.js +2 -1
  259. package/dist/sibling-staged-file.d.ts +1 -0
  260. package/dist/sibling-staged-file.d.ts.map +1 -1
  261. package/dist/sibling-staged-file.js +42 -8
  262. package/dist/sibling-temp.d.ts +2 -0
  263. package/dist/sibling-temp.d.ts.map +1 -1
  264. package/dist/sibling-temp.js +6 -4
  265. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  266. package/dist/sidecar-lock-acquire.js +17 -5
  267. package/dist/sidecar-lock-policy.d.ts +2 -0
  268. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  269. package/dist/sidecar-lock-policy.js +20 -1
  270. package/dist/staged-directory.d.ts.map +1 -1
  271. package/dist/staged-directory.js +4 -3
  272. package/dist/temp-cleanup.d.ts.map +1 -1
  273. package/dist/temp-cleanup.js +3 -2
  274. package/dist/temp-target.d.ts +14 -12
  275. package/dist/temp-target.d.ts.map +1 -1
  276. package/dist/temp-target.js +12 -6
  277. package/dist/timing.d.ts +1 -0
  278. package/dist/timing.d.ts.map +1 -1
  279. package/dist/timing.js +25 -6
  280. package/dist/trash.d.ts.map +1 -1
  281. package/dist/trash.js +9 -7
  282. package/dist/unicode-path.d.ts +3 -0
  283. package/dist/unicode-path.d.ts.map +1 -0
  284. package/dist/unicode-path.js +13 -0
  285. package/dist/walk.d.ts.map +1 -1
  286. package/dist/walk.js +9 -28
  287. package/dist/windows-owner.d.ts +9 -12
  288. package/dist/windows-owner.d.ts.map +1 -1
  289. package/dist/windows-owner.js +24 -58
  290. package/dist/write-file-handle.d.ts +7 -0
  291. package/dist/write-file-handle.d.ts.map +1 -0
  292. package/dist/write-file-handle.js +26 -0
  293. package/docs/advanced.md +22 -4
  294. package/docs/archive.md +44 -9
  295. package/docs/atomic.md +24 -1
  296. package/docs/config.md +1 -0
  297. package/docs/contributing.md +36 -1
  298. package/docs/copy.md +155 -0
  299. package/docs/directory-identity.md +85 -0
  300. package/docs/durability.md +53 -3
  301. package/docs/entries.md +109 -0
  302. package/docs/errors.md +4 -4
  303. package/docs/file-store.md +15 -0
  304. package/docs/guest.md +141 -0
  305. package/docs/in-place-write.md +81 -0
  306. package/docs/index.md +2 -0
  307. package/docs/install.md +31 -0
  308. package/docs/local-roots.md +8 -1
  309. package/docs/native-helper.md +10 -3
  310. package/docs/native.md +18 -1
  311. package/docs/output.md +32 -6
  312. package/docs/path-case.md +64 -0
  313. package/docs/path-scope.md +1 -1
  314. package/docs/permissions.md +29 -10
  315. package/docs/positional-read.md +63 -0
  316. package/docs/public-api.md +31 -2
  317. package/docs/root.md +196 -6
  318. package/docs/secure-file.md +2 -0
  319. package/docs/security-model.md +14 -0
  320. package/docs/sidecar-lock.md +12 -3
  321. package/docs/temp.md +35 -6
  322. package/docs/timing.md +2 -0
  323. package/docs/types.md +19 -12
  324. package/docs/walk.md +54 -5
  325. package/docs/writing.md +173 -5
  326. package/package.json +19 -8
package/docs/writing.md CHANGED
@@ -38,6 +38,12 @@ await fs.mkdir("snapshots/2026/05");
38
38
  Private sibling temporary names are independent of the destination basename,
39
39
  so staging does not add a suffix to an otherwise valid long filename.
40
40
 
41
+ Write paths reject `path-alias` when symlinks and parent components select a
42
+ different target before and after lexical normalization, such as `link/../file`
43
+ where `link` points into a deeper directory. This avoids silently modifying the
44
+ wrong file. Resolve an intended alias explicitly with `Root.resolve()` before
45
+ passing its canonical path to a mutation.
46
+
41
47
  A failure before the final rename leaves the destination at its previous
42
48
  contents. A successful rename publishes the complete replacement. This
43
49
  old-or-new guarantee does not apply to `append()` or `openWritable()`, which
@@ -48,6 +54,10 @@ Post-publication verification can still reject after a complete replacement has
48
54
  been committed. Rejection does not promise that a successful rename was rolled
49
55
  back; the published file or a raced replacement may remain at the destination.
50
56
 
57
+ Failed-write cleanup compares exact parent and file identities, including large
58
+ Windows file indexes. Replaced paths and paths whose ownership cannot be verified
59
+ are preserved.
60
+
51
61
  ## Denying mutations
52
62
 
53
63
  All mutation verbs accept `denyMutations?: DenyMutationPolicy`, either as a root default or per-call option:
@@ -99,8 +109,9 @@ writes but skips file and parent-directory fsync calls. Create-only and append
99
109
  publication behavior, permissions, identity checks, and error codes are unchanged.
100
110
  Use it only for reconstructible data: a crash may lose the write or leave the
101
111
  previous file. `move` and streaming `openWritable` do not use this option.
102
- The existing pure-JavaScript Windows writer performs no fsync calls in either
103
- setting; native Windows writes honor the option. Directory sync remains best-effort.
112
+ Native and pure-JavaScript Windows writers honor the option. Replacement writes
113
+ sync staged content before rename and the final mode through the retained file
114
+ handle. Directory sync remains best-effort.
104
115
 
105
116
  POSIX modes without read permission, including `0o000` and `0o200`, succeed:
106
117
  final verification uses a descriptor retained by the writer rather than reopening
@@ -143,6 +154,61 @@ try {
143
154
  }
144
155
  ```
145
156
 
157
+ ### Streamed creation
158
+
159
+ Pass an `AsyncIterable<Uint8Array>` to `create()` when bytes come from a database,
160
+ network response, or another incremental producer. Buffers are accepted chunks.
161
+ The writer consumes each chunk completely before requesting the next one; it
162
+ does not collect the full input in memory or expose a writable descriptor.
163
+
164
+ ```ts
165
+ async function* snapshotChunks(): AsyncGenerator<Uint8Array> {
166
+ yield Buffer.from("first stored chunk\n");
167
+ yield Buffer.from("second stored chunk\n");
168
+ }
169
+
170
+ await fs.create("restored/config.txt", snapshotChunks(), {
171
+ mode: 0o600,
172
+ maxBytes: 8 * 1024 * 1024,
173
+ durable: false,
174
+ signal: AbortSignal.timeout(30_000),
175
+ });
176
+ ```
177
+
178
+ `RootCreateStreamOptions` keeps `mkdir`, `mode`, `durable`,
179
+ `assertBeforeMutation`, `denyMutations`, and `mutationSymlinks` from buffered
180
+ creation and adds `maxBytes` and `signal`. Byte chunks have no encoding option;
181
+ streamed creation uses strict publication identity and does not support
182
+ `renameIdentity: "verify-content-with-lock"`. Existing Root defaults apply,
183
+ including explicit `maxBytes`; with no byte cap at either level, input size is
184
+ unlimited. Zero permits an empty input only. Invalid limits reject before I/O.
185
+
186
+ Unlike buffered creation's JavaScript fallback, streamed creation stages all
187
+ chunks before publishing the final name. Native mode uses no-replace rename;
188
+ the JavaScript fallback hardlinks the completed stage and removes its temporary
189
+ name in the same JavaScript turn. That fallback requires a filesystem supporting
190
+ hardlinks; other processes may briefly observe both names. An existing or
191
+ concurrently created destination is preserved. Existing-target preflight does
192
+ not consume the input. The final mode and durability policy use the same guarded
193
+ writer as other Root operations.
194
+
195
+ Cancellation checks run before and after producer pulls, before content writes,
196
+ and before publication. `assertBeforeMutation` also rechecks current application
197
+ authority after producer waits and before each partial write. The operation
198
+ waits for any pending producer pull or filesystem write, then awaits the
199
+ producer's `return()` and cleans only the owned unpublished stage. Pass the same
200
+ signal into a producer that may stall: an arbitrary async iterator cannot be
201
+ forcibly interrupted, so cancellation waits for its pending work and cleanup to
202
+ settle. Do not mutate a yielded chunk until the next pull. Producer errors retain
203
+ their original value when cleanup succeeds.
204
+
205
+ An aborted or failed operation can leave created parent directories. If a
206
+ stage's identity or parent cannot be verified during cleanup, the existing
207
+ guarded cleanup preserves it. After publication, later verification or cleanup
208
+ failures preserve the destination; rejection does not prove that no file was
209
+ created. Cancellation arriving after publication does not undo the completed
210
+ file. Application recovery remains caller-owned.
211
+
146
212
  ### `fs.writeJson(rel, value, options?)`
147
213
 
148
214
  `JSON.stringify(value, replacer, space)` + atomic write. Adds a trailing newline by default.
@@ -202,16 +268,118 @@ await fs.move("incoming/foo.txt", "archive/foo.txt", { overwrite: true });
202
268
 
203
269
  Both `from` and `to` are bounded; `..` in either is rejected.
204
270
 
271
+ The JavaScript fallback checks both parent directories before and after the
272
+ rename. A failed post-operation check rejects even though the rename may
273
+ already have completed; rejection does not imply rollback.
274
+
205
275
  ### `fs.remove(rel)`
206
276
 
207
277
  Unlink a file or `rmdir` an empty directory. Non-empty directories throw `not-empty`. For atomic directory replacement, use [`replaceDirectoryAtomic`](atomic.md#replacedirectoryatomic).
208
278
 
279
+ The JavaScript fallback reports failed parent-directory checks after removal.
280
+ The entry may already have been removed when this verification rejects.
281
+
209
282
  ```ts
210
283
  await fs.remove("logs/yesterday.log");
211
284
  await fs.remove("snapshots/empty-dir"); // ok
212
285
  await fs.remove("snapshots/full-dir"); // throws not-empty
213
286
  ```
214
287
 
288
+ For a tree, opt into bounded recursive removal:
289
+
290
+ ```ts
291
+ await fs.remove("scratch/finished-job", {
292
+ recursive: true,
293
+ force: true,
294
+ maxEntries: 20_000,
295
+ maxDepth: 32,
296
+ mutationSymlinks: "reject",
297
+ signal: AbortSignal.timeout(30_000),
298
+ assertBeforeMutation: () => assertJobLeaseCurrent(),
299
+ });
300
+ ```
301
+
302
+ | Option | Default / behavior |
303
+ | --- | --- |
304
+ | `recursive` | `false`; opt in to removing non-empty directories. |
305
+ | `force` | `false`; `true` tolerates missing targets and vanished child entries. Other errors still reject. |
306
+ | `order` | `"filesystem"`; `"sorted"` collects and sorts child names lexicographically before descending. Requires `recursive: true`. |
307
+ | `maxEntries` | `100_000` in recursive mode; counts the requested target and every encountered child, including directories and symlinks. |
308
+ | `maxDepth` | `64` in recursive mode; the requested target has depth 0 and each child adds one. An empty directory at the limit can be removed. |
309
+ | `signal` | Stops traversal and new mutations when aborted. Already dispatched work settles and directory handles close before rejection. |
310
+
311
+ Budgets must be non-negative safe integers or explicit `Infinity`, and require
312
+ `recursive: true`. Omitted budgets retain their finite defaults. An entry or
313
+ depth limit throws `too-large` before processing the over-budget entry.
314
+
315
+ The default filesystem order streams each directory once, using memory and open
316
+ handles proportional to depth rather than directory width. Sorted order also
317
+ enumerates each directory once, but collects its names before descending and
318
+ deletes directories after their children. It uses JavaScript's default string
319
+ sort, not locale collation. Collected names consume the shared entry budget,
320
+ including sibling names still pending while an earlier directory is traversed.
321
+ With a finite budget, collection overflow rejects before processing that
322
+ directory's children; one extra name distinguishes an exact limit from overflow.
323
+ An `Infinity` entry budget uses a bulk name read while retaining the opened
324
+ directory handle and identity checks. Sorted mode retains collected names, so
325
+ unlimited budgets also permit unlimited name storage.
326
+
327
+ Sorted traversal preserves lexicographic processing, not the incidental syscall
328
+ timing of a caller that repeatedly rescans parent directories. Each child is
329
+ inspected when visited. Newly added entries can make the final `rmdir` fail with
330
+ `not-empty`; the operation does not retry indefinitely.
331
+
332
+ Recursive removal never follows a discovered symlink or junction. With an
333
+ omitted `mutationSymlinks` policy it unlinks that entry, preserving the existing
334
+ nonrecursive behavior. Both explicit mutation policies reject discovered links.
335
+ `follow-parents-within-root` permits aliases only in the requested target's
336
+ parents, and still rejects a final link. Root and per-call `denyMutations` policies
337
+ remain additive; a denied descendant prevents removal of the enclosing requested
338
+ tree before any entry is removed.
339
+
340
+ The operation retains exact identities for the traversal directories and each
341
+ observed target. Swapped or missing ancestors reject even with `force: true`;
342
+ an abort reason or authority refusal carrying `ENOENT` is not treated as absence.
343
+ Authority is rechecked immediately before each direct `unlink` or `rmdir`
344
+ dispatch. Directory handles close before their directories are removed, including
345
+ on Windows. If both traversal and close fail, `SuppressedError` retains both
346
+ failures. Directory-stream filesystem errors use the same removal codes as
347
+ `unlink` and `rmdir`, with the original error in `cause`; caller abort and
348
+ authority refusals retain their original values.
349
+
350
+ When `force: true` encounters a missing directory during an `opendir` or read,
351
+ it closes any open stream and reaches the ordinary final target check. The
352
+ surviving ancestors and original target identity are checked again; a replacement
353
+ is never accepted as a missing directory. An actually vanished child does not
354
+ prevent processing later siblings.
355
+
356
+ Filesystem failures normalized by the recursive removal owner and its direct
357
+ symlink rejections carry additional `FsSafeError.details` context:
358
+
359
+ ```ts
360
+ type RemoveFailureDetails = {
361
+ operation: "remove";
362
+ phase: "enumerate" | "inspect" | "remove";
363
+ relativePath: string;
364
+ };
365
+ ```
366
+
367
+ `relativePath` is relative to the requested removal target, using host path
368
+ separators: `""` identifies that target and `"nested/link"` identifies a child
369
+ on POSIX. It does not replace caller spelling with the canonical Root path.
370
+ `enumerate` covers directory stream operations, `inspect` covers initial child
371
+ observations, and `remove` covers final identity checks and unlink/rmdir failures.
372
+ The existing error codes, messages, and causes remain unchanged. Errors that
373
+ already have their own identity, including caller authority and cancellation
374
+ reasons, are propagated without adding or changing their details. Context is
375
+ diagnostic; it is not permission to retry or mutate an entry.
376
+
377
+ Removal is incremental, not atomic. A later budget, cancellation, identity, or
378
+ filesystem failure does not restore already removed entries. As with existing
379
+ `remove`, this is a guarded JavaScript operation in every native mode: pathname
380
+ checks are best-effort against a hostile concurrent process and do not create
381
+ an atomic check-and-delete syscall. Use OS isolation for that threat model.
382
+
215
383
  ### `fs.mkdir(rel)`
216
384
 
217
385
  `mkdir -p`. Creates missing parents.
@@ -247,9 +415,9 @@ try {
247
415
  Options are `{ denyMutations?, mkdir?, mode?, writeMode? }`, where `writeMode`
248
416
  is `"replace"` (default), `"append"`, or `"update"`. `replace` truncates existing
249
417
  files; `update` keeps existing contents. Streaming writes go directly to the
250
- destination — there is no atomic-rename step. If you need both streaming and
251
- atomicity, write to a sibling temp yourself and rename when done; the
252
- [`atomic`](atomic.md) helpers can do this for you.
418
+ destination — there is no atomic-rename step. For exclusive publication of a
419
+ complete stream, use [`create()`](#streamed-creation). For streamed replacement,
420
+ the [`atomic`](atomic.md) helpers provide a staged writer.
253
421
 
254
422
  On POSIX, existing-target opens use `O_NONBLOCK` as an admission safeguard so
255
423
  a no-reader FIFO cannot stall regular-file validation. This does not change
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "keywords": [
6
6
  "filesystem",
@@ -46,6 +46,14 @@
46
46
  "types": "./dist/root.d.ts",
47
47
  "default": "./dist/root.js"
48
48
  },
49
+ "./copy": {
50
+ "types": "./dist/copy.d.ts",
51
+ "default": "./dist/copy.js"
52
+ },
53
+ "./guest": {
54
+ "types": "./dist/guest.d.ts",
55
+ "default": "./dist/guest.js"
56
+ },
49
57
  "./config": {
50
58
  "types": "./dist/config.d.ts",
51
59
  "default": "./dist/config.js"
@@ -126,12 +134,15 @@
126
134
  },
127
135
  "scripts": {
128
136
  "benchmark": "node scripts/benchmark.mjs",
137
+ "benchmark:methods": "node benchmarks/runner.mjs",
129
138
  "benchmark:publish": "pnpm build && node scripts/bench-publish.mjs",
130
139
  "build": "node scripts/prepack-build.mjs",
131
140
  "lint:file-size": "node scripts/check-file-size.mjs",
132
141
  "lint:fs-boundary": "node scripts/check-fs-boundary-primitives.mjs",
133
142
  "prepack": "node scripts/prepack-build.mjs",
134
143
  "test": "vitest run",
144
+ "test:bun": "bun node_modules/vitest/vitest.mjs run --config scripts/bun-vitest.config.ts",
145
+ "test:bun:native": "bun scripts/bun-native-proof.mjs && bun node_modules/vitest/vitest.mjs run --config scripts/bun-native-vitest.config.ts",
135
146
  "test:coverage": "vitest run --coverage",
136
147
  "test:coverage:collect": "pnpm build && vitest run --coverage --coverage.reporter=json --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.statements=0 --coverage.thresholds.branches=0",
137
148
  "test:coverage:merge": "node scripts/merge-coverage.mjs",
@@ -156,13 +167,13 @@
156
167
  "archive:producer-smoke": "node scripts/archive-producer-smoke.mjs"
157
168
  },
158
169
  "optionalDependencies": {
159
- "@openclaw/fs-safe-darwin-arm64": "0.9.0",
160
- "@openclaw/fs-safe-darwin-x64": "0.9.0",
161
- "@openclaw/fs-safe-linux-arm64-gnu": "0.9.0",
162
- "@openclaw/fs-safe-linux-arm64-musl": "0.9.0",
163
- "@openclaw/fs-safe-linux-x64-gnu": "0.9.0",
164
- "@openclaw/fs-safe-linux-x64-musl": "0.9.0",
165
- "@openclaw/fs-safe-win32-x64-msvc": "0.9.0",
170
+ "@openclaw/fs-safe-darwin-arm64": "0.11.0",
171
+ "@openclaw/fs-safe-darwin-x64": "0.11.0",
172
+ "@openclaw/fs-safe-linux-arm64-gnu": "0.11.0",
173
+ "@openclaw/fs-safe-linux-arm64-musl": "0.11.0",
174
+ "@openclaw/fs-safe-linux-x64-gnu": "0.11.0",
175
+ "@openclaw/fs-safe-linux-x64-musl": "0.11.0",
176
+ "@openclaw/fs-safe-win32-x64-msvc": "0.11.0",
166
177
  "jszip": "^3.10.2"
167
178
  },
168
179
  "devDependencies": {