@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
@@ -6,9 +6,6 @@ import { FsSafeError } from "./errors.js";
6
6
  import { nativeOpenFlags, removeNativeCreatedFileIfStillPinned, writeNativeInput, } from "./native-operations.js";
7
7
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
8
8
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
9
- export function sameNativeIdentity(left, right) {
10
- return left.dev === right.dev && left.ino === right.ino;
11
- }
12
9
  function closeWriteFd(fd) {
13
10
  if (fd === undefined)
14
11
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAK3D,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA2FvH"}
1
+ {"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAI3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAIV,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AAoV3B,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqGvH"}
@@ -50,16 +50,278 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
- import fsSync from "node:fs";
54
- import fs from "node:fs/promises";
53
+ import fsSync, {} from "node:fs";
55
54
  import path from "node:path";
56
55
  import { inspectDirectoryIdentity } from "./directory-guard.js";
57
56
  import { FsSafeError } from "./errors.js";
58
- import { runPinnedWriteWindows, sameNativeIdentity } from "./native-pinned-write-windows.js";
57
+ import { runPinnedWriteWindows } from "./native-pinned-write-windows.js";
58
+ import { openNativeParentAdmission, openNativeRootAdmission } from "./native-parent-admission.js";
59
59
  import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
60
- import { describeStagedDirectory, exactIdentityMatches } from "./staged-directory.js";
60
+ import { assertPolicyStagedDirectoryCurrent, assertStagedDirectoryCurrent, describePolicyStagedDirectory, describeStagedDirectory, refreshPolicyStagedDirectoryObservation, } from "./staged-directory.js";
61
61
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
62
62
  import { realpathSync } from "./realpath.js";
63
+ import { isNotFoundPathError, isSymlinkOpenError } from "./path.js";
64
+ import { checkedMutationDirectory, } from "./pinned-mutation-observation.js";
65
+ import { createPathSegmentRoute, joinPathSegmentRoute } from "./path-segment-route.js";
66
+ function relativeParentSegments(relativeParentPath) {
67
+ return relativeParentPath.split("/").filter(Boolean);
68
+ }
69
+ function prospectiveTargetPath(parentPath, parentRoute, offset, basename) {
70
+ return joinPathSegmentRoute(parentPath, parentRoute, offset, basename);
71
+ }
72
+ function sameAbsolutePath(left, right) {
73
+ return path.relative(path.resolve(left), path.resolve(right)) === "";
74
+ }
75
+ function assertDirectChildBasename(basename) {
76
+ if (!basename || basename === "." || basename === ".." || basename.includes("/") ||
77
+ basename.includes("\0") || (process.platform === "win32" && basename.includes("\\"))) {
78
+ throw new FsSafeError("invalid-path", "native parent creation requires one direct-child basename");
79
+ }
80
+ }
81
+ function mkdirPolicyChild(binding, parentFd, basename, mode) {
82
+ assertDirectChildBasename(basename);
83
+ const mkdirChild = binding.mkdirChildBeneath;
84
+ if (typeof mkdirChild === "function") {
85
+ const created = mkdirChild.call(binding, parentFd, basename, mode);
86
+ if (typeof created === "boolean")
87
+ return created;
88
+ }
89
+ // Old or malformed optional helpers still execute through the established
90
+ // recursive primitive, but cannot provide exclusive-creation provenance.
91
+ binding.mkdirBeneath(parentFd, basename, mode);
92
+ return false;
93
+ }
94
+ async function authorizePinnedMutation(params, request) {
95
+ return await params.mutationAdmission?.authorize(Object.freeze(request));
96
+ }
97
+ function normalizePolicyParentOpenError(error, params) {
98
+ if (!isSymlinkOpenError(error))
99
+ return error;
100
+ return params.mutationAdmission?.rejectParentSymlinks
101
+ ? new FsSafeError("symlink", "symlink path component not allowed", {
102
+ cause: error instanceof Error ? error : undefined,
103
+ })
104
+ : new FsSafeError("path-mismatch", "native write parent changed during policy admission", {
105
+ cause: error instanceof Error ? error : undefined,
106
+ });
107
+ }
108
+ async function describePosixParent(parentFd, pathname) {
109
+ const parentPath = realpathSync.native(pathname);
110
+ const directory = describeStagedDirectory(parentFd, parentPath);
111
+ const parentPathStat = await inspectDirectoryIdentity(parentPath, inspectFileIdentitySync(() => fsSync.fstatSync(parentFd, { bigint: true })));
112
+ return {
113
+ parentFd,
114
+ parentPath,
115
+ directory,
116
+ parentPathStat,
117
+ observation: checkedMutationDirectory(parentPath, directory.realPath, parentPathStat),
118
+ };
119
+ }
120
+ function describePolicyPosixParent(parentFd, pathname) {
121
+ const policyDirectory = describePolicyStagedDirectory(parentFd, pathname);
122
+ return {
123
+ parentFd,
124
+ parentPath: policyDirectory.directory.realPath,
125
+ directory: policyDirectory.directory,
126
+ parentPathStat: policyDirectory.stat,
127
+ observation: policyDirectory.observation,
128
+ policyDirectory,
129
+ };
130
+ }
131
+ function policyParentCaptureDeopt(error) {
132
+ return error instanceof FsSafeError &&
133
+ (error.code === "path-mismatch" || error.code === "not-file");
134
+ }
135
+ function tryDescribePolicyPosixParent(parentFd, pathname) {
136
+ try {
137
+ return describePolicyPosixParent(parentFd, pathname);
138
+ }
139
+ catch (error) {
140
+ if (!policyParentCaptureDeopt(error))
141
+ throw error;
142
+ return undefined;
143
+ }
144
+ }
145
+ function assertPosixParentCurrent(parent) {
146
+ return parent.policyDirectory
147
+ ? assertPolicyStagedDirectoryCurrent(parent.policyDirectory)
148
+ : assertStagedDirectoryCurrent(parent.directory);
149
+ }
150
+ async function capturePolicyAwarePosixParent(binding, params, rootFd, directoryFlags) {
151
+ const segments = relativeParentSegments(params.relativeParentPath);
152
+ const parentSpelling = segments.length
153
+ ? path.join(params.rootPath, ...segments)
154
+ : params.rootPath;
155
+ const segmentRoute = createPathSegmentRoute(segments);
156
+ const initialTarget = prospectiveTargetPath(params.rootPath, segmentRoute, 0, params.basename);
157
+ let retainedTargetPath = params.mutationAdmission?.beginParentWalk?.();
158
+ if (retainedTargetPath && !sameAbsolutePath(retainedTargetPath, initialTarget)) {
159
+ retainedTargetPath = undefined;
160
+ }
161
+ // Preserve the one-open hot path when the complete parent still exists.
162
+ // Policy is attached only after the opened descriptor is associated with its
163
+ // current canonical pathname, so a contained redirect cannot retain the old
164
+ // preflight authorization.
165
+ let completeParentFd;
166
+ try {
167
+ completeParentFd = binding.openBeneath(rootFd, params.relativeParentPath, directoryFlags).fd;
168
+ }
169
+ catch (error) {
170
+ if (error?.code !== "ENOENT" || !params.mkdir)
171
+ throw error;
172
+ }
173
+ if (completeParentFd !== undefined) {
174
+ const parentFd = completeParentFd;
175
+ try {
176
+ let admitted = retainedTargetPath
177
+ ? tryDescribePolicyPosixParent(parentFd, parentSpelling)
178
+ : undefined;
179
+ if (!admitted) {
180
+ retainedTargetPath = undefined;
181
+ admitted = await describePosixParent(parentFd, parentSpelling);
182
+ }
183
+ const targetPath = path.join(admitted.parentPath, params.basename);
184
+ await authorizePinnedMutation(params, {
185
+ targetPath,
186
+ mutationPath: targetPath,
187
+ phase: "parent",
188
+ });
189
+ assertPosixParentCurrent(admitted);
190
+ return admitted;
191
+ }
192
+ catch (error) {
193
+ fsSync.closeSync(parentFd);
194
+ throw error;
195
+ }
196
+ }
197
+ // The canonical preflight spelling has no intentional symlinks in its
198
+ // existing prefix. Walk missing-parent cases one direct child at a time so
199
+ // every existing/opened object is authorized and every mkdir is authorized
200
+ // before dispatch. O_NOFOLLOW makes a concurrently introduced link fail
201
+ // before the next component can be created through it.
202
+ const secureDirectoryFlags = directoryFlags | (fsSync.constants.O_NOFOLLOW ?? 0);
203
+ let currentFd = rootFd;
204
+ let currentOwnedFd;
205
+ let currentPath = params.rootPath;
206
+ let current = retainedTargetPath
207
+ ? tryDescribePolicyPosixParent(rootFd, currentPath)
208
+ : undefined;
209
+ if (!current) {
210
+ retainedTargetPath = undefined;
211
+ current = await describePosixParent(rootFd, currentPath);
212
+ }
213
+ try {
214
+ await authorizePinnedMutation(params, {
215
+ targetPath: retainedTargetPath ?? initialTarget,
216
+ mutationPath: retainedTargetPath ?? initialTarget,
217
+ phase: "parent",
218
+ });
219
+ assertPosixParentCurrent(current);
220
+ for (let index = 0; index < segments.length; index += 1) {
221
+ const segment = segments[index];
222
+ const childPath = path.join(currentPath, segment);
223
+ let childFd;
224
+ let createdByMkdir = false;
225
+ let createReceipt;
226
+ try {
227
+ childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
228
+ }
229
+ catch (openError) {
230
+ if (!isNotFoundPathError(openError)) {
231
+ throw normalizePolicyParentOpenError(openError, params);
232
+ }
233
+ const targetPath = retainedTargetPath ?? prospectiveTargetPath(currentPath, segmentRoute, index, params.basename);
234
+ const request = Object.freeze({
235
+ targetPath,
236
+ mutationPath: childPath,
237
+ phase: "parent-create",
238
+ });
239
+ createReceipt = params.mutationAdmission?.tryAuthorizeAtParent?.(request, current.observation);
240
+ if (!createReceipt) {
241
+ createReceipt = await authorizePinnedMutation(params, request);
242
+ assertPosixParentCurrent(current);
243
+ }
244
+ params.assertBeforeMutation?.();
245
+ // Do not carry pathname freshness across the live authority callback.
246
+ assertPosixParentCurrent(current);
247
+ createdByMkdir = mkdirPolicyChild(binding, currentFd, segment, 0o777);
248
+ try {
249
+ childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
250
+ }
251
+ catch (createdOpenError) {
252
+ throw normalizePolicyParentOpenError(createdOpenError, params);
253
+ }
254
+ }
255
+ let child;
256
+ try {
257
+ const acceleratedChild = retainedTargetPath
258
+ ? tryDescribePolicyPosixParent(childFd, childPath)
259
+ : undefined;
260
+ if (acceleratedChild) {
261
+ child = acceleratedChild;
262
+ }
263
+ else {
264
+ retainedTargetPath = undefined;
265
+ child = await describePosixParent(childFd, childPath);
266
+ }
267
+ if (!sameAbsolutePath(child.parentPath, childPath))
268
+ retainedTargetPath = undefined;
269
+ let childAuthorization;
270
+ if (createdByMkdir && createReceipt && params.mutationAdmission?.advanceCreatedDirectory) {
271
+ let evidence;
272
+ try {
273
+ const parent = current.policyDirectory
274
+ ? refreshPolicyStagedDirectoryObservation(current.policyDirectory)
275
+ : checkedMutationDirectory(currentPath, current.directory.realPath, assertStagedDirectoryCurrent(current.directory));
276
+ evidence = Object.freeze({
277
+ admission: createReceipt,
278
+ parent,
279
+ child: child.observation,
280
+ });
281
+ }
282
+ catch {
283
+ // Leave failed evidence to the full ordered admission below.
284
+ }
285
+ if (evidence) {
286
+ childAuthorization = params.mutationAdmission.advanceCreatedDirectory(evidence);
287
+ }
288
+ }
289
+ const targetPath = retainedTargetPath ?? prospectiveTargetPath(child.parentPath, segmentRoute, index + 1, params.basename);
290
+ const request = Object.freeze({
291
+ targetPath,
292
+ mutationPath: targetPath,
293
+ phase: "parent",
294
+ });
295
+ childAuthorization ??= params.mutationAdmission?.tryAuthorizeAtParent?.(request, child.observation);
296
+ if (!childAuthorization) {
297
+ await authorizePinnedMutation(params, request);
298
+ assertPosixParentCurrent(child);
299
+ }
300
+ }
301
+ catch (error) {
302
+ fsSync.closeSync(childFd);
303
+ throw error;
304
+ }
305
+ const previousOwnedFd = currentOwnedFd;
306
+ currentOwnedFd = child.parentFd;
307
+ currentFd = child.parentFd;
308
+ currentPath = child.parentPath;
309
+ current = child;
310
+ if (previousOwnedFd !== undefined)
311
+ fsSync.closeSync(previousOwnedFd);
312
+ if (index === segments.length - 1) {
313
+ currentOwnedFd = undefined;
314
+ return child;
315
+ }
316
+ }
317
+ // Empty relative parents are handled by the complete-parent fast path.
318
+ throw new FsSafeError("path-mismatch", "native write parent admission did not complete");
319
+ }
320
+ finally {
321
+ if (currentOwnedFd !== undefined)
322
+ fsSync.closeSync(currentOwnedFd);
323
+ }
324
+ }
63
325
  export async function runPinnedWriteNative(binding, params) {
64
326
  const env_1 = { stack: [], error: void 0, hasError: false };
65
327
  try {
@@ -68,7 +330,12 @@ export async function runPinnedWriteNative(binding, params) {
68
330
  assertNativeStaging(binding);
69
331
  }
70
332
  const directoryFlags = fsSync.constants.O_RDONLY | (fsSync.constants.O_DIRECTORY ?? 0);
71
- const root = await fs.open(params.rootPath, directoryFlags);
333
+ const rootAdmission = await openNativeRootAdmission(binding, {
334
+ rootPath: params.rootPath,
335
+ rootIdentity: params.rootIdentity,
336
+ operation: "native write",
337
+ });
338
+ const root = rootAdmission.root;
72
339
  const posixRoot = __addDisposableResource(env_1, windows ? undefined : root, true);
73
340
  let parentFd;
74
341
  let windowsOwnsDirectories = false;
@@ -82,44 +349,37 @@ export async function runPinnedWriteNative(binding, params) {
82
349
  },
83
350
  }, false);
84
351
  try {
85
- const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
86
- ? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
87
- let rootMatches;
88
- if (exactRoot) {
89
- inspectFileIdentitySync(() => fsSync.fstatSync(root.fd, { bigint: true }), exactRoot);
90
- rootMatches = true;
91
- }
92
- else if (windows) {
93
- const identity = binding.fstatIdentity(root.fd);
94
- rootMatches = !params.rootIdentity || sameNativeIdentity(params.rootIdentity, identity);
352
+ let parentPath;
353
+ let directory;
354
+ let parentPathStat;
355
+ let policyParentAdmitted = false;
356
+ if (!windows && params.mutationAdmission) {
357
+ const admitted = await capturePolicyAwarePosixParent(binding, params, root.fd, directoryFlags);
358
+ parentFd = admitted.parentFd;
359
+ parentPath = admitted.parentPath;
360
+ directory = admitted.directory;
361
+ parentPathStat = admitted.parentPathStat;
362
+ policyParentAdmitted = true;
95
363
  }
96
364
  else {
97
- const identity = fsSync.fstatSync(root.fd, { bigint: true });
98
- rootMatches = !params.rootIdentity || exactIdentityMatches(params.rootIdentity, identity);
99
- }
100
- if (!rootMatches) {
101
- throw new FsSafeError("path-mismatch", "root path changed during native write");
102
- }
103
- if (params.mkdir) {
104
- params.assertBeforeMutation?.();
105
- binding.mkdirBeneath(root.fd, params.relativeParentPath, 0o777);
106
- }
107
- parentFd = binding.openBeneath(root.fd, params.relativeParentPath, directoryFlags).fd;
108
- const parentPath = realpathSync.native(params.relativeParentPath
109
- ? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
110
- : params.rootPath);
111
- const directory = windows ? undefined : describeStagedDirectory(parentFd, parentPath);
112
- const parentPathStat = exactRoot
113
- ? await inspectDirectoryIdentity(parentPath, inspectFileIdentitySync(() => fsSync.fstatSync(parentFd, { bigint: true })))
114
- : fsSync.lstatSync(parentPath);
115
- if (windows && !exactRoot) {
116
- const parentIdentity = binding.fstatIdentity(parentFd);
117
- if (parentPathStat.isSymbolicLink() || !sameNativeIdentity(parentPathStat, parentIdentity)) {
118
- throw new FsSafeError("path-mismatch", "native write parent changed during resolution");
365
+ if (params.mkdir) {
366
+ params.assertBeforeMutation?.();
367
+ binding.mkdirBeneath(root.fd, params.relativeParentPath, 0o777);
119
368
  }
369
+ const admitted = await openNativeParentAdmission(binding, rootAdmission, params.relativeParentPath);
370
+ parentFd = admitted.fd;
371
+ parentPath = admitted.guard.realPath;
372
+ directory = admitted.stagedDirectory;
373
+ parentPathStat = admitted.guard.stat;
120
374
  }
121
- else if (!windows && (parentPathStat.isSymbolicLink() || !exactIdentityMatches(parentPathStat, directory.identity))) {
122
- throw new FsSafeError("path-mismatch", "native write parent changed during resolution");
375
+ if (!policyParentAdmitted && params.mutationAdmission) {
376
+ const targetPath = path.join(parentPath, params.basename);
377
+ await authorizePinnedMutation(params, {
378
+ targetPath,
379
+ mutationPath: targetPath,
380
+ phase: "parent",
381
+ });
382
+ await inspectDirectoryIdentity(parentPath, inspectFileIdentitySync(() => fsSync.fstatSync(parentFd, { bigint: true })));
123
383
  }
124
384
  const verificationGuard = { dir: parentPath, realPath: parentPath, stat: parentPathStat };
125
385
  if (params.overwrite === false) {
@@ -145,7 +405,16 @@ export async function runPinnedWriteNative(binding, params) {
145
405
  finally {
146
406
  if (windows && !windowsOwnsDirectories) {
147
407
  if (parentFd !== undefined) {
148
- fsSync.closeSync(parentFd);
408
+ // Match the Windows leaf cleanup contract: an admission or identity
409
+ // failure remains primary, while every owned directory is still given
410
+ // a close attempt. In particular, a parent close failure must not skip
411
+ // the root FileHandle close.
412
+ try {
413
+ fsSync.closeSync(parentFd);
414
+ }
415
+ catch {
416
+ // Best effort while propagating the operation failure.
417
+ }
149
418
  }
150
419
  await root.close().catch(() => undefined);
151
420
  }
@@ -1 +1 @@
1
- {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAiDrC"}
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CA2DrC"}
package/dist/output.js CHANGED
@@ -7,6 +7,7 @@ import { fitFileNameToPortableComponent, sanitizeUntrustedFileName } from "./fil
7
7
  import { isPathInside } from "./path.js";
8
8
  import { root } from "./root.js";
9
9
  import { tempFile } from "./temp-target.js";
10
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
10
11
  const NON_PORTABLE_FILE_NAME_CHARACTERS = /[\u0000-\u001f\u007f-\u009f<>:"/\\|?*]/u;
11
12
  function tempFileNameForTarget(targetPath, fallbackFileName) {
12
13
  const fallback = sanitizeUntrustedFileName(fallbackFileName ?? "output.bin", "output.bin");
@@ -51,8 +52,12 @@ function assertFileTargetPath(targetPath) {
51
52
  }
52
53
  export async function writeExternalFileWithinRoot(options) {
53
54
  const maxBytes = normalizeMaxBytes(options.maxBytes);
54
- const targetRoot = await root(options.rootDir);
55
+ const rootDir = options.rootDir;
56
+ assertNoWindowsPathAlias(rootDir, "filesystem", "output root uses a Windows filesystem namespace alias");
55
57
  const requestedTargetPath = options.path;
58
+ assertNoWindowsPathAlias(path.dirname(requestedTargetPath), "filesystem", "output target parent uses a Windows filesystem namespace alias");
59
+ const producerIsolation = options.producerIsolation;
60
+ const targetRoot = await root(rootDir);
56
61
  if (requestedTargetPath.length === 0) {
57
62
  throw new FsSafeError("invalid-path", "target path is required");
58
63
  }
@@ -64,7 +69,9 @@ export async function writeExternalFileWithinRoot(options) {
64
69
  });
65
70
  assertFileTargetPath(rawTargetPath);
66
71
  const targetPath = sanitizedTargetPath(rawTargetPath, options.fallbackFileName);
72
+ assertNoWindowsPathAlias(targetPath, "filesystem", "output target uses a Windows filesystem namespace alias");
67
73
  const finalPath = await targetRoot.resolve(targetPath);
74
+ assertNoWindowsPathAlias(finalPath, "filesystem", "output target uses a Windows filesystem namespace alias");
68
75
  if (options.staging === "sibling") {
69
76
  const parentPath = path.dirname(targetPath);
70
77
  if (parentPath !== ".") {
@@ -74,7 +81,7 @@ export async function writeExternalFileWithinRoot(options) {
74
81
  const result = await writeExternalFileViaSibling({
75
82
  finalPath: siblingFinalPath,
76
83
  write: options.write,
77
- producerIsolation: options.producerIsolation,
84
+ producerIsolation,
78
85
  fallbackFileName: options.fallbackFileName,
79
86
  maxBytes,
80
87
  mode: options.mode,
@@ -99,7 +106,7 @@ export async function writeExternalFileWithinRoot(options) {
99
106
  await staged.cleanup();
100
107
  }
101
108
  }
102
- function buildSiblingTempPath(targetPath, fallbackFileName) {
109
+ function buildSiblingTempName(targetPath, fallbackFileName) {
103
110
  const prefix = `.fs-safe-output-${process.pid}-${randomUUID()}-`;
104
111
  const suffix = ".part";
105
112
  const safeTail = fitFileNameToPortableComponent({
@@ -107,12 +114,15 @@ function buildSiblingTempPath(targetPath, fallbackFileName) {
107
114
  fileName: tempFileNameForTarget(targetPath, fallbackFileName),
108
115
  suffix,
109
116
  });
110
- return path.join(path.dirname(targetPath), `${prefix}${safeTail}${suffix}`);
117
+ return `${prefix}${safeTail}${suffix}`;
111
118
  }
112
119
  async function writeExternalFileViaSibling(params) {
120
+ assertNoWindowsPathAlias(params.finalPath, "filesystem", "output target uses a Windows filesystem namespace alias");
113
121
  const finalPath = path.resolve(params.finalPath);
122
+ assertNoWindowsPathAlias(finalPath, "filesystem", "output target uses a Windows filesystem namespace alias");
114
123
  const { result } = await writeCallbackSibling({
115
- tempPath: buildSiblingTempPath(finalPath, params.fallbackFileName),
124
+ tempDir: path.dirname(finalPath),
125
+ tempName: buildSiblingTempName(finalPath, params.fallbackFileName),
116
126
  write: params.write,
117
127
  producerIsolation: params.producerIsolation,
118
128
  resolveFinalPath: () => finalPath,
@@ -1 +1 @@
1
- {"version":3,"file":"overwrite-file-handle.d.ts","sourceRoot":"","sources":["../src/overwrite-file-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,MAAM,MAAM,0BAA0B,GAAG;IACvC,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAEF,4FAA4F;AAC5F,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAoCf"}
1
+ {"version":3,"file":"overwrite-file-handle.d.ts","sourceRoot":"","sources":["../src/overwrite-file-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,MAAM,MAAM,0BAA0B,GAAG;IACvC,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAEF,4FAA4F;AAC5F,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CA2Cf"}
@@ -4,6 +4,10 @@ import { readFileWindowFully } from "./positional-read.js";
4
4
  import { writeAllToFile } from "./write-file-handle.js";
5
5
  /** Overwrite a borrowed, non-append read/write handle with best-effort failure rollback. */
6
6
  export async function overwriteFileHandle(handle, data, options = {}) {
7
+ // Snapshot caller-owned admission before the first await. Preserve the
8
+ // former method-call receiver while preventing a later getter mutation from
9
+ // replacing authority after rollback preparation.
10
+ const beforeWrite = options.beforeWrite;
7
11
  const payload = data.subarray(0, data.byteLength);
8
12
  const payloadBytes = payload.byteLength;
9
13
  const stat = await handle.stat();
@@ -19,7 +23,7 @@ export async function overwriteFileHandle(handle, data, options = {}) {
19
23
  if (await readFileWindowFully(handle, originalPrefix, 0) !== prefixBytes) {
20
24
  throw new FsSafeError("read-failed", "file ended before overwrite rollback preparation completed");
21
25
  }
22
- assertSynchronousCallbackResult(options.beforeWrite?.(), "beforeWrite");
26
+ assertSynchronousCallbackResult(beforeWrite === undefined ? undefined : Reflect.apply(beforeWrite, options, []), "beforeWrite");
23
27
  let prefixStarted = false;
24
28
  try {
25
29
  // Leave existing bytes intact until any additional storage has been written.
@@ -1 +1 @@
1
- {"version":3,"file":"owner-dacl.d.ts","sourceRoot":"","sources":["../src/owner-dacl.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,KAAK,EAAE,eAAe,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B;IACE,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,IAAI,EAAE,yBAAyB,EAAE,CAAC;CACnC,GACD;IACE,MAAM,EAAE,sBAAsB,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC3B,CAAC;AAEN,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,kBAAkB,CA6BvE"}
1
+ {"version":3,"file":"owner-dacl.d.ts","sourceRoot":"","sources":["../src/owner-dacl.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,KAAK,EAAE,eAAe,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B;IACE,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,IAAI,EAAE,yBAAyB,EAAE,CAAC;CACnC,GACD;IACE,MAAM,EAAE,sBAAsB,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC3B,CAAC;AAEN,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,kBAAkB,CAmCvE"}
@@ -1,9 +1,11 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  import { getNativeBinding } from "./native.js";
3
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
3
4
  export function readOwnerAndDacl(targetPath) {
4
5
  if (process.platform !== "win32") {
5
6
  return { status: "unsupported-platform", platform: process.platform };
6
7
  }
8
+ assertNoWindowsPathAlias(targetPath, "filesystem", "owner and DACL path uses a Windows filesystem namespace alias");
7
9
  const native = getNativeBinding();
8
10
  if (!native) {
9
11
  throw new FsSafeError("helper-unavailable", "Windows owner and DACL facts require the matching optional native platform package; " +
@@ -1 +1 @@
1
- {"version":3,"file":"path-policy.d.ts","sourceRoot":"","sources":["../src/path-policy.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,gBAAgB,CAAC;AAIxB,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAElD,eAAO,MAAM,mBAAmB;;;;;;;;;CAA2B,CAAC;AAE5D,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB;AAED,wBAAsB,2BAA2B,CAAC,MAAM,EAAE;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBhB"}
1
+ {"version":3,"file":"path-policy.d.ts","sourceRoot":"","sources":["../src/path-policy.ts"],"names":[],"mappings":"AACA,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,gBAAgB,CAAC;AAKxB,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAElD,eAAO,MAAM,mBAAmB;;;;;;;;;CAA2B,CAAC;AAE5D,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB;AAED,wBAAsB,2BAA2B,CAAC,MAAM,EAAE;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBhB"}
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import { ROOT_PATH_ALIAS_POLICIES, resolveRootPath, } from "./root-path.js";
3
3
  import { isNotFoundPathError } from "./path.js";
4
4
  import { shortPath } from "./error-detail.js";
5
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
5
6
  export const PATH_ALIAS_POLICIES = ROOT_PATH_ALIAS_POLICIES;
6
7
  export async function assertNoPathAliasEscape(params) {
7
8
  const resolved = await resolveRootPath({
@@ -25,9 +26,13 @@ export async function assertNoHardlinkedFinalPath(params) {
25
26
  if (params.allowFinalHardlinkForUnlink) {
26
27
  return;
27
28
  }
29
+ const filePath = params.filePath;
30
+ const root = params.root;
31
+ const boundaryLabel = params.boundaryLabel;
32
+ assertNoWindowsPathAlias(filePath);
28
33
  let stat;
29
34
  try {
30
- stat = fs.statSync(params.filePath);
35
+ stat = fs.statSync(filePath);
31
36
  }
32
37
  catch (err) {
33
38
  if (isNotFoundPathError(err)) {
@@ -39,6 +44,6 @@ export async function assertNoHardlinkedFinalPath(params) {
39
44
  return;
40
45
  }
41
46
  if (stat.nlink > 1) {
42
- throw new Error(`Hardlinked path is not allowed under ${params.boundaryLabel} (${shortPath(params.root)}): ${shortPath(params.filePath)}`);
47
+ throw new Error(`Hardlinked path is not allowed under ${boundaryLabel} (${shortPath(root)}): ${shortPath(filePath)}`);
43
48
  }
44
49
  }
@@ -0,0 +1,7 @@
1
+ export type ResolvedPathPrefix = {
2
+ readonly absolutePath: string;
3
+ readonly existingPath: string;
4
+ readonly unresolvedSegments: readonly string[];
5
+ };
6
+ export declare function resolvePathPrefixSync(input: string): ResolvedPathPrefix;
7
+ //# sourceMappingURL=path-prefix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-prefix.d.ts","sourceRoot":"","sources":["../src/path-prefix.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD,CAAC;AAkBF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAsDvE"}
@@ -0,0 +1,82 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { formatErrorDetail } from "./error-detail.js";
4
+ import { assertNoNulPathInput, hasNodeErrorCode } from "./path.js";
5
+ import { realpathSync } from "./realpath.js";
6
+ import { absolutePathWithRawSegments } from "./root-path-existing.js";
7
+ import { pathForWindowsFilesystem } from "./windows-path-alias.js";
8
+ function rawRoot(absolutePath) {
9
+ if (path.sep === "\\" && /^\\\\[?.]\\UNC\\/i.test(absolutePath)) {
10
+ // Node parses only the namespace prefix as the root of an extended UNC path.
11
+ const shareRoot = path.parse(`\\\\${absolutePath.slice(8)}`).root;
12
+ return absolutePath.slice(0, shareRoot.length + 6);
13
+ }
14
+ return path.parse(absolutePath).root;
15
+ }
16
+ function resolutionError(code, absolutePath) {
17
+ const detail = code === "ELOOP" ? "too many symbolic links" : "not a directory";
18
+ return Object.assign(new Error(`${code}: ${detail}, resolvePathPrefix ${formatErrorDetail(absolutePath)}`), {
19
+ code, syscall: "resolvePathPrefix", path: absolutePath,
20
+ });
21
+ }
22
+ export function resolvePathPrefixSync(input) {
23
+ assertNoNulPathInput(input);
24
+ const absolutePath = absolutePathWithRawSegments(input);
25
+ let resolved = rawRoot(absolutePath);
26
+ const remaining = absolutePath.slice(resolved.length).split(path.sep);
27
+ const visitedStates = new Set();
28
+ let symlinkHops = 0;
29
+ while (remaining.length > 0) {
30
+ const segment = remaining.shift();
31
+ if (segment === "")
32
+ continue;
33
+ if (segment === "." || segment === "..") {
34
+ const atRoot = resolved === rawRoot(resolved);
35
+ const lookup = segment === ".." && atRoot ? "." : segment;
36
+ // lstat(dir) does not test search permission, and realpath may erase dots.
37
+ const operationBase = pathForWindowsFilesystem(resolved);
38
+ fs.lstatSync(`${operationBase}${operationBase.endsWith(path.sep) ? "" : path.sep}${lookup}`, { bigint: true });
39
+ if (segment === ".." && !atRoot)
40
+ resolved = path.dirname(resolved);
41
+ continue;
42
+ }
43
+ const candidate = path.join(resolved, segment);
44
+ let stat;
45
+ try {
46
+ stat = fs.lstatSync(candidate, { bigint: true });
47
+ }
48
+ catch (error) {
49
+ if (!hasNodeErrorCode(error, "ENOENT"))
50
+ throw error;
51
+ return {
52
+ absolutePath,
53
+ existingPath: realpathSync.native(pathForWindowsFilesystem(resolved)),
54
+ unresolvedSegments: [segment, ...remaining],
55
+ };
56
+ }
57
+ if (!stat.isSymbolicLink()) {
58
+ if (!stat.isDirectory() && remaining.length > 0)
59
+ throw resolutionError("ENOTDIR", absolutePath);
60
+ resolved = candidate;
61
+ continue;
62
+ }
63
+ const state = JSON.stringify([String(stat.dev), String(stat.ino), candidate, remaining]);
64
+ if (symlinkHops >= 64 || visitedStates.has(state))
65
+ throw resolutionError("ELOOP", absolutePath);
66
+ visitedStates.add(state);
67
+ symlinkHops++;
68
+ const target = fs.readlinkSync(candidate);
69
+ const rawTarget = path.sep === "\\" ? target.replaceAll("/", "\\") : target;
70
+ if (path.isAbsolute(rawTarget)) {
71
+ const targetRoot = rawRoot(rawTarget);
72
+ // A rooted Windows link target uses the link's drive/share, not cwd's.
73
+ resolved = path.sep === "\\" && targetRoot === "\\" ? rawRoot(resolved) : targetRoot;
74
+ remaining.unshift(...rawTarget.slice(targetRoot.length).split(path.sep));
75
+ }
76
+ else {
77
+ // A target's parent traversal applies after resolving its preceding links.
78
+ remaining.unshift(...rawTarget.split(path.sep));
79
+ }
80
+ }
81
+ return { absolutePath, existingPath: realpathSync.native(pathForWindowsFilesystem(resolved)), unresolvedSegments: [] };
82
+ }