@openclaw/fs-safe 0.12.0 → 0.13.1

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 (395) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +12 -7
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +32 -11
  5. package/dist/advanced.d.ts +2 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +2 -0
  8. package/dist/archive-input.d.ts.map +1 -1
  9. package/dist/archive-input.js +6 -2
  10. package/dist/archive-merge.d.ts.map +1 -1
  11. package/dist/archive-merge.js +15 -2
  12. package/dist/archive-native.d.ts.map +1 -1
  13. package/dist/archive-native.js +7 -19
  14. package/dist/archive-plan.js +1 -1
  15. package/dist/archive-read.d.ts.map +1 -1
  16. package/dist/archive-read.js +18 -13
  17. package/dist/archive-staging.d.ts.map +1 -1
  18. package/dist/archive-staging.js +43 -10
  19. package/dist/archive-tar-inspect.d.ts.map +1 -1
  20. package/dist/archive-tar-inspect.js +4 -1
  21. package/dist/archive-zip-admission.d.ts +1 -1
  22. package/dist/archive-zip-admission.d.ts.map +1 -1
  23. package/dist/archive-zip-admission.js +2 -2
  24. package/dist/archive-zip-directory.d.ts +3 -0
  25. package/dist/archive-zip-directory.d.ts.map +1 -1
  26. package/dist/archive-zip-directory.js +13 -2
  27. package/dist/archive-zip-loader.d.ts +3 -2
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +30 -4
  30. package/dist/archive-zip-manifest.d.ts +5 -0
  31. package/dist/archive-zip-manifest.d.ts.map +1 -0
  32. package/dist/archive-zip-manifest.js +22 -0
  33. package/dist/archive-zip-names.d.ts +6 -1
  34. package/dist/archive-zip-names.d.ts.map +1 -1
  35. package/dist/archive-zip-names.js +35 -14
  36. package/dist/archive-zip-preflight.d.ts.map +1 -1
  37. package/dist/archive-zip-preflight.js +3 -2
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +9 -4
  40. package/dist/copy-file-input.d.ts +1 -1
  41. package/dist/copy-file-input.d.ts.map +1 -1
  42. package/dist/copy-file-input.js +2 -0
  43. package/dist/darwin-acl.d.ts +4 -0
  44. package/dist/darwin-acl.d.ts.map +1 -0
  45. package/dist/darwin-acl.js +24 -0
  46. package/dist/deny-mutations.d.ts.map +1 -1
  47. package/dist/deny-mutations.js +8 -2
  48. package/dist/directory-durability.d.ts.map +1 -1
  49. package/dist/directory-durability.js +67 -23
  50. package/dist/directory-entry-path.d.ts +3 -0
  51. package/dist/directory-entry-path.d.ts.map +1 -0
  52. package/dist/directory-entry-path.js +21 -0
  53. package/dist/directory-guard.d.ts +17 -1
  54. package/dist/directory-guard.d.ts.map +1 -1
  55. package/dist/directory-guard.js +134 -48
  56. package/dist/directory-mode-node.d.ts +12 -0
  57. package/dist/directory-mode-node.d.ts.map +1 -1
  58. package/dist/directory-mode-node.js +102 -4
  59. package/dist/effective-uid.d.ts +2 -0
  60. package/dist/effective-uid.d.ts.map +1 -0
  61. package/dist/effective-uid.js +25 -0
  62. package/dist/file-handle-transfer.d.ts.map +1 -1
  63. package/dist/file-handle-transfer.js +98 -27
  64. package/dist/file-hash.d.ts.map +1 -1
  65. package/dist/file-hash.js +3 -0
  66. package/dist/file-lock-sync.d.ts.map +1 -1
  67. package/dist/file-lock-sync.js +36 -6
  68. package/dist/file-lock.d.ts.map +1 -1
  69. package/dist/file-lock.js +29 -9
  70. package/dist/file-observation.d.ts +1 -1
  71. package/dist/file-observation.d.ts.map +1 -1
  72. package/dist/file-store-boundary.d.ts +6 -2
  73. package/dist/file-store-boundary.d.ts.map +1 -1
  74. package/dist/file-store-boundary.js +20 -65
  75. package/dist/file-store-copy-source.d.ts +5 -0
  76. package/dist/file-store-copy-source.d.ts.map +1 -0
  77. package/dist/file-store-copy-source.js +31 -0
  78. package/dist/file-store-path.d.ts.map +1 -1
  79. package/dist/file-store-path.js +4 -1
  80. package/dist/file-store-sync-directory.d.ts +16 -0
  81. package/dist/file-store-sync-directory.d.ts.map +1 -0
  82. package/dist/file-store-sync-directory.js +349 -0
  83. package/dist/file-store.d.ts.map +1 -1
  84. package/dist/file-store.js +11 -28
  85. package/dist/filename.d.ts.map +1 -1
  86. package/dist/filename.js +65 -20
  87. package/dist/fs.d.ts.map +1 -1
  88. package/dist/fs.js +3 -2
  89. package/dist/guarded-mkdir.d.ts +8 -0
  90. package/dist/guarded-mkdir.d.ts.map +1 -1
  91. package/dist/guarded-mkdir.js +176 -27
  92. package/dist/guest.d.ts.map +1 -1
  93. package/dist/guest.js +4 -1
  94. package/dist/home-dir.d.ts.map +1 -1
  95. package/dist/home-dir.js +73 -10
  96. package/dist/install-path.d.ts.map +1 -1
  97. package/dist/install-path.js +54 -19
  98. package/dist/json-durable-queue-ownership.d.ts +6 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  100. package/dist/json-durable-queue-ownership.js +89 -42
  101. package/dist/json-durable-queue-paths.d.ts +11 -0
  102. package/dist/json-durable-queue-paths.d.ts.map +1 -0
  103. package/dist/json-durable-queue-paths.js +42 -0
  104. package/dist/json-durable-queue-read.d.ts.map +1 -1
  105. package/dist/json-durable-queue-read.js +2 -0
  106. package/dist/json-durable-queue.d.ts.map +1 -1
  107. package/dist/json-durable-queue.js +50 -58
  108. package/dist/json-store.d.ts.map +1 -1
  109. package/dist/json-store.js +5 -1
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +54 -22
  112. package/dist/local-file-access.d.ts.map +1 -1
  113. package/dist/local-file-access.js +4 -0
  114. package/dist/local-file-descriptor.d.ts +17 -0
  115. package/dist/local-file-descriptor.d.ts.map +1 -0
  116. package/dist/local-file-descriptor.js +84 -0
  117. package/dist/local-roots.d.ts.map +1 -1
  118. package/dist/local-roots.js +35 -7
  119. package/dist/move-path-cleanup.d.ts +2 -0
  120. package/dist/move-path-cleanup.d.ts.map +1 -1
  121. package/dist/move-path-cleanup.js +44 -18
  122. package/dist/move-path.d.ts.map +1 -1
  123. package/dist/move-path.js +31 -6
  124. package/dist/native-binding.d.ts +17 -0
  125. package/dist/native-binding.d.ts.map +1 -1
  126. package/dist/native-binding.js +8 -1
  127. package/dist/native-directory-observation.d.ts +17 -0
  128. package/dist/native-directory-observation.d.ts.map +1 -0
  129. package/dist/native-directory-observation.js +37 -0
  130. package/dist/native-operations.d.ts.map +1 -1
  131. package/dist/native-operations.js +6 -4
  132. package/dist/native-parent-admission.d.ts +32 -0
  133. package/dist/native-parent-admission.d.ts.map +1 -0
  134. package/dist/native-parent-admission.js +127 -0
  135. package/dist/native-pinned-write-windows.d.ts +0 -1
  136. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  137. package/dist/native-pinned-write-windows.js +8 -9
  138. package/dist/native-pinned-write.d.ts.map +1 -1
  139. package/dist/native-pinned-write.js +314 -42
  140. package/dist/native-staged-file.d.ts +4 -4
  141. package/dist/native-staged-file.d.ts.map +1 -1
  142. package/dist/native-staged-file.js +20 -10
  143. package/dist/native.d.ts +1 -1
  144. package/dist/native.d.ts.map +1 -1
  145. package/dist/native.js +7 -2
  146. package/dist/output.d.ts.map +1 -1
  147. package/dist/output.js +15 -5
  148. package/dist/overwrite-file-handle.d.ts.map +1 -1
  149. package/dist/overwrite-file-handle.js +5 -1
  150. package/dist/owner-dacl.d.ts.map +1 -1
  151. package/dist/owner-dacl.js +2 -0
  152. package/dist/path-policy.d.ts.map +1 -1
  153. package/dist/path-policy.js +7 -2
  154. package/dist/path-prefix.d.ts +7 -0
  155. package/dist/path-prefix.d.ts.map +1 -0
  156. package/dist/path-prefix.js +82 -0
  157. package/dist/path-scope-lexical.d.ts.map +1 -1
  158. package/dist/path-scope-lexical.js +18 -8
  159. package/dist/path-segment-route.d.ts +7 -0
  160. package/dist/path-segment-route.d.ts.map +1 -0
  161. package/dist/path-segment-route.js +24 -0
  162. package/dist/path-suffix-aliases.d.ts +10 -0
  163. package/dist/path-suffix-aliases.d.ts.map +1 -0
  164. package/dist/path-suffix-aliases.js +386 -0
  165. package/dist/path.d.ts.map +1 -1
  166. package/dist/path.js +16 -5
  167. package/dist/permissions-windows.d.ts.map +1 -1
  168. package/dist/permissions-windows.js +14 -3
  169. package/dist/permissions.d.ts.map +1 -1
  170. package/dist/permissions.js +37 -8
  171. package/dist/pinned-mutation-admission.d.ts +25 -0
  172. package/dist/pinned-mutation-admission.d.ts.map +1 -0
  173. package/dist/pinned-mutation-admission.js +425 -0
  174. package/dist/pinned-mutation-observation.d.ts +34 -0
  175. package/dist/pinned-mutation-observation.d.ts.map +1 -0
  176. package/dist/pinned-mutation-observation.js +142 -0
  177. package/dist/pinned-mutation-shared-route.d.ts +24 -0
  178. package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
  179. package/dist/pinned-mutation-shared-route.js +70 -0
  180. package/dist/pinned-open.d.ts +6 -0
  181. package/dist/pinned-open.d.ts.map +1 -1
  182. package/dist/pinned-open.js +28 -10
  183. package/dist/pinned-write-types.d.ts +75 -0
  184. package/dist/pinned-write-types.d.ts.map +1 -0
  185. package/dist/pinned-write-types.js +1 -0
  186. package/dist/pinned-write.d.ts +7 -33
  187. package/dist/pinned-write.d.ts.map +1 -1
  188. package/dist/pinned-write.js +151 -17
  189. package/dist/private-directory.d.ts.map +1 -1
  190. package/dist/private-directory.js +2 -0
  191. package/dist/private-producer-handoff.d.ts +16 -0
  192. package/dist/private-producer-handoff.d.ts.map +1 -0
  193. package/dist/private-producer-handoff.js +272 -0
  194. package/dist/private-temp-workspace.d.ts +2 -39
  195. package/dist/private-temp-workspace.d.ts.map +1 -1
  196. package/dist/private-temp-workspace.js +183 -77
  197. package/dist/publish-file.d.ts.map +1 -1
  198. package/dist/publish-file.js +38 -32
  199. package/dist/regular-file.d.ts.map +1 -1
  200. package/dist/regular-file.js +56 -45
  201. package/dist/replace-directory.d.ts.map +1 -1
  202. package/dist/replace-directory.js +12 -5
  203. package/dist/replace-file-temp-owner.d.ts +1 -0
  204. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  205. package/dist/replace-file-temp-owner.js +12 -1
  206. package/dist/replace-file.d.ts.map +1 -1
  207. package/dist/replace-file.js +24 -24
  208. package/dist/root-boundary.d.ts +4 -0
  209. package/dist/root-boundary.d.ts.map +1 -1
  210. package/dist/root-boundary.js +6 -1
  211. package/dist/root-context.d.ts +12 -1
  212. package/dist/root-context.d.ts.map +1 -1
  213. package/dist/root-context.js +57 -11
  214. package/dist/root-directory-creation.d.ts +13 -0
  215. package/dist/root-directory-creation.d.ts.map +1 -0
  216. package/dist/root-directory-creation.js +212 -0
  217. package/dist/root-directory-list.d.ts +16 -4
  218. package/dist/root-directory-list.d.ts.map +1 -1
  219. package/dist/root-directory-list.js +180 -39
  220. package/dist/root-directory.d.ts +23 -0
  221. package/dist/root-directory.d.ts.map +1 -0
  222. package/dist/root-directory.js +141 -0
  223. package/dist/root-errors.d.ts.map +1 -1
  224. package/dist/root-errors.js +12 -1
  225. package/dist/root-file-final-admission.d.ts +21 -0
  226. package/dist/root-file-final-admission.d.ts.map +1 -0
  227. package/dist/root-file-final-admission.js +83 -0
  228. package/dist/root-file.d.ts.map +1 -1
  229. package/dist/root-file.js +103 -24
  230. package/dist/root-impl.d.ts +1 -0
  231. package/dist/root-impl.d.ts.map +1 -1
  232. package/dist/root-impl.js +289 -317
  233. package/dist/root-move-noreplace.d.ts +14 -0
  234. package/dist/root-move-noreplace.d.ts.map +1 -0
  235. package/dist/root-move-noreplace.js +202 -0
  236. package/dist/root-observed-path.d.ts +9 -0
  237. package/dist/root-observed-path.d.ts.map +1 -0
  238. package/dist/root-observed-path.js +95 -0
  239. package/dist/root-path-errors.d.ts +12 -0
  240. package/dist/root-path-errors.d.ts.map +1 -0
  241. package/dist/root-path-errors.js +13 -0
  242. package/dist/root-path-existing.d.ts.map +1 -1
  243. package/dist/root-path-existing.js +53 -20
  244. package/dist/root-path-observation.d.ts +63 -0
  245. package/dist/root-path-observation.d.ts.map +1 -0
  246. package/dist/root-path-observation.js +180 -0
  247. package/dist/root-path-stat.d.ts +5 -0
  248. package/dist/root-path-stat.d.ts.map +1 -0
  249. package/dist/root-path-stat.js +101 -0
  250. package/dist/root-path-symlink.d.ts.map +1 -1
  251. package/dist/root-path-symlink.js +23 -4
  252. package/dist/root-path.d.ts +11 -0
  253. package/dist/root-path.d.ts.map +1 -1
  254. package/dist/root-path.js +215 -53
  255. package/dist/root-paths-lexical.d.ts +9 -0
  256. package/dist/root-paths-lexical.d.ts.map +1 -0
  257. package/dist/root-paths-lexical.js +22 -0
  258. package/dist/root-paths.d.ts +3 -25
  259. package/dist/root-paths.d.ts.map +1 -1
  260. package/dist/root-paths.js +77 -154
  261. package/dist/root-read-admission.d.ts +26 -0
  262. package/dist/root-read-admission.d.ts.map +1 -0
  263. package/dist/root-read-admission.js +94 -0
  264. package/dist/root-remove-identity.d.ts +15 -0
  265. package/dist/root-remove-identity.d.ts.map +1 -0
  266. package/dist/root-remove-identity.js +89 -0
  267. package/dist/root-remove-receipt.d.ts +17 -0
  268. package/dist/root-remove-receipt.d.ts.map +1 -0
  269. package/dist/root-remove-receipt.js +37 -0
  270. package/dist/root-remove.d.ts +2 -1
  271. package/dist/root-remove.d.ts.map +1 -1
  272. package/dist/root-remove.js +172 -10
  273. package/dist/root-write-admission.d.ts +68 -0
  274. package/dist/root-write-admission.d.ts.map +1 -0
  275. package/dist/root-write-admission.js +322 -0
  276. package/dist/root-write-compatibility.d.ts +13 -0
  277. package/dist/root-write-compatibility.d.ts.map +1 -0
  278. package/dist/root-write-compatibility.js +74 -0
  279. package/dist/root-write-complete-parent.d.ts +42 -0
  280. package/dist/root-write-complete-parent.d.ts.map +1 -0
  281. package/dist/root-write-complete-parent.js +195 -0
  282. package/dist/root-write-publication.d.ts +24 -0
  283. package/dist/root-write-publication.d.ts.map +1 -0
  284. package/dist/root-write-publication.js +85 -0
  285. package/dist/root-write-verification.d.ts.map +1 -1
  286. package/dist/root-write-verification.js +6 -4
  287. package/dist/secret-file.d.ts.map +1 -1
  288. package/dist/secret-file.js +62 -24
  289. package/dist/secret-read-async.d.ts.map +1 -1
  290. package/dist/secret-read-async.js +18 -13
  291. package/dist/secret-read-policy.d.ts.map +1 -1
  292. package/dist/secret-read-policy.js +5 -1
  293. package/dist/secure-file.d.ts.map +1 -1
  294. package/dist/secure-file.js +92 -7
  295. package/dist/secure-temp-dir.d.ts +6 -3
  296. package/dist/secure-temp-dir.d.ts.map +1 -1
  297. package/dist/secure-temp-dir.js +121 -101
  298. package/dist/secure-temp-repair.d.ts +35 -0
  299. package/dist/secure-temp-repair.d.ts.map +1 -0
  300. package/dist/secure-temp-repair.js +106 -0
  301. package/dist/sibling-staged-file.d.ts +3 -2
  302. package/dist/sibling-staged-file.d.ts.map +1 -1
  303. package/dist/sibling-staged-file.js +179 -55
  304. package/dist/sibling-temp.d.ts.map +1 -1
  305. package/dist/sibling-temp.js +27 -13
  306. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  307. package/dist/sidecar-lock-acquire.js +44 -14
  308. package/dist/sidecar-lock-root.d.ts.map +1 -1
  309. package/dist/sidecar-lock-root.js +4 -2
  310. package/dist/staged-directory.d.ts +14 -5
  311. package/dist/staged-directory.d.ts.map +1 -1
  312. package/dist/staged-directory.js +54 -3
  313. package/dist/standalone-publication-path.d.ts +2 -0
  314. package/dist/standalone-publication-path.d.ts.map +1 -0
  315. package/dist/standalone-publication-path.js +6 -0
  316. package/dist/stat-observation.d.ts +11 -0
  317. package/dist/stat-observation.d.ts.map +1 -0
  318. package/dist/stat-observation.js +64 -0
  319. package/dist/strict-file-identity.d.ts.map +1 -1
  320. package/dist/strict-file-identity.js +39 -2
  321. package/dist/symlink-parents.d.ts.map +1 -1
  322. package/dist/symlink-parents.js +7 -2
  323. package/dist/temp-target.d.ts +2 -0
  324. package/dist/temp-target.d.ts.map +1 -1
  325. package/dist/temp-target.js +130 -3
  326. package/dist/temp-workspace-admission.d.ts +22 -0
  327. package/dist/temp-workspace-admission.d.ts.map +1 -0
  328. package/dist/temp-workspace-admission.js +374 -0
  329. package/dist/temp-workspace-child-admission.d.ts +13 -0
  330. package/dist/temp-workspace-child-admission.d.ts.map +1 -0
  331. package/dist/temp-workspace-child-admission.js +95 -0
  332. package/dist/temp-workspace-descriptor.d.ts +48 -0
  333. package/dist/temp-workspace-descriptor.d.ts.map +1 -0
  334. package/dist/temp-workspace-descriptor.js +363 -0
  335. package/dist/temp-workspace-identity.d.ts +15 -0
  336. package/dist/temp-workspace-identity.d.ts.map +1 -0
  337. package/dist/temp-workspace-identity.js +41 -0
  338. package/dist/temp-workspace-owner.d.ts +7 -6
  339. package/dist/temp-workspace-owner.d.ts.map +1 -1
  340. package/dist/temp-workspace-owner.js +121 -61
  341. package/dist/temp-workspace-permissions.d.ts +4 -0
  342. package/dist/temp-workspace-permissions.d.ts.map +1 -0
  343. package/dist/temp-workspace-permissions.js +32 -0
  344. package/dist/temp-workspace-types.d.ts +40 -0
  345. package/dist/temp-workspace-types.d.ts.map +1 -0
  346. package/dist/temp-workspace-types.js +1 -0
  347. package/dist/temp.d.ts +1 -1
  348. package/dist/temp.d.ts.map +1 -1
  349. package/dist/temp.js +1 -1
  350. package/dist/test-hooks.d.ts +7 -0
  351. package/dist/test-hooks.d.ts.map +1 -1
  352. package/dist/text-atomic.d.ts.map +1 -1
  353. package/dist/text-atomic.js +3 -1
  354. package/dist/trash.d.ts.map +1 -1
  355. package/dist/trash.js +54 -8
  356. package/dist/walk.d.ts.map +1 -1
  357. package/dist/walk.js +9 -6
  358. package/dist/windows-owner.d.ts.map +1 -1
  359. package/dist/windows-owner.js +5 -0
  360. package/dist/windows-path-alias.d.ts +39 -0
  361. package/dist/windows-path-alias.d.ts.map +1 -0
  362. package/dist/windows-path-alias.js +153 -0
  363. package/docs/advanced.md +25 -0
  364. package/docs/archive.md +23 -2
  365. package/docs/atomic.md +27 -3
  366. package/docs/copy.md +3 -0
  367. package/docs/durability.md +14 -0
  368. package/docs/errors.md +14 -6
  369. package/docs/file-store.md +24 -3
  370. package/docs/filename.md +14 -7
  371. package/docs/install-path.md +2 -2
  372. package/docs/install.md +8 -7
  373. package/docs/json-store.md +5 -1
  374. package/docs/json.md +11 -0
  375. package/docs/mutation-policy-proof.md +65 -0
  376. package/docs/native-helper.md +41 -11
  377. package/docs/native.md +32 -8
  378. package/docs/output.md +33 -11
  379. package/docs/path-prefix.md +64 -0
  380. package/docs/path-suffix-aliases.md +159 -0
  381. package/docs/path.md +11 -0
  382. package/docs/private-file-store.md +14 -0
  383. package/docs/public-api.md +9 -0
  384. package/docs/quickstart.md +1 -1
  385. package/docs/reading.md +8 -1
  386. package/docs/root.md +18 -3
  387. package/docs/secret-file.md +3 -0
  388. package/docs/secure-file.md +6 -2
  389. package/docs/security-model.md +75 -8
  390. package/docs/sidecar-lock.md +28 -1
  391. package/docs/store.md +5 -1
  392. package/docs/temp.md +260 -36
  393. package/docs/test-hooks.md +14 -0
  394. package/docs/writing.md +38 -8
  395. package/package.json +10 -10
