@openclaw/fs-safe 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +12 -7
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +32 -11
  5. package/dist/advanced.d.ts +2 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +2 -0
  8. package/dist/archive-input.d.ts.map +1 -1
  9. package/dist/archive-input.js +6 -2
  10. package/dist/archive-merge.d.ts.map +1 -1
  11. package/dist/archive-merge.js +15 -2
  12. package/dist/archive-native.d.ts.map +1 -1
  13. package/dist/archive-native.js +7 -19
  14. package/dist/archive-plan.js +1 -1
  15. package/dist/archive-read.d.ts.map +1 -1
  16. package/dist/archive-read.js +18 -13
  17. package/dist/archive-staging.d.ts.map +1 -1
  18. package/dist/archive-staging.js +43 -10
  19. package/dist/archive-tar-inspect.d.ts.map +1 -1
  20. package/dist/archive-tar-inspect.js +4 -1
  21. package/dist/archive-zip-admission.d.ts +1 -1
  22. package/dist/archive-zip-admission.d.ts.map +1 -1
  23. package/dist/archive-zip-admission.js +2 -2
  24. package/dist/archive-zip-directory.d.ts +3 -0
  25. package/dist/archive-zip-directory.d.ts.map +1 -1
  26. package/dist/archive-zip-directory.js +13 -2
  27. package/dist/archive-zip-loader.d.ts +3 -2
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +30 -4
  30. package/dist/archive-zip-manifest.d.ts +5 -0
  31. package/dist/archive-zip-manifest.d.ts.map +1 -0
  32. package/dist/archive-zip-manifest.js +22 -0
  33. package/dist/archive-zip-names.d.ts +6 -1
  34. package/dist/archive-zip-names.d.ts.map +1 -1
  35. package/dist/archive-zip-names.js +35 -14
  36. package/dist/archive-zip-preflight.d.ts.map +1 -1
  37. package/dist/archive-zip-preflight.js +3 -2
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +9 -4
  40. package/dist/copy-file-input.d.ts +1 -1
  41. package/dist/copy-file-input.d.ts.map +1 -1
  42. package/dist/copy-file-input.js +2 -0
  43. package/dist/darwin-acl.d.ts +4 -0
  44. package/dist/darwin-acl.d.ts.map +1 -0
  45. package/dist/darwin-acl.js +24 -0
  46. package/dist/deny-mutations.d.ts.map +1 -1
  47. package/dist/deny-mutations.js +8 -2
  48. package/dist/directory-durability.d.ts.map +1 -1
  49. package/dist/directory-durability.js +67 -23
  50. package/dist/directory-entry-path.d.ts +3 -0
  51. package/dist/directory-entry-path.d.ts.map +1 -0
  52. package/dist/directory-entry-path.js +21 -0
  53. package/dist/directory-guard.d.ts +17 -1
  54. package/dist/directory-guard.d.ts.map +1 -1
  55. package/dist/directory-guard.js +134 -48
  56. package/dist/directory-mode-node.d.ts +12 -0
  57. package/dist/directory-mode-node.d.ts.map +1 -1
  58. package/dist/directory-mode-node.js +102 -4
  59. package/dist/effective-uid.d.ts +2 -0
  60. package/dist/effective-uid.d.ts.map +1 -0
  61. package/dist/effective-uid.js +25 -0
  62. package/dist/file-handle-transfer.d.ts.map +1 -1
  63. package/dist/file-handle-transfer.js +98 -27
  64. package/dist/file-hash.d.ts.map +1 -1
  65. package/dist/file-hash.js +3 -0
  66. package/dist/file-lock-sync.d.ts.map +1 -1
  67. package/dist/file-lock-sync.js +36 -6
  68. package/dist/file-lock.d.ts.map +1 -1
  69. package/dist/file-lock.js +29 -9
  70. package/dist/file-observation.d.ts +1 -1
  71. package/dist/file-observation.d.ts.map +1 -1
  72. package/dist/file-store-boundary.d.ts +6 -2
  73. package/dist/file-store-boundary.d.ts.map +1 -1
  74. package/dist/file-store-boundary.js +20 -65
  75. package/dist/file-store-copy-source.d.ts +5 -0
  76. package/dist/file-store-copy-source.d.ts.map +1 -0
  77. package/dist/file-store-copy-source.js +31 -0
  78. package/dist/file-store-path.d.ts.map +1 -1
  79. package/dist/file-store-path.js +4 -1
  80. package/dist/file-store-sync-directory.d.ts +16 -0
  81. package/dist/file-store-sync-directory.d.ts.map +1 -0
  82. package/dist/file-store-sync-directory.js +349 -0
  83. package/dist/file-store.d.ts.map +1 -1
  84. package/dist/file-store.js +11 -28
  85. package/dist/filename.d.ts.map +1 -1
  86. package/dist/filename.js +65 -20
  87. package/dist/fs.d.ts.map +1 -1
  88. package/dist/fs.js +3 -2
  89. package/dist/guarded-mkdir.d.ts +8 -0
  90. package/dist/guarded-mkdir.d.ts.map +1 -1
  91. package/dist/guarded-mkdir.js +176 -27
  92. package/dist/guest.d.ts.map +1 -1
  93. package/dist/guest.js +4 -1
  94. package/dist/home-dir.d.ts.map +1 -1
  95. package/dist/home-dir.js +73 -10
  96. package/dist/install-path.d.ts.map +1 -1
  97. package/dist/install-path.js +54 -19
  98. package/dist/json-durable-queue-ownership.d.ts +6 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  100. package/dist/json-durable-queue-ownership.js +89 -42
  101. package/dist/json-durable-queue-paths.d.ts +11 -0
  102. package/dist/json-durable-queue-paths.d.ts.map +1 -0
  103. package/dist/json-durable-queue-paths.js +42 -0
  104. package/dist/json-durable-queue-read.d.ts.map +1 -1
  105. package/dist/json-durable-queue-read.js +2 -0
  106. package/dist/json-durable-queue.d.ts.map +1 -1
  107. package/dist/json-durable-queue.js +50 -58
  108. package/dist/json-store.d.ts.map +1 -1
  109. package/dist/json-store.js +5 -1
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +54 -22
  112. package/dist/local-file-access.d.ts.map +1 -1
  113. package/dist/local-file-access.js +4 -0
  114. package/dist/local-file-descriptor.d.ts +17 -0
  115. package/dist/local-file-descriptor.d.ts.map +1 -0
  116. package/dist/local-file-descriptor.js +84 -0
  117. package/dist/local-roots.d.ts.map +1 -1
  118. package/dist/local-roots.js +35 -7
  119. package/dist/move-path-cleanup.d.ts +2 -0
  120. package/dist/move-path-cleanup.d.ts.map +1 -1
  121. package/dist/move-path-cleanup.js +44 -18
  122. package/dist/move-path.d.ts.map +1 -1
  123. package/dist/move-path.js +31 -6
  124. package/dist/native-binding.d.ts +17 -0
  125. package/dist/native-binding.d.ts.map +1 -1
  126. package/dist/native-binding.js +8 -1
  127. package/dist/native-directory-observation.d.ts +17 -0
  128. package/dist/native-directory-observation.d.ts.map +1 -0
  129. package/dist/native-directory-observation.js +37 -0
  130. package/dist/native-operations.d.ts.map +1 -1
  131. package/dist/native-operations.js +6 -4
  132. package/dist/native-parent-admission.d.ts +32 -0
  133. package/dist/native-parent-admission.d.ts.map +1 -0
  134. package/dist/native-parent-admission.js +127 -0
  135. package/dist/native-pinned-write-windows.d.ts +0 -1
  136. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  137. package/dist/native-pinned-write-windows.js +8 -9
  138. package/dist/native-pinned-write.d.ts.map +1 -1
  139. package/dist/native-pinned-write.js +314 -42
  140. package/dist/native-staged-file.d.ts +4 -4
  141. package/dist/native-staged-file.d.ts.map +1 -1
  142. package/dist/native-staged-file.js +20 -10
  143. package/dist/native.d.ts +1 -1
  144. package/dist/native.d.ts.map +1 -1
  145. package/dist/native.js +7 -2
  146. package/dist/output.d.ts.map +1 -1
  147. package/dist/output.js +15 -5
  148. package/dist/overwrite-file-handle.d.ts.map +1 -1
  149. package/dist/overwrite-file-handle.js +5 -1
  150. package/dist/owner-dacl.d.ts.map +1 -1
  151. package/dist/owner-dacl.js +2 -0
  152. package/dist/path-policy.d.ts.map +1 -1
  153. package/dist/path-policy.js +7 -2
  154. package/dist/path-prefix.d.ts +7 -0
  155. package/dist/path-prefix.d.ts.map +1 -0
  156. package/dist/path-prefix.js +82 -0
  157. package/dist/path-scope-lexical.d.ts.map +1 -1
  158. package/dist/path-scope-lexical.js +18 -8
  159. package/dist/path-segment-route.d.ts +7 -0
  160. package/dist/path-segment-route.d.ts.map +1 -0
  161. package/dist/path-segment-route.js +24 -0
  162. package/dist/path-suffix-aliases.d.ts +10 -0
  163. package/dist/path-suffix-aliases.d.ts.map +1 -0
  164. package/dist/path-suffix-aliases.js +386 -0
  165. package/dist/path.d.ts.map +1 -1
  166. package/dist/path.js +16 -5
  167. package/dist/permissions-windows.d.ts.map +1 -1
  168. package/dist/permissions-windows.js +14 -3
  169. package/dist/permissions.d.ts.map +1 -1
  170. package/dist/permissions.js +37 -8
  171. package/dist/pinned-mutation-admission.d.ts +25 -0
  172. package/dist/pinned-mutation-admission.d.ts.map +1 -0
  173. package/dist/pinned-mutation-admission.js +425 -0
  174. package/dist/pinned-mutation-observation.d.ts +34 -0
  175. package/dist/pinned-mutation-observation.d.ts.map +1 -0
  176. package/dist/pinned-mutation-observation.js +142 -0
  177. package/dist/pinned-mutation-shared-route.d.ts +24 -0
  178. package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
  179. package/dist/pinned-mutation-shared-route.js +70 -0
  180. package/dist/pinned-open.d.ts +6 -0
  181. package/dist/pinned-open.d.ts.map +1 -1
  182. package/dist/pinned-open.js +28 -10
  183. package/dist/pinned-write-types.d.ts +75 -0
  184. package/dist/pinned-write-types.d.ts.map +1 -0
  185. package/dist/pinned-write-types.js +1 -0
  186. package/dist/pinned-write.d.ts +7 -33
  187. package/dist/pinned-write.d.ts.map +1 -1
  188. package/dist/pinned-write.js +151 -17
  189. package/dist/private-directory.d.ts.map +1 -1
  190. package/dist/private-directory.js +2 -0
  191. package/dist/private-producer-handoff.d.ts +16 -0
  192. package/dist/private-producer-handoff.d.ts.map +1 -0
  193. package/dist/private-producer-handoff.js +272 -0
  194. package/dist/private-temp-workspace.d.ts +2 -39
  195. package/dist/private-temp-workspace.d.ts.map +1 -1
  196. package/dist/private-temp-workspace.js +183 -77
  197. package/dist/publish-file.d.ts.map +1 -1
  198. package/dist/publish-file.js +38 -32
  199. package/dist/regular-file.d.ts.map +1 -1
  200. package/dist/regular-file.js +56 -45
  201. package/dist/replace-directory.d.ts.map +1 -1
  202. package/dist/replace-directory.js +12 -5
  203. package/dist/replace-file-temp-owner.d.ts +1 -0
  204. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  205. package/dist/replace-file-temp-owner.js +12 -1
  206. package/dist/replace-file.d.ts.map +1 -1
  207. package/dist/replace-file.js +24 -24
  208. package/dist/root-boundary.d.ts +4 -0
  209. package/dist/root-boundary.d.ts.map +1 -1
  210. package/dist/root-boundary.js +6 -1
  211. package/dist/root-context.d.ts +12 -1
  212. package/dist/root-context.d.ts.map +1 -1
  213. package/dist/root-context.js +57 -11
  214. package/dist/root-directory-creation.d.ts +13 -0
  215. package/dist/root-directory-creation.d.ts.map +1 -0
  216. package/dist/root-directory-creation.js +212 -0
  217. package/dist/root-directory-list.d.ts +16 -4
  218. package/dist/root-directory-list.d.ts.map +1 -1
  219. package/dist/root-directory-list.js +180 -39
  220. package/dist/root-directory.d.ts +23 -0
  221. package/dist/root-directory.d.ts.map +1 -0
  222. package/dist/root-directory.js +141 -0
  223. package/dist/root-errors.d.ts.map +1 -1
  224. package/dist/root-errors.js +12 -1
  225. package/dist/root-file-final-admission.d.ts +21 -0
  226. package/dist/root-file-final-admission.d.ts.map +1 -0
  227. package/dist/root-file-final-admission.js +83 -0
  228. package/dist/root-file.d.ts.map +1 -1
  229. package/dist/root-file.js +103 -24
  230. package/dist/root-impl.d.ts +1 -0
  231. package/dist/root-impl.d.ts.map +1 -1
  232. package/dist/root-impl.js +289 -317
  233. package/dist/root-move-noreplace.d.ts +14 -0
  234. package/dist/root-move-noreplace.d.ts.map +1 -0
  235. package/dist/root-move-noreplace.js +202 -0
  236. package/dist/root-observed-path.d.ts +9 -0
  237. package/dist/root-observed-path.d.ts.map +1 -0
  238. package/dist/root-observed-path.js +95 -0
  239. package/dist/root-path-errors.d.ts +12 -0
  240. package/dist/root-path-errors.d.ts.map +1 -0
  241. package/dist/root-path-errors.js +13 -0
  242. package/dist/root-path-existing.d.ts.map +1 -1
  243. package/dist/root-path-existing.js +53 -20
  244. package/dist/root-path-observation.d.ts +63 -0
  245. package/dist/root-path-observation.d.ts.map +1 -0
  246. package/dist/root-path-observation.js +180 -0
  247. package/dist/root-path-stat.d.ts +5 -0
  248. package/dist/root-path-stat.d.ts.map +1 -0
  249. package/dist/root-path-stat.js +101 -0
  250. package/dist/root-path-symlink.d.ts.map +1 -1
  251. package/dist/root-path-symlink.js +23 -4
  252. package/dist/root-path.d.ts +11 -0
  253. package/dist/root-path.d.ts.map +1 -1
  254. package/dist/root-path.js +215 -53
  255. package/dist/root-paths-lexical.d.ts +9 -0
  256. package/dist/root-paths-lexical.d.ts.map +1 -0
  257. package/dist/root-paths-lexical.js +22 -0
  258. package/dist/root-paths.d.ts +3 -25
  259. package/dist/root-paths.d.ts.map +1 -1
  260. package/dist/root-paths.js +77 -154
  261. package/dist/root-read-admission.d.ts +26 -0
  262. package/dist/root-read-admission.d.ts.map +1 -0
  263. package/dist/root-read-admission.js +94 -0
  264. package/dist/root-remove-identity.d.ts +15 -0
  265. package/dist/root-remove-identity.d.ts.map +1 -0
  266. package/dist/root-remove-identity.js +89 -0
  267. package/dist/root-remove-receipt.d.ts +17 -0
  268. package/dist/root-remove-receipt.d.ts.map +1 -0
  269. package/dist/root-remove-receipt.js +37 -0
  270. package/dist/root-remove.d.ts +2 -1
  271. package/dist/root-remove.d.ts.map +1 -1
  272. package/dist/root-remove.js +172 -10
  273. package/dist/root-write-admission.d.ts +68 -0
  274. package/dist/root-write-admission.d.ts.map +1 -0
  275. package/dist/root-write-admission.js +322 -0
  276. package/dist/root-write-compatibility.d.ts +13 -0
  277. package/dist/root-write-compatibility.d.ts.map +1 -0
  278. package/dist/root-write-compatibility.js +74 -0
  279. package/dist/root-write-complete-parent.d.ts +42 -0
  280. package/dist/root-write-complete-parent.d.ts.map +1 -0
  281. package/dist/root-write-complete-parent.js +195 -0
  282. package/dist/root-write-publication.d.ts +24 -0
  283. package/dist/root-write-publication.d.ts.map +1 -0
  284. package/dist/root-write-publication.js +85 -0
  285. package/dist/root-write-verification.d.ts.map +1 -1
  286. package/dist/root-write-verification.js +6 -4
  287. package/dist/secret-file.d.ts.map +1 -1
  288. package/dist/secret-file.js +62 -24
  289. package/dist/secret-read-async.d.ts.map +1 -1
  290. package/dist/secret-read-async.js +18 -13
  291. package/dist/secret-read-policy.d.ts.map +1 -1
  292. package/dist/secret-read-policy.js +5 -1
  293. package/dist/secure-file.d.ts.map +1 -1
  294. package/dist/secure-file.js +92 -7
  295. package/dist/secure-temp-dir.d.ts +6 -3
  296. package/dist/secure-temp-dir.d.ts.map +1 -1
  297. package/dist/secure-temp-dir.js +121 -101
  298. package/dist/secure-temp-repair.d.ts +35 -0
  299. package/dist/secure-temp-repair.d.ts.map +1 -0
  300. package/dist/secure-temp-repair.js +106 -0
  301. package/dist/sibling-staged-file.d.ts +3 -2
  302. package/dist/sibling-staged-file.d.ts.map +1 -1
  303. package/dist/sibling-staged-file.js +179 -55
  304. package/dist/sibling-temp.d.ts.map +1 -1
  305. package/dist/sibling-temp.js +27 -13
  306. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  307. package/dist/sidecar-lock-acquire.js +44 -14
  308. package/dist/sidecar-lock-root.d.ts.map +1 -1
  309. package/dist/sidecar-lock-root.js +4 -2
  310. package/dist/staged-directory.d.ts +14 -5
  311. package/dist/staged-directory.d.ts.map +1 -1
  312. package/dist/staged-directory.js +54 -3
  313. package/dist/standalone-publication-path.d.ts +2 -0
  314. package/dist/standalone-publication-path.d.ts.map +1 -0
  315. package/dist/standalone-publication-path.js +6 -0
  316. package/dist/stat-observation.d.ts +11 -0
  317. package/dist/stat-observation.d.ts.map +1 -0
  318. package/dist/stat-observation.js +64 -0
  319. package/dist/strict-file-identity.d.ts.map +1 -1
  320. package/dist/strict-file-identity.js +39 -2
  321. package/dist/symlink-parents.d.ts.map +1 -1
  322. package/dist/symlink-parents.js +7 -2
  323. package/dist/temp-target.d.ts +2 -0
  324. package/dist/temp-target.d.ts.map +1 -1
  325. package/dist/temp-target.js +130 -3
  326. package/dist/temp-workspace-admission.d.ts +22 -0
  327. package/dist/temp-workspace-admission.d.ts.map +1 -0
  328. package/dist/temp-workspace-admission.js +374 -0
  329. package/dist/temp-workspace-child-admission.d.ts +13 -0
  330. package/dist/temp-workspace-child-admission.d.ts.map +1 -0
  331. package/dist/temp-workspace-child-admission.js +95 -0
  332. package/dist/temp-workspace-descriptor.d.ts +48 -0
  333. package/dist/temp-workspace-descriptor.d.ts.map +1 -0
  334. package/dist/temp-workspace-descriptor.js +363 -0
  335. package/dist/temp-workspace-identity.d.ts +15 -0
  336. package/dist/temp-workspace-identity.d.ts.map +1 -0
  337. package/dist/temp-workspace-identity.js +41 -0
  338. package/dist/temp-workspace-owner.d.ts +7 -6
  339. package/dist/temp-workspace-owner.d.ts.map +1 -1
  340. package/dist/temp-workspace-owner.js +121 -61
  341. package/dist/temp-workspace-permissions.d.ts +4 -0
  342. package/dist/temp-workspace-permissions.d.ts.map +1 -0
  343. package/dist/temp-workspace-permissions.js +32 -0
  344. package/dist/temp-workspace-types.d.ts +40 -0
  345. package/dist/temp-workspace-types.d.ts.map +1 -0
  346. package/dist/temp-workspace-types.js +1 -0
  347. package/dist/temp.d.ts +1 -1
  348. package/dist/temp.d.ts.map +1 -1
  349. package/dist/temp.js +1 -1
  350. package/dist/test-hooks.d.ts +7 -0
  351. package/dist/test-hooks.d.ts.map +1 -1
  352. package/dist/text-atomic.d.ts.map +1 -1
  353. package/dist/text-atomic.js +3 -1
  354. package/dist/trash.d.ts.map +1 -1
  355. package/dist/trash.js +54 -8
  356. package/dist/walk.d.ts.map +1 -1
  357. package/dist/walk.js +9 -6
  358. package/dist/windows-owner.d.ts.map +1 -1
  359. package/dist/windows-owner.js +5 -0
  360. package/dist/windows-path-alias.d.ts +39 -0
  361. package/dist/windows-path-alias.d.ts.map +1 -0
  362. package/dist/windows-path-alias.js +153 -0
  363. package/docs/advanced.md +25 -0
  364. package/docs/archive.md +23 -2
  365. package/docs/atomic.md +27 -3
  366. package/docs/copy.md +3 -0
  367. package/docs/durability.md +14 -0
  368. package/docs/errors.md +14 -6
  369. package/docs/file-store.md +24 -3
  370. package/docs/filename.md +14 -7
  371. package/docs/install-path.md +2 -2
  372. package/docs/install.md +8 -7
  373. package/docs/json-store.md +5 -1
  374. package/docs/json.md +11 -0
  375. package/docs/mutation-policy-proof.md +65 -0
  376. package/docs/native-helper.md +41 -11
  377. package/docs/native.md +32 -8
  378. package/docs/output.md +33 -11
  379. package/docs/path-prefix.md +64 -0
  380. package/docs/path-suffix-aliases.md +159 -0
  381. package/docs/path.md +11 -0
  382. package/docs/private-file-store.md +14 -0
  383. package/docs/public-api.md +9 -0
  384. package/docs/quickstart.md +1 -1
  385. package/docs/reading.md +8 -1
  386. package/docs/root.md +18 -3
  387. package/docs/secret-file.md +3 -0
  388. package/docs/secure-file.md +6 -2
  389. package/docs/security-model.md +75 -8
  390. package/docs/sidecar-lock.md +28 -1
  391. package/docs/store.md +5 -1
  392. package/docs/temp.md +260 -36
  393. package/docs/test-hooks.md +14 -0
  394. package/docs/writing.md +38 -8
  395. package/package.json +10 -10
