@openclaw/openshell-sandbox 2026.10.5-beta.4 → 2026.10.10-beta.2

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 (240) hide show
  1. package/README.md +1 -1
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +148 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +20 -4
  4. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -1
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  6. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
  7. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  8. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
  9. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
  10. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
  11. package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +2 -2
  12. package/node_modules/@openclaw/fs-safe/dist/bounded-read.d.ts +1 -0
  13. package/node_modules/@openclaw/fs-safe/dist/bounded-read.js +9 -2
  14. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.d.ts +2 -0
  15. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +2 -1
  16. package/node_modules/@openclaw/fs-safe/dist/config.d.ts +6 -0
  17. package/node_modules/@openclaw/fs-safe/dist/config.js +5 -0
  18. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +3 -1
  19. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +8 -7
  20. package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
  21. package/node_modules/@openclaw/fs-safe/dist/copy-link.js +170 -0
  22. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
  23. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
  24. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +11 -2
  25. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +2 -4
  26. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +4 -2
  27. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
  28. package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
  29. package/node_modules/@openclaw/fs-safe/dist/create.js +1 -1
  30. package/node_modules/@openclaw/fs-safe/dist/creation-boundary.js +7 -4
  31. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
  32. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
  33. package/node_modules/@openclaw/fs-safe/dist/directory-durability.d.ts +7 -4
  34. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +20 -5
  35. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
  36. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
  37. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.d.ts +7 -7
  38. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +9 -4
  39. package/node_modules/@openclaw/fs-safe/dist/durability.d.ts +2 -1
  40. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +2 -2
  41. package/node_modules/@openclaw/fs-safe/dist/exact-identity.d.ts +17 -0
  42. package/node_modules/@openclaw/fs-safe/dist/exact-identity.js +29 -0
  43. package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
  44. package/node_modules/@openclaw/fs-safe/dist/file-hash.d.ts +1 -1
  45. package/node_modules/@openclaw/fs-safe/dist/file-hash.js +11 -9
  46. package/node_modules/@openclaw/fs-safe/dist/file-identity.d.ts +7 -1
  47. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +27 -5
  48. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +1 -2
  49. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -1
  50. package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
  51. package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
  52. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
  53. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
  54. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +1 -0
  55. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +22 -15
  56. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  57. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
  58. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
  59. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -0
  60. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -0
  61. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
  62. package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.js +24 -13
  63. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +85 -18
  64. package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +5 -0
  65. package/node_modules/@openclaw/fs-safe/dist/native-config.js +11 -0
  66. package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +6 -4
  67. package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.d.ts +2 -1
  68. package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.js +26 -2
  69. package/node_modules/@openclaw/fs-safe/dist/native-guidance.d.ts +3 -0
  70. package/node_modules/@openclaw/fs-safe/dist/native-guidance.js +21 -0
  71. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
  72. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
  73. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +8 -6
  74. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +44 -4
  75. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +85 -15
  76. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +3 -3
  77. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +33 -6
  78. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
  79. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +22 -3
  80. package/node_modules/@openclaw/fs-safe/dist/native-policy-directory-observation.js +4 -3
  81. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
  82. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
  83. package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.d.ts +3 -0
  84. package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.js +16 -0
  85. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  86. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +15 -5
  87. package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.d.ts +3 -0
  88. package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.js +7 -0
  89. package/node_modules/@openclaw/fs-safe/dist/native-status.d.ts +17 -0
  90. package/node_modules/@openclaw/fs-safe/dist/native-status.js +30 -0
  91. package/node_modules/@openclaw/fs-safe/dist/native.d.ts +31 -4
  92. package/node_modules/@openclaw/fs-safe/dist/native.js +107 -9
  93. package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
  94. package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
  95. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +1 -1
  96. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -1
  97. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +9 -1
  98. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
  99. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  100. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +83 -47
  101. package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +2 -0
  102. package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +8 -3
  103. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +13 -0
  104. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +10 -8
  105. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
  106. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +7 -5
  107. package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
  108. package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
  109. package/node_modules/@openclaw/fs-safe/dist/private-directory.js +1 -1
  110. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
  111. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.d.ts +3 -2
  112. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +38 -10
  113. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
  114. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +3 -0
  115. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +3 -0
  116. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +5 -3
  117. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +26 -20
  118. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -0
  119. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.js +3 -4
  120. package/node_modules/@openclaw/fs-safe/dist/realpath.js +32 -0
  121. package/node_modules/@openclaw/fs-safe/dist/regular-file.d.ts +4 -0
  122. package/node_modules/@openclaw/fs-safe/dist/regular-file.js +11 -15
  123. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +3 -2
  124. package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
  125. package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
  126. package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +8 -13
  127. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +252 -27
  128. package/node_modules/@openclaw/fs-safe/dist/root-directory-creation.js +5 -1
  129. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +11 -8
  130. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +35 -13
  131. package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.d.ts +8 -0
  132. package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.js +108 -0
  133. package/node_modules/@openclaw/fs-safe/dist/root-file.d.ts +7 -0
  134. package/node_modules/@openclaw/fs-safe/dist/root-file.js +15 -0
  135. package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +2 -0
  136. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +154 -175
  137. package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.d.ts +2 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.js +239 -0
  139. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +37 -15
  140. package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.d.ts +32 -0
  141. package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.js +56 -0
  142. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
  143. package/node_modules/@openclaw/fs-safe/dist/root-public-types.d.ts +1 -0
  144. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +95 -16
  145. package/node_modules/@openclaw/fs-safe/dist/root-remove.js +6 -4
  146. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +23 -19
  147. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -6
  148. package/node_modules/@openclaw/fs-safe/dist/root-write-publication.js +5 -1
  149. package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +3 -1
  150. package/node_modules/@openclaw/fs-safe/dist/secure-file.d.ts +2 -0
  151. package/node_modules/@openclaw/fs-safe/dist/secure-file.js +10 -4
  152. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
  153. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +2 -2
  154. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
  155. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
  156. package/node_modules/@openclaw/fs-safe/dist/staged-directory.d.ts +13 -0
  157. package/node_modules/@openclaw/fs-safe/dist/staged-directory.js +29 -0
  158. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +5 -3
  159. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.d.ts +8 -0
  160. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +13 -2
  161. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +71 -3
  162. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +2 -1
  163. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +22 -0
  164. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-types.d.ts +3 -0
  165. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
  166. package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
  167. package/node_modules/@openclaw/fs-safe/dist/trash.js +2 -2
  168. package/node_modules/@openclaw/fs-safe/dist/types.d.ts +4 -0
  169. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
  170. package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.d.ts +5 -0
  171. package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.js +39 -0
  172. package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +1 -0
  173. package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +25 -36
  174. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +24 -5
  175. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +2 -1
  176. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +4 -0
  177. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.d.ts +2 -0
  178. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
  179. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.js +7 -0
  180. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
  181. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
  182. package/node_modules/@openclaw/fs-safe/dist/write-file-handle.d.ts +1 -0
  183. package/node_modules/@openclaw/fs-safe/dist/write-file-handle.js +12 -10
  184. package/node_modules/@openclaw/fs-safe/docs/advanced.md +2 -1
  185. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  186. package/node_modules/@openclaw/fs-safe/docs/config.md +71 -1
  187. package/node_modules/@openclaw/fs-safe/docs/contributing.md +45 -5
  188. package/node_modules/@openclaw/fs-safe/docs/copy.md +45 -5
  189. package/node_modules/@openclaw/fs-safe/docs/durability.md +34 -7
  190. package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
  191. package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
  192. package/node_modules/@openclaw/fs-safe/docs/install.md +47 -27
  193. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -6
  194. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.6.md +1 -1
  195. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +79 -6
  196. package/node_modules/@openclaw/fs-safe/docs/native.md +279 -8
  197. package/node_modules/@openclaw/fs-safe/docs/permissions.md +64 -15
  198. package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
  199. package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
  200. package/node_modules/@openclaw/fs-safe/docs/public-api.md +10 -0
  201. package/node_modules/@openclaw/fs-safe/docs/queue-link-free-design.md +237 -0
  202. package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
  203. package/node_modules/@openclaw/fs-safe/docs/reading.md +56 -2
  204. package/node_modules/@openclaw/fs-safe/docs/regular-file.md +2 -0
  205. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
  206. package/node_modules/@openclaw/fs-safe/docs/secure-file.md +1 -1
  207. package/node_modules/@openclaw/fs-safe/docs/security-model.md +35 -0
  208. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
  209. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +18 -0
  210. package/node_modules/@openclaw/fs-safe/docs/store.md +15 -0
  211. package/node_modules/@openclaw/fs-safe/docs/temp.md +39 -0
  212. package/node_modules/@openclaw/fs-safe/docs/testing.md +13 -0
  213. package/node_modules/@openclaw/fs-safe/docs/types.md +51 -81
  214. package/node_modules/@openclaw/fs-safe/docs/walk.md +4 -1
  215. package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
  216. package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
  217. package/node_modules/@openclaw/fs-safe/package.json +21 -12
  218. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  219. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  220. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  221. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  222. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  223. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
  224. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  225. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
  226. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  227. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  228. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  229. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  230. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  231. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  232. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  233. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  234. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  235. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
  236. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  237. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  238. package/package.json +5 -5
  239. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +0 -336
  240. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +0 -90