@@ -1,33 +1,44 @@
1
1
  import fsSync from "node:fs";
2
- import fs from "node:fs/promises";
3
2
  import path from "node:path";
4
- import { formatErrorDetail } from "./error-detail.js";
5
3
  import { FsSafeError } from "./errors.js";
6
- import { assertNoNulPathInput, hasNodeErrorCode, isNodeError, isNotFoundPathError, isPathInside, isPathRelativeEscape, } from "./path.js";
7
- import { root as openRoot } from "./root.js";
4
+ import { isNotFoundPathError, isPathInside, } from "./path.js";
8
5
  import { realpathSync } from "./realpath.js";
6
+ import { root as openRoot } from "./root.js";
7
+ import { hasWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
8
+ import { resolvePathWithinRoot } from "./root-paths-lexical.js";
9
9
  import { resolvePathWithinNormalizedRoot } from "./path-scope-lexical.js";
10
+ import { assertNoSymlinkSegments, ensureDirectoryWithinRoot, resolveNearestExistingPath, } from "./root-directory.js";
11
+ export { resolvePathWithinRoot } from "./root-paths-lexical.js";
12
+ export { ensureDirectoryWithinRoot } from "./root-directory.js";
10
13
  function invalidPath(scopeLabel) {
11
14
  return {
12
15
  ok: false,
13
16
  error: `Invalid path: must stay within ${scopeLabel}`,
14
17
  };
15
18
  }
19
+ const INVALID_REAL_PATH = Symbol("invalid-real-path");
16
20
  async function resolveRealPathIfExists(targetPath) {
21
+ if (hasWindowsPathAlias(targetPath, "filesystem"))
22
+ return INVALID_REAL_PATH;
17
23
  try {
18
- return realpathSync.native(targetPath);
24
+ const realPath = realpathSync.native(pathForWindowsFilesystem(targetPath));
25
+ return hasWindowsPathAlias(realPath, "filesystem") ? INVALID_REAL_PATH : realPath;
19
26
  }
20
27
  catch {
21
28
  return undefined;
22
29
  }
23
30
  }
24
31
  async function resolveTrustedRootRealPath(rootDir) {
32
+ if (hasWindowsPathAlias(rootDir, "filesystem"))
33
+ return undefined;
25
34
  try {
26
- const rootLstat = fsSync.lstatSync(rootDir);
35
+ const operationPath = pathForWindowsFilesystem(rootDir);
36
+ const rootLstat = fsSync.lstatSync(operationPath);
27
37
  if (!rootLstat.isDirectory() || rootLstat.isSymbolicLink()) {
28
38
  return undefined;
29
39
  }
30
- return realpathSync.native(rootDir);
40
+ const realPath = realpathSync.native(operationPath);
41
+ return hasWindowsPathAlias(realPath, "filesystem") ? undefined : realPath;
31
42
  }
32
43
  catch {
33
44
  return undefined;
@@ -35,7 +46,8 @@ async function resolveTrustedRootRealPath(rootDir) {
35
46
  }
36
47
  async function validateCanonicalPathWithinRoot(params) {
37
48
  try {
38
- const candidateLstat = fsSync.lstatSync(params.candidatePath);
49
+ const operationPath = pathForWindowsFilesystem(params.candidatePath);
50
+ const candidateLstat = fsSync.lstatSync(operationPath);
39
51
  if (candidateLstat.isSymbolicLink()) {
40
52
  return "invalid";
41
53
  }
@@ -48,25 +60,33 @@ async function validateCanonicalPathWithinRoot(params) {
48
60
  if (params.expect === "file" && candidateLstat.nlink > 1) {
49
61
  return "invalid";
50
62
  }
51
- const candidateRealPath = realpathSync.native(params.candidatePath);
63
+ const candidateRealPath = realpathSync.native(operationPath);
64
+ if (hasWindowsPathAlias(candidateRealPath, "filesystem"))
65
+ return "invalid";
52
66
  return isPathInside(params.rootRealPath, candidateRealPath) ? "ok" : "invalid";
53
67
  }
54
68
  catch (err) {
55
69
  return isNotFoundPathError(err) ? "not-found" : "invalid";
56
70
  }
57
71
  }
58
- export function resolvePathWithinRoot(params) {
59
- return resolvePathWithinNormalizedRoot(params, path.resolve(params.rootDir));
60
- }
61
72
  export async function resolveWritablePathWithinRoot(params) {
62
- const lexical = resolvePathWithinRoot(params);
73
+ const rootDirInput = params.rootDir;
74
+ const requestedPathInput = params.requestedPath;
75
+ const scopeLabel = params.scopeLabel;
76
+ const defaultFileName = params.defaultFileName;
77
+ const lexical = resolvePathWithinRoot({
78
+ rootDir: rootDirInput,
79
+ requestedPath: requestedPathInput,
80
+ scopeLabel,
81
+ defaultFileName,
82
+ });
63
83
  if (!lexical.ok) {
64
84
  return lexical;
65
85
  }
66
- const rootDir = path.resolve(params.rootDir);
86
+ const rootDir = resolvePathPreservingWindowsRoot(rootDirInput);
67
87
  const rootRealPath = await resolveTrustedRootRealPath(rootDir);
68
88
  if (!rootRealPath) {
69
- return invalidPath(params.scopeLabel);
89
+ return invalidPath(scopeLabel);
70
90
  }
71
91
  const requestedPath = lexical.path;
72
92
  const parentDir = path.dirname(requestedPath);
@@ -76,7 +96,7 @@ export async function resolveWritablePathWithinRoot(params) {
76
96
  expect: "directory",
77
97
  });
78
98
  if (parentStatus !== "ok") {
79
- return invalidPath(params.scopeLabel);
99
+ return invalidPath(scopeLabel);
80
100
  }
81
101
  const targetStatus = await validateCanonicalPathWithinRoot({
82
102
  rootRealPath,
@@ -84,131 +104,10 @@ export async function resolveWritablePathWithinRoot(params) {
84
104
  expect: "file",
85
105
  });
86
106
  if (targetStatus === "invalid") {
87
- return invalidPath(params.scopeLabel);
107
+ return invalidPath(scopeLabel);
88
108
  }
89
109
  return lexical;
90
110
  }
91
- async function resolveNearestExistingPath(targetPath) {
92
- let current = path.resolve(targetPath);
93
- while (true) {
94
- try {
95
- fsSync.lstatSync(current);
96
- return current;
97
- }
98
- catch (err) {
99
- if (!isNotFoundPathError(err)) {
100
- throw err;
101
- }
102
- }
103
- const parent = path.dirname(current);
104
- if (parent === current) {
105
- throw new Error(`failed to resolve existing path for ${targetPath}`);
106
- }
107
- current = parent;
108
- }
109
- }
110
- async function assertNoSymlinkSegments(params) {
111
- const relative = path.relative(params.rootDir, params.targetPath);
112
- if (isPathRelativeEscape(relative)) {
113
- throw new FsSafeError("outside-workspace", `Invalid path: must stay within ${params.scopeLabel}`);
114
- }
115
- let current = params.rootDir;
116
- for (const segment of relative.split(path.sep).filter(Boolean)) {
117
- current = path.join(current, segment);
118
- try {
119
- const stat = fsSync.lstatSync(current);
120
- if (stat.isSymbolicLink()) {
121
- throw new FsSafeError("symlink", `Invalid path: must not traverse symlinks within ${params.scopeLabel}`);
122
- }
123
- if (!stat.isDirectory()) {
124
- throw new FsSafeError("not-file", `Invalid path: existing segment must be a directory within ${params.scopeLabel}`);
125
- }
126
- }
127
- catch (err) {
128
- if (isNotFoundPathError(err)) {
129
- return;
130
- }
131
- throw err;
132
- }
133
- }
134
- }
135
- export async function ensureDirectoryWithinRoot(params) {
136
- const scopeLabel = formatErrorDetail(params.scopeLabel.slice(0, 80));
137
- try {
138
- assertNoNulPathInput(params.rootDir);
139
- assertNoNulPathInput(params.requestedPath);
140
- if (!params.requestedPath.trim())
141
- assertNoNulPathInput(params.defaultDirName ?? "");
142
- const lexical = resolvePathWithinRoot({
143
- ...params, scopeLabel, defaultFileName: params.defaultDirName,
144
- });
145
- if (!lexical.ok)
146
- return lexical;
147
- const rootDir = path.resolve(params.rootDir);
148
- const targetPath = lexical.path;
149
- const rootStat = fsSync.lstatSync(rootDir);
150
- if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
151
- return invalidPath(scopeLabel);
152
- }
153
- await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel });
154
- const rootReal = realpathSync.native(rootDir);
155
- const nearestExistingPath = await resolveNearestExistingPath(targetPath);
156
- const nearestExistingReal = realpathSync.native(nearestExistingPath);
157
- if (!isPathInside(rootReal, nearestExistingReal)) {
158
- return invalidPath(scopeLabel);
159
- }
160
- const relative = path.relative(rootDir, targetPath);
161
- let current = rootDir;
162
- for (const segment of relative.split(path.sep).filter(Boolean)) {
163
- current = path.join(current, segment);
164
- while (true) {
165
- try {
166
- const stat = fsSync.lstatSync(current);
167
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
168
- return invalidPath(scopeLabel);
169
- }
170
- break;
171
- }
172
- catch (err) {
173
- if (!isNotFoundPathError(err)) {
174
- throw err;
175
- }
176
- try {
177
- await fs.mkdir(current, { mode: params.mode });
178
- }
179
- catch (mkdirErr) {
180
- if (hasNodeErrorCode(mkdirErr, "EEXIST")) {
181
- continue;
182
- }
183
- throw mkdirErr;
184
- }
185
- }
186
- }
187
- const currentReal = realpathSync.native(current);
188
- if (!isPathInside(rootReal, currentReal)) {
189
- return invalidPath(scopeLabel);
190
- }
191
- }
192
- const targetReal = realpathSync.native(targetPath);
193
- if (!isPathInside(rootReal, targetReal)) {
194
- return invalidPath(scopeLabel);
195
- }
196
- return { ok: true, path: targetPath };
197
- }
198
- catch (cause) {
199
- if ((cause instanceof FsSafeError && cause.category === "policy") ||
200
- hasNodeErrorCode(cause, "ENOTDIR")) {
201
- return invalidPath(scopeLabel);
202
- }
203
- // Native messages include unbounded paths; display only bounded errno metadata.
204
- const code = isNodeError(cause) && typeof cause.code === "string"
205
- ? formatErrorDetail(cause.code.slice(0, 40)) : "filesystem operation failed";
206
- const syscall = isNodeError(cause) && typeof cause.syscall === "string"
207
- ? ` during ${formatErrorDetail(cause.syscall.slice(0, 40))}` : "";
208
- const diagnostic = new FsSafeError("helper-failed", `Could not prepare ${scopeLabel}: ${code}${syscall}`, { cause });
209
- return { ok: false, error: diagnostic.message, diagnostic };
210
- }
211
- }
212
111
  export function resolvePathsWithinRoot(params) {
213
112
  const resolvedPaths = [];
214
113
  let previousRootDir;
@@ -219,11 +118,16 @@ export function resolvePathsWithinRoot(params) {
219
118
  requestedPath: raw,
220
119
  scopeLabel: params.scopeLabel,
221
120
  };
121
+ // Each lexical request owns its admitted root without consuming later items.
122
+ if (typeof request.rootDir !== "string")
123
+ path.resolve(request.rootDir);
124
+ if (hasWindowsPathAlias(request.rootDir, "filesystem"))
125
+ return invalidPath(request.scopeLabel);
222
126
  // Relative roots may follow cwd changes made by an input iterator or getter.
223
127
  const resolvedRoot = process.platform !== "win32" && typeof request.rootDir === "string" &&
224
128
  request.rootDir.startsWith("/") && request.rootDir === previousRootDir
225
129
  ? previousResolvedRoot
226
- : path.resolve(request.rootDir);
130
+ : resolvePathPreservingWindowsRoot(request.rootDir);
227
131
  previousRootDir = request.rootDir;
228
132
  previousResolvedRoot = resolvedRoot;
229
133
  const pathResult = resolvePathWithinNormalizedRoot(request, resolvedRoot);
@@ -241,10 +145,11 @@ export async function resolveStrictExistingPathsWithinRoot(params) {
241
145
  return await resolveCheckedPathsWithinRoot(params, false);
242
146
  }
243
147
  export function pathScope(rootDir, options) {
244
- const base = { rootDir, scopeLabel: options.label };
148
+ const label = options.label;
149
+ const base = { rootDir, scopeLabel: label };
245
150
  return {
246
151
  rootDir,
247
- label: options.label,
152
+ label,
248
153
  resolve: (requestedPath, pathOptions) => resolvePathWithinRoot({
249
154
  ...base,
250
155
  requestedPath,
@@ -276,8 +181,19 @@ export function pathScope(rootDir, options) {
276
181
  };
277
182
  }
278
183
  async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
279
- const rootDir = path.resolve(params.rootDir);
280
- const rootRealPath = await resolveRealPathIfExists(rootDir);
184
+ const rootDirInput = params.rootDir;
185
+ const scopeLabel = params.scopeLabel;
186
+ if (hasWindowsPathAlias(rootDirInput, "filesystem")) {
187
+ return invalidPath(scopeLabel);
188
+ }
189
+ const requestedPaths = [...params.requestedPaths];
190
+ const rootDir = resolvePathPreservingWindowsRoot(rootDirInput);
191
+ if (hasWindowsPathAlias(rootDir, "filesystem"))
192
+ return invalidPath(scopeLabel);
193
+ const rootRealPathResult = await resolveRealPathIfExists(rootDir);
194
+ if (rootRealPathResult === INVALID_REAL_PATH)
195
+ return invalidPath(scopeLabel);
196
+ const rootRealPath = rootRealPathResult;
281
197
  const root = rootRealPath ? await openRoot(rootDir) : undefined;
282
198
  const isInRoot = (relativePath) => Boolean(relativePath) &&
283
199
  relativePath !== ".." &&
@@ -288,7 +204,7 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
288
204
  const lexicalPathResult = resolvePathWithinRoot({
289
205
  rootDir,
290
206
  requestedPath,
291
- scopeLabel: params.scopeLabel,
207
+ scopeLabel,
292
208
  });
293
209
  if (lexicalPathResult.ok) {
294
210
  return {
@@ -297,11 +213,17 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
297
213
  fallbackPath: lexicalPathResult.path,
298
214
  };
299
215
  }
300
- if (!rootRealPath || !raw || !path.isAbsolute(raw)) {
216
+ if (!rootRealPath ||
217
+ !raw ||
218
+ !path.isAbsolute(raw) ||
219
+ hasWindowsPathAlias(raw, "filesystem")) {
301
220
  return lexicalPathResult;
302
221
  }
303
222
  try {
304
- const resolvedExistingPath = realpathSync.native(raw);
223
+ const resolvedExistingPath = realpathSync.native(pathForWindowsFilesystem(raw));
224
+ if (hasWindowsPathAlias(resolvedExistingPath, "filesystem")) {
225
+ return lexicalPathResult;
226
+ }
305
227
  const relativePath = path.relative(rootRealPath, resolvedExistingPath);
306
228
  if (!isInRoot(relativePath)) {
307
229
  return lexicalPathResult;
@@ -317,7 +239,7 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
317
239
  }
318
240
  };
319
241
  const resolvedPaths = [];
320
- for (const raw of params.requestedPaths) {
242
+ for (const raw of requestedPaths) {
321
243
  const pathResult = await resolveExistingRelativePath(raw);
322
244
  if (!pathResult.ok) {
323
245
  return { ok: false, error: pathResult.error };
@@ -340,16 +262,17 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
340
262
  await assertNoSymlinkSegments({
341
263
  rootDir,
342
264
  targetPath: pathResult.fallbackPath,
343
- scopeLabel: params.scopeLabel,
265
+ scopeLabel,
344
266
  });
345
267
  const existingPath = await resolveNearestExistingPath(pathResult.fallbackPath);
346
- const existingRealPath = realpathSync.native(existingPath);
347
- if (!isPathInside(rootRealPath, existingRealPath)) {
348
- return invalidPath(params.scopeLabel);
268
+ const existingRealPath = realpathSync.native(pathForWindowsFilesystem(existingPath));
269
+ if (hasWindowsPathAlias(existingRealPath, "filesystem") ||
270
+ !isPathInside(rootRealPath, existingRealPath)) {
271
+ return invalidPath(scopeLabel);
349
272
  }
350
273
  }
351
274
  catch {
352
- return invalidPath(params.scopeLabel);
275
+ return invalidPath(scopeLabel);
353
276
  }
354
277
  resolvedPaths.push(pathResult.fallbackPath);
355
278
  continue;
@@ -357,12 +280,12 @@ async function resolveCheckedPathsWithinRoot(params, allowMissingFallback) {
357
280
  if (err instanceof FsSafeError && err.code === "outside-workspace") {
358
281
  return {
359
282
  ok: false,
360
- error: `File is outside ${params.scopeLabel}`,
283
+ error: `File is outside ${scopeLabel}`,
361
284
  };
362
285
  }
363
286
  return {
364
287
  ok: false,
365
- error: `Invalid path: must stay within ${params.scopeLabel} and be a regular non-symlink file`,
288
+ error: `Invalid path: must stay within ${scopeLabel} and be a regular non-symlink file`,
366
289
  };
367
290
  }
368
291
  finally {
@@ -0,0 +1,26 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import type { FileHandle } from "node:fs/promises";
3
+ import { type RootContext } from "./root-context.js";
4
+ import type { HardlinkPolicy } from "./root-options.js";
5
+ import type { SymlinkPolicy } from "./root-symlink-policy.js";
6
+ export declare function inspectOpenedPathIdentitySync(filePath: string, symlinks: SymlinkPolicy | undefined): BigIntStats;
7
+ type OwnedRootReadHandle = {
8
+ handle: FileHandle;
9
+ stat: {
10
+ nlink: number;
11
+ };
12
+ };
13
+ export declare function admitRootReadHandle<T extends OwnedRootReadHandle>(params: {
14
+ root: RootContext;
15
+ filePath: string;
16
+ opened: T;
17
+ identity: BigIntStats;
18
+ hardlinks?: HardlinkPolicy;
19
+ symlinks?: SymlinkPolicy;
20
+ beforeFinalFence?: (filePath: string, handle: FileHandle) => Promise<void> | void;
21
+ afterPathIdentityCheck?: (filePath: string, handle: FileHandle) => void;
22
+ }): Promise<T & {
23
+ realPath: string;
24
+ }>;
25
+ export {};
26
+ //# sourceMappingURL=root-read-admission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-read-admission.d.ts","sourceRoot":"","sources":["../src/root-read-admission.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,OAAO,EAAiC,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAmB9D,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,aAAa,GAAG,SAAS,GAClC,WAAW,CAQb;AA6CD,KAAK,mBAAmB,GAAG;IACzB,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACzB,CAAC;AAEF,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,mBAAmB,EAAE,MAAM,EAAE;IAC/E,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;IACV,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClF,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;CACzE,GAAG,OAAO,CAAC,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA8BpC"}
@@ -0,0 +1,94 @@
1
+ import fsSync, {} from "node:fs";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { openedPathResolutionError, recordOpenedFileFailure } from "./opened-file-failure.js";
4
+ import { isNotFoundPathError } from "./path.js";
5
+ import { admitPathInsideRoot } from "./root-boundary.js";
6
+ import { assertRootIdentityCurrentSync } from "./root-context.js";
7
+ import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
8
+ import { realpathSync } from "./realpath.js";
9
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
10
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
11
+ function resolveFinalOpenedRealPath(filePath) {
12
+ try {
13
+ return realpathSync.native(filePath);
14
+ }
15
+ catch (error) {
16
+ // Windows can report a deleted-but-open file through either resolver
17
+ // failure. Only the same-descriptor unlink proof may consume this receipt.
18
+ if (process.platform === "win32" && error instanceof Error &&
19
+ ["EPERM", "EBADF"].includes(error.code ?? "")) {
20
+ throw openedPathResolutionError(error);
21
+ }
22
+ throw error;
23
+ }
24
+ }
25
+ export function inspectOpenedPathIdentitySync(filePath, symlinks) {
26
+ const stat = symlinks === "follow-within-root"
27
+ ? fsSync.statSync(filePath, { bigint: true })
28
+ : fsSync.lstatSync(filePath, { bigint: true });
29
+ if (stat.isSymbolicLink() && symlinks !== "follow-within-root") {
30
+ throw new FsSafeError("symlink", "symlink not allowed");
31
+ }
32
+ return stat;
33
+ }
34
+ function assertRootReadAdmissionCurrent(params) {
35
+ assertRootIdentityCurrentSync(params.root);
36
+ const admittedPath = (() => {
37
+ try {
38
+ inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(params.filePath, params.symlinks), params.identity);
39
+ const canonicalPath = resolveFinalOpenedRealPath(params.filePath);
40
+ assertNoWindowsPathAlias(canonicalPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
41
+ const admittedRealPath = admitPathInsideRoot({
42
+ rootPath: params.root.rootReal,
43
+ candidatePath: canonicalPath,
44
+ rootIdentity: params.root.rootIdentity,
45
+ });
46
+ if (!admittedRealPath) {
47
+ throw openedPathResolutionError(outsideWorkspaceError());
48
+ }
49
+ // This is a fresh no-follow observation even when canonicalPath and
50
+ // filePath have the same spelling; realpath may have raced a replacement.
51
+ inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(canonicalPath, undefined), params.identity);
52
+ return admittedRealPath.path;
53
+ }
54
+ catch (error) {
55
+ throw isNotFoundPathError(error)
56
+ ? openedPathResolutionError(fileNotFoundError())
57
+ : error;
58
+ }
59
+ })();
60
+ params.afterPathIdentityCheck?.(params.filePath, params.handle);
61
+ assertRootIdentityCurrentSync(params.root);
62
+ return admittedPath;
63
+ }
64
+ export async function admitRootReadHandle(params) {
65
+ try {
66
+ if (params.hardlinks !== "allow" && params.opened.stat.nlink > 1) {
67
+ throw hardlinkedPathNotAllowedError();
68
+ }
69
+ if (params.beforeFinalFence) {
70
+ await params.beforeFinalFence(params.filePath, params.opened.handle);
71
+ }
72
+ try {
73
+ const finalRealPath = assertRootReadAdmissionCurrent({
74
+ root: params.root,
75
+ filePath: params.filePath,
76
+ handle: params.opened.handle,
77
+ identity: params.identity,
78
+ symlinks: params.symlinks,
79
+ afterPathIdentityCheck: params.afterPathIdentityCheck,
80
+ });
81
+ return { ...params.opened, realPath: finalRealPath };
82
+ }
83
+ catch (error) {
84
+ if (params.identity.nlink <= 1n) {
85
+ await recordOpenedFileFailure(error, params.opened.handle, params.filePath, params.identity);
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+ catch (error) {
91
+ await params.opened.handle.close().catch(() => { });
92
+ throw error;
93
+ }
94
+ }
@@ -0,0 +1,15 @@
1
+ import type { BigIntStats } from "node:fs";
2
+ type ExactDirectoryIdentity = Readonly<Pick<BigIntStats, "dev" | "ino">>;
3
+ export type RemovalDirectoryAssertion = Readonly<{
4
+ path: string;
5
+ dev: bigint;
6
+ ino: bigint;
7
+ numericDev?: number;
8
+ numericIno?: number;
9
+ platform: NodeJS.Platform;
10
+ realPath?: string;
11
+ }>;
12
+ export declare function createRemovalDirectoryAssertion(path: string, identity: ExactDirectoryIdentity, realPath?: string, platform?: NodeJS.Platform): RemovalDirectoryAssertion;
13
+ export declare function assertRemovalDirectoryCurrent(assertion: RemovalDirectoryAssertion): void;
14
+ export {};
15
+ //# sourceMappingURL=root-remove-identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-remove-identity.d.ts","sourceRoot":"","sources":["../src/root-remove-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAS,MAAM,SAAS,CAAC;AAOlD,KAAK,sBAAsB,GAAG,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;AAGzE,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AAaH,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,sBAAsB,EAChC,QAAQ,CAAC,EAAE,MAAM,EACjB,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,yBAAyB,CAc3B;AA0DD,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,yBAAyB,GAAG,IAAI,CAUxF"}
@@ -0,0 +1,89 @@
1
+ import fsSync from "node:fs";
2
+ import { inspectDirectoryIdentitySync } from "./directory-guard.js";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { realpathSync } from "./realpath.js";
5
+ import { directoryComponentNotDirectoryError } from "./root-errors.js";
6
+ const MAX_SAFE_DIRECTORY_IDENTITY = BigInt(Number.MAX_SAFE_INTEGER);
7
+ function identityMismatch(cause) {
8
+ return new FsSafeError("path-mismatch", "removal ancestor identity changed or could not be verified", {
9
+ cause: cause instanceof Error ? cause : undefined,
10
+ });
11
+ }
12
+ function safeIdentityNumber(value) {
13
+ if (value < 0n || value > MAX_SAFE_DIRECTORY_IDENTITY)
14
+ return undefined;
15
+ return Number(value);
16
+ }
17
+ export function createRemovalDirectoryAssertion(path, identity, realPath, platform = process.platform) {
18
+ const dev = safeIdentityNumber(identity.dev);
19
+ const ino = safeIdentityNumber(identity.ino);
20
+ const numeric = dev !== undefined && ino !== undefined &&
21
+ (platform !== "win32" || (dev !== 0 && ino !== 0));
22
+ return Object.freeze({
23
+ path,
24
+ dev: identity.dev,
25
+ ino: identity.ino,
26
+ numericDev: numeric ? dev : undefined,
27
+ numericIno: numeric ? ino : undefined,
28
+ platform,
29
+ realPath,
30
+ });
31
+ }
32
+ function assertDirectoryType(stat) {
33
+ if (typeof stat?.isDirectory !== "function" || typeof stat.isSymbolicLink !== "function") {
34
+ throw identityMismatch();
35
+ }
36
+ if (stat.isSymbolicLink() || !stat.isDirectory())
37
+ throw directoryComponentNotDirectoryError();
38
+ }
39
+ function assertExactObservation(stat, expected, platform) {
40
+ assertDirectoryType(stat);
41
+ if (typeof stat.dev !== "bigint" || typeof stat.ino !== "bigint")
42
+ throw identityMismatch();
43
+ if (platform === "win32" && (stat.dev === 0n || stat.ino === 0n))
44
+ throw identityMismatch();
45
+ if (stat.dev !== expected.dev || stat.ino !== expected.ino)
46
+ throw identityMismatch();
47
+ }
48
+ function assertNumericObservation(assertion) {
49
+ const stat = fsSync.lstatSync(assertion.path);
50
+ assertDirectoryType(stat);
51
+ if (!Number.isSafeInteger(stat.dev) || stat.dev < 0 ||
52
+ !Number.isSafeInteger(stat.ino) || stat.ino < 0) {
53
+ throw identityMismatch();
54
+ }
55
+ let unknown = false;
56
+ const observedDev = stat.dev;
57
+ const expectedDev = assertion.numericDev;
58
+ if (assertion.platform === "win32" && observedDev === 0) {
59
+ unknown = true;
60
+ }
61
+ else if (observedDev !== expectedDev) {
62
+ // Reject a known mismatch before considering an unknown companion field.
63
+ throw identityMismatch();
64
+ }
65
+ const observedIno = stat.ino;
66
+ const expectedIno = assertion.numericIno;
67
+ if (assertion.platform === "win32" && observedIno === 0) {
68
+ unknown = true;
69
+ }
70
+ else if (observedIno !== expectedIno) {
71
+ throw identityMismatch();
72
+ }
73
+ if (unknown) {
74
+ // The numeric lstat is the first observation. Permit one exact retry only,
75
+ // retaining every known component and the original exact admission receipt.
76
+ assertExactObservation(fsSync.lstatSync(assertion.path, { bigint: true }), assertion, assertion.platform);
77
+ }
78
+ }
79
+ export function assertRemovalDirectoryCurrent(assertion) {
80
+ if (assertion.numericDev !== undefined && assertion.numericIno !== undefined) {
81
+ assertNumericObservation(assertion);
82
+ }
83
+ else {
84
+ inspectDirectoryIdentitySync(assertion.path, assertion, undefined, assertion.platform);
85
+ }
86
+ if (assertion.realPath !== undefined && realpathSync.native(assertion.path) !== assertion.realPath) {
87
+ throw identityMismatch();
88
+ }
89
+ }
@@ -0,0 +1,17 @@
1
+ import type { BigIntStats } from "node:fs";
2
+ export type CapturedRemovalDirectory = Readonly<{
3
+ path: string;
4
+ stat: BigIntStats;
5
+ }>;
6
+ export declare class RemovalPathReceipts {
7
+ private rootStat;
8
+ private directories;
9
+ readonly observeRoot: (stat: BigIntStats) => void;
10
+ observeDirectory(dir: string, stat: BigIntStats): void;
11
+ complete(rootReal: string, targetPath: string): {
12
+ rootStat: BigIntStats;
13
+ parent: CapturedRemovalDirectory | undefined;
14
+ directories: readonly CapturedRemovalDirectory[];
15
+ } | undefined;
16
+ }
17
+ //# sourceMappingURL=root-remove-receipt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-remove-receipt.d.ts","sourceRoot":"","sources":["../src/root-remove-receipt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI3C,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,WAAW,CAAC;CACnB,CAAC,CAAC;AAKH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,WAAW,CAAoD;IAEvE,QAAQ,CAAC,WAAW,SAAU,WAAW,KAAG,IAAI,CAA8B;IAE9E,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAIrD;IAED,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG;QAC9C,QAAQ,EAAE,WAAW,CAAC;QACtB,MAAM,EAAE,wBAAwB,GAAG,SAAS,CAAC;QAC7C,WAAW,EAAE,SAAS,wBAAwB,EAAE,CAAC;KAClD,GAAG,SAAS,CAiBZ;CACF"}
@@ -0,0 +1,37 @@
1
+ import path from "node:path";
2
+ import { isPathInside } from "./path.js";
3
+ // Private to a single nonrecursive removal. No observations are shared across
4
+ // operations, and a lexical observation never stands in for an unvisited alias
5
+ // target on the final canonical chain.
6
+ export class RemovalPathReceipts {
7
+ rootStat;
8
+ directories;
9
+ observeRoot = (stat) => { this.rootStat = stat; };
10
+ observeDirectory(dir, stat) {
11
+ // Retain the first object if raw traversal revisits the same spelling.
12
+ const directories = this.directories ??= new Map();
13
+ if (!directories.has(dir))
14
+ directories.set(dir, { path: dir, stat });
15
+ }
16
+ complete(rootReal, targetPath) {
17
+ const parentPath = path.dirname(targetPath);
18
+ if (!this.rootStat || !isPathInside(rootReal, parentPath))
19
+ return undefined;
20
+ const directories = [];
21
+ let current = parentPath;
22
+ while (current !== rootReal) {
23
+ // Exact spelling is intentional: case/namespace aliases and symlink hops
24
+ // that did not visit the complete canonical chain use fresh admission.
25
+ const directory = this.directories?.get(current);
26
+ if (!directory)
27
+ return undefined;
28
+ directories.push(directory);
29
+ const parent = path.dirname(current);
30
+ if (parent === current)
31
+ return undefined;
32
+ current = parent;
33
+ }
34
+ directories.reverse();
35
+ return { rootStat: this.rootStat, parent: directories.at(-1), directories };
36
+ }
37
+ }