@openclaw/fs-safe 0.11.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 (416) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +15 -8
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +32 -11
  5. package/dist/advanced.d.ts +3 -1
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +3 -1
  8. package/dist/archive-entry.d.ts.map +1 -1
  9. package/dist/archive-entry.js +4 -3
  10. package/dist/archive-gzip-tail.d.ts.map +1 -1
  11. package/dist/archive-gzip-tail.js +13 -9
  12. package/dist/archive-input.d.ts.map +1 -1
  13. package/dist/archive-input.js +6 -2
  14. package/dist/archive-merge.d.ts.map +1 -1
  15. package/dist/archive-merge.js +15 -2
  16. package/dist/archive-native.d.ts.map +1 -1
  17. package/dist/archive-native.js +7 -19
  18. package/dist/archive-plan.js +1 -1
  19. package/dist/archive-read.d.ts.map +1 -1
  20. package/dist/archive-read.js +55 -46
  21. package/dist/archive-staging.d.ts.map +1 -1
  22. package/dist/archive-staging.js +43 -10
  23. package/dist/archive-tar-inspect.d.ts.map +1 -1
  24. package/dist/archive-tar-inspect.js +4 -1
  25. package/dist/archive-zip-admission.d.ts +1 -1
  26. package/dist/archive-zip-admission.d.ts.map +1 -1
  27. package/dist/archive-zip-admission.js +2 -2
  28. package/dist/archive-zip-directory.d.ts +3 -0
  29. package/dist/archive-zip-directory.d.ts.map +1 -1
  30. package/dist/archive-zip-directory.js +13 -2
  31. package/dist/archive-zip-loader.d.ts +3 -2
  32. package/dist/archive-zip-loader.d.ts.map +1 -1
  33. package/dist/archive-zip-loader.js +30 -4
  34. package/dist/archive-zip-manifest.d.ts +5 -0
  35. package/dist/archive-zip-manifest.d.ts.map +1 -0
  36. package/dist/archive-zip-manifest.js +22 -0
  37. package/dist/archive-zip-names.d.ts +6 -1
  38. package/dist/archive-zip-names.d.ts.map +1 -1
  39. package/dist/archive-zip-names.js +50 -17
  40. package/dist/archive-zip-preflight.d.ts.map +1 -1
  41. package/dist/archive-zip-preflight.js +3 -2
  42. package/dist/archive.d.ts.map +1 -1
  43. package/dist/archive.js +9 -4
  44. package/dist/bounded-read.js +2 -2
  45. package/dist/darwin-acl.d.ts +4 -0
  46. package/dist/darwin-acl.d.ts.map +1 -0
  47. package/dist/darwin-acl.js +24 -0
  48. package/dist/deny-mutations.d.ts.map +1 -1
  49. package/dist/deny-mutations.js +8 -2
  50. package/dist/device-path.d.ts.map +1 -1
  51. package/dist/device-path.js +5 -3
  52. package/dist/directory-durability.d.ts.map +1 -1
  53. package/dist/directory-durability.js +67 -23
  54. package/dist/directory-entry-path.d.ts +3 -0
  55. package/dist/directory-entry-path.d.ts.map +1 -0
  56. package/dist/directory-entry-path.js +21 -0
  57. package/dist/directory-guard.d.ts +17 -1
  58. package/dist/directory-guard.d.ts.map +1 -1
  59. package/dist/directory-guard.js +134 -48
  60. package/dist/directory-mode-node.d.ts +12 -0
  61. package/dist/directory-mode-node.d.ts.map +1 -1
  62. package/dist/directory-mode-node.js +102 -4
  63. package/dist/effective-uid.d.ts +2 -0
  64. package/dist/effective-uid.d.ts.map +1 -0
  65. package/dist/effective-uid.js +25 -0
  66. package/dist/file-handle-transfer.d.ts.map +1 -1
  67. package/dist/file-handle-transfer.js +98 -27
  68. package/dist/file-hash.d.ts.map +1 -1
  69. package/dist/file-hash.js +12 -2
  70. package/dist/file-lock-sync.d.ts.map +1 -1
  71. package/dist/file-lock-sync.js +36 -6
  72. package/dist/file-lock.d.ts.map +1 -1
  73. package/dist/file-lock.js +29 -9
  74. package/dist/file-observation.d.ts +1 -1
  75. package/dist/file-observation.d.ts.map +1 -1
  76. package/dist/file-store-boundary.d.ts +6 -2
  77. package/dist/file-store-boundary.d.ts.map +1 -1
  78. package/dist/file-store-boundary.js +20 -65
  79. package/dist/file-store-copy-source.d.ts +5 -0
  80. package/dist/file-store-copy-source.d.ts.map +1 -0
  81. package/dist/file-store-copy-source.js +31 -0
  82. package/dist/file-store-path.d.ts.map +1 -1
  83. package/dist/file-store-path.js +4 -1
  84. package/dist/file-store-prune.d.ts.map +1 -1
  85. package/dist/file-store-prune.js +9 -1
  86. package/dist/file-store-sync-directory.d.ts +16 -0
  87. package/dist/file-store-sync-directory.d.ts.map +1 -0
  88. package/dist/file-store-sync-directory.js +349 -0
  89. package/dist/file-store.d.ts.map +1 -1
  90. package/dist/file-store.js +11 -28
  91. package/dist/filename.d.ts +1 -0
  92. package/dist/filename.d.ts.map +1 -1
  93. package/dist/filename.js +85 -23
  94. package/dist/fs.d.ts.map +1 -1
  95. package/dist/fs.js +3 -2
  96. package/dist/guarded-mkdir.d.ts +10 -0
  97. package/dist/guarded-mkdir.d.ts.map +1 -1
  98. package/dist/guarded-mkdir.js +217 -33
  99. package/dist/guest.d.ts.map +1 -1
  100. package/dist/guest.js +16 -5
  101. package/dist/home-dir.d.ts.map +1 -1
  102. package/dist/home-dir.js +73 -10
  103. package/dist/install-path.d.ts +6 -0
  104. package/dist/install-path.d.ts.map +1 -1
  105. package/dist/install-path.js +67 -19
  106. package/dist/json-durable-queue-ownership.d.ts +8 -0
  107. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  108. package/dist/json-durable-queue-ownership.js +134 -43
  109. package/dist/json-durable-queue-paths.d.ts +11 -0
  110. package/dist/json-durable-queue-paths.d.ts.map +1 -0
  111. package/dist/json-durable-queue-paths.js +42 -0
  112. package/dist/json-durable-queue-read.d.ts +6 -0
  113. package/dist/json-durable-queue-read.d.ts.map +1 -0
  114. package/dist/json-durable-queue-read.js +61 -0
  115. package/dist/json-durable-queue.d.ts +1 -1
  116. package/dist/json-durable-queue.d.ts.map +1 -1
  117. package/dist/json-durable-queue.js +83 -134
  118. package/dist/json-store.d.ts.map +1 -1
  119. package/dist/json-store.js +5 -1
  120. package/dist/json.d.ts.map +1 -1
  121. package/dist/json.js +54 -22
  122. package/dist/local-file-access.d.ts.map +1 -1
  123. package/dist/local-file-access.js +4 -0
  124. package/dist/local-file-descriptor.d.ts +17 -0
  125. package/dist/local-file-descriptor.d.ts.map +1 -0
  126. package/dist/local-file-descriptor.js +84 -0
  127. package/dist/local-roots.d.ts.map +1 -1
  128. package/dist/local-roots.js +35 -7
  129. package/dist/move-path-cleanup.d.ts +2 -0
  130. package/dist/move-path-cleanup.d.ts.map +1 -1
  131. package/dist/move-path-cleanup.js +44 -18
  132. package/dist/move-path.d.ts.map +1 -1
  133. package/dist/move-path.js +31 -6
  134. package/dist/native-binding.d.ts +21 -0
  135. package/dist/native-binding.d.ts.map +1 -1
  136. package/dist/native-directory-observation.d.ts +17 -0
  137. package/dist/native-directory-observation.d.ts.map +1 -0
  138. package/dist/native-directory-observation.js +37 -0
  139. package/dist/native-parent-admission.d.ts +31 -0
  140. package/dist/native-parent-admission.d.ts.map +1 -0
  141. package/dist/native-parent-admission.js +124 -0
  142. package/dist/native-pinned-write-windows.d.ts +0 -1
  143. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  144. package/dist/native-pinned-write-windows.js +0 -3
  145. package/dist/native-pinned-write.d.ts.map +1 -1
  146. package/dist/native-pinned-write.js +309 -40
  147. package/dist/output.d.ts.map +1 -1
  148. package/dist/output.js +15 -5
  149. package/dist/overwrite-file-handle.d.ts.map +1 -1
  150. package/dist/overwrite-file-handle.js +5 -1
  151. package/dist/owner-dacl.d.ts.map +1 -1
  152. package/dist/owner-dacl.js +2 -0
  153. package/dist/path-policy.d.ts.map +1 -1
  154. package/dist/path-policy.js +7 -2
  155. package/dist/path-prefix.d.ts +7 -0
  156. package/dist/path-prefix.d.ts.map +1 -0
  157. package/dist/path-prefix.js +82 -0
  158. package/dist/path-scope-lexical.d.ts +14 -0
  159. package/dist/path-scope-lexical.d.ts.map +1 -0
  160. package/dist/path-scope-lexical.js +37 -0
  161. package/dist/path-segment-route.d.ts +7 -0
  162. package/dist/path-segment-route.d.ts.map +1 -0
  163. package/dist/path-segment-route.js +24 -0
  164. package/dist/path-suffix-aliases.d.ts +10 -0
  165. package/dist/path-suffix-aliases.d.ts.map +1 -0
  166. package/dist/path-suffix-aliases.js +386 -0
  167. package/dist/path.d.ts.map +1 -1
  168. package/dist/path.js +35 -6
  169. package/dist/permissions-windows.d.ts.map +1 -1
  170. package/dist/permissions-windows.js +14 -3
  171. package/dist/permissions.d.ts.map +1 -1
  172. package/dist/permissions.js +37 -8
  173. package/dist/pinned-mutation-admission.d.ts +25 -0
  174. package/dist/pinned-mutation-admission.d.ts.map +1 -0
  175. package/dist/pinned-mutation-admission.js +425 -0
  176. package/dist/pinned-mutation-observation.d.ts +34 -0
  177. package/dist/pinned-mutation-observation.d.ts.map +1 -0
  178. package/dist/pinned-mutation-observation.js +142 -0
  179. package/dist/pinned-mutation-shared-route.d.ts +24 -0
  180. package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
  181. package/dist/pinned-mutation-shared-route.js +70 -0
  182. package/dist/pinned-open.d.ts +6 -0
  183. package/dist/pinned-open.d.ts.map +1 -1
  184. package/dist/pinned-open.js +28 -10
  185. package/dist/pinned-write-types.d.ts +75 -0
  186. package/dist/pinned-write-types.d.ts.map +1 -0
  187. package/dist/pinned-write-types.js +1 -0
  188. package/dist/pinned-write.d.ts +7 -33
  189. package/dist/pinned-write.d.ts.map +1 -1
  190. package/dist/pinned-write.js +152 -17
  191. package/dist/private-directory.d.ts.map +1 -1
  192. package/dist/private-directory.js +2 -0
  193. package/dist/private-producer-handoff.d.ts +16 -0
  194. package/dist/private-producer-handoff.d.ts.map +1 -0
  195. package/dist/private-producer-handoff.js +272 -0
  196. package/dist/private-temp-workspace.d.ts +2 -39
  197. package/dist/private-temp-workspace.d.ts.map +1 -1
  198. package/dist/private-temp-workspace.js +183 -77
  199. package/dist/publish-file.d.ts.map +1 -1
  200. package/dist/publish-file.js +33 -29
  201. package/dist/regular-file.d.ts.map +1 -1
  202. package/dist/regular-file.js +56 -45
  203. package/dist/replace-directory.d.ts.map +1 -1
  204. package/dist/replace-directory.js +12 -5
  205. package/dist/replace-file-temp-owner.d.ts +1 -0
  206. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  207. package/dist/replace-file-temp-owner.js +12 -1
  208. package/dist/replace-file.d.ts.map +1 -1
  209. package/dist/replace-file.js +24 -24
  210. package/dist/root-boundary.d.ts +29 -0
  211. package/dist/root-boundary.d.ts.map +1 -0
  212. package/dist/root-boundary.js +182 -0
  213. package/dist/root-context.d.ts +12 -1
  214. package/dist/root-context.d.ts.map +1 -1
  215. package/dist/root-context.js +89 -16
  216. package/dist/root-directory-creation.d.ts +13 -0
  217. package/dist/root-directory-creation.d.ts.map +1 -0
  218. package/dist/root-directory-creation.js +212 -0
  219. package/dist/root-directory-list.d.ts +16 -4
  220. package/dist/root-directory-list.d.ts.map +1 -1
  221. package/dist/root-directory-list.js +182 -33
  222. package/dist/root-directory.d.ts +23 -0
  223. package/dist/root-directory.d.ts.map +1 -0
  224. package/dist/root-directory.js +141 -0
  225. package/dist/root-errors.d.ts.map +1 -1
  226. package/dist/root-errors.js +12 -1
  227. package/dist/root-file-final-admission.d.ts +21 -0
  228. package/dist/root-file-final-admission.d.ts.map +1 -0
  229. package/dist/root-file-final-admission.js +83 -0
  230. package/dist/root-file.d.ts.map +1 -1
  231. package/dist/root-file.js +103 -24
  232. package/dist/root-impl.d.ts +1 -0
  233. package/dist/root-impl.d.ts.map +1 -1
  234. package/dist/root-impl.js +316 -325
  235. package/dist/root-move-noreplace.d.ts +14 -0
  236. package/dist/root-move-noreplace.d.ts.map +1 -0
  237. package/dist/root-move-noreplace.js +202 -0
  238. package/dist/root-move-preflight.d.ts +8 -0
  239. package/dist/root-move-preflight.d.ts.map +1 -0
  240. package/dist/root-move-preflight.js +16 -0
  241. package/dist/root-observed-path.d.ts +9 -0
  242. package/dist/root-observed-path.d.ts.map +1 -0
  243. package/dist/root-observed-path.js +95 -0
  244. package/dist/root-path-errors.d.ts +12 -0
  245. package/dist/root-path-errors.d.ts.map +1 -0
  246. package/dist/root-path-errors.js +13 -0
  247. package/dist/root-path-existing.d.ts +2 -0
  248. package/dist/root-path-existing.d.ts.map +1 -1
  249. package/dist/root-path-existing.js +63 -22
  250. package/dist/root-path-observation.d.ts +63 -0
  251. package/dist/root-path-observation.d.ts.map +1 -0
  252. package/dist/root-path-observation.js +180 -0
  253. package/dist/root-path-stat.d.ts +5 -0
  254. package/dist/root-path-stat.d.ts.map +1 -0
  255. package/dist/root-path-stat.js +101 -0
  256. package/dist/root-path-symlink.d.ts.map +1 -1
  257. package/dist/root-path-symlink.js +23 -4
  258. package/dist/root-path.d.ts +13 -0
  259. package/dist/root-path.d.ts.map +1 -1
  260. package/dist/root-path.js +263 -73
  261. package/dist/root-paths-lexical.d.ts +9 -0
  262. package/dist/root-paths-lexical.d.ts.map +1 -0
  263. package/dist/root-paths-lexical.js +22 -0
  264. package/dist/root-paths.d.ts +3 -29
  265. package/dist/root-paths.d.ts.map +1 -1
  266. package/dist/root-paths.js +89 -178
  267. package/dist/root-read-admission.d.ts +26 -0
  268. package/dist/root-read-admission.d.ts.map +1 -0
  269. package/dist/root-read-admission.js +94 -0
  270. package/dist/root-remove-identity.d.ts +15 -0
  271. package/dist/root-remove-identity.d.ts.map +1 -0
  272. package/dist/root-remove-identity.js +89 -0
  273. package/dist/root-remove-receipt.d.ts +17 -0
  274. package/dist/root-remove-receipt.d.ts.map +1 -0
  275. package/dist/root-remove-receipt.js +37 -0
  276. package/dist/root-remove.d.ts +2 -1
  277. package/dist/root-remove.d.ts.map +1 -1
  278. package/dist/root-remove.js +172 -10
  279. package/dist/root-walk.d.ts.map +1 -1
  280. package/dist/root-walk.js +2 -1
  281. package/dist/root-write-admission.d.ts +68 -0
  282. package/dist/root-write-admission.d.ts.map +1 -0
  283. package/dist/root-write-admission.js +322 -0
  284. package/dist/root-write-compatibility.d.ts +13 -0
  285. package/dist/root-write-compatibility.d.ts.map +1 -0
  286. package/dist/root-write-compatibility.js +74 -0
  287. package/dist/root-write-complete-parent.d.ts +42 -0
  288. package/dist/root-write-complete-parent.d.ts.map +1 -0
  289. package/dist/root-write-complete-parent.js +195 -0
  290. package/dist/root-write-mode.d.ts +2 -0
  291. package/dist/root-write-mode.d.ts.map +1 -1
  292. package/dist/root-write-mode.js +20 -7
  293. package/dist/root-write-publication.d.ts +24 -0
  294. package/dist/root-write-publication.d.ts.map +1 -0
  295. package/dist/root-write-publication.js +85 -0
  296. package/dist/root-write-verification.d.ts.map +1 -1
  297. package/dist/root-write-verification.js +18 -7
  298. package/dist/safe-path-segment.d.ts.map +1 -1
  299. package/dist/safe-path-segment.js +3 -1
  300. package/dist/secret-file.d.ts.map +1 -1
  301. package/dist/secret-file.js +62 -24
  302. package/dist/secret-read-async.d.ts.map +1 -1
  303. package/dist/secret-read-async.js +18 -13
  304. package/dist/secret-read-policy.d.ts.map +1 -1
  305. package/dist/secret-read-policy.js +5 -1
  306. package/dist/secure-file-windows.d.ts +10 -0
  307. package/dist/secure-file-windows.d.ts.map +1 -0
  308. package/dist/secure-file-windows.js +186 -0
  309. package/dist/secure-file.d.ts.map +1 -1
  310. package/dist/secure-file.js +98 -10
  311. package/dist/secure-temp-dir.d.ts +6 -3
  312. package/dist/secure-temp-dir.d.ts.map +1 -1
  313. package/dist/secure-temp-dir.js +121 -101
  314. package/dist/secure-temp-repair.d.ts +35 -0
  315. package/dist/secure-temp-repair.d.ts.map +1 -0
  316. package/dist/secure-temp-repair.js +106 -0
  317. package/dist/sibling-staged-file.d.ts +3 -1
  318. package/dist/sibling-staged-file.d.ts.map +1 -1
  319. package/dist/sibling-staged-file.js +181 -50
  320. package/dist/sibling-temp.d.ts.map +1 -1
  321. package/dist/sibling-temp.js +27 -12
  322. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  323. package/dist/sidecar-lock-acquire.js +45 -15
  324. package/dist/sidecar-lock-root.d.ts.map +1 -1
  325. package/dist/sidecar-lock-root.js +4 -2
  326. package/dist/sidecar-lock.d.ts.map +1 -1
  327. package/dist/sidecar-lock.js +2 -3
  328. package/dist/staged-directory.d.ts +14 -5
  329. package/dist/staged-directory.d.ts.map +1 -1
  330. package/dist/staged-directory.js +54 -3
  331. package/dist/standalone-publication-path.d.ts +2 -0
  332. package/dist/standalone-publication-path.d.ts.map +1 -0
  333. package/dist/standalone-publication-path.js +6 -0
  334. package/dist/stat-observation.d.ts +11 -0
  335. package/dist/stat-observation.d.ts.map +1 -0
  336. package/dist/stat-observation.js +64 -0
  337. package/dist/strict-file-identity.d.ts.map +1 -1
  338. package/dist/strict-file-identity.js +39 -2
  339. package/dist/symlink-parents.d.ts.map +1 -1
  340. package/dist/symlink-parents.js +7 -2
  341. package/dist/temp-target.d.ts +2 -0
  342. package/dist/temp-target.d.ts.map +1 -1
  343. package/dist/temp-target.js +133 -4
  344. package/dist/temp-workspace-admission.d.ts +22 -0
  345. package/dist/temp-workspace-admission.d.ts.map +1 -0
  346. package/dist/temp-workspace-admission.js +374 -0
  347. package/dist/temp-workspace-child-admission.d.ts +13 -0
  348. package/dist/temp-workspace-child-admission.d.ts.map +1 -0
  349. package/dist/temp-workspace-child-admission.js +95 -0
  350. package/dist/temp-workspace-descriptor.d.ts +48 -0
  351. package/dist/temp-workspace-descriptor.d.ts.map +1 -0
  352. package/dist/temp-workspace-descriptor.js +363 -0
  353. package/dist/temp-workspace-identity.d.ts +15 -0
  354. package/dist/temp-workspace-identity.d.ts.map +1 -0
  355. package/dist/temp-workspace-identity.js +41 -0
  356. package/dist/temp-workspace-owner.d.ts +7 -6
  357. package/dist/temp-workspace-owner.d.ts.map +1 -1
  358. package/dist/temp-workspace-owner.js +121 -61
  359. package/dist/temp-workspace-permissions.d.ts +4 -0
  360. package/dist/temp-workspace-permissions.d.ts.map +1 -0
  361. package/dist/temp-workspace-permissions.js +32 -0
  362. package/dist/temp-workspace-types.d.ts +40 -0
  363. package/dist/temp-workspace-types.d.ts.map +1 -0
  364. package/dist/temp-workspace-types.js +1 -0
  365. package/dist/temp.d.ts +1 -1
  366. package/dist/temp.d.ts.map +1 -1
  367. package/dist/temp.js +1 -1
  368. package/dist/test-hooks.d.ts +7 -0
  369. package/dist/test-hooks.d.ts.map +1 -1
  370. package/dist/text-atomic.d.ts.map +1 -1
  371. package/dist/text-atomic.js +3 -1
  372. package/dist/trash.d.ts.map +1 -1
  373. package/dist/trash.js +54 -8
  374. package/dist/walk.d.ts.map +1 -1
  375. package/dist/walk.js +20 -16
  376. package/dist/windows-owner.d.ts.map +1 -1
  377. package/dist/windows-owner.js +5 -0
  378. package/dist/windows-path-alias.d.ts +39 -0
  379. package/dist/windows-path-alias.d.ts.map +1 -0
  380. package/dist/windows-path-alias.js +153 -0
  381. package/docs/advanced.md +28 -1
  382. package/docs/archive.md +29 -2
  383. package/docs/atomic.md +27 -3
  384. package/docs/contributing.md +4 -1
  385. package/docs/copy.md +4 -1
  386. package/docs/durability.md +18 -2
  387. package/docs/errors.md +14 -6
  388. package/docs/file-store.md +30 -3
  389. package/docs/filename.md +21 -7
  390. package/docs/guest.md +5 -0
  391. package/docs/install-path.md +61 -15
  392. package/docs/install.md +12 -8
  393. package/docs/json-store.md +5 -1
  394. package/docs/json.md +11 -0
  395. package/docs/mutation-policy-proof.md +65 -0
  396. package/docs/native-helper.md +28 -9
  397. package/docs/native.md +46 -10
  398. package/docs/output.md +33 -11
  399. package/docs/path-prefix.md +64 -0
  400. package/docs/path-suffix-aliases.md +159 -0
  401. package/docs/path.md +12 -1
  402. package/docs/permissions.md +26 -1
  403. package/docs/private-file-store.md +14 -0
  404. package/docs/public-api.md +14 -0
  405. package/docs/quickstart.md +1 -1
  406. package/docs/reading.md +8 -1
  407. package/docs/root.md +21 -4
  408. package/docs/secret-file.md +3 -0
  409. package/docs/secure-file.md +21 -16
  410. package/docs/security-model.md +76 -9
  411. package/docs/sidecar-lock.md +28 -1
  412. package/docs/store.md +27 -2
  413. package/docs/temp.md +262 -34
  414. package/docs/test-hooks.md +14 -0
  415. package/docs/writing.md +38 -8
  416. 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`);
@@ -29,11 +30,10 @@ function shouldStop(result, options) {
29
30
  }
30
31
  function buildEntry(params) {
31
32
  const fullPath = params.fullPath;
32
- const relativePath = path.relative(params.rootDir, fullPath) || params.dirent.name;
33
33
  return {
34
34
  name: params.dirent.name,
35
35
  path: fullPath,
36
- relativePath,
36
+ relativePath: params.relativePath,
37
37
  depth: params.depth,
38
38
  kind: params.kind ?? kindForDirent(params.dirent),
39
39
  dirent: params.dirent,
@@ -70,7 +70,7 @@ function resolveKind(fullPath, dirent, symlinks) {
70
70
  }
71
71
  export function walkDirectorySync(rootDir, options = {}) {
72
72
  validateWalkOptions(options);
73
- const root = path.resolve(rootDir);
73
+ const root = resolvePathPreservingWindowsRoot(rootDir);
74
74
  const symlinks = options.symlinks ?? "skip";
75
75
  const result = {
76
76
  entries: [],
@@ -79,12 +79,13 @@ export function walkDirectorySync(rootDir, options = {}) {
79
79
  failedDirs: [],
80
80
  };
81
81
  const visitedDirs = new Set();
82
- function visit(dir, depth) {
82
+ function visit(dir, relativeDir, depth) {
83
83
  if (options.maxDepth !== undefined && depth > options.maxDepth)
84
84
  return;
85
85
  let realDir;
86
+ const operationPath = pathForWindowsFilesystem(dir);
86
87
  try {
87
- realDir = realpathSync(dir);
88
+ realDir = realpathSync(operationPath);
88
89
  }
89
90
  catch (error) {
90
91
  recordFailedDir(result, root, dir, depth, error);
@@ -95,7 +96,7 @@ export function walkDirectorySync(rootDir, options = {}) {
95
96
  visitedDirs.add(realDir);
96
97
  let entries;
97
98
  try {
98
- entries = fsSync.readdirSync(dir, { withFileTypes: true });
99
+ entries = fsSync.readdirSync(operationPath, { withFileTypes: true });
99
100
  }
100
101
  catch (error) {
101
102
  recordFailedDir(result, root, dir, depth, error);
@@ -111,25 +112,26 @@ export function walkDirectorySync(rootDir, options = {}) {
111
112
  const kind = resolveKind(fullPath, dirent, symlinks);
112
113
  if (!kind)
113
114
  continue;
114
- const entry = buildEntry({ rootDir: root, fullPath, dirent, depth, kind });
115
+ const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
116
+ const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
115
117
  if (options.include?.(entry) ?? true) {
116
118
  result.entries.push(entry);
117
119
  }
118
120
  if (kind === "directory" &&
119
121
  (options.maxDepth === undefined || depth < options.maxDepth) &&
120
122
  (options.descend?.(entry) ?? true)) {
121
- visit(fullPath, depth + 1);
123
+ visit(fullPath, relativePath, depth + 1);
122
124
  if (result.truncated)
123
125
  return;
124
126
  }
125
127
  }
126
128
  }
127
- visit(root, 1);
129
+ visit(root, "", 1);
128
130
  return result;
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: [],
@@ -138,12 +140,13 @@ export async function walkDirectory(rootDir, options = {}) {
138
140
  failedDirs: [],
139
141
  };
140
142
  const visitedDirs = new Set();
141
- async function visit(dir, depth) {
143
+ async function visit(dir, relativeDir, depth) {
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);
@@ -170,19 +173,20 @@ export async function walkDirectory(rootDir, options = {}) {
170
173
  const kind = resolveKind(fullPath, dirent, symlinks);
171
174
  if (!kind)
172
175
  continue;
173
- const entry = buildEntry({ rootDir: root, fullPath, dirent, depth, kind });
176
+ const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
177
+ const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
174
178
  if (options.include?.(entry) ?? true) {
175
179
  result.entries.push(entry);
176
180
  }
177
181
  if (kind === "directory" &&
178
182
  (options.maxDepth === undefined || depth < options.maxDepth) &&
179
183
  (options.descend?.(entry) ?? true)) {
180
- await visit(fullPath, depth + 1);
184
+ await visit(fullPath, relativePath, depth + 1);
181
185
  if (result.truncated)
182
186
  return;
183
187
  }
184
188
  }
185
189
  }
186
- await visit(root, 1);
190
+ await visit(root, "", 1);
187
191
  return result;
188
192
  }
@@ -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.
@@ -96,6 +121,8 @@ byte limit. Continuation buffers grow only after filling with actual bytes,
96
121
  up to the byte budget plus its probe; file-size hints cannot force that growth.
97
122
  Unknown-size inputs start with at most 64 KiB. This avoids per-chunk copies and
98
123
  a final concatenation when a file exceeds the initial allocation.
124
+ Regular files that report a size of zero, such as virtual files, continue through
125
+ positive short reads until actual EOF or byte-limit overflow.
99
126
 
100
127
  The bounded descriptor helpers start at the descriptor's current offset and
101
128
  leave ownership with the caller. They are intended for the second half of a
@@ -133,7 +160,7 @@ component is followed by another segment, both helpers throw
133
160
 
134
161
  | Export | Page | Notes |
135
162
  |---|---|---|
136
- | `safeDirName`, `safePathSegmentHashed`, `resolveSafeInstallDir`, `assertCanonicalPathWithinBase` | [install-path.md](install-path.md) | Build install-target directories from caller-supplied identifiers. |
163
+ | `safeDirName`, `safePathSegmentHashed`, `safePathSegmentHashedV2`, `resolveSafeInstallDir`, `assertCanonicalPathWithinBase` | [install-path.md](install-path.md) | Build install-target directories; use V2 for untrusted identifier mappings. |
137
164
  | `sanitizeUntrustedFileName` | [filename.md](filename.md) | Coerce an untrusted string into a safe filename. |
138
165
  | `resolveHomeRelativePath` | – | Expand a leading `~` before resolving `.` and `..`; tildes inside relative paths stay literal. |
139
166
 
package/docs/archive.md CHANGED
@@ -139,7 +139,29 @@ 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.
159
+
160
+ Within one ZIP entry, identical local and central name bytes reuse the same
161
+ decoded validation. Unicode Path admission is shared only when both the raw names
162
+ and the complete Unicode fields match; different fields still verify their own
163
+ CRC and interpretation. Shared backing memory is checked independently. Decoded
164
+ name validation is not reused across entries or archives.
143
165
 
144
166
  `stripComponents` removes leading nonempty, non-`.` path components after
145
167
  normalizing separators. For example, `./pkg/hello.txt` with
@@ -226,7 +248,7 @@ omitted and do not consume output payload budgets. The shared core admits these
226
248
  record and are then cleared; local PAX on unsupported types and GNU sparse
227
249
  `S` records retain their existing fail-closed format policy.
228
250
 
229
- 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.
230
252
 
231
253
  Extraction captures the destination's lossless filesystem identity before any
232
254
  entry filter runs and retains that capability through final publication. If a
@@ -315,6 +337,11 @@ codes remain `"destination-not-directory"`, `"destination-symlink"`, and
315
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.
316
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.
317
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
+
318
345
  ### Raw TAR framing
319
346
 
320
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. |
@@ -76,7 +76,10 @@ setup use `useSuiteFixture` from `test/helpers/suite-fixture.ts`: setup has a se
76
76
  removing directories. Run shared-state corpora sequentially with a deadline per
77
77
  payload. Keep child-process liveness limits separate from fixture preparation.
78
78
  The Windows CI slow-copy proof runs the real package-copy process-exit test with a
79
- six-second copy delay, retaining its four-second child deadline:
79
+ 16-second copy delay, a 10-second child deadline, a 15-second test deadline, and
80
+ 60-second setup and teardown hook budgets. Other hosts retain the ordinary
81
+ six-second delay, four-second child deadline, five-second test deadline, and
82
+ 30-second hook budgets:
80
83
 
81
84
  ```bash
