@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
@@ -0,0 +1,70 @@
1
+ import path from "node:path";
2
+ import { WINDOWS_RESERVED_DEVICE_NAMES } from "./device-path.js";
3
+ import { admitPathInsideRoot } from "./root-boundary.js";
4
+ import { isSafePathSegment } from "./safe-path-segment.js";
5
+ function windowsReservedDeviceSegment(segment) {
6
+ const extension = segment.indexOf(".");
7
+ const stem = segment.slice(0, extension < 0 ? segment.length : extension).toUpperCase();
8
+ return WINDOWS_RESERVED_DEVICE_NAMES.has(stem);
9
+ }
10
+ function ordinaryWindowsSegments(relativePath) {
11
+ if (relativePath === "" || relativePath.includes("/") || relativePath.includes("\0"))
12
+ return false;
13
+ const segments = relativePath.split("\\");
14
+ return segments.every((segment) => isSafePathSegment(segment, { allowDotPrefix: true }) &&
15
+ !segment.endsWith(".") && !segment.endsWith(" ") &&
16
+ !windowsReservedDeviceSegment(segment));
17
+ }
18
+ function ordinaryPosixSegments(relativePath) {
19
+ return relativePath !== "" && !relativePath.includes("\\") &&
20
+ relativePath.split("/").every((segment) => isSafePathSegment(segment, { allowDotPrefix: true }));
21
+ }
22
+ function exactNonzeroRootIdentity(identity) {
23
+ return typeof identity?.dev === "bigint" && typeof identity.ino === "bigint" &&
24
+ identity.dev !== 0n && identity.ino !== 0n
25
+ ? Object.freeze({ dev: identity.dev, ino: identity.ino })
26
+ : undefined;
27
+ }
28
+ export function ordinarySharedAbsoluteInsideRoot(rootReal, candidatePath, rootIdentity) {
29
+ if (!path.isAbsolute(candidatePath) || candidatePath.includes("\0") ||
30
+ path.resolve(candidatePath) !== candidatePath ||
31
+ (process.platform === "win32"
32
+ ? candidatePath.startsWith("\\\\") || candidatePath.includes("/")
33
+ : candidatePath.includes("\\")))
34
+ return false;
35
+ const admitted = admitPathInsideRoot({
36
+ rootPath: rootReal,
37
+ candidatePath,
38
+ rootIdentity,
39
+ });
40
+ return admitted?.admission === "exact" && admitted.path === candidatePath &&
41
+ (admitted.relativePath === "" || (process.platform === "win32"
42
+ ? ordinaryWindowsSegments(admitted.relativePath)
43
+ : ordinaryPosixSegments(admitted.relativePath)));
44
+ }
45
+ export function simpleSharedRoute(params) {
46
+ if (process.versions.bun ||
47
+ params.policy.mutationSymlinks !== "reject" || !params.originalPath ||
48
+ params.originalPath.startsWith("~") || path.isAbsolute(params.originalPath) ||
49
+ !(process.platform === "win32"
50
+ ? ordinaryWindowsSegments(params.originalPath) &&
51
+ !params.rootReal.startsWith("\\\\") && !params.rootReal.includes("/")
52
+ : ordinaryPosixSegments(params.originalPath) && !params.rootReal.includes("\\")) ||
53
+ path.resolve(params.rootReal) !== params.rootReal)
54
+ return undefined;
55
+ const rootIdentity = exactNonzeroRootIdentity(params.rootIdentity);
56
+ if (!rootIdentity)
57
+ return undefined;
58
+ const route = path.resolve(params.rootReal, params.originalPath);
59
+ if (route !== params.selectedTarget ||
60
+ !ordinarySharedAbsoluteInsideRoot(params.rootReal, route, rootIdentity))
61
+ return undefined;
62
+ const policyEntries = [
63
+ ...(params.policy.denyMutations?.paths ?? []),
64
+ ...(params.policy.denyMutations?.prefixes ?? []),
65
+ ];
66
+ if (policyEntries.some((entry) => entry !== path.resolve(entry) ||
67
+ !ordinarySharedAbsoluteInsideRoot(params.rootReal, entry, rootIdentity)))
68
+ return undefined;
69
+ return { route, rootIdentity };
70
+ }
@@ -11,6 +11,11 @@ export type PinnedOpenSyncResult = {
11
11
  error?: unknown;
12
12
  };