@@ -4,22 +4,21 @@ import fsSync, {} from "node:fs";
4
4
  import path from "node:path";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { nativeOpenFlags, removeNativeCreatedFileIfStillPinned, writeNativeInput, } from "./native-operations.js";
7
+ import { captureNativeFdClose } from "./native-binding.js";
7
8
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
8
9
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
9
- export function sameNativeIdentity(left, right) {
10
- return left.dev === right.dev && left.ino === right.ino;
11
- }
12
- function closeWriteFd(fd) {
10
+ function closeWriteFd(closeFd, fd) {
13
11
  if (fd === undefined)
14
12
  return;
15
13
  try {
16
- fsSync.closeSync(fd);
14
+ closeFd(fd);
17
15
  }
18
16
  catch (error) {
19
17
  return error;
20
18
  }
21
19
  }
22
20
  export async function runPinnedWriteWindows(binding, params, root, parentFd, parentGuard) {
21
+ const closeFd = captureNativeFdClose(binding);
23
22
  const parentPath = parentGuard.realPath;
24
23
  let tempFd;
25
24
  let targetFd;
@@ -67,7 +66,7 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
67
66
  syncFileBestEffortSync(targetFd);
68
67
  }
69
68
  catch (error) {
70
- closeWriteFd(targetFd);
69
+ closeWriteFd(closeFd, targetFd);
71
70
  targetFd = undefined;
72
71
  // Staged copies and streamed creates preserve publication for caller recovery.
73
72
  if (params.input.kind === "buffer" ||
@@ -89,8 +88,8 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
89
88
  return { dev: targetIdentity.dev, ino: targetIdentity.ino };
90
89
  }
91
90
  finally {
92
- const targetCloseError = closeWriteFd(targetFd);
93
- const tempCloseError = closeWriteFd(tempFd);
91
+ const targetCloseError = closeWriteFd(closeFd, targetFd);
92
+ const tempCloseError = closeWriteFd(closeFd, tempFd);
94
93
  if (!renamed) {
95
94
  removeNativeCreatedFileIfStillPinned({
96
95
  parentPath,
@@ -99,7 +98,7 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
99
98
  created: tempIdentity,
100
99
  });
101
100
  }
102
- const parentCloseError = closeWriteFd(parentFd);
101
+ const parentCloseError = closeWriteFd(closeFd, parentFd);
103
102
  await root.close().catch(() => undefined);
104
103
  const closeError = targetCloseError ?? tempCloseError ?? parentCloseError;
105
104
  if (completed && closeError !== undefined)
@@ -1 +1 @@
1
- {"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAK3D,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA2FvH"}
1
+ {"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAI3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,KAAK,EAIV,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AAqV3B,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsGvH"}
@@ -50,25 +50,295 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
50
50
  var e = new Error(message);
51
51
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
52
  });
53
- import fsSync from "node:fs";
54
- import fs from "node:fs/promises";
53
+ import fsSync, {} from "node:fs";
55
54
  import path from "node:path";
56
55
  import { inspectDirectoryIdentity } from "./directory-guard.js";
57
56
  import { FsSafeError } from "./errors.js";
58
- import { runPinnedWriteWindows, sameNativeIdentity } from "./native-pinned-write-windows.js";
57
+ import { runPinnedWriteWindows } from "./native-pinned-write-windows.js";
58
+ import { openNativeParentAdmission, openNativeRootAdmission } from "./native-parent-admission.js";
59
59
  import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
60
- import { describeStagedDirectory, exactIdentityMatches } from "./staged-directory.js";
60
+ import { captureNativeFdClose } from "./native-binding.js";
61
+ import { assertPolicyStagedDirectoryCurrent, assertStagedDirectoryCurrent, describePolicyStagedDirectory, describeStagedDirectory, refreshPolicyStagedDirectoryObservation, } from "./staged-directory.js";
61
62
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
62
63
  import { realpathSync } from "./realpath.js";
64
+ import { isNotFoundPathError, isSymlinkOpenError } from "./path.js";
65
+ import { checkedMutationDirectory, } from "./pinned-mutation-observation.js";
66
+ import { createPathSegmentRoute, joinPathSegmentRoute } from "./path-segment-route.js";
67
+ function relativeParentSegments(relativeParentPath) {
68
+ return relativeParentPath.split("/").filter(Boolean);
69
+ }
70
+ function prospectiveTargetPath(parentPath, parentRoute, offset, basename) {
71
+ return joinPathSegmentRoute(parentPath, parentRoute, offset, basename);
72
+ }
73
+ function sameAbsolutePath(left, right) {
74
+ return path.relative(path.resolve(left), path.resolve(right)) === "";
75
+ }
76
+ function assertDirectChildBasename(basename) {
77
+ if (!basename || basename === "." || basename === ".." || basename.includes("/") ||
78
+ basename.includes("\0") || (process.platform === "win32" && basename.includes("\\"))) {
79
+ throw new FsSafeError("invalid-path", "native parent creation requires one direct-child basename");
80
+ }
81
+ }
82
+ function mkdirPolicyChild(binding, parentFd, basename, mode) {
83
+ assertDirectChildBasename(basename);
84
+ const mkdirChild = binding.mkdirChildBeneath;
85
+ if (typeof mkdirChild === "function") {
86
+ const created = mkdirChild.call(binding, parentFd, basename, mode);
87
+ if (typeof created === "boolean")
88
+ return created;
89
+ }
90
+ // Old or malformed optional helpers still execute through the established
91
+ // recursive primitive, but cannot provide exclusive-creation provenance.
92
+ binding.mkdirBeneath(parentFd, basename, mode);
93
+ return false;
94
+ }
95
+ async function authorizePinnedMutation(params, request) {
96
+ return await params.mutationAdmission?.authorize(Object.freeze(request));
97
+ }
98
+ function normalizePolicyParentOpenError(error, params) {
99
+ if (!isSymlinkOpenError(error))
100
+ return error;
101
+ return params.mutationAdmission?.rejectParentSymlinks
102
+ ? new FsSafeError("symlink", "symlink path component not allowed", {
103
+ cause: error instanceof Error ? error : undefined,
104
+ })
105
+ : new FsSafeError("path-mismatch", "native write parent changed during policy admission", {
106
+ cause: error instanceof Error ? error : undefined,
107
+ });
108
+ }
109
+ async function describePosixParent(parentFd, pathname) {
110
+ const parentPath = realpathSync.native(pathname);
111
+ const directory = describeStagedDirectory(parentFd, parentPath);
112
+ const parentPathStat = await inspectDirectoryIdentity(parentPath, inspectFileIdentitySync(() => fsSync.fstatSync(parentFd, { bigint: true })));
113
+ return {
114
+ parentFd,
115
+ parentPath,
116
+ directory,
117
+ parentPathStat,
118
+ observation: checkedMutationDirectory(parentPath, directory.realPath, parentPathStat),
119
+ };
120
+ }
121
+ function describePolicyPosixParent(parentFd, pathname) {
122
+ const policyDirectory = describePolicyStagedDirectory(parentFd, pathname);
123
+ return {
124
+ parentFd,
125
+ parentPath: policyDirectory.directory.realPath,
126
+ directory: policyDirectory.directory,
127
+ parentPathStat: policyDirectory.stat,
128
+ observation: policyDirectory.observation,
129
+ policyDirectory,
130
+ };
131
+ }
132
+ function policyParentCaptureDeopt(error) {
133
+ return error instanceof FsSafeError &&
134
+ (error.code === "path-mismatch" || error.code === "not-file");
135
+ }
136
+ function tryDescribePolicyPosixParent(parentFd, pathname) {
137
+ try {
138
+ return describePolicyPosixParent(parentFd, pathname);
139
+ }
140
+ catch (error) {
141
+ if (!policyParentCaptureDeopt(error))
142
+ throw error;
143
+ return undefined;
144
+ }
145
+ }
146
+ function assertPosixParentCurrent(parent) {
147
+ return parent.policyDirectory
148
+ ? assertPolicyStagedDirectoryCurrent(parent.policyDirectory)
149
+ : assertStagedDirectoryCurrent(parent.directory);
150
+ }
151
+ async function capturePolicyAwarePosixParent(binding, params, rootFd, directoryFlags) {
152
+ const closeFd = captureNativeFdClose(binding);
153
+ const segments = relativeParentSegments(params.relativeParentPath);
154
+ const parentSpelling = segments.length
155
+ ? path.join(params.rootPath, ...segments)
156
+ : params.rootPath;
157
+ const segmentRoute = createPathSegmentRoute(segments);
158
+ const initialTarget = prospectiveTargetPath(params.rootPath, segmentRoute, 0, params.basename);
159
+ let retainedTargetPath = params.mutationAdmission?.beginParentWalk?.();
160
+ if (retainedTargetPath && !sameAbsolutePath(retainedTargetPath, initialTarget)) {
161
+ retainedTargetPath = undefined;
162
+ }
163
+ // Preserve the one-open hot path when the complete parent still exists.
164
+ // Policy is attached only after the opened descriptor is associated with its
165
+ // current canonical pathname, so a contained redirect cannot retain the old
166
+ // preflight authorization.
167
+ let completeParentFd;
168
+ try {
169
+ completeParentFd = binding.openBeneath(rootFd, params.relativeParentPath, directoryFlags).fd;
170
+ }
171
+ catch (error) {
172
+ if (error?.code !== "ENOENT" || !params.mkdir)
173
+ throw error;
174
+ }
175
+ if (completeParentFd !== undefined) {
176
+ const parentFd = completeParentFd;
177
+ try {
178
+ let admitted = retainedTargetPath
179
+ ? tryDescribePolicyPosixParent(parentFd, parentSpelling)
180
+ : undefined;
181
+ if (!admitted) {
182
+ retainedTargetPath = undefined;
183
+ admitted = await describePosixParent(parentFd, parentSpelling);
184
+ }
185
+ const targetPath = path.join(admitted.parentPath, params.basename);
186
+ await authorizePinnedMutation(params, {
187
+ targetPath,
188
+ mutationPath: targetPath,
189
+ phase: "parent",
190
+ });
191
+ assertPosixParentCurrent(admitted);
192
+ return admitted;
193
+ }
194
+ catch (error) {
195
+ closeFd(parentFd);
196
+ throw error;
197
+ }
198
+ }
199
+ // The canonical preflight spelling has no intentional symlinks in its
200
+ // existing prefix. Walk missing-parent cases one direct child at a time so
201
+ // every existing/opened object is authorized and every mkdir is authorized
202
+ // before dispatch. O_NOFOLLOW makes a concurrently introduced link fail
203
+ // before the next component can be created through it.
204
+ const secureDirectoryFlags = directoryFlags | (fsSync.constants.O_NOFOLLOW ?? 0);
205
+ let currentFd = rootFd;
206
+ let currentOwnedFd;
207
+ let currentPath = params.rootPath;
208
+ let current = retainedTargetPath
209
+ ? tryDescribePolicyPosixParent(rootFd, currentPath)
210
+ : undefined;
211
+ if (!current) {
212
+ retainedTargetPath = undefined;
213
+ current = await describePosixParent(rootFd, currentPath);
214
+ }
215
+ try {
216
+ await authorizePinnedMutation(params, {
217
+ targetPath: retainedTargetPath ?? initialTarget,
218
+ mutationPath: retainedTargetPath ?? initialTarget,
219
+ phase: "parent",
220
+ });
221
+ assertPosixParentCurrent(current);
222
+ for (let index = 0; index < segments.length; index += 1) {
223
+ const segment = segments[index];
224
+ const childPath = path.join(currentPath, segment);
225
+ let childFd;
226
+ let createdByMkdir = false;
227
+ let createReceipt;
228
+ try {
229
+ childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
230
+ }
231
+ catch (openError) {
232
+ if (!isNotFoundPathError(openError)) {
233
+ throw normalizePolicyParentOpenError(openError, params);
234
+ }
235
+ const targetPath = retainedTargetPath ?? prospectiveTargetPath(currentPath, segmentRoute, index, params.basename);
236
+ const request = Object.freeze({
237
+ targetPath,
238
+ mutationPath: childPath,
239
+ phase: "parent-create",
240
+ });
241
+ createReceipt = params.mutationAdmission?.tryAuthorizeAtParent?.(request, current.observation);
242
+ if (!createReceipt) {
243
+ createReceipt = await authorizePinnedMutation(params, request);
244
+ assertPosixParentCurrent(current);
245
+ }
246
+ params.assertBeforeMutation?.();
247
+ // Do not carry pathname freshness across the live authority callback.
248
+ assertPosixParentCurrent(current);
249
+ createdByMkdir = mkdirPolicyChild(binding, currentFd, segment, 0o777);
250
+ try {
251
+ childFd = binding.openBeneath(currentFd, segment, secureDirectoryFlags).fd;
252
+ }
253
+ catch (createdOpenError) {
254
+ throw normalizePolicyParentOpenError(createdOpenError, params);
255
+ }
256
+ }
257
+ let child;
258
+ try {
259
+ const acceleratedChild = retainedTargetPath
260
+ ? tryDescribePolicyPosixParent(childFd, childPath)
261
+ : undefined;
262
+ if (acceleratedChild) {
263
+ child = acceleratedChild;
264
+ }
265
+ else {
266
+ retainedTargetPath = undefined;
267
+ child = await describePosixParent(childFd, childPath);
268
+ }
269
+ if (!sameAbsolutePath(child.parentPath, childPath))
270
+ retainedTargetPath = undefined;
271
+ let childAuthorization;
272
+ if (createdByMkdir && createReceipt && params.mutationAdmission?.advanceCreatedDirectory) {
273
+ let evidence;
274
+ try {
275
+ const parent = current.policyDirectory
276
+ ? refreshPolicyStagedDirectoryObservation(current.policyDirectory)
277
+ : checkedMutationDirectory(currentPath, current.directory.realPath, assertStagedDirectoryCurrent(current.directory));
278
+ evidence = Object.freeze({
279
+ admission: createReceipt,
280
+ parent,
281
+ child: child.observation,
282
+ });
283
+ }
284
+ catch {
285
+ // Leave failed evidence to the full ordered admission below.
286
+ }
287
+ if (evidence) {
288
+ childAuthorization = params.mutationAdmission.advanceCreatedDirectory(evidence);
289
+ }
290
+ }
291
+ const targetPath = retainedTargetPath ?? prospectiveTargetPath(child.parentPath, segmentRoute, index + 1, params.basename);
292
+ const request = Object.freeze({
293
+ targetPath,
294
+ mutationPath: targetPath,
295
+ phase: "parent",
296
+ });
297
+ childAuthorization ??= params.mutationAdmission?.tryAuthorizeAtParent?.(request, child.observation);
298
+ if (!childAuthorization) {
299
+ await authorizePinnedMutation(params, request);
300
+ assertPosixParentCurrent(child);
301
+ }
302
+ }
303
+ catch (error) {
304
+ closeFd(childFd);
305
+ throw error;
306
+ }
307
+ const previousOwnedFd = currentOwnedFd;
308
+ currentOwnedFd = child.parentFd;
309
+ currentFd = child.parentFd;
310
+ currentPath = child.parentPath;
311
+ current = child;
312
+ if (previousOwnedFd !== undefined)
313
+ closeFd(previousOwnedFd);
314
+ if (index === segments.length - 1) {
315
+ currentOwnedFd = undefined;
316
+ return child;
317
+ }
318
+ }
319
+ // Empty relative parents are handled by the complete-parent fast path.
320
+ throw new FsSafeError("path-mismatch", "native write parent admission did not complete");
321
+ }
322
+ finally {
323
+ if (currentOwnedFd !== undefined)
324
+ closeFd(currentOwnedFd);
325
+ }
326
+ }
63
327
  export async function runPinnedWriteNative(binding, params) {
64
328
  const env_1 = { stack: [], error: void 0, hasError: false };
65
329
  try {
330
+ const closeFd = captureNativeFdClose(binding);
66
331
  const windows = process.platform === "win32";
67
332
  if (!windows) {
68
333
  assertNativeStaging(binding);
69
334
  }
70
335
  const directoryFlags = fsSync.constants.O_RDONLY | (fsSync.constants.O_DIRECTORY ?? 0);
71
- const root = await fs.open(params.rootPath, directoryFlags);
336
+ const rootAdmission = await openNativeRootAdmission(binding, {
337
+ rootPath: params.rootPath,
338
+ rootIdentity: params.rootIdentity,
339
+ operation: "native write",
340
+ });
341
+ const root = rootAdmission.root;
72
342
  const posixRoot = __addDisposableResource(env_1, windows ? undefined : root, true);
73
343
  let parentFd;
74
344
  let windowsOwnsDirectories = false;
@@ -77,49 +347,42 @@ export async function runPinnedWriteNative(binding, params) {
77
347
  const parentGuard = __addDisposableResource(env_1, {
78
348
  [Symbol.dispose]() {
79
349
  if (!windows && parentFd !== undefined) {
80
- fsSync.closeSync(parentFd);
350
+ closeFd(parentFd);
81
351
  }
82
352
  },
83
353
  }, false);
84
354
  try {
85
- const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
86
- ? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
87
- let rootMatches;
88
- if (exactRoot) {
89
- inspectFileIdentitySync(() => fsSync.fstatSync(root.fd, { bigint: true }), exactRoot);
90
- rootMatches = true;
91
- }
92
- else if (windows) {
93
- const identity = binding.fstatIdentity(root.fd);
94
- rootMatches = !params.rootIdentity || sameNativeIdentity(params.rootIdentity, identity);
355
+ let parentPath;
356
+ let directory;
357
+ let parentPathStat;
358
+ let policyParentAdmitted = false;
359
+ if (!windows && params.mutationAdmission) {
360
+ const admitted = await capturePolicyAwarePosixParent(binding, params, root.fd, directoryFlags);
361
+ parentFd = admitted.parentFd;
362
+ parentPath = admitted.parentPath;
363
+ directory = admitted.directory;
364
+ parentPathStat = admitted.parentPathStat;
365
+ policyParentAdmitted = true;
95
366
  }
96
367
  else {
97
- const identity = fsSync.fstatSync(root.fd, { bigint: true });
98
- rootMatches = !params.rootIdentity || exactIdentityMatches(params.rootIdentity, identity);
99
- }
100
- if (!rootMatches) {
101
- throw new FsSafeError("path-mismatch", "root path changed during native write");
102
- }
103
- if (params.mkdir) {
104
- params.assertBeforeMutation?.();
105
- binding.mkdirBeneath(root.fd, params.relativeParentPath, 0o777);
106
- }
107
- parentFd = binding.openBeneath(root.fd, params.relativeParentPath, directoryFlags).fd;
108
- const parentPath = realpathSync.native(params.relativeParentPath
109
- ? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
110
- : params.rootPath);
111
- const directory = windows ? undefined : describeStagedDirectory(parentFd, parentPath);
112
- const parentPathStat = exactRoot
113
- ? await inspectDirectoryIdentity(parentPath, inspectFileIdentitySync(() => fsSync.fstatSync(parentFd, { bigint: true })))
114
- : fsSync.lstatSync(parentPath);
115
- if (windows && !exactRoot) {
116
- const parentIdentity = binding.fstatIdentity(parentFd);
117
- if (parentPathStat.isSymbolicLink() || !sameNativeIdentity(parentPathStat, parentIdentity)) {
118
- throw new FsSafeError("path-mismatch", "native write parent changed during resolution");
368
+ if (params.mkdir) {
369
+ params.assertBeforeMutation?.();
370
+ binding.mkdirBeneath(root.fd, params.relativeParentPath, 0o777);
119
371
  }
372
+ const admitted = await openNativeParentAdmission(binding, rootAdmission, params.relativeParentPath);
373
+ parentFd = admitted.fd;
374
+ parentPath = admitted.guard.realPath;
375
+ directory = admitted.stagedDirectory;
376
+ parentPathStat = admitted.guard.stat;
120
377
  }
121
- else if (!windows && (parentPathStat.isSymbolicLink() || !exactIdentityMatches(parentPathStat, directory.identity))) {
122
- throw new FsSafeError("path-mismatch", "native write parent changed during resolution");
378
+ if (!policyParentAdmitted && params.mutationAdmission) {
379
+ const targetPath = path.join(parentPath, params.basename);
380
+ await authorizePinnedMutation(params, {
381
+ targetPath,
382
+ mutationPath: targetPath,
383
+ phase: "parent",
384
+ });
385
+ await inspectDirectoryIdentity(parentPath, inspectFileIdentitySync(() => fsSync.fstatSync(parentFd, { bigint: true })));
123
386
  }
124
387
  const verificationGuard = { dir: parentPath, realPath: parentPath, stat: parentPathStat };
125
388
  if (params.overwrite === false) {
@@ -140,12 +403,21 @@ export async function runPinnedWriteNative(binding, params) {
140
403
  }
141
404
  const ownedParent = parentFd;
142
405
  parentFd = undefined;
143
- return await writeNativeStage(binding, ownedParent, directory, params, verificationGuard);
406
+ return await writeNativeStage(binding, ownedParent, closeFd, directory, params, verificationGuard);
144
407
  }
145
408
  finally {
146
409
  if (windows && !windowsOwnsDirectories) {
147
410
  if (parentFd !== undefined) {
148
- fsSync.closeSync(parentFd);
411
+ // Match the Windows leaf cleanup contract: an admission or identity
412
+ // failure remains primary, while every owned directory is still given
413
+ // a close attempt. In particular, a parent close failure must not skip
414
+ // the root FileHandle close.
415
+ try {
416
+ closeFd(parentFd);
417
+ }
418
+ catch {
419
+ // Best effort while propagating the operation failure.
420
+ }
149
421
  }
150
422
  await root.close().catch(() => undefined);
151
423
  }
@@ -1,16 +1,16 @@
1
1
  import type { DirectoryReceipt } from "./directory-durability.js";
2
2
  import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
3
  import type { FileIdentityStat } from "./file-identity.js";
4
- import type { NativeBinding } from "./native-binding.js";
4
+ import { type NativeBinding } from "./native-binding.js";
5
5
  import type { PinnedWriteInput, PinnedWriteParams } from "./pinned-write.js";
6
6
  import type { PublishedFileReceipt, StagedFile, StagedFileCleanupReceipt, StagedFileReceipt } from "./staged-file-types.js";
7
7
  export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding, "createStagedFile" | "stagedFileMatches" | "removeStagedFile">>;
8
8
  export declare function assertNativeStaging(binding: NativeBinding): asserts binding is NativeStagingBinding;
9
9
  declare class NativeStagedFile implements StagedFile {
10
10
  #private;
11
- constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
12
- static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
13
- static write(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
11
+ constructor(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
12
+ static create(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
13
+ static write(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
14
14
  get receipt(): StagedFileReceipt;
15
15
  assertCurrent(): Promise<void>;
16
16
  publish(basename: string, options: {
@@ -1 +1 @@
1
- {"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAMlE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG7E,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,wBAAwB,EAGxB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,QAAQ,CAAC,IAAI,CAC9D,aAAa,EACb,kBAAkB,GAAG,mBAAmB,GAAG,kBAAkB,CAC9D,CAAC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,IAAI,oBAAoB,CAUnG;AA6BD,cAAM,gBAAiB,YAAW,UAAU;;IAa1C,YACE,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,OAAO,EACb,oBAAoB,CAAC,EAAE,MAAM,IAAI,EASlC;IAGD,OAAa,MAAM,CACjB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EAEjB,aAAa,UAAO,EACpB,IAAI,UAAO,EACX,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,OAAa,KAAK,CAChB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,IAAI,OAAO,IAAI,iBAAiB,CAK/B;IA8FK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAEK,OAAO,CACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,EAC/B,WAAW,CAAC,EAAE,iBAAiB,CAAC,aAAa,CAAC,GAC7C,OAAO,CAAC,oBAAoB,CAAC,CAyD/B;IA6CK,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAMjD;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3C;CACF;AAED,eAAO,MAAM,iBAAiB,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,UAAU,CAClF,CAAC;AAC1B,eAAO,MAAM,gBAAgB,+BAAyB,CAAC;AAEvD,YAAY,EACV,oBAAoB,EAAE,UAAU,EAAE,wBAAwB,EAAE,wBAAwB,EACpF,qBAAqB,EAAE,iBAAiB,GACzC,MAAM,wBAAwB,CAAC;AAEhC,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACrC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,UAAU,CAAC,CAUtB"}
1
+ {"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAMlE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAG/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG7E,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,wBAAwB,EAGxB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,QAAQ,CAAC,IAAI,CAC9D,aAAa,EACb,kBAAkB,GAAG,mBAAmB,GAAG,kBAAkB,CAC9D,CAAC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,IAAI,oBAAoB,CAWnG;AA6BD,cAAM,gBAAiB,YAAW,UAAU;;IAe1C,YACE,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,EACnC,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,OAAO,EACb,oBAAoB,CAAC,EAAE,MAAM,IAAI,EAWlC;IAID,OAAa,MAAM,CACjB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,EACnC,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EAEjB,aAAa,UAAO,EACpB,IAAI,UAAO,EACX,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,OAAa,KAAK,CAChB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,EACnC,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,IAAI,OAAO,IAAI,iBAAiB,CAK/B;IA8FK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAEK,OAAO,CACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,EAC/B,WAAW,CAAC,EAAE,iBAAiB,CAAC,aAAa,CAAC,GAC7C,OAAO,CAAC,oBAAoB,CAAC,CAyD/B;IAgDK,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAMjD;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3C;CACF;AAED,eAAO,MAAM,iBAAiB,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,UAAU,CAClF,CAAC;AAC1B,eAAO,MAAM,gBAAgB,+BAAyB,CAAC;AAEvD,YAAY,EACV,oBAAoB,EAAE,UAAU,EAAE,wBAAwB,EAAE,wBAAwB,EACpF,qBAAqB,EAAE,iBAAiB,GACzC,MAAM,wBAAwB,CAAC;AAEhC,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACrC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,UAAU,CAAC,CAUtB"}
@@ -57,12 +57,14 @@ import fs from "node:fs";
57
57
  import path from "node:path";
58
58
  import { FsSafeError } from "./errors.js";
59
59
  import { MutationAuthorityError } from "./mutation-authority.js";
60
+ import { captureNativeFdClose } from "./native-binding.js";
60
61
  import { writeNativeInput } from "./native-operations.js";
61
62
  import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
62
63
  import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
63
64
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
64
65
  export function assertNativeStaging(binding) {
65
66
  if ([
67
+ binding.closeOwnedFd,
66
68
  binding.createStagedFile,
67
69
  binding.stagedFileMatches,
68
70
  binding.removeStagedFile,
@@ -92,7 +94,9 @@ function failure(error, details) {
92
94
  }
93
95
  class NativeStagedFile {
94
96
  #binding;
97
+ #closeFd;
95
98
  #parentFd;
99
+ #closeParentFd;
96
100
  #directory;
97
101
  #portableNames;
98
102
  #publishedMode;
@@ -102,26 +106,29 @@ class NativeStagedFile {
102
106
  #state = { status: "open", publication: NOT_PUBLISHED };
103
107
  #receipt;
104
108
  #rejectFinalSymlink = false;
105
- constructor(binding, parentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation) {
109
+ constructor(binding, parentFd, closeParentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation) {
110
+ this.#closeFd = captureNativeFdClose(binding);
106
111
  this.#binding = binding;
107
112
  this.#parentFd = parentFd;
113
+ this.#closeParentFd = closeParentFd;
108
114
  this.#directory = directory;
109
115
  this.#portableNames = portableNames;
110
116
  this.#publishedMode = publishedMode;
111
117
  this.#sync = sync;
112
118
  this.#assertBeforeMutation = assertBeforeMutation;
113
119
  }
114
- // Takes ownership of parentFd, including all construction and preparation failures.
115
- static async create(binding, parentFd, directory, input, mode, maxBytes,
120
+ // Public staging supplies a Node parent; pinned writes supply a native parent.
121
+ // The supplied closer owns parentFd even on construction or preparation failure.
122
+ static async create(binding, parentFd, closeParentFd, directory, input, mode, maxBytes,
116
123
  // Public staging uses portable names; existing POSIX writes accept literal names.
117
124
  portableNames = true, sync = true, assertBeforeMutation) {
118
125
  let staged;
119
126
  try {
120
- staged = new NativeStagedFile(binding, parentFd, directory, portableNames, mode, sync, assertBeforeMutation);
127
+ staged = new NativeStagedFile(binding, parentFd, closeParentFd, directory, portableNames, mode, sync, assertBeforeMutation);
121
128
  }
122
129
  catch (error) {
123
130
  try {
124
- fs.closeSync(parentFd);
131
+ closeParentFd(parentFd);
125
132
  }
126
133
  catch (closeError) {
127
134
  throw new AggregateError([error, closeError], "staged owner construction and close failed");
@@ -131,12 +138,12 @@ class NativeStagedFile {
131
138
  await staged.#prepare(input, maxBytes);
132
139
  return staged;
133
140
  }
134
- static async write(binding, parentFd, directory, params, parentGuard) {
141
+ static async write(binding, parentFd, closeParentFd, directory, params, parentGuard) {
135
142
  const env_1 = { stack: [], error: void 0, hasError: false };
136
143
  try {
137
144
  // This owner never escapes. Only the internal verifier borrows its fd;
138
145
  // public descriptor methods remain await-free and cannot race disposal.
139
- const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation), true);
146
+ const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, closeParentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation), true);
140
147
  staged.#rejectFinalSymlink = params.rejectFinalSymlink === true;
141
148
  if (params.input.kind === "file")
142
149
  await params.input.verifySource();
@@ -336,12 +343,15 @@ class NativeStagedFile {
336
343
  }
337
344
  }
338
345
  let resources = "closed";
339
- for (const fd of [state.fileFd, this.#parentFd]) {
346
+ for (const [fd, closeFd] of [
347
+ [state.fileFd, this.#closeFd],
348
+ [this.#parentFd, this.#closeParentFd],
349
+ ]) {
340
350
  if (fd === undefined) {
341
351
  continue;
342
352
  }
343
353
  try {
344
- fs.closeSync(fd);
354
+ closeFd(fd);
345
355
  }
346
356
  catch (error) {
347
357
  resources = "close-failed";
@@ -385,5 +395,5 @@ export async function stageFileInDirectory(options) {
385
395
  const input = { kind: "buffer", data: Buffer.from(options.content) };
386
396
  const mode = options.mode ?? 0o600;
387
397
  const parent = openStagedDirectory(options.directory);
388
- return await createNativeStage(binding, parent.fd, parent.receipt, input, mode);
398
+ return await createNativeStage(binding, parent.fd, fs.closeSync, parent.receipt, input, mode);
389
399
  }
package/dist/native.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { NativeBinding } from "./native-binding.js";
1
+ import { type NativeBinding } from "./native-binding.js";
2
2
  export type { NativeBinding } from "./native-binding.js";
3
3
  export declare function __loadBundledNativeForTest(): NativeBinding;
4
4
  export declare function __nativeLoaderDetectorsForTest(): {
@@ -1 +1 @@
1
- {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAsKzD,wBAAgB,0BAA0B,IAAI,aAAa,CAE1D;AAED,wBAAgB,8BAA8B,IAAI;IAChD,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,UAAU,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,cAAc,EAAE,OAAO,GAAG,SAAS,CAAC;CACrC,CAMA;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EACzB,IAAI,EAAE,MAAM,EACZ,IAAI,UAAQ,GACX,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,SAAS,CAkB5D;AAED,wBAAgB,oBAAoB,IAAI,aAAa,CAQpD;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,aAAa,GAAG,IAAI,CAK1E;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAKjD"}
1
+ {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAQA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAG/E,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAwKzD,wBAAgB,0BAA0B,IAAI,aAAa,CAE1D;AAED,wBAAgB,8BAA8B,IAAI;IAChD,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,UAAU,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC,cAAc,EAAE,OAAO,GAAG,SAAS,CAAC;CACrC,CAMA;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EACzB,IAAI,EAAE,MAAM,EACZ,IAAI,UAAQ,GACX,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,SAAS,CAoB5D;AAED,wBAAgB,oBAAoB,IAAI,aAAa,CAQpD;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,aAAa,GAAG,IAAI,CAK1E;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAKjD"}
package/dist/native.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { closeSync, openSync, readSync, readdirSync, } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { FsSafeError } from "./errors.js";
4
+ import { captureNativeFdClose } from "./native-binding.js";
4
5
  import { getFsSafeNativeConfig } from "./native-config.js";
5
6
  const require = createRequire(import.meta.url);
6
7
  let binding;
@@ -144,7 +145,9 @@ function loadBundledBinding() {
144
145
  if (!target) {
145
146
  throw new Error(`Unsupported OS or architecture: ${process.platform}-${process.arch}`);
146
147
  }
147
- return require(nativePackageForTarget(target));
148
+ const loaded = require(nativePackageForTarget(target));
149
+ captureNativeFdClose(loaded);
150
+ return loaded;
148
151
  }
149
152
  export function __loadBundledNativeForTest() {
150
153
  return loadBundledBinding();
@@ -166,7 +169,9 @@ export function getNativeBinding() {
166
169
  if (!attempted) {
167
170
  attempted = true;
168
171
  try {
169
- binding = loadBinding();
172
+ const loaded = loadBinding();
173
+ captureNativeFdClose(loaded);
174
+ binding = loaded;
170
175
  }
171
176
  catch (error) {
172
177
  loadError = error;
@@ -1 +1 @@
1
- {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CAiDrC"}
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CA2DrC"}