@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
package/dist/root-impl.js CHANGED
@@ -56,28 +56,30 @@ import fs from "node:fs/promises";
56
56
  import path from "node:path";
57
57
  import { normalizeMaxBytes } from "./byte-budget.js";
58
58
  import { assertCopySourceCurrent, resolveFileCopyCloneMode } from "./copy-file-input.js";
59
- import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
59
+ import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
60
60
  import { FsSafeError } from "./errors.js";
61
61
  import { syncDirectoryBestEffort } from "./directory-durability.js";
62
- import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
63
- import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
64
62
  import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
65
- import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
66
- import { resolveOpenedFileRealPathForFd, resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
67
- import { openedPathResolutionError, recordExclusiveCreateFailure, recordFileOpenFailure, recordOpenedFileFailure, recordPreOpenFileChange } from "./opened-file-failure.js";
68
- import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy, } from "./pinned-write.js";
63
+ import { openLocalFileDescriptor } from "./local-file-descriptor.js";
64
+ import { assertMutationNotDenied, mergeDenyMutationPolicies } from "./deny-mutations.js";
65
+ import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
66
+ import { openedPathResolutionError, recordExclusiveCreateFailure, recordOpenedFileFailure } from "./opened-file-failure.js";
67
+ import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy } from "./pinned-write.js";
68
+ import { preparePinnedWriteMutationAdmission, snapshotPinnedMutationPolicy } from "./pinned-mutation-admission.js";
69
69
  import { getNativeBinding } from "./native.js";
70
70
  import { validatePinnedOperationPayload } from "./pinned-operation.js";
71
- import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
72
- import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError, } from "./path.js";
71
+ import { PATH_ALIAS_POLICIES } from "./path-policy.js";
72
+ import { assertNoNulPathInput, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError, } from "./path.js";
73
73
  import { readOpenedFileSafely } from "./read-opened-file.js";
74
- import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
75
- import { resolveReadOpenFlags } from "./read-open-flags.js";
74
+ import { cleanupPinnedFilePath, removePathIfIdentityUnchanged } from "./replace-file-temp-owner.js";
76
75
  import { realpathSync } from "./realpath.js";
76
+ import { mkdirPathFallback, prepareRootWriteTarget, tryMkdirAtExactParent } from "./root-directory-creation.js";
77
77
  import { isNonRegularWriteOpenError, resolveNonblockingWriteFlag } from "./write-open-flags.js";
78
- import { resolveRootPath } from "./root-path.js";
78
+ import { resolveRootPath, resolveRootPathForRemoval } from "./root-path.js";
79
+ import { RemovalPathReceipts } from "./root-remove-receipt.js";
79
80
  import { admitPathInsideRoot } from "./root-boundary.js";
80
- import { openRootDirectoryListing, listDirectoryPath, pathStatFromStats } from "./root-directory-list.js";
81
+ import { listDirectoryPath, openRootDirectoryListing } from "./root-directory-list.js";
82
+ import { statResolvedPathInRoot } from "./root-path-stat.js";
81
83
  import { entriesInRoot } from "./root-entries.js";
82
84
  import { assertMoveMutationAllowed } from "./root-move-preflight.js";
83
85
  import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
@@ -89,12 +91,19 @@ import { registerTempPathForExit } from "./temp-cleanup.js";
89
91
  import { removePathInRootFallback, validateRemoveOptions } from "./root-remove.js";
90
92
  import { serializePathWrite } from "./write-queue.js";
91
93
  import { verifyAtomicWriteResult } from "./root-write-verification.js";
92
- import { inheritWriteTargetMode } from "./root-write-mode.js";
93
- import { inspectFileIdentity } from "./strict-file-identity.js";
94
+ import { assertRootWritePathSelectionSync, assertRootWriteSelectionSync, createRootWriteSelectionForFd, prepareGuardedRootWritePathSelection, resolveGuardedWritePathInRoot, resolveGuardedWriteTargetInRoot, resolvePinnedWriteTargetInRoot, refreshRetainedRootWriteAdmission, refreshRootWritePathSelection, retainRootWriteSelection, takeRootWriteSelection, } from "./root-write-admission.js";
95
+ import { prepareSharedRootWriteTarget } from "./root-write-complete-parent.js";
96
+ import { finishRootFallbackWrite } from "./root-write-publication.js";
97
+ import { assertRootFallbackWritePath, withRootFallbackCompatibilityLock } from "./root-write-compatibility.js";
98
+ import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
99
+ import { movePathNoReplaceNative } from "./root-move-noreplace.js";
100
+ import { admitRootReadHandle, inspectOpenedPathIdentitySync } from "./root-read-admission.js";
94
101
  import { createCopyPublicationObserver, onCopyPublication } from "./copy-publication.js";
95
102
  import { writeAllToFile } from "./write-file-handle.js";
96
103
  import { createInputOptions, rethrowCreateInputError, rootWriteInput } from "./root-create-input.js";
97
104
  import { assertFinalSymlinkRejected, mutationSymlinkResolution, readSymlinkResolution } from "./root-symlink-policy.js";
105
+ import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot } from "./windows-path-alias.js";
106
+ import { resolvePinnedObservedPathInRoot } from "./root-observed-path.js";
98
107
  import { mergeReadOptions, readDefaults, } from "./root-options.js";
99
108
  import { composeMutationAssertions, MutationAuthorityError, rethrowMutationAuthorityError } from "./mutation-authority.js";
100
109
  export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
@@ -105,8 +114,6 @@ function logWarn(message) {
105
114
  }
106
115
  }
107
116
  const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in fsConstants;
108
- const OPEN_READ_FLAGS = resolveReadOpenFlags();
109
- const OPEN_READ_FOLLOW_FLAGS = resolveReadOpenFlags({ followSymlinks: true });
110
117
  const OPEN_WRITE_EXISTING_FLAGS = fsConstants.O_WRONLY | (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0) |
111
118
  resolveNonblockingWriteFlag();
112
119
  const OPEN_WRITE_CREATE_FLAGS = fsConstants.O_WRONLY |
@@ -130,69 +137,9 @@ function openResult(params) {
130
137
  };
131
138
  }