@@ -12,8 +12,7 @@ import { syncDirectoryBestEffort } from "./directory-durability.js";
12
12
  import { sha256Hex } from "./file-identity.js";
13
13
  import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
14
14
  import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
15
- import { runPinnedWriteNative } from "./native-pinned-write.js";
16
- import { getNativeBinding } from "./native.js";
15
+ import { getPinnedWriteNativeBinding, runPinnedWriteNative } from "./native-pinned-write.js";
17
16
  import { validatePinnedRelativePath } from "./pinned-operation.js";
18
17
  import { cleanupPinnedFilePath } from "./file-cleanup.js";
19
18
  import { withSidecarLock } from "./sidecar-lock.js";
@@ -23,6 +22,7 @@ import { assertPinnedWriteMode, preparePinnedWriteMode } from "./pinned-write-mo
23
22
  import { runPinnedStagedWrite } from "./pinned-write-staged.js";
24
23
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
25
24
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
25
+ import { copyLinkAtParent } from "./copy-link.js";
26
26
  function assertSafeBasename(basename) {
27
27
  if (!basename ||
28
28
  basename === "." ||
@@ -88,7 +88,7 @@ export async function runPinnedWriteHelper(params) {
88
88
  if (normalizedParams.onRenameIdentityMismatch === "verify-content") {
89
89
  return await runPinnedWriteFallback(normalizedParams);
90
90
  }
91
- const native = getNativeBinding();
91
+ const native = getPinnedWriteNativeBinding(normalizedParams);
92
92
  if (native) {
93
93
  return await runPinnedWriteNative(native, normalizedParams, input => runPinnedWriteFallback({ ...normalizedParams, input }));
94
94
  }
@@ -225,6 +225,8 @@ async function runPinnedWriteFallback(params) {
225
225
  }));
226
226
  await assertAsyncDirectoryGuard(parentGuard);
227
227
  }