82
85
  pnpm build
package/docs/copy.md CHANGED
@@ -64,7 +64,7 @@ Automatic copying does not recover from permission errors, I/O errors, cancellat
64
64
 
65
65
  On Windows, automatic byte copying uses the native binding when available to transfer data between the already-checked file handles in chunks of at most 1 MiB, with smaller buffers for small files. This accelerates NTFS and cross-volume copies without reopening source or destination pathnames. The native worker finishes before its descriptors are closed or cancellation is reported. `clone: "never"` and native-disabled copies use JavaScript read/write loops with reusable buffers: at most 1 MiB per active file on Windows, or 128 KiB elsewhere. Both paths share the file-worker budget across sibling directories, wait for all admitted writes after cancellation or failure, and restore directory timestamps after their descendants finish. Completed directory traversals awaiting writes or metadata are bounded by concurrency; ancestors remain pinned while traversing their children.
66
66
 
67
- On Linux, automatic byte copying also uses the native binding when available. It reads in 1 MiB chunks and leaves leading and trailing zero-filled portions of each chunk unwritten in the new file, avoiding their allocation on filesystems that support sparse files. A final size update preserves trailing holes and all-zero files. This path uses reads and writes, without cloning or copy offload; it still reads the full logical contents and does not promise identical sparse extent layout. `clone: "never"` retains the JavaScript byte-copy path.
67
+ On Linux, automatic byte copying also uses the native binding when available. It reads in chunks up to 1 MiB, using smaller buffers for smaller source-size hints, and leaves leading and trailing zero-filled portions of each chunk unwritten in the new file, avoiding their allocation on filesystems that support sparse files. A final size update preserves trailing holes and all-zero files. This path uses reads and writes, without cloning or copy offload; it still reads the full logical contents and does not promise identical sparse extent layout. `clone: "never"` retains the JavaScript byte-copy path.
68
68
 
69
69
  Clones preserve file contents, empty directories, timestamps, executable modes where supported, and literal symbolic links. Editing a clone does not modify its source. Unsupported filesystem operations fail; callers may choose their own copy or checkout fallback after the failed operation has settled.
70
70
 
@@ -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:
@@ -242,8 +256,10 @@ When the optional binding is active, hashing runs as an async native task and
242
256
  does not occupy the JavaScript event loop with digest updates. With native mode
243
257
  `off`, or in `auto` when no binding loads, the fallback performs asynchronous
244
258
  positioned reads in chunks of up to 256 KiB but updates Node's `Hash` on the JavaScript
245
- thread. Both paths stream constant-size buffers rather than loading the file
246
- into memory. Native mode `require` keeps its usual fail-closed loader semantics.
259
+ thread. Both paths stream bounded buffers rather than loading the file into memory.
260
+ The fallback sizes its scratch buffer to small files and grows it if a stale
261
+ size hint is exceeded, while still probing for actual EOF and byte-limit overflow.
262
+ Native mode `require` keeps its usual fail-closed loader semantics.
247
263
 
248
264
  ### Synchronous hashing
249
265