@openclaw/fs-safe 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +15 -8
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +32 -11
  5. package/dist/advanced.d.ts +3 -1
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +3 -1
  8. package/dist/archive-entry.d.ts.map +1 -1
  9. package/dist/archive-entry.js +4 -3
  10. package/dist/archive-gzip-tail.d.ts.map +1 -1
  11. package/dist/archive-gzip-tail.js +13 -9
  12. package/dist/archive-input.d.ts.map +1 -1
  13. package/dist/archive-input.js +6 -2
  14. package/dist/archive-merge.d.ts.map +1 -1
  15. package/dist/archive-merge.js +15 -2
  16. package/dist/archive-native.d.ts.map +1 -1
  17. package/dist/archive-native.js +7 -19
  18. package/dist/archive-plan.js +1 -1
  19. package/dist/archive-read.d.ts.map +1 -1
  20. package/dist/archive-read.js +55 -46
  21. package/dist/archive-staging.d.ts.map +1 -1
  22. package/dist/archive-staging.js +43 -10
  23. package/dist/archive-tar-inspect.d.ts.map +1 -1
  24. package/dist/archive-tar-inspect.js +4 -1
  25. package/dist/archive-zip-admission.d.ts +1 -1
  26. package/dist/archive-zip-admission.d.ts.map +1 -1
  27. package/dist/archive-zip-admission.js +2 -2
  28. package/dist/archive-zip-directory.d.ts +3 -0
  29. package/dist/archive-zip-directory.d.ts.map +1 -1
  30. package/dist/archive-zip-directory.js +13 -2
  31. package/dist/archive-zip-loader.d.ts +3 -2
  32. package/dist/archive-zip-loader.d.ts.map +1 -1
  33. package/dist/archive-zip-loader.js +30 -4
  34. package/dist/archive-zip-manifest.d.ts +5 -0
  35. package/dist/archive-zip-manifest.d.ts.map +1 -0
  36. package/dist/archive-zip-manifest.js +22 -0
  37. package/dist/archive-zip-names.d.ts +6 -1
  38. package/dist/archive-zip-names.d.ts.map +1 -1
  39. package/dist/archive-zip-names.js +50 -17
  40. package/dist/archive-zip-preflight.d.ts.map +1 -1
  41. package/dist/archive-zip-preflight.js +3 -2
  42. package/dist/archive.d.ts.map +1 -1
  43. package/dist/archive.js +9 -4
  44. package/dist/bounded-read.js +2 -2
  45. package/dist/darwin-acl.d.ts +4 -0
  46. package/dist/darwin-acl.d.ts.map +1 -0
  47. package/dist/darwin-acl.js +24 -0
  48. package/dist/deny-mutations.d.ts.map +1 -1
  49. package/dist/deny-mutations.js +8 -2
  50. package/dist/device-path.d.ts.map +1 -1
  51. package/dist/device-path.js +5 -3
  52. package/dist/directory-durability.d.ts.map +1 -1
  53. package/dist/directory-durability.js +67 -23
  54. package/dist/directory-entry-path.d.ts +3 -0
  55. package/dist/directory-entry-path.d.ts.map +1 -0
  56. package/dist/directory-entry-path.js +21 -0
  57. package/dist/directory-guard.d.ts +17 -1
  58. package/dist/directory-guard.d.ts.map +1 -1
  59. package/dist/directory-guard.js +134 -48
  60. package/dist/directory-mode-node.d.ts +12 -0
  61. package/dist/directory-mode-node.d.ts.map +1 -1
  62. package/dist/directory-mode-node.js +102 -4
  63. package/dist/effective-uid.d.ts +2 -0
  64. package/dist/effective-uid.d.ts.map +1 -0
  65. package/dist/effective-uid.js +25 -0
  66. package/dist/file-handle-transfer.d.ts.map +1 -1
  67. package/dist/file-handle-transfer.js +98 -27
  68. package/dist/file-hash.d.ts.map +1 -1
  69. package/dist/file-hash.js +12 -2
  70. package/dist/file-lock-sync.d.ts.map +1 -1
  71. package/dist/file-lock-sync.js +36 -6
  72. package/dist/file-lock.d.ts.map +1 -1
  73. package/dist/file-lock.js +29 -9
  74. package/dist/file-observation.d.ts +1 -1
  75. package/dist/file-observation.d.ts.map +1 -1
  76. package/dist/file-store-boundary.d.ts +6 -2
  77. package/dist/file-store-boundary.d.ts.map +1 -1
  78. package/dist/file-store-boundary.js +20 -65
  79. package/dist/file-store-copy-source.d.ts +5 -0
  80. package/dist/file-store-copy-source.d.ts.map +1 -0
  81. package/dist/file-store-copy-source.js +31 -0
  82. package/dist/file-store-path.d.ts.map +1 -1
  83. package/dist/file-store-path.js +4 -1
  84. package/dist/file-store-prune.d.ts.map +1 -1
  85. package/dist/file-store-prune.js +9 -1
  86. package/dist/file-store-sync-directory.d.ts +16 -0
  87. package/dist/file-store-sync-directory.d.ts.map +1 -0
  88. package/dist/file-store-sync-directory.js +349 -0
  89. package/dist/file-store.d.ts.map +1 -1
  90. package/dist/file-store.js +11 -28
  91. package/dist/filename.d.ts +1 -0
  92. package/dist/filename.d.ts.map +1 -1
  93. package/dist/filename.js +85 -23
  94. package/dist/fs.d.ts.map +1 -1
  95. package/dist/fs.js +3 -2
  96. package/dist/guarded-mkdir.d.ts +10 -0
  97. package/dist/guarded-mkdir.d.ts.map +1 -1
  98. package/dist/guarded-mkdir.js +217 -33
  99. package/dist/guest.d.ts.map +1 -1
  100. package/dist/guest.js +16 -5
  101. package/dist/home-dir.d.ts.map +1 -1
  102. package/dist/home-dir.js +73 -10
  103. package/dist/install-path.d.ts +6 -0
  104. package/dist/install-path.d.ts.map +1 -1
  105. package/dist/install-path.js +67 -19
  106. package/dist/json-durable-queue-ownership.d.ts +8 -0
  107. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  108. package/dist/json-durable-queue-ownership.js +134 -43
  109. package/dist/json-durable-queue-paths.d.ts +11 -0
  110. package/dist/json-durable-queue-paths.d.ts.map +1 -0
  111. package/dist/json-durable-queue-paths.js +42 -0
  112. package/dist/json-durable-queue-read.d.ts +6 -0
  113. package/dist/json-durable-queue-read.d.ts.map +1 -0
  114. package/dist/json-durable-queue-read.js +61 -0
  115. package/dist/json-durable-queue.d.ts +1 -1
  116. package/dist/json-durable-queue.d.ts.map +1 -1
  117. package/dist/json-durable-queue.js +83 -134
  118. package/dist/json-store.d.ts.map +1 -1
  119. package/dist/json-store.js +5 -1
  120. package/dist/json.d.ts.map +1 -1
  121. package/dist/json.js +54 -22
  122. package/dist/local-file-access.d.ts.map +1 -1
  123. package/dist/local-file-access.js +4 -0
  124. package/dist/local-file-descriptor.d.ts +17 -0
  125. package/dist/local-file-descriptor.d.ts.map +1 -0
  126. package/dist/local-file-descriptor.js +84 -0
  127. package/dist/local-roots.d.ts.map +1 -1
  128. package/dist/local-roots.js +35 -7
  129. package/dist/move-path-cleanup.d.ts +2 -0
  130. package/dist/move-path-cleanup.d.ts.map +1 -1
  131. package/dist/move-path-cleanup.js +44 -18
  132. package/dist/move-path.d.ts.map +1 -1
  133. package/dist/move-path.js +31 -6
  134. package/dist/native-binding.d.ts +21 -0
  135. package/dist/native-binding.d.ts.map +1 -1
  136. package/dist/native-directory-observation.d.ts +17 -0
  137. package/dist/native-directory-observation.d.ts.map +1 -0
  138. package/dist/native-directory-observation.js +37 -0
  139. package/dist/native-parent-admission.d.ts +31 -0
  140. package/dist/native-parent-admission.d.ts.map +1 -0
  141. package/dist/native-parent-admission.js +124 -0
  142. package/dist/native-pinned-write-windows.d.ts +0 -1
  143. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  144. package/dist/native-pinned-write-windows.js +0 -3
  145. package/dist/native-pinned-write.d.ts.map +1 -1
  146. package/dist/native-pinned-write.js +309 -40
  147. package/dist/output.d.ts.map +1 -1
  148. package/dist/output.js +15 -5
  149. package/dist/overwrite-file-handle.d.ts.map +1 -1
  150. package/dist/overwrite-file-handle.js +5 -1
  151. package/dist/owner-dacl.d.ts.map +1 -1
  152. package/dist/owner-dacl.js +2 -0
  153. package/dist/path-policy.d.ts.map +1 -1
  154. package/dist/path-policy.js +7 -2
  155. package/dist/path-prefix.d.ts +7 -0
  156. package/dist/path-prefix.d.ts.map +1 -0
  157. package/dist/path-prefix.js +82 -0
  158. package/dist/path-scope-lexical.d.ts +14 -0
  159. package/dist/path-scope-lexical.d.ts.map +1 -0
  160. package/dist/path-scope-lexical.js +37 -0
  161. package/dist/path-segment-route.d.ts +7 -0
  162. package/dist/path-segment-route.d.ts.map +1 -0
  163. package/dist/path-segment-route.js +24 -0
  164. package/dist/path-suffix-aliases.d.ts +10 -0
  165. package/dist/path-suffix-aliases.d.ts.map +1 -0
  166. package/dist/path-suffix-aliases.js +386 -0
  167. package/dist/path.d.ts.map +1 -1
  168. package/dist/path.js +35 -6
  169. package/dist/permissions-windows.d.ts.map +1 -1
  170. package/dist/permissions-windows.js +14 -3
  171. package/dist/permissions.d.ts.map +1 -1
  172. package/dist/permissions.js +37 -8
  173. package/dist/pinned-mutation-admission.d.ts +25 -0
  174. package/dist/pinned-mutation-admission.d.ts.map +1 -0
  175. package/dist/pinned-mutation-admission.js +425 -0
  176. package/dist/pinned-mutation-observation.d.ts +34 -0
  177. package/dist/pinned-mutation-observation.d.ts.map +1 -0
  178. package/dist/pinned-mutation-observation.js +142 -0
  179. package/dist/pinned-mutation-shared-route.d.ts +24 -0
  180. package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
  181. package/dist/pinned-mutation-shared-route.js +70 -0
  182. package/dist/pinned-open.d.ts +6 -0
  183. package/dist/pinned-open.d.ts.map +1 -1
  184. package/dist/pinned-open.js +28 -10
  185. package/dist/pinned-write-types.d.ts +75 -0
  186. package/dist/pinned-write-types.d.ts.map +1 -0
  187. package/dist/pinned-write-types.js +1 -0
  188. package/dist/pinned-write.d.ts +7 -33
  189. package/dist/pinned-write.d.ts.map +1 -1
  190. package/dist/pinned-write.js +152 -17
  191. package/dist/private-directory.d.ts.map +1 -1
  192. package/dist/private-directory.js +2 -0
  193. package/dist/private-producer-handoff.d.ts +16 -0
  194. package/dist/private-producer-handoff.d.ts.map +1 -0
  195. package/dist/private-producer-handoff.js +272 -0
  196. package/dist/private-temp-workspace.d.ts +2 -39
  197. package/dist/private-temp-workspace.d.ts.map +1 -1
  198. package/dist/private-temp-workspace.js +183 -77
  199. package/dist/publish-file.d.ts.map +1 -1
  200. package/dist/publish-file.js +33 -29
  201. package/dist/regular-file.d.ts.map +1 -1
  202. package/dist/regular-file.js +56 -45
  203. package/dist/replace-directory.d.ts.map +1 -1
  204. package/dist/replace-directory.js +12 -5
  205. package/dist/replace-file-temp-owner.d.ts +1 -0
  206. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  207. package/dist/replace-file-temp-owner.js +12 -1
  208. package/dist/replace-file.d.ts.map +1 -1
  209. package/dist/replace-file.js +24 -24
  210. package/dist/root-boundary.d.ts +29 -0
  211. package/dist/root-boundary.d.ts.map +1 -0
  212. package/dist/root-boundary.js +182 -0
  213. package/dist/root-context.d.ts +12 -1
  214. package/dist/root-context.d.ts.map +1 -1
  215. package/dist/root-context.js +89 -16
  216. package/dist/root-directory-creation.d.ts +13 -0
  217. package/dist/root-directory-creation.d.ts.map +1 -0
  218. package/dist/root-directory-creation.js +212 -0
  219. package/dist/root-directory-list.d.ts +16 -4
  220. package/dist/root-directory-list.d.ts.map +1 -1
  221. package/dist/root-directory-list.js +182 -33
  222. package/dist/root-directory.d.ts +23 -0
  223. package/dist/root-directory.d.ts.map +1 -0
  224. package/dist/root-directory.js +141 -0
  225. package/dist/root-errors.d.ts.map +1 -1
  226. package/dist/root-errors.js +12 -1
  227. package/dist/root-file-final-admission.d.ts +21 -0
  228. package/dist/root-file-final-admission.d.ts.map +1 -0
  229. package/dist/root-file-final-admission.js +83 -0
  230. package/dist/root-file.d.ts.map +1 -1
  231. package/dist/root-file.js +103 -24
  232. package/dist/root-impl.d.ts +1 -0
  233. package/dist/root-impl.d.ts.map +1 -1
  234. package/dist/root-impl.js +316 -325
  235. package/dist/root-move-noreplace.d.ts +14 -0
  236. package/dist/root-move-noreplace.d.ts.map +1 -0
  237. package/dist/root-move-noreplace.js +202 -0
  238. package/dist/root-move-preflight.d.ts +8 -0
  239. package/dist/root-move-preflight.d.ts.map +1 -0
  240. package/dist/root-move-preflight.js +16 -0
  241. package/dist/root-observed-path.d.ts +9 -0
  242. package/dist/root-observed-path.d.ts.map +1 -0
  243. package/dist/root-observed-path.js +95 -0
  244. package/dist/root-path-errors.d.ts +12 -0
  245. package/dist/root-path-errors.d.ts.map +1 -0
  246. package/dist/root-path-errors.js +13 -0
  247. package/dist/root-path-existing.d.ts +2 -0
  248. package/dist/root-path-existing.d.ts.map +1 -1
  249. package/dist/root-path-existing.js +63 -22
  250. package/dist/root-path-observation.d.ts +63 -0
  251. package/dist/root-path-observation.d.ts.map +1 -0
  252. package/dist/root-path-observation.js +180 -0
  253. package/dist/root-path-stat.d.ts +5 -0
  254. package/dist/root-path-stat.d.ts.map +1 -0
  255. package/dist/root-path-stat.js +101 -0
  256. package/dist/root-path-symlink.d.ts.map +1 -1
  257. package/dist/root-path-symlink.js +23 -4
  258. package/dist/root-path.d.ts +13 -0
  259. package/dist/root-path.d.ts.map +1 -1
  260. package/dist/root-path.js +263 -73
  261. package/dist/root-paths-lexical.d.ts +9 -0
  262. package/dist/root-paths-lexical.d.ts.map +1 -0
  263. package/dist/root-paths-lexical.js +22 -0
  264. package/dist/root-paths.d.ts +3 -29
  265. package/dist/root-paths.d.ts.map +1 -1
  266. package/dist/root-paths.js +89 -178
  267. package/dist/root-read-admission.d.ts +26 -0
  268. package/dist/root-read-admission.d.ts.map +1 -0
  269. package/dist/root-read-admission.js +94 -0
  270. package/dist/root-remove-identity.d.ts +15 -0
  271. package/dist/root-remove-identity.d.ts.map +1 -0
  272. package/dist/root-remove-identity.js +89 -0
  273. package/dist/root-remove-receipt.d.ts +17 -0
  274. package/dist/root-remove-receipt.d.ts.map +1 -0
  275. package/dist/root-remove-receipt.js +37 -0
  276. package/dist/root-remove.d.ts +2 -1
  277. package/dist/root-remove.d.ts.map +1 -1
  278. package/dist/root-remove.js +172 -10
  279. package/dist/root-walk.d.ts.map +1 -1
  280. package/dist/root-walk.js +2 -1
  281. package/dist/root-write-admission.d.ts +68 -0
  282. package/dist/root-write-admission.d.ts.map +1 -0
  283. package/dist/root-write-admission.js +322 -0
  284. package/dist/root-write-compatibility.d.ts +13 -0
  285. package/dist/root-write-compatibility.d.ts.map +1 -0
  286. package/dist/root-write-compatibility.js +74 -0
  287. package/dist/root-write-complete-parent.d.ts +42 -0
  288. package/dist/root-write-complete-parent.d.ts.map +1 -0
  289. package/dist/root-write-complete-parent.js +195 -0
  290. package/dist/root-write-mode.d.ts +2 -0
  291. package/dist/root-write-mode.d.ts.map +1 -1
  292. package/dist/root-write-mode.js +20 -7
  293. package/dist/root-write-publication.d.ts +24 -0
  294. package/dist/root-write-publication.d.ts.map +1 -0
  295. package/dist/root-write-publication.js +85 -0
  296. package/dist/root-write-verification.d.ts.map +1 -1
  297. package/dist/root-write-verification.js +18 -7
  298. package/dist/safe-path-segment.d.ts.map +1 -1
  299. package/dist/safe-path-segment.js +3 -1
  300. package/dist/secret-file.d.ts.map +1 -1
  301. package/dist/secret-file.js +62 -24
  302. package/dist/secret-read-async.d.ts.map +1 -1
  303. package/dist/secret-read-async.js +18 -13
  304. package/dist/secret-read-policy.d.ts.map +1 -1
  305. package/dist/secret-read-policy.js +5 -1
  306. package/dist/secure-file-windows.d.ts +10 -0
  307. package/dist/secure-file-windows.d.ts.map +1 -0
  308. package/dist/secure-file-windows.js +186 -0
  309. package/dist/secure-file.d.ts.map +1 -1
  310. package/dist/secure-file.js +98 -10
  311. package/dist/secure-temp-dir.d.ts +6 -3
  312. package/dist/secure-temp-dir.d.ts.map +1 -1
  313. package/dist/secure-temp-dir.js +121 -101
  314. package/dist/secure-temp-repair.d.ts +35 -0
  315. package/dist/secure-temp-repair.d.ts.map +1 -0
  316. package/dist/secure-temp-repair.js +106 -0
  317. package/dist/sibling-staged-file.d.ts +3 -1
  318. package/dist/sibling-staged-file.d.ts.map +1 -1
  319. package/dist/sibling-staged-file.js +181 -50
  320. package/dist/sibling-temp.d.ts.map +1 -1
  321. package/dist/sibling-temp.js +27 -12
  322. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  323. package/dist/sidecar-lock-acquire.js +45 -15
  324. package/dist/sidecar-lock-root.d.ts.map +1 -1
  325. package/dist/sidecar-lock-root.js +4 -2
  326. package/dist/sidecar-lock.d.ts.map +1 -1
  327. package/dist/sidecar-lock.js +2 -3
  328. package/dist/staged-directory.d.ts +14 -5
  329. package/dist/staged-directory.d.ts.map +1 -1
  330. package/dist/staged-directory.js +54 -3
  331. package/dist/standalone-publication-path.d.ts +2 -0
  332. package/dist/standalone-publication-path.d.ts.map +1 -0
  333. package/dist/standalone-publication-path.js +6 -0
  334. package/dist/stat-observation.d.ts +11 -0
  335. package/dist/stat-observation.d.ts.map +1 -0
  336. package/dist/stat-observation.js +64 -0
  337. package/dist/strict-file-identity.d.ts.map +1 -1
  338. package/dist/strict-file-identity.js +39 -2
  339. package/dist/symlink-parents.d.ts.map +1 -1
  340. package/dist/symlink-parents.js +7 -2
  341. package/dist/temp-target.d.ts +2 -0
  342. package/dist/temp-target.d.ts.map +1 -1
  343. package/dist/temp-target.js +133 -4
  344. package/dist/temp-workspace-admission.d.ts +22 -0
  345. package/dist/temp-workspace-admission.d.ts.map +1 -0
  346. package/dist/temp-workspace-admission.js +374 -0
  347. package/dist/temp-workspace-child-admission.d.ts +13 -0
  348. package/dist/temp-workspace-child-admission.d.ts.map +1 -0
  349. package/dist/temp-workspace-child-admission.js +95 -0
  350. package/dist/temp-workspace-descriptor.d.ts +48 -0
  351. package/dist/temp-workspace-descriptor.d.ts.map +1 -0
  352. package/dist/temp-workspace-descriptor.js +363 -0
  353. package/dist/temp-workspace-identity.d.ts +15 -0
  354. package/dist/temp-workspace-identity.d.ts.map +1 -0
  355. package/dist/temp-workspace-identity.js +41 -0
  356. package/dist/temp-workspace-owner.d.ts +7 -6
  357. package/dist/temp-workspace-owner.d.ts.map +1 -1
  358. package/dist/temp-workspace-owner.js +121 -61
  359. package/dist/temp-workspace-permissions.d.ts +4 -0
  360. package/dist/temp-workspace-permissions.d.ts.map +1 -0
  361. package/dist/temp-workspace-permissions.js +32 -0
  362. package/dist/temp-workspace-types.d.ts +40 -0
  363. package/dist/temp-workspace-types.d.ts.map +1 -0
  364. package/dist/temp-workspace-types.js +1 -0
  365. package/dist/temp.d.ts +1 -1
  366. package/dist/temp.d.ts.map +1 -1
  367. package/dist/temp.js +1 -1
  368. package/dist/test-hooks.d.ts +7 -0
  369. package/dist/test-hooks.d.ts.map +1 -1
  370. package/dist/text-atomic.d.ts.map +1 -1
  371. package/dist/text-atomic.js +3 -1
  372. package/dist/trash.d.ts.map +1 -1
  373. package/dist/trash.js +54 -8
  374. package/dist/walk.d.ts.map +1 -1
  375. package/dist/walk.js +20 -16
  376. package/dist/windows-owner.d.ts.map +1 -1
  377. package/dist/windows-owner.js +5 -0
  378. package/dist/windows-path-alias.d.ts +39 -0
  379. package/dist/windows-path-alias.d.ts.map +1 -0
  380. package/dist/windows-path-alias.js +153 -0
  381. package/docs/advanced.md +28 -1
  382. package/docs/archive.md +29 -2
  383. package/docs/atomic.md +27 -3
  384. package/docs/contributing.md +4 -1
  385. package/docs/copy.md +4 -1
  386. package/docs/durability.md +18 -2
  387. package/docs/errors.md +14 -6
  388. package/docs/file-store.md +30 -3
  389. package/docs/filename.md +21 -7
  390. package/docs/guest.md +5 -0
  391. package/docs/install-path.md +61 -15
  392. package/docs/install.md +12 -8
  393. package/docs/json-store.md +5 -1
  394. package/docs/json.md +11 -0
  395. package/docs/mutation-policy-proof.md +65 -0
  396. package/docs/native-helper.md +28 -9
  397. package/docs/native.md +46 -10
  398. package/docs/output.md +33 -11
  399. package/docs/path-prefix.md +64 -0
  400. package/docs/path-suffix-aliases.md +159 -0
  401. package/docs/path.md +12 -1
  402. package/docs/permissions.md +26 -1
  403. package/docs/private-file-store.md +14 -0
  404. package/docs/public-api.md +14 -0
  405. package/docs/quickstart.md +1 -1
  406. package/docs/reading.md +8 -1
  407. package/docs/root.md +21 -4
  408. package/docs/secret-file.md +3 -0
  409. package/docs/secure-file.md +21 -16
  410. package/docs/security-model.md +76 -9
  411. package/docs/sidecar-lock.md +28 -1
  412. package/docs/store.md +27 -2
  413. package/docs/temp.md +262 -34
  414. package/docs/test-hooks.md +14 -0
  415. package/docs/writing.md +38 -8
  416. package/package.json +10 -10
