@openclaw/fs-safe 0.12.0 → 0.13.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 (383) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +12 -7
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +32 -11
  5. package/dist/advanced.d.ts +2 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +2 -0
  8. package/dist/archive-input.d.ts.map +1 -1
  9. package/dist/archive-input.js +6 -2
  10. package/dist/archive-merge.d.ts.map +1 -1
  11. package/dist/archive-merge.js +15 -2
  12. package/dist/archive-native.d.ts.map +1 -1
  13. package/dist/archive-native.js +7 -19
  14. package/dist/archive-plan.js +1 -1
  15. package/dist/archive-read.d.ts.map +1 -1
  16. package/dist/archive-read.js +18 -13
  17. package/dist/archive-staging.d.ts.map +1 -1
  18. package/dist/archive-staging.js +43 -10
  19. package/dist/archive-tar-inspect.d.ts.map +1 -1
  20. package/dist/archive-tar-inspect.js +4 -1
  21. package/dist/archive-zip-admission.d.ts +1 -1
  22. package/dist/archive-zip-admission.d.ts.map +1 -1
  23. package/dist/archive-zip-admission.js +2 -2
  24. package/dist/archive-zip-directory.d.ts +3 -0
  25. package/dist/archive-zip-directory.d.ts.map +1 -1
  26. package/dist/archive-zip-directory.js +13 -2
  27. package/dist/archive-zip-loader.d.ts +3 -2
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +30 -4
  30. package/dist/archive-zip-manifest.d.ts +5 -0
  31. package/dist/archive-zip-manifest.d.ts.map +1 -0
  32. package/dist/archive-zip-manifest.js +22 -0
  33. package/dist/archive-zip-names.d.ts +6 -1
  34. package/dist/archive-zip-names.d.ts.map +1 -1
  35. package/dist/archive-zip-names.js +35 -14
  36. package/dist/archive-zip-preflight.d.ts.map +1 -1
  37. package/dist/archive-zip-preflight.js +3 -2
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +9 -4
  40. package/dist/darwin-acl.d.ts +4 -0
  41. package/dist/darwin-acl.d.ts.map +1 -0
  42. package/dist/darwin-acl.js +24 -0
  43. package/dist/deny-mutations.d.ts.map +1 -1
  44. package/dist/deny-mutations.js +8 -2
  45. package/dist/directory-durability.d.ts.map +1 -1
  46. package/dist/directory-durability.js +67 -23
  47. package/dist/directory-entry-path.d.ts +3 -0
  48. package/dist/directory-entry-path.d.ts.map +1 -0
  49. package/dist/directory-entry-path.js +21 -0
  50. package/dist/directory-guard.d.ts +17 -1
  51. package/dist/directory-guard.d.ts.map +1 -1
  52. package/dist/directory-guard.js +134 -48
  53. package/dist/directory-mode-node.d.ts +12 -0
  54. package/dist/directory-mode-node.d.ts.map +1 -1
  55. package/dist/directory-mode-node.js +102 -4
  56. package/dist/effective-uid.d.ts +2 -0
  57. package/dist/effective-uid.d.ts.map +1 -0
  58. package/dist/effective-uid.js +25 -0
  59. package/dist/file-handle-transfer.d.ts.map +1 -1
  60. package/dist/file-handle-transfer.js +98 -27
  61. package/dist/file-hash.d.ts.map +1 -1
  62. package/dist/file-hash.js +3 -0
  63. package/dist/file-lock-sync.d.ts.map +1 -1
  64. package/dist/file-lock-sync.js +36 -6
  65. package/dist/file-lock.d.ts.map +1 -1
  66. package/dist/file-lock.js +29 -9
  67. package/dist/file-observation.d.ts +1 -1
  68. package/dist/file-observation.d.ts.map +1 -1
  69. package/dist/file-store-boundary.d.ts +6 -2
  70. package/dist/file-store-boundary.d.ts.map +1 -1
  71. package/dist/file-store-boundary.js +20 -65
  72. package/dist/file-store-copy-source.d.ts +5 -0
  73. package/dist/file-store-copy-source.d.ts.map +1 -0
  74. package/dist/file-store-copy-source.js +31 -0
  75. package/dist/file-store-path.d.ts.map +1 -1
  76. package/dist/file-store-path.js +4 -1
  77. package/dist/file-store-sync-directory.d.ts +16 -0
  78. package/dist/file-store-sync-directory.d.ts.map +1 -0
  79. package/dist/file-store-sync-directory.js +349 -0
  80. package/dist/file-store.d.ts.map +1 -1
  81. package/dist/file-store.js +11 -28
  82. package/dist/filename.d.ts.map +1 -1
  83. package/dist/filename.js +65 -20
  84. package/dist/fs.d.ts.map +1 -1
  85. package/dist/fs.js +3 -2
  86. package/dist/guarded-mkdir.d.ts +8 -0
  87. package/dist/guarded-mkdir.d.ts.map +1 -1
  88. package/dist/guarded-mkdir.js +176 -27
  89. package/dist/guest.d.ts.map +1 -1
  90. package/dist/guest.js +4 -1
  91. package/dist/home-dir.d.ts.map +1 -1
  92. package/dist/home-dir.js +73 -10
  93. package/dist/install-path.d.ts.map +1 -1
  94. package/dist/install-path.js +54 -19
  95. package/dist/json-durable-queue-ownership.d.ts +6 -0
  96. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  97. package/dist/json-durable-queue-ownership.js +89 -42
  98. package/dist/json-durable-queue-paths.d.ts +11 -0
  99. package/dist/json-durable-queue-paths.d.ts.map +1 -0
  100. package/dist/json-durable-queue-paths.js +42 -0
  101. package/dist/json-durable-queue-read.d.ts.map +1 -1
  102. package/dist/json-durable-queue-read.js +2 -0
  103. package/dist/json-durable-queue.d.ts.map +1 -1
  104. package/dist/json-durable-queue.js +50 -58
  105. package/dist/json-store.d.ts.map +1 -1
  106. package/dist/json-store.js +5 -1
  107. package/dist/json.d.ts.map +1 -1
  108. package/dist/json.js +54 -22
  109. package/dist/local-file-access.d.ts.map +1 -1
  110. package/dist/local-file-access.js +4 -0
  111. package/dist/local-file-descriptor.d.ts +17 -0
  112. package/dist/local-file-descriptor.d.ts.map +1 -0
  113. package/dist/local-file-descriptor.js +84 -0
  114. package/dist/local-roots.d.ts.map +1 -1
  115. package/dist/local-roots.js +35 -7
  116. package/dist/move-path-cleanup.d.ts +2 -0
  117. package/dist/move-path-cleanup.d.ts.map +1 -1
  118. package/dist/move-path-cleanup.js +44 -18
  119. package/dist/move-path.d.ts.map +1 -1
  120. package/dist/move-path.js +31 -6
  121. package/dist/native-binding.d.ts +14 -0
  122. package/dist/native-binding.d.ts.map +1 -1
  123. package/dist/native-directory-observation.d.ts +17 -0
  124. package/dist/native-directory-observation.d.ts.map +1 -0
  125. package/dist/native-directory-observation.js +37 -0
  126. package/dist/native-parent-admission.d.ts +31 -0
  127. package/dist/native-parent-admission.d.ts.map +1 -0
  128. package/dist/native-parent-admission.js +124 -0
  129. package/dist/native-pinned-write-windows.d.ts +0 -1
  130. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  131. package/dist/native-pinned-write-windows.js +0 -3
  132. package/dist/native-pinned-write.d.ts.map +1 -1
  133. package/dist/native-pinned-write.js +309 -40
  134. package/dist/output.d.ts.map +1 -1
  135. package/dist/output.js +15 -5
  136. package/dist/overwrite-file-handle.d.ts.map +1 -1
  137. package/dist/overwrite-file-handle.js +5 -1
  138. package/dist/owner-dacl.d.ts.map +1 -1
  139. package/dist/owner-dacl.js +2 -0
  140. package/dist/path-policy.d.ts.map +1 -1
  141. package/dist/path-policy.js +7 -2
  142. package/dist/path-prefix.d.ts +7 -0
  143. package/dist/path-prefix.d.ts.map +1 -0
  144. package/dist/path-prefix.js +82 -0
  145. package/dist/path-scope-lexical.d.ts.map +1 -1
  146. package/dist/path-scope-lexical.js +18 -8
  147. package/dist/path-segment-route.d.ts +7 -0
  148. package/dist/path-segment-route.d.ts.map +1 -0
  149. package/dist/path-segment-route.js +24 -0
  150. package/dist/path-suffix-aliases.d.ts +10 -0
  151. package/dist/path-suffix-aliases.d.ts.map +1 -0
  152. package/dist/path-suffix-aliases.js +386 -0
  153. package/dist/path.d.ts.map +1 -1
  154. package/dist/path.js +16 -5
  155. package/dist/permissions-windows.d.ts.map +1 -1
  156. package/dist/permissions-windows.js +14 -3
  157. package/dist/permissions.d.ts.map +1 -1
  158. package/dist/permissions.js +37 -8
  159. package/dist/pinned-mutation-admission.d.ts +25 -0
  160. package/dist/pinned-mutation-admission.d.ts.map +1 -0
  161. package/dist/pinned-mutation-admission.js +425 -0
  162. package/dist/pinned-mutation-observation.d.ts +34 -0
  163. package/dist/pinned-mutation-observation.d.ts.map +1 -0
  164. package/dist/pinned-mutation-observation.js +142 -0
  165. package/dist/pinned-mutation-shared-route.d.ts +24 -0
  166. package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
  167. package/dist/pinned-mutation-shared-route.js +70 -0
  168. package/dist/pinned-open.d.ts +6 -0
  169. package/dist/pinned-open.d.ts.map +1 -1
  170. package/dist/pinned-open.js +28 -10
  171. package/dist/pinned-write-types.d.ts +75 -0
  172. package/dist/pinned-write-types.d.ts.map +1 -0
  173. package/dist/pinned-write-types.js +1 -0
  174. package/dist/pinned-write.d.ts +7 -33
  175. package/dist/pinned-write.d.ts.map +1 -1
  176. package/dist/pinned-write.js +151 -17
  177. package/dist/private-directory.d.ts.map +1 -1
  178. package/dist/private-directory.js +2 -0
  179. package/dist/private-producer-handoff.d.ts +16 -0
  180. package/dist/private-producer-handoff.d.ts.map +1 -0
  181. package/dist/private-producer-handoff.js +272 -0
  182. package/dist/private-temp-workspace.d.ts +2 -39
  183. package/dist/private-temp-workspace.d.ts.map +1 -1
  184. package/dist/private-temp-workspace.js +183 -77
  185. package/dist/publish-file.d.ts.map +1 -1
  186. package/dist/publish-file.js +33 -29
  187. package/dist/regular-file.d.ts.map +1 -1
  188. package/dist/regular-file.js +56 -45
  189. package/dist/replace-directory.d.ts.map +1 -1
  190. package/dist/replace-directory.js +12 -5
  191. package/dist/replace-file-temp-owner.d.ts +1 -0
  192. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  193. package/dist/replace-file-temp-owner.js +12 -1
  194. package/dist/replace-file.d.ts.map +1 -1
  195. package/dist/replace-file.js +24 -24
  196. package/dist/root-boundary.d.ts +4 -0
  197. package/dist/root-boundary.d.ts.map +1 -1
  198. package/dist/root-boundary.js +6 -1
  199. package/dist/root-context.d.ts +12 -1
  200. package/dist/root-context.d.ts.map +1 -1
  201. package/dist/root-context.js +57 -11
  202. package/dist/root-directory-creation.d.ts +13 -0
  203. package/dist/root-directory-creation.d.ts.map +1 -0
  204. package/dist/root-directory-creation.js +212 -0
  205. package/dist/root-directory-list.d.ts +16 -4
  206. package/dist/root-directory-list.d.ts.map +1 -1
  207. package/dist/root-directory-list.js +180 -39
  208. package/dist/root-directory.d.ts +23 -0
  209. package/dist/root-directory.d.ts.map +1 -0
  210. package/dist/root-directory.js +141 -0
  211. package/dist/root-errors.d.ts.map +1 -1
  212. package/dist/root-errors.js +12 -1
  213. package/dist/root-file-final-admission.d.ts +21 -0
  214. package/dist/root-file-final-admission.d.ts.map +1 -0
  215. package/dist/root-file-final-admission.js +83 -0
  216. package/dist/root-file.d.ts.map +1 -1
  217. package/dist/root-file.js +103 -24
  218. package/dist/root-impl.d.ts +1 -0
  219. package/dist/root-impl.d.ts.map +1 -1
  220. package/dist/root-impl.js +289 -317
  221. package/dist/root-move-noreplace.d.ts +14 -0
  222. package/dist/root-move-noreplace.d.ts.map +1 -0
  223. package/dist/root-move-noreplace.js +202 -0
  224. package/dist/root-observed-path.d.ts +9 -0
  225. package/dist/root-observed-path.d.ts.map +1 -0
  226. package/dist/root-observed-path.js +95 -0
  227. package/dist/root-path-errors.d.ts +12 -0
  228. package/dist/root-path-errors.d.ts.map +1 -0
  229. package/dist/root-path-errors.js +13 -0
  230. package/dist/root-path-existing.d.ts.map +1 -1
  231. package/dist/root-path-existing.js +53 -20
  232. package/dist/root-path-observation.d.ts +63 -0
  233. package/dist/root-path-observation.d.ts.map +1 -0
  234. package/dist/root-path-observation.js +180 -0
  235. package/dist/root-path-stat.d.ts +5 -0
  236. package/dist/root-path-stat.d.ts.map +1 -0
  237. package/dist/root-path-stat.js +101 -0
  238. package/dist/root-path-symlink.d.ts.map +1 -1
  239. package/dist/root-path-symlink.js +23 -4
  240. package/dist/root-path.d.ts +11 -0
  241. package/dist/root-path.d.ts.map +1 -1
  242. package/dist/root-path.js +215 -53
  243. package/dist/root-paths-lexical.d.ts +9 -0
  244. package/dist/root-paths-lexical.d.ts.map +1 -0
  245. package/dist/root-paths-lexical.js +22 -0
  246. package/dist/root-paths.d.ts +3 -25
  247. package/dist/root-paths.d.ts.map +1 -1
  248. package/dist/root-paths.js +77 -154
  249. package/dist/root-read-admission.d.ts +26 -0
  250. package/dist/root-read-admission.d.ts.map +1 -0
  251. package/dist/root-read-admission.js +94 -0
  252. package/dist/root-remove-identity.d.ts +15 -0
  253. package/dist/root-remove-identity.d.ts.map +1 -0
  254. package/dist/root-remove-identity.js +89 -0
  255. package/dist/root-remove-receipt.d.ts +17 -0
  256. package/dist/root-remove-receipt.d.ts.map +1 -0
  257. package/dist/root-remove-receipt.js +37 -0
  258. package/dist/root-remove.d.ts +2 -1
  259. package/dist/root-remove.d.ts.map +1 -1
  260. package/dist/root-remove.js +172 -10
  261. package/dist/root-write-admission.d.ts +68 -0
  262. package/dist/root-write-admission.d.ts.map +1 -0
  263. package/dist/root-write-admission.js +322 -0
  264. package/dist/root-write-compatibility.d.ts +13 -0
  265. package/dist/root-write-compatibility.d.ts.map +1 -0
  266. package/dist/root-write-compatibility.js +74 -0
  267. package/dist/root-write-complete-parent.d.ts +42 -0
  268. package/dist/root-write-complete-parent.d.ts.map +1 -0
  269. package/dist/root-write-complete-parent.js +195 -0
  270. package/dist/root-write-publication.d.ts +24 -0
  271. package/dist/root-write-publication.d.ts.map +1 -0
  272. package/dist/root-write-publication.js +85 -0
  273. package/dist/root-write-verification.d.ts.map +1 -1
  274. package/dist/root-write-verification.js +6 -4
  275. package/dist/secret-file.d.ts.map +1 -1
  276. package/dist/secret-file.js +62 -24
  277. package/dist/secret-read-async.d.ts.map +1 -1
  278. package/dist/secret-read-async.js +18 -13
  279. package/dist/secret-read-policy.d.ts.map +1 -1
  280. package/dist/secret-read-policy.js +5 -1
  281. package/dist/secure-file.d.ts.map +1 -1
  282. package/dist/secure-file.js +92 -7
  283. package/dist/secure-temp-dir.d.ts +6 -3
  284. package/dist/secure-temp-dir.d.ts.map +1 -1
  285. package/dist/secure-temp-dir.js +121 -101
  286. package/dist/secure-temp-repair.d.ts +35 -0
  287. package/dist/secure-temp-repair.d.ts.map +1 -0
  288. package/dist/secure-temp-repair.js +106 -0
  289. package/dist/sibling-staged-file.d.ts +3 -2
  290. package/dist/sibling-staged-file.d.ts.map +1 -1
  291. package/dist/sibling-staged-file.js +179 -55
  292. package/dist/sibling-temp.d.ts.map +1 -1
  293. package/dist/sibling-temp.js +27 -13
  294. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  295. package/dist/sidecar-lock-acquire.js +44 -14
  296. package/dist/sidecar-lock-root.d.ts.map +1 -1
  297. package/dist/sidecar-lock-root.js +4 -2
  298. package/dist/staged-directory.d.ts +14 -5
  299. package/dist/staged-directory.d.ts.map +1 -1
  300. package/dist/staged-directory.js +54 -3
  301. package/dist/standalone-publication-path.d.ts +2 -0
  302. package/dist/standalone-publication-path.d.ts.map +1 -0
  303. package/dist/standalone-publication-path.js +6 -0
  304. package/dist/stat-observation.d.ts +11 -0
  305. package/dist/stat-observation.d.ts.map +1 -0
  306. package/dist/stat-observation.js +64 -0
  307. package/dist/strict-file-identity.d.ts.map +1 -1
  308. package/dist/strict-file-identity.js +39 -2
  309. package/dist/symlink-parents.d.ts.map +1 -1
  310. package/dist/symlink-parents.js +7 -2
  311. package/dist/temp-target.d.ts +2 -0
  312. package/dist/temp-target.d.ts.map +1 -1
  313. package/dist/temp-target.js +130 -3
  314. package/dist/temp-workspace-admission.d.ts +22 -0
  315. package/dist/temp-workspace-admission.d.ts.map +1 -0
  316. package/dist/temp-workspace-admission.js +374 -0
  317. package/dist/temp-workspace-child-admission.d.ts +13 -0
  318. package/dist/temp-workspace-child-admission.d.ts.map +1 -0
  319. package/dist/temp-workspace-child-admission.js +95 -0
  320. package/dist/temp-workspace-descriptor.d.ts +48 -0
  321. package/dist/temp-workspace-descriptor.d.ts.map +1 -0
  322. package/dist/temp-workspace-descriptor.js +363 -0
  323. package/dist/temp-workspace-identity.d.ts +15 -0
  324. package/dist/temp-workspace-identity.d.ts.map +1 -0
  325. package/dist/temp-workspace-identity.js +41 -0
  326. package/dist/temp-workspace-owner.d.ts +7 -6
  327. package/dist/temp-workspace-owner.d.ts.map +1 -1
  328. package/dist/temp-workspace-owner.js +121 -61
  329. package/dist/temp-workspace-permissions.d.ts +4 -0
  330. package/dist/temp-workspace-permissions.d.ts.map +1 -0
  331. package/dist/temp-workspace-permissions.js +32 -0
  332. package/dist/temp-workspace-types.d.ts +40 -0
  333. package/dist/temp-workspace-types.d.ts.map +1 -0
  334. package/dist/temp-workspace-types.js +1 -0
  335. package/dist/temp.d.ts +1 -1
  336. package/dist/temp.d.ts.map +1 -1
  337. package/dist/temp.js +1 -1
  338. package/dist/test-hooks.d.ts +7 -0
  339. package/dist/test-hooks.d.ts.map +1 -1
  340. package/dist/text-atomic.d.ts.map +1 -1
  341. package/dist/text-atomic.js +3 -1
  342. package/dist/trash.d.ts.map +1 -1
  343. package/dist/trash.js +54 -8
  344. package/dist/walk.d.ts.map +1 -1
  345. package/dist/walk.js +9 -6
  346. package/dist/windows-owner.d.ts.map +1 -1
  347. package/dist/windows-owner.js +5 -0
  348. package/dist/windows-path-alias.d.ts +39 -0
  349. package/dist/windows-path-alias.d.ts.map +1 -0
  350. package/dist/windows-path-alias.js +153 -0
  351. package/docs/advanced.md +25 -0
  352. package/docs/archive.md +23 -2
  353. package/docs/atomic.md +27 -3
  354. package/docs/copy.md +3 -0
  355. package/docs/durability.md +14 -0
  356. package/docs/errors.md +14 -6
  357. package/docs/file-store.md +24 -3
  358. package/docs/filename.md +14 -7
  359. package/docs/install-path.md +2 -2
  360. package/docs/install.md +8 -7
  361. package/docs/json-store.md +5 -1
  362. package/docs/json.md +11 -0
  363. package/docs/mutation-policy-proof.md +65 -0
  364. package/docs/native-helper.md +26 -9
  365. package/docs/native.md +32 -8
  366. package/docs/output.md +33 -11
  367. package/docs/path-prefix.md +64 -0
  368. package/docs/path-suffix-aliases.md +159 -0
  369. package/docs/path.md +11 -0
  370. package/docs/private-file-store.md +14 -0
  371. package/docs/public-api.md +9 -0
  372. package/docs/quickstart.md +1 -1
  373. package/docs/reading.md +8 -1
  374. package/docs/root.md +18 -3
  375. package/docs/secret-file.md +3 -0
  376. package/docs/secure-file.md +6 -2
  377. package/docs/security-model.md +75 -8
  378. package/docs/sidecar-lock.md +28 -1
  379. package/docs/store.md +5 -1
  380. package/docs/temp.md +260 -36
  381. package/docs/test-hooks.md +14 -0
  382. package/docs/writing.md +38 -8
  383. package/package.json +10 -10
