@openclaw/fs-safe 0.15.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 (311) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +36 -7
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +2 -8
  5. package/dist/advanced.d.ts +3 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +3 -0
  8. package/dist/archive-deadline.d.ts.map +1 -1
  9. package/dist/archive-deadline.js +22 -23
  10. package/dist/archive-kind.d.ts +0 -1
  11. package/dist/archive-kind.d.ts.map +1 -1
  12. package/dist/archive-kind.js +5 -17
  13. package/dist/archive-merge.d.ts +1 -0
  14. package/dist/archive-merge.d.ts.map +1 -1
  15. package/dist/archive-merge.js +4 -4
  16. package/dist/archive-native.d.ts +1 -0
  17. package/dist/archive-native.d.ts.map +1 -1
  18. package/dist/archive-native.js +1 -0
  19. package/dist/archive-options.d.ts +2 -0
  20. package/dist/archive-options.d.ts.map +1 -1
  21. package/dist/archive-parser.wasm +0 -0
  22. package/dist/archive-read.d.ts.map +1 -1
  23. package/dist/archive-read.js +6 -7
  24. package/dist/archive-tar-stream.d.ts +3 -0
  25. package/dist/archive-tar-stream.d.ts.map +1 -1
  26. package/dist/archive-tar-stream.js +56 -37
  27. package/dist/archive-tar-wasm.d.ts +16 -4
  28. package/dist/archive-tar-wasm.d.ts.map +1 -1
  29. package/dist/archive-tar-wasm.js +134 -34
  30. package/dist/archive-zip-count.d.ts.map +1 -1
  31. package/dist/archive-zip-count.js +21 -1
  32. package/dist/archive-zip-directory.d.ts.map +1 -1
  33. package/dist/archive-zip-directory.js +23 -1
  34. package/dist/archive-zip-loader.d.ts +2 -0
  35. package/dist/archive-zip-loader.d.ts.map +1 -1
  36. package/dist/archive-zip-loader.js +7 -0
  37. package/dist/archive-zip-names.d.ts.map +1 -1
  38. package/dist/archive-zip-names.js +7 -2
  39. package/dist/archive.d.ts.map +1 -1
  40. package/dist/archive.js +14 -9
  41. package/dist/byte-view.d.ts +3 -0
  42. package/dist/byte-view.d.ts.map +1 -0
  43. package/dist/byte-view.js +13 -0
  44. package/dist/clone-metadata.d.ts +1 -0
  45. package/dist/clone-metadata.d.ts.map +1 -1
  46. package/dist/clone-metadata.js +6 -2
  47. package/dist/create-directory.d.ts +20 -0
  48. package/dist/create-directory.d.ts.map +1 -0
  49. package/dist/create-directory.js +130 -0
  50. package/dist/create-file-async.d.ts +7 -0
  51. package/dist/create-file-async.d.ts.map +1 -0
  52. package/dist/create-file-async.js +121 -0
  53. package/dist/create-file.d.ts +8 -0
  54. package/dist/create-file.d.ts.map +1 -0
  55. package/dist/create-file.js +190 -0
  56. package/dist/create-owned-file.d.ts +8 -0
  57. package/dist/create-owned-file.d.ts.map +1 -0
  58. package/dist/create-owned-file.js +16 -0
  59. package/dist/create.d.ts +4 -0
  60. package/dist/create.d.ts.map +1 -0
  61. package/dist/create.js +2 -0
  62. package/dist/creation-darwin.d.ts +6 -0
  63. package/dist/creation-darwin.d.ts.map +1 -0
  64. package/dist/creation-darwin.js +70 -0
  65. package/dist/creation-file-state.d.ts +19 -0
  66. package/dist/creation-file-state.d.ts.map +1 -0
  67. package/dist/creation-file-state.js +118 -0
  68. package/dist/creation-path.d.ts +21 -0
  69. package/dist/creation-path.d.ts.map +1 -0
  70. package/dist/creation-path.js +71 -0
  71. package/dist/creation-permissions.d.ts +19 -0
  72. package/dist/creation-permissions.d.ts.map +1 -0
  73. package/dist/creation-permissions.js +125 -0
  74. package/dist/directory-durability.d.ts +7 -7
  75. package/dist/directory-durability.d.ts.map +1 -1
  76. package/dist/directory-durability.js +22 -80
  77. package/dist/directory-guard.d.ts +3 -0
  78. package/dist/directory-guard.d.ts.map +1 -1
  79. package/dist/directory-mode-node.d.ts +2 -0
  80. package/dist/directory-mode-node.d.ts.map +1 -1
  81. package/dist/directory-mode-node.js +8 -0
  82. package/dist/directory-receipt.d.ts +24 -0
  83. package/dist/directory-receipt.d.ts.map +1 -0
  84. package/dist/directory-receipt.js +123 -0
  85. package/dist/file-cleanup.d.ts +20 -0
  86. package/dist/file-cleanup.d.ts.map +1 -0
  87. package/dist/file-cleanup.js +81 -0
  88. package/dist/file-contents.d.ts +6 -0
  89. package/dist/file-contents.d.ts.map +1 -0
  90. package/dist/file-contents.js +40 -0
  91. package/dist/file-hash.d.ts.map +1 -1
  92. package/dist/file-hash.js +16 -4
  93. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -1
  94. package/dist/file-lock-sync-root-acquire.js +3 -0
  95. package/dist/file-lock-sync-root-held.d.ts +1 -2
  96. package/dist/file-lock-sync-root-held.d.ts.map +1 -1
  97. package/dist/file-lock-sync-root-held.js +7 -5
  98. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -1
  99. package/dist/file-lock-sync-stale-admission.js +3 -0
  100. package/dist/file-lock-sync.d.ts.map +1 -1
  101. package/dist/file-lock-sync.js +8 -11
  102. package/dist/file-observation.d.ts +1 -1
  103. package/dist/file-observation.d.ts.map +1 -1
  104. package/dist/file-store-boundary.d.ts +2 -6
  105. package/dist/file-store-boundary.d.ts.map +1 -1
  106. package/dist/file-store-boundary.js +3 -9
  107. package/dist/file-store-sync-write.d.ts.map +1 -1
  108. package/dist/file-store-sync-write.js +2 -5
  109. package/dist/file-store.js +3 -3
  110. package/dist/guarded-mkdir.d.ts +1 -0
  111. package/dist/guarded-mkdir.d.ts.map +1 -1
  112. package/dist/guarded-mkdir.js +27 -19
  113. package/dist/install-path.d.ts.map +1 -1
  114. package/dist/install-path.js +2 -5
  115. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  116. package/dist/json-durable-queue-ownership.js +2 -6
  117. package/dist/json-durable-queue-paths.d.ts.map +1 -1
  118. package/dist/json-durable-queue-paths.js +2 -24
  119. package/dist/json-durable-queue.d.ts.map +1 -1
  120. package/dist/json-durable-queue.js +10 -9
  121. package/dist/json.d.ts.map +1 -1
  122. package/dist/json.js +32 -75
  123. package/dist/local-roots.d.ts.map +1 -1
  124. package/dist/local-roots.js +19 -21
  125. package/dist/move-path-cleanup.d.ts +5 -19
  126. package/dist/move-path-cleanup.d.ts.map +1 -1
  127. package/dist/move-path-cleanup.js +57 -21
  128. package/dist/move-path.d.ts.map +1 -1
  129. package/dist/move-path.js +63 -40
  130. package/dist/native-binding.d.ts +11 -1
  131. package/dist/native-binding.d.ts.map +1 -1
  132. package/dist/native-fallback-warning.d.ts +4 -0
  133. package/dist/native-fallback-warning.d.ts.map +1 -0
  134. package/dist/native-fallback-warning.js +11 -0
  135. package/dist/native-operations.d.ts +0 -2
  136. package/dist/native-operations.d.ts.map +1 -1
  137. package/dist/native-operations.js +0 -24
  138. package/dist/native-parent-admission.d.ts +2 -0
  139. package/dist/native-parent-admission.d.ts.map +1 -1
  140. package/dist/native-parent-admission.js +3 -2
  141. package/dist/native-pinned-write-windows.d.ts +1 -1
  142. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  143. package/dist/native-pinned-write-windows.js +173 -28
  144. package/dist/native-pinned-write.d.ts.map +1 -1
  145. package/dist/native-pinned-write.js +19 -3
  146. package/dist/native-policy-parent-windows.d.ts.map +1 -1
  147. package/dist/native-policy-parent-windows.js +15 -6
  148. package/dist/native-staged-file.d.ts +5 -3
  149. package/dist/native-staged-file.d.ts.map +1 -1
  150. package/dist/native-staged-file.js +90 -40
  151. package/dist/native.js +2 -2
  152. package/dist/opened-realpath.d.ts.map +1 -1
  153. package/dist/opened-realpath.js +11 -2
  154. package/dist/owner-dacl.d.ts.map +1 -1
  155. package/dist/owner-dacl.js +10 -4
  156. package/dist/path.d.ts.map +1 -1
  157. package/dist/path.js +2 -1
  158. package/dist/permissions.d.ts.map +1 -1
  159. package/dist/permissions.js +3 -17
  160. package/dist/pinned-write-input.d.ts +4 -0
  161. package/dist/pinned-write-input.d.ts.map +1 -0
  162. package/dist/pinned-write-input.js +35 -0
  163. package/dist/pinned-write-mode.d.ts +5 -0
  164. package/dist/pinned-write-mode.d.ts.map +1 -0
  165. package/dist/pinned-write-mode.js +31 -0
  166. package/dist/pinned-write-staged.d.ts +6 -0
  167. package/dist/pinned-write-staged.d.ts.map +1 -0
  168. package/dist/pinned-write-staged.js +186 -0
  169. package/dist/pinned-write-types.d.ts +3 -0
  170. package/dist/pinned-write-types.d.ts.map +1 -1
  171. package/dist/pinned-write.d.ts.map +1 -1
  172. package/dist/pinned-write.js +41 -147
  173. package/dist/private-directory.d.ts.map +1 -1
  174. package/dist/private-directory.js +18 -4
  175. package/dist/private-producer-handoff-sync.d.ts +14 -0
  176. package/dist/private-producer-handoff-sync.d.ts.map +1 -0
  177. package/dist/private-producer-handoff-sync.js +114 -0
  178. package/dist/private-producer-handoff.d.ts +22 -4
  179. package/dist/private-producer-handoff.d.ts.map +1 -1
  180. package/dist/private-producer-handoff.js +140 -77
  181. package/dist/publish-copy-stage.d.ts +2 -1
  182. package/dist/publish-copy-stage.d.ts.map +1 -1
  183. package/dist/publish-copy-stage.js +16 -7
  184. package/dist/publish-file.d.ts +2 -2
  185. package/dist/publish-file.d.ts.map +1 -1
  186. package/dist/publish-file.js +58 -98
  187. package/dist/regular-file.d.ts.map +1 -1
  188. package/dist/regular-file.js +35 -44
  189. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  190. package/dist/replace-file-copy-fallback.js +28 -26
  191. package/dist/replace-file-copy-source.d.ts.map +1 -1
  192. package/dist/replace-file-copy-source.js +13 -22
  193. package/dist/replace-file-descriptor.d.ts.map +1 -1
  194. package/dist/replace-file-descriptor.js +10 -16
  195. package/dist/replace-file-temp-owner.d.ts +0 -7
  196. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  197. package/dist/replace-file-temp-owner.js +9 -60
  198. package/dist/replace-file.d.ts.map +1 -1
  199. package/dist/replace-file.js +9 -13
  200. package/dist/root-create-input.d.ts +2 -1
  201. package/dist/root-create-input.d.ts.map +1 -1
  202. package/dist/root-create-input.js +13 -4
  203. package/dist/root-directory-creation.d.ts +3 -3
  204. package/dist/root-directory-creation.d.ts.map +1 -1
  205. package/dist/root-directory-creation.js +15 -3
  206. package/dist/root-directory-list.d.ts.map +1 -1
  207. package/dist/root-directory-list.js +20 -3
  208. package/dist/root-file-final-admission.d.ts +1 -1
  209. package/dist/root-file-final-admission.d.ts.map +1 -1
  210. package/dist/root-file-final-admission.js +5 -2
  211. package/dist/root-file.d.ts.map +1 -1
  212. package/dist/root-file.js +3 -2
  213. package/dist/root-impl.d.ts.map +1 -1
  214. package/dist/root-impl.js +78 -27
  215. package/dist/root-move-noreplace.d.ts +2 -0
  216. package/dist/root-move-noreplace.d.ts.map +1 -1
  217. package/dist/root-move-noreplace.js +22 -13
  218. package/dist/root-options.d.ts +12 -4
  219. package/dist/root-options.d.ts.map +1 -1
  220. package/dist/root-path-stat.d.ts.map +1 -1
  221. package/dist/root-path-stat.js +59 -7
  222. package/dist/root-read-admission.d.ts.map +1 -1
  223. package/dist/root-read-admission.js +7 -2
  224. package/dist/root-remove.d.ts.map +1 -1
  225. package/dist/root-remove.js +15 -1
  226. package/dist/root-write-publication.js +1 -1
  227. package/dist/secret-file.d.ts.map +1 -1
  228. package/dist/secret-file.js +1 -0
  229. package/dist/secure-file-windows.d.ts +6 -0
  230. package/dist/secure-file-windows.d.ts.map +1 -1
  231. package/dist/secure-file-windows.js +34 -117
  232. package/dist/secure-file.js +2 -2
  233. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  234. package/dist/sidecar-lock-acquire.js +4 -6
  235. package/dist/sidecar-lock-handle.d.ts +3 -0
  236. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  237. package/dist/sidecar-lock-handle.js +6 -0
  238. package/dist/sidecar-lock-reclaim.d.ts +1 -1
  239. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  240. package/dist/sidecar-lock-reclaim.js +11 -8
  241. package/dist/sidecar-lock-root.d.ts.map +1 -1
  242. package/dist/sidecar-lock-root.js +2 -1
  243. package/dist/sidecar-lock.d.ts.map +1 -1
  244. package/dist/sidecar-lock.js +3 -5
  245. package/dist/staged-directory.d.ts +2 -2
  246. package/dist/staged-directory.d.ts.map +1 -1
  247. package/dist/staged-directory.js +6 -6
  248. package/dist/staged-file-settlement.d.ts +17 -0
  249. package/dist/staged-file-settlement.d.ts.map +1 -0
  250. package/dist/staged-file-settlement.js +57 -0
  251. package/dist/strict-file-identity.d.ts +1 -1
  252. package/dist/strict-file-identity.d.ts.map +1 -1
  253. package/dist/strict-file-identity.js +9 -9
  254. package/dist/symlink-parents.d.ts.map +1 -1
  255. package/dist/symlink-parents.js +2 -27
  256. package/dist/temp-workspace-owner.js +4 -4
  257. package/dist/unicode-path.d.ts.map +1 -1
  258. package/dist/unicode-path.js +3 -0
  259. package/dist/walk.d.ts.map +1 -1
  260. package/dist/walk.js +4 -2
  261. package/dist/windows-owner.d.ts.map +1 -1
  262. package/dist/windows-owner.js +2 -1
  263. package/dist/windows-security-bridge.cs +336 -0
  264. package/dist/windows-security-bridge.ps1 +15 -0
  265. package/dist/windows-security-command.d.ts +26 -0
  266. package/dist/windows-security-command.d.ts.map +1 -0
  267. package/dist/windows-security-command.js +363 -0
  268. package/dist/windows-security-facts.d.ts +6 -0
  269. package/dist/windows-security-facts.d.ts.map +1 -0
  270. package/dist/windows-security-facts.js +108 -0
  271. package/dist/write-file-handle.d.ts +7 -0
  272. package/dist/write-file-handle.d.ts.map +1 -1
  273. package/dist/write-file-handle.js +23 -0
  274. package/dist/write-open-flags.d.ts.map +1 -1
  275. package/dist/write-open-flags.js +1 -8
  276. package/dist/write-queue.d.ts.map +1 -1
  277. package/dist/write-queue.js +1 -4
  278. package/docs/advanced.md +71 -2
  279. package/docs/archive.md +102 -39
  280. package/docs/atomic.md +29 -5
  281. package/docs/config.md +6 -2
  282. package/docs/contributing.md +48 -4
  283. package/docs/copy.md +2 -0
  284. package/docs/creation.md +132 -0
  285. package/docs/durability.md +59 -0
  286. package/docs/file-contents.md +68 -0
  287. package/docs/install.md +31 -7
  288. package/docs/json.md +5 -4
  289. package/docs/local-roots.md +2 -0
  290. package/docs/migrating-to-0.5.md +15 -6
  291. package/docs/migrating-to-0.6.md +9 -4
  292. package/docs/mutation-policy-proof.md +5 -3
  293. package/docs/native-helper.md +22 -9
  294. package/docs/native.md +47 -15
  295. package/docs/path.md +4 -4
  296. package/docs/permissions.md +37 -14
  297. package/docs/public-api.md +5 -0
  298. package/docs/quickstart.md +1 -1
  299. package/docs/reading.md +2 -2
  300. package/docs/regular-file.md +3 -0
  301. package/docs/root.md +43 -0
  302. package/docs/secret-file.md +11 -2
  303. package/docs/secure-file.md +9 -4
  304. package/docs/sidecar-lock.md +14 -5
  305. package/docs/staged-file.md +9 -3
  306. package/docs/store.md +3 -1
  307. package/docs/temp.md +4 -1
  308. package/docs/types.md +18 -2
  309. package/docs/walk.md +7 -0
  310. package/docs/writing.md +80 -7
  311. package/package.json +18 -15
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. Each documented helper has its own page; everything else is reference-only and tracked here.
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
 
