@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
@@ -4,7 +4,7 @@ import fsSync, {} from "node:fs";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { normalizeMaxBytes } from "./byte-budget.js";
7
- import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
7
+ import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
8
8
  import { FsSafeError } from "./errors.js";
9
9
  import { syncDirectoryBestEffort } from "./directory-durability.js";
10
10
  import { sameFileIdentity, sha256Hex } from "./file-identity.js";
@@ -21,6 +21,7 @@ import { writeAllToFile } from "./write-file-handle.js";
21
21
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
22
22
  import { writeCopyFileToFd } from "./copy-file-input.js";
23
23
  import { publishCopyStage } from "./publish-copy-stage.js";
24
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
24
25
  function byteLength(input, encoding) {
25
26
  return typeof input === "string"
26
27
  ? Buffer.byteLength(input, encoding ?? "utf8")
@@ -31,6 +32,7 @@ function assertSafeBasename(basename) {
31
32
  basename === "." ||
32
33
  basename === ".." ||
33
34
  basename.includes("/") ||
35
+ (process.platform === "win32" && basename.includes("\\")) ||
34
36
  basename.includes("\0")) {
35
37
  throw new FsSafeError("invalid-path", "invalid target path");
36
38
  }
@@ -40,6 +42,10 @@ function assertWithinMaxBytes(bytes, maxBytes) {
40
42
  throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
41
43
  }
42
44
  }
45
+ function fastParentGuardDeopt(error) {
46
+ return error instanceof FsSafeError &&
47
+ (error.code === "path-mismatch" || error.code === "not-file");
48
+ }
43
49
  async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutation) {
44
50
  let bytes = 0;
45
51
  for await (const chunk of stream) {
@@ -49,12 +55,54 @@ async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutatio
49
55
  await writeAllToFile(handle, buffer, { assertBeforeMutation });
50
56
  }
51
57
  }