package/dist/walk.js CHANGED
@@ -2,6 +2,7 @@ import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { realpathSync } from "./realpath.js";
5
+ import { pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
5
6
  function validateWalkBudget(name, value) {
6
7
  if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
7
8
  throw new RangeError(`${name} must be a non-negative safe integer`);
@@ -69,7 +70,7 @@ function resolveKind(fullPath, dirent, symlinks) {
69
70
  }
70
71
  export function walkDirectorySync(rootDir, options = {}) {
71
72
  validateWalkOptions(options);
72
- const root = path.resolve(rootDir);
73
+ const root = resolvePathPreservingWindowsRoot(rootDir);
73
74
  const symlinks = options.symlinks ?? "skip";
74
75
  const result = {
75
76
  entries: [],
@@ -82,8 +83,9 @@ export function walkDirectorySync(rootDir, options = {}) {
82
83
  if (options.maxDepth !== undefined && depth > options.maxDepth)
83
84
  return;
84
85
  let realDir;
86
+ const operationPath = pathForWindowsFilesystem(dir);
85
87
  try {
86
- realDir = realpathSync(dir);
88
+ realDir = realpathSync(operationPath);
87
89
  }
88
90
  catch (error) {
89
91
  recordFailedDir(result, root, dir, depth, error);
@@ -94,7 +96,7 @@ export function walkDirectorySync(rootDir, options = {}) {
94
96
  visitedDirs.add(realDir);
95
97
  let entries;
96
98
  try {
97
- entries = fsSync.readdirSync(dir, { withFileTypes: true });
99
+ entries = fsSync.readdirSync(operationPath, { withFileTypes: true });
98
100
  }
99
101
  catch (error) {
100
102
  recordFailedDir(result, root, dir, depth, error);
@@ -129,7 +131,7 @@ export function walkDirectorySync(rootDir, options = {}) {
129
131
  }
130
132
  export async function walkDirectory(rootDir, options = {}) {
131
133
  validateWalkOptions(options);
132
- const root = path.resolve(rootDir);
134
+ const root = resolvePathPreservingWindowsRoot(rootDir);
133
135
  const symlinks = options.symlinks ?? "skip";
134
136
  const result = {
135
137
  entries: [],
@@ -142,8 +144,9 @@ export async function walkDirectory(rootDir, options = {}) {
142
144
  if (options.maxDepth !== undefined && depth > options.maxDepth)
143
145
  return;
144
146
  let realDir;
147
+ const operationPath = pathForWindowsFilesystem(dir);
145
148
  try {
146
- realDir = realpathSync.native(dir);
149
+ realDir = realpathSync.native(operationPath);
147
150
  }
148
151
  catch (error) {
149
152
  recordFailedDir(result, root, dir, depth, error);
@@ -154,7 +157,7 @@ export async function walkDirectory(rootDir, options = {}) {
154
157
  visitedDirs.add(realDir);
155
158
  let entries;
156
159
  try {
157
- entries = await fs.readdir(dir, { withFileTypes: true });
160
+ entries = await fs.readdir(operationPath, { withFileTypes: true });
158
161
  }
159
162
  catch (error) {
160
163
  recordFailedDir(result, root, dir, depth, error);
@@ -1 +1 @@
1
- {"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,sBAAsB,CAAC;AAG9B,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,eAAe,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAqDF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA+C/B"}
1
+ {"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,sBAAsB,CAAC;AAI9B,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,eAAe,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAqDF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmD/B"}
@@ -1,5 +1,6 @@
1
1
  import { formatPermissionErrorDetail, getPermissionCommandFailure, } from "./permission-exec.js";
2
2
  import { resolveWindowsSystemCommand } from "./windows-command.js";
3
+ import { hasWindowsPathAlias } from "./windows-path-alias.js";
3
4
  const SID_RE = /^\*?s-\d+-\d+(-\d+)+$/i;
4
5
  const TRUSTED_OWNER_SIDS = new Set(["s-1-5-18", "s-1-5-32-544"]);
5
6
  function normalizeSid(value) {
@@ -47,6 +48,10 @@ function parseWindowsAclFacts(parsed) {
47
48
  return { daclPresent: parsed.daclPresent, aces };
48
49
  }
49
50
  export async function inspectWindowsOwner(params) {
51
+ if (hasWindowsPathAlias(params.targetPath, "filesystem", "win32")) {
52
+ const error = new Error("Path uses a Windows filesystem namespace alias");
53
+ return { error: String(error), errorCause: error };
54
+ }
50
55
  let command = "";
51
56
  let startedAt = performance.now();
52
57
  try {
@@ -0,0 +1,39 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ export type WindowsPathAliasKind = "filesystem" | "relative";
3
+ /**
4
+ * Capture an ordinary Windows drive-relative path without normalizing its raw
5
+ * suffix. This is only for public APIs whose existing contract accepts such
6
+ * paths; callers must still run namespace-alias admission on the result.
7
+ */
8
+ export declare function anchorWindowsDriveRelativePath(value: string): string;
9
+ /**
10
+ * Resolve a path without letting Node erase the separator from an exact
11
+ * extended-length drive root such as `\\?\C:\`. Bare `\\?\C:` input remains
12
+ * unchanged so the surrounding alias admission rejects it.
13
+ */
14
+ export declare function resolvePathPreservingWindowsRoot(value: string): string;
15
+ /**
16
+ * Preserve a namespaced drive root after a caller has already resolved the
17
+ * input. This lets admission fast paths keep exactly one live path.resolve
18
+ * call while retaining the same root-repair behavior as the general helper.
19
+ */
20
+ export declare function repairResolvedWindowsRoot(value: string, resolved: string): string;
21
+ /**
22
+ * Resolve path segments against a base while preserving a namespaced drive
23
+ * root when Node normalizes a legitimate rooted input back to that root.
24
+ * Raw bare namespace drives stay bare so admission checks still reject them.
25
+ */
26
+ export declare function resolvePathFromBasePreservingWindowsRoot(base: string, ...segments: string[]): string;
27
+ /**
28
+ * Adapt an admitted namespaced drive root for Node's Windows filesystem layer.
29
+ * Node removes the root separator from these paths during filesystem dispatch,
30
+ * so use the equivalent ordinary drive root for the operation. This is not an
31
+ * admission check: callers must validate attacker-controlled input first.
32
+ */
33
+ export declare function pathForWindowsFilesystem(value: string): string;
34
+ /** Returns true when a Windows pathname can address an alternate filesystem namespace. */
35
+ export declare function hasWindowsPathAlias(value: string, kind: WindowsPathAliasKind, platform?: NodeJS.Platform | string): boolean;
36
+ export declare function assertNoWindowsPathAliasForPlatform(value: string, kind: WindowsPathAliasKind, message: string, platform: NodeJS.Platform | string | undefined): void;
37
+ export declare function assertNoWindowsPathAlias(value: string, kind?: WindowsPathAliasKind, message?: string, platform?: NodeJS.Platform | string): void;
38
+ export declare function isWindowsPathAliasError(error: unknown): error is FsSafeError;
39
+ //# sourceMappingURL=windows-path-alias.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windows-path-alias.d.ts","sourceRoot":"","sources":["../src/windows-path-alias.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,UAAU,CAAC;AAsD7D;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYpE;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUtE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUjF;AAED;;;;GAIG;AACH,wBAAgB,wCAAwC,CACtD,IAAI,EAAE,MAAM,EACZ,GAAG,QAAQ,EAAE,MAAM,EAAE,GACpB,MAAM,CAgBR;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAkB9D;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,oBAAoB,EAC1B,QAAQ,GAAE,MAAM,CAAC,QAAQ,GAAG,MAAyB,GACpD,OAAO,CAMT;AAED,wBAAgB,mCAAmC,CACjD,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,oBAAoB,EAC1B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,MAAM,GAAG,SAAS,GAC7C,IAAI,CAMN;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,oBAAmC,EACzC,OAAO,SAAmD,EAC1D,QAAQ,GAAE,MAAM,CAAC,QAAQ,GAAG,MAAyB,GACpD,IAAI,CAMN;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAE5E"}
@@ -0,0 +1,153 @@
1
+ import path from "node:path";
2
+ import { FsSafeError } from "./errors.js";
3
+ const COLON = 0x3a;
4
+ const FORWARD_SLASH = 0x2f;
5
+ const BACKSLASH = 0x5c;
6
+ const DOT = 0x2e;
7
+ const QUESTION_MARK = 0x3f;
8
+ function isAsciiLetter(code) {
9
+ return (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a);
10
+ }
11
+ function isSeparator(code) {
12
+ return code === FORWARD_SLASH || code === BACKSLASH;
13
+ }
14
+ function rootedDriveColonIndex(value) {
15
+ if (value.length >= 3 &&
16
+ isAsciiLetter(value.charCodeAt(0)) &&
17
+ value.charCodeAt(1) === COLON &&
18
+ isSeparator(value.charCodeAt(2))) {
19
+ return 1;
20
+ }
21
+ if (value.length >= 7 &&
22
+ isSeparator(value.charCodeAt(0)) &&
23
+ isSeparator(value.charCodeAt(1)) &&
24
+ (value.charCodeAt(2) === QUESTION_MARK || value.charCodeAt(2) === DOT) &&
25
+ isSeparator(value.charCodeAt(3)) &&
26
+ isAsciiLetter(value.charCodeAt(4)) &&
27
+ value.charCodeAt(5) === COLON &&
28
+ isSeparator(value.charCodeAt(6))) {
29
+ return 5;
30
+ }
31
+ return -1;
32
+ }
33
+ function isBareWindowsNamespaceDrive(value) {
34
+ return (value.length === 6 &&
35
+ isSeparator(value.charCodeAt(0)) &&
36
+ isSeparator(value.charCodeAt(1)) &&
37
+ (value.charCodeAt(2) === QUESTION_MARK || value.charCodeAt(2) === DOT) &&
38
+ isSeparator(value.charCodeAt(3)) &&
39
+ isAsciiLetter(value.charCodeAt(4)) &&
40
+ value.charCodeAt(5) === COLON);
41
+ }
42
+ /**
43
+ * Capture an ordinary Windows drive-relative path without normalizing its raw
44
+ * suffix. This is only for public APIs whose existing contract accepts such
45
+ * paths; callers must still run namespace-alias admission on the result.
46
+ */
47
+ export function anchorWindowsDriveRelativePath(value) {
48
+ if (process.platform !== "win32" || path.isAbsolute(value))
49
+ return value;
50
+ if (value.length < 2 ||
51
+ !isAsciiLetter(value.charCodeAt(0)) ||
52
+ value.charCodeAt(1) !== COLON) {
53
+ return value;
54
+ }
55
+ const drive = value.slice(0, 2);
56
+ const base = path.resolve(drive);
57
+ return `${base}${path.sep}${value.slice(2)}`;
58
+ }
59
+ /**
60
+ * Resolve a path without letting Node erase the separator from an exact
61
+ * extended-length drive root such as `\\?\C:\`. Bare `\\?\C:` input remains
62
+ * unchanged so the surrounding alias admission rejects it.
63
+ */
64
+ export function resolvePathPreservingWindowsRoot(value) {
65
+ if (value.length === 7 &&
66
+ process.platform === "win32" &&
67
+ rootedDriveColonIndex(value) === 5) {
68
+ return value.includes("/") ? value.replaceAll("/", "\\") : value;
69
+ }
70
+ const resolved = path.resolve(value);
71
+ return repairResolvedWindowsRoot(value, resolved);
72
+ }
73
+ /**
74
+ * Preserve a namespaced drive root after a caller has already resolved the
75
+ * input. This lets admission fast paths keep exactly one live path.resolve
76
+ * call while retaining the same root-repair behavior as the general helper.
77
+ */
78
+ export function repairResolvedWindowsRoot(value, resolved) {
79
+ if (resolved.length === 6 &&
80
+ process.platform === "win32" &&
81
+ isBareWindowsNamespaceDrive(resolved) &&
82
+ !hasWindowsPathAlias(value, "filesystem")) {
83
+ return `${resolved}\\`;
84
+ }
85
+ return resolved;
86
+ }
87
+ /**
88
+ * Resolve path segments against a base while preserving a namespaced drive
89
+ * root when Node normalizes a legitimate rooted input back to that root.
90
+ * Raw bare namespace drives stay bare so admission checks still reject them.
91
+ */
92
+ export function resolvePathFromBasePreservingWindowsRoot(base, ...segments) {
93
+ const resolved = path.resolve(base, ...segments);
94
+ if (resolved.length !== 6 ||
95
+ process.platform !== "win32" ||
96
+ !isBareWindowsNamespaceDrive(resolved)) {
97
+ return resolved;
98
+ }
99
+ if (hasWindowsPathAlias(base, "filesystem") ||
100
+ segments.some((segment) => hasWindowsPathAlias(segment, "filesystem"))) {
101
+ return resolved;
102
+ }
103
+ return `${resolved}\\`;
104
+ }
105
+ /**
106
+ * Adapt an admitted namespaced drive root for Node's Windows filesystem layer.
107
+ * Node removes the root separator from these paths during filesystem dispatch,
108
+ * so use the equivalent ordinary drive root for the operation. This is not an
109
+ * admission check: callers must validate attacker-controlled input first.
110
+ */
111
+ export function pathForWindowsFilesystem(value) {
112
+ if (process.platform !== "win32" ||
113
+ rootedDriveColonIndex(value) !== 5) {
114
+ return value;
115
+ }
116
+ if (value.length === 7) {
117
+ return `${value[4]}:\\`;
118
+ }
119
+ const resolved = path.resolve(value);
120
+ if (isBareWindowsNamespaceDrive(resolved) &&
121
+ !hasWindowsPathAlias(value, "filesystem")) {
122
+ return `${resolved[4]}:\\`;
123
+ }
124
+ return value;
125
+ }
126
+ /** Returns true when a Windows pathname can address an alternate filesystem namespace. */
127
+ export function hasWindowsPathAlias(value, kind, platform = process.platform) {
128
+ if (platform !== "win32")
129
+ return false;
130
+ const firstColon = value.indexOf(":");
131
+ if (firstColon === -1)
132
+ return false;
133
+ if (kind === "relative")
134
+ return true;
135
+ return firstColon !== rootedDriveColonIndex(value) || value.indexOf(":", firstColon + 1) !== -1;
136
+ }
137
+ export function assertNoWindowsPathAliasForPlatform(value, kind, message, platform) {
138
+ if (hasWindowsPathAlias(value, kind, platform)) {
139
+ throw new FsSafeError("invalid-path", message, {
140
+ details: { reason: "windows-path-alias" },
141
+ });
142
+ }
143
+ }
144
+ export function assertNoWindowsPathAlias(value, kind = "filesystem", message = "path uses a Windows filesystem namespace alias", platform = process.platform) {
145
+ if (hasWindowsPathAlias(value, kind, platform)) {
146
+ throw new FsSafeError("invalid-path", message, {
147
+ details: { reason: "windows-path-alias" },
148
+ });
149
+ }
150
+ }
151
+ export function isWindowsPathAliasError(error) {
152
+ return error instanceof FsSafeError && error.details?.reason === "windows-path-alias";
153
+ }
package/docs/advanced.md CHANGED
@@ -32,7 +32,9 @@ The exports group into a handful of themes. Each documented helper has its own p
32
32
  | `resolveWritablePathWithinRoot` | – | Resolve a write target inside a root. |
33
33
  | `resolveRootPath`, `resolveRootPathSync`, `ResolvedRootPath`, `ROOT_PATH_ALIAS_POLICIES`, `RootPathAliasPolicy` | – | Resolve a root directory honoring alias policy. |
34
34
  | `resolvePathViaExistingAncestorSync` | – | Walk to an existing ancestor for paths whose tail does not yet exist. |
35
+ | `resolvePathPrefixSync`, `ResolvedPathPrefix` | [path-prefix.md](path-prefix.md) | Follow physical symlink targets and return a canonical existing prefix with the raw missing suffix; propagate uncertain resolution failures. |
35
36
  | `probePathCaseInsensitiveSync`, `ProbePathCaseOptions` | [path-case.md](path-case.md) | Observe local ASCII-case behavior with explicit read-only mode and owned temporary-probe cleanup. |
37
+ | `probePathSuffixAliasesSync`, `ProbePathSuffixAliasesOptions` | [path-suffix-aliases.md](path-suffix-aliases.md) | Observe selected missing suffix aliases with bounded temporary directory probes; ambiguity, dynamic budget exhaustion, or incomplete cleanup returns `undefined`. |
36
38
 
37
39
  `ensureDirectoryWithinRoot({ rootDir, requestedPath, scopeLabel, defaultDirName?, mode? })`
38
40
  returns `{ ok: true, path }` or `{ ok: false, error: string, diagnostic?: FsSafeError }`.
@@ -77,6 +79,11 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
77
79
  | `assertNoSymlinkParents`, `assertNoSymlinkParentsSync`, `AssertNoSymlinkParentsOptions` | – | Reject paths whose ancestor chain contains symlinks. |
78
80
  | `assertNoHardlinkedFinalPath`, `assertNoPathAliasEscape`, `PATH_ALIAS_POLICIES`, `PathAliasPolicy` | – | Hardlink/alias defense building blocks. |
79
81
 
82
+ `pathExists()` and `pathExistsSync()` intentionally retain ordinary `stat`
83
+ semantics and are not caller-path admission boundaries. Validate an untrusted
84
+ path with the boundary appropriate to the operation before using these
85
+ existence probes.
86
+
80
87
  `openRootFile()` and `openRootFileSync()` compare exact bigint identities before
81
88
  open, on the retained descriptor, and on the current resolved path. Their `stat`
82
89
  receipts remain numeric. Custom `ioFs` adapters must honor `{ bigint: true }` for
@@ -84,6 +91,24 @@ receipts remain numeric. Custom `ioFs` adapters must honor `{ bigint: true }` fo
84
91
  Windows identities receive one re-inspection without reopening, then fail
85
92
  validation if still unknown.
86
93
 
94
+ On Windows, these existing-object readers retain their historical support for a
95
+ leading drive-relative spelling such as `C:existing.txt`: it is anchored to that
96
+ drive before confinement and namespace admission. Additional colons remain in
97
+ the anchored spelling, so alternate-stream and directory-index aliases are still
98
+ rejected before opening.
99
+
100
+ These adapters also capture the canonical root directory's exact bigint identity
101
+ before component traversal. Immediately before transferring descriptor ownership,
102
+ they check that root, freshly canonicalize the consumed pathname, admit the fresh
103
+ spelling under the captured root, compare a no-follow canonical-leaf observation
104
+ with the retained descriptor, and check the root again. Boundary or identity drift
105
+ is a validation failure and the descriptor is closed. A custom `ioFs` supplies
106
+ these observations; the built-in adapter uses fs-safe's native realpath wrapper.
107
+ On Windows, the built-in adapter binds native root spelling before traversal,
108
+ including supplied `rootRealPath`, and returns that spelling in its root receipt.
109
+ This is an operation-local detection fence, not atomic confinement against a peer
110
+ that can keep racing pathname bindings.
111
+
87
112
  The explicit `symlinks` policy takes precedence over the existing `rejectSymlinks`
88
113
  boolean. Without `symlinks`, `rejectSymlinks: false` retains its existing behavior
89
114
  of following contained links, and omission still rejects all symlink components.
package/docs/archive.md CHANGED
@@ -139,7 +139,23 @@ accepted-entry plan back to Rust. Rust owns raw-stream admission, decompression,
139
139
  and fd-relative `mkdirBeneath`/exclusive-open writes. This keeps filter policy identical
140
140
  between native and JavaScript paths rather than reimplementing it in Rust.
141
141
 
142
- 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 separator and dot-component equivalence is allowed only after validation. 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.
142
+ 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.
143
+
144
+ ZIP admission establishes each entry's kind before callbacks: a high-word UNIX
145
+ symlink type takes precedence regardless of creator, followed by the DOS directory
146
+ bit, the exact UNIX directory type, or a terminal slash/backslash. Native manifests
147
+ must agree with this kind, physical index, size, known path, and UNIX-creator mode
148
+ before extraction or any member read. Bounded ZIP reads retain this metadata from
149
+ their single admission pass without another input copy or scan.
150
+
151
+ Portable ZIP preflight, extraction, and reads also check the decoded kind against
152
+ admission. Unsupported JSZip metadata rejects with `ArchiveFormatError` before
153
+ filters, including UNIX-only directory attributes without a terminal slash or DOS
154
+ directory bit, backslash-only directory names without directory attributes, and
155
+ non-UNIX creators whose high-word symlink mode JSZip does not expose. Symlinks that
156
+ the decoder represents faithfully remain subject to the existing filter and
157
+ blocked-link policy. UNIX creator metadata and permission defaults remain as
158
+ described above.
143
159
 
144
160
  Within one ZIP entry, identical local and central name bytes reuse the same
145
161
  decoded validation. Unicode Path admission is shared only when both the raw names
@@ -232,7 +248,7 @@ omitted and do not consume output payload budgets. The shared core admits these
232
248
  record and are then cleared; local PAX on unsupported types and GNU sparse
233
249
  `S` records retain their existing fail-closed format policy.
234
250
 
235
- If `kind` is omitted, the helper calls `resolveArchiveKind(archivePath)` and throws if the extension is not recognized. Pass `kind` explicitly when the archive name doesn't carry the type (e.g. content-addressed names). Archive inputs must remain regular files from preview through descriptor admission; POSIX opens are no-follow and nonblocking, so a FIFO swap cannot stall before deadline checks resume. A positive finite `timeoutMs` is a wall-clock budget; zero, negative, `NaN`, and infinity disable the deadline. Non-mutating work rejects promptly when the budget expires. If a live destination mutation is already in flight, rejection waits only for that mutation and any rollback to finish; no later destination mutation can begin.
251
+ If `kind` is omitted, the helper calls `resolveArchiveKind(archivePath)` and throws if the extension is not recognized. Pass `kind` explicitly when the archive name doesn't carry the type (e.g. content-addressed names). Archive inputs must remain regular files from preview through descriptor admission; POSIX opens are no-follow and nonblocking, so a FIFO swap cannot stall before deadline checks resume. On Windows, archive source and destination filesystem paths reject NTFS alternate-stream and directory-index namespace spellings such as `file:stream` and `dir::$INDEX_ALLOCATION`; ordinary colon-bearing POSIX names remain valid. A positive finite `timeoutMs` is a wall-clock budget; zero, negative, `NaN`, and infinity disable the deadline. Non-mutating work rejects promptly when the budget expires. If a live destination mutation is already in flight, rejection waits only for that mutation and any rollback to finish; no later destination mutation can begin.
236
252
 
237
253
  Extraction captures the destination's lossless filesystem identity before any
238
254
  entry filter runs and retains that capability through final publication. If a
@@ -321,6 +337,11 @@ codes remain `"destination-not-directory"`, `"destination-symlink"`, and
321
337
  - **Slow-loris archives:** `timeoutMs` is a hard wall-clock budget for non-mutating work. Extraction is aborted on overrun; if a destination mutation is already in flight, that mutation and rollback are joined before rejection so archive-controlled publication cannot continue afterward.
322
338
  - **Metadata bombs:** a streaming pass-through reader rejects oversized PAX, GNU long-name, and GNU long-link bodies before buffering their bodies. It understands octal and base-256 fixed sizes and validates bounded local PAX bodies before using their size overrides for member framing. Original archive bytes remain unchanged.
323
339
 
340
+ Native gzip, zstd, and bzip2 readers check cancellation before refilling
341
+ compressed input and before each decoded read, including buffered output. These checks
342
+ apply to file extraction and in-memory member reads; they cannot interrupt an
343
+ already-running filesystem read or a decoder step using already-buffered input.
344
+
324
345
  ### Raw TAR framing
325
346
 
326
347
  Extraction and bounded reads admit the complete decoded TAR stream through the
package/docs/atomic.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Atomic writes
2
2
 
3
- `@openclaw/fs-safe/atomic` re-exports the lower-level helpers that `root()`'s write methods are built on. Reach for them when you have an absolute path you trust and want sibling-temp + rename without setting up a `Root`, or when you need finer control over `fsync`, mode preservation, or pre-rename hooks.
3
+ `@openclaw/fs-safe/atomic` re-exports the lower-level helpers that `root()`'s write methods are built on. Reach for them when you have a path you trust and want sibling-temp + rename without setting up a `Root`, or when you need finer control over `fsync`, mode preservation, or pre-rename hooks.
4
4
 
5
5
  ```ts
6
6
  import {
@@ -20,6 +20,14 @@ On POSIX, the parent is opened with no-follow and directory-only flags, checked
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
 
23
+ Relative paths retain their literal suffix so `.` and `..` keep Node's existing
24
+ filesystem semantics. On Windows, an ordinary drive-relative destination such
25
+ as `C:.\\state.json` is captured at entry by anchoring the drive's current
26
+ directory without normalizing that suffix. The anchored absolute spelling is
27
+ used for staging, locks, callbacks, publication, and cleanup. Any additional
28
+ colon remains visible and is rejected as a filesystem namespace alias before
29
+ I/O; malformed namespace-drive spellings remain rejected.
30
+
23
31
  ```ts
24
32
  import { replaceFileAtomic } from "@openclaw/fs-safe/atomic";
25
33
 
@@ -174,6 +182,8 @@ await replaceDirectoryAtomic({
174
182
  The helper renames `targetDir` to a generated backup path, renames `stagedDir → targetDir`, then removes the backup. If the second rename fails, it tries to restore the original target before rethrowing.
175
183
  Concurrent replacements of the same resolved target are serialized inside the
176
184
  current process so their backup, commit, and cleanup phases cannot interleave.
185
+ On Windows, ordinary drive-relative staged and target paths are anchored at
186
+ entry before namespace-alias admission and resolution.
177
187
  `backupPrefix`, when supplied, is sanitized as one path prefix and cannot contain
178
188
  path separators or NUL bytes; the generated backup tail is randomized.
179
189
 
@@ -248,6 +258,8 @@ await movePathWithCopyFallback({
248
258
  ```
249
259
 
250
260
  Use it when source and destination might live on different filesystems (containers, tmpfs, separate volumes).
261
+ On Windows, ordinary drive-relative `from` and `to` paths are anchored at entry;
262
+ publication receipts report the resulting absolute destination.
251
263
  The hardlink policy is captured when the move starts. Changing or reusing the
252
264
  options object later does not change the policy of an in-flight move.
253
265
  `sourceHardlinks: "reject"` performs a recursive preflight capped at 50,000
@@ -260,7 +272,19 @@ the preflight cap fails with `FsSafeError("too-large")`.
260
272
  If another writer changes source entries during the fallback, the staged copy
261
273
  throws `ESTALE` before commit when possible. If the destination has already
262
274
  been committed, cleanup still preserves the changed source entries and throws
263
- `ESTALE`. When allowed source names are hardlinks to the same inode, each owned
275
+ `ESTALE`. Directory manifests retain an exact bigint device/inode receipt from
276
+ copy admission. Each directory is rechecked after traversal, and the source root
277
+ is checked again before publication. Cleanup checks the same receipt before
278
+ removing children, then invokes mutation authority and rechecks the receipt and
279
+ directory type immediately before removal. Unknown Windows identity
280
+ components get at most one retry that retains known components; persistent
281
+ ambiguity fails closed before further removal. A directory that disappeared or
282
+ was replaced during child cleanup is reported as stale; an observed replacement
283
+ is preserved. Unrelated children
284
+ added to the original directory are preserved while unchanged copied children
285
+ are still removed. These pathname checks remain best-effort: they cannot make
286
+ the final identity check and removal atomic against another process.
287
+ When allowed source names are hardlinks to the same inode, each owned
264
288
  unlink is verified through a remaining manifested alias and its exact resulting
265
289
  identity becomes the next cleanup receipt. This accounts for the operation's
266
290
  own link-count and ctime changes without suppressing unexpected external
@@ -337,7 +361,7 @@ preserves the existing move and source-identity behavior.
337
361
 
338
362
  | `Root` methods | `atomic` helpers |
339
363
  |---|---|
340
- | Take relative paths, bound to a `rootDir`. | Take absolute paths, no boundary. |
364
+ | Take relative paths, bound to a `rootDir`. | Take trusted absolute or relative paths, no boundary. |
341
365
  | Throw `FsSafeError` with `code`. | Throw `FsSafeError` *or* the underlying `NodeJS.ErrnoException`, depending on failure point. |
342
366
  | Atomicity, mode, hooks, fsync are sane defaults. | Caller controls all of the above. |
343
367
  | `mkdir`, identity check, hardlink reject built in. | No root boundary; `movePathWithCopyFallback` has explicit `sourceHardlinks` policy, while other helpers expose their own narrower checks. |
package/docs/copy.md CHANGED
@@ -97,6 +97,9 @@ const bytes = await copyFileHandle(sourceHandle, targetHandle, {
97
97
 
98
98
  `CopyFileHandleOptions` contains optional `maxBytes`, `signal`, `onChunk`, and
99
99
  `assertBeforeMutation`. The result is the actual byte count copied through EOF.
100
+ The four options are selected once before descriptor inspection; later mutation
101
+ of the options object cannot replace the active budget, signal, observer, or
102
+ mutation-authority callback.
100
103
  The byte limit is not a prefix length: excess data rejects with `too-large`,
101
104
  including data added after admission. Omitted limits are unlimited; Root's
102
105
  default read cap does not apply. Zero accepts only an empty source. Invalid
@@ -53,6 +53,14 @@ propagate.
53
53
  discard both unsupported outcomes and failures. Use them only when the primary
54
54
  write remains useful without a crash-durability promise.
55
55
 
56
+ On Windows, pathname inputs and supplied directory receipts reject NTFS
57
+ alternate-stream and directory-index namespace spellings before opening,
58
+ creating, hashing, or publishing anything. This applies to directory
59
+ durability, `publishFileExclusive()`, and the pathname overloads of
60
+ `sha256File()` and `sha256FileSync()`; the already-open `FileHandle` and
61
+ borrowed numeric file-descriptor overloads are unchanged. Ordinary colon-bearing
62
+ POSIX paths remain valid.
63
+
56
64
  ## Pinned directories
57
65
 
58
66
  `pinDirectory()` rejects final symlinks and non-directories. On POSIX it opens
@@ -94,6 +102,12 @@ target. It pins the source with nonblocking `O_NOFOLLOW`, optionally verifies
94
102
  `expectedSourceIdentity`, tries a hardlink first, then synchronizes the target
95
103
  parent directory.
96
104
 
105
+ Trusted relative source and target paths are resolved to absolute paths before
106
+ authority checks. On Windows, ordinary drive-relative operands are anchored at
107
+ entry before namespace-alias admission. A caller-supplied `parentReceipt` must
108
+ still name the resolved target parent and is not relaxed by this compatibility
109
+ rule.
110
+
97
111
  For example, a backup archive is complete before publication. If directory
98
112
  sync fails, keeping that complete file is more useful than conditionally
99
113
  deleting it by pathname:
package/docs/errors.md CHANGED
@@ -38,6 +38,14 @@ class FsSafeError extends Error {
38
38
 
39
39
  `cause` is available through the standard `Error` `cause` property when the failure was triggered by a `NodeJS.ErrnoException` (e.g. a wrapped `EACCES`). Inspect it for the original `code` / `errno` / `syscall` if you need finer-grained reporting.
40
40
 
41
+ Guarded write preparation describes permission, read-only filesystem, and disk-space
42
+ failures with messages such as `permission denied (EACCES)` or
43
+ `no space left on device (ENOSPC)`. Other errno failures include their code in
44
+ `filesystem write failed (EIO)`. These wrappers retain the existing `invalid-path`
45
+ code and `policy` category for compatibility, along with the original `cause`;
46
+ they do not expose native message text or paths. Already-classified `FsSafeError`
47
+ instances and missing-path errors keep their existing classification.
48
+
41
49
  `details` is an operation-specific receipt, not an alternate error code. For
42
50
  example, `publishFileExclusive()` uses it to report the failing phase, created
43
51
  target identity, cleanup decision, and failed directory-sync outcome. Narrow
@@ -114,7 +122,7 @@ type FsSafeErrorCode =
114
122
  | `device-path` | A read/open target is a known unsafe device or process-fd path. | `/dev/zero`, `/dev/random`, `/dev/stdin`, `/dev/fd/*`, `/proc/*/fd/*`, or a Windows reserved device name. |
115
123
  | `hardlink` | Read or copy with `hardlinks: "reject"` saw `nlink > 1`. | File is hardlinked — possibly an alias of an out-of-tree inode. |
116
124
  | `helper-failed` | A native mechanism or multi-step operational helper failed. | Inspect `cause` and any operation-specific `details`; retrying may be unsafe if the operation partially completed. |
117
- | `helper-unavailable` | Required native binding could not be loaded. | Unsupported platform, omitted/missing/incompatible platform package, or `FS_SAFE_NATIVE_MODE=require`. `auto` falls back where possible; `require` fails closed. |
125
+ | `helper-unavailable` | A required native binding or bounded primitive could not be loaded. | Unsupported platform, omitted/missing/incompatible platform package, `FS_SAFE_NATIVE_MODE=off`, or a no-clobber `Root.move()` without safe native parent admission. `auto` falls back only where a safe fallback exists. |
118
126
  | `insecure-permissions` | A secure file or path permission check found a mode/ACL that allows broader access than requested. | File or directory is group/world writable/readable; Windows ACL grants broad read. |
119
127
  | `invalid-path` | Input was empty, contained NUL, was an unparseable URL, or otherwise unusable; a FileStore key used a noncanonical spelling. | Noncanonical FileStore aliases, backslashes, or complete parent segments; a network path on Windows; a drive-relative segment in a portable relative path or store key; or a leading drive-relative spelling such as `C:name` used as a Root destination. Existing-object Root lookups retain broader confined path compatibility, including legal POSIX drive-like names. |
120
128
  | `not-empty` | Nonrecursive `remove()` on a non-empty directory, or new children appeared during recursive removal. | Use bounded `recursive: true` removal or coordinate concurrent writers. |
@@ -136,11 +144,11 @@ type FsSafeErrorCode =
136
144
 
137
145
  Secret writes reject invalid `mode` / `dirMode` values with `invalid-path` before directory creation. Existing secret directories with a mode different from the requested `dirMode` report `insecure-permissions` without chmod; a created directory whose descriptor ownership no longer matches its initializing effective user reports `not-owned`.
138
146
 
139
- Pathname `sha256File()` also reports `path-mismatch` when pre-open, descriptor,
140
- or current-path identity remains unknown after one bounded Windows retry, even
141
- if the file is benign. It never reopens to recover identity. Preview symlinks
142
- report `symlink`, preview or descriptor non-files report `not-file`, and a
143
- current-path symlink or non-file reports `path-mismatch`.
147
+ Pathname `sha256File()` and `sha256FileSync()` also report `path-mismatch` when
148
+ pre-open, descriptor, or current-path identity remains unknown after one bounded
149
+ Windows retry, even if the file is benign. Neither reopens to recover identity.
150
+ Preview symlinks report `symlink`, preview or descriptor non-files report
151
+ `not-file`, and a current-path symlink or non-file reports `path-mismatch`.
144
152
 
145
153
  ## Branching
146
154
 
@@ -90,13 +90,16 @@ or converts one caller-supplied key onto another:
90
90
  are rejected.
91
91
  - Windows drive-relative segments such as `C:name` or `C:` are rejected
92
92
  anywhere in a key, including `a/C:name`.
93
+ - On Windows, every other colon is rejected too, preventing a key from naming
94
+ an NTFS alternate stream or directory-index alias. POSIX keeps accepting
95
+ ordinary colon-bearing segments that are not drive-relative spellings.
93
96
  - No segment may end in an ASCII dot or space.
94
97
 
95
98
  Violations report `invalid-path` when key validation is reached. Ordinary nested
96
99
  keys, NFC Unicode such as `café/日本語.txt`, `.hidden`, `a..b`, and internal spaces
97
- such as `internal space/a b.txt` are accepted. Colons elsewhere, such as the
98
- timestamp in `logs/2026-08-02T10:30:00Z.log`, remain lexically valid; filesystem
99
- success still depends on the platform and the underlying Root policies.
100
+ such as `internal space/a b.txt` are accepted. On POSIX, colons elsewhere, such
101
+ as the timestamp in `logs/2026-08-02T10:30:00Z.log`, remain lexically valid;
102
+ Windows rejects that spelling as stream syntax.
100
103
 
101
104
  Validation retains each method's operation order. Async reads, `exists`, and
102
105
  `remove` open the root first: if the root is missing, strict methods report
@@ -136,6 +139,24 @@ its exact publication identity cannot be verified. There is no equal-content
136
139
  fallback. The published entry remains present, so callers must inspect or
137
140
  recover that outcome instead of assuming the write did not occur.
138
141
 
142
+ Synchronous directory creation retains exact bigint receipts for the store root
143
+ and every parent component. On POSIX, an existing or newly created directory
144
+ whose complete requested mode differs is reopened without following the final
145
+ name, checked against its receipt and parent chain, and finalized through that
146
+ descriptor. A concurrent root or parent replacement is rejected without
147
+ applying the mode to the replacement. Directories already at the requested mode
148
+ skip the descriptor and mode operation. Windows retains its bounded `mkdir`
149
+ mode request and identity checks without relying on directory descriptors or a
150
+ pathname `chmod`, because Node does not enforce POSIX directory modes there.
151
+
152
+ Node does not expose a portable, `fchmod`-capable search-only directory
153
+ descriptor on Linux. If a mismatched existing directory, or one created under
154
+ an owner-read-removing umask, cannot be opened for reading, the synchronous
155
+ store therefore fails closed with `permission-unverified`; it never falls back
156
+ to pathname `chmod`. On supported macOS x64/arm64 hosts it also tries an
157
+ `O_SEARCH` descriptor, so owner-searchable directories can still be repaired.
158
+ Directories with neither usable read nor search access remain fail-closed.
159
+
139
160
  | Method | Durability support |
140
161
  |---|---|
141
162
  | `write`, `writeText`, `writeJson` (async and sync) | Per-call option overrides store default. |
package/docs/filename.md CHANGED
@@ -17,22 +17,27 @@ function sanitizeUntrustedFileName(fileName: string, fallbackName: string): stri
17
17
 
18
18
  ## What it does
19
19
 
20
- In order:
20
+ The primary name goes through this pipeline first:
21
21
 
22
- 1. **Trim** whitespace. If the result is empty, return `fallbackName`.
22
+ 1. **Trim** whitespace. An empty result is unusable.
23
23
  2. **Strip path components.** Apply `path.posix.basename` then `path.win32.basename` so neither `foo/bar.txt` nor `foo\bar.txt` survives — only the final segment remains.
24
24
  3. **Strip non-portable characters.** C0/C1 controls (`0x00`–`0x1f`, `0x7f`–`0x9f`) and the Windows-invalid set `< > : " / \\ | ? *` are removed on every platform.
25
25
  4. **Trim again.**
26
- 5. If the result is empty, `"."`, or `".."`, return `fallbackName`.
27
- 6. **Suffix Windows reserved basenames.** Compare the part before the first `.` case-insensitively with the Windows device-name set, including `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`, `CONOUT$`, `COM1..9`, `LPT1..9`, and their superscript `¹`, `²`, and `³` variants. Windows-ignored spaces and dots at the end of that basename do not disguise a device name. A match gains `_` before its extension, preserving the original case and extension on every platform.
28
- 7. **Truncate.** If the cleaned segment is longer than 200 UTF-16 code units, take up to the first 200 without splitting a valid Unicode surrogate pair.
26
+ 5. An empty result, `"."`, or `".."` is unusable.
27
+ 6. **Truncate.** If the cleaned segment is longer than 200 UTF-16 code units, take up to the first 200 without splitting a valid Unicode surrogate pair.
28
+ 7. **Make the final name device-safe.** Compare the part before the first `.` case-insensitively with the Windows device-name set, including `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`, `CONOUT$`, `COM1..9`, `LPT1..9`, and their superscript `¹`, `²`, and `³` variants. Windows-ignored spaces and dots at the end of that basename do not disguise a device name. A match gains `_` before its extension on every platform. If the suffix would exceed 200 code units, the unsuffixed tail is shortened first, so truncation cannot recreate a device name.
29
+
30
+ Only when the primary name is unusable does `fallbackName` go through the same
31
+ nonrecursive pipeline. A safe fallback is preserved exactly; path components,
32
+ controls, reserved device names, and overlong fallback names receive the same
33
+ treatment as the primary name. If both candidates are unusable, the function
34
+ returns the fixed safe literal `"file"`.
29
35
 
30
36
  If truncation itself exposes a reserved-device basename after Windows ignores
31
37
  trailing spaces or dots, the result is shortened once more and receives the
32
38
  same underscore suffix. A name that reaches the sanitization branch therefore
33
39
  remains at most 200 UTF-16 code units and is never a Windows reserved-device
34
- alias. `fallbackName` is returned verbatim for empty or path-alias input, so
35
- callers must supply a fallback that already satisfies their filename policy.
40
+ alias. Fallback names pass through the same checks before they can be returned.
36
41
 
37
42
  That's it. The function stays intentionally small: it removes traversal and
38
43
  the most obvious cross-platform device and character hazards, but it is not a
@@ -48,6 +53,8 @@ sanitizeUntrustedFileName("a\u0000b\tc", "upload"); // "abc"
48
53
  sanitizeUntrustedFileName(" ", "fallback"); // "fallback"
49
54
  sanitizeUntrustedFileName(".", "fallback"); // "fallback"
50
55
  sanitizeUntrustedFileName("..", "fallback"); // "fallback"
56
+ sanitizeUntrustedFileName("<>", "../../etc/passwd"); // "passwd"
57
+ sanitizeUntrustedFileName("<>", "../.."); // "file"
51
58
  sanitizeUntrustedFileName("CON", "fallback"); // "CON_"
52
59
  sanitizeUntrustedFileName("nul.txt", "fallback"); // "nul_.txt"
53
60
  sanitizeUntrustedFileName("aux.c", "fallback"); // "aux_.c"