@@ -1,26 +1,33 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import { FsSafeError } from "./errors.js";
4
- import { isNotFoundPathError, isPathInside } from "./path.js";
4
+ import { isNotFoundPathError } from "./path.js";
5
5
  import { resolveReadOpenFlags } from "./read-open-flags.js";
6
6
  import { realpathSync } from "./realpath.js";
7
7
  import { hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
8
8
  import { inspectFileIdentity } from "./strict-file-identity.js";
9
+ import { admitPathInsideRoot } from "./root-boundary.js";
9
10
  // The caller has already resolved and guarded this write target.
10
11
  export async function inheritWriteTargetMode(params) {
11
12
  try {
12
- const existing = await inspectFileIdentity(() => fsSync.lstatSync(params.targetPath, { bigint: true }));
13
+ const admittedTarget = admitPathInsideRoot({
14
+ rootPath: params.rootWithSep,
15
+ candidatePath: params.targetPath,
16
+ rootIdentity: params.rootIdentity,
17
+ });
18
+ if (!admittedTarget)
19
+ throw outsideWorkspaceError();
20
+ const targetPath = admittedTarget.path;
21
+ const existing = await inspectFileIdentity(() => fsSync.lstatSync(targetPath, { bigint: true }));
13
22
  if (existing.isSymbolicLink())
14
23
  throw new FsSafeError("path-alias", "path alias escape blocked");
15
24
  if (!existing.isFile())
16
25
  throw new FsSafeError("not-file", "not a file");
17
26
  if (existing.nlink > 1n)
18
27
  throw hardlinkedPathNotAllowedError();
19
- if (!isPathInside(params.rootWithSep, params.targetPath))
20
- throw outsideWorkspaceError();
21
28
  // Preserve read-open admission of the pre-existing destination. access(2)
22
29
  // is not equivalent: it ignores ACLs on Windows and capabilities on Linux.
23
- const handle = await fs.open(params.targetPath, resolveReadOpenFlags());
30
+ const handle = await fs.open(targetPath, resolveReadOpenFlags());
24
31
  try {
25
32
  // Bind admission to the inode whose metadata is inherited.
26
33
  await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }), existing);
@@ -31,9 +38,15 @@ export async function inheritWriteTargetMode(params) {
31
38
  try {
32
39
  // A parent can change after guarded resolution. Do not inherit metadata
33
40
  // from an outside inode, even if the parent is restored before publication.
34
- const realPath = realpathSync.native(params.targetPath);
35
- if (!isPathInside(params.rootWithSep, realPath))
41
+ const observedRealPath = realpathSync.native(targetPath);
42
+ const admittedRealPath = admitPathInsideRoot({
43
+ rootPath: params.rootWithSep,
44
+ candidatePath: observedRealPath,
45
+ rootIdentity: params.rootIdentity,
46
+ });
47
+ if (!admittedRealPath)
36
48
  throw outsideWorkspaceError();
49
+ const realPath = admittedRealPath.path;
37
50
  await inspectFileIdentity(async () => {
38
51
  const current = fsSync.statSync(realPath, { bigint: true });
39
52
  if (!current.isFile())
@@ -0,0 +1,24 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import type { FileHandle } from "node:fs/promises";
3
+ import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
4
+ import type { RootContext } from "./root-context.js";
5
+ import type { RootWriteOptions } from "./root-options.js";
6
+ export declare function finishRootFallbackWrite(params: {
7
+ root: RootContext;
8
+ targetPath: string;
9
+ handle: FileHandle;
10
+ identity: BigIntStats;
11
+ parentGuard: AnyAsyncDirectoryGuard;
12
+ mode: number;
13
+ options: RootWriteOptions & {
14
+ data: string | Buffer;
15
+ };
16
+ openForCompatibility: () => Promise<{
17
+ opened: {
18
+ handle: FileHandle;
19
+ };
20
+ identity: BigIntStats;
21
+ }>;
22
+ onVerificationFailure: (error: unknown) => void;
23
+ }): Promise<void>;
24
+ //# sourceMappingURL=root-write-publication.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-write-publication.d.ts","sourceRoot":"","sources":["../src/root-write-publication.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAKnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAK1D,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,gBAAgB,GAAG;QAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACtD,oBAAoB,EAAE,MAAM,OAAO,CAAC;QAAE,MAAM,EAAE;YAAE,MAAM,EAAE,UAAU,CAAA;SAAE,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IAC/F,qBAAqB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACjD,GAAG,OAAO,CAAC,IAAI,CAAC,CAoEhB"}
@@ -0,0 +1,85 @@
1
+ import fsSync, {} from "node:fs";
2
+ import path from "node:path";
3
+ import { readFileHandleBounded } from "./bounded-read.js";
4
+ import { syncDirectoryBestEffort } from "./directory-durability.js";
5
+ import { FsSafeError } from "./errors.js";
6
+ import { sameFileIdentityForCleanup, sha256Hex } from "./file-identity.js";
7
+ import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
8
+ import { verifyAtomicWriteResult } from "./root-write-verification.js";
9
+ // Compatibility callers hold the lock throughout this operation. The caller
10
+ // owns the original write handle; this helper owns a content-verified reopen.
11
+ export async function finishRootFallbackWrite(params) {
12
+ let handle = params.handle;
13
+ let identity = params.identity;
14
+ let accepted;
15
+ const compatibility = params.options.renameIdentity === "verify-content-with-lock";
16
+ const verify = async () => {
17
+ try {
18
+ await verifyAtomicWriteResult({
19
+ root: params.root, targetPath: params.targetPath, fd: handle.fd,
20
+ expectedIdentity: identity, parentGuard: params.parentGuard,
21
+ });
22
+ }
23
+ catch (error) {
24
+ params.onVerificationFailure(error);
25
+ throw error;
26
+ }
27
+ };
28
+ try {
29
+ if (compatibility) {
30
+ const current = fsSync.lstatSync(params.targetPath, { bigint: true });
31
+ if (current.isSymbolicLink() || !current.isFile()) {
32
+ throw new FsSafeError("path-mismatch", "fallback target changed during write");
33
+ }
34
+ if (process.platform === "win32" && (current.dev === 0n || current.ino === 0n)) {
35
+ // Opaque pathname stats are not evidence of rename identity drift. The
36
+ // strict verifier can reopen against the retained descriptor's exact
37
+ // identity without reading bytes; a different object must still fail.
38
+ await verify();
39
+ }
40
+ else if (!sameFileIdentityForCleanup(current, identity)) {
41
+ const opened = await params.openForCompatibility();
42
+ accepted = handle = opened.opened.handle;
43
+ identity = opened.identity;
44
+ // Admit the destination under the retained Root and parent before any
45
+ // read, chmod, or sync; the original temp no longer proves its identity.
46
+ await verify();
47
+ const data = params.options.data;
48
+ const expectedBytes = typeof data === "string"
49
+ ? Buffer.byteLength(data, params.options.encoding ?? "utf8") : data.byteLength;
50
+ const content = await readFileHandleBounded(handle, expectedBytes).catch((error) => {
51
+ if (error instanceof FsSafeError && error.code === "too-large") {
52
+ throw new FsSafeError("path-mismatch", "fallback target changed during write", { cause: error });
53
+ }
54
+ throw error;
55
+ });
56
+ if (sha256Hex(content) !== sha256Hex(data, params.options.encoding)) {
57
+ throw new FsSafeError("path-mismatch", "fallback target changed during write");
58
+ }
59
+ await verify();
60
+ }
61
+ }
62
+ // Final mode belongs to the accepted object, including on rename-unstable
63
+ // filesystems; never chmod the old temp and assume its mode was transferred.
64
+ try {
65
+ await handle.chmod(params.mode);
66
+ }
67
+ catch (error) {
68
+ await cleanupPinnedFilePath({
69
+ pathname: params.targetPath, handle, identity, parentGuard: params.parentGuard,
70
+ });
71
+ throw error;
72
+ }
73
+ if (params.options.durable !== false)
74
+ await handle.sync();
75
+ await verify();
76
+ if (params.options.durable !== false) {
77
+ await syncDirectoryBestEffort(path.dirname(params.targetPath));
78
+ if (compatibility)
79
+ await verify();
80
+ }
81
+ }
82
+ finally {
83
+ await accepted?.close().catch(() => undefined);
84
+ }
85
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFhB"}
1
+ {"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CAgGhB"}
@@ -4,10 +4,11 @@ import { assertAsyncDirectoryGuard } from "./directory-guard.js";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { sameFileIdentity } from "./file-identity.js";
6
6
  import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
7
- import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError } from "./path.js";
7
+ import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError } from "./path.js";
8
8
  import { resolveReadOpenFlags } from "./read-open-flags.js";
9
9
  import { assertRootIdentityCurrent } from "./root-context.js";
10
10
  import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
11
+ import { admitPathInsideRoot } from "./root-boundary.js";
11
12
  export async function verifyAtomicWriteResult(params) {
12
13
  let needsPathOpen = false;
13
14
  const assertFile = (stat) => {
@@ -47,9 +48,15 @@ export async function verifyAtomicWriteResult(params) {
47
48
  // This descriptor remains owned by the writer, even when final mode forbids opens.
48
49
  const stat = assertDescriptor();
49
50
  assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
50
- const { realPath } = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
51
- assertPath(fsSync.statSync(realPath, { bigint: true }));
52
- if (!isPathInside(params.root.rootWithSep, realPath)) {
51
+ const { realPath, stat: resolvedStat } = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
52
+ // Consume the resolver's observation only in this pass; the checks after
53
+ // the directory guards must still sample the current path and descriptor.
54
+ assertPath(resolvedStat);
55
+ if (!admitPathInsideRoot({
56
+ rootPath: params.root.rootReal,
57
+ candidatePath: realPath,
58
+ rootIdentity: params.root.rootIdentity,
59
+ })) {
53
60
  throw outsideWorkspaceError();
54
61
  }
55
62
  await assertAsyncDirectoryGuard(params.parentGuard);
@@ -74,9 +81,13 @@ export async function verifyAtomicWriteResult(params) {
74
81
  try {
75
82
  const reopenedStat = assertDescriptor(opened.fd);
76
83
  assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
77
- const { realPath: reopenedPath } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
78
- assertPath(fsSync.statSync(reopenedPath, { bigint: true }));
79
- if (!isPathInside(params.root.rootWithSep, reopenedPath)) {
84
+ const { realPath: reopenedPath, stat: reopenedPathStat } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
85
+ assertPath(reopenedPathStat);
86
+ if (!admitPathInsideRoot({
87
+ rootPath: params.root.rootReal,
88
+ candidatePath: reopenedPath,
89
+ rootIdentity: params.root.rootIdentity,
90
+ })) {
80
91
  throw outsideWorkspaceError();
81
92
  }
82
93
  await assertAsyncDirectoryGuard(params.parentGuard);
@@ -1 +1 @@
1
- {"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
1
+ {"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
@@ -4,12 +4,14 @@ const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
4
4
  // Windows treats "C:name" as relative to the drive's current directory even
5
5
  // though path.win32.isAbsolute() reports false.
6
6
  const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?![\\/])/;
7
+ // A forward slash ends the segment, so "C:/file" still contains the "C:" segment.
8
+ const DRIVE_RELATIVE_SEGMENT = /(?:^|\/)[A-Za-z]:(?!\\)/;
7
9
  const HYPHEN_CHAR_CODE = 0x2d;
8
10
  export function isDriveRelativePath(value) {
9
11
  return DRIVE_RELATIVE_PREFIX.test(value);
10
12
  }
11
13
  export function assertNoDriveRelativePathSegments(value, label) {
12
- if (value.split("/").some(isDriveRelativePath)) {
14
+ if (DRIVE_RELATIVE_SEGMENT.test(value)) {
13
15
  throw new FsSafeError("invalid-path", `${label} must not contain a drive letter`);
14
16
  }
15
17
  return value;
@@ -1 +1 @@
1
- {"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,OAAO,EAAkF,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAUhJ,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAkER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AA8JD,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAYF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,CAAC,GAC7C,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC5C,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAuBD;AAwDD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAKxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAezF"}
1
+ {"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAM/C,OAAO,EAAkF,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAUhJ,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAQjC,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAmER;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAUpB;AA8JD,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AA+BF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,IAAI,CAAC,qBAAqB,EAAE,SAAS,CAAC,GAC7C,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC5C,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC,CAgCD;AAwDD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUxF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBzF"}
@@ -17,32 +17,36 @@ import { verifyAtomicWriteResult } from "./root-write-verification.js";
17
17
  import { assertSecretFilePreview, resolveSecretReadPolicy, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
18
18
  import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
19
19
  import { serializePathWrite } from "./write-queue.js";
20
+ import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
20
21
  export const PRIVATE_SECRET_DIR_MODE = 0o700;
21
22
  export const PRIVATE_SECRET_FILE_MODE = 0o600;
22
23
  export function readSecretFileSync(filePath, label, options = {}) {
23
24
  const { resolvedPath, maxBytes } = resolveSecretReadPolicy(filePath, label, options);
24
- function inspectInput(symlinkMessage) {
25
- const stat = options.rejectSymlink
26
- ? fs.lstatSync(resolvedPath, { bigint: true })
27
- : fs.statSync(resolvedPath, { bigint: true });
28
- if (options.rejectSymlink && stat.isSymbolicLink()) {
29
- throw new FsSafeError("symlink", symlinkMessage);
30
- }
31
- return stat;
32
- }
25
+ let rejectSymlink;
33
26
  let previewStat;
34
27
  try {
35
28
  assertNoUnsafeDeviceReadPath(resolvedPath);
29
+ rejectSymlink = Boolean(options.rejectSymlink);
36
30
  previewStat = inspectFileIdentitySync(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
37
31
  }
38
32
  catch (error) {
39
33
  throw secretReadError(error instanceof FsSafeError ? error.code : secretPathErrorCode(error), "inspect", label, resolvedPath, error);
40
34
  }
41
- assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, options.rejectHardlinks !== false);
35
+ function inspectInput(symlinkMessage) {
36
+ const stat = rejectSymlink
37
+ ? fs.lstatSync(resolvedPath, { bigint: true })
38
+ : fs.statSync(resolvedPath, { bigint: true });
39
+ if (rejectSymlink && stat.isSymbolicLink()) {
40
+ throw new FsSafeError("symlink", symlinkMessage);
41
+ }
42
+ return stat;
43
+ }
44
+ const rejectHardlinks = options.rejectHardlinks !== false;
45
+ assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, rejectHardlinks);
42
46
  const opened = openPinnedFileSync({
43
47
  filePath: resolvedPath,
44
- rejectPathSymlink: options.rejectSymlink,
45
- rejectHardlinks: options.rejectHardlinks !== false,
48
+ rejectPathSymlink: rejectSymlink,
49
+ rejectHardlinks,
46
50
  });
47
51
  if (!opened.ok) {
48
52
  throw secretReadError(opened.reason === "path" ? "not-found" : "path-mismatch", "read", label, resolvedPath, opened.reason === "validation" ? new Error("security validation failed") : opened.error);
@@ -51,14 +55,14 @@ export function readSecretFileSync(filePath, label, options = {}) {
51
55
  try {
52
56
  const openedIdentity = inspectFileIdentitySync(() => {
53
57
  const stat = fs.fstatSync(opened.fd, { bigint: true });
54
- if (!stat.isFile() || (options.rejectHardlinks !== false && stat.nlink > 1n)) {
58
+ if (!stat.isFile() || (rejectHardlinks && stat.nlink > 1n)) {
55
59
  throw new FsSafeError("path-mismatch", "security validation failed");
56
60
  }
57
61
  return stat;
58
62
  }, previewStat);
59
63
  inspectFileIdentitySync(() => {
60
64
  const stat = fs.lstatSync(opened.path, { bigint: true });
61
- if (!stat.isFile() || (options.rejectHardlinks !== false && stat.nlink > 1n)) {
65
+ if (!stat.isFile() || (rejectHardlinks && stat.nlink > 1n)) {
62
66
  throw new FsSafeError("path-mismatch", "security validation failed");
63
67
  }
64
68
  return stat;
@@ -152,8 +156,8 @@ async function inspectPrivateDirectory(directory, kind) {
152
156
  });
153
157
  }
154
158
  async function ensurePrivateDirectory(rootDir, targetDir, mode) {
155
- const resolvedRoot = path.resolve(rootDir);
156
- const resolvedTarget = path.resolve(targetDir);
159
+ const resolvedRoot = resolvePathPreservingWindowsRoot(rootDir);
160
+ const resolvedTarget = resolvePathPreservingWindowsRoot(targetDir);
157
161
  let rootStat = await inspectPrivateDirectory(resolvedRoot, "root").catch((error) => {
158
162
  if (error.code !== "ENOENT")
159
163
  throw error;
@@ -228,21 +232,42 @@ async function ensurePrivateDirectory(rootDir, targetDir, mode) {
228
232
  }
229
233
  return { rootGuard, parentGuard: targetGuard };
230
234
  }
235
+ function snapshotSecretFileWriteParams(params, rootDir, filePath) {
236
+ return {
237
+ rootDir,
238
+ filePath,
239
+ content: params.content,
240
+ mode: params.mode,
241
+ dirMode: params.dirMode,
242
+ durable: params.durable,
243
+ };
244
+ }
231
245
  async function secretFileWriteQueueKey(filePath) {
246
+ assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
232
247
  try {
233
248
  return await canonicalPathFromExistingAncestor(filePath);
234
249
  }
235
- catch {
250
+ catch (error) {
251
+ if (error instanceof FsSafeError && error.code === "invalid-path")
252
+ throw error;
236
253
  // Keep validation and its public error shape owned by the write path below.
237
- return path.resolve(filePath);
254
+ const resolved = path.resolve(filePath);
255
+ assertNoWindowsPathAlias(resolved, "filesystem", "private secret path uses a Windows filesystem namespace alias");
256
+ return resolved;
238
257
  }
239
258
  }
240
259
  // Internal preparation for private writers and their pre-write locks; not lasting authorization.
241
260
  export async function prepareSecretFileWrite(params) {
261
+ const rootDir = params.rootDir;
262
+ const filePath = params.filePath;
263
+ assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
264
+ assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
242
265
  const mode = params.mode ?? PRIVATE_SECRET_FILE_MODE;
243
266
  const dirMode = params.dirMode ?? PRIVATE_SECRET_DIR_MODE;
244
- const resolvedRoot = path.resolve(params.rootDir);
245
- const resolvedFile = path.resolve(params.filePath);
267
+ const resolvedRoot = resolvePathPreservingWindowsRoot(rootDir);
268
+ const resolvedFile = path.resolve(filePath);
269
+ assertNoWindowsPathAlias(resolvedRoot, "filesystem", "private secret root uses a Windows filesystem namespace alias");
270
+ assertNoWindowsPathAlias(resolvedFile, "filesystem", "private secret path uses a Windows filesystem namespace alias");
246
271
  assertPathWithinRoot(resolvedRoot, resolvedFile);
247
272
  for (const [kind, value] of [["file", mode], ["directory", dirMode]]) {
248
273
  if (!Number.isInteger(value) || value < 0 || value > 0o7777) {
@@ -254,8 +279,11 @@ export async function prepareSecretFileWrite(params) {
254
279
  await assertAsyncDirectoryGuard(rootGuard);
255
280
  await assertAsyncDirectoryGuard(parentGuard);
256
281
  assertRealPathWithinRoot(rootGuard.realPath, parentGuard.realPath);
282
+ assertNoWindowsPathAlias(rootGuard.realPath, "filesystem", "private secret root uses a Windows filesystem namespace alias");
283
+ assertNoWindowsPathAlias(parentGuard.realPath, "filesystem", "private secret parent uses a Windows filesystem namespace alias");
257
284
  const fileName = path.basename(resolvedFile);
258
285
  const finalFilePath = path.join(parentGuard.realPath, fileName);
286
+ assertNoWindowsPathAlias(finalFilePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
259
287
  return { mode, rootGuard, parentGuard, fileName, finalFilePath };
260
288
  }
261
289
  async function materializeSecretFileAtomic(params, createOnly) {
@@ -309,16 +337,26 @@ async function materializeSecretFileAtomic(params, createOnly) {
309
337
  });
310
338
  }
311
339
  export async function writeSecretFileAtomic(params) {
312
- const canonicalPath = await secretFileWriteQueueKey(params.filePath);
340
+ const rootDir = params.rootDir;
341
+ const filePath = params.filePath;
342
+ assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
343
+ assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
344
+ const ownedParams = snapshotSecretFileWriteParams(params, rootDir, filePath);
345
+ const canonicalPath = await secretFileWriteQueueKey(filePath);
313
346
  await serializePathWrite(canonicalPath, async () => {
314
- await materializeSecretFileAtomic(params, false);
347
+ await materializeSecretFileAtomic(ownedParams, false);
315
348
  });
316
349
  }
317
350
  export async function createSecretFileAtomic(params) {
318
351
  try {
319
- const canonicalPath = await secretFileWriteQueueKey(params.filePath);
352
+ const rootDir = params.rootDir;
353
+ const filePath = params.filePath;
354
+ assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
355
+ assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
356
+ const ownedParams = snapshotSecretFileWriteParams(params, rootDir, filePath);
357
+ const canonicalPath = await secretFileWriteQueueKey(filePath);
320
358
  await serializePathWrite(canonicalPath, async () => {
321
- await materializeSecretFileAtomic(params, true);
359
+ await materializeSecretFileAtomic(ownedParams, true);
322
360
  });
323
361
  }
324
362
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAQA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CA0DjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
1
+ {"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAQA,OAAO,EAML,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAGjC,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CA6DjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
@@ -5,38 +5,43 @@ import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { resolveReadOpenFlags } from "./read-open-flags.js";
7
7
  import { realpathSync } from "./realpath.js";
8
- import { inspectFileIdentity } from "./strict-file-identity.js";
8
+ import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
9
9
  import { assertSecretFilePreview, resolveSecretReadPolicy, secretPathErrorCode, secretReadError, trimSecretFileContent, } from "./secret-read-policy.js";
10
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
10
11
  export async function readSecretFile(filePath, label, options = {}) {
11
12
  const { resolvedPath, maxBytes } = resolveSecretReadPolicy(filePath, label, options);
12
- async function inspectInput(symlinkMessage) {
13
- const stat = options.rejectSymlink
14
- ? fsSync.lstatSync(resolvedPath, { bigint: true })
15
- : fsSync.statSync(resolvedPath, { bigint: true });
16
- if (options.rejectSymlink && stat.isSymbolicLink()) {
17
- throw new FsSafeError("symlink", symlinkMessage);
18
- }
19
- return stat;
20
- }
13
+ let rejectSymlink;
21
14
  let previewStat;
22
15
  try {
23
16
  assertNoUnsafeDeviceReadPath(resolvedPath);
24
- previewStat = await inspectFileIdentity(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
17
+ rejectSymlink = Boolean(options.rejectSymlink);
18
+ previewStat = inspectFileIdentitySync(() => inspectInput(`${label} file at ${resolvedPath} must not be a symlink.`));
25
19
  }
26
20
  catch (error) {
27
21
  throw secretReadError(error instanceof FsSafeError ? error.code : secretPathErrorCode(error), "inspect", label, resolvedPath, error);
28
22
  }
29
- assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, options.rejectHardlinks !== false);
23
+ function inspectInput(symlinkMessage) {
24
+ const stat = rejectSymlink
25
+ ? fsSync.lstatSync(resolvedPath, { bigint: true })
26
+ : fsSync.statSync(resolvedPath, { bigint: true });
27
+ if (rejectSymlink && stat.isSymbolicLink()) {
28
+ throw new FsSafeError("symlink", symlinkMessage);
29
+ }
30
+ return stat;
31
+ }
32
+ const rejectHardlinks = options.rejectHardlinks !== false;
33
+ assertSecretFilePreview(previewStat, label, resolvedPath, maxBytes, rejectHardlinks);
30
34
  let handle;
31
35
  let raw;
32
36
  try {
33
37
  const realPath = realpathSync.native(resolvedPath);
38
+ assertNoWindowsPathAlias(realPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
34
39
  assertNoUnsafeDeviceReadPath(realPath);
35
40
  handle = await fs.open(realPath, resolveReadOpenFlags());
36
41
  const openedHandle = handle;
37
42
  const openedStat = await inspectFileIdentity(async () => {
38
43
  const stat = fsSync.fstatSync(openedHandle.fd, { bigint: true });
39
- if (!stat.isFile() || (options.rejectHardlinks !== false && stat.nlink > 1n)) {
44
+ if (!stat.isFile() || (rejectHardlinks && stat.nlink > 1n)) {
40
45
  throw new FsSafeError("path-mismatch", "security validation failed");
41
46
  }
42
47
  return stat;
@@ -1 +1 @@
1
- {"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAEhE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAGnE;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,GAAG,MAAM,EAC1B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,OAAO,GACb,WAAW,CAGb;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,OAAO,GACvB,IAAI,CAUN;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAM9F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG;IACxG,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,CASA"}
1
+ {"version":3,"file":"secret-read-policy.d.ts","sourceRoot":"","sources":["../src/secret-read-policy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAGhE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAGnE;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,eAAe,EACrB,MAAM,EAAE,SAAS,GAAG,MAAM,EAC1B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,OAAO,GACb,WAAW,CAGb;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,OAAO,GACvB,IAAI,CAUN;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAM9F;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB,GAAG;IACxG,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB,CAYA"}
@@ -1,6 +1,7 @@
1
1
  import { normalizeMaxBytes } from "./byte-budget.js";
2
2
  import { resolveHomeRelativePath } from "./home-dir.js";
3
3
  import { FsSafeError } from "./errors.js";
4
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
4
5
  export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
5
6
  export function secretPathErrorCode(error) {
6
7
  const code = error.code;
@@ -29,10 +30,13 @@ export function trimSecretFileContent(raw, label, resolvedPath) {
29
30
  return secret;
30
31
  }
31
32
  export function resolveSecretReadPolicy(filePath, label, options) {
32
- const resolvedPath = resolveHomeRelativePath(filePath.trim());
33
+ const trimmedPath = filePath.trim();
34
+ assertNoWindowsPathAlias(trimmedPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
35
+ const resolvedPath = resolveHomeRelativePath(trimmedPath);
33
36
  if (!resolvedPath) {
34
37
  throw new FsSafeError("invalid-path", `${label} file path is empty.`, { cause: undefined });
35
38
  }
39
+ assertNoWindowsPathAlias(resolvedPath, "filesystem", `${label} file path uses a Windows filesystem namespace alias`);
36
40
  const maxBytes = normalizeMaxBytes(options.maxBytes, {
37
41
  defaultValue: DEFAULT_SECRET_FILE_MAX_BYTES,
38
42
  });
@@ -0,0 +1,10 @@
1
+ import type { BigIntStats, Stats } from "node:fs";
2
+ import type { PermissionCheck } from "./permissions.js";
3
+ type ExactIdentity = Pick<BigIntStats, "dev" | "ino">;
4
+ export declare function inspectSecureWindowsDescriptor(params: {
5
+ fd: number;
6
+ identity: ExactIdentity;
7
+ stat: Stats;
8
+ }): PermissionCheck;
9
+ export {};
10
+ //# sourceMappingURL=secure-file-windows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secure-file-windows.d.ts","sourceRoot":"","sources":["../src/secure-file-windows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AASlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA4BxD,KAAK,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AAuHtD,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,KAAK,CAAC;CACb,GAAG,eAAe,CA0ClB"}