132
139
  async function openVerifiedLocalFile(filePath, options) {
133
- assertNoUnsafeDeviceReadPath(filePath);
134
140
  const fsSafeTestHooks = getFsSafeTestHooks();
135
- let preOpenStat;
136
- let observedBeforeOpen = false;
137
- // Reject directories before opening so we never surface EISDIR to callers (e.g. tool
138
- // results that get sent to messaging channels). See openclaw/openclaw#31186.
141
+ const { handle, stat, identity, preOpenStat } = await openLocalFileDescriptor(filePath, options);
139
142
  try {
140
- preOpenStat = await inspectFileIdentity(async () => {
141
- const stat = fsSync.lstatSync(filePath, { bigint: true });
142
- observedBeforeOpen = true;
143
- if (stat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
144
- throw new FsSafeError("symlink", "symlink not allowed");
145
- }
146
- if (!stat.isFile() && !stat.isSymbolicLink()) {
147
- throw new FsSafeError("not-file", "not a file");
148
- }
149
- return stat;
150
- });
151
- }
152
- catch (err) {
153
- if (err instanceof FsSafeError || observedBeforeOpen) {
154
- throw err;
155
- }
156
- // Only an initial lookup failure falls through; a failed re-inspection aborts.
157
- }
158
- if (preOpenStat) {
159
- await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
160
- }
161
- const openFlags = options?.symlinks === "follow-within-root"
162
- ? OPEN_READ_FOLLOW_FLAGS
163
- : OPEN_READ_FLAGS;
164
- await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
165
- let handle;
166
- try {
167
- handle = await fs.open(filePath, openFlags).catch((error) => recordFileOpenFailure(isNotFoundPathError(error) ? fileNotFoundError() : error, filePath));
168
- }
169
- catch (err) {
170
- if (isSymlinkOpenError(err)) {
171
- throw new FsSafeError("symlink", "symlink open blocked", { cause: err });
172
- }
173
- // Defensive: if open still throws EISDIR (e.g. race), sanitize so it never leaks.
174
- if (hasNodeErrorCode(err, "EISDIR")) {
175
- throw new FsSafeError("not-file", "not a file");
176
- }
177
- throw err;
178
- }
179
- try {
180
- await fsSafeTestHooks?.afterOpen?.(filePath, handle);
181
- const stat = fsSync.fstatSync(handle.fd);
182
- if (!stat.isFile()) {
183
- throw new FsSafeError("not-file", "not a file");
184
- }
185
- // Keep numeric Stats for the public receipt, never for identity verification.
186
- let openedIdentity;
187
- const identity = await inspectFileIdentity(async () => (openedIdentity = fsSync.fstatSync(handle.fd, { bigint: true })), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined).catch(async (error) => {
188
- if ([0, 1].includes(stat.nlink)) {
189
- await recordPreOpenFileChange(error, handle, filePath, preOpenStat, openedIdentity);
190
- }
191
- throw error;
192
- });
193
- if (options?.hardlinks === "reject" && stat.nlink > 1) {
194
- throw hardlinkedPathNotAllowedError();
195
- }
196
143
  const inspectPathIdentity = async (inspect) => {
197
144
  try {
198
145
  return await inspectFileIdentity(inspect, identity);
@@ -205,15 +152,7 @@ async function openVerifiedLocalFile(filePath, options) {
205
152
  throw failure;
206
153
  }
207
154
  };
208
- await inspectPathIdentity(async () => {
209
- const pathStat = options?.symlinks === "follow-within-root"
210
- ? fsSync.statSync(filePath, { bigint: true })
211
- : fsSync.lstatSync(filePath, { bigint: true });
212
- if (pathStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
213
- throw new FsSafeError("symlink", "symlink not allowed");
214
- }
215
- return pathStat;
216
- });
155
+ await inspectPathIdentity(async () => inspectOpenedPathIdentitySync(filePath, options?.symlinks));
217
156
  await fsSafeTestHooks?.afterOpenedPathIdentityCheck?.(filePath, handle);
218
157
  const resolved = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath)
219
158
  .catch(async (error) => {
@@ -223,6 +162,7 @@ async function openVerifiedLocalFile(filePath, options) {
223
162
  throw error;
224
163
  });
225
164
  const { realPath } = resolved;
165
+ assertNoWindowsPathAlias(realPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
226
166
  let resolvedStat = resolved.stat;
227
167
  await inspectPathIdentity(async () => {
228
168
  // Reuse the post-realpath observation; unknown Windows identities still
@@ -242,12 +182,14 @@ async function openVerifiedLocalFile(filePath, options) {
242
182
  }
243
183
  }
244
184
  export class RootHandle {
185
+ rootGuard;
245
186
  rootIdentity;
246
187
  rootDir;
247
188
  rootReal;
248
189
  rootWithSep;
249
190
  defaults;
250
191
  constructor(context, defaults = {}) {
192
+ this.rootGuard = context.rootGuard;
251
193
  this.rootIdentity = context.rootIdentity;
252
194
  this.rootDir = context.rootDir;
253
195
  this.rootReal = context.rootReal;
@@ -257,6 +199,7 @@ export class RootHandle {
257
199
  get context() {
258
200
  return {
259
201
  rootDir: this.rootDir,
202
+ rootGuard: this.rootGuard,
260
203
  rootIdentity: this.rootIdentity,
261
204
  rootReal: this.rootReal,
262
205
  rootWithSep: this.rootWithSep,
@@ -311,7 +254,7 @@ export class RootHandle {
311
254
  async openWritable(relativePath, options = {}) {
312
255
  assertValidRootDestinationPath(relativePath);
313
256
  const writeMode = options.writeMode ?? "replace";
314
- return await openWritableFileInRoot(this.context, {
257
+ const target = await openWritableFileInRoot(this.context, {
315
258
  relativePath,
316
259
  mkdir: this.defaults.mkdir,
317
260
  mode: this.defaults.mode,
@@ -319,6 +262,7 @@ export class RootHandle {
319
262
  append: writeMode === "append",
320
263
  truncateExisting: writeMode === "replace",
321
264
  }).catch(rethrowMutationAuthorityError);
265
+ return target.opened;
322
266
  }
323
267
  async append(relativePath, data, options = {}) {
324
268
  assertValidRootDestinationPath(relativePath);
@@ -430,6 +374,7 @@ export class RootHandle {
430
374
  assertValidRootDestinationPath(toRelative);
431
375
  validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
432
376
  const { denyMutations, assertBeforeMutation, mutationSymlinks } = this.mutationOptions(options);
377
+ const overwrite = options.overwrite ?? false;
433
378
  await assertMoveMutationAllowed(this.context, {
434
379
  fromRelative,
435
380
  toRelative,
@@ -440,7 +385,7 @@ export class RootHandle {
440
385
  denyMutations,
441
386
  assertBeforeMutation,
442
387
  mutationSymlinks,
443
- overwrite: options.overwrite ?? false,
388
+ overwrite,
444
389
  toRelative,
445
390
  }).catch(rethrowMutationAuthorityError);
446
391
  }
@@ -473,7 +418,8 @@ export async function root(rootDir, defaults = {}) {
473
418
  export function rootFromDirectoryGuard(guard, defaults = {}) {
474
419
  normalizeMaxBytes(defaults.maxBytes);
475
420
  return new RootHandle({
476
- rootDir: path.resolve(guard.dir),
421
+ rootDir: resolvePathPreservingWindowsRoot(guard.dir),
422
+ rootGuard: { dir: guard.realPath, realPath: guard.realPath, stat: guard.stat },
477
423
  rootIdentity: { dev: guard.stat.dev, ino: guard.stat.ino },
478
424
  rootReal: guard.realPath,
479
425
  rootWithSep: ensureTrailingSep(guard.realPath),
@@ -486,24 +432,22 @@ async function openFileInRoot(root, params) {
486
432
  ...readSymlinkResolution(params.symlinks),
487
433
  resolveCanonical: true,
488
434
  });
489
- const { opened } = await openVerifiedLocalFile(resolved, {
435
+ const fsSafeTestHooks = getFsSafeTestHooks();
436
+ if (fsSafeTestHooks?.afterRootReadPathResolution) {
437
+ await fsSafeTestHooks.afterRootReadPathResolution(resolved);
438
+ }
439
+ const { handle, stat, identity } = await openLocalFileDescriptor(resolved, {
490
440
  symlinks: params.symlinks,
491
441
  });
492
- if (params.hardlinks !== "allow" && opened.stat.nlink > 1) {
493
- await opened.handle.close().catch(() => { });
494
- throw hardlinkedPathNotAllowedError();
495
- }
496
- const admittedRealPath = admitPathInsideRoot({
497
- rootPath: root.rootReal,
498
- candidatePath: opened.realPath,
499
- rootIdentity: root.rootIdentity,
442
+ // The admission helper owns the descriptor until the complete root/file/root
443
+ // fence succeeds, then transfers that still-open handle to the caller.
444
+ const admitted = await admitRootReadHandle({
445
+ root, filePath: resolved, opened: { handle, stat }, identity,
446
+ hardlinks: params.hardlinks, symlinks: params.symlinks,
447
+ beforeFinalFence: fsSafeTestHooks?.beforeRootReadFinalFence,
448
+ afterPathIdentityCheck: fsSafeTestHooks?.afterRootReadFinalPathIdentityCheck,
500
449
  });
501
- if (!admittedRealPath) {
502
- await opened.handle.close().catch(() => { });
503
- throw outsideWorkspaceError();
504
- }
505
- opened.realPath = admittedRealPath.path;
506
- return opened;
450
+ return openResult(admitted);
507
451
  }
508
452
  async function readFileInRoot(root, params) {
509
453
  const opened = await openFileInRoot(root, params);
@@ -534,8 +478,10 @@ export async function readLocalFileSafely(params) {
534
478
  }
535
479
  }
536
480
  export async function openLocalFileSafely(params) {
537
- assertNoNulPathInput(params.filePath, "file path contains a NUL byte");
538
- return (await openVerifiedLocalFile(params.filePath)).opened;
481
+ const filePath = params.filePath;
482
+ assertNoNulPathInput(filePath, "file path contains a NUL byte");
483
+ assertNoWindowsPathAlias(filePath, "filesystem", "file path uses a Windows filesystem namespace alias");
484
+ return (await openVerifiedLocalFile(filePath)).opened;
539
485
  }
540
486
  function emitWriteBoundaryWarning(reason) {
541
487
  logWarn(`security: fs-safe write boundary warning (${reason})`);
@@ -546,50 +492,26 @@ function buildAtomicWriteTempPath(targetPath) {
546
492
  function rootWriteQueueKey(root, relativePath) {
547
493
  return `${root.rootReal}\0${relativePath}`;
548
494
  }
549
- async function prepareRootWriteTarget(root, targetPath, assertBeforeMutation) {
550
- const parentPath = await mkdirPathComponentsWithGuards({
551
- rootReal: root.rootReal,
552
- rootIdentity: root.rootIdentity,
553
- targetPath: path.dirname(targetPath),
554
- assertBeforeMutation,
555
- });
556
- // Continue through the guarded walk's real parent instead of re-entering
557
- // the original path through a symlinked component.
558
- return path.join(parentPath, path.basename(targetPath));
559
- }
560
- async function resolveGuardedWritePathInRoot(root, params) {
561
- const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
562
- aliasErrorCode: "path-alias",
563
- rejectAmbiguousParents: true,
564
- allowFinalSymlink: params.allowFinalSymlink,
565
- ...mutationSymlinkResolution(params.mutationSymlinks),
566
- });
567
- await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
568
- if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
569
- try {
570
- await assertNoPathAliasEscape({
571
- absolutePath: resolvedPath.resolved,
572
- rootPath: resolvedPath.rootReal,
573
- boundaryLabel: "root",
574
- });
575
- }
576
- catch (error) {
577
- throw new FsSafeError("path-alias", "path alias escape blocked", {
578
- cause: error instanceof Error ? error : undefined,
579
- });
580
- }
581
- }
582
- return resolvedPath;
583
- }
584
495
  async function openWritableFileInRoot(root, params) {
585
- const { resolved } = await resolveGuardedWritePathInRoot(root, {
496
+ const guardedTarget = params.denyMutations === undefined && params.mutationSymlinks === undefined
497
+ ? undefined
498
+ : await resolveGuardedWriteTargetInRoot(root, {
499
+ relativePath: params.relativePath,
500
+ denyMutations: params.denyMutations,
501
+ mutationSymlinks: params.mutationSymlinks,
502
+ });
503
+ const { resolved } = guardedTarget?.resolvedPath ?? await resolveGuardedWritePathInRoot(root, {
586
504
  relativePath: params.relativePath,
587
- denyMutations: params.denyMutations,
588
- mutationSymlinks: params.mutationSymlinks,
589
505
  });
590
- let ioPath = params.mkdir === false
591
- ? resolved
592
- : await prepareRootWriteTarget(root, resolved, params.assertBeforeMutation);
506
+ const prepared = guardedTarget ? await prepareSharedRootWriteTarget(root, {
507
+ relativePath: params.relativePath, guardedTarget, mkdir: params.mkdir,
508
+ assertBeforeMutation: params.assertBeforeMutation,
509
+ }) : undefined;
510
+ const mutationAdmission = prepared?.mutationAdmission;
511
+ const preparedParent = prepared?.preparedParent;
512
+ let ioPath = prepared?.targetPath ?? (params.mkdir === false ? resolved :
513
+ await prepareRootWriteTarget(root, resolved, params.assertBeforeMutation));
514
+ const operationTargetPath = ioPath;
593
515
  try {
594
516
  assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
595
517
  const resolvedRealPath = params.mutationSymlinks === undefined ? realpathSync.native(ioPath) : ioPath;
@@ -611,13 +533,20 @@ async function openWritableFileInRoot(root, params) {
611
533
  throw err;
612
534
  }
613
535
  }
536
+ assertRootFallbackWritePath(params.expectedWritePath, ioPath);
614
537
  const mode = params.mode ?? 0o600;
615
538
  let handle;
616
539
  let createdForWrite = false;
540
+ let writePathSelection = undefined;
617
541
  const existingFlags = params.append ? OPEN_APPEND_EXISTING_FLAGS : OPEN_WRITE_EXISTING_FLAGS;
618
542
  const createFlags = params.append ? OPEN_APPEND_CREATE_FLAGS : OPEN_WRITE_CREATE_FLAGS;
619
543
  try {
544
+ writePathSelection = guardedTarget
545
+ ? await prepareGuardedRootWritePathSelection(guardedTarget, ioPath, operationTargetPath, preparedParent)
546
+ : undefined;
620
547
  try {
548
+ if (writePathSelection)
549
+ assertRootWritePathSelectionSync(root, writePathSelection);
621
550
  handle = await fs.open(ioPath, existingFlags, mode);
622
551
  }
623
552
  catch (err) {
@@ -627,7 +556,11 @@ async function openWritableFileInRoot(root, params) {
627
556
  if (!isNotFoundPathError(err)) {
628
557
  throw err;
629
558
  }
559
+ if (writePathSelection)
560
+ await refreshRootWritePathSelection(writePathSelection);
630
561
  params.assertBeforeMutation?.();
562
+ if (writePathSelection)
563
+ assertRootWritePathSelectionSync(root, writePathSelection);
631
564
  handle = await fs.open(ioPath, createFlags, mode);
632
565
  createdForWrite = true;
633
566
  }
@@ -648,36 +581,60 @@ async function openWritableFileInRoot(root, params) {
648
581
  let createdIdentity = null;
649
582
  try {
650
583
  const stat = fsSync.fstatSync(handle.fd);
651
- if (createdForWrite) {
652
- createdIdentity = stat;
653
- }
654
- if (!stat.isFile()) {
655
- throw new FsSafeError("not-file", "path is not a regular file under root");
656
- }
657
- if (stat.nlink > 1) {
658
- throw hardlinkedPathNotAllowedError();
659
- }
660
- try {
661
- const lstat = fsSync.lstatSync(ioPath);
662
- if (lstat.isSymbolicLink() || !lstat.isFile()) {
663
- throw new FsSafeError(lstat.isSymbolicLink() ? "symlink" : "not-file", "path is not a regular file under root");
664
- }
665
- if (!sameFileIdentity(stat, lstat)) {
666
- throw new FsSafeError("path-mismatch", "path changed during write");
584
+ const identity = inspectFileIdentitySync(() => {
585
+ const observed = fsSync.fstatSync(handle.fd, { bigint: true });
586
+ if (!observed.isFile()) {
587
+ throw new FsSafeError("not-file", "path is not a regular file under root");
667
588
  }
589
+ if (observed.nlink > 1n)
590
+ throw hardlinkedPathNotAllowedError();
591
+ return observed;
592
+ });
593
+ if (createdForWrite)
594
+ createdIdentity = identity;
595
+ let observedIoPath = false;
596
+ try {
597
+ inspectFileIdentitySync(() => {
598
+ const lstat = fsSync.lstatSync(ioPath, { bigint: true });
599
+ observedIoPath = true;
600
+ if (lstat.isSymbolicLink() || !lstat.isFile()) {
601
+ throw new FsSafeError(lstat.isSymbolicLink() ? "symlink" : "not-file", "path is not a regular file under root");
602
+ }
603
+ if (lstat.nlink > 1n)
604
+ throw hardlinkedPathNotAllowedError();
605
+ return lstat;
606
+ }, identity);
668
607
  }
669
608
  catch (err) {
670
- if (!isNotFoundPathError(err)) {
609
+ if (isNotFoundPathError(err) && !observedIoPath) {
610
+ // The opened file may have been renamed before its first pathname
611
+ // observation. The descriptor-bound resolver below must still find it.
612
+ }
613
+ else if (err instanceof FsSafeError && err.code === "path-mismatch") {
614
+ throw new FsSafeError("path-mismatch", "path changed during write", { cause: err });
615
+ }
616
+ else {
671
617
  throw err;
672
618
  }
673
619
  }
674
- let realPath = await resolveOpenedFileRealPathForHandle(handle, ioPath);
675
- const realStat = fsSync.statSync(realPath);
676
- if (!sameFileIdentity(stat, realStat)) {
677
- throw new FsSafeError("path-mismatch", "path mismatch");
620
+ let realPath = (await resolveOpenedFileRealPathForFd(handle.fd, identity, ioPath)).realPath;
621
+ assertNoWindowsPathAlias(realPath, "filesystem", "resolved file path uses a Windows filesystem namespace alias");
622
+ try {
623
+ inspectFileIdentitySync(() => {
624
+ const realStat = fsSync.statSync(realPath, { bigint: true });
625
+ if (!realStat.isFile()) {
626
+ throw new FsSafeError("not-file", "path is not a regular file under root");
627
+ }
628
+ if (realStat.nlink > 1n)
629
+ throw hardlinkedPathNotAllowedError();
630
+ return realStat;
631
+ }, identity);
678
632
  }
679
- if (realStat.nlink > 1) {
680
- throw hardlinkedPathNotAllowedError();
633
+ catch (err) {
634
+ if (err instanceof FsSafeError && err.code === "path-mismatch") {
635
+ throw new FsSafeError("path-mismatch", "path mismatch", { cause: err });
636
+ }
637
+ throw err;
681
638
  }
682
639
  const admittedRealPath = admitPathInsideRoot({
683
640
  rootPath: root.rootReal,
@@ -689,14 +646,26 @@ async function openWritableFileInRoot(root, params) {
689
646
  }
690
647
  realPath = admittedRealPath.path;
691
648
  realPathForCleanup = realPath;
649
+ assertRootFallbackWritePath(params.expectedWritePath, realPath);
650
+ const writeSelection = writePathSelection
651
+ ? createRootWriteSelectionForFd(writePathSelection, handle.fd)
652
+ : undefined;
692
653
  // Truncate only after boundary and identity checks complete. This avoids
693
654
  // irreversible side effects if a symlink target changes before validation.
694
655
  if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
656
+ if (writeSelection)
657
+ await refreshRetainedRootWriteAdmission(root, writeSelection, true, handle.fd);
695
658
  assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
696
659
  params.assertBeforeMutation?.();
660
+ if (writeSelection)
661
+ assertRootWriteSelectionSync(root, writeSelection, true, handle.fd);
697
662
  await handle.truncate(0);
698
663
  }
699
- return {
664
+ if (writeSelection) {
665
+ await refreshRetainedRootWriteAdmission(root, writeSelection, true, handle.fd);
666
+ assertRootWriteSelectionSync(root, writeSelection, true, handle.fd);
667
+ }
668
+ const result = {
700
669
  handle,
701
670
  containment: "best-effort",
702
671
  createdForWrite,
@@ -704,6 +673,9 @@ async function openWritableFileInRoot(root, params) {
704
673
  stat,
705
674
  [Symbol.asyncDispose]: () => handle.close().catch(() => undefined),
706
675
  };
676
+ if (writeSelection)
677
+ retainRootWriteSelection(result, writeSelection);
678
+ return { opened: result, identity };
707
679
  }
708
680
  catch (err) {
709
681
  const cleanupCreatedPath = createdForWrite && err instanceof FsSafeError;
@@ -718,7 +690,7 @@ async function openWritableFileInRoot(root, params) {
718
690
  async function appendFileInRoot(root, params) {
719
691
  const env_1 = { stack: [], error: void 0, hasError: false };
720
692
  try {
721
- const target = await openWritableFileInRoot(root, {
693
+ const { opened: target, identity } = await openWritableFileInRoot(root, {
722
694
  relativePath: params.relativePath,
723
695
  mkdir: params.mkdir,
724
696
  mode: params.mode,
@@ -733,7 +705,7 @@ async function appendFileInRoot(root, params) {
733
705
  const cleanup = __addDisposableResource(env_1, {
734
706
  async [Symbol.asyncDispose]() {
735
707
  if (!dispatched && target.createdForWrite) {
736
- await removePathIfIdentityUnchanged(target.realPath, target.stat);
708
+ await removePathIfIdentityUnchanged(target.realPath, identity);
737
709
  }
738
710
  },
739
711
  }, true);
@@ -786,14 +758,16 @@ async function appendFileInRoot(root, params) {
786
758
  }
787
759
  async function removePathInRoot(root, params) {
788
760
  validatePinnedOperationPayload({ relativePath: params.relativePath });
761
+ const removalReceipts = params.recursive ? undefined : new RemovalPathReceipts();
789
762
  const resolved = await resolvePinnedPathInRoot(root, {
790
763
  relativePath: params.relativePath,
791
764
  denyMutations: params.denyMutations,
792
765
  mutationSymlinks: params.mutationSymlinks,
793
766
  remove: true,
767
+ removalReceipts,
794
768
  });
795
769
  try {
796
- await removePathInRootFallback(root, resolved.resolved, params);
770
+ await removePathInRootFallback(root, resolved.resolved, params, removalReceipts);
797
771
  }
798
772
  catch (error) {
799
773
  if (params.recursive)
@@ -803,9 +777,41 @@ async function removePathInRoot(root, params) {
803
777
  }
804
778
  async function mkdirPathInRoot(root, params) {
805
779
  validatePinnedOperationPayload({ relativePath: params.relativePath });
806
- const resolved = await resolvePinnedPathInRoot(root, params);
780
+ const policy = params.denyMutations === undefined && params.mutationSymlinks === undefined
781
+ ? undefined
782
+ : snapshotPinnedMutationPolicy(params.denyMutations, params.mutationSymlinks);
783
+ const resolveCurrent = policy
784
+ ? async () => await resolvePinnedPathInRoot(root, {
785
+ ...params,
786
+ denyMutations: policy.denyMutations,
787
+ mutationSymlinks: policy.mutationSymlinks,
788
+ })
789
+ : undefined;
790
+ const resolved = resolveCurrent
791
+ ? await resolveCurrent()
792
+ : await resolvePinnedPathInRoot(root, params);
793
+ const prepared = policy && resolved.relativePosix !== ""
794
+ ? await preparePinnedWriteMutationAdmission({
795
+ rootReal: resolved.rootReal,
796
+ rootWithSep: ensureTrailingSep(resolved.rootReal),
797
+ rootIdentity: root.rootIdentity,
798
+ resolvedTargetPath: resolved.resolved,
799
+ originalPath: params.relativePath,
800
+ defaultRelativeParentPath: path.posix.dirname(resolved.relativePosix) === "."
801
+ ? ""
802
+ : path.posix.dirname(resolved.relativePosix),
803
+ policy,
804
+ resolveCurrent: resolveCurrent,
805
+ })
806
+ : undefined;
807
+ if (prepared?.mutationAdmission) {
808
+ await getFsSafeTestHooks()?.beforePinnedWriteParentAdmission?.(resolved.resolved);
809
+ }
807
810
  try {
808
- await mkdirPathFallback(root, resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
811
+ if (prepared?.mutationAdmission && params.assertBeforeMutation === undefined &&
812
+ await tryMkdirAtExactParent(root, resolved.resolved, prepared.mutationAdmission))
813
+ return;
814
+ await mkdirPathFallback(root, resolved, params.assertBeforeMutation, policy?.mutationSymlinks !== undefined, prepared?.mutationAdmission);
809
815
  }
810
816
  catch (error) {
811
817
  throw normalizePinnedPathError(error);
@@ -842,6 +848,7 @@ async function commitPinnedWriteInRoot(root, pinned, params, input) {
842
848
  input,
843
849
  maxBytes: params.maxBytes,
844
850
  rootIdentity: root.rootIdentity,
851
+ mutationAdmission: pinned.mutationAdmission,
845
852
  assertBeforeMutation: params.assertBeforeMutation,
846
853
  verifyPublished: async (fd, expectedIdentity, parentGuard) => {
847
854
  verifyingPublication = true;
@@ -883,6 +890,7 @@ async function copyFileInRoot(root, params) {
883
890
  let sourceIdentity;
884
891
  if (typeof params.source === "string") {
885
892
  assertNoNulPathInput(params.source, "source path contains a NUL byte");
893
+ assertNoWindowsPathAlias(params.source, "filesystem", "source path uses a Windows filesystem namespace alias");
886
894
  ({ opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.source, {
887
895
  hardlinks: params.sourceHardlinks,
888
896
  }));
@@ -934,6 +942,7 @@ async function copyFileInRoot(root, params) {
934
942
  onPublished: observer.onPublished,
935
943
  input: { kind: "file", handle: source.handle, size: source.stat.size, clone, signal: params.signal, verifySource },
936
944
  rootIdentity: root.rootIdentity,
945
+ mutationAdmission: pinned.mutationAdmission,
937
946
  });
938
947
  }
939
948
  catch (error) {
@@ -953,66 +962,6 @@ async function copyFileInRoot(root, params) {
953
962
  await source.handle.close().catch(() => { });
954
963
  }
955
964
  }
956
- async function removePathIfIdentityUnchanged(targetPath, identity) {
957
- const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
958
- await withAsyncDirectoryGuards([parentGuard], async () => {
959
- const current = fsSync.lstatSync(targetPath);
960
- if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity))
961
- return;
962
- await fs.unlink(targetPath);
963
- });
964
- }
965
- async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true, mutationSymlinks) {
966
- const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
967
- relativePath,
968
- denyMutations,
969
- mutationSymlinks,
970
- });
971
- // resolvePathInRoot already admits and rebases the path at the exact Root
972
- // boundary, so any actual escape is rejected upstream.
973
- const relativeResolved = path.relative(rootReal, resolved);
974
- if (path.isAbsolute(relativeResolved)) {
975
- throw outsideWorkspaceError();
976
- }
977
- const relativePosix = relativeResolved
978
- ? relativeResolved.split(path.sep).join(path.posix.sep)
979
- : "";
980
- const basename = path.posix.basename(relativePosix);
981
- if (!basename || basename === "." || basename === "/") {
982
- throw new FsSafeError("invalid-path", "invalid target path");
983
- }
984
- // Exclusive publication cannot inherit an existing file's mode. Keep type
985
- // and alias checks, but never open a competing owner's record just to reject it.
986
- if (!overwrite) {
987
- try {
988
- const existing = fsSync.statSync(resolved);
989
- if (!existing.isFile())
990
- throw new FsSafeError("not-file", "not a file");
991
- if (existing.nlink > 1)
992
- throw hardlinkedPathNotAllowedError();
993
- throw new FsSafeError("already-exists", "file already exists");
994
- }
995
- catch (error) {
996
- if (!isNotFoundPathError(error))
997
- throw error;
998
- }
999
- }
1000
- const mode = overwrite
1001
- ? await inheritWriteTargetMode({
1002
- targetPath: resolved,
1003
- rootWithSep,
1004
- rootIdentity: root.rootIdentity,
1005
- requestedMode,
1006
- })
1007
- : requestedMode ?? 0o600;
1008
- return {
1009
- rootReal,
1010
- targetPath: resolved,
1011
- relativeParentPath: path.posix.dirname(relativePosix) === "." ? "" : path.posix.dirname(relativePosix),
1012
- basename,
1013
- mode,
1014
- };
1015
- }
1016
965
  async function resolvePinnedPathInRoot(root, params) {
1017
966
  return await resolvePinnedOperationPathInRoot(root, {
1018
967
  allowRoot: params.allowRoot,
@@ -1021,6 +970,7 @@ async function resolvePinnedPathInRoot(root, params) {
1021
970
  protectDenyMutationAncestors: params.remove === true,
1022
971
  relativePath: params.relativePath,
1023
972
  policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
973
+ removalReceipts: params.removalReceipts,
1024
974
  });
1025
975
  }
1026
976
  async function resolvePinnedOperationPathInRoot(root, params) {
@@ -1028,6 +978,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
1028
978
  relativePath: params.relativePath,
1029
979
  policy: params.policy,
1030
980
  mutationSymlinks: params.mutationSymlinks,
981
+ removalReceipts: params.removalReceipts,
1031
982
  });
1032
983
  const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
1033
984
  if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
@@ -1057,12 +1008,12 @@ async function resolvePinnedOperationPathInRoot(root, params) {
1057
1008
  return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
1058
1009
  }
1059
1010
  async function resolvePinnedRootPathInRoot(root, params) {
1060
- await assertRootIdentityCurrent(root);
1011
+ await assertRootIdentityCurrent(root, params.removalReceipts?.observeRoot);
1061
1012
  const rootReal = root.rootReal;
1062
1013
  let resolved;
1063
1014
  try {
1064
1015
  const expandedPath = await expandRelativePathWithHome(params.relativePath);
1065
- resolved = await resolveRootPath({
1016
+ const resolution = {
1066
1017
  absolutePath: path.isAbsolute(expandedPath)
1067
1018
  ? expandedPath
1068
1019
  : `${ensureTrailingSep(rootReal)}${expandedPath}`,
@@ -1072,7 +1023,8 @@ async function resolvePinnedRootPathInRoot(root, params) {
1072
1023
  boundaryLabel: "root",
1073
1024
  policy: params.policy,
1074
1025
  ...mutationSymlinkResolution(params.mutationSymlinks),
1075
- });
1026
+ };
1027
+ resolved = await (params.removalReceipts ? resolveRootPathForRemoval(resolution, params.removalReceipts) : resolveRootPath(resolution));
1076
1028
  }
1077
1029
  catch (err) {
1078
1030
  if (err instanceof FsSafeError && err.code === "symlink")
@@ -1086,31 +1038,20 @@ async function resolvePinnedRootPathInRoot(root, params) {
1086
1038
  canonicalPath: resolved.canonicalPath,
1087
1039
  };
1088
1040
  }
1089
- async function mkdirPathFallback(root, resolved, assertBeforeMutation, rejectSymlinks = false) {
1090
- await mkdirPathComponentsWithGuards({
1091
- rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
1092
- rootIdentity: root.rootIdentity,
1093
- rejectSymlinks,
1094
- beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
1095
- });
1096
- }
1097
1041
  async function statPathFallback(root, relativePath) {
1098
- const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
1099
- try {
1100
- const stat = pathStatFromStats(fsSync.lstatSync(resolved.resolved));
1101
- await assertRootIdentityCurrent(root);
1102
- return stat;
1103
- }
1104
- catch (error) {
1105
- if (isNotFoundPathError(error)) {
1106
- throw fileNotFoundError(error instanceof Error ? error : undefined);
1107
- }
1108
- throw error;
1109
- }
1042
+ const initialObservationHook = getFsSafeTestHooks()?.beforeRootStatInitialObservation;
1043
+ const observed = initialObservationHook
1044
+ ? undefined
1045
+ : await resolvePinnedObservedPathInRoot(root, relativePath, "stat");
1046
+ const resolved = observed ??
1047
+ await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
1048
+ return await statResolvedPathInRoot(root, resolved.resolved, resolved.receipt);
1110
1049
  }
1111
1050
  async function listPathFallback(root, relativePath, withFileTypes) {
1112
- const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
1113
- return await listDirectoryPath(root, resolved.resolved, withFileTypes);
1051
+ const observed = await resolvePinnedObservedPathInRoot(root, relativePath, "directory");
1052
+ const resolved = observed ??
1053
+ await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
1054
+ return await listDirectoryPath(root, resolved.resolved, withFileTypes, resolved.receipt);
1114
1055
  }
1115
1056
  async function movePathFallback(root, params) {
1116
1057
  const source = await resolvePathInRoot(root, params.fromRelative, {
@@ -1119,11 +1060,12 @@ async function movePathFallback(root, params) {
1119
1060
  ...mutationSymlinkResolution(params.mutationSymlinks),
1120
1061
  });
1121
1062
  await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
1122
- await resolvePinnedRootPathInRoot(root, {
1063
+ const pinnedSource = await resolvePinnedRootPathInRoot(root, {
1123
1064
  relativePath: params.fromRelative,
1124
1065
  policy: PATH_ALIAS_POLICIES.strict,
1125
1066
  mutationSymlinks: params.mutationSymlinks,
1126
1067
  });
1068
+ let pinnedTarget;
1127
1069
  const target = await resolveGuardedWritePathInRoot(root, {
1128
1070
  relativePath: params.toRelative,
1129
1071
  denyMutations: params.denyMutations,
@@ -1131,10 +1073,12 @@ async function movePathFallback(root, params) {
1131
1073
  allowFinalSymlink: true,
1132
1074
  protectDeniedAncestors: true,
1133
1075
  shouldAssertNoPathAlias: async (resolvedTarget) => {
1134
- await resolvePinnedRootPathInRoot(root, {
1076
+ pinnedTarget = await resolvePinnedRootPathInRoot(root, {
1135
1077
  relativePath: params.toRelative,
1136
1078
  policy: PATH_ALIAS_POLICIES.unlinkTarget,
1137
1079
  });
1080
+ if (!params.overwrite)
1081
+ return true;
1138
1082
  let targetStat;
1139
1083
  try {
1140
1084
  targetStat = fsSync.lstatSync(resolvedTarget.resolved);
@@ -1165,18 +1109,16 @@ async function movePathFallback(root, params) {
1165
1109
  throw new FsSafeError("invalid-path", "directory moves require overwrite: true");
1166
1110
  }
1167
1111
  if (!params.overwrite) {
1168
- try {
1169
- fsSync.lstatSync(target.resolved);
1170
- throw new FsSafeError("already-exists", "destination exists");
1171
- }
1172
- catch (error) {
1173
- if (error instanceof FsSafeError) {
1174
- throw error;
1175
- }
1176
- if (!isNotFoundPathError(error)) {
1177
- throw error;
1178
- }
1112
+ if (!pinnedTarget) {
1113
+ throw new FsSafeError("path-mismatch", "destination admission was not completed");
1179
1114
  }
1115
+ await movePathNoReplaceNative(root, params, {
1116
+ sourcePath: source.resolved,
1117
+ sourceParentPath: path.dirname(pinnedSource.canonicalPath),
1118
+ targetPath: target.resolved,
1119
+ targetParentPath: path.dirname(pinnedTarget.canonicalPath),
1120
+ });
1121
+ return;
1180
1122
  }
1181
1123
  const sourceParentGuard = await createAsyncDirectoryGuard(path.dirname(source.resolved));
1182
1124
  const targetParentGuard = await createNearestExistingDirectoryGuard(target.rootReal, path.dirname(target.resolved));
@@ -1209,11 +1151,22 @@ async function movePathFallback(root, params) {
1209
1151
  }
1210
1152
  }
1211
1153
  async function writeFileFallback(root, params) {
1154
+ if (params.renameIdentity !== "verify-content-with-lock")
1155
+ return await writeFileFallbackUnlocked(root, params);
1156
+ const policy = snapshotPinnedMutationPolicy(params.denyMutations, params.mutationSymlinks);
1157
+ if (policy)
1158
+ params = { ...params, ...policy };
1159
+ const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, params);
1160
+ await withRootFallbackCompatibilityLock({
1161
+ rootPath: rootReal, rootIdentity: root.rootIdentity, targetPath: resolved, assertBeforeMutation: params.assertBeforeMutation,
1162
+ }, async ({ targetPath, ...binding }) => await writeFileFallbackUnlocked(root, { ...params, ...binding }, targetPath));
1163
+ }
1164
+ async function writeFileFallbackUnlocked(root, params, expectedWritePath) {
1212
1165
  if (params.overwrite === false) {
1213
- await writeMissingFileFallback(root, params);
1166
+ await writeMissingFileFallback(root, params, expectedWritePath);
1214
1167
  return;
1215
1168
  }
1216
- const target = await openWritableFileInRoot(root, {
1169
+ const { opened: target, identity: targetIdentity } = await openWritableFileInRoot(root, {
1217
1170
  relativePath: params.relativePath,
1218
1171
  mkdir: params.mkdir,
1219
1172
  // Private, writable placeholder: Windows cannot rename over a read-only file.
@@ -1222,13 +1175,21 @@ async function writeFileFallback(root, params) {
1222
1175
  assertBeforeMutation: params.assertBeforeMutation,
1223
1176
  mutationSymlinks: params.mutationSymlinks,
1224
1177
  truncateExisting: false,
1178
+ expectedWritePath,
1225
1179
  });
1226
- const destinationPath = target.realPath;
1180
+ const policyEnabled = params.denyMutations !== undefined || params.mutationSymlinks !== undefined;
1181
+ const retainedSelection = policyEnabled ? takeRootWriteSelection(target) : undefined;
1182
+ const destinationPath = retainedSelection?.selectedPath ?? target.realPath;
1227
1183
  const mode = params.mode ?? (target.stat.mode & 0o777);
1228
- const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath), { bigint: true }).catch(async (error) => {
1184
+ if (policyEnabled && !retainedSelection) {
1229
1185
  await target.handle.close().catch(() => undefined);
1230
- throw error;
1231
- });
1186
+ throw new FsSafeError("path-mismatch", "write admission state was not retained");
1187
+ }
1188
+ const destinationGuard = retainedSelection?.parentGuard ??
1189
+ await createAsyncDirectoryGuard(path.dirname(destinationPath), { bigint: true }).catch(async (error) => {
1190
+ await target.handle.close().catch(() => undefined);
1191
+ throw error;
1192
+ });
1232
1193
  let tempPath = null;
1233
1194
  let unregisterTempPath = null;
1234
1195
  let writtenHandle;
@@ -1237,9 +1198,15 @@ async function writeFileFallback(root, params) {
1237
1198
  let published = false;
1238
1199
  try {
1239
1200
  if (target.createdForWrite)
1240
- placeholderIdentity = fsSync.fstatSync(target.handle.fd, { bigint: true });
1201
+ placeholderIdentity = targetIdentity;
1241
1202
  tempPath = buildAtomicWriteTempPath(destinationPath);
1203
+ if (retainedSelection) {
1204
+ await refreshRetainedRootWriteAdmission(root, retainedSelection, true, target.handle.fd);
1205
+ }
1242
1206
  params.assertBeforeMutation?.();
1207
+ if (retainedSelection) {
1208
+ assertRootWriteSelectionSync(root, retainedSelection, true, target.handle.fd);
1209
+ }
1243
1210
  writtenHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, 0o600);
1244
1211
  writtenIdentity = fsSync.fstatSync(writtenHandle.fd, { bigint: true });
1245
1212
  unregisterTempPath = registerTempPathForExit(tempPath, { identity: writtenIdentity, singleLinkFile: true });
@@ -1263,41 +1230,27 @@ async function writeFileFallback(root, params) {
1263
1230
  }
1264
1231
  // Windows cannot replace a destination while its old handle remains open.
1265
1232
  await target.handle.close();
1233
+ if (retainedSelection) {
1234
+ await refreshRetainedRootWriteAdmission(root, retainedSelection, !target.createdForWrite);
1235
+ }
1266
1236
  assertFinalSymlinkRejected(destinationPath, params.mutationSymlinks !== undefined);
1267
1237
  params.assertBeforeMutation?.();
1238
+ if (retainedSelection) {
1239
+ assertRootWriteSelectionSync(root, retainedSelection, !target.createdForWrite);
1240
+ }
1268
1241
  await fs.rename(commitTempPath, destinationPath);
1269
1242
  tempPath = null;
1270
1243
  published = true;
1271
1244
  });
1272
1245
  unregisterTempPath();
1273
1246
  unregisterTempPath = null;
1274
- // Final mode via the retained handle after publication, as the native writer does.
1275
- try {
1276
- await writtenHandle.chmod(mode);
1277
- }
1278
- catch (error) {
1279
- await cleanupPinnedFilePath({
1280
- pathname: destinationPath, handle: writtenHandle, identity: writtenIdentity, parentGuard: destinationGuard,
1281
- });
1282
- throw error;
1283
- }
1284
- if (params.durable !== false)
1285
- await writtenHandle.sync();
1286
- try {
1287
- await verifyAtomicWriteResult({
1288
- root,
1289
- targetPath: destinationPath,
1290
- expectedIdentity: writtenIdentity,
1291
- fd: writtenHandle.fd,
1292
- parentGuard: destinationGuard,
1293
- });
1294
- }
1295
- catch (err) {
1296
- emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
1297
- throw err;
1298
- }
1299
- if (params.durable !== false)
1300
- await syncDirectoryBestEffort(path.dirname(destinationPath));
1247
+ await finishRootFallbackWrite({
1248
+ root, targetPath: destinationPath, handle: writtenHandle, identity: writtenIdentity,
1249
+ parentGuard: destinationGuard, mode, options: params,
1250
+ // Read/write access is needed to sync the accepted destination on Windows.
1251
+ openForCompatibility: () => openVerifiedLocalFile(destinationPath, { hardlinks: "reject", readWrite: true }),
1252
+ onVerificationFailure: err => emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`),
1253
+ });
1301
1254
  }
1302
1255
  finally {
1303
1256
  if (!published && target.createdForWrite) {
@@ -1315,16 +1268,31 @@ async function writeFileFallback(root, params) {
1315
1268
  unregisterTempPath?.();
1316
1269
  }
1317
1270
  }
1318
- async function writeMissingFileFallback(root, params) {
1319
- const { resolved } = await resolveGuardedWritePathInRoot(root, {
1271
+ async function writeMissingFileFallback(root, params, expectedWritePath) {
1272
+ const guardedTarget = params.denyMutations === undefined && params.mutationSymlinks === undefined
1273
+ ? undefined
1274
+ : await resolveGuardedWriteTargetInRoot(root, {
1275
+ relativePath: params.relativePath,
1276
+ denyMutations: params.denyMutations,
1277
+ mutationSymlinks: params.mutationSymlinks,
1278
+ });
1279
+ const { resolved } = guardedTarget?.resolvedPath ?? await resolveGuardedWritePathInRoot(root, {
1320
1280
  relativePath: params.relativePath,
1321
- denyMutations: params.denyMutations,
1322
- mutationSymlinks: params.mutationSymlinks,
1323
1281
  });
1324
- const targetPath = params.mkdir === false
1325
- ? resolved
1326
- : await prepareRootWriteTarget(root, resolved, params.assertBeforeMutation);
1327
- const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
1282
+ const prepared = guardedTarget ? await prepareSharedRootWriteTarget(root, {
1283
+ relativePath: params.relativePath, guardedTarget, mkdir: params.mkdir,
1284
+ assertBeforeMutation: params.assertBeforeMutation,
1285
+ }) : undefined;
1286
+ const mutationAdmission = prepared?.mutationAdmission;
1287
+ const preparedParent = prepared?.preparedParent;
1288
+ const targetPath = prepared?.targetPath ?? (params.mkdir === false ? resolved :
1289
+ await prepareRootWriteTarget(root, resolved, params.assertBeforeMutation));
1290
+ assertRootFallbackWritePath(expectedWritePath, targetPath);
1291
+ const pathSelection = guardedTarget
1292
+ ? await prepareGuardedRootWritePathSelection(guardedTarget, targetPath, targetPath, preparedParent)
1293
+ : undefined;
1294
+ const parentGuard = pathSelection?.parentGuard ??
1295
+ await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
1328
1296
  let created = false;
1329
1297
  let createdIdentity;
1330
1298
  let writtenHandle;
@@ -1333,6 +1301,8 @@ async function writeMissingFileFallback(root, params) {
1333
1301
  const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
1334
1302
  assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
1335
1303
  params.assertBeforeMutation?.();
1304
+ if (mutationAdmission)
1305
+ assertSyncDirectoryGuard(parentGuard);
1336
1306
  const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600).catch((error) => recordExclusiveCreateFailure(error, targetPath));
1337
1307
  writtenHandle = handle;
1338
1308
  created = true;
@@ -1343,6 +1313,8 @@ async function writeMissingFileFallback(root, params) {
1343
1313
  assertBeforeMutation: () => {
1344
1314
  assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
1345
1315
  params.assertBeforeMutation?.();
1316
+ if (mutationAdmission)
1317
+ assertSyncDirectoryGuard(parentGuard);
1346
1318
  },
1347
1319
  });
1348
1320
  if (params.durable !== false)