@openclaw/fs-safe 0.12.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 (383) hide show
  1. package/CHANGELOG.md +61 -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/darwin-acl.d.ts +4 -0
  41. package/dist/darwin-acl.d.ts.map +1 -0
  42. package/dist/darwin-acl.js +24 -0
  43. package/dist/deny-mutations.d.ts.map +1 -1
  44. package/dist/deny-mutations.js +8 -2
  45. package/dist/directory-durability.d.ts.map +1 -1
  46. package/dist/directory-durability.js +67 -23
  47. package/dist/directory-entry-path.d.ts +3 -0
  48. package/dist/directory-entry-path.d.ts.map +1 -0
  49. package/dist/directory-entry-path.js +21 -0
  50. package/dist/directory-guard.d.ts +17 -1
  51. package/dist/directory-guard.d.ts.map +1 -1
  52. package/dist/directory-guard.js +134 -48
  53. package/dist/directory-mode-node.d.ts +12 -0
  54. package/dist/directory-mode-node.d.ts.map +1 -1
  55. package/dist/directory-mode-node.js +102 -4
  56. package/dist/effective-uid.d.ts +2 -0
  57. package/dist/effective-uid.d.ts.map +1 -0
  58. package/dist/effective-uid.js +25 -0
  59. package/dist/file-handle-transfer.d.ts.map +1 -1
  60. package/dist/file-handle-transfer.js +98 -27
  61. package/dist/file-hash.d.ts.map +1 -1
  62. package/dist/file-hash.js +3 -0
  63. package/dist/file-lock-sync.d.ts.map +1 -1
  64. package/dist/file-lock-sync.js +36 -6
  65. package/dist/file-lock.d.ts.map +1 -1
  66. package/dist/file-lock.js +29 -9
  67. package/dist/file-observation.d.ts +1 -1
  68. package/dist/file-observation.d.ts.map +1 -1
  69. package/dist/file-store-boundary.d.ts +6 -2
  70. package/dist/file-store-boundary.d.ts.map +1 -1
  71. package/dist/file-store-boundary.js +20 -65
  72. package/dist/file-store-copy-source.d.ts +5 -0
  73. package/dist/file-store-copy-source.d.ts.map +1 -0
  74. package/dist/file-store-copy-source.js +31 -0
  75. package/dist/file-store-path.d.ts.map +1 -1
  76. package/dist/file-store-path.js +4 -1
  77. package/dist/file-store-sync-directory.d.ts +16 -0
  78. package/dist/file-store-sync-directory.d.ts.map +1 -0
  79. package/dist/file-store-sync-directory.js +349 -0
  80. package/dist/file-store.d.ts.map +1 -1
  81. package/dist/file-store.js +11 -28
  82. package/dist/filename.d.ts.map +1 -1
  83. package/dist/filename.js +65 -20
  84. package/dist/fs.d.ts.map +1 -1
  85. package/dist/fs.js +3 -2
  86. package/dist/guarded-mkdir.d.ts +8 -0
  87. package/dist/guarded-mkdir.d.ts.map +1 -1
  88. package/dist/guarded-mkdir.js +176 -27
  89. package/dist/guest.d.ts.map +1 -1
  90. package/dist/guest.js +4 -1
  91. package/dist/home-dir.d.ts.map +1 -1
  92. package/dist/home-dir.js +73 -10
  93. package/dist/install-path.d.ts.map +1 -1
  94. package/dist/install-path.js +54 -19
  95. package/dist/json-durable-queue-ownership.d.ts +6 -0
  96. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  97. package/dist/json-durable-queue-ownership.js +89 -42
  98. package/dist/json-durable-queue-paths.d.ts +11 -0
  99. package/dist/json-durable-queue-paths.d.ts.map +1 -0
  100. package/dist/json-durable-queue-paths.js +42 -0
  101. package/dist/json-durable-queue-read.d.ts.map +1 -1
  102. package/dist/json-durable-queue-read.js +2 -0
  103. package/dist/json-durable-queue.d.ts.map +1 -1
  104. package/dist/json-durable-queue.js +50 -58
  105. package/dist/json-store.d.ts.map +1 -1
  106. package/dist/json-store.js +5 -1
  107. package/dist/json.d.ts.map +1 -1
  108. package/dist/json.js +54 -22
  109. package/dist/local-file-access.d.ts.map +1 -1
  110. package/dist/local-file-access.js +4 -0
  111. package/dist/local-file-descriptor.d.ts +17 -0
  112. package/dist/local-file-descriptor.d.ts.map +1 -0
  113. package/dist/local-file-descriptor.js +84 -0
  114. package/dist/local-roots.d.ts.map +1 -1
  115. package/dist/local-roots.js +35 -7
  116. package/dist/move-path-cleanup.d.ts +2 -0
  117. package/dist/move-path-cleanup.d.ts.map +1 -1
  118. package/dist/move-path-cleanup.js +44 -18
  119. package/dist/move-path.d.ts.map +1 -1
  120. package/dist/move-path.js +31 -6
  121. package/dist/native-binding.d.ts +14 -0
  122. package/dist/native-binding.d.ts.map +1 -1
  123. package/dist/native-directory-observation.d.ts +17 -0
  124. package/dist/native-directory-observation.d.ts.map +1 -0
  125. package/dist/native-directory-observation.js +37 -0
  126. package/dist/native-parent-admission.d.ts +31 -0
  127. package/dist/native-parent-admission.d.ts.map +1 -0
  128. package/dist/native-parent-admission.js +124 -0
  129. package/dist/native-pinned-write-windows.d.ts +0 -1
  130. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  131. package/dist/native-pinned-write-windows.js +0 -3
  132. package/dist/native-pinned-write.d.ts.map +1 -1
  133. package/dist/native-pinned-write.js +309 -40
  134. package/dist/output.d.ts.map +1 -1
  135. package/dist/output.js +15 -5
  136. package/dist/overwrite-file-handle.d.ts.map +1 -1
  137. package/dist/overwrite-file-handle.js +5 -1
  138. package/dist/owner-dacl.d.ts.map +1 -1
  139. package/dist/owner-dacl.js +2 -0
  140. package/dist/path-policy.d.ts.map +1 -1
  141. package/dist/path-policy.js +7 -2
  142. package/dist/path-prefix.d.ts +7 -0
  143. package/dist/path-prefix.d.ts.map +1 -0
  144. package/dist/path-prefix.js +82 -0
  145. package/dist/path-scope-lexical.d.ts.map +1 -1
  146. package/dist/path-scope-lexical.js +18 -8
  147. package/dist/path-segment-route.d.ts +7 -0
  148. package/dist/path-segment-route.d.ts.map +1 -0
  149. package/dist/path-segment-route.js +24 -0
  150. package/dist/path-suffix-aliases.d.ts +10 -0
  151. package/dist/path-suffix-aliases.d.ts.map +1 -0
  152. package/dist/path-suffix-aliases.js +386 -0
  153. package/dist/path.d.ts.map +1 -1
  154. package/dist/path.js +16 -5
  155. package/dist/permissions-windows.d.ts.map +1 -1
  156. package/dist/permissions-windows.js +14 -3
  157. package/dist/permissions.d.ts.map +1 -1
  158. package/dist/permissions.js +37 -8
  159. package/dist/pinned-mutation-admission.d.ts +25 -0
  160. package/dist/pinned-mutation-admission.d.ts.map +1 -0
  161. package/dist/pinned-mutation-admission.js +425 -0
  162. package/dist/pinned-mutation-observation.d.ts +34 -0
  163. package/dist/pinned-mutation-observation.d.ts.map +1 -0
  164. package/dist/pinned-mutation-observation.js +142 -0
  165. package/dist/pinned-mutation-shared-route.d.ts +24 -0
  166. package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
  167. package/dist/pinned-mutation-shared-route.js +70 -0
  168. package/dist/pinned-open.d.ts +6 -0
  169. package/dist/pinned-open.d.ts.map +1 -1
  170. package/dist/pinned-open.js +28 -10
  171. package/dist/pinned-write-types.d.ts +75 -0
  172. package/dist/pinned-write-types.d.ts.map +1 -0
  173. package/dist/pinned-write-types.js +1 -0
  174. package/dist/pinned-write.d.ts +7 -33
  175. package/dist/pinned-write.d.ts.map +1 -1
  176. package/dist/pinned-write.js +151 -17
  177. package/dist/private-directory.d.ts.map +1 -1
  178. package/dist/private-directory.js +2 -0
  179. package/dist/private-producer-handoff.d.ts +16 -0
  180. package/dist/private-producer-handoff.d.ts.map +1 -0
  181. package/dist/private-producer-handoff.js +272 -0
  182. package/dist/private-temp-workspace.d.ts +2 -39
  183. package/dist/private-temp-workspace.d.ts.map +1 -1
  184. package/dist/private-temp-workspace.js +183 -77
  185. package/dist/publish-file.d.ts.map +1 -1
  186. package/dist/publish-file.js +33 -29
  187. package/dist/regular-file.d.ts.map +1 -1
  188. package/dist/regular-file.js +56 -45
  189. package/dist/replace-directory.d.ts.map +1 -1
  190. package/dist/replace-directory.js +12 -5
  191. package/dist/replace-file-temp-owner.d.ts +1 -0
  192. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  193. package/dist/replace-file-temp-owner.js +12 -1
  194. package/dist/replace-file.d.ts.map +1 -1
  195. package/dist/replace-file.js +24 -24
  196. package/dist/root-boundary.d.ts +4 -0
  197. package/dist/root-boundary.d.ts.map +1 -1
  198. package/dist/root-boundary.js +6 -1
  199. package/dist/root-context.d.ts +12 -1
  200. package/dist/root-context.d.ts.map +1 -1
  201. package/dist/root-context.js +57 -11
  202. package/dist/root-directory-creation.d.ts +13 -0
  203. package/dist/root-directory-creation.d.ts.map +1 -0
  204. package/dist/root-directory-creation.js +212 -0
  205. package/dist/root-directory-list.d.ts +16 -4
  206. package/dist/root-directory-list.d.ts.map +1 -1
  207. package/dist/root-directory-list.js +180 -39
  208. package/dist/root-directory.d.ts +23 -0
  209. package/dist/root-directory.d.ts.map +1 -0
  210. package/dist/root-directory.js +141 -0
  211. package/dist/root-errors.d.ts.map +1 -1
  212. package/dist/root-errors.js +12 -1
  213. package/dist/root-file-final-admission.d.ts +21 -0
  214. package/dist/root-file-final-admission.d.ts.map +1 -0
  215. package/dist/root-file-final-admission.js +83 -0
  216. package/dist/root-file.d.ts.map +1 -1
  217. package/dist/root-file.js +103 -24
  218. package/dist/root-impl.d.ts +1 -0
  219. package/dist/root-impl.d.ts.map +1 -1
  220. package/dist/root-impl.js +289 -317
  221. package/dist/root-move-noreplace.d.ts +14 -0
  222. package/dist/root-move-noreplace.d.ts.map +1 -0
  223. package/dist/root-move-noreplace.js +202 -0
  224. package/dist/root-observed-path.d.ts +9 -0
  225. package/dist/root-observed-path.d.ts.map +1 -0
  226. package/dist/root-observed-path.js +95 -0
  227. package/dist/root-path-errors.d.ts +12 -0
  228. package/dist/root-path-errors.d.ts.map +1 -0
  229. package/dist/root-path-errors.js +13 -0
  230. package/dist/root-path-existing.d.ts.map +1 -1
  231. package/dist/root-path-existing.js +53 -20
  232. package/dist/root-path-observation.d.ts +63 -0
  233. package/dist/root-path-observation.d.ts.map +1 -0
  234. package/dist/root-path-observation.js +180 -0
  235. package/dist/root-path-stat.d.ts +5 -0
  236. package/dist/root-path-stat.d.ts.map +1 -0
  237. package/dist/root-path-stat.js +101 -0
  238. package/dist/root-path-symlink.d.ts.map +1 -1
  239. package/dist/root-path-symlink.js +23 -4
  240. package/dist/root-path.d.ts +11 -0
  241. package/dist/root-path.d.ts.map +1 -1
  242. package/dist/root-path.js +215 -53
  243. package/dist/root-paths-lexical.d.ts +9 -0
  244. package/dist/root-paths-lexical.d.ts.map +1 -0
  245. package/dist/root-paths-lexical.js +22 -0
  246. package/dist/root-paths.d.ts +3 -25
  247. package/dist/root-paths.d.ts.map +1 -1
  248. package/dist/root-paths.js +77 -154
  249. package/dist/root-read-admission.d.ts +26 -0
  250. package/dist/root-read-admission.d.ts.map +1 -0
  251. package/dist/root-read-admission.js +94 -0
  252. package/dist/root-remove-identity.d.ts +15 -0
  253. package/dist/root-remove-identity.d.ts.map +1 -0
  254. package/dist/root-remove-identity.js +89 -0
  255. package/dist/root-remove-receipt.d.ts +17 -0
  256. package/dist/root-remove-receipt.d.ts.map +1 -0
  257. package/dist/root-remove-receipt.js +37 -0
  258. package/dist/root-remove.d.ts +2 -1
  259. package/dist/root-remove.d.ts.map +1 -1
  260. package/dist/root-remove.js +172 -10
  261. package/dist/root-write-admission.d.ts +68 -0
  262. package/dist/root-write-admission.d.ts.map +1 -0
  263. package/dist/root-write-admission.js +322 -0
  264. package/dist/root-write-compatibility.d.ts +13 -0
  265. package/dist/root-write-compatibility.d.ts.map +1 -0
  266. package/dist/root-write-compatibility.js +74 -0
  267. package/dist/root-write-complete-parent.d.ts +42 -0
  268. package/dist/root-write-complete-parent.d.ts.map +1 -0
  269. package/dist/root-write-complete-parent.js +195 -0
  270. package/dist/root-write-publication.d.ts +24 -0
  271. package/dist/root-write-publication.d.ts.map +1 -0
  272. package/dist/root-write-publication.js +85 -0
  273. package/dist/root-write-verification.d.ts.map +1 -1
  274. package/dist/root-write-verification.js +6 -4
  275. package/dist/secret-file.d.ts.map +1 -1
  276. package/dist/secret-file.js +62 -24
  277. package/dist/secret-read-async.d.ts.map +1 -1
  278. package/dist/secret-read-async.js +18 -13
  279. package/dist/secret-read-policy.d.ts.map +1 -1
  280. package/dist/secret-read-policy.js +5 -1
  281. package/dist/secure-file.d.ts.map +1 -1
  282. package/dist/secure-file.js +92 -7
  283. package/dist/secure-temp-dir.d.ts +6 -3
  284. package/dist/secure-temp-dir.d.ts.map +1 -1
  285. package/dist/secure-temp-dir.js +121 -101
  286. package/dist/secure-temp-repair.d.ts +35 -0
  287. package/dist/secure-temp-repair.d.ts.map +1 -0
  288. package/dist/secure-temp-repair.js +106 -0
  289. package/dist/sibling-staged-file.d.ts +3 -2
  290. package/dist/sibling-staged-file.d.ts.map +1 -1
  291. package/dist/sibling-staged-file.js +179 -55
  292. package/dist/sibling-temp.d.ts.map +1 -1
  293. package/dist/sibling-temp.js +27 -13
  294. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  295. package/dist/sidecar-lock-acquire.js +44 -14
  296. package/dist/sidecar-lock-root.d.ts.map +1 -1
  297. package/dist/sidecar-lock-root.js +4 -2
  298. package/dist/staged-directory.d.ts +14 -5
  299. package/dist/staged-directory.d.ts.map +1 -1
  300. package/dist/staged-directory.js +54 -3
  301. package/dist/standalone-publication-path.d.ts +2 -0
  302. package/dist/standalone-publication-path.d.ts.map +1 -0
  303. package/dist/standalone-publication-path.js +6 -0
  304. package/dist/stat-observation.d.ts +11 -0
  305. package/dist/stat-observation.d.ts.map +1 -0
  306. package/dist/stat-observation.js +64 -0
  307. package/dist/strict-file-identity.d.ts.map +1 -1
  308. package/dist/strict-file-identity.js +39 -2
  309. package/dist/symlink-parents.d.ts.map +1 -1
  310. package/dist/symlink-parents.js +7 -2
  311. package/dist/temp-target.d.ts +2 -0
  312. package/dist/temp-target.d.ts.map +1 -1
  313. package/dist/temp-target.js +130 -3
  314. package/dist/temp-workspace-admission.d.ts +22 -0
  315. package/dist/temp-workspace-admission.d.ts.map +1 -0
  316. package/dist/temp-workspace-admission.js +374 -0
  317. package/dist/temp-workspace-child-admission.d.ts +13 -0
  318. package/dist/temp-workspace-child-admission.d.ts.map +1 -0
  319. package/dist/temp-workspace-child-admission.js +95 -0
  320. package/dist/temp-workspace-descriptor.d.ts +48 -0
  321. package/dist/temp-workspace-descriptor.d.ts.map +1 -0
  322. package/dist/temp-workspace-descriptor.js +363 -0
  323. package/dist/temp-workspace-identity.d.ts +15 -0
  324. package/dist/temp-workspace-identity.d.ts.map +1 -0
  325. package/dist/temp-workspace-identity.js +41 -0
  326. package/dist/temp-workspace-owner.d.ts +7 -6
  327. package/dist/temp-workspace-owner.d.ts.map +1 -1
  328. package/dist/temp-workspace-owner.js +121 -61
  329. package/dist/temp-workspace-permissions.d.ts +4 -0
  330. package/dist/temp-workspace-permissions.d.ts.map +1 -0
  331. package/dist/temp-workspace-permissions.js +32 -0
  332. package/dist/temp-workspace-types.d.ts +40 -0
  333. package/dist/temp-workspace-types.d.ts.map +1 -0
  334. package/dist/temp-workspace-types.js +1 -0
  335. package/dist/temp.d.ts +1 -1
  336. package/dist/temp.d.ts.map +1 -1
  337. package/dist/temp.js +1 -1
  338. package/dist/test-hooks.d.ts +7 -0
  339. package/dist/test-hooks.d.ts.map +1 -1
  340. package/dist/text-atomic.d.ts.map +1 -1
  341. package/dist/text-atomic.js +3 -1
  342. package/dist/trash.d.ts.map +1 -1
  343. package/dist/trash.js +54 -8
  344. package/dist/walk.d.ts.map +1 -1
  345. package/dist/walk.js +9 -6
  346. package/dist/windows-owner.d.ts.map +1 -1
  347. package/dist/windows-owner.js +5 -0
  348. package/dist/windows-path-alias.d.ts +39 -0
  349. package/dist/windows-path-alias.d.ts.map +1 -0
  350. package/dist/windows-path-alias.js +153 -0
  351. package/docs/advanced.md +25 -0
  352. package/docs/archive.md +23 -2
  353. package/docs/atomic.md +27 -3
  354. package/docs/copy.md +3 -0
  355. package/docs/durability.md +14 -0
  356. package/docs/errors.md +14 -6
  357. package/docs/file-store.md +24 -3
  358. package/docs/filename.md +14 -7
  359. package/docs/install-path.md +2 -2
  360. package/docs/install.md +8 -7
  361. package/docs/json-store.md +5 -1
  362. package/docs/json.md +11 -0
  363. package/docs/mutation-policy-proof.md +65 -0
  364. package/docs/native-helper.md +26 -9
  365. package/docs/native.md +32 -8
  366. package/docs/output.md +33 -11
  367. package/docs/path-prefix.md +64 -0
  368. package/docs/path-suffix-aliases.md +159 -0
  369. package/docs/path.md +11 -0
  370. package/docs/private-file-store.md +14 -0
  371. package/docs/public-api.md +9 -0
  372. package/docs/quickstart.md +1 -1
  373. package/docs/reading.md +8 -1
  374. package/docs/root.md +18 -3
  375. package/docs/secret-file.md +3 -0
  376. package/docs/secure-file.md +6 -2
  377. package/docs/security-model.md +75 -8
  378. package/docs/sidecar-lock.md +28 -1
  379. package/docs/store.md +5 -1
  380. package/docs/temp.md +260 -36
  381. package/docs/test-hooks.md +14 -0
  382. package/docs/writing.md +38 -8
  383. package/package.json +10 -10
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { isNotFoundPathError } from "./path.js";
5
+ import { admitPathInsideRoot } from "./root-boundary.js";
6
+ import { withPinnedWriteRenameIdentityLock } from "./pinned-write.js";
7
+ import { realpathSync } from "./realpath.js";
8
+ import { serializePathWrite } from "./write-queue.js";
9
+ function unsupportedSpelling() {
10
+ return new FsSafeError("path-alias", "Windows compatibility writes require lower-case ASCII destination components");
11
+ }
12
+ // The caller first applies the Root's ordinary path and mutation policies. This
13
+ // extra admission rule is deliberately narrower than general Windows filenames:
14
+ // the existing lock protocol has no portable case/Unicode equivalence key for a
15
+ // missing name. Do not guess one, or switch keys when a placeholder appears.
16
+ function effectiveDestination(rootPath, targetPath, rootIdentity) {
17
+ let effective;
18
+ try {
19
+ effective = realpathSync.native(targetPath);
20
+ }
21
+ catch (error) {
22
+ if (!isNotFoundPathError(error))
23
+ throw error;
24
+ let cursor = targetPath;
25
+ const missing = [];
26
+ for (;;) {
27
+ try {
28
+ fs.lstatSync(cursor);
29
+ break;
30
+ }
31
+ catch (error) {
32
+ if (!isNotFoundPathError(error) || cursor === rootPath)
33
+ throw error;
34
+ const parent = path.dirname(cursor);
35
+ if (parent === cursor)
36
+ throw error;
37
+ missing.unshift(path.basename(cursor));
38
+ cursor = parent;
39
+ }
40
+ }
41
+ // An existing dangling link must fail resolution, not become a missing
42
+ // lexical component. Keep the fallback bounded by the retained Root.
43
+ effective = path.join(realpathSync.native(cursor), ...missing);
44
+ }
45
+ const admitted = admitPathInsideRoot({ rootPath, candidatePath: effective, rootIdentity });
46
+ if (!admitted)
47
+ throw unsupportedSpelling();
48
+ const relative = admitted.relativePath;
49
+ if (!relative || !relative.split(path.sep).every(component => /^[a-z0-9._-]+$/.test(component) && !component.endsWith(".")))
50
+ throw unsupportedSpelling();
51
+ return admitted.path;
52
+ }
53
+ export function assertRootFallbackWritePath(expected, actual) {
54
+ if (expected !== undefined && actual !== expected) {
55
+ throw new FsSafeError("path-mismatch", "compatibility write destination changed after lock selection");
56
+ }
57
+ }
58
+ export async function withRootFallbackCompatibilityLock(params, run) {
59
+ const targetPath = effectiveDestination(params.rootPath, params.targetPath, params.rootIdentity);
60
+ const relativePath = path.relative(params.rootPath, targetPath).split(path.sep).join("/");
61
+ const assertCurrent = () => assertRootFallbackWritePath(targetPath, effectiveDestination(params.rootPath, targetPath, params.rootIdentity));
62
+ // Sidecars are reentrant within one manager; aliases also need the same local
63
+ // queue before acquisition. The outer Root queue uses the caller's spelling.
64
+ return await serializePathWrite(targetPath, async () => await withPinnedWriteRenameIdentityLock({
65
+ rootPath: params.rootPath, targetPath, relativeTargetPath: relativePath,
66
+ }, async () => {
67
+ // The guarded open resolves again under the lock; each mutation rechecks
68
+ // below. Lock selection is never reused as post-lock filesystem authority.
69
+ return await run({
70
+ targetPath, relativePath,
71
+ assertBeforeMutation: () => { params.assertBeforeMutation?.(); assertCurrent(); },
72
+ });
73
+ }));
74
+ }
@@ -0,0 +1,42 @@
1
+ import type { BigIntStats } from "node:fs";
2
+ import { type AsyncDirectoryGuard } from "./directory-guard.js";
3
+ import type { RootContext } from "./root-context.js";
4
+ import type { GuardedRootWriteTarget } from "./root-write-admission.js";
5
+ type PreparedWriteTargetObservation = Readonly<{
6
+ exists: false;
7
+ } | {
8
+ exists: true;
9
+ stat: BigIntStats;
10
+ }>;
11
+ /**
12
+ * Exact, operation-local evidence that an ordinary shared-JavaScript write
13
+ * already has a complete parent. Path selection consumes this instead of
14
+ * recapturing the same parent after a component walk.
15
+ */
16
+ export type PreparedRootWriteParent = Readonly<{
17
+ operationTargetPath: string;
18
+ selectedTargetPath: string;
19
+ parentGuard: AsyncDirectoryGuard<BigIntStats>;
20
+ rootPath: string;
21
+ rootIdentity: Readonly<{
22
+ dev: bigint;
23
+ ino: bigint;
24
+ }>;
25
+ nativeMode: string;
26
+ target: PreparedWriteTargetObservation;
27
+ }>;
28
+ type SharedRootWriteTargetParams = Readonly<{
29
+ relativePath: string;
30
+ guardedTarget: GuardedRootWriteTarget;
31
+ mkdir?: boolean;
32
+ assertBeforeMutation?: () => void;
33
+ }>;
34
+ export type SharedRootWriteTarget = Readonly<{
35
+ targetPath: string;
36
+ mutationAdmission: GuardedRootWriteTarget["mutationAdmission"];
37
+ preparedParent?: PreparedRootWriteParent;
38
+ }>;
39
+ export declare function assertPreparedRootWriteParentCurrent(prepared: PreparedRootWriteParent, verifyTarget?: boolean): void;
40
+ export declare function prepareSharedRootWriteTarget(root: RootContext, params: SharedRootWriteTargetParams): Promise<SharedRootWriteTarget>;
41
+ export {};
42
+ //# sourceMappingURL=root-write-complete-parent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-write-complete-parent.d.ts","sourceRoot":"","sources":["../src/root-write-complete-parent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAO9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAKxE,KAAK,8BAA8B,GAAG,QAAQ,CAC1C;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,GACjB;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CACtC,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAC7C,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,QAAQ,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,8BAA8B,CAAC;CACxC,CAAC,CAAC;AAEH,KAAK,2BAA2B,GAAG,QAAQ,CAAC;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,sBAAsB,CAAC;IACtC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;CACnC,CAAC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,sBAAsB,CAAC,mBAAmB,CAAC,CAAC;IAC/D,cAAc,CAAC,EAAE,uBAAuB,CAAC;CAC1C,CAAC,CAAC;AA2GH,wBAAgB,oCAAoC,CAClD,QAAQ,EAAE,uBAAuB,EACjC,YAAY,UAAQ,GACnB,IAAI,CAqBN;AAwCD,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,qBAAqB,CAAC,CAqBhC"}
@@ -0,0 +1,195 @@
1
+ import fsSync from "node:fs";
2
+ import path from "node:path";
3
+ import { assertDirectoryIdentitySync, createAsyncDirectoryGuard, inspectDirectoryIdentitySync, } from "./directory-guard.js";
4
+ import { WINDOWS_RESERVED_DEVICE_NAMES } from "./device-path.js";
5
+ import { FsSafeError } from "./errors.js";
6
+ import { getFsSafeNativeConfig } from "./native-config.js";
7
+ import { hasNodeErrorCode } from "./path.js";
8
+ import { realpathSync } from "./realpath.js";
9
+ import { admitPathInsideRoot } from "./root-boundary.js";
10
+ import { prepareRootWriteTarget } from "./root-directory-creation.js";
11
+ import { isSafePathSegment } from "./safe-path-segment.js";
12
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
13
+ import { getFsSafeTestHooks } from "./test-hooks.js";
14
+ function writeSelectionChanged(cause) {
15
+ return new FsSafeError("path-mismatch", "write target changed during operation", {
16
+ cause: cause instanceof Error ? cause : undefined,
17
+ });
18
+ }
19
+ function sameNormalizedPathSpelling(left, right) {
20
+ // Windows can expose case-sensitive directories, so normalize separators
21
+ // and roots without using path.relative's case-folding comparison.
22
+ return path.resolve(left) === path.resolve(right);
23
+ }
24
+ function windowsReservedDeviceSegment(segment) {
25
+ const extension = segment.indexOf(".");
26
+ const stem = segment.slice(0, extension < 0 ? segment.length : extension).toUpperCase();
27
+ return WINDOWS_RESERVED_DEVICE_NAMES.has(stem);
28
+ }
29
+ function ordinarySharedWriteRoute(root, relativePath, operationTargetPath) {
30
+ if (process.versions.bun || relativePath === "" || relativePath.startsWith("~") ||
31
+ relativePath.includes("\0") || path.isAbsolute(relativePath))
32
+ return false;
33
+ const separator = process.platform === "win32" ? "\\" : "/";
34
+ const alternateSeparator = process.platform === "win32" ? "/" : "\\";
35
+ if (relativePath.includes(alternateSeparator))
36
+ return false;
37
+ const segments = relativePath.split(separator);
38
+ if (segments.some((segment) => segment === "" || segment === "." || segment === ".." ||
39
+ (process.platform === "win32" &&
40
+ (!isSafePathSegment(segment, { allowDotPrefix: true }) ||
41
+ segment.endsWith(".") || segment.endsWith(" ") ||
42
+ windowsReservedDeviceSegment(segment)))))
43
+ return false;
44
+ const routed = path.resolve(root.rootReal, relativePath);
45
+ const admitted = admitPathInsideRoot({
46
+ rootPath: root.rootReal,
47
+ candidatePath: routed,
48
+ rootIdentity: root.rootIdentity,
49
+ });
50
+ return admitted?.admission === "exact" &&
51
+ sameNormalizedPathSpelling(admitted.path, routed) &&
52
+ sameNormalizedPathSpelling(routed, operationTargetPath);
53
+ }
54
+ // Preparation is optional evidence. An exact identity mismatch is conclusive
55
+ // and remains fail-closed; every other probe failure must let the established
56
+ // ordered component admission classify the same filesystem state.
57
+ function deoptPreparationFailure(error) {
58
+ if (error instanceof FsSafeError && error.code === "path-mismatch")
59
+ throw error;
60
+ return undefined;
61
+ }
62
+ function capturePreparedTargetObservation(targetPath) {
63
+ let stat;
64
+ try {
65
+ stat = inspectFileIdentitySync(() => fsSync.lstatSync(targetPath, { bigint: true }));
66
+ }
67
+ catch (error) {
68
+ if (hasNodeErrorCode(error, "ENOENT"))
69
+ return Object.freeze({ exists: false });
70
+ return deoptPreparationFailure(error);
71
+ }
72
+ // Final aliases retain the established full component walk and selected-path
73
+ // admission. The shortcut is intentionally limited to an ordinary spelling.
74
+ if (stat.isSymbolicLink())
75
+ return undefined;
76
+ let realPath;
77
+ try {
78
+ realPath = realpathSync.native(targetPath);
79
+ }
80
+ catch (error) {
81
+ return deoptPreparationFailure(error);
82
+ }
83
+ if (!sameNormalizedPathSpelling(realPath, targetPath))
84
+ return undefined;
85
+ return Object.freeze({ exists: true, stat });
86
+ }
87
+ function samePreparedTargetFacts(left, right) {
88
+ return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode &&
89
+ left.nlink === right.nlink;
90
+ }
91
+ function assertPreparedTargetCurrent(prepared) {
92
+ if (!prepared.target.exists) {
93
+ try {
94
+ fsSync.lstatSync(prepared.selectedTargetPath, { bigint: true });
95
+ }
96
+ catch (error) {
97
+ if (hasNodeErrorCode(error, "ENOENT"))
98
+ return;
99
+ throw writeSelectionChanged(error);
100
+ }
101
+ throw writeSelectionChanged();
102
+ }
103
+ try {
104
+ const current = inspectFileIdentitySync(() => fsSync.lstatSync(prepared.selectedTargetPath, { bigint: true }), prepared.target.stat);
105
+ if (current.isSymbolicLink() || !samePreparedTargetFacts(prepared.target.stat, current)) {
106
+ throw writeSelectionChanged();
107
+ }
108
+ }
109
+ catch (error) {
110
+ if (error instanceof FsSafeError && error.code === "path-mismatch")
111
+ throw error;
112
+ throw writeSelectionChanged(error);
113
+ }
114
+ }
115
+ export function assertPreparedRootWriteParentCurrent(prepared, verifyTarget = false) {
116
+ try {
117
+ if (getFsSafeNativeConfig().mode !== prepared.nativeMode)
118
+ throw writeSelectionChanged();
119
+ if (!sameNormalizedPathSpelling(prepared.parentGuard.dir, prepared.rootPath)) {
120
+ assertDirectoryIdentitySync(prepared.rootPath, prepared.rootIdentity);
121
+ }
122
+ else if (prepared.parentGuard.stat.dev !== prepared.rootIdentity.dev ||
123
+ prepared.parentGuard.stat.ino !== prepared.rootIdentity.ino) {
124
+ throw writeSelectionChanged();
125
+ }
126
+ const parent = inspectDirectoryIdentitySync(prepared.parentGuard.dir, prepared.parentGuard.stat);
127
+ if (parent.mode !== prepared.parentGuard.stat.mode ||
128
+ parent.nlink !== prepared.parentGuard.stat.nlink ||
129
+ !sameNormalizedPathSpelling(realpathSync.native(prepared.parentGuard.dir), prepared.parentGuard.realPath))
130
+ throw writeSelectionChanged();
131
+ if (verifyTarget)
132
+ assertPreparedTargetCurrent(prepared);
133
+ }
134
+ catch (error) {
135
+ if (error instanceof FsSafeError && error.code === "path-mismatch")
136
+ throw error;
137
+ throw writeSelectionChanged(error);
138
+ }
139
+ }
140
+ async function prepareCompleteRootWriteParent(root, guarded, relativePath) {
141
+ if (!guarded.mutationAdmission || !guarded.selectedTargetAdmission ||
142
+ typeof root.rootIdentity.dev !== "bigint" || typeof root.rootIdentity.ino !== "bigint" ||
143
+ !ordinarySharedWriteRoute(root, relativePath, guarded.targetPath) ||
144
+ !sameNormalizedPathSpelling(guarded.resolvedPath.resolved, guarded.targetPath))
145
+ return undefined;
146
+ const parentPath = path.dirname(guarded.targetPath);
147
+ const admittedParent = admitPathInsideRoot({
148
+ rootPath: root.rootReal,
149
+ candidatePath: parentPath,
150
+ rootIdentity: root.rootIdentity,
151
+ });
152
+ if (admittedParent?.admission !== "exact" ||
153
+ !sameNormalizedPathSpelling(admittedParent.path, parentPath))
154
+ return undefined;
155
+ let parentGuard;
156
+ try {
157
+ parentGuard = await createAsyncDirectoryGuard(parentPath, { bigint: true });
158
+ }
159
+ catch (error) {
160
+ return deoptPreparationFailure(error);
161
+ }
162
+ if (!sameNormalizedPathSpelling(parentGuard.dir, parentPath) ||
163
+ !sameNormalizedPathSpelling(parentGuard.realPath, parentPath))
164
+ return undefined;
165
+ const target = capturePreparedTargetObservation(guarded.targetPath);
166
+ if (!target)
167
+ return undefined;
168
+ return Object.freeze({
169
+ operationTargetPath: guarded.targetPath,
170
+ selectedTargetPath: guarded.targetPath,
171
+ parentGuard,
172
+ rootPath: root.rootReal,
173
+ rootIdentity: Object.freeze({ dev: root.rootIdentity.dev, ino: root.rootIdentity.ino }),
174
+ nativeMode: getFsSafeNativeConfig().mode,
175
+ target,
176
+ });
177
+ }
178
+ export async function prepareSharedRootWriteTarget(root, params) {
179
+ const { guardedTarget } = params;
180
+ const resolvedPath = guardedTarget.resolvedPath.resolved;
181
+ const mutationAdmission = guardedTarget.mutationAdmission;
182
+ const beforeParentAdmission = mutationAdmission
183
+ ? getFsSafeTestHooks()?.beforePinnedWriteParentAdmission
184
+ : undefined;
185
+ if (mutationAdmission)
186
+ await beforeParentAdmission?.(resolvedPath);
187
+ const preparedParent = params.mkdir !== false &&
188
+ params.assertBeforeMutation === undefined && beforeParentAdmission === undefined
189
+ ? await prepareCompleteRootWriteParent(root, guardedTarget, params.relativePath)
190
+ : undefined;
191
+ const targetPath = params.mkdir === false
192
+ ? guardedTarget.targetPath
193
+ : preparedParent?.selectedTargetPath ?? await prepareRootWriteTarget(root, resolvedPath, params.assertBeforeMutation, mutationAdmission);
194
+ return { targetPath, mutationAdmission, preparedParent };
195
+ }
@@ -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;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,CA6FhB"}
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"}
@@ -48,8 +48,10 @@ export async function verifyAtomicWriteResult(params) {
48
48
  // This descriptor remains owned by the writer, even when final mode forbids opens.
49
49
  const stat = assertDescriptor();
50
50
  assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
51
- const { realPath } = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
52
- assertPath(fsSync.statSync(realPath, { bigint: true }));
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);
53
55
  if (!admitPathInsideRoot({
54
56
  rootPath: params.root.rootReal,
55
57
  candidatePath: realPath,
@@ -79,8 +81,8 @@ export async function verifyAtomicWriteResult(params) {
79
81
  try {
80
82
  const reopenedStat = assertDescriptor(opened.fd);
81
83
  assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
82
- const { realPath: reopenedPath } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
83
- assertPath(fsSync.statSync(reopenedPath, { bigint: true }));
84
+ const { realPath: reopenedPath, stat: reopenedPathStat } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
85
+ assertPath(reopenedPathStat);
84
86
  if (!admitPathInsideRoot({
85
87
  rootPath: params.root.rootReal,
86
88
  candidatePath: reopenedPath,
@@ -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"}