228
+ if (params.input.kind === "link")
229
+ return await copyLinkAtParent(params, params.input, parentGuard);
228
230
  if (params.overwrite === false && params.input.kind !== "file" && !params.input.stageBeforePublish) {
229
231
  const assertBeforeMutation = () => {
230
232
  assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
@@ -261,7 +263,7 @@ async function runPinnedWriteFallback(params) {
261
263
  if (verifyPosixMode)
262
264
  assertPinnedWriteMode(handle.fd, 0o600, params.private);
263
265
  // Content writes may clear set-ID bits; finalize them through the owned fd.
264
- await handle.chmod(params.mode);
266
+ fsSync.fchmodSync(handle.fd, params.mode);
265
267
  if (verifyPosixMode)
266
268
  assertPinnedWriteMode(handle.fd, params.mode, params.private);
267
269
  if (params.sync !== false) {
@@ -270,7 +272,7 @@ async function runPinnedWriteFallback(params) {
270
272
  else
271
273
  await syncFileBestEffort(handle);
272
274
  }
273
- const stat = fsSync.fstatSync(handle.fd);
275
+ const stat = fsSync.fstatSync(handle.fd, { bigint: true });
274
276
  if (params.sync !== false)
275
277
  await syncDirectoryBestEffort(parentPath);
276
278
  // Publication is complete. A failed outer check must not remove its target.
@@ -0,0 +1,9 @@
1
+ import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
2
+ export type OwnedPipeSync = {
3
+ readonly reader: OwnedFileDescriptorSync;
4
+ readonly writer: OwnedFileDescriptorSync;
5
+ /** True when close-on-exec was set atomically at creation. */
6
+ readonly atomicCloseOnExec: boolean;
7
+ };
8
+ /** Creates a POSIX anonymous pipe with two native-owned, close-on-exec ends. */
9
+ export declare function createPipe(): OwnedPipeSync;
@@ -0,0 +1,26 @@
1
+ import { ownFileDescriptorSync } from "./create-owned-file.js";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { captureNativeFdClose } from "./native-binding.js";
4
+ import { requireNativeBinding } from "./native.js";
5
+ /** Creates a POSIX anonymous pipe with two native-owned, close-on-exec ends. */
6
+ export function createPipe() {
7
+ if (!["linux", "darwin", "freebsd"].includes(process.platform)) {
8
+ throw new FsSafeError("unsupported-platform", "anonymous pipes require Linux, Darwin, or FreeBSD");
9
+ }
10
+ const native = requireNativeBinding();
11
+ if (!native.createPipe) {
12
+ throw new FsSafeError("helper-unavailable", "native anonymous pipes are unavailable");
13
+ }
14
+ const close = captureNativeFdClose(native);
15
+ try {
16
+ const { reader, writer, atomicCloseOnExec } = native.createPipe();
17
+ return Object.freeze({
18
+ reader: ownFileDescriptorSync(reader, close),
19
+ writer: ownFileDescriptorSync(writer, close),
20
+ atomicCloseOnExec,
21
+ });
22
+ }
23
+ catch (cause) {
24
+ throw new FsSafeError("helper-failed", "anonymous pipe creation failed", { cause });
25
+ }
26
+ }
@@ -25,6 +25,6 @@ export async function createPrivateDirectory(targetPath, options) {
25
25
  if (components.some(component => component.endsWith(".") || component.endsWith(" "))) {
26
26
  throw Object.assign(new Error("private directory path has an ambiguous component"), { code: "EINVAL" });
27
27
  }
28
- warnNativeFallback("windows-private-directory", "Private Windows directory creation uses a slower built-in system command.");
28
+ warnNativeFallback("windows-private-directory", "Private Windows directory creation launches the packaged .NET helper for each check or creation.", "createPrivateDirectory");
29
29
  await createPrivateWindowsDirectoryCommand(path.win32.resolve(targetPath));
30
30
  }
@@ -2,6 +2,7 @@ import fsSync, {} from "node:fs";
2
2
  import fs, {} from "node:fs/promises";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  import { sameFileIdentityForCleanup } from "./file-identity.js";
5
+ import { isHardlinkFallbackError } from "./hardlink-fallback.js";
5
6
  import { assertSynchronousCallbackResult } from "./mutation-authority.js";
6
7
  import { resolveReadOpenFlags } from "./read-open-flags.js";
7
8
  import { errorCauseOptions } from "./root-errors.js";
@@ -39,11 +40,7 @@ export function normalizeLinkError(error) {
39
40
  if (code === "EEXIST") {
40
41
  return new FsSafeError("already-exists", "isolated producer sibling already exists", errorCauseOptions(error));
41
42
  }
42
- if (code === "EXDEV" ||
43
- code === "ENOSYS" ||
44
- code === "ENOTSUP" ||
45
- code === "EOPNOTSUPP" ||
46
- code === "EPERM") {
43
+ if (isHardlinkFallbackError(error)) {
47
44
  return new FsSafeError("helper-unavailable", "atomic isolated producer handoff is unavailable", errorCauseOptions(error));
48
45
  }
49
46
  return error;
@@ -1,7 +1,8 @@
1
+ import { type ExactIdentityReceipt } from "./exact-identity.js";
1
2
  import type { TempWorkspaceOptions, TempWorkspace, TempWorkspaceSync } from "./temp-workspace-types.js";
2
3
  export type { TempWorkspaceCleanupResult, TempWorkspaceCleanupSafety, } from "./temp-workspace-owner.js";
3
4
  export type { TempWorkspaceOptions, TempWorkspace, TempWorkspaceSync } from "./temp-workspace-types.js";
4
- export declare function tempWorkspace(options: TempWorkspaceOptions): Promise<TempWorkspace>;
5
+ export declare function tempWorkspace(options: TempWorkspaceOptions): Promise<TempWorkspace & ExactIdentityReceipt>;
5
6
  export declare function withTempWorkspace<T>(options: TempWorkspaceOptions, run: (workspace: TempWorkspace) => Promise<T>): Promise<T>;
6
- export declare function tempWorkspaceSync(options: TempWorkspaceOptions): TempWorkspaceSync;
7
+ export declare function tempWorkspaceSync(options: TempWorkspaceOptions): TempWorkspaceSync & ExactIdentityReceipt;
7
8
  export declare function withTempWorkspaceSync<T>(options: TempWorkspaceOptions, run: (workspace: TempWorkspaceSync) => T): T;
@@ -1,3 +1,4 @@
1
+ import { exactFileIdentity } from "./exact-identity.js";
1
2
  import { randomInt, randomUUID } from "node:crypto";
2
3
  import fsSync, {} from "node:fs";
3
4
  import fs from "node:fs/promises";
@@ -61,6 +62,26 @@ function createTempWorkspaceWithRequestedModeSync(childPrefix, dirMode, prepareC
61
62
  }
62
63
  throw collision;
63
64
  }
65
+ function createNativeTempWorkspace(childPrefix, capability) {
66
+ const create = capability.binding?.createTempDirectoryBeneath;
67
+ if (!create || capability.parent?.access !== "read")
68
+ return undefined;
69
+ for (let attempt = 0; attempt < TEMP_WORKSPACE_DIRECT_CREATE_ATTEMPTS; attempt += 1) {
70
+ const dir = randomTempWorkspaceChildPath(childPrefix);
71
+ capability.prepareChildCreation();
72
+ let created;
73
+ try {
74
+ created = create.call(capability.binding, capability.parent.fd, path.basename(dir));
75
+ }
76
+ catch (error) {
77
+ if (error.code === "EEXIST")
78
+ continue;
79
+ throw error;
80
+ }
81
+ return { dir, ...TempWorkspaceRetainedChild.adoptCreated(dir, created.fd, created.searchOnly) };
82
+ }
83
+ throw Object.assign(new Error("temp workspace name collisions exhausted"), { code: "EEXIST" });
84
+ }
64
85
  function resolveWorkspaceLeaf(dir, fileName) {
65
86
  const resolved = path.join(dir, assertWorkspaceFileName(fileName));
66
87
  assertNoWindowsPathAlias(resolved, "filesystem", "temp workspace path uses a Windows filesystem namespace alias");
@@ -143,7 +164,7 @@ function registerTempWorkspace(dir, retainedChild, capability, admission, dirMod
143
164
  catch (error) {
144
165
  throwTempWorkspaceCreationFailure(error, retainedChild, capability, owner);
145
166
  }
146
- return { owner: owner, unregisterTempDir, identity: { dev: Number(stat.dev), ino: Number(stat.ino) } };
167
+ return { owner: owner, unregisterTempDir, exactIdentity: exactFileIdentity(stat), identity: { dev: Number(stat.dev), ino: Number(stat.ino) } };
147
168
  }
148
169
  async function createTempWorkspace(options, scopedPrefix = false) {
149
170
  const { rootDir, dirMode, mode, cleanupSafety } = tempWorkspaceSettings(options);
@@ -154,12 +175,17 @@ async function createTempWorkspace(options, scopedPrefix = false) {
154
175
  let dir;
155
176
  let retainedChild;
156
177
  try {
157
- // Native capability discovery is complete before this synchronous
158
- // boundary. The existing canonical-root route now captures the complete
159
- // ancestry and associates its provisional parent descriptor here, then
160
- // dispatches mkdtemp without yielding or invoking another probe.
161
- capability.prepareChildCreation();
162
- dir = await fs.mkdtemp(childPrefix);
178
+ // Both creation routes first capture the complete ancestry and associate
179
+ // the provisional parent descriptor without another capability probe.
180
+ const created = createNativeTempWorkspace(childPrefix, capability);
181
+ if (created) {
182
+ dir = created.dir;
183
+ retainedChild = created.retained;
184
+ }
185
+ else {
186
+ capability.prepareChildCreation();
187
+ dir = await fs.mkdtemp(childPrefix);
188
+ }
163
189
  assertNoWindowsPathAlias(dir, "filesystem", "temp workspace path uses a Windows filesystem namespace alias");
164
190
  if (capability.parent)
165
191
  capability.assertCurrent();
@@ -169,7 +195,7 @@ async function createTempWorkspace(options, scopedPrefix = false) {
169
195
  const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode, dir);
170
196
  // Retain while the child still has its private creation mode so an
171
197
  // explicit dirMode such as 0 cannot make identity descriptor acquisition fail.
172
- retainedChild = TempWorkspaceRetainedChild.retain(dir, stat);
198
+ retainedChild ??= TempWorkspaceRetainedChild.retain(dir, stat);
173
199
  if (needsModeInitialization) {
174
200
  await retainedChild.initializeMode(dirMode, admission.ownerUid, admission.assertCurrent);
175
201
  }
@@ -177,12 +203,13 @@ async function createTempWorkspace(options, scopedPrefix = false) {
177
203
  catch (error) {
178
204
  throwTempWorkspaceCreationFailure(error, retainedChild, capability);
179
205
  }
180
- const { owner, identity, unregisterTempDir } = registerTempWorkspace(dir, retainedChild, capability, admission, dirMode);
206
+ const { owner, identity, exactIdentity, unregisterTempDir } = registerTempWorkspace(dir, retainedChild, capability, admission, dirMode);
181
207
  // Once registered, even a store-construction failure remains exit-cleanable.
182
208
  const store = fileStore({ rootDir: dir, private: true, dirMode, mode });
183
209
  return {
184
210
  dir,
185
211
  identity,
212
+ exactIdentity,
186
213
  store,
187
214
  path: (fileName) => resolveWorkspaceLeaf(dir, fileName),
188
215
  write: async (fileName, data) => await store.write(assertWorkspaceFileName(fileName), data, { mode }),
@@ -273,12 +300,13 @@ function createTempWorkspaceSync(options, scopedPrefix = false) {
273
300
  catch (error) {
274
301
  throwTempWorkspaceCreationFailure(error, retainedChild, capability);
275
302
  }
276
- const { owner, identity, unregisterTempDir } = registerTempWorkspace(dir, retainedChild, capability, admission, dirMode);
303
+ const { owner, identity, exactIdentity, unregisterTempDir } = registerTempWorkspace(dir, retainedChild, capability, admission, dirMode);
277
304
  // Once registered, even a store-construction failure remains exit-cleanable.
278
305
  const store = fileStoreSync({ rootDir: dir, private: true, dirMode, mode });
279
306
  return {
280
307
  dir,
281
308
  identity,
309
+ exactIdentity,
282
310
  store,
283
311
  path: (fileName) => resolveWorkspaceLeaf(dir, fileName),
284
312
  write: (fileName, data) => store.write(assertWorkspaceFileName(fileName), data, { mode }),
@@ -2,7 +2,10 @@ import fs, {} from "node:fs";
2
2
  import path from "node:path";
3
3
  import { assertSyncDirectoryGuard } from "./directory-guard.js";
4
4
  import { FsSafeError } from "./errors.js";
5
+ import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
5
6
  import { sameFileIdentityForCleanup } from "./file-identity.js";
7
+ import { hasErrorCode } from "./file-cleanup.js";
8
+ import { isHardlinkFallbackError } from "./hardlink-fallback.js";
6
9
  // Node has no portable no-replace rename. Publish the completed stage by link,
7
10
  // then remove its verified temporary name without yielding.
8
11
  export function publishCopyStage(params) {
@@ -27,7 +30,16 @@ export function publishCopyStage(params) {
27
30
  throw new FsSafeError("already-exists", "destination already exists");
28
31
  }
29
32
  params.onPublicationAttempt?.();
30
- fs.linkSync(temporaryPath, targetPath);
33
+ let renamed = false;
34
+ try {
35
+ fs.linkSync(temporaryPath, targetPath);
36
+ }
37
+ catch (error) {
38
+ if (!isHardlinkFallbackError(error))
39
+ throw error;
40
+ publishThroughPlaceholder(params, assertCurrent);
41
+ renamed = true;
42
+ }
31
43
  let observerRejected = false;
32
44
  let observerError;
33
45
  try {
@@ -38,16 +50,29 @@ export function publishCopyStage(params) {
38
50
  observerError = error;
39
51
  }
40
52
  try {
41
- const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
42
- const current = fs.lstatSync(temporaryPath, { bigint: true });
43
- if (parent.isSymbolicLink() || !parent.isDirectory() ||
44
- !sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
45
- current.isSymbolicLink() || !current.isFile() ||
46
- !sameFileIdentityForCleanup(current, identity)) {
47
- throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
53
+ if (renamed) {
54
+ assertSyncDirectoryGuard(params.parentGuard);
55
+ if (fs.lstatSync(temporaryPath, { throwIfNoEntry: false })) {
56
+ throw new FsSafeError("path-mismatch", "renamed copy stage still exists");
57
+ }
58
+ const target = fs.lstatSync(targetPath, { bigint: true });
59
+ if (!target.isFile() || target.isSymbolicLink() || target.nlink !== 1n ||
60
+ !sameFileIdentityForCleanup(target, identity)) {
61
+ throw new FsSafeError("path-mismatch", "renamed copy target changed after publication");
62
+ }
63
+ }
64
+ else {
65
+ const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
66
+ const current = fs.lstatSync(temporaryPath, { bigint: true });
67
+ if (parent.isSymbolicLink() || !parent.isDirectory() ||
68
+ !sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
69
+ current.isSymbolicLink() || !current.isFile() ||
70
+ !sameFileIdentityForCleanup(current, identity)) {
71
+ throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
72
+ }
73
+ // This removes the stage's name, never the published destination or source.
74
+ fs.unlinkSync(temporaryPath);
48
75
  }
49
- // This removes the stage's name, never the published destination or source.
50
- fs.unlinkSync(temporaryPath);
51
76
  }
52
77
  catch (error) {
53
78
  throw new FsSafeError("helper-failed", "published copy temporary cleanup failed", {
@@ -58,3 +83,60 @@ export function publishCopyStage(params) {
58
83
  if (observerRejected)
59
84
  throw observerError;
60
85
  }
86
+ function isEmptyPlaceholder(current, identity) {
87
+ return current.isFile() && !current.isSymbolicLink() && current.nlink === 1n &&
88
+ current.size === 0n && sameFileIdentityForCleanup(current, identity);
89
+ }
90
+ function publishThroughPlaceholder(params, assertCurrent) {
91
+ const { targetPath, temporaryPath } = params;
92
+ // An empty placeholder can be observed before the completed stage atomically replaces it.
93
+ // The stage proved directory write access; a real permission denial still fails O_EXCL.
94
+ assertCurrent();
95
+ let fd;
96
+ try {
97
+ assertExclusiveCreateLeaf(targetPath);
98
+ fd = fs.openSync(targetPath, fs.constants.O_WRONLY | fs.constants.O_CREAT |
99
+ fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
100
+ }
101
+ catch (error) {
102
+ if (hasErrorCode(error, "EEXIST")) {
103
+ throw new FsSafeError("already-exists", "destination already exists", { cause: error });
104
+ }
105
+ throw error;
106
+ }
107
+ let placeholder;
108
+ try {
109
+ try {
110
+ placeholder = fs.fstatSync(fd, { bigint: true });
111
+ }
112
+ finally {
113
+ fs.closeSync(fd);
114
+ }
115
+ assertCurrent();
116
+ const current = fs.lstatSync(targetPath, { bigint: true, throwIfNoEntry: false });
117
+ if (!current || !isEmptyPlaceholder(current, placeholder)) {
118
+ throw new FsSafeError("path-mismatch", "exclusive copy placeholder changed before publication");
119
+ }
120
+ fs.renameSync(temporaryPath, targetPath);
121
+ }
122
+ catch (error) {
123
+ try {
124
+ assertSyncDirectoryGuard(params.parentGuard);
125
+ const current = fs.lstatSync(targetPath, { bigint: true, throwIfNoEntry: false });
126
+ if (current) {
127
+ if (!placeholder)
128
+ throw new FsSafeError("path-mismatch", "copy placeholder identity unavailable for cleanup");
129
+ if (isEmptyPlaceholder(current, placeholder))
130
+ fs.unlinkSync(targetPath);
131
+ }
132
+ }
133
+ catch (cleanupError) {
134
+ throw new FsSafeError("helper-failed", "unpublished copy placeholder cleanup failed", {
135
+ cause: new AggregateError([error, cleanupError], "publication and cleanup failed"),
136
+ details: { publication: "not-published", path: targetPath, cleanup: "failed",
137
+ ...(placeholder ? { dev: placeholder.dev, ino: placeholder.ino } : {}) },
138
+ });
139
+ }
140
+ throw error;
141
+ }
142
+ }
@@ -1,3 +1,4 @@
1
+ import { type ExactFileIdentity } from "./exact-identity.js";
1
2
  import { FsSafeError } from "./errors.js";
2
3
  import type { FileIdentityStat } from "./file-identity.js";
3
4
  export type PublishFileExclusiveSyncFailurePolicy = "rollback" | "preserve";
@@ -11,6 +12,7 @@ export type PublishFileExclusiveFailureDetails = {
11
12
  phase: PublishFileExclusiveFailurePhase;
12
13
  targetCreated: boolean;
13
14
  targetIdentity?: FileIdentityStat;
15
+ exactTargetIdentity?: ExactFileIdentity;
14
16
  cleanup: PublishFileExclusiveCleanup;
15
17
  directorySync?: PublishFileExclusiveDirectorySyncFailure;
16
18
  fallback?: "link-unlink";
@@ -21,6 +23,7 @@ export type PublishFailureState = {
21
23
  phase: PublishFileExclusiveFailurePhase;
22
24
  targetCreated: boolean;
23
25
  targetIdentity?: FileIdentityStat;
26
+ exactTargetIdentity?: ExactFileIdentity;
24
27
  targetCleanupIdentity?: FileIdentityStat;
25
28
  preserveTarget: boolean;
26
29
  directorySync?: PublishFileExclusiveDirectorySyncFailure;
@@ -1,6 +1,8 @@
1
+ import { exactFileIdentity } from "./exact-identity.js";
1
2
  import { FsSafeError } from "./errors.js";
2
3
  export function rememberCreatedTarget(state, identity, phase) {
3
4
  state.targetCreated = true;
5
+ state.exactTargetIdentity = exactFileIdentity(identity);
4
6
  state.targetIdentity = { dev: Number(identity.dev), ino: Number(identity.ino) };
5
7
  state.targetCleanupIdentity = { dev: identity.dev, ino: identity.ino };
6
8
  state.phase = phase;
@@ -10,6 +12,7 @@ export function publicationFailure(error, state, cleanup) {
10
12
  const details = {
11
13
  phase: state.phase,
12
14
  targetCreated: state.targetCreated,
15
+ ...(state.exactTargetIdentity ? { exactTargetIdentity: state.exactTargetIdentity } : {}),
13
16
  ...(state.targetIdentity ? { targetIdentity: state.targetIdentity } : {}),
14
17
  ...(state.directorySync ? { directorySync: state.directorySync } : {}),
15
18
  ...(state.fallback ? { fallback: state.fallback, publication: "published", sourceRemoval: state.sourceRemoval } : {}),
@@ -1,20 +1,22 @@
1
+ import { type ExactFileIdentity } from "./exact-identity.js";
1
2
  import { type BigIntStats, type Stats } from "node:fs";
2
3
  import { type DirectoryReceipt, type DirectorySyncOutcome } from "./directory-durability.js";
3
- import { type FileIdentityStat } from "./file-identity.js";
4
+ import { type FileIdentityInput } from "./file-identity.js";
4
5
  import { type PublishFileExclusiveSyncFailurePolicy } from "./publish-file-failure.js";
6
+ export { isHardlinkFallbackError } from "./hardlink-fallback.js";
5
7
  export type PublishFileExclusiveStrategy = "link-or-copy" | "link-required" | "rename-noreplace";
6
8
  export type PublishFileExclusiveResult = {
7
9
  method: "hardlink" | "exclusive-copy" | "rename-noreplace";
8
10
  /** Linux auto fallback; exclusive and identity-checked, but not crash-atomic. */
9
11
  fallback?: "link-unlink";
10
12
  identity: Stats;
13
+ exactIdentity: ExactFileIdentity;
11
14
  directorySync: DirectorySyncOutcome;
12
15
  };
13
- export declare function isHardlinkFallbackError(error: unknown): boolean;
14
16
  export declare function publishFileExclusive(params: {
15
17
  sourcePath: string;
16
18
  targetPath: string;
17
- expectedSourceIdentity?: FileIdentityStat;
19
+ expectedSourceIdentity?: FileIdentityInput;
18
20
  strategy: PublishFileExclusiveStrategy;
19
21
  onSyncFailure?: PublishFileExclusiveSyncFailurePolicy;
20
22
  parentReceipt?: DirectoryReceipt<Stats | BigIntStats>;
@@ -1,3 +1,4 @@
1
+ import { exactFileIdentity, numberStats } from "./exact-identity.js";
1
2
  import { nativeMoveNoReplace } from "./native-move-fallback.js";
2
3
  import { getFsSafeNativeConfig } from "./native-config.js";
3
4
  import { createHash } from "node:crypto";
@@ -9,9 +10,9 @@ import { pinDirectory, } from "./directory-durability.js";
9
10
  import { FsSafeError } from "./errors.js";
10
11
  import { ownDirectoryReceipt } from "./directory-receipt.js";
11
12
  import { hashFileHandle } from "./file-hash.js";
12
- import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
13
+ import { sameFileIdentity, sameFileIdentityForCleanup, hasSafeFileIdentity, identityFromReceipt, } from "./file-identity.js";
13
14
  import { syncFileBestEffortSync } from "./file-sync.js";
14
- import { getNativeBinding, requireNativeBinding } from "./native.js";
15
+ import { getNativeBinding, requireNativeBinding, selectNativeBinding } from "./native.js";
15
16
  import { captureNativeFdClose } from "./native-binding.js";
16
17
  import { resolveReadOpenFlags } from "./read-open-flags.js";
17
18
  import { directorySyncFailure, publicationFailure, rememberCreatedTarget, } from "./publish-file-failure.js";
@@ -19,11 +20,9 @@ import { admitStandalonePublicationPath, assertNoWindowsPathAlias } from "./wind
19
20
  import { getFsSafeTestHooks } from "./test-hooks.js";
20
21
  import { hasErrorCode } from "./file-cleanup.js";
21
22
  import { writeAllToFile } from "./write-file-handle.js";
22
- const HARDLINK_FALLBACK_CODES = new Set(["EPERM", "EXDEV", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
23
+ import { isHardlinkFallbackError } from "./hardlink-fallback.js";
24
+ export { isHardlinkFallbackError } from "./hardlink-fallback.js";
23
25
  const NATIVE_COPY_FALLBACK_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV"]);
24
- export function isHardlinkFallbackError(error) {
25
- return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
26
- }
27
26
  function directoryOpenFlags() {
28
27
  return (fsSync.constants.O_RDONLY |
29
28
  (typeof fsSync.constants.O_DIRECTORY === "number" ? fsSync.constants.O_DIRECTORY : 0));
@@ -78,13 +77,16 @@ async function copyPinnedSource(params) {
78
77
  if (params.native && params.targetNativeParent) {
79
78
  const closeFd = captureNativeFdClose(params.native);
80
79
  for (const method of ["clone", "copy-file-range"]) {
80
+ const native = selectNativeBinding(params.native, method === "clone" ? "cloneFileExclusive" : "copyFileRangeExclusive");
81
+ if (!native)
82
+ continue;
81
83
  let nativeFd;
82
84
  try {
83
85
  if (method === "clone") {
84
- nativeFd = params.native.cloneFileExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
86
+ nativeFd = native.cloneFileExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
85
87
  }
86
88
  else {
87
- const copied = await params.native.copyFileRangeExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
89
+ const copied = await native.copyFileRangeExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
88
90
  if (copied.errorCode) {
89
91
  throw Object.assign(new Error(copied.errorMessage ?? "native copy failed"), {
90
92
  code: copied.errorCode,
@@ -184,7 +186,7 @@ async function removeCreatedTargetIfUnchanged(targetPath, identity) {
184
186
  async function syncPublishedParent(params) {
185
187
  params.failure.phase = "directory-sync";
186
188
  try {
187
- await getFsSafeTestHooks()?.beforePublishDirectorySync?.(params.method, params.targetPath, params.failure.targetIdentity);
189
+ await getFsSafeTestHooks()?.beforePublishDirectorySync?.(params.method, params.targetPath, params.failure.exactTargetIdentity);
188
190
  // PinnedDirectory.sync() revalidates descriptor/path identity immediately
189
191
  // before and after fsync; keep that check inside the shared sync boundary.
190
192
  return await params.parent.sync();
@@ -195,6 +197,12 @@ async function syncPublishedParent(params) {
195
197
  }
196
198
  }
197
199
  export async function publishFileExclusive(params) {
200
+ const expectedInput = params.expectedSourceIdentity;
201
+ const expected = expectedInput && identityFromReceipt(expectedInput);
202
+ const expectedSourceIdentity = expected && { dev: expected.dev, ino: expected.ino };
203
+ if (expectedSourceIdentity && !hasSafeFileIdentity(expectedSourceIdentity)) {
204
+ throw new FsSafeError("path-mismatch", "publication source identity is not exact; pass fs.stat(path, { bigint: true })");
205
+ }
198
206
  const sourcePathInput = admitStandalonePublicationPath(params.sourcePath, "publication source uses a Windows filesystem namespace alias");
199
207
  const targetPathInput = admitStandalonePublicationPath(params.targetPath, "publication target uses a Windows filesystem namespace alias");
200
208
  const parentReceiptInput = params.parentReceipt;
@@ -234,18 +242,13 @@ export async function publishFileExclusive(params) {
234
242
  parent = await pinDirectory(parentReceipt ?? parentPath, {
235
243
  label: "publication parent",
236
244
  });
237
- const sourceIdentity = fsSync.fstatSync(source.fd);
238
245
  const sourceExactIdentity = fsSync.fstatSync(source.fd, { bigint: true });
239
246
  const sourcePathExactIdentity = fsSync.lstatSync(sourcePath, { bigint: true });
240
- const expectedSourceIdentity = params.expectedSourceIdentity;
241
247
  if (sourcePathExactIdentity.isSymbolicLink() ||
242
248
  !sourcePathExactIdentity.isFile() ||
243
249
  !sameFileIdentity(sourcePathExactIdentity, sourceExactIdentity) ||
244
250
  (expectedSourceIdentity &&
245
- !sameFileIdentity(expectedSourceIdentity, typeof expectedSourceIdentity.dev === "bigint" ||
246
- typeof expectedSourceIdentity.ino === "bigint"
247
- ? sourceExactIdentity
248
- : sourceIdentity))) {
251
+ !sameFileIdentity(expectedSourceIdentity, sourceExactIdentity))) {
249
252
  throw new FsSafeError("path-mismatch", "publication source identity did not match");
250
253
  }
251
254
  const assertSourceCurrent = () => assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceExactIdentity });
@@ -258,19 +261,22 @@ export async function publishFileExclusive(params) {
258
261
  else
259
262
  assertSourceCurrent();
260
263
  assertPublishedTargetCurrent(targetPath, identity, "publication target changed during directory sync");
261
- return { method, ...(failure.fallback ? { fallback: failure.fallback } : {}), identity: fsSync.fstatSync(handle.fd), directorySync };
264
+ const observed = fsSync.fstatSync(handle.fd, { bigint: true });
265
+ if (!sameFileIdentity(observed, identity))
266
+ throw new FsSafeError("path-mismatch", "publication descriptor changed");
267
+ return { method, ...(failure.fallback ? { fallback: failure.fallback } : {}), identity: numberStats(observed), exactIdentity: exactFileIdentity(observed), directorySync };
262
268
  };
263
269
  await parent.assertCurrent();
264
270
  assertSourceCurrent();
265
271
  const native = strategy === "rename-noreplace"
266
- ? requireNativeBinding()
267
- : getNativeBinding();
272
+ ? requireNativeBinding("renameNoReplace")
273
+ : getNativeBinding("linkBeneath");
268
274
  if (native) {
269
275
  sourceNativeParent = await openNativeParent(sourcePath);
270
276
  targetNativeParent = await openNativeParent(targetPath);
271
277
  }
272
278
  if (strategy === "rename-noreplace") {
273
- const binding = requireNativeBinding();
279
+ const binding = requireNativeBinding("renameNoReplace");
274
280
  try {
275
281
  const fallback = nativeMoveNoReplace(binding, [
276
282
  sourceNativeParent.handle.fd, sourceNativeParent.basename,
@@ -313,7 +319,7 @@ export async function publishFileExclusive(params) {
313
319
  return await finishPublication("rename-noreplace", source, sourceExactIdentity);
314
320
  }
315
321
  try {
316
- if (native) {
322
+ if (native?.linkBeneath) {
317
323
  native.linkBeneath(sourceNativeParent.handle.fd, sourceNativeParent.basename, targetNativeParent.handle.fd, targetNativeParent.basename);
318
324
  }
319
325
  else {
@@ -1,3 +1,4 @@
1
+ import type { ExactFileIdentity } from "./exact-identity.js";
1
2
  import type { Stats } from "node:fs";
2
3
  import type { FileHandle } from "node:fs/promises";
3
4
  import type { ContainmentGuarantee } from "./containment.js";
@@ -6,6 +7,7 @@ export type ReadResult = {
6
7
  containment: ContainmentGuarantee;
7
8
  realPath: string;
8
9
  stat: Stats;
10
+ exactIdentity: ExactFileIdentity;
9
11
  };
10
12
  type OpenedFile = Omit<ReadResult & {
11
13
  handle: FileHandle;
@@ -1,18 +1,17 @@
1
1
  import { normalizeMaxBytes } from "./byte-budget.js";
2
- import { readFileHandleBounded } from "./bounded-read.js";
2
+ import { readFileHandleBoundedWithSize } from "./bounded-read.js";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  export async function readOpenedFileSafely(params) {
5
5
  const maxBytes = normalizeMaxBytes(params.maxBytes);
6
6
  if (maxBytes !== undefined && params.opened.stat.size > maxBytes) {
7
7
  throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got ${params.opened.stat.size})`);
8
8
  }
9
- const buffer = maxBytes === undefined
10
- ? await params.opened.handle.readFile()
11
- : await readFileHandleBounded(params.opened.handle, maxBytes);
9
+ const buffer = await readFileHandleBoundedWithSize(params.opened.handle, maxBytes ?? Infinity, params.opened.stat.size);
12
10
  return {
13
11
  buffer,
14
12
  containment: params.opened.containment,
15
13
  realPath: params.opened.realPath,
16
14
  stat: params.opened.stat,
15
+ exactIdentity: params.opened.exactIdentity,
17
16
  };
18
17
  }
@@ -4,9 +4,41 @@ import { getSystemErrorName } from "node:util";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { getNativeBinding } from "./native.js";
6
6
  import { getFsSafeNativeConfig } from "./native-config.js";
7
+ import { isWindowsAppContainer } from "./windows-appcontainer.js";
7
8
  const runtimePlatform = process.versions.bun ? platform() : undefined;
8
9
  const bunPosix = runtimePlatform === "darwin" || runtimePlatform === "linux" ? runtimePlatform : undefined;
9
10
  function resolve(input, native) {
11
+ if (native && process.platform === "win32") {
12
+ try {
13
+ return fs.realpathSync.native(input);
14
+ }
15
+ catch (error) {
16
+ if (error.code !== "EPERM" || error.syscall !== "realpath")
17
+ throw error;
18
+ let binding;
19
+ try {
20
+ binding = getNativeBinding();
21
+ }
22
+ catch {
23
+ binding = undefined;
24
+ }
25
+ if (!binding || !isWindowsAppContainer(binding))
26
+ throw error;
27
+ if (!binding?.resolveWindowsCanonicalPath) {
28
+ throw new FsSafeError("helper-unavailable", "Windows canonicalization after a denied DOS-name query requires the native binding", { cause: error });
29
+ }
30
+ let resolved;
31
+ try {
32
+ resolved = binding.resolveWindowsCanonicalPath(input);
33
+ }
34
+ catch (cause) {
35
+ throw new FsSafeError("path-mismatch", "Windows canonical path cannot be verified safely", { cause });
36
+ }
37
+ if (resolved === null)
38
+ throw error;
39
+ return resolved;
40
+ }
41
+ }
10
42
  if (bunPosix) {
11
43
  // Bun 1.4.2 normalizes native path components and opens the leaf for reads.
12
44
  // The Rust OS resolver preserves permissions, locks and physical spelling.