@openclaw/fs-safe 0.14.0 → 0.16.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 (370) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +38 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +2 -1
  6. package/dist/archive-kind.d.ts +0 -1
  7. package/dist/archive-kind.d.ts.map +1 -1
  8. package/dist/archive-kind.js +5 -17
  9. package/dist/archive-merge.d.ts.map +1 -1
  10. package/dist/archive-merge.js +113 -46
  11. package/dist/archive-parser.wasm +0 -0
  12. package/dist/archive-read.d.ts.map +1 -1
  13. package/dist/archive-read.js +10 -11
  14. package/dist/archive-tar-stream.d.ts +3 -0
  15. package/dist/archive-tar-stream.d.ts.map +1 -1
  16. package/dist/archive-tar-stream.js +56 -37
  17. package/dist/archive-tar-wasm.d.ts +16 -4
  18. package/dist/archive-tar-wasm.d.ts.map +1 -1
  19. package/dist/archive-tar-wasm.js +134 -34
  20. package/dist/archive-zip-directory.d.ts +4 -0
  21. package/dist/archive-zip-directory.d.ts.map +1 -1
  22. package/dist/archive-zip-directory.js +2 -0
  23. package/dist/archive-zip-entry.d.ts +6 -2
  24. package/dist/archive-zip-entry.d.ts.map +1 -1
  25. package/dist/archive-zip-entry.js +23 -8
  26. package/dist/archive-zip-integrity.d.ts.map +1 -1
  27. package/dist/archive-zip-integrity.js +3 -4
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +107 -31
  30. package/dist/archive-zip-names.d.ts +1 -0
  31. package/dist/archive-zip-names.d.ts.map +1 -1
  32. package/dist/archive-zip-names.js +6 -0
  33. package/dist/archive.d.ts.map +1 -1
  34. package/dist/archive.js +11 -11
  35. package/dist/bounded-read-stream.d.ts +0 -1
  36. package/dist/bounded-read-stream.d.ts.map +1 -1
  37. package/dist/bounded-read-stream.js +0 -6
  38. package/dist/clone-metadata.d.ts +1 -0
  39. package/dist/clone-metadata.d.ts.map +1 -1
  40. package/dist/clone-metadata.js +6 -2
  41. package/dist/copy-publication.d.ts +6 -0
  42. package/dist/copy-publication.d.ts.map +1 -1
  43. package/dist/copy-publication.js +3 -0
  44. package/dist/copy-tree-portable.d.ts.map +1 -1
  45. package/dist/copy-tree-portable.js +44 -24
  46. package/dist/copy.d.ts.map +1 -1
  47. package/dist/copy.js +29 -11
  48. package/dist/create-directory.d.ts +20 -0
  49. package/dist/create-directory.d.ts.map +1 -0
  50. package/dist/create-directory.js +130 -0
  51. package/dist/create-file-async.d.ts +7 -0
  52. package/dist/create-file-async.d.ts.map +1 -0
  53. package/dist/create-file-async.js +121 -0
  54. package/dist/create-file.d.ts +8 -0
  55. package/dist/create-file.d.ts.map +1 -0
  56. package/dist/create-file.js +190 -0
  57. package/dist/create-owned-file.d.ts +8 -0
  58. package/dist/create-owned-file.d.ts.map +1 -0
  59. package/dist/create-owned-file.js +16 -0
  60. package/dist/create.d.ts +4 -0
  61. package/dist/create.d.ts.map +1 -0
  62. package/dist/create.js +2 -0
  63. package/dist/creation-darwin.d.ts +7 -0
  64. package/dist/creation-darwin.d.ts.map +1 -0
  65. package/dist/creation-darwin.js +79 -0
  66. package/dist/creation-file-state.d.ts +19 -0
  67. package/dist/creation-file-state.d.ts.map +1 -0
  68. package/dist/creation-file-state.js +118 -0
  69. package/dist/creation-path.d.ts +21 -0
  70. package/dist/creation-path.d.ts.map +1 -0
  71. package/dist/creation-path.js +71 -0
  72. package/dist/creation-permissions.d.ts +19 -0
  73. package/dist/creation-permissions.d.ts.map +1 -0
  74. package/dist/creation-permissions.js +125 -0
  75. package/dist/directory-durability.d.ts +1 -1
  76. package/dist/directory-durability.d.ts.map +1 -1
  77. package/dist/directory-durability.js +22 -80
  78. package/dist/directory-guard.d.ts +3 -0
  79. package/dist/directory-guard.d.ts.map +1 -1
  80. package/dist/directory-mode-node.d.ts +2 -0
  81. package/dist/directory-mode-node.d.ts.map +1 -1
  82. package/dist/directory-mode-node.js +8 -0
  83. package/dist/directory-mode-owner.js +5 -5
  84. package/dist/directory-receipt.d.ts +24 -0
  85. package/dist/directory-receipt.d.ts.map +1 -0
  86. package/dist/directory-receipt.js +127 -0
  87. package/dist/file-cleanup.d.ts +19 -0
  88. package/dist/file-cleanup.d.ts.map +1 -0
  89. package/dist/file-cleanup.js +78 -0
  90. package/dist/file-handle-transfer.d.ts +2 -0
  91. package/dist/file-handle-transfer.d.ts.map +1 -1
  92. package/dist/file-handle-transfer.js +57 -2
  93. package/dist/file-identity.d.ts.map +1 -1
  94. package/dist/file-identity.js +18 -4
  95. package/dist/file-lock-sync-admission.d.ts +19 -0
  96. package/dist/file-lock-sync-admission.d.ts.map +1 -0
  97. package/dist/file-lock-sync-admission.js +93 -0
  98. package/dist/file-lock-sync-root-acquire.d.ts +4 -0
  99. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
  100. package/dist/file-lock-sync-root-acquire.js +370 -0
  101. package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
  102. package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
  103. package/dist/file-lock-sync-root-arbitration.js +66 -0
  104. package/dist/file-lock-sync-root-held.d.ts +34 -0
  105. package/dist/file-lock-sync-root-held.d.ts.map +1 -0
  106. package/dist/file-lock-sync-root-held.js +393 -0
  107. package/dist/file-lock-sync-root-io.d.ts +44 -0
  108. package/dist/file-lock-sync-root-io.d.ts.map +1 -0
  109. package/dist/file-lock-sync-root-io.js +209 -0
  110. package/dist/file-lock-sync-root-mutation.d.ts +17 -0
  111. package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
  112. package/dist/file-lock-sync-root-mutation.js +277 -0
  113. package/dist/file-lock-sync-root-options.d.ts +20 -0
  114. package/dist/file-lock-sync-root-options.d.ts.map +1 -0
  115. package/dist/file-lock-sync-root-options.js +58 -0
  116. package/dist/file-lock-sync-root-registration.d.ts +2 -0
  117. package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
  118. package/dist/file-lock-sync-root-registration.js +90 -0
  119. package/dist/file-lock-sync-root.d.ts +36 -0
  120. package/dist/file-lock-sync-root.d.ts.map +1 -0
  121. package/dist/file-lock-sync-root.js +361 -0
  122. package/dist/file-lock-sync-stale-admission.d.ts +24 -0
  123. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
  124. package/dist/file-lock-sync-stale-admission.js +205 -0
  125. package/dist/file-lock-sync.d.ts.map +1 -1
  126. package/dist/file-lock-sync.js +245 -205
  127. package/dist/file-observation.d.ts +1 -1
  128. package/dist/file-observation.d.ts.map +1 -1
  129. package/dist/file-store-boundary.d.ts +2 -6
  130. package/dist/file-store-boundary.d.ts.map +1 -1
  131. package/dist/file-store-boundary.js +3 -9
  132. package/dist/file-store-prune.d.ts.map +1 -1
  133. package/dist/file-store-prune.js +5 -1
  134. package/dist/file-store-sync-write.d.ts.map +1 -1
  135. package/dist/file-store-sync-write.js +5 -8
  136. package/dist/file-store.d.ts.map +1 -1
  137. package/dist/file-store.js +47 -12
  138. package/dist/guarded-mkdir.d.ts +1 -0
  139. package/dist/guarded-mkdir.d.ts.map +1 -1
  140. package/dist/guarded-mkdir.js +36 -7
  141. package/dist/json-document-store.d.ts.map +1 -1
  142. package/dist/json-document-store.js +22 -15
  143. package/dist/json-durable-queue-ownership.d.ts +0 -1
  144. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  145. package/dist/json-durable-queue-ownership.js +0 -6
  146. package/dist/move-path.js +1 -1
  147. package/dist/native-binding.d.ts +13 -1
  148. package/dist/native-binding.d.ts.map +1 -1
  149. package/dist/native-fallback-warning.d.ts +4 -0
  150. package/dist/native-fallback-warning.d.ts.map +1 -0
  151. package/dist/native-fallback-warning.js +11 -0
  152. package/dist/native-operations.d.ts +0 -2
  153. package/dist/native-operations.d.ts.map +1 -1
  154. package/dist/native-operations.js +0 -24
  155. package/dist/native-parent-admission.d.ts +5 -2
  156. package/dist/native-parent-admission.d.ts.map +1 -1
  157. package/dist/native-parent-admission.js +27 -7
  158. package/dist/native-pinned-write-windows.d.ts +1 -1
  159. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  160. package/dist/native-pinned-write-windows.js +174 -29
  161. package/dist/native-pinned-write.d.ts.map +1 -1
  162. package/dist/native-pinned-write.js +26 -14
  163. package/dist/native-policy-parent-windows.d.ts +14 -0
  164. package/dist/native-policy-parent-windows.d.ts.map +1 -0
  165. package/dist/native-policy-parent-windows.js +209 -0
  166. package/dist/native-rename-outcome.d.ts +4 -0
  167. package/dist/native-rename-outcome.d.ts.map +1 -0
  168. package/dist/native-rename-outcome.js +8 -0
  169. package/dist/native-staged-file.d.ts +3 -2
  170. package/dist/native-staged-file.d.ts.map +1 -1
  171. package/dist/native-staged-file.js +121 -72
  172. package/dist/output.d.ts.map +1 -1
  173. package/dist/output.js +12 -8
  174. package/dist/owner-dacl.d.ts.map +1 -1
  175. package/dist/owner-dacl.js +10 -4
  176. package/dist/path-prefix.d.ts.map +1 -1
  177. package/dist/path-prefix.js +30 -8
  178. package/dist/path-suffix-aliases.d.ts +2 -0
  179. package/dist/path-suffix-aliases.d.ts.map +1 -1
  180. package/dist/path-suffix-aliases.js +25 -17
  181. package/dist/permission-exec.d.ts +2 -0
  182. package/dist/permission-exec.d.ts.map +1 -1
  183. package/dist/permission-exec.js +150 -21
  184. package/dist/permissions-windows.js +1 -1
  185. package/dist/pinned-mutation-admission.d.ts.map +1 -1
  186. package/dist/pinned-mutation-admission.js +10 -5
  187. package/dist/pinned-mutation-observation.d.ts +0 -1
  188. package/dist/pinned-mutation-observation.d.ts.map +1 -1
  189. package/dist/pinned-mutation-observation.js +0 -19
  190. package/dist/pinned-mutation-shared-route.d.ts +1 -0
  191. package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
  192. package/dist/pinned-mutation-shared-route.js +1 -1
  193. package/dist/pinned-write-input.d.ts +4 -0
  194. package/dist/pinned-write-input.d.ts.map +1 -0
  195. package/dist/pinned-write-input.js +25 -0
  196. package/dist/pinned-write-mode.d.ts +5 -0
  197. package/dist/pinned-write-mode.d.ts.map +1 -0
  198. package/dist/pinned-write-mode.js +24 -0
  199. package/dist/pinned-write-staged.d.ts +6 -0
  200. package/dist/pinned-write-staged.d.ts.map +1 -0
  201. package/dist/pinned-write-staged.js +187 -0
  202. package/dist/pinned-write-types.d.ts +5 -0
  203. package/dist/pinned-write-types.d.ts.map +1 -1
  204. package/dist/pinned-write.d.ts.map +1 -1
  205. package/dist/pinned-write.js +35 -145
  206. package/dist/private-directory.d.ts.map +1 -1
  207. package/dist/private-directory.js +18 -4
  208. package/dist/private-producer-handoff-sync.d.ts +14 -0
  209. package/dist/private-producer-handoff-sync.d.ts.map +1 -0
  210. package/dist/private-producer-handoff-sync.js +114 -0
  211. package/dist/private-producer-handoff.d.ts +22 -4
  212. package/dist/private-producer-handoff.d.ts.map +1 -1
  213. package/dist/private-producer-handoff.js +140 -77
  214. package/dist/private-temp-workspace.d.ts.map +1 -1
  215. package/dist/private-temp-workspace.js +75 -121
  216. package/dist/publish-copy-stage.d.ts +2 -1
  217. package/dist/publish-copy-stage.d.ts.map +1 -1
  218. package/dist/publish-copy-stage.js +16 -7
  219. package/dist/publish-file.d.ts.map +1 -1
  220. package/dist/publish-file.js +2 -2
  221. package/dist/regular-file.d.ts.map +1 -1
  222. package/dist/regular-file.js +1 -15
  223. package/dist/replace-directory.d.ts.map +1 -1
  224. package/dist/replace-directory.js +256 -18
  225. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  226. package/dist/replace-file-copy-fallback.js +62 -70
  227. package/dist/replace-file-copy-source.d.ts.map +1 -1
  228. package/dist/replace-file-copy-source.js +10 -12
  229. package/dist/replace-file-temp-owner.d.ts +5 -9
  230. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  231. package/dist/replace-file-temp-owner.js +56 -72
  232. package/dist/replace-file.js +6 -6
  233. package/dist/retained-directory-replacement.d.ts +26 -0
  234. package/dist/retained-directory-replacement.d.ts.map +1 -0
  235. package/dist/retained-directory-replacement.js +193 -0
  236. package/dist/root-boundary.d.ts +1 -0
  237. package/dist/root-boundary.d.ts.map +1 -1
  238. package/dist/root-boundary.js +4 -0
  239. package/dist/root-context.d.ts +0 -8
  240. package/dist/root-context.d.ts.map +1 -1
  241. package/dist/root-context.js +0 -3
  242. package/dist/root-create-input.d.ts +8 -1
  243. package/dist/root-create-input.d.ts.map +1 -1
  244. package/dist/root-create-input.js +17 -4
  245. package/dist/root-directory-creation.d.ts +3 -3
  246. package/dist/root-directory-creation.d.ts.map +1 -1
  247. package/dist/root-directory-creation.js +15 -3
  248. package/dist/root-directory-list.d.ts +1 -0
  249. package/dist/root-directory-list.d.ts.map +1 -1
  250. package/dist/root-directory-list.js +1 -0
  251. package/dist/root-impl.d.ts.map +1 -1
  252. package/dist/root-impl.js +46 -17
  253. package/dist/root-move-noreplace.d.ts.map +1 -1
  254. package/dist/root-move-noreplace.js +24 -15
  255. package/dist/root-options.d.ts +12 -4
  256. package/dist/root-options.d.ts.map +1 -1
  257. package/dist/root-path-errors.d.ts +1 -0
  258. package/dist/root-path-errors.d.ts.map +1 -1
  259. package/dist/root-path-errors.js +11 -2
  260. package/dist/root-path-existing.d.ts.map +1 -1
  261. package/dist/root-path-existing.js +11 -35
  262. package/dist/root-path-stat.d.ts.map +1 -1
  263. package/dist/root-path-stat.js +59 -7
  264. package/dist/root-path.js +1 -13
  265. package/dist/root-remove.d.ts +1 -0
  266. package/dist/root-remove.d.ts.map +1 -1
  267. package/dist/root-remove.js +4 -0
  268. package/dist/root-walk.d.ts +1 -1
  269. package/dist/root-walk.d.ts.map +1 -1
  270. package/dist/root-walk.js +17 -2
  271. package/dist/root-write-admission.d.ts +0 -2
  272. package/dist/root-write-admission.d.ts.map +1 -1
  273. package/dist/root-write-admission.js +1 -15
  274. package/dist/root-write-complete-parent.d.ts.map +1 -1
  275. package/dist/root-write-complete-parent.js +7 -23
  276. package/dist/root-write-publication.js +1 -1
  277. package/dist/root-write-verification.d.ts.map +1 -1
  278. package/dist/root-write-verification.js +29 -42
  279. package/dist/secret-file.d.ts.map +1 -1
  280. package/dist/secret-file.js +3 -24
  281. package/dist/secret-read-async.d.ts.map +1 -1
  282. package/dist/secret-read-async.js +3 -24
  283. package/dist/secret-read-policy.d.ts +6 -2
  284. package/dist/secret-read-policy.d.ts.map +1 -1
  285. package/dist/secret-read-policy.js +26 -2
  286. package/dist/secure-file-windows.d.ts +6 -0
  287. package/dist/secure-file-windows.d.ts.map +1 -1
  288. package/dist/secure-file-windows.js +34 -117
  289. package/dist/secure-file.js +2 -2
  290. package/dist/sibling-temp.d.ts.map +1 -1
  291. package/dist/sibling-temp.js +23 -12
  292. package/dist/sidecar-lock-acquire.d.ts +2 -28
  293. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  294. package/dist/sidecar-lock-acquire.js +288 -199
  295. package/dist/sidecar-lock-admission-context.d.ts +19 -0
  296. package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
  297. package/dist/sidecar-lock-admission-context.js +60 -0
  298. package/dist/sidecar-lock-admission-parser.d.ts +43 -0
  299. package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
  300. package/dist/sidecar-lock-admission-parser.js +113 -0
  301. package/dist/sidecar-lock-admission.d.ts +35 -0
  302. package/dist/sidecar-lock-admission.d.ts.map +1 -0
  303. package/dist/sidecar-lock-admission.js +7 -0
  304. package/dist/sidecar-lock-reclaim.d.ts +9 -4
  305. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  306. package/dist/sidecar-lock-reclaim.js +80 -25
  307. package/dist/sidecar-lock-root.d.ts.map +1 -1
  308. package/dist/sidecar-lock-root.js +2 -1
  309. package/dist/sidecar-lock-stale-admission.d.ts +39 -0
  310. package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
  311. package/dist/sidecar-lock-stale-admission.js +232 -0
  312. package/dist/sidecar-lock-target.d.ts +8 -0
  313. package/dist/sidecar-lock-target.d.ts.map +1 -0
  314. package/dist/sidecar-lock-target.js +55 -0
  315. package/dist/sidecar-lock.d.ts.map +1 -1
  316. package/dist/sidecar-lock.js +100 -16
  317. package/dist/staged-directory.d.ts.map +1 -1
  318. package/dist/staged-directory.js +6 -6
  319. package/dist/staged-file-settlement.d.ts +17 -0
  320. package/dist/staged-file-settlement.d.ts.map +1 -0
  321. package/dist/staged-file-settlement.js +57 -0
  322. package/dist/temp-workspace-descriptor.d.ts.map +1 -1
  323. package/dist/temp-workspace-descriptor.js +9 -27
  324. package/dist/temp-workspace-owner.d.ts.map +1 -1
  325. package/dist/temp-workspace-owner.js +8 -8
  326. package/dist/walk.d.ts +5 -1
  327. package/dist/walk.d.ts.map +1 -1
  328. package/dist/walk.js +19 -6
  329. package/dist/windows-owner.d.ts.map +1 -1
  330. package/dist/windows-owner.js +4 -3
  331. package/dist/windows-security-bridge.cs +336 -0
  332. package/dist/windows-security-bridge.ps1 +15 -0
  333. package/dist/windows-security-command.d.ts +26 -0
  334. package/dist/windows-security-command.d.ts.map +1 -0
  335. package/dist/windows-security-command.js +363 -0
  336. package/dist/windows-security-facts.d.ts +6 -0
  337. package/dist/windows-security-facts.d.ts.map +1 -0
  338. package/dist/windows-security-facts.js +108 -0
  339. package/docs/advanced.md +4 -2
  340. package/docs/archive.md +97 -46
  341. package/docs/atomic.md +85 -8
  342. package/docs/config.md +6 -2
  343. package/docs/contributing.md +44 -4
  344. package/docs/copy.md +37 -0
  345. package/docs/creation.md +128 -0
  346. package/docs/durability.md +24 -0
  347. package/docs/file-store.md +19 -0
  348. package/docs/install.md +31 -7
  349. package/docs/json-store.md +5 -0
  350. package/docs/migrating-to-0.5.md +15 -6
  351. package/docs/migrating-to-0.6.md +9 -4
  352. package/docs/native-helper.md +32 -12
  353. package/docs/native.md +38 -7
  354. package/docs/output.md +6 -0
  355. package/docs/path-prefix.md +10 -0
  356. package/docs/path-suffix-aliases.md +51 -6
  357. package/docs/permissions.md +50 -14
  358. package/docs/public-api.md +3 -2
  359. package/docs/root.md +56 -3
  360. package/docs/secret-file.md +11 -2
  361. package/docs/secure-file.md +9 -4
  362. package/docs/sidecar-lock.md +114 -8
  363. package/docs/staged-file.md +12 -3
  364. package/docs/temp.md +20 -3
  365. package/docs/walk.md +67 -1
  366. package/docs/writing.md +76 -6
  367. package/package.json +19 -16
  368. package/dist/darwin-acl.d.ts +0 -4
  369. package/dist/darwin-acl.d.ts.map +0 -1
  370. package/dist/darwin-acl.js +0 -24
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { assertSynchronousCallbackResult } from "./mutation-authority.js";
4
+ export function assertPinnedWriteMode(fd, mode) {
5
+ if (process.platform === "win32")
6
+ return;
7
+ if (Number(fs.fstatSync(fd, { bigint: true }).mode & 4095n) !== mode) {
8
+ throw new FsSafeError("insecure-permissions", "filesystem did not enforce the secret file mode");
9
+ }
10
+ }
11
+ export async function preparePinnedWriteMode(handle, createdMode, assertBeforeMutation) {
12
+ if (process.platform === "win32")
13
+ return assertBeforeMutation;
14
+ assertSynchronousCallbackResult(assertBeforeMutation?.(), "assertBeforeMutation");
15
+ if (createdMode !== 0o600)
16
+ await handle.chmod(0o600);
17
+ return pinnedWriteModeAssertion(handle.fd, 0o600, assertBeforeMutation);
18
+ }
19
+ export function pinnedWriteModeAssertion(fd, mode, assertBeforeMutation) {
20
+ return () => {
21
+ assertSynchronousCallbackResult(assertBeforeMutation?.(), "assertBeforeMutation");
22
+ assertPinnedWriteMode(fd, mode);
23
+ };
24
+ }
@@ -0,0 +1,6 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import type { AsyncDirectoryGuard } from "./directory-guard.js";
3
+ import { type FileIdentityStat } from "./file-identity.js";
4
+ import type { PinnedWriteParams } from "./pinned-write-types.js";
5
+ export declare function runPinnedStagedWrite(params: PinnedWriteParams, parentPath: string, parentGuard: AsyncDirectoryGuard<BigIntStats>): Promise<FileIdentityStat>;
6
+ //# sourceMappingURL=pinned-write-staged.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-write-staged.d.ts","sourceRoot":"","sources":["../src/pinned-write-staged.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAMnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGhE,OAAO,EAA+B,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAMxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AASjE,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,GAC5C,OAAO,CAAC,gBAAgB,CAAC,CAqJ3B"}
@@ -0,0 +1,187 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fsSync, {} from "node:fs";
3
+ import fs, {} from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { createFileHandle } from "./create.js";
6
+ import { hasPreservedCreationArtifacts } from "./creation-file-state.js";
7
+ import { creationAdmissionFromParent } from "./creation-path.js";
8
+ import { syncDirectoryBestEffort } from "./directory-durability.js";
9
+ import { FsSafeError } from "./errors.js";
10
+ import { sameFileIdentity, sha256Hex } from "./file-identity.js";
11
+ import { syncFileBestEffort } from "./file-sync.js";
12
+ import { assertDarwinCreationAcl, privateFileMutationAssertion } from "./creation-darwin.js";
13
+ import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
14
+ import { writePinnedInput } from "./pinned-write-input.js";
15
+ import { assertPinnedWriteMode, pinnedWriteModeAssertion, preparePinnedWriteMode } from "./pinned-write-mode.js";
16
+ import { publishCopyStage } from "./publish-copy-stage.js";
17
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
18
+ import { cleanupPinnedFilePath } from "./file-cleanup.js";
19
+ import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
20
+ import { createStagedFileReceipt, settleStagedFile } from "./staged-file-settlement.js";
21
+ import { getFsSafeTestHooks } from "./test-hooks.js";
22
+ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
23
+ const verifyPosixMode = params.verifyPosixMode === true && process.platform !== "win32";
24
+ const targetPath = path.join(parentPath, params.basename);
25
+ // Private staging must not consume the destination basename's filename budget.
26
+ const tempPath = path.join(parentPath, `.fs-safe-${randomUUID()}.tmp`);
27
+ const tempFlags = fsSync.constants.O_WRONLY |
28
+ fsSync.constants.O_CREAT |
29
+ fsSync.constants.O_EXCL |
30
+ (process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
31
+ ? fsSync.constants.O_NOFOLLOW
32
+ : 0);
33
+ let handle;
34
+ let tempStat;
35
+ let tempIdentity;
36
+ let readHandle;
37
+ let renamed = false;
38
+ const completeCreate = params.overwrite === false && params.input.kind !== "file" && params.input.stageBeforePublish === true;
39
+ let receipt;
40
+ let publication = Object.freeze({ status: "not-published" });
41
+ let phase = "prepare";
42
+ let failure;
43
+ try {
44
+ params.assertBeforeMutation?.();
45
+ handle = params.private
46
+ ? await createFileHandle(tempPath, {
47
+ private: true, mode: 0o600, assertBeforeMutation: params.assertBeforeMutation,
48
+ }, creationAdmissionFromParent(parentGuard))
49
+ : await fs.open(tempPath, tempFlags, verifyPosixMode ? 0o600 : params.mode);
50
+ let verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
51
+ tempIdentity = verificationIdentity;
52
+ let assertBeforeMutation = params.private
53
+ ? privateFileMutationAssertion(handle.fd, params.assertBeforeMutation) : params.assertBeforeMutation;
54
+ const assertBeforeWrite = verifyPosixMode
55
+ ? await preparePinnedWriteMode(handle, Number(tempIdentity.mode & 4095n), assertBeforeMutation)
56
+ : assertBeforeMutation;
57
+ if (verifyPosixMode)
58
+ assertPinnedWriteMode(handle.fd, 0o600);
59
+ await writePinnedInput(handle, params.input, params.maxBytes, assertBeforeWrite);
60
+ if (verifyPosixMode)
61
+ assertPinnedWriteMode(handle.fd, 0o600);
62
+ if (params.private)
63
+ assertDarwinCreationAcl(handle.fd);
64
+ tempStat = fsSync.fstatSync(handle.fd);
65
+ const tempPathStat = fsSync.lstatSync(tempPath);
66
+ if (tempPathStat.isSymbolicLink() || !sameFileIdentity(tempPathStat, tempStat)) {
67
+ throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
68
+ }
69
+ const expectedTempStat = tempStat;
70
+ await handle.chmod(params.mode);
71
+ if (verifyPosixMode) {
72
+ assertPinnedWriteMode(handle.fd, params.mode);
73
+ assertBeforeMutation = pinnedWriteModeAssertion(handle.fd, params.mode, assertBeforeMutation);
74
+ }
75
+ if (params.sync !== false) {
76
+ if (params.strictFileSync)
77
+ await handle.sync();
78
+ else
79
+ await syncFileBestEffort(handle);
80
+ }
81
+ if (params.input.kind === "file")
82
+ await params.input.verifySource();
83
+ if (completeCreate) {
84
+ receipt = createStagedFileReceipt(Object.freeze({
85
+ path: parentGuard.dir,
86
+ realPath: parentGuard.realPath,
87
+ identity: Object.freeze({ dev: parentGuard.stat.dev, ino: parentGuard.stat.ino }),
88
+ }), path.basename(tempPath), fsSync.fstatSync(handle.fd, { bigint: true }));
89
+ }
90
+ let verifiedIdentity = expectedTempStat;
91
+ phase = "publish";
92
+ await withAsyncDirectoryGuards([parentGuard], async () => {
93
+ assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
94
+ if (params.overwrite === false) {
95
+ publishCopyStage({
96
+ temporaryPath: tempPath, targetPath, fd: handle.fd,
97
+ identity: tempIdentity, parentGuard,
98
+ assertBeforeMutation,
99
+ onPublicationAttempt: () => {
100
+ if (receipt)
101
+ publication = Object.freeze({ status: "indeterminate", basename: params.basename, overwrite: false });
102
+ },
103
+ onPublished: (identity) => {
104
+ renamed = true;
105
+ if (receipt)
106
+ publication = Object.freeze({ status: "published", staged: receipt, basename: params.basename, overwrite: false });
107
+ params.onPublished?.(identity);
108
+ },
109
+ });
110
+ }
111
+ else {
112
+ assertBeforeMutation?.();
113
+ await fs.rename(tempPath, targetPath);
114
+ renamed = true;
115
+ params.onPublished?.(verificationIdentity);
116
+ }
117
+ await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
118
+ if (params.sync !== false)
119
+ await syncDirectoryBestEffort(parentPath);
120
+ const targetStat = fsSync.lstatSync(targetPath);
121
+ if (targetStat.isSymbolicLink()) {
122
+ throw new FsSafeError("path-mismatch", "fallback target changed during write");
123
+ }
124
+ if (!sameFileIdentity(targetStat, expectedTempStat)) {
125
+ // On filesystems like rclone FUSE, rename(2) can give the destination a
126
+ // different inode from the source temp fd even with zero concurrency. The
127
+ // caller must ensure mutual exclusion before passing "verify-content";
128
+ // fall back to a content hash for this rename-boundary check only.
129
+ if (params.onRenameIdentityMismatch !== "verify-content") {
130
+ throw new FsSafeError("path-mismatch", "fallback target changed during write");
131
+ }
132
+ if (params.input.kind !== "buffer") {
133
+ throw new FsSafeError("path-mismatch", "fallback target changed during write");
134
+ }
135
+ const expectedHash = sha256Hex(params.input.data, params.input.encoding);
136
+ readHandle = await fs.open(targetPath, resolveReadOpenFlags());
137
+ const readHandleStat = fsSync.fstatSync(readHandle.fd, { bigint: true });
138
+ const actualHash = sha256Hex(await readHandle.readFile());
139
+ if (actualHash !== expectedHash) {
140
+ throw new FsSafeError("path-mismatch", "fallback target changed during write");
141
+ }
142
+ // The content-verified destination, not the old temp inode, is now pinned.
143
+ verificationIdentity = readHandleStat;
144
+ // Preserve the helper's legacy numeric return facts, not the private proof.
145
+ verifiedIdentity = { dev: Number(readHandleStat.dev), ino: Number(readHandleStat.ino) };
146
+ }
147
+ });
148
+ await params.verifyPublished?.((readHandle ?? handle).fd, verificationIdentity, parentGuard);
149
+ return { dev: verifiedIdentity.dev, ino: verifiedIdentity.ino };
150
+ }
151
+ catch (error) {
152
+ failure = { error };
153
+ throw error;
154
+ }
155
+ finally {
156
+ const preservedPreparation = params.private && !handle && hasPreservedCreationArtifacts(failure?.error);
157
+ if (completeCreate || preservedPreparation) {
158
+ await settleStagedFile({
159
+ temporaryBasename: path.basename(tempPath), publication, phase, failure,
160
+ cleanup: async () => {
161
+ if (preservedPreparation)
162
+ return "preserved";
163
+ if (publication.status === "indeterminate")
164
+ return "preserved";
165
+ if (renamed) {
166
+ return failure?.error instanceof FsSafeError && failure.error.details?.cleanup === "failed"
167
+ ? "failed" : "not-needed";
168
+ }
169
+ return handle ? await cleanupPinnedFilePath({
170
+ pathname: tempPath, handle, identity: tempIdentity, parentGuard, throwOnCleanupError: true,
171
+ }) : "not-needed";
172
+ },
173
+ close: [async () => { await readHandle?.close(); }, async () => { await handle?.close(); }],
174
+ });
175
+ }
176
+ else
177
+ try {
178
+ if (!renamed && handle) {
179
+ await cleanupPinnedFilePath({ pathname: tempPath, handle, identity: tempIdentity, parentGuard });
180
+ }
181
+ }
182
+ finally {
183
+ await readHandle?.close().catch(() => undefined);
184
+ await handle?.close().catch(() => undefined);
185
+ }
186
+ }
187
+ }
@@ -6,6 +6,7 @@ export type PinnedWriteInput = {
6
6
  kind: "buffer";
7
7
  data: string | Buffer;
8
8
  encoding?: BufferEncoding;
9
+ stageBeforePublish?: boolean;
9
10
  } | {
10
11
  kind: "stream";
11
12
  stream: AsyncIterable<Uint8Array | string>;
@@ -42,6 +43,7 @@ export type PinnedWriteMutationAdmission = Readonly<{
42
43
  rejectParentSymlinks: boolean;
43
44
  beginParentWalk?(): string | undefined;
44
45
  beginSharedParentWalk?(): PinnedMutationParentWalkSession | undefined;
46
+ beginNativeParentWalk?(): PinnedMutationParentWalkSession | undefined;
45
47
  tryAuthorizeAtParent?(request: Readonly<{
46
48
  targetPath: string;
47
49
  mutationPath: string;
@@ -60,7 +62,10 @@ export type PinnedWriteParams = {
60
62
  basename: string;
61
63
  mkdir: boolean;
62
64
  mode: number;
65
+ private?: boolean;
66
+ verifyPosixMode?: boolean;
63
67
  sync?: boolean;
68
+ strictFileSync?: boolean;
64
69
  overwrite?: boolean;
65
70
  assertBeforeMutation?: () => void;
66
71
  rejectFinalSymlink?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-write-types.d.ts","sourceRoot":"","sources":["../src/pinned-write-types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pinned-write-types.d.ts","sourceRoot":"","sources":["../src/pinned-write-types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAiB3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACvF,YAAY,EACV,gBAAgB,EAAE,oBAAoB,EAAE,sBAAsB,EAC9D,8BAA8B,EAAE,gCAAgC,EAChE,6BAA6B,EAAE,+BAA+B,EAC9D,4BAA4B,EAAE,iBAAiB,GAChD,MAAM,yBAAyB,CAAC;AAgFjC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsD/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CAkB3B;AAED,wBAAsB,iCAAiC,CAAC,CAAC,EACvD,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,EAC5E,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC,CAiBZ"}
1
+ {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAgB3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACvF,YAAY,EACV,gBAAgB,EAAE,oBAAoB,EAAE,sBAAsB,EAC9D,8BAA8B,EAAE,gCAAgC,EAChE,6BAA6B,EAAE,+BAA+B,EAC9D,4BAA4B,EAAE,iBAAiB,GAChD,MAAM,yBAAyB,CAAC;AAkDjC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsD/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CAkB3B;AAED,wBAAsB,iCAAiC,CAAC,CAAC,EACvD,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,EAC5E,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC,CAiBZ"}
@@ -1,32 +1,28 @@
1
1
  import { syncFileBestEffort } from "./file-sync.js";
2
- import { randomUUID } from "node:crypto";
2
+ import { assertDarwinCreationAcl, privateFileMutationAssertion } from "./creation-darwin.js";
3
3
  import fsSync, {} from "node:fs";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
+ import { createFileHandle } from "./create.js";
7
+ import { creationAdmissionFromParent } from "./creation-path.js";
6
8
  import { normalizeMaxBytes } from "./byte-budget.js";
7
9
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
8
10
  import { FsSafeError } from "./errors.js";
9
11
  import { syncDirectoryBestEffort } from "./directory-durability.js";
10
- import { sameFileIdentity, sha256Hex } from "./file-identity.js";
12
+ import { sha256Hex } from "./file-identity.js";
11
13
  import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
12
14
  import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
13
15
  import { runPinnedWriteNative } from "./native-pinned-write.js";
14
16
  import { getNativeBinding } from "./native.js";
15
17
  import { validatePinnedOperationPayload } from "./pinned-operation.js";
16
- import { resolveReadOpenFlags } from "./read-open-flags.js";
17
- import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
18
+ import { cleanupPinnedFilePath } from "./file-cleanup.js";
18
19
  import { withSidecarLock } from "./sidecar-lock.js";
19
20
  import { getFsSafeTestHooks } from "./test-hooks.js";
20
- import { writeAllToFile } from "./write-file-handle.js";
21
+ import { writePinnedInput } from "./pinned-write-input.js";
22
+ import { assertPinnedWriteMode, preparePinnedWriteMode } from "./pinned-write-mode.js";
23
+ import { runPinnedStagedWrite } from "./pinned-write-staged.js";
21
24
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
22
- import { writeCopyFileToFd } from "./copy-file-input.js";
23
- import { publishCopyStage } from "./publish-copy-stage.js";
24
25
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
25
- function byteLength(input, encoding) {
26
- return typeof input === "string"
27
- ? Buffer.byteLength(input, encoding ?? "utf8")
28
- : input.byteLength;
29
- }
30
26
  function assertSafeBasename(basename) {
31
27
  if (!basename ||
32
28
  basename === "." ||
@@ -37,24 +33,10 @@ function assertSafeBasename(basename) {
37
33
  throw new FsSafeError("invalid-path", "invalid target path");
38
34
  }
39
35
  }
40
- function assertWithinMaxBytes(bytes, maxBytes) {
41
- if (maxBytes !== undefined && bytes > maxBytes) {
42
- throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
43
- }
44
- }
45
36
  function fastParentGuardDeopt(error) {
46
37
  return error instanceof FsSafeError &&
47
38
  (error.code === "path-mismatch" || error.code === "not-file");
48
39
  }
49
- async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutation) {
50
- let bytes = 0;
51
- for await (const chunk of stream) {
52
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
53
- bytes += buffer.byteLength;
54
- assertWithinMaxBytes(bytes, maxBytes);
55
- await writeAllToFile(handle, buffer, { assertBeforeMutation });
56
- }
57
- }
58
40
  const PINNED_WRITE_SNAPSHOT_KEYS = [
59
41
  "rootPath", "relativeParentPath", "basename", "maxBytes", "rootIdentity",
60
42
  ];
@@ -142,6 +124,7 @@ export async function withPinnedWriteRenameIdentityLock(params, run) {
142
124
  }, run);
143
125
  }
144
126
  async function runPinnedWriteFallback(params) {
127
+ const verifyPosixMode = params.verifyPosixMode === true && process.platform !== "win32";
145
128
  const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
146
129
  ? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
147
130
  const mutationAdmission = params.mutationAdmission;
@@ -246,15 +229,19 @@ async function runPinnedWriteFallback(params) {
246
229
  }));
247
230
  await assertAsyncDirectoryGuard(parentGuard);
248
231
  }
249
- if (params.overwrite === false && (params.input.kind === "buffer" ||
250
- (params.input.kind === "stream" && !params.input.stageBeforePublish))) {
232
+ if (params.overwrite === false && params.input.kind !== "file" && !params.input.stageBeforePublish) {
251
233
  const assertBeforeMutation = () => {
252
234
  assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
253
235
  params.assertBeforeMutation?.();
254
236
  };
255
237
  const handle = await withAsyncDirectoryGuards([parentGuard], async () => {
256
238
  assertBeforeMutation();
257
- return await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode);
239
+ if (params.private) {
240
+ return await createFileHandle(targetPath, {
241
+ private: true, mode: 0o600, assertBeforeMutation,
242
+ }, creationAdmissionFromParent(parentGuard));
243
+ }
244
+ return await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, verifyPosixMode ? 0o600 : params.mode);
258
245
  }, {
259
246
  onPostGuardFailure: async (openedHandle) => {
260
247
  // The parent failed verification, so targetPath may now resolve
@@ -267,19 +254,27 @@ async function runPinnedWriteFallback(params) {
267
254
  try {
268
255
  const verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
269
256
  createdIdentity = verificationIdentity;
270
- if (params.input.kind === "buffer") {
271
- assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
272
- await writeAllToFile(handle, params.input.data, {
273
- encoding: params.input.encoding, assertBeforeMutation,
274
- });
275
- }
276
- else {
277
- await writeStreamToHandle(params.input.stream, handle, params.maxBytes, assertBeforeMutation);
257
+ let assertBeforeWrite = params.private
258
+ ? privateFileMutationAssertion(handle.fd, assertBeforeMutation) : assertBeforeMutation;
259
+ if (verifyPosixMode) {
260
+ assertBeforeWrite = await preparePinnedWriteMode(handle, Number(createdIdentity.mode & 4095n), assertBeforeWrite);
261
+ assertPinnedWriteMode(handle.fd, 0o600);
278
262
  }
263
+ await writePinnedInput(handle, params.input, params.maxBytes, assertBeforeWrite);
264
+ if (verifyPosixMode)
265
+ assertPinnedWriteMode(handle.fd, 0o600);
266
+ if (params.private)
267
+ assertDarwinCreationAcl(handle.fd);
279
268
  // Content writes may clear set-ID bits; finalize them through the owned fd.
280
269
  await handle.chmod(params.mode);
281
- if (params.sync !== false)
282
- await syncFileBestEffort(handle);
270
+ if (verifyPosixMode)
271
+ assertPinnedWriteMode(handle.fd, params.mode);
272
+ if (params.sync !== false) {
273
+ if (params.strictFileSync)
274
+ await handle.sync();
275
+ else
276
+ await syncFileBestEffort(handle);
277
+ }
283
278
  const stat = fsSync.fstatSync(handle.fd);
284
279
  if (params.sync !== false)
285
280
  await syncDirectoryBestEffort(parentPath);
@@ -299,110 +294,5 @@ async function runPinnedWriteFallback(params) {
299
294
  }
300
295
  }
301
296
  }
302
- // Private staging must not consume the destination basename's filename budget.
303
- const tempPath = path.join(parentPath, `.fs-safe-${randomUUID()}.tmp`);
304
- const tempFlags = fsSync.constants.O_WRONLY |
305
- fsSync.constants.O_CREAT |
306
- fsSync.constants.O_EXCL |
307
- (process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
308
- ? fsSync.constants.O_NOFOLLOW
309
- : 0);
310
- let handle;
311
- let tempStat;
312
- let tempIdentity;
313
- let readHandle;
314
- let renamed = false;
315
- try {
316
- params.assertBeforeMutation?.();
317
- handle = await fs.open(tempPath, tempFlags, params.mode);
318
- let verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
319
- tempIdentity = verificationIdentity;
320
- if (params.input.kind === "buffer") {
321
- assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
322
- await writeAllToFile(handle, params.input.data, {
323
- encoding: params.input.encoding, assertBeforeMutation: params.assertBeforeMutation,
324
- });
325
- }
326
- else if (params.input.kind === "file") {
327
- await writeCopyFileToFd(handle.fd, params.input, params.maxBytes, params.assertBeforeMutation);
328
- }
329
- else {
330
- await writeStreamToHandle(params.input.stream, handle, params.maxBytes, params.assertBeforeMutation);
331
- }
332
- tempStat = fsSync.fstatSync(handle.fd);
333
- const tempPathStat = fsSync.lstatSync(tempPath);
334
- if (tempPathStat.isSymbolicLink() || !sameFileIdentity(tempPathStat, tempStat)) {
335
- throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
336
- }
337
- const expectedTempStat = tempStat;
338
- await handle.chmod(params.mode);
339
- if (params.sync !== false)
340
- await syncFileBestEffort(handle);
341
- if (params.input.kind === "file")
342
- await params.input.verifySource();
343
- let verifiedIdentity = expectedTempStat;
344
- await withAsyncDirectoryGuards([parentGuard], async () => {
345
- assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
346
- if (params.overwrite === false) {
347
- publishCopyStage({
348
- temporaryPath: tempPath, targetPath, fd: handle.fd,
349
- identity: tempIdentity, parentGuard,
350
- assertBeforeMutation: params.assertBeforeMutation,
351
- onPublished: (identity) => {
352
- renamed = true;
353
- params.onPublished?.(identity);
354
- },
355
- });
356
- }
357
- else {
358
- params.assertBeforeMutation?.();
359
- await fs.rename(tempPath, targetPath);
360
- renamed = true;
361
- params.onPublished?.(verificationIdentity);
362
- }
363
- await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
364
- if (params.sync !== false)
365
- await syncDirectoryBestEffort(parentPath);
366
- const targetStat = fsSync.lstatSync(targetPath);
367
- if (targetStat.isSymbolicLink()) {
368
- throw new FsSafeError("path-mismatch", "fallback target changed during write");
369
- }
370
- if (!sameFileIdentity(targetStat, expectedTempStat)) {
371
- // On filesystems like rclone FUSE, rename(2) can give the destination a
372
- // different inode from the source temp fd even with zero concurrency. The
373
- // caller must ensure mutual exclusion before passing "verify-content";
374
- // fall back to a content hash for this rename-boundary check only.
375
- if (params.onRenameIdentityMismatch !== "verify-content") {
376
- throw new FsSafeError("path-mismatch", "fallback target changed during write");
377
- }
378
- if (params.input.kind !== "buffer") {
379
- throw new FsSafeError("path-mismatch", "fallback target changed during write");
380
- }
381
- const expectedHash = sha256Hex(params.input.data, params.input.encoding);
382
- readHandle = await fs.open(targetPath, resolveReadOpenFlags());
383
- const readHandleStat = fsSync.fstatSync(readHandle.fd, { bigint: true });
384
- const actualHash = sha256Hex(await readHandle.readFile());
385
- if (actualHash !== expectedHash) {
386
- throw new FsSafeError("path-mismatch", "fallback target changed during write");
387
- }
388
- // The content-verified destination, not the old temp inode, is now pinned.
389
- verificationIdentity = readHandleStat;
390
- // Preserve the helper's legacy numeric return facts, not the private proof.
391
- verifiedIdentity = { dev: Number(readHandleStat.dev), ino: Number(readHandleStat.ino) };
392
- }
393
- });
394
- await params.verifyPublished?.((readHandle ?? handle).fd, verificationIdentity, parentGuard);
395
- return { dev: verifiedIdentity.dev, ino: verifiedIdentity.ino };
396
- }
397
- finally {
398
- try {
399
- if (!renamed && handle) {
400
- await cleanupPinnedFilePath({ pathname: tempPath, handle, identity: tempIdentity, parentGuard });
401
- }
402
- }
403
- finally {
404
- await readHandle?.close().catch(() => undefined);
405
- await handle?.close().catch(() => undefined);
406
- }
407
- }
297
+ return await runPinnedStagedWrite(params, parentPath, parentGuard);
408
298
  }
@@ -1 +1 @@
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
+ {"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAQA,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,CAgCf"}
@@ -1,6 +1,10 @@
1
+ import path from "node:path";
1
2
  import { FsSafeError } from "./errors.js";
2
3
  import { getNativeBinding } from "./native.js";
4
+ import { getFsSafeNativeConfig } from "./native-config.js";
5
+ import { warnNativeFallback } from "./native-fallback-warning.js";
3
6
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
7
+ import { createPrivateWindowsDirectoryCommand } from "./windows-security-command.js";
4
8
  export async function createPrivateDirectory(targetPath, options) {
5
9
  const platform = options?.platform ?? process.platform;
6
10
  if (platform !== "win32") {
@@ -8,9 +12,19 @@ export async function createPrivateDirectory(targetPath, options) {
8
12
  }
9
13
  assertNoWindowsPathAlias(targetPath, "filesystem", "private directory path uses a Windows filesystem namespace alias", platform);
10
14
  const native = getNativeBinding();
11
- if (!native) {
12
- throw new FsSafeError("helper-unavailable", "private Windows directory creation requires the matching optional native platform package; " +
13
- "install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
15
+ if (typeof native?.createPrivateDirectory === "function") {
16
+ native.createPrivateDirectory(targetPath);
17
+ return;
14
18
  }
15
- native.createPrivateDirectory(targetPath);
19
+ if (getFsSafeNativeConfig().mode === "require") {
20
+ throw new FsSafeError("helper-unavailable", "private Windows directory creation requires an up-to-date native helper");
21
+ }
22
+ // Preserve the native parser's raw component restrictions before resolve()
23
+ // could discard dot components or Win32 could trim trailing aliases.
24
+ const components = targetPath.replaceAll("/", "\\").split("\\");
25
+ if (components.some(component => component.endsWith(".") || component.endsWith(" "))) {
26
+ throw Object.assign(new Error("private directory path has an ambiguous component"), { code: "EINVAL" });
27
+ }
28
+ warnNativeFallback("windows-private-directory", "Private Windows directory creation uses a slower built-in system command.");
29
+ await createPrivateWindowsDirectoryCommand(path.win32.resolve(targetPath));
16
30
  }
@@ -0,0 +1,14 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
3
+ export declare function handoffCreatedFileSync(params: {
4
+ source: OwnedFileDescriptorSync;
5
+ sourcePath: string;
6
+ targetPath: string;
7
+ identity: BigIntStats;
8
+ assertSourceParent: () => void;
9
+ assertTargetParent: () => void;
10
+ assertBeforeMutation?: () => void;
11
+ verifyDescriptor?: (fd: number, path: string, links: number) => void;
12
+ onPublished?: () => void;
13
+ }): OwnedFileDescriptorSync;
14
+ //# sourceMappingURL=private-producer-handoff-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"private-producer-handoff-sync.d.ts","sourceRoot":"","sources":["../src/private-producer-handoff-sync.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAyB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAW7F,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,MAAM,EAAE,uBAAuB,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,GAAG,uBAAuB,CA0H1B"}