58
+ const PINNED_WRITE_SNAPSHOT_KEYS = [
59
+ "rootPath", "relativeParentPath", "basename", "maxBytes", "rootIdentity",
60
+ ];
61
+ const RENAME_POLICY_SNAPSHOT_KEYS = ["targetPath", "renameIdentity"];
62
+ function copyOwnEnumerableExcept(source, excluded) {
63
+ const owned = {};
64
+ for (const key of Reflect.ownKeys(source)) {
65
+ // Node 22's object-rest fast path can read excluded accessors eagerly.
66
+ // Exclude before even inspecting the descriptor so named authority and
67
+ // pathname fields remain single-read snapshots on every supported Node.
68
+ if (excluded.includes(key))
69
+ continue;
70
+ if (!Object.getOwnPropertyDescriptor(source, key)?.enumerable)
71
+ continue;
72
+ Object.defineProperty(owned, key, {
73
+ value: Reflect.get(source, key),
74
+ enumerable: true,
75
+ writable: true,
76
+ configurable: true,
77
+ });
78
+ }
79
+ return owned;
80
+ }
52
81
  export async function runPinnedWriteHelper(params) {
53
- const normalizedParams = { ...params, maxBytes: normalizeMaxBytes(params.maxBytes) };
54
- assertSafeBasename(params.basename);
82
+ const { rootPath, relativeParentPath, basename, maxBytes, rootIdentity } = params;
83
+ const ownedParams = copyOwnEnumerableExcept(params, PINNED_WRITE_SNAPSHOT_KEYS);
84
+ const normalizedParams = {
85
+ ...ownedParams,
86
+ rootPath,
87
+ relativeParentPath,
88
+ basename,
89
+ maxBytes: normalizeMaxBytes(maxBytes),
90
+ rootIdentity,
91
+ };
92
+ assertSafeBasename(normalizedParams.basename);
55
93
  validatePinnedOperationPayload({
56
- relativeParentPath: params.relativeParentPath,
94
+ relativeParentPath: normalizedParams.relativeParentPath,
57
95
  });
96
+ assertNoWindowsPathAlias(normalizedParams.rootPath, "filesystem", "pinned write root uses a Windows filesystem namespace alias");
97
+ assertNoWindowsPathAlias(normalizedParams.relativeParentPath, "relative", "pinned write parent uses a Windows filesystem namespace alias");
98
+ assertNoWindowsPathAlias(normalizedParams.basename, "relative", "pinned write basename uses a Windows filesystem namespace alias");
99
+ if (rootIdentity !== undefined) {
100
+ normalizedParams.rootIdentity = { dev: rootIdentity.dev, ino: rootIdentity.ino };
101
+ }
102
+ if (normalizedParams.mutationAdmission) {
103
+ const targetPath = path.join(normalizedParams.rootPath, ...normalizedParams.relativeParentPath.split("/").filter(Boolean), normalizedParams.basename);
104
+ await getFsSafeTestHooks()?.beforePinnedWriteParentAdmission?.(targetPath);
105
+ }
58
106
  // The explicit compatibility policy uses the guarded Node fallback, where
59
107
  // content verification can replace the strict post-rename inode check.
60
108
  if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
@@ -67,51 +115,137 @@ export async function runPinnedWriteHelper(params) {
67
115
  return await runPinnedWriteFallback(normalizedParams);
68
116
  }
69
117
  export async function runPinnedWriteWithRenamePolicy(params) {
70
- const { targetPath, renameIdentity, ...writeParams } = params;
118
+ const { targetPath, renameIdentity } = params;
119
+ const writeParams = copyOwnEnumerableExcept(params, RENAME_POLICY_SNAPSHOT_KEYS);
71
120
  if (renameIdentity !== "verify-content-with-lock") {
72
121
  return await runPinnedWriteHelper(writeParams);
73
122
  }
74
123
  const relativeTargetPath = writeParams.relativeParentPath
75
124
  ? `${writeParams.relativeParentPath}/${writeParams.basename}`
76
125
  : writeParams.basename;
77
- const lockPath = path.join(writeParams.rootPath, `.fs-safe-write-${sha256Hex(relativeTargetPath)}.lock`);
78
- return await withSidecarLock(writeParams.rootPath, {
79
- managerKey: `fs-safe.write:${targetPath}`,
126
+ return await withPinnedWriteRenameIdentityLock({
127
+ rootPath: writeParams.rootPath, targetPath, relativeTargetPath,
128
+ }, async () => await runPinnedWriteHelper({
129
+ ...writeParams,
130
+ onRenameIdentityMismatch: "verify-content",
131
+ }));
132
+ }
133
+ export async function withPinnedWriteRenameIdentityLock(params, run) {
134
+ const lockPath = path.join(params.rootPath, `.fs-safe-write-${sha256Hex(params.relativeTargetPath)}.lock`);
135
+ return await withSidecarLock(params.rootPath, {
136
+ managerKey: `fs-safe.write:${params.targetPath}`,
80
137
  lockPath,
81
138
  staleMs: 30_000,
82
139
  timeoutMs: 5_000,
83
140
  payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
84
141
  retry: { retries: 5, minTimeout: 100, maxTimeout: 2_000, factor: 2 },
85
- }, async () => await runPinnedWriteHelper({
86
- ...writeParams,
87
- onRenameIdentityMismatch: "verify-content",
88
- }));
142
+ }, run);
89
143
  }
90
144
  async function runPinnedWriteFallback(params) {
91
145
  const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
92
146
  ? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
147
+ const mutationAdmission = params.mutationAdmission;
93
148
  if (exactRoot)
94
149
  await inspectDirectoryIdentity(params.rootPath, exactRoot);
95
150
  let parentPath = params.relativeParentPath
96
151
  ? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
97
152
  : params.rootPath;
98
- if (params.mkdir) {
153
+ let parentGuard;
154
+ let parentAdmitted = false;
155
+ const initialTargetPath = path.join(parentPath, params.basename);
156
+ const retainedTargetPath = mutationAdmission?.beginParentWalk?.();
157
+ const ordinaryRetainedTarget = retainedTargetPath !== undefined &&
158
+ path.relative(path.resolve(retainedTargetPath), path.resolve(initialTargetPath)) === "";
159
+ let fastGuardClassifierFailure;
160
+ if (ordinaryRetainedTarget) {
161
+ try {
162
+ parentGuard = await createAsyncDirectoryGuard(parentPath, { bigint: true });
163
+ }
164
+ catch (error) {
165
+ // Only a genuinely missing complete parent may enter the component
166
+ // creator. ENOTDIR, permission, identity, and canonicalization failures
167
+ // retain their original fail-closed result.
168
+ if (error?.code !== "ENOENT") {
169
+ if (!fastParentGuardDeopt(error))
170
+ throw error;
171
+ fastGuardClassifierFailure = error;
172
+ }
173
+ }
174
+ if (parentGuard) {
175
+ await mutationAdmission.authorize(Object.freeze({
176
+ targetPath: initialTargetPath,
177
+ mutationPath: initialTargetPath,
178
+ phase: "parent",
179
+ }));
180
+ await assertAsyncDirectoryGuard(parentGuard);
181
+ parentAdmitted = true;
182
+ }
183
+ }
184
+ if (fastGuardClassifierFailure) {
185
+ // The optimized exact-guard classifier runs before policy admission. Let
186
+ // the established resolver restore deny/symlink precedence before using
187
+ // the guarded walker or reporting the classifier failure. Because this
188
+ // was not an ENOENT probe, it may never authorize a newly missing parent.
189
+ await mutationAdmission.authorize(Object.freeze({
190
+ targetPath: initialTargetPath,
191
+ mutationPath: initialTargetPath,
192
+ phase: "parent",
193
+ }));
194
+ }
195
+ if (params.mkdir && !parentGuard) {
99
196
  // mkdirPathComponentsWithGuards may resolve the final component through
100
197
  // an in-root symlink (e.g. a skill-bank layout). Use its returned real
101
198
  // path for the subsequent guard and target path so we don't re-check the
102
199
  // original, possibly-symlinked, lexical path and reject it outright.
103
- parentPath = await mkdirPathComponentsWithGuards({
200
+ const mkdirParams = {
104
201
  rootReal: params.rootPath,
105
202
  targetPath: parentPath,
106
203
  rootIdentity: params.rootIdentity,
107
204
  assertBeforeMutation: params.assertBeforeMutation,
108
- beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
109
- });
205
+ beforeComponent: async (componentPath) => {
206
+ await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath);
207
+ },
208
+ };
209
+ parentPath = await mkdirPathComponentsWithGuards(mutationAdmission ? {
210
+ ...mkdirParams,
211
+ rejectSymlinks: mutationAdmission.rejectParentSymlinks,
212
+ revalidateParentAfterBeforeComponent: true,
213
+ synchronousAuthorizationIncludesFence: true,
214
+ retainedTargetPath,
215
+ afterCreateComponent: mutationAdmission.advanceCreatedDirectory,
216
+ beforeCreateComponent: (componentPath, prospectiveParentPath, retainedTarget, parent) => {
217
+ if (fastGuardClassifierFailure)
218
+ throw fastGuardClassifierFailure;
219
+ const request = Object.freeze({
220
+ targetPath: retainedTarget ?? path.join(prospectiveParentPath, params.basename),
221
+ mutationPath: componentPath,
222
+ phase: "parent-create",
223
+ });
224
+ return mutationAdmission.tryAuthorizeAtParent?.(request, parent) ??
225
+ mutationAdmission.authorize(request);
226
+ },
227
+ beforeUseComponent: async (_componentPath, prospectiveParentPath, retainedTarget) => {
228
+ const targetPath = retainedTarget ?? path.join(prospectiveParentPath, params.basename);
229
+ await mutationAdmission.authorize(Object.freeze({
230
+ targetPath,
231
+ mutationPath: targetPath,
232
+ phase: "parent",
233
+ }));
234
+ },
235
+ } : mkdirParams);
110
236
  }
111
- const parentGuard = params.mkdir
237
+ parentGuard ??= params.mkdir
112
238
  ? await createAsyncDirectoryGuard(parentPath, { bigint: true })
113
239
  : await createNearestExistingDirectoryGuard(params.rootPath, parentPath, { bigint: true });
114
240
  const targetPath = path.join(parentPath, params.basename);
241
+ if (mutationAdmission && !parentAdmitted) {
242
+ await mutationAdmission.authorize(Object.freeze({
243
+ targetPath,
244
+ mutationPath: targetPath,
245
+ phase: "parent",
246
+ }));
247
+ await assertAsyncDirectoryGuard(parentGuard);
248
+ }
115
249
  if (params.overwrite === false && (params.input.kind === "buffer" ||
116
250
  (params.input.kind === "stream" && !params.input.stageBeforePublish))) {
117
251
  const assertBeforeMutation = () => {
@@ -1 +1 @@
1
- {"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,IAAI,CAAC,CAkBf"}
1
+ {"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,IAAI,CAAC,CAyBf"}
@@ -1,10 +1,12 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  import { getNativeBinding } from "./native.js";
3
+ import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
3
4
  export async function createPrivateDirectory(targetPath, options) {
4
5
  const platform = options?.platform ?? process.platform;
5
6
  if (platform !== "win32") {
6
7
  throw new FsSafeError("helper-unavailable", "private-directory creation is supported only on Windows");
7
8
  }
9
+ assertNoWindowsPathAlias(targetPath, "filesystem", "private directory path uses a Windows filesystem namespace alias", platform);
8
10
  const native = getNativeBinding();
9
11
  if (!native) {
10
12
  throw new FsSafeError("helper-unavailable", "private Windows directory creation requires the matching optional native platform package; " +
@@ -0,0 +1,16 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import { type FileHandle } from "node:fs/promises";
3
+ import { type TempPathRegistration } from "./temp-cleanup.js";
4
+ export type PrivateProducerHandoff = {
5
+ handle: FileHandle;
6
+ identity: BigIntStats;
7
+ unregister: TempPathRegistration;
8
+ };
9
+ export declare function handoffPrivateProducerFile(params: {
10
+ sourcePath: string;
11
+ targetPath: string;
12
+ assertSourceParent: () => void;
13
+ assertTargetParent: () => void;
14
+ readWrite: boolean;
15
+ }): Promise<PrivateProducerHandoff>;
16
+ //# sourceMappingURL=private-producer-handoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"private-producer-handoff.d.ts","sourceRoot":"","sources":["../src/private-producer-handoff.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAW,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKvD,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,oBAAoB,CAAC;CAClC,CAAC;AA2OF,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA6HlC"}
@@ -0,0 +1,272 @@
1
+ import fsSync, {} from "node:fs";
2
+ import fs, {} from "node:fs/promises";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { sameFileIdentityForCleanup } from "./file-identity.js";
5
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
6
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
7
+ import { registerTempPathForExit, } from "./temp-cleanup.js";
8
+ function pathMismatch(message) {
9
+ return new FsSafeError("path-mismatch", message);
10
+ }
11
+ function assertInitialSource(stat) {
12
+ if (stat.isSymbolicLink()) {
13
+ throw new FsSafeError("path-alias", "isolated producer output must not be a symlink");
14
+ }
15
+ if (!stat.isFile()) {
16
+ throw new FsSafeError("not-file", "isolated producer output must be a regular file");
17
+ }
18
+ if (stat.nlink !== 1n) {
19
+ throw new FsSafeError("hardlink", "isolated producer output must have exactly one link");
20
+ }
21
+ }
22
+ function inspectLinkedFile(inspect, expected, links, label) {
23
+ return inspectFileIdentitySync(() => {
24
+ const stat = inspect();
25
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== links) {
26
+ throw pathMismatch(`${label} changed during isolated producer handoff`);
27
+ }
28
+ return stat;
29
+ }, expected);
30
+ }
31
+ function assertParents(params) {
32
+ params.assertTargetParent();
33
+ params.assertSourceParent();
34
+ }
35
+ function normalizeLinkError(error) {
36
+ const code = error?.code;
37
+ if (code === "EEXIST") {
38
+ return new FsSafeError("already-exists", "isolated producer sibling already exists", {
39
+ cause: error instanceof Error ? error : undefined,
40
+ });
41
+ }
42
+ if (code === "EXDEV" ||
43
+ code === "ENOSYS" ||
44
+ code === "ENOTSUP" ||
45
+ code === "EOPNOTSUPP" ||
46
+ code === "EPERM") {
47
+ return new FsSafeError("helper-unavailable", "atomic isolated producer handoff is unavailable", {
48
+ cause: error instanceof Error ? error : undefined,
49
+ });
50
+ }
51
+ return error;
52
+ }
53
+ function removeSiblingIfOwned(params) {
54
+ if (!params.state.siblingOwned)
55
+ return;
56
+ params.assertTargetParent();
57
+ let current;
58
+ try {
59
+ current = fsSync.lstatSync(params.targetPath, { bigint: true });
60
+ }
61
+ catch (error) {
62
+ if (error?.code === "ENOENT") {
63
+ params.state.siblingOwned = false;
64
+ return;
65
+ }
66
+ throw error;
67
+ }
68
+ if (current.isSymbolicLink() ||
69
+ !current.isFile() ||
70
+ (current.nlink !== 1n && current.nlink !== 2n) ||
71
+ !sameFileIdentityForCleanup(current, params.identity)) {
72
+ throw pathMismatch("isolated producer sibling changed before cleanup");
73
+ }
74
+ fsSync.unlinkSync(params.targetPath);
75
+ params.state.siblingOwned = false;
76
+ params.assertTargetParent();
77
+ }
78
+ function combinedFailure(primary, settlement) {
79
+ if (settlement.length === 0)
80
+ return primary;
81
+ return new AggregateError([primary, ...settlement], "isolated producer handoff and settlement failed");
82
+ }
83
+ function hasCode(error, codes) {
84
+ return !(error instanceof FsSafeError) &&
85
+ codes.includes(error?.code ?? "");
86
+ }
87
+ async function throwAfterClosing(primary, handles) {
88
+ const settlement = [];
89
+ const closed = new Set();
90
+ for (const handle of handles) {
91
+ if (!handle || closed.has(handle))
92
+ continue;
93
+ closed.add(handle);
94
+ try {
95
+ await handle.close();
96
+ }
97
+ catch (error) {
98
+ settlement.push(error);
99
+ }
100
+ }
101
+ throw combinedFailure(primary, settlement);
102
+ }
103
+ function inspectSourceDescriptor(handle, identity, links = 1n) {
104
+ inspectLinkedFile(() => fsSync.fstatSync(handle.fd, { bigint: true }), identity, links, "isolated producer descriptor");
105
+ }
106
+ function historicalOpenFlags(readWrite) {
107
+ const access = readWrite ? fsSync.constants.O_RDWR : fsSync.constants.O_RDONLY;
108
+ return access | resolveReadOpenFlags();
109
+ }
110
+ async function openHistoricalSource(sourcePath, identity, readWrite, links) {
111
+ let handle;
112
+ try {
113
+ handle = await fs.open(sourcePath, historicalOpenFlags(readWrite));
114
+ inspectSourceDescriptor(handle, identity, links);
115
+ return handle;
116
+ }
117
+ catch (error) {
118
+ if (handle)
119
+ return await throwAfterClosing(error, [handle]);
120
+ throw error;
121
+ }
122
+ }
123
+ // Windows data-read access can synchronously inspect a completed file through
124
+ // filesystem filters. Prefer write-only admission, which still supports the
125
+ // descriptor metadata, chmod and sync operations used below. Providers or
126
+ // read-only files that reject that access retain the historical open contract.
127
+ async function openProducerSource(params, identity, links = 1n) {
128
+ if (process.platform !== "win32") {
129
+ return await openHistoricalSource(params.sourcePath, identity, params.readWrite, links);
130
+ }
131
+ let provisional;
132
+ try {
133
+ provisional = await fs.open(params.sourcePath, fsSync.constants.O_WRONLY);
134
+ }
135
+ catch (error) {
136
+ if (!hasCode(error, ["EACCES", "EPERM", "EBUSY"]))
137
+ throw error;
138
+ return await openHistoricalSource(params.sourcePath, identity, params.readWrite, links);
139
+ }
140
+ try {
141
+ inspectSourceDescriptor(provisional, identity, links);
142
+ return provisional;
143
+ }
144
+ catch (error) {
145
+ if (!hasCode(error, ["EACCES", "EPERM"])) {
146
+ return await throwAfterClosing(error, [provisional]);
147
+ }
148
+ }
149
+ let fallback;
150
+ try {
151
+ fallback = await fs.open(params.sourcePath, historicalOpenFlags(params.readWrite));
152
+ inspectSourceDescriptor(fallback, identity, links);
153
+ inspectLinkedFile(() => fsSync.lstatSync(params.sourcePath, { bigint: true }), identity, links, "isolated producer path");
154
+ assertParents(params);
155
+ }
156
+ catch (error) {
157
+ return await throwAfterClosing(error, [fallback, provisional]);
158
+ }
159
+ try {
160
+ await provisional.close();
161
+ }
162
+ catch (error) {
163
+ return await throwAfterClosing(error, [fallback]);
164
+ }
165
+ return fallback;
166
+ }
167
+ function assertLinkedHandoff(params, handle, identity) {
168
+ assertParents(params);
169
+ inspectSourceDescriptor(handle, identity, 2n);
170
+ inspectLinkedFile(() => fsSync.lstatSync(params.sourcePath, { bigint: true }), identity, 2n, "isolated producer path");
171
+ inspectLinkedFile(() => fsSync.lstatSync(params.targetPath, { bigint: true }), identity, 2n, "isolated producer sibling");
172
+ }
173
+ export async function handoffPrivateProducerFile(params) {
174
+ assertParents(params);
175
+ const identity = inspectFileIdentitySync(() => {
176
+ const stat = fsSync.lstatSync(params.sourcePath, { bigint: true });
177
+ assertInitialSource(stat);
178
+ return stat;
179
+ });
180
+ let handle;
181
+ let siblingHandle;
182
+ let unregister;
183
+ const state = { siblingOwned: false };
184
+ try {
185
+ handle = await openProducerSource(params, identity);
186
+ inspectLinkedFile(() => fsSync.lstatSync(params.sourcePath, { bigint: true }), identity, 1n, "isolated producer path");
187
+ assertParents(params);
188
+ unregister = registerTempPathForExit(params.targetPath, {
189
+ cleanupSync: () => removeSiblingIfOwned({
190
+ assertTargetParent: params.assertTargetParent,
191
+ identity,
192
+ state,
193
+ targetPath: params.targetPath,
194
+ }),
195
+ });
196
+ assertParents(params);
197
+ try {
198
+ fsSync.linkSync(params.sourcePath, params.targetPath);
199
+ }
200
+ catch (error) {
201
+ throw normalizeLinkError(error);
202
+ }
203
+ state.siblingOwned = true;
204
+ assertLinkedHandoff(params, handle, identity);
205
+ if (process.platform === "win32") {
206
+ // Legacy Windows unlink retains a delete-pending name until handles
207
+ // opened through that name close. Transfer the pin before retiring it.
208
+ siblingHandle = await openProducerSource({ ...params, sourcePath: params.targetPath }, identity, 2n);
209
+ assertLinkedHandoff(params, siblingHandle, identity);
210
+ inspectSourceDescriptor(handle, identity, 2n);
211
+ await handle.close();
212
+ handle = siblingHandle;
213
+ siblingHandle = undefined;
214
+ // The old handle's close yielded; both names must still be ours.
215
+ assertLinkedHandoff(params, handle, identity);
216
+ }
217
+ assertParents(params);
218
+ fsSync.unlinkSync(params.sourcePath);
219
+ assertParents(params);
220
+ const opened = inspectLinkedFile(() => fsSync.fstatSync(handle.fd, { bigint: true }), identity, 1n, "isolated producer descriptor");
221
+ const sibling = inspectLinkedFile(() => fsSync.lstatSync(params.targetPath, { bigint: true }), identity, 1n, "isolated producer sibling");
222
+ const originalMode = identity.mode & 511n;
223
+ if (process.platform === "win32" && (originalMode & 128n) === 0n &&
224
+ ((opened.mode & 511n) !== originalMode || (sibling.mode & 511n) !== originalMode)) {
225
+ // Legacy unlink clears the shared read-only attribute. Repair only the
226
+ // admitted sibling inode, without reopening or chmodding its pathname.
227
+ assertParents(params);
228
+ fsSync.fchmodSync(handle.fd, Number(originalMode));
229
+ assertParents(params);
230
+ const restored = inspectLinkedFile(() => fsSync.fstatSync(handle.fd, { bigint: true }), identity, 1n, "isolated producer descriptor");
231
+ const restoredSibling = inspectLinkedFile(() => fsSync.lstatSync(params.targetPath, { bigint: true }), identity, 1n, "isolated producer sibling");
232
+ if ((restored.mode & 511n) !== originalMode || (restoredSibling.mode & 511n) !== originalMode) {
233
+ throw pathMismatch("isolated producer read-only mode could not be restored");
234
+ }
235
+ }
236
+ const registered = unregister;
237
+ const release = (() => {
238
+ state.siblingOwned = false;
239
+ registered();
240
+ });
241
+ release.setIdentity = registered.setIdentity;
242
+ return { handle, identity, unregister: release };
243
+ }
244
+ catch (error) {
245
+ const settlement = [];
246
+ if (unregister) {
247
+ try {
248
+ removeSiblingIfOwned({
249
+ assertTargetParent: params.assertTargetParent,
250
+ identity,
251
+ state,
252
+ targetPath: params.targetPath,
253
+ });
254
+ unregister();
255
+ }
256
+ catch (cleanupError) {
257
+ settlement.push(cleanupError);
258
+ }
259
+ }
260
+ for (const retained of new Set([siblingHandle, handle])) {
261
+ if (!retained)
262
+ continue;
263
+ try {
264
+ await retained.close();
265
+ }
266
+ catch (closeError) {
267
+ settlement.push(closeError);
268
+ }
269
+ }
270
+ throw combinedFailure(error, settlement);
271
+ }
272
+ }
@@ -1,43 +1,6 @@
1
- import { type FileStore, type FileStoreSync } from "./file-store.js";
2
- import { type TempPathIdentityReceipt } from "./temp-cleanup.js";
3
- import { type TempWorkspaceCleanupResult, type TempWorkspaceCleanupSafety } from "./temp-workspace-owner.js";
1
+ import type { TempWorkspaceOptions, TempWorkspace, TempWorkspaceSync } from "./temp-workspace-types.js";
4
2
  export type { TempWorkspaceCleanupResult, TempWorkspaceCleanupSafety, } from "./temp-workspace-owner.js";
5
- export type TempWorkspaceOptions = {
6
- rootDir: string;
7
- prefix: string;
8
- dirMode?: number;
9
- mode?: number;
10
- cleanupSafety?: TempWorkspaceCleanupSafety;
11
- };
12
- export type TempWorkspace = {
13
- dir: string;
14
- identity: TempPathIdentityReceipt;
15
- store: FileStore;
16
- path(fileName: string): string;
17
- write(fileName: string, data: string | Uint8Array): Promise<string>;
18
- writeText(fileName: string, data: string): Promise<string>;
19
- writeJson(fileName: string, data: unknown, options?: {
20
- trailingNewline?: boolean;
21
- }): Promise<string>;
22
- copyIn(fileName: string, sourcePath: string): Promise<string>;
23
- read(fileName: string): Promise<Buffer>;
24
- cleanup(): Promise<TempWorkspaceCleanupResult>;
25
- [Symbol.asyncDispose](): Promise<void>;
26
- };
27
- export type TempWorkspaceSync = {
28
- dir: string;
29
- identity: TempPathIdentityReceipt;
30
- store: FileStoreSync;
31
- path(fileName: string): string;
32
- write(fileName: string, data: string | Uint8Array): string;
33
- writeText(fileName: string, data: string): string;
34
- writeJson(fileName: string, data: unknown, options?: {
35
- trailingNewline?: boolean;
36
- }): string;
37
- read(fileName: string): Buffer;
38
- cleanup(): TempWorkspaceCleanupResult;
39
- [Symbol.dispose](): void;
40
- };
3
+ export type { TempWorkspaceOptions, TempWorkspace, TempWorkspaceSync } from "./temp-workspace-types.js";
41
4
  export declare function tempWorkspace(options: TempWorkspaceOptions): Promise<TempWorkspace>;
42
5
  export declare function withTempWorkspace<T>(options: TempWorkspaceOptions, run: (workspace: TempWorkspace) => Promise<T>): Promise<T>;
43
6
  export declare function tempWorkspaceSync(options: TempWorkspaceOptions): TempWorkspaceSync;
@@ -1 +1 @@
1
- {"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAExG,YAAY,EACV,0BAA0B,EAC1B,0BAA0B,GAC3B,MAAM,2BAA2B,CAAC;AAEnC,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AA8OxG,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAOZ;AA6JD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CAEnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAOH"}