@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
@@ -2,15 +2,23 @@ import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { setImmediate as yieldToEventLoop } from "node:timers/promises";
5
- import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
5
+ import { assertAsyncDirectoryGuard, assertDirectoryObservationGuardSync, assertDirectoryObservationSync, createAsyncDirectoryGuard, } from "./directory-guard.js";
6
6
  import { FsSafeError } from "./errors.js";
7
+ import { inspectNativeDirectoryObservation, isNativeDirectoryObservationGuard, } from "./native-directory-observation.js";
7
8
  import { isNotFoundPathError } from "./path.js";
8
9
  import { assertRootIdentityCurrent } from "./root-context.js";
9
10
  import { rootPathChangedError } from "./root-errors.js";
10
11
  import { admitPathInsideRoot } from "./root-boundary.js";
11
12
  import { createSuppressedError } from "./suppressed-error.js";
13
+ import { realpathSync } from "./realpath.js";
14
+ import { getFsSafeTestHooks } from "./test-hooks.js";
12
15
  const METADATA_BATCH_SIZE = 32;
13
16
  export function pathStatFromStats(stat) {
17
+ const mtimeMs = typeof stat.mtimeMs === "bigint"
18
+ ? "mtimeNs" in stat && typeof stat.mtimeNs === "bigint"
19
+ ? Number(stat.mtimeNs) / 1_000_000
20
+ : stat.mtime.getTime()
21
+ : stat.mtimeMs;
14
22
  return {
15
23
  dev: Number(stat.dev),
16
24
  gid: Number(stat.gid),
@@ -18,13 +26,129 @@ export function pathStatFromStats(stat) {
18
26
  isDirectory: stat.isDirectory(),
19
27
  isFile: stat.isFile(),
20
28
  isSymbolicLink: stat.isSymbolicLink(),
21
- mode: stat.mode,
22
- mtimeMs: stat.mtimeMs,
23
- nlink: stat.nlink,
24
- size: stat.size,
25
- uid: stat.uid,
29
+ mode: Number(stat.mode),
30
+ mtimeMs,
31
+ nlink: Number(stat.nlink),
32
+ size: Number(stat.size),
33
+ uid: Number(stat.uid),
26
34
  };
27
35
  }
36
+ export async function createRootDirectoryObservationGuard(root, directory) {
37
+ const guard = await createAsyncDirectoryGuard(directory, { bigint: true });
38
+ const admittedRealPath = admitPathInsideRoot({
39
+ rootPath: root.rootReal,
40
+ candidatePath: guard.realPath,
41
+ rootIdentity: root.rootIdentity,
42
+ });
43
+ if (!admittedRealPath) {
44
+ throw new FsSafeError("outside-workspace", "directory is outside workspace root");
45
+ }
46
+ guard.dir = admittedRealPath.path;
47
+ guard.realPath = admittedRealPath.path;
48
+ return guard;
49
+ }
50
+ function directoryChangedError(error) {
51
+ if (error instanceof FsSafeError && error.code === "path-mismatch")
52
+ return error;
53
+ return new FsSafeError("path-mismatch", "directory changed during operation", {
54
+ cause: error instanceof Error ? error : undefined,
55
+ });
56
+ }
57
+ export async function assertRootDirectoryObservationGuard(root, guard) {
58
+ if (isNativeDirectoryObservationGuard(guard)) {
59
+ throw new FsSafeError("path-mismatch", "native directory observation cannot be revalidated");
60
+ }
61
+ const identity = "identity" in guard ? guard.identity : guard.stat;
62
+ const guardPinsRoot = guard.dir === root.rootReal &&
63
+ identity.dev === root.rootIdentity.dev && identity.ino === root.rootIdentity.ino;
64
+ if (guardPinsRoot) {
65
+ try {
66
+ if ("identity" in guard)
67
+ assertDirectoryObservationGuardSync(guard);
68
+ else
69
+ await assertAsyncDirectoryGuard(guard);
70
+ }
71
+ catch (error) {
72
+ throw rootPathChangedError(error instanceof Error ? error : undefined);
73
+ }
74
+ return;
75
+ }
76
+ await assertRootIdentityCurrent(root);
77
+ try {
78
+ if ("identity" in guard)
79
+ assertDirectoryObservationGuardSync(guard);
80
+ else
81
+ await assertAsyncDirectoryGuard(guard);
82
+ }
83
+ catch (error) {
84
+ throw directoryChangedError(error);
85
+ }
86
+ }
87
+ function sameObservationDirectory(left, right) {
88
+ return left.dir === right.dir && left.realPath === right.realPath &&
89
+ left.identity.dev === right.identity.dev && left.identity.ino === right.identity.ino;
90
+ }
91
+ function assertReceiptDirectoryGuardSync(root, guard, backend) {
92
+ if (backend) {
93
+ const observed = inspectNativeDirectoryObservation(backend, guard.dir, guard.identity);
94
+ const admitted = admitPathInsideRoot({
95
+ rootPath: root.rootReal,
96
+ candidatePath: observed.realPath,
97
+ rootIdentity: root.rootIdentity,
98
+ });
99
+ if (!admitted || admitted.path !== guard.realPath) {
100
+ throw new FsSafeError("path-mismatch", "directory changed during operation");
101
+ }
102
+ return;
103
+ }
104
+ if (isNativeDirectoryObservationGuard(guard)) {
105
+ throw new FsSafeError("path-mismatch", "native directory observation cannot be revalidated");
106
+ }
107
+ assertDirectoryObservationGuardSync(guard);
108
+ }
109
+ /**
110
+ * Close an operation-local traversal receipt with fresh exact observations.
111
+ * Descendant canonical paths are admitted while the receipt is created. The
112
+ * final directory check repeats canonical admission; the Root-only case does
113
+ * both its identity and canonical check here.
114
+ */
115
+ export function assertRootPathObservationReceiptCurrent(root, receipt, finalTarget) {
116
+ const { rootGuard, directoryGuard } = receipt;
117
+ if (rootGuard.dir !== root.rootReal || rootGuard.realPath !== root.rootReal ||
118
+ rootGuard.identity.dev !== root.rootIdentity.dev || rootGuard.identity.ino !== root.rootIdentity.ino) {
119
+ throw rootPathChangedError();
120
+ }
121
+ if (sameObservationDirectory(rootGuard, directoryGuard)) {
122
+ try {
123
+ if (receipt.kind === "stat" && receipt.target === rootGuard && finalTarget &&
124
+ !receipt.directoryObserver) {
125
+ if (finalTarget.isSymbolicLink() || !finalTarget.isDirectory() ||
126
+ realpathSync.native(rootGuard.dir) !== rootGuard.realPath) {
127
+ throw new FsSafeError("path-mismatch", "root path changed during operation");
128
+ }
129
+ }
130
+ else {
131
+ assertReceiptDirectoryGuardSync(root, directoryGuard, receipt.directoryObserver);
132
+ }
133
+ }
134
+ catch (error) {
135
+ throw rootPathChangedError(error instanceof Error ? error : undefined);
136
+ }
137
+ return;
138
+ }
139
+ try {
140
+ assertDirectoryObservationSync(rootGuard.dir, rootGuard.identity);
141
+ }
142
+ catch (error) {
143
+ throw rootPathChangedError(error instanceof Error ? error : undefined);
144
+ }
145
+ try {
146
+ assertReceiptDirectoryGuardSync(root, directoryGuard, receipt.directoryObserver);
147
+ }
148
+ catch (error) {
149
+ throw directoryChangedError(error);
150
+ }
151
+ }
28
152
  function normalizeDirectoryError(error) {
29
153
  if (isNotFoundPathError(error)) {
30
154
  return new FsSafeError("not-found", "directory not found", {
@@ -33,53 +157,70 @@ function normalizeDirectoryError(error) {
33
157
  }
34
158
  return error;
35
159
  }
36
- export async function listDirectoryPath(root, directory, withFileTypes) {
160
+ function normalizeInitialDirectoryError(error) {
161
+ if (error instanceof FsSafeError && error.code === "not-file") {
162
+ return new FsSafeError("not-found", "directory not found", { cause: error });
163
+ }
164
+ return normalizeDirectoryError(error);
165
+ }
166
+ export async function listDirectoryPath(root, directory, withFileTypes, receipt) {
167
+ let guard;
168
+ if (receipt) {
169
+ if (receipt.kind !== "directory" || receipt.targetPath !== directory ||
170
+ receipt.directoryGuard.dir !== directory ||
171
+ receipt.target !== receipt.directoryGuard ||
172
+ (!isNativeDirectoryObservationGuard(receipt.directoryGuard) &&
173
+ !receipt.directoryGuard.stat.isDirectory())) {
174
+ throw new FsSafeError("path-mismatch", "directory observation receipt does not match target");
175
+ }
176
+ guard = receipt.directoryGuard;
177
+ }
178
+ else {
179
+ try {
180
+ guard = await createRootDirectoryObservationGuard(root, directory);
181
+ }
182
+ catch (error) {
183
+ throw normalizeInitialDirectoryError(error);
184
+ }
185
+ }
186
+ return await listGuardedDirectoryPath(root, guard, withFileTypes, receipt);
187
+ }
188
+ async function listGuardedDirectoryPath(root, guard, withFileTypes, receipt) {
189
+ let entries;
37
190
  try {
38
- const names = (await fs.readdir(directory)).sort();
39
- const entries = withFileTypes
191
+ const beforeObservation = getFsSafeTestHooks()?.beforeRootListObservation;
192
+ if (beforeObservation)
193
+ await beforeObservation(guard.realPath, withFileTypes);
194
+ const names = (await fs.readdir(guard.realPath)).sort();
195
+ entries = withFileTypes
40
196
  ? names.map(name => ({
41
197
  name,
42
- ...pathStatFromStats(fsSync.lstatSync(path.join(directory, name))),
198
+ ...pathStatFromStats(fsSync.lstatSync(path.join(guard.realPath, name))),
43
199
  }))
44
200
  : names;
45
- await assertRootIdentityCurrent(root);
46
- return entries;
47
201
  }
48
202
  catch (error) {
203
+ // Preserve ordinary observation errors only while the admitted directory is
204
+ // still current. A post-admission replacement is an identity failure.
205
+ if (receipt)
206
+ assertRootPathObservationReceiptCurrent(root, receipt);
207
+ else
208
+ await assertRootDirectoryObservationGuard(root, guard);
49
209
  throw normalizeDirectoryError(error);
50
210
  }
211
+ if (receipt)
212
+ assertRootPathObservationReceiptCurrent(root, receipt);
213
+ else
214
+ await assertRootDirectoryObservationGuard(root, guard);
215
+ return entries;
51
216
  }
52
217
  export async function openRootDirectoryListing(root, directory, options) {
53
- const guard = await createAsyncDirectoryGuard(directory, { bigint: true }).catch((error) => {
218
+ const guard = await createRootDirectoryObservationGuard(root, directory).catch((error) => {
54
219
  throw normalizeDirectoryError(error);
55
220
  });
56
- const admittedRealPath = admitPathInsideRoot({
57
- rootPath: root.rootReal,
58
- candidatePath: guard.realPath,
59
- rootIdentity: root.rootIdentity,
60
- });
61
- if (!admittedRealPath) {
62
- throw new FsSafeError("outside-workspace", "directory is outside workspace root");
63
- }
64
- guard.dir = admittedRealPath.path;
65
- guard.realPath = admittedRealPath.path;
66
- const guardPinsRoot = guard.dir === root.rootReal &&
67
- guard.stat.dev === root.rootIdentity.dev && guard.stat.ino === root.rootIdentity.ino;
68
221
  const assertCurrent = async () => {
69
222
  options.signal?.throwIfAborted();
70
- if (guardPinsRoot) {
71
- // One exact receipt proves both identities without dropping the canonical-path check.
72
- try {
73
- await assertAsyncDirectoryGuard(guard);
74
- }
75
- catch (error) {
76
- throw rootPathChangedError(error instanceof Error ? error : undefined);
77
- }
78
- }
79
- else {
80
- await assertRootIdentityCurrent(root);
81
- await assertAsyncDirectoryGuard(guard);
82
- }
223
+ await assertRootDirectoryObservationGuard(root, guard);
83
224
  options.signal?.throwIfAborted();
84
225
  };
85
226
  let handle;
@@ -103,7 +244,7 @@ export async function openRootDirectoryListing(root, directory, options) {
103
244
  handle = await fs.opendir(guard.realPath, { bufferSize: 1 });
104
245
  }
105
246
  else if (options.snapshot) {
106
- snapshot = await listDirectoryPath(root, guard.realPath, true);
247
+ snapshot = await listGuardedDirectoryPath(root, guard, true);
107
248
  }
108
249
  else if (options.maxNames !== undefined) {
109
250
  names = [];
@@ -0,0 +1,23 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ export type DirectoryResult = {
3
+ ok: true;
4
+ path: string;
5
+ } | {
6
+ ok: false;
7
+ error: string;
8
+ diagnostic?: FsSafeError;
9
+ };
10
+ export declare function resolveNearestExistingPath(targetPath: string): string;
11
+ export declare function assertNoSymlinkSegments(params: {
12
+ rootDir: string;
13
+ targetPath: string;
14
+ scopeLabel: string;
15
+ }): Promise<void>;
16
+ export declare function ensureDirectoryWithinRoot(params: {
17
+ rootDir: string;
18
+ requestedPath: string;
19
+ scopeLabel: string;
20
+ defaultDirName?: string;
21
+ mode?: number;
22
+ }): Promise<DirectoryResult>;
23
+ //# sourceMappingURL=root-directory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-directory.d.ts","sourceRoot":"","sources":["../src/root-directory.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAiB1C,MAAM,MAAM,eAAe,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,WAAW,CAAA;CAAE,CAAC;AAM3D,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAiBrE;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BhB;AAED,wBAAsB,yBAAyB,CAAC,MAAM,EAAE;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,eAAe,CAAC,CAiF3B"}
@@ -0,0 +1,141 @@
1
+ import fsSync from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { formatErrorDetail } from "./error-detail.js";
5
+ import { FsSafeError } from "./errors.js";
6
+ import { assertNoNulPathInput, hasNodeErrorCode, isNodeError, isNotFoundPathError, isPathInside, isPathRelativeEscape, } from "./path.js";
7
+ import { realpathSync } from "./realpath.js";
8
+ import { resolvePathWithinRoot } from "./root-paths-lexical.js";
9
+ import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
10
+ function invalidPath(scopeLabel) {
11
+ return { ok: false, error: `Invalid path: must stay within ${scopeLabel}` };
12
+ }
13
+ export function resolveNearestExistingPath(targetPath) {
14
+ assertNoWindowsPathAlias(targetPath);
15
+ let current = resolvePathPreservingWindowsRoot(targetPath);
16
+ assertNoWindowsPathAlias(current);
17
+ while (true) {
18
+ try {
19
+ fsSync.lstatSync(pathForWindowsFilesystem(current));
20
+ return current;
21
+ }
22
+ catch (err) {
23
+ if (!isNotFoundPathError(err))
24
+ throw err;
25
+ }
26
+ const parent = path.dirname(current);
27
+ if (parent === current) {
28
+ throw new Error(`failed to resolve existing path for ${targetPath}`);
29
+ }
30
+ current = parent;
31
+ }
32
+ }
33
+ export async function assertNoSymlinkSegments(params) {
34
+ const relative = path.relative(params.rootDir, params.targetPath);
35
+ if (isPathRelativeEscape(relative)) {
36
+ throw new FsSafeError("outside-workspace", `Invalid path: must stay within ${params.scopeLabel}`);
37
+ }
38
+ let current = params.rootDir;
39
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
40
+ current = path.join(current, segment);
41
+ try {
42
+ const stat = fsSync.lstatSync(current);
43
+ if (stat.isSymbolicLink()) {
44
+ throw new FsSafeError("symlink", `Invalid path: must not traverse symlinks within ${params.scopeLabel}`);
45
+ }
46
+ if (!stat.isDirectory()) {
47
+ throw new FsSafeError("not-file", `Invalid path: existing segment must be a directory within ${params.scopeLabel}`);
48
+ }
49
+ }
50
+ catch (err) {
51
+ if (isNotFoundPathError(err))
52
+ return;
53
+ throw err;
54
+ }
55
+ }
56
+ }
57
+ export async function ensureDirectoryWithinRoot(params) {
58
+ const scopeLabel = formatErrorDetail(params.scopeLabel.slice(0, 80));
59
+ try {
60
+ const rootDirInput = params.rootDir;
61
+ const requestedPathInput = params.requestedPath;
62
+ const useDefault = !requestedPathInput.trim();
63
+ const defaultDirName = useDefault ? params.defaultDirName : undefined;
64
+ const mode = params.mode;
65
+ assertNoNulPathInput(rootDirInput);
66
+ assertNoNulPathInput(requestedPathInput);
67
+ if (useDefault)
68
+ assertNoNulPathInput(defaultDirName ?? "");
69
+ assertNoWindowsPathAlias(rootDirInput);
70
+ assertNoWindowsPathAlias(requestedPathInput);
71
+ if (useDefault)
72
+ assertNoWindowsPathAlias(defaultDirName ?? "");
73
+ const lexical = resolvePathWithinRoot({
74
+ rootDir: rootDirInput,
75
+ requestedPath: requestedPathInput,
76
+ scopeLabel,
77
+ defaultFileName: defaultDirName,
78
+ });
79
+ if (!lexical.ok)
80
+ return lexical;
81
+ const rootDir = resolvePathPreservingWindowsRoot(rootDirInput);
82
+ const targetPath = lexical.path;
83
+ const rootStat = fsSync.lstatSync(pathForWindowsFilesystem(rootDir));
84
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
85
+ return invalidPath(scopeLabel);
86
+ await assertNoSymlinkSegments({ rootDir, targetPath, scopeLabel });
87
+ const rootReal = realpathSync.native(pathForWindowsFilesystem(rootDir));
88
+ assertNoWindowsPathAlias(rootReal);
89
+ const nearestExistingPath = resolveNearestExistingPath(targetPath);
90
+ const nearestExistingReal = realpathSync.native(pathForWindowsFilesystem(nearestExistingPath));
91
+ assertNoWindowsPathAlias(nearestExistingReal);
92
+ if (!isPathInside(rootReal, nearestExistingReal))
93
+ return invalidPath(scopeLabel);
94
+ const relative = path.relative(rootDir, targetPath);
95
+ let current = rootDir;
96
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
97
+ current = path.join(current, segment);
98
+ while (true) {
99
+ try {
100
+ const stat = fsSync.lstatSync(current);
101
+ if (stat.isSymbolicLink() || !stat.isDirectory())
102
+ return invalidPath(scopeLabel);
103
+ break;
104
+ }
105
+ catch (err) {
106
+ if (!isNotFoundPathError(err))
107
+ throw err;
108
+ try {
109
+ await fs.mkdir(current, { mode });
110
+ }
111
+ catch (mkdirErr) {
112
+ if (hasNodeErrorCode(mkdirErr, "EEXIST"))
113
+ continue;
114
+ throw mkdirErr;
115
+ }
116
+ }
117
+ }
118
+ const currentReal = realpathSync.native(current);
119
+ assertNoWindowsPathAlias(currentReal);
120
+ if (!isPathInside(rootReal, currentReal))
121
+ return invalidPath(scopeLabel);
122
+ }
123
+ const targetReal = realpathSync.native(pathForWindowsFilesystem(targetPath));
124
+ assertNoWindowsPathAlias(targetReal);
125
+ if (!isPathInside(rootReal, targetReal))
126
+ return invalidPath(scopeLabel);
127
+ return { ok: true, path: targetPath };
128
+ }
129
+ catch (cause) {
130
+ if ((cause instanceof FsSafeError && cause.category === "policy") ||
131
+ hasNodeErrorCode(cause, "ENOTDIR")) {
132
+ return invalidPath(scopeLabel);
133
+ }
134
+ const code = isNodeError(cause) && typeof cause.code === "string"
135
+ ? formatErrorDetail(cause.code.slice(0, 40)) : "filesystem operation failed";
136
+ const syscall = isNodeError(cause) && typeof cause.syscall === "string"
137
+ ? ` during ${formatErrorDetail(cause.syscall.slice(0, 40))}` : "";
138
+ const diagnostic = new FsSafeError("helper-failed", `Could not prepare ${scopeLabel}: ${code}${syscall}`, { cause });
139
+ return { ok: false, error: diagnostic.message, diagnostic };
140
+ }
141
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"root-errors.d.ts","sourceRoot":"","sources":["../src/root-errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAKnE,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,WAAW,CAI5F;AAED,wBAAgB,qBAAqB,IAAI,WAAW,CAEnD;AAED,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,WAAW,CAE/D;AAED,wBAAgB,mCAAmC,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,CAIhF;AAED,wBAAgB,6BAA6B,IAAI,WAAW,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAU/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAQ5F;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAQ7F;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAe5F;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAQzE"}
1
+ {"version":3,"file":"root-errors.d.ts","sourceRoot":"","sources":["../src/root-errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAWnE,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,WAAW,CAI5F;AAED,wBAAgB,qBAAqB,IAAI,WAAW,CAEnD;AAED,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,WAAW,CAE/D;AAED,wBAAgB,mCAAmC,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,CAIhF;AAED,wBAAgB,6BAA6B,IAAI,WAAW,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAe/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAQ5F;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAQ7F;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAe5F;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAQzE"}
@@ -1,6 +1,12 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  import { hasNodeErrorCode, isNodeError, isNotFoundPathError } from "./path.js";
3
3
  const REMOVE_NOT_EMPTY_CODES = new Set(["ENOTEMPTY", "EEXIST"]);
4
+ const PINNED_WRITE_ERRNO_MESSAGES = new Map([
5
+ ["EACCES", "permission denied"],
6
+ ["EPERM", "permission denied"],
7
+ ["EROFS", "read-only filesystem"],
8
+ ["ENOSPC", "no space left on device"],
9
+ ]);
4
10
  export function fileNotFoundError(cause, details) {
5
11
  return cause === undefined
6
12
  ? new FsSafeError("not-found", "file not found", { details })
@@ -30,7 +36,12 @@ export function normalizePinnedWriteError(error) {
30
36
  if (isNotFoundPathError(error)) {
31
37
  return fileNotFoundError(error instanceof Error ? error : undefined);
32
38
  }
33
- return new FsSafeError("invalid-path", "path is not a regular file under root", {
39
+ const code = isNodeError(error) && typeof error.code === "string" && /^E[A-Z0-9_]+$/.test(error.code)
40
+ ? error.code : undefined;
41
+ const message = code
42
+ ? `${PINNED_WRITE_ERRNO_MESSAGES.get(code) ?? "filesystem write failed"} (${code})`
43
+ : "path is not a regular file under root";
44
+ return new FsSafeError("invalid-path", message, {
34
45
  cause: error instanceof Error ? error : undefined,
35
46
  });
36
47
  }
@@ -0,0 +1,21 @@
1
+ import fs from "node:fs";
2
+ import type { PinnedOpenSyncFinalAdmission } from "./pinned-open.js";
3
+ export type RootFileAdmissionFs = Pick<typeof fs, "lstatSync" | "realpathSync">;
4
+ type ExactIdentity = Readonly<{
5
+ dev: bigint;
6
+ ino: bigint;
7
+ }>;
8
+ export type CanonicalRootObservation = {
9
+ ok: true;
10
+ path: string;
11
+ identity: ExactIdentity;
12
+ } | {
13
+ ok: false;
14
+ error: unknown;
15
+ };
16
+ export declare function observeCanonicalRoot(ioFs: RootFileAdmissionFs, rootPath: string): CanonicalRootObservation;
17
+ export declare function createRootFileFinalAdmission(ioFs: RootFileAdmissionFs, root: Extract<CanonicalRootObservation, {
18
+ ok: true;
19
+ }>, boundaryLabel: string): PinnedOpenSyncFinalAdmission;
20
+ export {};
21
+ //# sourceMappingURL=root-file-final-admission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-file-final-admission.d.ts","sourceRoot":"","sources":["../src/root-file-final-admission.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAMrE,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,GAAG,cAAc,CAAC,CAAC;AAEhF,KAAK,aAAa,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE5D,MAAM,MAAM,wBAAwB,GAChC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAA;CAAE,GACnD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAoClC,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,mBAAmB,EACzB,QAAQ,EAAE,MAAM,GACf,wBAAwB,CAe1B;AAMD,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,mBAAmB,EACzB,IAAI,EAAE,OAAO,CAAC,wBAAwB,EAAE;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC,EACrD,aAAa,EAAE,MAAM,GACpB,4BAA4B,CAsC9B"}
@@ -0,0 +1,83 @@
1
+ import fs from "node:fs";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { realpathSync } from "./realpath.js";
4
+ import { admitPathInsideRoot } from "./root-boundary.js";
5
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
6
+ import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
7
+ function rootIdentityMismatch(cause) {
8
+ return new FsSafeError("path-mismatch", "canonical root directory identity changed or could not be verified", cause === undefined ? {} : { cause });
9
+ }
10
+ function isExpectedPathError(error) {
11
+ const code = typeof error === "object" && error !== null && "code" in error
12
+ ? String(error.code)
13
+ : "";
14
+ return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
15
+ }
16
+ function inspectCanonicalRoot(ioFs, rootPath, expected) {
17
+ try {
18
+ assertNoWindowsPathAlias(rootPath);
19
+ const operationPath = ioFs === fs ? pathForWindowsFilesystem(rootPath) : rootPath;
20
+ return inspectFileIdentitySync(() => {
21
+ const stat = ioFs.lstatSync(operationPath, { bigint: true });
22
+ if (!stat.isDirectory() || stat.isSymbolicLink())
23
+ throw rootIdentityMismatch();
24
+ return stat;
25
+ }, expected);
26
+ }
27
+ catch (error) {
28
+ if (expected && isExpectedPathError(error))
29
+ throw rootIdentityMismatch(error);
30
+ throw error;
31
+ }
32
+ }
33
+ export function observeCanonicalRoot(ioFs, rootPath) {
34
+ try {
35
+ const stat = inspectCanonicalRoot(ioFs, rootPath);
36
+ const identity = Object.freeze({ dev: stat.dev, ino: stat.ino });
37
+ let observedPath = rootPath;
38
+ if (process.platform === "win32" && ioFs === fs) {
39
+ // Native leaf resolution expands Windows short names. Bind that root
40
+ // spelling before traversal without replacing the observed identity.
41
+ observedPath = realpathSync.native(pathForWindowsFilesystem(rootPath));
42
+ if (observedPath !== rootPath)
43
+ inspectCanonicalRoot(ioFs, observedPath, identity);
44
+ }
45
+ return { ok: true, path: observedPath, identity };
46
+ }
47
+ catch (error) {
48
+ return { ok: false, error };
49
+ }
50
+ }
51
+ function resolveConsumedPath(ioFs, filePath) {
52
+ return ioFs === fs ? realpathSync.native(filePath) : ioFs.realpathSync(filePath);
53
+ }
54
+ export function createRootFileFinalAdmission(ioFs, root, boundaryLabel) {
55
+ const inspectRoot = (candidateRootPath, expected) => {
56
+ inspectCanonicalRoot(ioFs, candidateRootPath, expected);
57
+ };
58
+ return ({ path: consumedPath, descriptorIdentity }) => {
59
+ inspectCanonicalRoot(ioFs, root.path, root.identity);
60
+ const canonicalPath = resolveConsumedPath(ioFs, consumedPath);
61
+ assertNoWindowsPathAlias(canonicalPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
62
+ const admitted = admitPathInsideRoot({
63
+ rootPath: root.path,
64
+ candidatePath: canonicalPath,
65
+ rootIdentity: root.identity,
66
+ inspectCandidateRoot: inspectRoot,
67
+ });
68
+ if (!admitted) {
69
+ throw new FsSafeError("outside-workspace", `resolved path escapes ${boundaryLabel}`);
70
+ }
71
+ try {
72
+ inspectFileIdentitySync(() => ioFs.lstatSync(admitted.path, { bigint: true }), descriptorIdentity);
73
+ }
74
+ catch (error) {
75
+ if (isExpectedPathError(error)) {
76
+ throw new FsSafeError("path-mismatch", "canonical file identity changed or could not be verified", { cause: error });
77
+ }
78
+ throw error;
79
+ }
80
+ inspectCanonicalRoot(ioFs, root.path, root.identity);
81
+ return admitted.path;
82
+ };
83
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"root-file.d.ts","sourceRoot":"","sources":["../src/root-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAOzB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EACjC,MAAM,kBAAkB,CAAC;AAE1B,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,EAAE,EACP,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,UAAU,GACV,cAAc,GACd,cAAc,CACjB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,2BAA2B,GAAG,YAAY,CAAC;AAEnF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,yBAAyB,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,EAAE;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC;AAE7E,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,sBAAsB,GAAG;IACxD,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAQF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAW3D;AASD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,kBAAkB,CAuBnF;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE;IACR,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;IACjD,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;IACzC,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;CAC/C,GACA,CAAC,CAUH;AAoDD,wBAAsB,YAAY,CAChC,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CAwB7B"}
1
+ {"version":3,"file":"root-file.d.ts","sourceRoot":"","sources":["../src/root-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAOzB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EACjC,MAAM,kBAAkB,CAAC;AAS1B,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,EAAE,EACP,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,UAAU,GACV,cAAc,GACd,cAAc,CACjB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,2BAA2B,GAAG,YAAY,CAAC;AAEnF,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,yBAAyB,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,EAAE;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC;AAE7E,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,sBAAsB,GAAG;IACxD,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAqBF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAW3D;AASD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,kBAAkB,CAwCnF;AAED,wBAAgB,wBAAwB,CAAC,CAAC,EACxC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE;IACR,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;IACjD,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;IACzC,QAAQ,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,CAAC,CAAC;CAC/C,GACA,CAAC,CAUH;AA6DD,wBAAsB,YAAY,CAChC,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CAmD7B"}