@openclaw/fs-safe 0.14.0 → 0.16.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 (370) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +38 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +2 -1
  6. package/dist/archive-kind.d.ts +0 -1
  7. package/dist/archive-kind.d.ts.map +1 -1
  8. package/dist/archive-kind.js +5 -17
  9. package/dist/archive-merge.d.ts.map +1 -1
  10. package/dist/archive-merge.js +113 -46
  11. package/dist/archive-parser.wasm +0 -0
  12. package/dist/archive-read.d.ts.map +1 -1
  13. package/dist/archive-read.js +10 -11
  14. package/dist/archive-tar-stream.d.ts +3 -0
  15. package/dist/archive-tar-stream.d.ts.map +1 -1
  16. package/dist/archive-tar-stream.js +56 -37
  17. package/dist/archive-tar-wasm.d.ts +16 -4
  18. package/dist/archive-tar-wasm.d.ts.map +1 -1
  19. package/dist/archive-tar-wasm.js +134 -34
  20. package/dist/archive-zip-directory.d.ts +4 -0
  21. package/dist/archive-zip-directory.d.ts.map +1 -1
  22. package/dist/archive-zip-directory.js +2 -0
  23. package/dist/archive-zip-entry.d.ts +6 -2
  24. package/dist/archive-zip-entry.d.ts.map +1 -1
  25. package/dist/archive-zip-entry.js +23 -8
  26. package/dist/archive-zip-integrity.d.ts.map +1 -1
  27. package/dist/archive-zip-integrity.js +3 -4
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +107 -31
  30. package/dist/archive-zip-names.d.ts +1 -0
  31. package/dist/archive-zip-names.d.ts.map +1 -1
  32. package/dist/archive-zip-names.js +6 -0
  33. package/dist/archive.d.ts.map +1 -1
  34. package/dist/archive.js +11 -11
  35. package/dist/bounded-read-stream.d.ts +0 -1
  36. package/dist/bounded-read-stream.d.ts.map +1 -1
  37. package/dist/bounded-read-stream.js +0 -6
  38. package/dist/clone-metadata.d.ts +1 -0
  39. package/dist/clone-metadata.d.ts.map +1 -1
  40. package/dist/clone-metadata.js +6 -2
  41. package/dist/copy-publication.d.ts +6 -0
  42. package/dist/copy-publication.d.ts.map +1 -1
  43. package/dist/copy-publication.js +3 -0
  44. package/dist/copy-tree-portable.d.ts.map +1 -1
  45. package/dist/copy-tree-portable.js +44 -24
  46. package/dist/copy.d.ts.map +1 -1
  47. package/dist/copy.js +29 -11
  48. package/dist/create-directory.d.ts +20 -0
  49. package/dist/create-directory.d.ts.map +1 -0
  50. package/dist/create-directory.js +130 -0
  51. package/dist/create-file-async.d.ts +7 -0
  52. package/dist/create-file-async.d.ts.map +1 -0
  53. package/dist/create-file-async.js +121 -0
  54. package/dist/create-file.d.ts +8 -0
  55. package/dist/create-file.d.ts.map +1 -0
  56. package/dist/create-file.js +190 -0
  57. package/dist/create-owned-file.d.ts +8 -0
  58. package/dist/create-owned-file.d.ts.map +1 -0
  59. package/dist/create-owned-file.js +16 -0
  60. package/dist/create.d.ts +4 -0
  61. package/dist/create.d.ts.map +1 -0
  62. package/dist/create.js +2 -0
  63. package/dist/creation-darwin.d.ts +7 -0
  64. package/dist/creation-darwin.d.ts.map +1 -0
  65. package/dist/creation-darwin.js +79 -0
  66. package/dist/creation-file-state.d.ts +19 -0
  67. package/dist/creation-file-state.d.ts.map +1 -0
  68. package/dist/creation-file-state.js +118 -0
  69. package/dist/creation-path.d.ts +21 -0
  70. package/dist/creation-path.d.ts.map +1 -0
  71. package/dist/creation-path.js +71 -0
  72. package/dist/creation-permissions.d.ts +19 -0
  73. package/dist/creation-permissions.d.ts.map +1 -0
  74. package/dist/creation-permissions.js +125 -0
  75. package/dist/directory-durability.d.ts +1 -1
  76. package/dist/directory-durability.d.ts.map +1 -1
  77. package/dist/directory-durability.js +22 -80
  78. package/dist/directory-guard.d.ts +3 -0
  79. package/dist/directory-guard.d.ts.map +1 -1
  80. package/dist/directory-mode-node.d.ts +2 -0
  81. package/dist/directory-mode-node.d.ts.map +1 -1
  82. package/dist/directory-mode-node.js +8 -0
  83. package/dist/directory-mode-owner.js +5 -5
  84. package/dist/directory-receipt.d.ts +24 -0
  85. package/dist/directory-receipt.d.ts.map +1 -0
  86. package/dist/directory-receipt.js +127 -0
  87. package/dist/file-cleanup.d.ts +19 -0
  88. package/dist/file-cleanup.d.ts.map +1 -0
  89. package/dist/file-cleanup.js +78 -0
  90. package/dist/file-handle-transfer.d.ts +2 -0
  91. package/dist/file-handle-transfer.d.ts.map +1 -1
  92. package/dist/file-handle-transfer.js +57 -2
  93. package/dist/file-identity.d.ts.map +1 -1
  94. package/dist/file-identity.js +18 -4
  95. package/dist/file-lock-sync-admission.d.ts +19 -0
  96. package/dist/file-lock-sync-admission.d.ts.map +1 -0
  97. package/dist/file-lock-sync-admission.js +93 -0
  98. package/dist/file-lock-sync-root-acquire.d.ts +4 -0
  99. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
  100. package/dist/file-lock-sync-root-acquire.js +370 -0
  101. package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
  102. package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
  103. package/dist/file-lock-sync-root-arbitration.js +66 -0
  104. package/dist/file-lock-sync-root-held.d.ts +34 -0
  105. package/dist/file-lock-sync-root-held.d.ts.map +1 -0
  106. package/dist/file-lock-sync-root-held.js +393 -0
  107. package/dist/file-lock-sync-root-io.d.ts +44 -0
  108. package/dist/file-lock-sync-root-io.d.ts.map +1 -0
  109. package/dist/file-lock-sync-root-io.js +209 -0
  110. package/dist/file-lock-sync-root-mutation.d.ts +17 -0
  111. package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
  112. package/dist/file-lock-sync-root-mutation.js +277 -0
  113. package/dist/file-lock-sync-root-options.d.ts +20 -0
  114. package/dist/file-lock-sync-root-options.d.ts.map +1 -0
  115. package/dist/file-lock-sync-root-options.js +58 -0
  116. package/dist/file-lock-sync-root-registration.d.ts +2 -0
  117. package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
  118. package/dist/file-lock-sync-root-registration.js +90 -0
  119. package/dist/file-lock-sync-root.d.ts +36 -0
  120. package/dist/file-lock-sync-root.d.ts.map +1 -0
  121. package/dist/file-lock-sync-root.js +361 -0
  122. package/dist/file-lock-sync-stale-admission.d.ts +24 -0
  123. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
  124. package/dist/file-lock-sync-stale-admission.js +205 -0
  125. package/dist/file-lock-sync.d.ts.map +1 -1
  126. package/dist/file-lock-sync.js +245 -205
  127. package/dist/file-observation.d.ts +1 -1
  128. package/dist/file-observation.d.ts.map +1 -1
  129. package/dist/file-store-boundary.d.ts +2 -6
  130. package/dist/file-store-boundary.d.ts.map +1 -1
  131. package/dist/file-store-boundary.js +3 -9
  132. package/dist/file-store-prune.d.ts.map +1 -1
  133. package/dist/file-store-prune.js +5 -1
  134. package/dist/file-store-sync-write.d.ts.map +1 -1
  135. package/dist/file-store-sync-write.js +5 -8
  136. package/dist/file-store.d.ts.map +1 -1
  137. package/dist/file-store.js +47 -12
  138. package/dist/guarded-mkdir.d.ts +1 -0
  139. package/dist/guarded-mkdir.d.ts.map +1 -1
  140. package/dist/guarded-mkdir.js +36 -7
  141. package/dist/json-document-store.d.ts.map +1 -1
  142. package/dist/json-document-store.js +22 -15
  143. package/dist/json-durable-queue-ownership.d.ts +0 -1
  144. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  145. package/dist/json-durable-queue-ownership.js +0 -6
  146. package/dist/move-path.js +1 -1
  147. package/dist/native-binding.d.ts +13 -1
  148. package/dist/native-binding.d.ts.map +1 -1
  149. package/dist/native-fallback-warning.d.ts +4 -0
  150. package/dist/native-fallback-warning.d.ts.map +1 -0
  151. package/dist/native-fallback-warning.js +11 -0
  152. package/dist/native-operations.d.ts +0 -2
  153. package/dist/native-operations.d.ts.map +1 -1
  154. package/dist/native-operations.js +0 -24
  155. package/dist/native-parent-admission.d.ts +5 -2
  156. package/dist/native-parent-admission.d.ts.map +1 -1
  157. package/dist/native-parent-admission.js +27 -7
  158. package/dist/native-pinned-write-windows.d.ts +1 -1
  159. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  160. package/dist/native-pinned-write-windows.js +174 -29
  161. package/dist/native-pinned-write.d.ts.map +1 -1
  162. package/dist/native-pinned-write.js +26 -14
  163. package/dist/native-policy-parent-windows.d.ts +14 -0
  164. package/dist/native-policy-parent-windows.d.ts.map +1 -0
  165. package/dist/native-policy-parent-windows.js +209 -0
  166. package/dist/native-rename-outcome.d.ts +4 -0
  167. package/dist/native-rename-outcome.d.ts.map +1 -0
  168. package/dist/native-rename-outcome.js +8 -0
  169. package/dist/native-staged-file.d.ts +3 -2
  170. package/dist/native-staged-file.d.ts.map +1 -1
  171. package/dist/native-staged-file.js +121 -72
  172. package/dist/output.d.ts.map +1 -1
  173. package/dist/output.js +12 -8
  174. package/dist/owner-dacl.d.ts.map +1 -1
  175. package/dist/owner-dacl.js +10 -4
  176. package/dist/path-prefix.d.ts.map +1 -1
  177. package/dist/path-prefix.js +30 -8
  178. package/dist/path-suffix-aliases.d.ts +2 -0
  179. package/dist/path-suffix-aliases.d.ts.map +1 -1
  180. package/dist/path-suffix-aliases.js +25 -17
  181. package/dist/permission-exec.d.ts +2 -0
  182. package/dist/permission-exec.d.ts.map +1 -1
  183. package/dist/permission-exec.js +150 -21
  184. package/dist/permissions-windows.js +1 -1
  185. package/dist/pinned-mutation-admission.d.ts.map +1 -1
  186. package/dist/pinned-mutation-admission.js +10 -5
  187. package/dist/pinned-mutation-observation.d.ts +0 -1
  188. package/dist/pinned-mutation-observation.d.ts.map +1 -1
  189. package/dist/pinned-mutation-observation.js +0 -19
  190. package/dist/pinned-mutation-shared-route.d.ts +1 -0
  191. package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
  192. package/dist/pinned-mutation-shared-route.js +1 -1
  193. package/dist/pinned-write-input.d.ts +4 -0
  194. package/dist/pinned-write-input.d.ts.map +1 -0
  195. package/dist/pinned-write-input.js +25 -0
  196. package/dist/pinned-write-mode.d.ts +5 -0
  197. package/dist/pinned-write-mode.d.ts.map +1 -0
  198. package/dist/pinned-write-mode.js +24 -0
  199. package/dist/pinned-write-staged.d.ts +6 -0
  200. package/dist/pinned-write-staged.d.ts.map +1 -0
  201. package/dist/pinned-write-staged.js +187 -0
  202. package/dist/pinned-write-types.d.ts +5 -0
  203. package/dist/pinned-write-types.d.ts.map +1 -1
  204. package/dist/pinned-write.d.ts.map +1 -1
  205. package/dist/pinned-write.js +35 -145
  206. package/dist/private-directory.d.ts.map +1 -1
  207. package/dist/private-directory.js +18 -4
  208. package/dist/private-producer-handoff-sync.d.ts +14 -0
  209. package/dist/private-producer-handoff-sync.d.ts.map +1 -0
  210. package/dist/private-producer-handoff-sync.js +114 -0
  211. package/dist/private-producer-handoff.d.ts +22 -4
  212. package/dist/private-producer-handoff.d.ts.map +1 -1
  213. package/dist/private-producer-handoff.js +140 -77
  214. package/dist/private-temp-workspace.d.ts.map +1 -1
  215. package/dist/private-temp-workspace.js +75 -121
  216. package/dist/publish-copy-stage.d.ts +2 -1
  217. package/dist/publish-copy-stage.d.ts.map +1 -1
  218. package/dist/publish-copy-stage.js +16 -7
  219. package/dist/publish-file.d.ts.map +1 -1
  220. package/dist/publish-file.js +2 -2
  221. package/dist/regular-file.d.ts.map +1 -1
  222. package/dist/regular-file.js +1 -15
  223. package/dist/replace-directory.d.ts.map +1 -1
  224. package/dist/replace-directory.js +256 -18
  225. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  226. package/dist/replace-file-copy-fallback.js +62 -70
  227. package/dist/replace-file-copy-source.d.ts.map +1 -1
  228. package/dist/replace-file-copy-source.js +10 -12
  229. package/dist/replace-file-temp-owner.d.ts +5 -9
  230. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  231. package/dist/replace-file-temp-owner.js +56 -72
  232. package/dist/replace-file.js +6 -6
  233. package/dist/retained-directory-replacement.d.ts +26 -0
  234. package/dist/retained-directory-replacement.d.ts.map +1 -0
  235. package/dist/retained-directory-replacement.js +193 -0
  236. package/dist/root-boundary.d.ts +1 -0
  237. package/dist/root-boundary.d.ts.map +1 -1
  238. package/dist/root-boundary.js +4 -0
  239. package/dist/root-context.d.ts +0 -8
  240. package/dist/root-context.d.ts.map +1 -1
  241. package/dist/root-context.js +0 -3
  242. package/dist/root-create-input.d.ts +8 -1
  243. package/dist/root-create-input.d.ts.map +1 -1
  244. package/dist/root-create-input.js +17 -4
  245. package/dist/root-directory-creation.d.ts +3 -3
  246. package/dist/root-directory-creation.d.ts.map +1 -1
  247. package/dist/root-directory-creation.js +15 -3
  248. package/dist/root-directory-list.d.ts +1 -0
  249. package/dist/root-directory-list.d.ts.map +1 -1
  250. package/dist/root-directory-list.js +1 -0
  251. package/dist/root-impl.d.ts.map +1 -1
  252. package/dist/root-impl.js +46 -17
  253. package/dist/root-move-noreplace.d.ts.map +1 -1
  254. package/dist/root-move-noreplace.js +24 -15
  255. package/dist/root-options.d.ts +12 -4
  256. package/dist/root-options.d.ts.map +1 -1
  257. package/dist/root-path-errors.d.ts +1 -0
  258. package/dist/root-path-errors.d.ts.map +1 -1
  259. package/dist/root-path-errors.js +11 -2
  260. package/dist/root-path-existing.d.ts.map +1 -1
  261. package/dist/root-path-existing.js +11 -35
  262. package/dist/root-path-stat.d.ts.map +1 -1
  263. package/dist/root-path-stat.js +59 -7
  264. package/dist/root-path.js +1 -13
  265. package/dist/root-remove.d.ts +1 -0
  266. package/dist/root-remove.d.ts.map +1 -1
  267. package/dist/root-remove.js +4 -0
  268. package/dist/root-walk.d.ts +1 -1
  269. package/dist/root-walk.d.ts.map +1 -1
  270. package/dist/root-walk.js +17 -2
  271. package/dist/root-write-admission.d.ts +0 -2
  272. package/dist/root-write-admission.d.ts.map +1 -1
  273. package/dist/root-write-admission.js +1 -15
  274. package/dist/root-write-complete-parent.d.ts.map +1 -1
  275. package/dist/root-write-complete-parent.js +7 -23
  276. package/dist/root-write-publication.js +1 -1
  277. package/dist/root-write-verification.d.ts.map +1 -1
  278. package/dist/root-write-verification.js +29 -42
  279. package/dist/secret-file.d.ts.map +1 -1
  280. package/dist/secret-file.js +3 -24
  281. package/dist/secret-read-async.d.ts.map +1 -1
  282. package/dist/secret-read-async.js +3 -24
  283. package/dist/secret-read-policy.d.ts +6 -2
  284. package/dist/secret-read-policy.d.ts.map +1 -1
  285. package/dist/secret-read-policy.js +26 -2
  286. package/dist/secure-file-windows.d.ts +6 -0
  287. package/dist/secure-file-windows.d.ts.map +1 -1
  288. package/dist/secure-file-windows.js +34 -117
  289. package/dist/secure-file.js +2 -2
  290. package/dist/sibling-temp.d.ts.map +1 -1
  291. package/dist/sibling-temp.js +23 -12
  292. package/dist/sidecar-lock-acquire.d.ts +2 -28
  293. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  294. package/dist/sidecar-lock-acquire.js +288 -199
  295. package/dist/sidecar-lock-admission-context.d.ts +19 -0
  296. package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
  297. package/dist/sidecar-lock-admission-context.js +60 -0
  298. package/dist/sidecar-lock-admission-parser.d.ts +43 -0
  299. package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
  300. package/dist/sidecar-lock-admission-parser.js +113 -0
  301. package/dist/sidecar-lock-admission.d.ts +35 -0
  302. package/dist/sidecar-lock-admission.d.ts.map +1 -0
  303. package/dist/sidecar-lock-admission.js +7 -0
  304. package/dist/sidecar-lock-reclaim.d.ts +9 -4
  305. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  306. package/dist/sidecar-lock-reclaim.js +80 -25
  307. package/dist/sidecar-lock-root.d.ts.map +1 -1
  308. package/dist/sidecar-lock-root.js +2 -1
  309. package/dist/sidecar-lock-stale-admission.d.ts +39 -0
  310. package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
  311. package/dist/sidecar-lock-stale-admission.js +232 -0
  312. package/dist/sidecar-lock-target.d.ts +8 -0
  313. package/dist/sidecar-lock-target.d.ts.map +1 -0
  314. package/dist/sidecar-lock-target.js +55 -0
  315. package/dist/sidecar-lock.d.ts.map +1 -1
  316. package/dist/sidecar-lock.js +100 -16
  317. package/dist/staged-directory.d.ts.map +1 -1
  318. package/dist/staged-directory.js +6 -6
  319. package/dist/staged-file-settlement.d.ts +17 -0
  320. package/dist/staged-file-settlement.d.ts.map +1 -0
  321. package/dist/staged-file-settlement.js +57 -0
  322. package/dist/temp-workspace-descriptor.d.ts.map +1 -1
  323. package/dist/temp-workspace-descriptor.js +9 -27
  324. package/dist/temp-workspace-owner.d.ts.map +1 -1
  325. package/dist/temp-workspace-owner.js +8 -8
  326. package/dist/walk.d.ts +5 -1
  327. package/dist/walk.d.ts.map +1 -1
  328. package/dist/walk.js +19 -6
  329. package/dist/windows-owner.d.ts.map +1 -1
  330. package/dist/windows-owner.js +4 -3
  331. package/dist/windows-security-bridge.cs +336 -0
  332. package/dist/windows-security-bridge.ps1 +15 -0
  333. package/dist/windows-security-command.d.ts +26 -0
  334. package/dist/windows-security-command.d.ts.map +1 -0
  335. package/dist/windows-security-command.js +363 -0
  336. package/dist/windows-security-facts.d.ts +6 -0
  337. package/dist/windows-security-facts.d.ts.map +1 -0
  338. package/dist/windows-security-facts.js +108 -0
  339. package/docs/advanced.md +4 -2
  340. package/docs/archive.md +97 -46
  341. package/docs/atomic.md +85 -8
  342. package/docs/config.md +6 -2
  343. package/docs/contributing.md +44 -4
  344. package/docs/copy.md +37 -0
  345. package/docs/creation.md +128 -0
  346. package/docs/durability.md +24 -0
  347. package/docs/file-store.md +19 -0
  348. package/docs/install.md +31 -7
  349. package/docs/json-store.md +5 -0
  350. package/docs/migrating-to-0.5.md +15 -6
  351. package/docs/migrating-to-0.6.md +9 -4
  352. package/docs/native-helper.md +32 -12
  353. package/docs/native.md +38 -7
  354. package/docs/output.md +6 -0
  355. package/docs/path-prefix.md +10 -0
  356. package/docs/path-suffix-aliases.md +51 -6
  357. package/docs/permissions.md +50 -14
  358. package/docs/public-api.md +3 -2
  359. package/docs/root.md +56 -3
  360. package/docs/secret-file.md +11 -2
  361. package/docs/secure-file.md +9 -4
  362. package/docs/sidecar-lock.md +114 -8
  363. package/docs/staged-file.md +12 -3
  364. package/docs/temp.md +20 -3
  365. package/docs/walk.md +67 -1
  366. package/docs/writing.md +76 -6
  367. package/package.json +19 -16
  368. package/dist/darwin-acl.d.ts +0 -4
  369. package/dist/darwin-acl.d.ts.map +0 -1
  370. package/dist/darwin-acl.js +0 -24