@@ -28,7 +28,8 @@ The exports group into a handful of themes. Each documented helper has its own p
28
28
  | `pathScope`, `PathScope`, `PathScopeOptions`, `PathScopeResolveOptions` | [path-scope.md](path-scope.md) | Absolute-path boundary helper with `Result`-shaped returns. |
29
29
  | `ensureDirectoryWithinRoot` | [path-scope.md](path-scope.md#ensuredir-rel-options) | Create a directory while enforcing the root boundary; same result contract as `pathScope().ensureDir()`. |
30
30
  | `resolvePathWithinRoot`, `resolvePathsWithinRoot` | – | Resolve one or many relative paths against a trusted root. |
31
- | `resolveExistingPathsWithinRoot`, `resolveStrictExistingPathsWithinRoot` | – | Same, but require the targets to exist. |
31
+ | `resolveExistingPathsWithinRoot` | – | Validate existing regular files inside the root, while allowing missing paths. |
32
+ | `resolveStrictExistingPathsWithinRoot` | – | Require every target to exist as a regular non-symlink file inside the root. |
32
33
  | `resolveWritablePathWithinRoot` | – | Resolve a write target inside a root. |
33
34
  | `resolveRootPath`, `resolveRootPathSync`, `ResolvedRootPath`, `ROOT_PATH_ALIAS_POLICIES`, `RootPathAliasPolicy` | – | Resolve a root directory honoring alias policy. |
34
35
  | `resolvePathViaExistingAncestorSync` | – | Walk to an existing ancestor for paths whose tail does not yet exist. |
@@ -68,8 +69,11 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
68
69
  | Export | Page | Notes |
69
70
  |---|---|---|
70
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
+ | `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. |
71
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. |
72
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. |
73
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. |
74
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. |
75
79
  | `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
@@ -149,6 +153,71 @@ component is followed by another segment, both helpers throw
149
153
  `FsSafeError("not-file")` before the platform can expose that state as POSIX
150
154
  `ENOTDIR` or Windows `ENOENT`.
151
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
+
152
221
  ### Local roots and file URLs
153
222
 
154
223
  | Export | Page | Notes |
package/docs/archive.md CHANGED
@@ -3,11 +3,27 @@
3
3
  `@openclaw/fs-safe/archive` extracts ZIP and TAR archives behind one API, with traversal checks, blocked-link-type rejection, and entry-count and byte budgets. When the native binding is available for the current platform, Rust streams ZIP, TAR, gzip, zstd, and bzip2 while TypeScript remains the sole policy owner; every accepted output is created fd-relative in a private staging root. Extraction then merges through the same safe-open boundary used by direct writes — a symlinked entry can't trick the merge into following an out-of-tree path.
4
4
 
5
5
  TAR admission uses one Rust core compiled into both the native binding and a
6
- bundled, import-free WebAssembly module. The guarded JavaScript fallback uses
7
- that module for TAR/gzip and optional `jszip` for ZIP. TAR needs no optional
8
- parser dependency, runtime download, install script, or consumer Rust toolchain.
9
- Installs omitting optional dependencies can import every public subpath and use
10
- TAR/gzip in `auto` or `off`; ZIP fallback still requires `jszip`.
6
+ bundled, import-free WebAssembly module. In `off`, or `auto` when the native
7
+ binding is unavailable, extraction and bounded entry reads use that module for
8
+ plain TAR, gzip, zstd, and bzip2. Zstd and bzip2 use bundled WASM builds of the
9
+ same codec implementations used by native; gzip uses Node's built-in decoder.
10
+ These TAR routes work with all optional dependencies omitted and need no
11
+ runtime interpreter, download, install script, or consumer compiler toolchain.
12
+ ZIP fallback still requires optional `jszip`.
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
+
22
+ `auto` prefers an available native binding; a native operation failure is
23
+ terminal and never retries through WASM. `require` rejects a missing binding
24
+ with `FsSafeError("helper-unavailable")`, including for zstd/bzip2 suffix
25
+ resolution. The separate `inspectTarArchive()` API still accepts only plain TAR
26
+ and gzip.
11
27
 
12
28
  ```ts
13
29
  import { extractArchive, resolveArchiveKind } from "@openclaw/fs-safe/archive";
@@ -23,6 +39,7 @@ await extractArchive({
23
39
  timeoutMs: 15_000, // hard budget; active destination mutation is joined
24
40
  stripComponents: 0, // tar-style strip-leading-dirs
25
41
  entryModes: "clamp", // default; use "preserve" for archive rwx bits
42
+ entryUmask: 0, // default; remove these bits from final modes
26
43
  entryFilter: ({ path, kind, size }) => "extract",
27
44
  onFiltered: "reject-archive", // default; opt into "skip-entry" explicitly
28
45
  limits: {
@@ -42,7 +59,7 @@ await extractArchive({
42
59
  type ExtractArchiveOptions = {
43
60
  archivePath: string; // absolute path to the archive
44
61
  destDir: string; // absolute destination directory; must already exist
45
- timeoutMs: number; // positive wall-clock budget; <= 0/non-finite disables it
62
+ timeoutMs: number; // positive elapsed-time budget; <= 0/non-finite disables it
46
63
  durable?: boolean; // false; opt into syncing published files and directories before completion
47
64
  kind?: ArchiveKind; // "zip" | "tar" | "tar-zstd" | "tar-bzip2"
48
65
  stripComponents?: number; // strip N leading dirs from entry paths
@@ -50,6 +67,7 @@ type ExtractArchiveOptions = {
50
67
  limits?: ArchiveExtractLimits;
51
68
  logger?: ArchiveLogger; // { info?, warn? }
52
69
  entryModes?: "clamp" | "preserve";
70
+ entryUmask?: number; // integer 0..0o777; defaults to 0
53
71
  entryFilter?: (entry: { path: string; kind: ArchiveEntryKind; size: number }) =>
54
72
  "extract" | "skip";
55
73
  onFiltered?: "reject-archive" | "skip-entry";
@@ -63,6 +81,12 @@ once, deepest first, and finally the destination directory. All work stays insid
63
81
  the extraction deadline; active syncs are joined before rejection. File sync
64
82
  failures use the same error surface as `Root.copyIn()`; directory I/O failures
65
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.
66
90
  Files whose final mode prevents reading, including `0o000` and write-only files,
67
91
  sync once through the copy's retained descriptor during publication. Permissions
68
92
  are never widened to reopen them. Directory modes are finalized after the file
@@ -95,6 +119,15 @@ including a mode containing only stripped special bits, stays zero under
95
119
  directories; ZIP UNIX creator records with zero attributes are explicit zero,
96
120
  while non-UNIX ZIP records use the absent-metadata defaults.
97
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
+
98
131
  TAR mode fields containing only NUL/ASCII-space padding use absent defaults.
99
132
  Both backends recognize GNU binary modes, including signed values,
100
133
  within JavaScript's safe-integer range before masking permission bits.
@@ -108,7 +141,7 @@ stay `0o600` and directories `0o700` until publication. Files receive their fina
108
141
  mode through the guarded copy's owned writer descriptor. Directories are pinned
109
142
  before descending and finalized after their children, including empty and
110
143
  restrictive directories. Explicit accepted directory modes win regardless of
111
- archive order; implicit parents receive `0o755`. Existing destination directories
144
+ archive order; implicit parents receive `0o755 & ~entryUmask`. Existing destination directories
112
145
  also receive the requested final mode. They are never temporarily widened to
113
146
  allow child writes; insufficient write/search access still rejects.
114
147
 
@@ -146,6 +179,13 @@ between native and JavaScript paths rather than reimplementing it in Rust.
146
179
 
147
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.
148
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
+
149
189
  ZIP admission establishes each entry's kind before callbacks: a high-word UNIX
150
190
  symlink type takes precedence regardless of creator, followed by the DOS directory
151
191
  bit, the exact UNIX directory type, or a terminal slash/backslash. Native manifests
@@ -177,6 +217,10 @@ decoded validation. Unicode Path admission is shared only when both the raw name
177
217
  and the complete Unicode fields match; different fields still verify their own
178
218
  CRC and interpretation. Shared backing memory is checked independently. Decoded
179
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.
180
224
 
181
225
  `stripComponents` removes leading nonempty, non-`.` path components after
182
226
  normalizing separators. For example, `./pkg/hello.txt` with
@@ -188,12 +232,15 @@ collision checks, writes, and mode application agree.
188
232
 
189
233
  An `entryFilter` sees the validated **canonical effective archive path before
190
234
  stripping**, entry kind, and declared size. On every JavaScript and native
191
- ZIP/TAR backend (including gzip and native zstd/bzip2), backslashes become `/`,
235
+ ZIP/TAR backend (including gzip, zstd, and bzip2), backslashes become `/`,
192
236
  empty and `.` components are removed, and trailing separators are removed from
193
237
  directory paths. For example, `./pkg//state\cache/value` is presented as
194
238
  `pkg/state/cache/value`, even with `stripComponents: 1`. Case and Unicode
195
239
  spelling are preserved. Local PAX `path`, GNU long-name, and supported ZIP
196
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.
197
244
 
198
245
  Raw paths undergo traversal, absolute/drive-path, and NUL validation **before**
199
246
  canonicalization; normalization cannot turn an unsafe path into an accepted
@@ -368,14 +415,16 @@ Native gzip, zstd, and bzip2 readers check cancellation before refilling
368
415
  compressed input and before each decoded read, including buffered output. These checks
369
416
  apply to file extraction and in-memory member reads; they cannot interrupt an
370
417
  already-running filesystem read or a decoder step using already-buffered input.
418
+ Portable zstd/bzip2 decoding checks cancellation between bounded codec steps and
419
+ periodically yields to the event loop, including while consuming output-free
420
+ members. An individual WASM call cannot be interrupted. Teardown joins the input,
421
+ parser, and any Node decoder streams before disposing their shared WASM state.
371
422
 
372
423
  ### Raw TAR framing
373
424
 
374
425
  Extraction and bounded reads admit the complete decoded TAR stream through the
375
- shared Rust core. This applies to plain TAR,
376
- gzip, and native-supported zstd/bzip2, without changing native-mode availability
377
- or fallback policy. The native and WASM builds enforce the same
378
- framing rules:
426
+ shared Rust core. This applies to plain TAR, gzip, zstd, and bzip2 on native and
427
+ fallback paths. The native and WASM builds enforce the same framing rules:
379
428
 
380
429
  - Every nonzero header must have a valid unsigned octal checksum, delimited
381
430
  within its field. Checksum validation precedes metadata allocation and member
@@ -420,14 +469,24 @@ returning selected bytes. Unrequested, filtered, and stripped members cannot
420
469
  bypass validation. Decompression remains streaming; no complete decoded archive
421
470
  is retained in memory or written to a decoded spool.
422
471
 
423
- The WASM transport has a fixed 64 KiB input buffer, one pending member event,
424
- and a 256 MiB maximum linear memory per isolated parser instance. JavaScript
425
- gzip decoding emits chunks of at most 64 KiB for both staged files and buffered
426
- inputs, matching that input window. Metadata is
427
- bounded before allocation; allocation failure rejects. Stream backpressure
428
- bounds queued chunks, and completion/error destroys the instance's parser
429
- state. The manifest retains the existing charged budget below; linear memory
430
- is an additional execution resource bound, not a new public limit option.
472
+ The WASM transport has fixed 64 KiB input/output windows, one pending member
473
+ event, and a 256 MiB maximum linear memory per isolated session. The parser and
474
+ portable zstd/bzip2 decoder share that session and memory ceiling. JavaScript
475
+ gzip decoding also emits chunks of at most 64 KiB for both staged files and
476
+ buffered inputs. Metadata is bounded before allocation; codec allocation
477
+ failure rejects. Stream backpressure bounds queued chunks, and completion or
478
+ error releases the session's parser and decoder state after stream teardown.
479
+ The manifest retains the existing charged budget below; linear memory is an
480
+ additional execution resource bound, not a new public limit option.
481
+
482
+ Portable zstd/bzip2 decoding consumes every concatenated member through physical
483
+ EOF and verifies container integrity, including available checksums. Zstd
484
+ skippable frames are consumed without becoming TAR data. Truncated members and
485
+ trailing non-container bytes reject with `ArchiveFormatError` before filters,
486
+ publication, or selected bytes are returned. Decoded TAR EOF and byte-budget
487
+ checks still apply across member boundaries; a second TAR after EOF is not
488
+ silently ignored. The gzip-only compressed-padding policy above does not extend
489
+ to zstd/bzip2 containers.
431
490
 
432
491
  The raw meter enforces `maxEntries` before consuming each logical member's body,
433
492
  including members later skipped by filtering or stripping. PAX/GNU metadata
@@ -524,7 +583,7 @@ parsers from disagreeing about a member's type.
524
583
  `K` validates encoding and NUL structure without authorizing link creation.
525
584
  Normal link/filter policy still governs the described member. Canonical
526
585
  pre-strip filter paths, decoded-stream ceilings, and physical EOF checks apply
527
- to plain/gzip TAR and native zstd/bzip2 alike.
586
+ to plain/gzip TAR and zstd/bzip2 alike.
528
587
 
529
588
  ## `inspectTarArchive`
530
589
 
@@ -590,7 +649,7 @@ import { resolveArchiveKind, type ArchiveKind } from "@openclaw/fs-safe/archive"
590
649
 
591
650
  const kind = resolveArchiveKind("upload.zip"); // "zip"
592
651
  const tar = resolveArchiveKind("upload.tar.gz"); // "tar"
593
- const zstd = resolveArchiveKind("upload.tar.zst"); // "tar-zstd" when native is available
652
+ const zstd = resolveArchiveKind("upload.tar.zst"); // "tar-zstd" in auto/off; require checks native
594
653
  const unknown = resolveArchiveKind("upload.bin"); // null
595
654
  ```
596
655
 
@@ -598,17 +657,18 @@ Recognizes:
598
657
 
599
658
  - `*.zip` → `"zip"`
600
659
  - `*.tar`, `*.tar.gz`, `*.tgz` → `"tar"`
601
- - `*.tar.zst`, `*.tar.zstd`, `*.tzst` → `"tar-zstd"` (native only)
602
- - `*.tar.bz2`, `*.tbz2`, `*.tbz` → `"tar-bzip2"` (native only)
660
+ - `*.tar.zst`, `*.tar.zstd`, `*.tzst` → `"tar-zstd"`
661
+ - `*.tar.bz2`, `*.tbz2`, `*.tbz` → `"tar-bzip2"`
603
662
 
604
663
  Returns `null` for unknown extensions; check the result before calling
605
- `extractArchive` if the filename is caller-controlled. A recognized zstd or
606
- bzip2 TAR extension with no native binding throws the typed
607
- `FsSafeError("helper-unavailable")` with installation guidance. This includes
608
- `mode: "off"`; those two formats have no JavaScript fallback.
664
+ `extractArchive` if the filename is caller-controlled. Recognized zstd and bzip2
665
+ TAR extensions resolve in `auto` and `off` even without a native binding, using
666
+ the bundled codecs for subsequent extraction or reads. Explicit `require`
667
+ still checks native availability during suffix resolution and throws
668
+ `FsSafeError("helper-unavailable")` when the binding cannot load.
609
669
 
610
- For a service whose input contract requires zstd, configure native mode before
611
- the first archive call so a packaging mistake fails at the boundary:
670
+ For a deployment that requires native archive processing, configure native mode
671
+ before the first archive call so a missing binding fails at the boundary:
612
672
 
613
673
  ```ts
614
674
  import { configureFsSafeNative } from "@openclaw/fs-safe/config";
@@ -627,8 +687,10 @@ await extractArchive({
627
687
 
628
688
  `readArchiveEntry(archivePath, entryPath, { maxBytes, kind? })` reads one
629
689
  regular-file entry into a bounded `Buffer` without extracting a tree. It reads
630
- the input through an identity-checked descriptor, rejects link, directory, and duplicate
631
- entries, verifies ZIP CRC and declared size,
690
+ the input through an identity-checked descriptor, rejects a requested link or
691
+ directory, and rejects duplicate entry names anywhere in the archive. Unrequested
692
+ links and directories do not prevent reading a regular file; no links are followed
693
+ or created. It verifies ZIP CRC and declared size,
632
694
  and throws `ArchiveLimitError` if the requested entry's output exceeds
633
695
  `maxBytes`. ZIP output within that cap must match the declared uncompressed
634
696
  size exactly; either a shorter or longer payload throws
@@ -641,7 +703,9 @@ plus the 768 MiB decoded ceiling derived from default extracted/archive byte
641
703
  limits. It does not apply payload budgets to unrequested members. ZIP
642
704
  inputs retain the archive subpath's 256 MiB compressed-input ceiling.
643
705
  With a native binding it uses the same Rust decoders as extraction, including
644
- zstd and bzip2 TAR. Without native it retains the JS ZIP/TAR/gzip implementation.
706
+ zstd and bzip2 TAR. Without native, the guarded fallback uses bundled WASM for
707
+ TAR admission and zstd/bzip2 decoding, Node gunzip for gzip, and optional JSZip
708
+ for ZIP. Native `require` still rejects an unavailable binding.
645
709
  Archive member reads retain their private in-memory input without a disk
646
710
  snapshot. JavaScript ZIP member reads reuse their completed physical admission
647
711
  when loading the decoder, which still checks its decoded names and entry count.
@@ -652,12 +716,11 @@ allocation without another copy where external buffers are supported.
652
716
  Native TAR retains the fully admitted member offsets alongside the same input
653
717
  allocation. Plain TAR copies only the selected payload range after full archive
654
718
  validation. Gzip, zstd, and bzip2 replay bounded decompression and still validate
655
- all framing, trailers, and physical padding before returning. The JavaScript
656
- TAR/gzip fallback copies each input window into WASM once, consuming member
657
- events at offsets within that window. After full admission, plain TAR copies the
658
- selected range directly from its private snapshot; gzip still replays bounded
659
- decompression through the parser. WASM transport and selected output still
660
- require copies.
719
+ all framing, trailers, and physical padding before returning. The fallback also
720
+ retains admitted member offsets. After full admission, plain TAR copies the
721
+ selected range directly from its private snapshot; gzip, zstd, and bzip2 replay
722
+ bounded decompression through the same parser. WASM transport and selected
723
+ output use owned copies, so reusable codec windows cannot escape to callers.
661
724
  Returned buffers own their bytes, so changing a result cannot modify an archive
662
725
  reader or retain an unrelated part of the input through its backing ArrayBuffer.
663
726
 
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
- Once such an operation failure reaches temp-owner settlement, atomic replacement
98
- preserves that value when cleanup and close succeed. With
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`. Directory manifests retain an exact bigint device/inode receipt from
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/config.md CHANGED
@@ -37,10 +37,14 @@ Set the process-global loading policy. Configure once at startup, before the fir
37
37
 
38
38
  | Mode | Behavior |
39
39
  |---|---|
40
- | `auto` | Default. Prefer the platform binding and use guarded JavaScript when it is unavailable. |
41
- | `off` | Do not load the binding; use guarded JavaScript deterministically. |
40
+ | `auto` | Default. Prefer the platform binding and use supported fallbacks when it is unavailable. |
41
+ | `off` | Do not load the binding; use supported fallbacks and reject native-only operations. |
42
42
  | `require` | Operations that need the binding raise `FsSafeError("helper-unavailable")` when it cannot load. |
43
43
 
44
+ Fallbacks include guarded JavaScript, the bundled TAR/gzip WASM parser, and the
45
+ [packaged Windows security scripts](install.md#windows-security-fallback).
46
+ Windows command fallbacks remain subject to normal system execution policy.
47
+
44
48
  ## `getFsSafeNativeConfig()`
45
49
 
46
50
  ```ts
@@ -19,10 +19,50 @@ manager version declared in `package.json`.
19
19
  pnpm build
20
20
  ```
21
21
 
22
- Runs TypeScript compilation and builds the portable Rust TAR parser for
23
- `wasm32-unknown-unknown`. Contributors need Rust (the native crate's declared
24
- minimum or newer) and `rustup target add wasm32-unknown-unknown`; Alpine's
25
- packaged toolchain uses `rust-wasm`. `pnpm archive:wasm` rebuilds just the parser.
22
+ Runs TypeScript compilation and builds the portable Rust TAR parser and its
23
+ bzip2/zstd codecs for `wasm32-unknown-unknown`. Contributors need Rust (the
24
+ native crate's declared minimum or newer), `rustup target add
25
+ wasm32-unknown-unknown`, and LLVM's WebAssembly-capable `clang` and `llvm-ar`.
26
+ The system's native `ar` is not sufficient. `pnpm archive:wasm` rebuilds just
27
+ the portable module.
28
+
29
+ Linux, macOS, and Windows CI use the same pinned WASI SDK 34 LLVM toolchain;
30
+ Alpine uses its versioned LLVM 22 packages alongside `rust-wasm`. For local
31
+ builds, install LLVM through your package manager or use the official
32
+ [WASI SDK](https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-34).
33
+ On macOS, `brew install llvm` supplies the archiver missing from Apple's
34
+ Command Line Tools. On Windows, install the LLVM distribution with both
35
+ `clang.exe` and `llvm-ar.exe`. On Linux, install the matching `clang` and
36
+ `llvm` packages; a GCC-only build toolchain cannot compile these WASM codecs.
37
+
38
+ The build discovers tools on `PATH`, in `LLVM_PATH/bin`, in Homebrew's LLVM
39
+ prefixes, and in Windows' standard LLVM installation. It also checks the
40
+ versioned `clang-18` through `clang-21` and `llvm-ar-18` through `llvm-ar-21`
41
+ executables. To select another installation explicitly, set
42
+ `CC_wasm32_unknown_unknown` and `AR_wasm32_unknown_unknown` to its compiler
43
+ and archiver. The corresponding hyphenated target variables and cc-rs's
44
+ `TARGET_CC`/`TARGET_AR` or `CC`/`AR` overrides are also respected; an unusable
45
+ explicit override fails with a builder diagnostic instead of being ignored.
46
+ Windows build environment names are case-insensitive, including when worker
47
+ processes uppercase them. The build normalizes only its copied child environment.
48
+ Clang's implicit configuration is disabled for this target so the WASI SDK's
49
+ default libc/sysroot cannot leak into the import-free module. These settings
50
+ affect compilation only and do not become runtime dependencies.
51
+
52
+ The build disables release LTO only in the WASM Cargo subprocess. An observed
53
+ Rust 1.98.1 optimized-WASM-LTO allocation/free failure makes that necessary;
54
+ the native release profile stays unchanged. The WASM linker strips debug
55
+ sections to keep the bundled module small without stripping native binaries.
56
+ The build verifies zero host
57
+ imports and one unshared 32-bit memory with the existing 256 MiB maximum
58
+ before copying the artifact. Allocator regression tests build a separate
59
+ instrumented module with `pnpm archive:wasm:allocator-tests` under the Cargo
60
+ target directory. That module is never copied to `dist/` or packaged. `pnpm
61
+ check` and coverage collection build it explicitly before testing. After a
62
+ fresh checkout, run that command before `pnpm test`, `pnpm test:coverage`, or
63
+ focused `test/archive-codec-wasm-allocator.test.ts` runs; the tests fail if
64
+ their prerequisite artifact is missing.
65
+
26
66
  The import-free asset lands at `dist/archive-parser.wasm`; source tests and
27
67
  compiled consumers both resolve that generated artifact. Run `pnpm build`
28
68
  before source tests in a fresh checkout. Do not commit `dist/` or built WASM.
@@ -117,6 +157,10 @@ pnpm archive:producer-smoke ./consumer require
117
157
  This uses a child bound to canonical cwd/device/inode running `/usr/bin/tar -czf - .`
118
158
  with unchanged stdout, and npm tar, on synthetic Unicode/newline/long-name files,
119
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.
120
164
  It also rejects a valid PAX override attached to an invalid raw UTF-8 field.
121
165
  The `require` command must resolve the freshly packed native binding; the
122
166
  `off` command uses the installed WASM asset. No live user files are read.
package/docs/copy.md CHANGED
@@ -78,6 +78,8 @@ XFS and ZFS preserve regular-file and directory modes, timestamps, extended attr
78
78
 
79
79
  `readCloneFileMetadata(files)` asynchronously reads APFS data-stream identities and file metadata in one native batch. Results correspond to input order; missing or unsupported entries return `undefined`. The returned `CloneFileMetadata` includes clone ID, device/inode, size, mode, ownership, and timestamps. These are point-in-time observations, not authorization or proof that later reads remain unchanged. Consumers such as Git index adapters must validate their own content and timestamp invariants. The reader does not follow leaf symbolic links.
80
80
 
81
+ All input paths must be absolute and valid before native availability is checked. On platforms other than macOS, `auto` and `off` can return one `undefined` per path without the addon, matching native's unsupported result. On macOS, `off` or an unavailable addon still rejects with `helper-unavailable`; JavaScript cannot supply APFS clone IDs. Explicit `require` mode rejects an unavailable addon on every platform, including for an empty batch. Errors from a loaded native helper remain terminal.
82
+
81
83
  ## Borrowed FileHandle transfers
82
84
 
83
85
  `copyFileHandle` from `@openclaw/fs-safe/advanced` copies bytes between two