13
13
  export type PinnedOpenSyncAllowedType = "file" | "directory";
14
+ export type PinnedOpenSyncFinalAdmission = (params: {
15
+ path: string;
16
+ preRealpathIdentity: fs.BigIntStats;
17
+ descriptorIdentity: fs.BigIntStats;
18
+ }) => string;
14
19
  export type PinnedOpenSyncFs = Pick<typeof fs, "constants" | "lstatSync" | "realpathSync" | "openSync" | "fstatSync" | "closeSync">;
15
20
  export declare function openPinnedFileSync(params: {
16
21
  filePath: string;
@@ -20,5 +25,6 @@ export declare function openPinnedFileSync(params: {
20
25
  maxBytes?: number;
21
26
  allowedType?: PinnedOpenSyncAllowedType;
22
27
  ioFs?: PinnedOpenSyncFs;
28
+ finalAdmission?: PinnedOpenSyncFinalAdmission;
23
29
  }): PinnedOpenSyncResult;
24
30
  //# sourceMappingURL=pinned-open.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAOzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAQF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,GAAG,oBAAoB,CAqDvB"}
1
+ {"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAQzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,4BAA4B,GAAG,CAAC,MAAM,EAAE;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB,EAAE,EAAE,CAAC,WAAW,CAAC;IACpC,kBAAkB,EAAE,EAAE,CAAC,WAAW,CAAC;CACpC,KAAK,MAAM,CAAC;AAEb,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAQF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,cAAc,CAAC,EAAE,4BAA4B,CAAC;CAC/C,GAAG,oBAAoB,CAuEvB"}
@@ -3,49 +3,67 @@ import { isUnsafeDeviceReadPath } from "./device-path.js";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
5
5
  import { resolveReadOpenFlags } from "./read-open-flags.js";
6
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
6
7
  import { realpathSync } from "./realpath.js";
7
8
  function isExpectedPathError(error) {
8
9
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
9
10
  return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
10
11
  }
11
12
  export function openPinnedFileSync(params) {
13
+ const filePath = params.filePath;
14
+ const resolvedPath = params.resolvedPath;
12
15
  const ioFs = params.ioFs ?? fs;
13
16
  const allowedType = params.allowedType ?? "file";
17
+ const rejectPathSymlink = params.rejectPathSymlink === true;
18
+ const statPolicy = {
19
+ rejectHardlinks: params.rejectHardlinks,
20
+ maxBytes: params.maxBytes,
21
+ };
14
22
  const openReadFlags = resolveReadOpenFlags({ constants: ioFs.constants });
15
23
  let fd = null;
16
24
  try {
17
- if (isUnsafeDeviceReadPath(params.filePath)) {
25
+ assertNoWindowsPathAlias(filePath, "filesystem", "file path uses a Windows filesystem namespace alias");
26
+ if (resolvedPath !== undefined) {
27
+ assertNoWindowsPathAlias(resolvedPath, "filesystem", "resolved path uses a Windows filesystem namespace alias");
28
+ }
29
+ if (isUnsafeDeviceReadPath(filePath)) {
18
30
  return { ok: false, reason: "validation" };
19
31
  }
20
- if (params.rejectPathSymlink) {
21
- const candidateStat = ioFs.lstatSync(params.filePath);
32
+ if (rejectPathSymlink) {
33
+ const candidateStat = ioFs.lstatSync(filePath);
22
34
  if (candidateStat.isSymbolicLink()) {
23
35
  return { ok: false, reason: "validation" };
24
36
  }
25
37
  }
26
- const realPath = params.resolvedPath ??
27
- (ioFs === fs ? realpathSync(params.filePath) : ioFs.realpathSync(params.filePath));
38
+ const realPath = resolvedPath ??
39
+ (ioFs === fs ? realpathSync(filePath) : ioFs.realpathSync(filePath));
40
+ assertNoWindowsPathAlias(realPath, "filesystem", "resolved path uses a Windows filesystem namespace alias");
28
41
  if (isUnsafeDeviceReadPath(realPath)) {
29
42
  return { ok: false, reason: "validation" };
30
43
  }
31
44
  const preOpenStat = inspectFileIdentitySync(() => {
32
45
  const stat = ioFs.lstatSync(realPath, { bigint: true });
33
- assertAllowedStat(stat, allowedType, params);
46
+ assertAllowedStat(stat, allowedType, statPolicy);
34
47
  return stat;
35
48
  });
36
49
  fd = ioFs.openSync(realPath, openReadFlags);
37
50
  const openedStat = ioFs.fstatSync(fd);
38
51
  const identity = inspectFileIdentitySync(() => {
39
52
  const stat = ioFs.fstatSync(fd, { bigint: true });
40
- assertAllowedStat(stat, allowedType, params);
53
+ assertAllowedStat(stat, allowedType, statPolicy);
41
54
  return stat;
42
55
  }, preOpenStat);
43
- inspectFileIdentitySync(() => {
56
+ const pathIdentity = inspectFileIdentitySync(() => {
44
57
  const stat = ioFs.lstatSync(realPath, { bigint: true });
45
- assertAllowedStat(stat, allowedType, params);
58
+ assertAllowedStat(stat, allowedType, statPolicy);
46
59
  return stat;
47
60
  }, identity);
48
- const opened = { ok: true, path: realPath, fd, stat: openedStat };
61
+ const admittedPath = params.finalAdmission?.({
62
+ path: realPath,
63
+ preRealpathIdentity: pathIdentity,
64
+ descriptorIdentity: identity,
65
+ }) ?? realPath;
66
+ const opened = { ok: true, path: admittedPath, fd, stat: openedStat };
49
67
  fd = null;
50
68
  return opened;
51
69
  }
@@ -0,0 +1,75 @@
1
+ import type { CopyFileInput } from "./copy-file-input.js";
2
+ import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
+ import type { FileIdentityStat } from "./file-identity.js";
4
+ import type { MutationDirectoryObservation } from "./pinned-mutation-observation.js";
5
+ export type PinnedWriteInput = {
6
+ kind: "buffer";
7
+ data: string | Buffer;
8
+ encoding?: BufferEncoding;
9
+ } | {
10
+ kind: "stream";
11
+ stream: AsyncIterable<Uint8Array | string>;
12
+ stageBeforePublish?: boolean;
13
+ } | CopyFileInput;
14
+ export type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
15
+ export type PublishedWriteIdentity = Readonly<{
16
+ dev: bigint;
17
+ ino: bigint;
18
+ }>;
19
+ export type PinnedMutationAdmissionReceipt = Readonly<object>;
20
+ export type PinnedMutationAuthorizationToken = Readonly<object>;
21
+ export type PinnedCreatedDirectoryReceipt = Readonly<{
22
+ admission: PinnedMutationAdmissionReceipt;
23
+ parent: MutationDirectoryObservation;
24
+ child: MutationDirectoryObservation;
25
+ }>;
26
+ export type PinnedMutationParentWalkSession = Readonly<{
27
+ retainedTargetPath: string;
28
+ tryAuthorizeAtParent(request: Readonly<{
29
+ targetPath: string;
30
+ mutationPath: string;
31
+ phase: "parent" | "parent-create";
32
+ }>, parent: MutationDirectoryObservation): PinnedMutationAuthorizationToken | undefined;
33
+ authorize(request: Readonly<{
34
+ targetPath: string;
35
+ mutationPath: string;
36
+ phase: "parent" | "parent-create";
37
+ }>): Promise<PinnedMutationAdmissionReceipt | undefined>;
38
+ advanceCreatedDirectory(receipt: PinnedCreatedDirectoryReceipt): PinnedMutationAuthorizationToken | undefined;
39
+ dispose(): void;
40
+ }>;
41
+ export type PinnedWriteMutationAdmission = Readonly<{
42
+ rejectParentSymlinks: boolean;
43
+ beginParentWalk?(): string | undefined;
44
+ beginSharedParentWalk?(): PinnedMutationParentWalkSession | undefined;
45
+ tryAuthorizeAtParent?(request: Readonly<{
46
+ targetPath: string;
47
+ mutationPath: string;
48
+ phase: "parent" | "parent-create";
49
+ }>, parent: MutationDirectoryObservation): PinnedMutationAuthorizationToken | undefined;
50
+ authorize(request: Readonly<{
51
+ targetPath: string;
52
+ mutationPath: string;
53
+ phase: "parent" | "parent-create";
54
+ }>): Promise<PinnedMutationAdmissionReceipt | undefined>;
55
+ advanceCreatedDirectory?(receipt: PinnedCreatedDirectoryReceipt): PinnedMutationAuthorizationToken | undefined;
56
+ }>;
57
+ export type PinnedWriteParams = {
58
+ rootPath: string;
59
+ relativeParentPath: string;
60
+ basename: string;
61
+ mkdir: boolean;
62
+ mode: number;
63
+ sync?: boolean;
64
+ overwrite?: boolean;
65
+ assertBeforeMutation?: () => void;
66
+ rejectFinalSymlink?: boolean;
67
+ maxBytes?: number;
68
+ input: PinnedWriteInput;
69
+ rootIdentity?: FileIdentityStat;
70
+ mutationAdmission?: PinnedWriteMutationAdmission;
71
+ onRenameIdentityMismatch?: "verify-content";
72
+ onPublished?: (identity: PublishedWriteIdentity) => void;
73
+ verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
74
+ };
75
+ //# sourceMappingURL=pinned-write-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-write-types.d.ts","sourceRoot":"","sources":["../src/pinned-write-types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,40 +1,14 @@
1
- import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
2
1
  import type { FileIdentityStat } from "./file-identity.js";
3
- import { type CopyFileInput } from "./copy-file-input.js";
4
- export type PinnedWriteInput = {
5
- kind: "buffer";
6
- data: string | Buffer;
7
- encoding?: BufferEncoding;
8
- } | {
9
- kind: "stream";
10
- stream: AsyncIterable<Uint8Array | string>;
11
- stageBeforePublish?: boolean;
12
- } | CopyFileInput;
13
- export type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
14
- export type PublishedWriteIdentity = Readonly<{
15
- dev: bigint;
16
- ino: bigint;
17
- }>;
18
- export type PinnedWriteParams = {
19
- rootPath: string;
20
- relativeParentPath: string;
21
- basename: string;
22
- mkdir: boolean;
23
- mode: number;
24
- sync?: boolean;
25
- overwrite?: boolean;
26
- assertBeforeMutation?: () => void;
27
- rejectFinalSymlink?: boolean;
28
- maxBytes?: number;
29
- input: PinnedWriteInput;
30
- rootIdentity?: FileIdentityStat;
31
- onRenameIdentityMismatch?: "verify-content";
32
- onPublished?: (identity: PublishedWriteIdentity) => void;
33
- verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
34
- };
2
+ import type { PinnedWriteParams, RenameIdentityPolicy } from "./pinned-write-types.js";
3
+ export type { PinnedWriteInput, RenameIdentityPolicy, PublishedWriteIdentity, PinnedMutationAdmissionReceipt, PinnedMutationAuthorizationToken, PinnedCreatedDirectoryReceipt, PinnedMutationParentWalkSession, PinnedWriteMutationAdmission, PinnedWriteParams, } from "./pinned-write-types.js";
35
4
  export declare function runPinnedWriteHelper(params: PinnedWriteParams): Promise<FileIdentityStat>;
36
5
  export declare function runPinnedWriteWithRenamePolicy(params: PinnedWriteParams & {
37
6
  targetPath: string;
38
7
  renameIdentity?: RenameIdentityPolicy;
39
8
  }): Promise<FileIdentityStat>;
9
+ export declare function withPinnedWriteRenameIdentityLock<T>(params: {
10
+ rootPath: string;
11
+ targetPath: string;
12
+ relativeTargetPath: string;
13
+ }, run: () => Promise<T>): Promise<T>;
40
14
  //# sourceMappingURL=pinned-write.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAOA,OAAO,EAA4F,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG7J,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAa3D,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AAG7E,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC,UAAU,GAAG,MAAM,CAAC,CAAC;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAE,GAC5F,aAAa,CAAC;AA4ClB,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,wBAAwB,CAAC,EAAE,gBAAgB,CAAC;IAC5C,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAEzD,eAAe,CAAC,EAAE,CAChB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgB/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
1
+ {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAiB3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACvF,YAAY,EACV,gBAAgB,EAAE,oBAAoB,EAAE,sBAAsB,EAC9D,8BAA8B,EAAE,gCAAgC,EAChE,6BAA6B,EAAE,+BAA+B,EAC9D,4BAA4B,EAAE,iBAAiB,GAChD,MAAM,yBAAyB,CAAC;AAgFjC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsD/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CAkB3B;AAED,wBAAsB,iCAAiC,CAAC,CAAC,EACvD,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,EAC5E,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC,CAiBZ"}
@@ -4,7 +4,7 @@ import fsSync, {} from "node:fs";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { normalizeMaxBytes } from "./byte-budget.js";
7
- import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
7
+ import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
8
8
  import { FsSafeError } from "./errors.js";
9
9
  import { syncDirectoryBestEffort } from "./directory-durability.js";
10
10
  import { sameFileIdentity, sha256Hex } from "./file-identity.js";
@@ -21,6 +21,7 @@ import { writeAllToFile } from "./write-file-handle.js";
21
21
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
22
22
  import { writeCopyFileToFd } from "./copy-file-input.js";
23
23
  import { publishCopyStage } from "./publish-copy-stage.js";
24
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
24
25
  function byteLength(input, encoding) {
25
26
  return typeof input === "string"
26
27
  ? Buffer.byteLength(input, encoding ?? "utf8")
@@ -31,6 +32,7 @@ function assertSafeBasename(basename) {
31
32
  basename === "." ||
32
33
  basename === ".." ||
33
34
  basename.includes("/") ||
35
+ (process.platform === "win32" && basename.includes("\\")) ||
34
36
  basename.includes("\0")) {
35
37
  throw new FsSafeError("invalid-path", "invalid target path");
36
38
  }
@@ -40,6 +42,10 @@ function assertWithinMaxBytes(bytes, maxBytes) {
40
42
  throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
41
43
  }
42
44
  }
45
+ function fastParentGuardDeopt(error) {
46
+ return error instanceof FsSafeError &&
47
+ (error.code === "path-mismatch" || error.code === "not-file");
48
+ }
43
49
  async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutation) {
44
50
  let bytes = 0;
45
51
  for await (const chunk of stream) {
@@ -49,12 +55,54 @@ async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutatio
49
55
  await writeAllToFile(handle, buffer, { assertBeforeMutation });
50
56
  }
51
57
  }
58
+ const PINNED_WRITE_SNAPSHOT_KEYS = [
59
+ "rootPath", "relativeParentPath", "basename", "maxBytes", "rootIdentity",
60
+ ];
61
+ const RENAME_POLICY_SNAPSHOT_KEYS = ["targetPath", "renameIdentity"];
62
+ function copyOwnEnumerableExcept(source, excluded) {
63
+ const owned = {};
64
+ for (const key of Reflect.ownKeys(source)) {
65
+ // Node 22's object-rest fast path can read excluded accessors eagerly.
66
+ // Exclude before even inspecting the descriptor so named authority and
67
+ // pathname fields remain single-read snapshots on every supported Node.
68
+ if (excluded.includes(key))
69
+ continue;
70
+ if (!Object.getOwnPropertyDescriptor(source, key)?.enumerable)
71
+ continue;
72
+ Object.defineProperty(owned, key, {
73
+ value: Reflect.get(source, key),
74
+ enumerable: true,
75
+ writable: true,
76
+ configurable: true,
77
+ });
78
+ }
79
+ return owned;
80
+ }
52
81
  export async function runPinnedWriteHelper(params) {
53
- const normalizedParams = { ...params, maxBytes: normalizeMaxBytes(params.maxBytes) };
54
- assertSafeBasename(params.basename);
82
+ const { rootPath, relativeParentPath, basename, maxBytes, rootIdentity } = params;
83
+ const ownedParams = copyOwnEnumerableExcept(params, PINNED_WRITE_SNAPSHOT_KEYS);
84
+ const normalizedParams = {
85
+ ...ownedParams,
86
+ rootPath,
87
+ relativeParentPath,
88
+ basename,
89
+ maxBytes: normalizeMaxBytes(maxBytes),
90
+ rootIdentity,
91
+ };
92
+ assertSafeBasename(normalizedParams.basename);
55
93
  validatePinnedOperationPayload({
56
- relativeParentPath: params.relativeParentPath,
94
+ relativeParentPath: normalizedParams.relativeParentPath,
57
95
  });
96
+ assertNoWindowsPathAlias(normalizedParams.rootPath, "filesystem", "pinned write root uses a Windows filesystem namespace alias");
97
+ assertNoWindowsPathAlias(normalizedParams.relativeParentPath, "relative", "pinned write parent uses a Windows filesystem namespace alias");
98
+ assertNoWindowsPathAlias(normalizedParams.basename, "relative", "pinned write basename uses a Windows filesystem namespace alias");
99
+ if (rootIdentity !== undefined) {
100
+ normalizedParams.rootIdentity = { dev: rootIdentity.dev, ino: rootIdentity.ino };
101
+ }
102
+ if (normalizedParams.mutationAdmission) {
103
+ const targetPath = path.join(normalizedParams.rootPath, ...normalizedParams.relativeParentPath.split("/").filter(Boolean), normalizedParams.basename);
104
+ await getFsSafeTestHooks()?.beforePinnedWriteParentAdmission?.(targetPath);
105
+ }
58
106
  // The explicit compatibility policy uses the guarded Node fallback, where
59
107
  // content verification can replace the strict post-rename inode check.
60
108
  if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
@@ -67,51 +115,137 @@ export async function runPinnedWriteHelper(params) {
67
115
  return await runPinnedWriteFallback(normalizedParams);
68
116
  }
69
117
  export async function runPinnedWriteWithRenamePolicy(params) {
70
- const { targetPath, renameIdentity, ...writeParams } = params;
118
+ const { targetPath, renameIdentity } = params;
119
+ const writeParams = copyOwnEnumerableExcept(params, RENAME_POLICY_SNAPSHOT_KEYS);
71
120
  if (renameIdentity !== "verify-content-with-lock") {
72
121
  return await runPinnedWriteHelper(writeParams);
73
122
  }
74
123
  const relativeTargetPath = writeParams.relativeParentPath
75
124
  ? `${writeParams.relativeParentPath}/${writeParams.basename}`
76
125
  : writeParams.basename;
77
- const lockPath = path.join(writeParams.rootPath, `.fs-safe-write-${sha256Hex(relativeTargetPath)}.lock`);
78
- return await withSidecarLock(writeParams.rootPath, {
79
- managerKey: `fs-safe.write:${targetPath}`,
126
+ return await withPinnedWriteRenameIdentityLock({
127
+ rootPath: writeParams.rootPath, targetPath, relativeTargetPath,
128
+ }, async () => await runPinnedWriteHelper({
129
+ ...writeParams,
130
+ onRenameIdentityMismatch: "verify-content",
131
+ }));
132
+ }
133
+ export async function withPinnedWriteRenameIdentityLock(params, run) {
134
+ const lockPath = path.join(params.rootPath, `.fs-safe-write-${sha256Hex(params.relativeTargetPath)}.lock`);
135
+ return await withSidecarLock(params.rootPath, {
136
+ managerKey: `fs-safe.write:${params.targetPath}`,
80
137
  lockPath,
81
138
  staleMs: 30_000,
82
139
  timeoutMs: 5_000,
83
140
  payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
84
141
  retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
85
- }, async () => await runPinnedWriteHelper({
86
- ...writeParams,
87
- onRenameIdentityMismatch: "verify-content",
88
- }));
142
+ }, run);
89
143
  }
90
144
  async function runPinnedWriteFallback(params) {
91
145
  const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
92
146
  ? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
147
+ const mutationAdmission = params.mutationAdmission;
93
148
  if (exactRoot)
94
149
  await inspectDirectoryIdentity(params.rootPath, exactRoot);
95
150
  let parentPath = params.relativeParentPath
96
151
  ? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
97
152
  : params.rootPath;
98
- if (params.mkdir) {
153
+ let parentGuard;
154
+ let parentAdmitted = false;
155
+ const initialTargetPath = path.join(parentPath, params.basename);
156
+ const retainedTargetPath = mutationAdmission?.beginParentWalk?.();
157
+ const ordinaryRetainedTarget = retainedTargetPath !== undefined &&
158
+ path.relative(path.resolve(retainedTargetPath), path.resolve(initialTargetPath)) === "";
159
+ let fastGuardClassifierFailure;
160
+ if (ordinaryRetainedTarget) {
161
+ try {
162
+ parentGuard = await createAsyncDirectoryGuard(parentPath, { bigint: true });
163
+ }
164
+ catch (error) {
165
+ // Only a genuinely missing complete parent may enter the component
166
+ // creator. ENOTDIR, permission, identity, and canonicalization failures
167
+ // retain their original fail-closed result.
168
+ if (error?.code !== "ENOENT") {
169
+ if (!fastParentGuardDeopt(error))
170
+ throw error;
171
+ fastGuardClassifierFailure = error;
172
+ }
173
+ }
174
+ if (parentGuard) {
175
+ await mutationAdmission.authorize(Object.freeze({
176
+ targetPath: initialTargetPath,
177
+ mutationPath: initialTargetPath,
178
+ phase: "parent",
179
+ }));
180
+ await assertAsyncDirectoryGuard(parentGuard);
181
+ parentAdmitted = true;
182
+ }
183
+ }
184
+ if (fastGuardClassifierFailure) {
185
+ // The optimized exact-guard classifier runs before policy admission. Let
186
+ // the established resolver restore deny/symlink precedence before using
187
+ // the guarded walker or reporting the classifier failure. Because this
188
+ // was not an ENOENT probe, it may never authorize a newly missing parent.
189
+ await mutationAdmission.authorize(Object.freeze({
190
+ targetPath: initialTargetPath,
191
+ mutationPath: initialTargetPath,
192
+ phase: "parent",
193
+ }));
194
+ }
195
+ if (params.mkdir && !parentGuard) {
99
196
  // mkdirPathComponentsWithGuards may resolve the final component through
100
197
  // an in-root symlink (e.g. a skill-bank layout). Use its returned real
101
198
  // path for the subsequent guard and target path so we don't re-check the
102
199
  // original, possibly-symlinked, lexical path and reject it outright.
103
- parentPath = await mkdirPathComponentsWithGuards({
200
+ const mkdirParams = {
104
201
  rootReal: params.rootPath,
105
202
  targetPath: parentPath,
106
203
  rootIdentity: params.rootIdentity,
107
204
  assertBeforeMutation: params.assertBeforeMutation,
108
- beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
109
- });
205
+ beforeComponent: async (componentPath) => {
206
+ await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath);
207
+ },
208
+ };
209
+ parentPath = await mkdirPathComponentsWithGuards(mutationAdmission ? {
210
+ ...mkdirParams,
211
+ rejectSymlinks: mutationAdmission.rejectParentSymlinks,
212
+ revalidateParentAfterBeforeComponent: true,
213
+ synchronousAuthorizationIncludesFence: true,
214
+ retainedTargetPath,
215
+ afterCreateComponent: mutationAdmission.advanceCreatedDirectory,
216
+ beforeCreateComponent: (componentPath, prospectiveParentPath, retainedTarget, parent) => {
217
+ if (fastGuardClassifierFailure)
218
+ throw fastGuardClassifierFailure;
219
+ const request = Object.freeze({
220
+ targetPath: retainedTarget ?? path.join(prospectiveParentPath, params.basename),
221
+ mutationPath: componentPath,
222
+ phase: "parent-create",
223
+ });
224
+ return mutationAdmission.tryAuthorizeAtParent?.(request, parent) ??
225
+ mutationAdmission.authorize(request);
226
+ },
227
+ beforeUseComponent: async (_componentPath, prospectiveParentPath, retainedTarget) => {
228
+ const targetPath = retainedTarget ?? path.join(prospectiveParentPath, params.basename);
229
+ await mutationAdmission.authorize(Object.freeze({
230
+ targetPath,
231
+ mutationPath: targetPath,
232
+ phase: "parent",
233
+ }));
234
+ },
235
+ } : mkdirParams);
110
236
  }
111
- const parentGuard = params.mkdir
237
+ parentGuard ??= params.mkdir
112
238
  ? await createAsyncDirectoryGuard(parentPath, { bigint: true })
113
239
  : await createNearestExistingDirectoryGuard(params.rootPath, parentPath, { bigint: true });
114
240
  const targetPath = path.join(parentPath, params.basename);
241
+ if (mutationAdmission && !parentAdmitted) {
242
+ await mutationAdmission.authorize(Object.freeze({
243
+ targetPath,
244
+ mutationPath: targetPath,
245
+ phase: "parent",
246
+ }));
247
+ await assertAsyncDirectoryGuard(parentGuard);
248
+ }
115
249
  if (params.overwrite === false && (params.input.kind === "buffer" ||
116
250
  (params.input.kind === "stream" && !params.input.stageBeforePublish))) {
117
251
  const assertBeforeMutation = () => {
@@ -1 +1 @@
1
- {"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,IAAI,CAAC,CAkBf"}
1
+ {"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,IAAI,CAAC,CAyBf"}
@@ -1,10 +1,12 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  import { getNativeBinding } from "./native.js";
3
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
3
4
  export async function createPrivateDirectory(targetPath, options) {
4
5
  const platform = options?.platform ?? process.platform;
5
6
  if (platform !== "win32") {
6
7
  throw new FsSafeError("helper-unavailable", "private-directory creation is supported only on Windows");
7
8
  }
9
+ assertNoWindowsPathAlias(targetPath, "filesystem", "private directory path uses a Windows filesystem namespace alias", platform);
8
10
  const native = getNativeBinding();
9
11
  if (!native) {
10
12
  throw new FsSafeError("helper-unavailable", "private Windows directory creation requires the matching optional native platform package; " +
@@ -0,0 +1,16 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import { type FileHandle } from "node:fs/promises";
3
+ import { type TempPathRegistration } from "./temp-cleanup.js";
4
+ export type PrivateProducerHandoff = {
5
+ handle: FileHandle;
6
+ identity: BigIntStats;
7
+ unregister: TempPathRegistration;
8
+ };
9
+ export declare function handoffPrivateProducerFile(params: {
10
+ sourcePath: string;
11
+ targetPath: string;
12
+ assertSourceParent: () => void;
13
+ assertTargetParent: () => void;
14
+ readWrite: boolean;
15
+ }): Promise<PrivateProducerHandoff>;
16
+ //# sourceMappingURL=private-producer-handoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"private-producer-handoff.d.ts","sourceRoot":"","sources":["../src/private-producer-handoff.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAW,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKvD,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,oBAAoB,CAAC;CAClC,CAAC;AA2OF,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA6HlC"}