package/CHANGELOG.md CHANGED
@@ -1,5 +1,93 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.16.0 - 2026-09-19
6
+
7
+ ### Highlights
8
+
9
+ - **Private files and directories through existing APIs:** add `private: true` to Root `mkdir()`, `ensureRoot()`, `create()`, and `createJson()`, with verified owner-only permissions and creation-time Windows ACL protection. New `/advanced` helpers `createDirectory()`, `createDirectorySync()`, and `createFileSync()` create one exclusive entry under an existing trusted parent; the file helper returns an owned, disposable descriptor. ([#487](https://github.com/openclaw/fs-safe/pull/487), [#482](https://github.com/openclaw/fs-safe/issues/482))
10
+ - **Safer secret and private writes:** verify the actual staging permissions before writing content, rejecting filesystems that accept permission changes without enforcing the required private mode. ([#491](https://github.com/openclaw/fs-safe/pull/491), [#493](https://github.com/openclaw/fs-safe/pull/493))
11
+ - **Atomic buffered file creation:** opt into `atomic: true` on `Root.create()` or `createJson()` to keep the destination absent until its content is ready, including without the native addon. Independently select `durable: "file"` when file-flush failures must propagate. Existing defaults remain unchanged. ([#486](https://github.com/openclaw/fs-safe/pull/486))
12
+ - **Compressed TAR without native installation:** extract and read bounded entries from zstd and bzip2 TAR archives through bundled WASM codecs, even with all optional dependencies omitted. The fallback retains the shared Rust TAR parser, full-stream validation, byte limits, and guarded publication, with no runtime interpreter or download. ([#488](https://github.com/openclaw/fs-safe/pull/488))
13
+ - **Windows security operations without the addon:** use packaged, readable PowerShell/C# helpers for raw owner/DACL inspection, private-directory creation, and secure-file reads in native `auto` or `off` mode when capabilities are unavailable. Private permissions apply at creation, and secure reads inspect the same open handle that supplies the bytes. ([#476](https://github.com/openclaw/fs-safe/pull/476))
14
+
15
+ ### Compatibility and upgrade notes
16
+
17
+ - Private creation on macOS requires the matching native helper's ACL-inspection capability; disabled, missing, or older helpers reject before creating parents or stages. Relevant inheritable parent ACLs reject creation, while noninheriting ACLs remain allowed. Existing entries are never repaired or made less restrictive. Nonprivate creation and native-free secret-file writes retain their behavior; see the [creation contract](https://github.com/openclaw/fs-safe/blob/v0.16.0/docs/creation.md) for supported modes and platform limits.
18
+ - Native-free `atomic: true` creation requires hardlinks and rejects unsupported filesystems without publishing partial content. Private Windows file publication also requires hardlinks on the same local filesystem. Streamed creates continue to stage complete content; atomic visibility does not strengthen pathname containment or guarantee crash durability.
19
+ - `durable: "file"` applies to buffered, streamed, and JSON creates and propagates file-sync errors, including `EPERM`. Parent-directory synchronization remains best-effort, independently of the selected publication strategy.
20
+ - Windows fallbacks require system PowerShell and permission to run the packaged scripts and `.NET Add-Type` under normal system policy. They emit a path-free `FS_SAFE_NATIVE_FALLBACK` warning once per capability per process and add startup and compilation overhead; disallowed execution fails closed. No system policy is bypassed.
21
+ - Native `require` remains strict, and failures from available native operations never trigger a fallback retry. No-clobber `Root.move()` still requires native support. ZIP fallback still needs optional `jszip`, and `inspectTarArchive()` continues to accept only plain TAR and gzip.
22
+ - Atomic, streamed, and private creation can report cleanup, close, mode, or synchronization errors after a complete file has been published. Preserve the publication and cleanup receipts when handling failures; completed destinations remain in place, and indeterminate outcomes retain names for recovery.
23
+
24
+ ### Security and correctness
25
+
26
+ - Verify `0600` before secret-file payload writes in native and JavaScript writers. Verify actual native private-file ownership and permissions before payload writes and at publication, rejecting filesystems that accept permission changes without enforcing them; retain explicit final modes and public staging's `0600` guarantees. ([#491](https://github.com/openclaw/fs-safe/pull/491), [#493](https://github.com/openclaw/fs-safe/pull/493))
27
+ - Compare exact directory identities before and after durability syncs, retry unknown Windows observations once, and reject rounded or unverifiable caller receipts. Bind numeric metadata to the same observation and preserve private identity authority across receipt mutation, durable creation, publication, and retained staging. ([#483](https://github.com/openclaw/fs-safe/pull/483))
28
+ - Retain streamed `Root.create()` authority callbacks and abort signals across producer waits so replacing caller options cannot detach a revoked lease or redirect cancellation; preserve callback receivers and owned-stage cleanup. ([#490](https://github.com/openclaw/fs-safe/pull/490))
29
+ - Recheck retained parent and staging identities after final publication authority callbacks on JavaScript and native writers, preserving substituted entries before their bytes can be published. Flush native Windows creation through the retained writable descriptor and preserve published destinations after finalization failures.
30
+ - Snapshot `Root.move()` mutation policy before asynchronous admission so changes to caller-owned deny paths or prefixes cannot change an in-flight move. Recheck native no-clobber source identity, type, and hardlink policy after the final authority callback; live revocation remains available through `assertBeforeMutation`.
31
+ - Keep asynchronous Root-backed file locks waiting through successive owner handoffs instead of failing with `path-mismatch`, while retaining identity checks and requiring a fresh exclusive acquisition.
32
+ - Return one unsupported clone-metadata result per input without the native addon on non-macOS platforms in `auto` and `off` modes. Preserve input validation, strict `require` mode, and native-only APFS metadata on macOS. ([#489](https://github.com/openclaw/fs-safe/pull/489))
33
+
34
+ ### Diagnostics and maintenance
35
+
36
+ - Address Windows security commands by their built-in module names and restrict discovery to PowerShell's bundled system modules, avoiding broad discovery scans on helper startup. ([#484](https://github.com/openclaw/fs-safe/pull/484))
37
+ - Clarify that `resolveExistingPathsWithinRoot()` permits missing paths while `resolveStrictExistingPathsWithinRoot()` requires existing regular files, and distinguish native beneath mechanisms from the best-effort containment reported by public Root open, read, and writable-open results. ([#485](https://github.com/openclaw/fs-safe/pull/485))
38
+ - Honor case-insensitive Windows build environment names so configured WASM compilers and archivers remain selected in worker processes; preserve child-only compiler flags without duplicate case variants. ([#492](https://github.com/openclaw/fs-safe/pull/492))
39
+ - Refresh JavaScript and Rust dependencies, pnpm, CodeQL, and archive/release build toolchains, including current NAPI interoperability fixes; retain Node 22 support and Rust 1.88 compatibility. Includes the Dependabot updates in [#481](https://github.com/openclaw/fs-safe/pull/481).
40
+
41
+ ## 0.15.0 - 2026-09-18
42
+
43
+ ### Highlights
44
+
45
+ - Enforce Root authority throughout synchronous lock admission, stale recovery, and cleanup; keep asynchronous reclaim guards inside the Root capability.
46
+ - Preserve directory and archive-source identity across publication, callbacks, and recovery, and reject corrupt portable ZIP payloads and metadata.
47
+ - Support asynchronous walk filters, configurable-depth suffix probing, and bounded synchronous descriptor copying.
48
+
49
+ ### Compatibility and upgrade notes
50
+
51
+ - Directory replacement now requires compatible native identity-fenced, no-replace support, including when the destination is absent. Disabled, missing, or older helpers reject before target-parent creation.
52
+ - Treat ordinary post-dispatch native rename errors as indeterminate. Preserve staged files and directory backups when a rename may have committed before its reply was lost; automatic retry and rollback require explicit evidence that dispatch never occurred.
53
+ - Synchronous Root locks require a genuine registered Root handle and enforce its policies. Interrupted asynchronous Root reclaim guards can remain after process exit or reset; recover them only after an application-owned liveness check proves the attempt ended.
54
+ - Root lock authority checks add measurable overhead. Removing redundant observations reduces some cost, but these paths remain slower than the earlier implementation that did not enforce the same boundary.
55
+
56
+ ### Security and correctness
57
+
58
+ - Serialize sidecar admission across raw and Root-based synchronous callers, retain separate cleanup authority, and recheck asynchronous authorization at the final stale-removal boundary.
59
+ - Keep asynchronous Root reclaim-guard creation, token/byte ownership checks, and removal inside the capability. Preserve unsettled guards instead of following replaced parents through raw cleanup.
60
+ - Create native Root lock records through retained-parent exclusive opens so racing contenders retry normally. Ordinary Root writes retain private staging. Treat raw reclaim guards disappearing or changing during collision inspection as contention.
61
+ - Verify reclaim-guard ownership after the final sidecar snapshot and parser, preserving mutation authorization and parser failures. Evaluate stale age after snapshot parsing and reject cleanup when exact identity is unknown.
62
+ - Retain source-root and child-directory authority during archive merges and bind file identity and mode to the admitted copy descriptor, including exact large Windows identities.
63
+ - Preserve ZIP payloads and physical entry metadata during portable loading so extraction rejects corrupt empty content and honors admitted directory, symlink, and special-file kinds without reordering callbacks.
64
+ - Snapshot FileStore policies before asynchronous work, streams, source reads, and JSON serialization; snapshot JsonStore durability/newline options before queued mutations and updater callbacks.
65
+ - Snapshot external-output and sibling-temp options before asynchronous setup while preserving callback receivers.
66
+ - Enforce Windows native parent mutation policies before creating each missing component and retain originating native descriptor ownership through cleanup.
67
+ - Preserve files and symlinks replacing an observed empty FileStore directory, and leave directories that become nonempty untouched.
68
+ - Recheck child entries before walk descent so symlink replacements during filters honor the selected policy.
69
+ - Keep Windows owner/ACL diagnostics bounded and fail-closed for hostile thrown values. Preserve ordinary diagnostics and the original cause without invoking getters, proxy traps, coercion, or altered buffer properties.
70
+
71
+ ### APIs and failure handling
72
+
73
+ - Allow asynchronous `include`/`descend` callbacks in `walkDirectory()` and `entryFilter` in `Root.walk()`, retaining serial selection, callback receivers, budgets, cancellation, and post-callback identity checks.
74
+ - Add `maxDepth` to `probePathSuffixAliasesSync()` with proportional budgets, a 32,768-forward-observation ceiling, unchanged defaults and string limits, and complete owned cleanup.
75
+ - Add `copyFileDescriptorSync()` for bounded positional transfers between caller-owned regular-file descriptors, preserving cursors, destination suffixes, descriptor lifetime, and caller-owned durability/publication.
76
+ - Preserve the first tree-copy operation failure, including falsy values and existing codes, while closing each owned resource once. Successful operations retain output-before-input and source-before-parent close-failure precedence.
77
+ - Preserve falsy atomic operation, cleanup, close, restoration, temporary-workspace removal, and Windows native cleanup failures; consume retained handles before close to prevent repeated release.
78
+ - Propagate portable ZIP fallback-close failures before publication and copy-fallback writer close failures after successful unsynchronized writes, including restoration with an absent destination.
79
+ - Preserve synchronous destination-admission failures when best-effort close also fails, and preserve directory-mode authority/deadline failures before and after dispatch.
80
+
81
+ ### Performance and maintenance
82
+
83
+ - Bound path-prefix queue consumption on long paths while preserving short-path behavior and resolution checks.
84
+ - Reuse eligible Windows parent-creation receipts, equivalent Root identity observations, and guarded regular-file unlink paths while retaining final identity and policy checks.
85
+ - Consolidate native copy ownership, regular/secret read admission, copy-fallback admission, and temporary-workspace setup and settlement; remove redundant wrappers, write-admission storage, and permission-diagnostic work.
86
+ - Bind public copy, ZIP extraction, atomic settlement, and permission benchmarks to verified workload receipts; keep setup, content checks, and cleanup verification outside timing.
87
+ - Update @napi-rs/cli to 3.10.0 and the locked Node type definitions to 26.6.0, and align Vitest/coverage on 5.0.1.
88
+ - Expand security-owner coverage to native/archive code, platform packages, executable benchmarks, and build/test/release configuration.
89
+ - Raise publication and release-proof job ceilings to 90 minutes, retaining bounded registry retries and fail-closed artifact, signature, and provenance checks.
90
+
3
91
  ## 0.14.0 - 2026-09-17
4
92
 
5
93
  ### Highlights
package/README.md CHANGED
@@ -45,7 +45,7 @@ The same idea has landed in other languages. Go [added `os.Root` and `OpenInRoot
45
45
  | `path.resolve().startsWith()` | string check only | – | – | – | – |
46
46
  | [`write-file-atomic`](https://www.npmjs.com/package/write-file-atomic) | – | ✓ | – | – | – |
47
47
  | Go [`os.Root`](https://go.dev/blog/osroot) / Rust [`cap-std`](https://github.com/bytecodealliance/cap-std) | ✓ | platform | ✓ | ✓ | – |
48
- | **`@openclaw/fs-safe`** | **✓** | **✓** | **✓** | **Linux atomic; others best-effort** | **✓ (ZIP/TAR; native zstd/bzip2)** |
48
+ | **`@openclaw/fs-safe`** | **✓** | **✓** | **✓** | **Linux atomic; others best-effort** | **✓ (ZIP/TAR/gzip/zstd/bzip2)** |
49
49
 
50
50
  ## Not a sandbox
51
51
 
@@ -57,7 +57,7 @@ This is a **library-level guardrail**, not OS-level isolation. It does not repla
57
57
  pnpm add @openclaw/fs-safe
58
58
  ```
59
59
 
60
- Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. With all optional dependencies omitted, public subpaths remain safe to import and fallback-capable operations work in `auto` or `off`. Native-only features, including no-clobber `Root.move()`, remain unavailable and fail with `helper-unavailable`. TAR/gzip fallback uses the bundled WASM build of the same Rust parser as native and works with optional dependencies omitted. ZIP fallback still needs optional `jszip`. See the [0.6 migration guide](docs/migrating-to-0.6.md).
60
+ Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. With all optional dependencies omitted, public subpaths remain safe to import and fallback-capable operations work in `auto` or `off`. Native-only features, including no-clobber `Root.move()` and [`private: true` creation on macOS](docs/creation.md#permission-options), remain unavailable and fail with `helper-unavailable`. TAR, gzip, zstd, and bzip2 extraction and bounded entry reads use the same Rust TAR parser through bundled WASM when native support is disabled or absent. Zstd/bzip2 codecs are bundled too; gzip uses Node's built-in decoder. ZIP fallback still needs optional `jszip`. See the [0.6 migration guide](docs/migrating-to-0.6.md).
61
61
 
62
62
  Bun 1.4.2 is also supported with the [Bun runtime requirements](docs/install.md#bun-runtime), including the matching Rust addon on macOS and Linux. JIT-disabled Bun works too.
63
63
 
@@ -70,7 +70,7 @@ environment policy:
70
70
  import { configureFsSafeNative } from "@openclaw/fs-safe";
71
71
 
72
72
  configureFsSafeNative({ mode: "auto" }); // default: native when available
73
- configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
73
+ configureFsSafeNative({ mode: "off" }); // disable the addon; use supported fallbacks
74
74
  configureFsSafeNative({ mode: "require" }); // fail closed if the binding is unavailable
75
75
  ```
76
76
 
@@ -158,6 +158,23 @@ const opened = await fs.open("notes/today.txt");
158
158
  await fs.create("notes/README.md", "seed\n"); // throws if it already exists
159
159
  ```
160
160
 
161
+ Use `private: true` on `mkdir()`, `ensureRoot()`, `create()`, or `createJson()`
162
+ for private creation. On macOS, this requires native ACL inspection before
163
+ creating parents or stages and verifies owner-only permissions with no ACL
164
+ before writing payload bytes. Native `off` or a missing ACL capability rejects
165
+ with `helper-unavailable`; nonprivate creation is unchanged. See the
166
+ [creation contract](docs/creation.md#permission-options) for parent ACL handling
167
+ and platform support.
168
+
169
+ Pass `{ atomic: true }` to buffered `create()` or `createJson()` to keep the
170
+ destination absent until complete content is ready, including with native support
171
+ disabled. The JavaScript fallback requires hardlinks and never downgrades to a
172
+ partial visible file. Omitted or `false` retains the existing buffered behavior.
173
+ Atomic visibility is separate from the existing `durable` synchronization policy.
174
+ Use `durable: "file"` on `create()` or `createJson()` when file-flush errors,
175
+ including `EPERM`, must propagate. It combines with `atomic: true` without
176
+ requiring strict parent-directory synchronization.
177
+
161
178
  `create()` also accepts an `AsyncIterable<Uint8Array>` for large or incrementally
162
179
  produced files. Streamed creates keep the destination absent until all chunks
163
180
  are written, support `maxBytes` and `signal`, and recheck mutation authority
@@ -259,8 +276,8 @@ contract. Low-level helpers that OpenClaw needs to compose higher-level APIs are
259
276
  | `@openclaw/fs-safe/permissions` | POSIX mode and Windows ACL inspection, raw owner/ACE facts, private-directory creation, and remediation helpers |
260
277
  | `@openclaw/fs-safe/walk` | budget-bounded directory walking with symlink policy, filters, and truncation accounting; not root-bounded |
261
278
  | `@openclaw/fs-safe/copy` | directory copying with `clone: "auto"`, `"always"`, or `"never"`; native APFS, Btrfs, ReFS, XFS, and ZFS cloning, portable byte copying, and clone metadata; see [directory copying](docs/copy.md) |
262
- | `@openclaw/fs-safe/archive` | policy-driven ZIP/TAR extraction, clamp/filter policy, metadata/path-depth limits, native gzip/zstd/bzip2, and bounded entry reads |
263
- | `@openclaw/fs-safe/advanced` | lower-level composition helpers such as path scopes, root-file open, bounded descriptor reads, [borrowed-handle copying](docs/copy.md#borrowed-filehandle-transfers), [exact directory identity](docs/directory-identity.md), [case probing](docs/path-case.md), [suffix-alias probing](docs/path-suffix-aliases.md), [in-place writes](docs/in-place-write.md), [versioned install-ID encoding](docs/install-path.md#safepathsegmenthashedv2), filename sanitizing, temp-file targets, sibling-temp writes, local-root readers, regular-file helpers, `pathExists`, and `withTimeout`; less stable than focused public subpaths |
279
+ | `@openclaw/fs-safe/archive` | policy-driven ZIP/TAR extraction, clamp/filter policy, metadata/path-depth limits, gzip/zstd/bzip2 support, and bounded entry reads |
280
+ | `@openclaw/fs-safe/advanced` | lower-level composition helpers such as path scopes, root-file open, bounded descriptor reads, [borrowed-handle and descriptor copying](docs/copy.md#borrowed-filehandle-transfers), [exact directory identity](docs/directory-identity.md), [case probing](docs/path-case.md), [suffix-alias probing](docs/path-suffix-aliases.md), [in-place writes](docs/in-place-write.md), [versioned install-ID encoding](docs/install-path.md#safepathsegmenthashedv2), filename sanitizing, temp-file targets, sibling-temp writes, local-root readers, regular-file helpers, `pathExists`, and `withTimeout`; less stable than focused public subpaths |
264
281
  | `@openclaw/fs-safe/errors` | `FsSafeError`, closed codes/categories, causes, and operation-specific details receipts |
265
282
  | `@openclaw/fs-safe/types` | shared types: `DirEntry`, `PathStat`, … |
266
283
  | `@openclaw/fs-safe/test-hooks` | hooks the test suite uses to inject races; registration requires `NODE_ENV=test` or `VITEST=true` |
@@ -466,8 +483,13 @@ instead of a root-relative workspace path. It opens the file first, validates th
466
483
  same handle it will read from, checks trusted directories, owner, POSIX mode or
467
484
  Windows ACLs, hardlink count, size, and optional timeout, then reads through the
468
485
  pinned handle. On Windows, both the bytes and the owner/DACL facts come from that
469
- handle; secure reads require the matching current native package and do not fall
470
- back to a pathname ACL command.
486
+ handle. In native `auto` or `off` mode, a packaged, readable PowerShell script can
487
+ inspect that borrowed handle when the native capability is unavailable, subject
488
+ to the [Windows security fallback prerequisites](docs/install.md#windows-security-fallback).
489
+ It emits one fallback warning per process for secure reads and adds PowerShell
490
+ startup and compilation overhead per call. Native `require` remains strict, and
491
+ native operation failures are terminal. Neither route reopens the pathname to
492
+ inspect its ACL.
471
493
 
472
494
  ```ts
473
495
  import { readSecureFile } from "@openclaw/fs-safe/secure-file";
@@ -517,12 +539,18 @@ for (const file of scan.entries) {
517
539
 
518
540
  Check `scan.truncated` before treating the result as complete, and `scan.failedDirs` to tell an incomplete scan (a directory that could not be read) from an empty one before pruning state from the listing.
519
541
 
542
+ `walkDirectory()` accepts asynchronous `include` and `descend` callbacks through `AsyncWalkDirectoryOptions`, so a marker lookup can prune a directory before its children are read. Decisions remain serial and retain the options object as their `this` receiver; `walkDirectorySync()` and its options remain synchronous. See [Directory walking](docs/walk.md) for callback timing, JavaScript result compatibility, and error handling.
543
+
520
544
  For caller-controlled paths, `Root.walk()` is the root-bounded async iterator.
521
545
  It supports entry/depth budgets, in-root symlink following, cancellation, and a
522
546
  truncation marker (or typed error) when a budget is reached. Its `entryFilter`
523
- can return `"skip-subtree"` to prune a directory, and
524
- `onDirectoryError: "skip-and-report"` yields typed `"directory-error"` markers
525
- while preserving entries from readable subtrees.
547
+ accepts `"include"`, `"skip"`, or `"skip-subtree"`, directly or through a Promise.
548
+ After an awaited decision resolves, the walk rechecks cancellation and the
549
+ current listing directory and Root identities before using it. Pending callbacks
550
+ settle before cancellation or iterator disposal completes. Callback failures
551
+ reject the walk. `onDirectoryError: "skip-and-report"` yields typed `"directory-error"` markers
552
+ for directory read or identity-check failures while preserving entries from
553
+ readable subtrees.
526
554
 
527
555
  ## Archive extraction
528
556
 
@@ -1,5 +1,6 @@
1
1
  export { createAsyncLock } from "./async-lock.js";
2
- export { copyFileHandle, type CopyFileHandleOptions } from "./file-handle-transfer.js";
2
+ export { createDirectory, createDirectorySync, createFileSync, type CreateDirectoryOptions, type CreateFileOptions, type OwnedFileDescriptorSync, } from "./create.js";
3
+ export { copyFileHandle, copyFileDescriptorSync, type CopyFileHandleOptions } from "./file-handle-transfer.js";
3
4
  export { overwriteFileHandle, type OverwriteFileHandleOptions } from "./overwrite-file-handle.js";
4
5
  export { probePathCaseInsensitiveSync, type ProbePathCaseOptions } from "./path-case.js";
5
6
  export { resolvePathPrefixSync, type ResolvedPathPrefix } from "./path-prefix.js";
@@ -1 +1 @@
1
- {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
package/dist/advanced.js CHANGED
@@ -2,7 +2,8 @@
2
2
  // public subpaths; prefer root/json/store/temp/archive unless you are building a
3
3
  // higher-level primitive.
4
4
  export { createAsyncLock } from "./async-lock.js";
5
- export { copyFileHandle } from "./file-handle-transfer.js";
5
+ export { createDirectory, createDirectorySync, createFileSync, } from "./create.js";
6
+ export { copyFileHandle, copyFileDescriptorSync } from "./file-handle-transfer.js";
6
7
  export { overwriteFileHandle } from "./overwrite-file-handle.js";
7
8
  export { probePathCaseInsensitiveSync } from "./path-case.js";
8
9
  export { resolvePathPrefixSync } from "./path-prefix.js";
@@ -4,6 +4,5 @@ type ResolvePackedRootDirOptions = {
4
4
  rootMarkers?: string[];
5
5
  };
6
6
  export declare function resolvePackedRootDir(extractDir: string, options?: ResolvePackedRootDirOptions): Promise<string>;
7
- export declare function assertPortableArchiveKind(kind: ArchiveKind): void;
8
7
  export {};
9
8
  //# sourceMappingURL=archive-kind.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"archive-kind.d.ts","sourceRoot":"","sources":["../src/archive-kind.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,WAAW,GAAG,UAAU,GAAG,KAAK,CAAC;AAmBnE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAcvE;AAED,KAAK,2BAA2B,GAAG;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAkBF,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAQjE"}
1
+ {"version":3,"file":"archive-kind.d.ts","sourceRoot":"","sources":["../src/archive-kind.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,WAAW,GAAG,UAAU,GAAG,KAAK,CAAC;AAQnE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAgBvE;AAED,KAAK,2BAA2B,GAAG;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAkBF,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,MAAM,CAAC,CA4BjB"}
@@ -1,29 +1,23 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { FsSafeError } from "./errors.js";
5
4
  import { getNativeBinding } from "./native.js";
6
5
  import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
7
6
  const TAR_SUFFIXES = [".tgz", ".tar.gz", ".tar"];
8
- const NATIVE_TAR_SUFFIXES = [
7
+ const COMPRESSED_TAR_SUFFIXES = [
9
8
  { suffixes: [".tbz2", ".tbz", ".tar.bz2"], kind: "tar-bzip2" },
10
9
  { suffixes: [".tzst", ".tar.zst", ".tar.zstd"], kind: "tar-zstd" },
11
10
  ];
12
- function requireNativeArchiveKind(kind) {
13
- if (!getNativeBinding()) {
14
- throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
15
- "install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
16
- }
17
- return kind;
18
- }
19
11
  export function resolveArchiveKind(filePath) {
20
12
  const lower = normalizeLowercaseStringOrEmpty(filePath);
21
13
  if (lower.endsWith(".zip")) {
22
14
  return "zip";
23
15
  }
24
- for (const { suffixes, kind } of NATIVE_TAR_SUFFIXES) {
16
+ for (const { suffixes, kind } of COMPRESSED_TAR_SUFFIXES) {
25
17
  if (suffixes.some((suffix) => lower.endsWith(suffix))) {
26
- return requireNativeArchiveKind(kind);
18
+ // Preserve strict require diagnostics even when only resolving a suffix.
19
+ getNativeBinding();
20
+ return kind;
27
21
  }
28
22
  }
29
23
  if (TAR_SUFFIXES.some((suffix) => lower.endsWith(suffix))) {
@@ -75,9 +69,3 @@ export async function resolvePackedRootDir(extractDir, options) {
75
69
  }
76
70
  return path.join(extractDir, onlyDir);
77
71
  }
78
- export function assertPortableArchiveKind(kind) {
79
- if (kind === "tar-zstd" || kind === "tar-bzip2") {
80
- throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
81
- "install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
82
- }
83
- }
@@ -1 +1 @@
1
- {"version":3,"file":"archive-merge.d.ts","sourceRoot":"","sources":["../src/archive-merge.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAClG,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAC;AAc9B,MAAM,MAAM,uBAAuB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjG,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AACF,KAAK,kBAAkB,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG;IACtE,gBAAgB,EAAE,qBAAqB,CAAC;CACzC,CAAC;AAEF,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,kBAAkB,GAAG;IAAE,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAS1F"}
1
+ {"version":3,"file":"archive-merge.d.ts","sourceRoot":"","sources":["../src/archive-merge.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAClG,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAC;AAiB9B,MAAM,MAAM,uBAAuB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjG,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AACF,KAAK,kBAAkB,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG;IACtE,gBAAgB,EAAE,qBAAqB,CAAC;CACzC,CAAC;AAEF,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,kBAAkB,GAAG;IAAE,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAS1F"}
@@ -5,16 +5,19 @@ import { ownExtractionDestinationMutation } from "./archive-deadline.js";
5
5
  import { assertDirectoryIdentityGuard, assertResolvedInsideDestination, createDirectoryIdentityGuard, createArchiveSymlinkTraversalError, preparePrivateArchiveOutputPath, } from "./archive-staging.js";
6
6
  import {} from "./directory-mode-owner.js";
7
7
  import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
8
+ import { assertSyncDirectoryGuard, inspectDirectoryIdentitySync } from "./directory-guard.js";
8
9
  import { FsSafeError } from "./errors.js";
9
10
  import { formatErrorDetail } from "./error-detail.js";
10
- import { isPathInside } from "./path.js";
11
+ import { isNotFoundPathError, isPathInside } from "./path.js";
11
12
  import { rootFromDirectoryGuard } from "./root-impl.js";
13
+ import { admitPathInsideRoot } from "./root-boundary.js";
12
14
  import { realpathSync } from "./realpath.js";
13
15
  import { getFsSafeTestHooks } from "./test-hooks.js";
14
- import { onCopyPublication } from "./copy-publication.js";
16
+ import { onCopyPublication, onCopySourceAdmission } from "./copy-publication.js";
15
17
  import { syncFileBestEffortSync } from "./file-sync.js";
16
18
  import { finalizeArchivePublication } from "./archive-durability.js";
17
19
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
20
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
18
21
  export async function mergePlannedArchiveIntoDestination(params) {
19
22
  assertMergePathInputs(params);
20
23
  await mergeTree(params, params.entries, params.durable === true);
@@ -58,6 +61,33 @@ async function mergeTree(params, publication, durable = true) {
58
61
  plan.set(stagedPath, entry);
59
62
  }
60
63
  const ancestors = [];
64
+ const sourceAncestors = [];
65
+ const assertSourceDirectory = (guard, canonical = false) => {
66
+ try {
67
+ // Child receipts already retain canonical ancestry. An exact no-follow
68
+ // observation checks each named association without re-running realpath.
69
+ if (canonical)
70
+ assertSyncDirectoryGuard(guard);
71
+ else
72
+ inspectDirectoryIdentitySync(guard.dir, guard.stat);
73
+ }
74
+ catch (error) {
75
+ if (error instanceof FsSafeError || isNotFoundPathError(error)) {
76
+ throw createArchiveSymlinkTraversalError(path.relative(params.sourceDir, guard.dir));
77
+ }
78
+ throw error;
79
+ }
80
+ check();
81
+ };
82
+ const assertSourceFrontier = () => {
83
+ const current = sourceAncestors.at(-1);
84
+ if (current)
85
+ assertSourceDirectory(current);
86
+ };
87
+ const assertSourceAncestors = () => {
88
+ for (const ancestor of sourceAncestors)
89
+ assertSourceDirectory(ancestor);
90
+ };
61
91
  const assertGuards = async () => {
62
92
  await assertDirectoryIdentityGuard(destinationGuard);
63
93
  check();
@@ -72,15 +102,21 @@ async function mergeTree(params, publication, durable = true) {
72
102
  };
73
103
  const walk = async (sourceDir) => {
74
104
  await assertGuards();
105
+ assertSourceFrontier();
75
106
  const entries = await fs.readdir(sourceDir, { withFileTypes: true });
76
107
  check();
108
+ if (entries.length === 0) {
109
+ await assertGuards();
110
+ assertSourceFrontier();
111
+ }
77
112
  for (const entry of entries) {
78
113
  await assertGuards();
114
+ assertSourceFrontier();
79
115
  const sourcePath = path.join(sourceDir, entry.name);
80
116
  const relPath = path.relative(params.sourceDir, sourcePath);
81
117
  const originalPath = relPath.split(path.sep).join("/");
82
118
  const destinationPath = path.join(destinationDir, relPath);
83
- const sourceStat = fsSync.lstatSync(sourcePath);
119
+ const sourceStat = inspectFileIdentitySync(() => fsSync.lstatSync(sourcePath, { bigint: true }));
84
120
  check();
85
121
  if (sourceStat.isSymbolicLink())
86
122
  throw createArchiveSymlinkTraversalError(originalPath);
@@ -96,61 +132,73 @@ async function mergeTree(params, publication, durable = true) {
96
132
  if (plan && ((planned && planned.kind !== kind) || (!planned && kind === "file"))) {
97
133
  throw new FsSafeError("path-mismatch", "archive staging disagrees with the admitted publication plan");
98
134
  }
99
- const mode = plan ? planned?.mode ?? 0o755 : sourceStat.mode & 0o777;
135
+ let mode = plan ? planned?.mode ?? 0o755 : Number(sourceStat.mode & 511n);
100
136
  await preparePrivateArchiveOutputPath({
101
137
  destinationDir, destinationRealDir, deadline: params.deadline,
102
138
  relPath, outPath: destinationPath, originalPath, isDirectory: kind === "directory",
103
139
  }, assertGuards, destinationGuard);
104
140
  check();
105
141
  if (kind === "directory") {
106
- // Ownership spans open, descendants, finalization and close, including timeout.
107
- await ownExtractionDestinationMutation(params.deadline, async () => {
108
- await assertGuards();
109
- const owner = await pinNodeDirectoryForMode(destinationPath).catch((error) => {
110
- if (error instanceof FsSafeError && (error.code === "not-file" || error.code === "path-mismatch")) {
111
- throw createArchiveSymlinkTraversalError(originalPath);
112
- }
113
- throw error;
114
- });
115
- try {
116
- check();
117
- const guard = await createDirectoryIdentityGuard(destinationPath);
118
- check();
119
- await owner.verify(check);
120
- ancestors.push({ guard, owner });
121
- try {
122
- await walk(sourcePath);
123
- if (publication) {
124
- await assertGuards();
125
- publishedDirectories.push({ guard, mode, parents: ancestors.slice(0, -1).map((ancestor) => ancestor.guard) });
126
- return;
142
+ // Retain the observation made before output preparation: recapturing
143
+ // here could silently authorize a replacement source directory.
144
+ sourceAncestors.push({ dir: sourcePath, realPath: sourceReal, stat: sourceStat });
145
+ try {
146
+ // Ownership spans open, descendants, finalization and close, including timeout.
147
+ await ownExtractionDestinationMutation(params.deadline, async () => {
148
+ await assertGuards();
149
+ assertSourceFrontier();
150
+ const owner = await pinNodeDirectoryForMode(destinationPath).catch((error) => {
151
+ if (error instanceof FsSafeError && (error.code === "not-file" || error.code === "path-mismatch")) {
152
+ throw createArchiveSymlinkTraversalError(originalPath);
127
153
  }
128
- await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("chmod", destinationPath);
154
+ throw error;
155
+ });
156
+ try {
129
157
  check();
130
- await assertGuards();
131
- // Do not recursively verify this owner from inside its serialized apply.
132
- ancestors.pop();
133
- await owner.apply(mode, { check, beforeChmod: async () => {
134
- await assertGuards();
135
- await assertDirectoryIdentityGuard(guard);
136
- check();
137
- await assertResolvedInsideDestination({
138
- destinationRealDir, targetPath: destinationPath, originalPath,
139
- });
140
- check();
141
- } });
158
+ const guard = await createDirectoryIdentityGuard(destinationPath);
142
159
  check();
143
- await assertGuards();
160
+ await owner.verify(check);
161
+ ancestors.push({ guard, owner });
162
+ try {
163
+ await walk(sourcePath);
164
+ if (publication) {
165
+ await assertGuards();
166
+ assertSourceAncestors();
167
+ publishedDirectories.push({ guard, mode, parents: ancestors.slice(0, -1).map((ancestor) => ancestor.guard) });
168
+ return;
169
+ }
170
+ await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("chmod", destinationPath);
171
+ check();
172
+ await assertGuards();
173
+ // Do not recursively verify this owner from inside its serialized apply.
174
+ ancestors.pop();
175
+ await owner.apply(mode, { check, beforeChmod: async () => {
176
+ await assertGuards();
177
+ assertSourceAncestors();
178
+ await assertDirectoryIdentityGuard(guard);
179
+ check();
180
+ await assertResolvedInsideDestination({
181
+ destinationRealDir, targetPath: destinationPath, originalPath,
182
+ });
183
+ check();
184
+ } });
185
+ check();
186
+ await assertGuards();
187
+ assertSourceAncestors();
188
+ }
189
+ finally {
190
+ if (ancestors.at(-1)?.owner === owner)
191
+ ancestors.pop();
192
+ }
144
193
  }
145
194
  finally {
146
- if (ancestors.at(-1)?.owner === owner)
147
- ancestors.pop();
195
+ await owner.close();
148
196
  }
149
- }
150
- finally {
151
- await owner.close();
152
- }
153
- });
197
+ });
198
+ }
199
+ finally {
200
+ sourceAncestors.pop();
201
+ }
154
202
  }
155
203
  else {
156
204
  await ownExtractionDestinationMutation(params.deadline, async () => {
@@ -158,6 +206,25 @@ async function mergeTree(params, publication, durable = true) {
158
206
  try {
159
207
  const options = {
160
208
  mkdir: true, mode, durable: publication ? false : true,
209
+ [onCopySourceAdmission]: (identity, realPath) => {
210
+ assertSourceDirectory(sourceGuard, true);
211
+ assertSourceFrontier();
212
+ inspectFileIdentitySync(() => identity, sourceStat);
213
+ if (!admitPathInsideRoot({
214
+ rootPath: sourceGuard.realPath, candidatePath: realPath, rootIdentity: sourceGuard.stat,
215
+ }))
216
+ throw createArchiveSymlinkTraversalError(originalPath);
217
+ // Public merge permissions come from the admitted descriptor;
218
+ // extraction keeps the separately admitted publication plan.
219
+ if (!plan)
220
+ mode = Number(identity.mode & 511n);
221
+ return { mode, verify() {
222
+ // Only copy publication boundaries scan the active chain.
223
+ // Destination mkdir callbacks must not multiply this work.
224
+ assertSourceDirectory(sourceGuard, true);
225
+ assertSourceAncestors();
226
+ } };
227
+ },
161
228
  };
162
229
  if (publication && durable) {
163
230
  options[onCopyPublication] = async (fd, identity) => {
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AA0OpG,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAkBjB"}
1
+ {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAqOzE,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAiBjB"}
@@ -7,12 +7,13 @@ import { readBoundedAsync } from "./bounded-read.js";
7
7
  import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
8
8
  import { formatErrorDetail } from "./error-detail.js";
9
9
  import { stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
10
- import { assertPortableArchiveKind, resolveArchiveKind } from "./archive-kind.js";
10
+ import { resolveArchiveKind } from "./archive-kind.js";
11
11
  import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP, ArchiveLimitError, ARCHIVE_LIMIT_ERROR_CODE, } from "./archive-limits.js";
12
12
  import { isGzipBuffer } from "./archive-gzip-tail.js";
13
13
  import { inspectTar, replayTar } from "./archive-tar-stream.js";
14
14
  import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
15
15
  import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
16
+ import { isZipSymlinkEntry, zipEntryKind } from "./archive-zip-entry.js";
16
17
  import { FsSafeError } from "./errors.js";
17
18
  import { inspectFileIdentity } from "./strict-file-identity.js";
18
19
  import { resolveReadOpenFlags } from "./read-open-flags.js";
@@ -22,8 +23,6 @@ import { admitZipBuffer } from "./archive-zip-admission.js";
22
23
  import { validateNativeZipManifest } from "./archive-zip-manifest.js";
23
24
  import { resolveExtractLimits, resolveTarMeterLimits } from "./archive-limits.js";
24
25
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
25
- const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
26
- const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
27
26
  function canonicalEntryPath(entryPath) {
28
27
  validateArchiveEntryPath(entryPath, { escapeLabel: "archive root" });
29
28
  return stripArchivePath(entryPath, 0) ?? "";
@@ -109,10 +108,11 @@ async function readZipEntry(buffer, entryPath, maxBytes, admitted) {
109
108
  if (!entry || entry.dir) {
110
109
  throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
111
110
  }
112
- if (typeof entry.unixPermissions === "number" &&
113
- (entry.unixPermissions & ZIP_UNIX_FILE_TYPE_MASK) === ZIP_UNIX_SYMLINK_TYPE) {
111
+ if (isZipSymlinkEntry(entry)) {
114
112
  throw new Error(`archive entry is a link: ${formatErrorDetail(entryPath)}`);
115
113
  }
114
+ if (zipEntryKind(entry) !== "file")
115
+ throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
116
116
  const integrity = createZipIntegrityTransform(entry);
117
117
  const stream = typeof entry.nodeStream === "function"
118
118
  ? entry.nodeStream()
@@ -139,11 +139,11 @@ async function readZipEntry(buffer, entryPath, maxBytes, admitted) {
139
139
  throw normalizeZipIntegrityError(error);
140
140
  }
141
141
  }
142
- async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
142
+ async function readTarEntry(archiveBuffer, entryPath, maxBytes, kind) {
143
143
  const seenPaths = new Set();
144
144
  let selected;
145
145
  const limits = resolveTarMeterLimits();
146
- await inspectTar({ archiveBuffer, limits, onMember(info) {
146
+ await inspectTar({ archiveBuffer, kind, limits, onMember(info) {
147
147
  const normalized = canonicalEntryPath(info.path);
148
148
  if (seenPaths.has(normalized)) {
149
149
  throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
@@ -159,7 +159,7 @@ async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
159
159
  }
160
160
  if (selected.size > maxBytes)
161
161
  throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
162
- if (!isGzipBuffer(archiveBuffer)) {
162
+ if (kind === "tar" && !isGzipBuffer(archiveBuffer)) {
163
163
  // Complete admission already validated this private snapshot through EOF.
164
164
  // Copy the admitted payload so the result cannot expose or mutate its input.
165
165
  const end = selected.offset + selected.size;
@@ -170,7 +170,7 @@ async function readTarEntry(archiveBuffer, entryPath, maxBytes) {
170
170
  return Buffer.from(archiveBuffer.subarray(selected.offset, end));
171
171
  }
172
172
  let result;
173
- await replayTar({ archiveBuffer, limits, members: [selected], async consume(member, payload) {
173
+ await replayTar({ archiveBuffer, kind, limits, members: [selected], async consume(member, payload) {
174
174
  result = await readAdmittedTarPayload(payload, member.size);
175
175
  } });
176
176
  return result;
@@ -238,7 +238,6 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
238
238
  const native = getNativeBinding();
239
239
  if (native)
240
240
  return await readNativeBufferEntry(native, buffer, kind, requestedEntry, entryPath, options.maxBytes, zipEntries);
241
- assertPortableArchiveKind(kind);
242
241
  return kind === "zip" ? await readZipEntry(buffer, requestedEntry, options.maxBytes, zipEntries)
243
- : await readTarEntry(buffer, requestedEntry, options.maxBytes);
242
+ : await readTarEntry(buffer, requestedEntry, options.maxBytes, kind);
244
243
  }