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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -0,0 +1,170 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs, {} from "node:fs";
3
+ import fsp from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
6
+ import { FsSafeError } from "./errors.js";
7
+ import { statIdentityFromNative } from "./native-stat-identity.js";
8
+ import { getNativeBinding } from "./native.js";
9
+ import { isFsSafeNativeRequired } from "./native-config.js";
10
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
11
+ import { timestampSeconds } from "./copy-metadata.js";
12
+ import { syncDirectoryBestEffort } from "./directory-durability.js";
13
+ import { noReplaceUnavailable } from "./native-noreplace.js";
14
+ import { createSuppressedError } from "./suppressed-error.js";
15
+ /** Shared by tree byte copying and per-file copying; never resolves the target. */
16
+ export async function readCopyLinkTarget(source) {
17
+ return await fsp.readlink(source, { encoding: "buffer" });
18
+ }
19
+ export function assertCopyLinkAvailable() {
20
+ const native = getNativeBinding();
21
+ if (process.platform === "darwin" && !native?.createCopySymlink) {
22
+ throw new FsSafeError("helper-unavailable", "exclusive macOS link copying requires the native helper; link(2) follows its source");
23
+ }
24
+ if (process.platform === "win32" && (!native?.copyLinkExclusive || !native.publishCopyLink || !native.removeCopyLink)) {
25
+ throw new FsSafeError("helper-unavailable", "copying Windows links without following them requires the native helper");
26
+ }
27
+ if (native && process.platform !== "win32" && !native.createCopySymlink) {
28
+ throw new FsSafeError("helper-unavailable", "native link copying is unavailable");
29
+ }
30
+ }
31
+ export async function admitCopyLink(sourcePath, preserveMetadata, signal) {
32
+ const parent = await createAsyncDirectoryGuard(path.dirname(sourcePath), { bigint: true });
33
+ sourcePath = path.join(parent.realPath, path.basename(sourcePath));
34
+ const identity = fs.lstatSync(sourcePath, { bigint: true });
35
+ if (!identity.isSymbolicLink())
36
+ return undefined;
37
+ const target = process.platform === "win32" ? undefined : await readCopyLinkTarget(sourcePath);
38
+ const verifySource = async () => {
39
+ signal?.throwIfAborted();
40
+ await assertAsyncDirectoryGuard(parent);
41
+ const current = inspectFileIdentitySync(() => fs.lstatSync(sourcePath, { bigint: true }), identity);
42
+ if (!current.isSymbolicLink() || current.mtimeNs !== identity.mtimeNs || current.ctimeNs !== identity.ctimeNs) {
43
+ throw new FsSafeError("path-mismatch", "copy source link changed");
44
+ }
45
+ };
46
+ await verifySource();
47
+ return { kind: "link", stageBeforePublish: true, sourcePath, identity, target, preserveMetadata, signal, verifySource };
48
+ }
49
+ /** The caller has admitted the destination parent under Root's mutation policy. */
50
+ export async function copyLinkAtParent(params, input, parent, native) {
51
+ const temporaryName = `.fs-safe-${randomUUID()}.tmp`;
52
+ const temporaryPath = path.join(parent.realPath, temporaryName);
53
+ const destination = path.join(parent.realPath, params.basename);
54
+ let identity;
55
+ let sourceParent;
56
+ let failure;
57
+ const assertStage = () => {
58
+ assertSyncDirectoryGuard(parent);
59
+ const stat = inspectFileIdentitySync(() => fs.lstatSync(temporaryPath, { bigint: true }), identity);
60
+ if (!stat.isSymbolicLink() || stat.nlink !== 1n)
61
+ throw new FsSafeError("path-mismatch", "copy link stage changed");
62
+ };
63
+ try {
64
+ await input.verifySource();
65
+ await assertAsyncDirectoryGuard(parent);
66
+ if (process.platform === "win32") {
67
+ if (!native?.binding.copyLinkExclusive)
68
+ throw new FsSafeError("helper-unavailable", "native Windows link copying is unavailable");
69
+ const sourceGuard = await createAsyncDirectoryGuard(path.dirname(input.sourcePath), { bigint: true });
70
+ sourceParent = await fsp.open(sourceGuard.realPath, fs.constants.O_RDONLY);
71
+ inspectFileIdentitySync(() => fs.fstatSync(sourceParent.fd, { bigint: true }), sourceGuard.stat);
72
+ await input.verifySource();
73
+ params.assertBeforeMutation?.();
74
+ assertSyncDirectoryGuard(parent);
75
+ identity = native.binding.copyLinkExclusive(sourceParent.fd, path.basename(input.sourcePath), native.fd, temporaryName, input.identity.dev, input.identity.ino, input.preserveMetadata, params.mode);
76
+ }
77
+ else {
78
+ if (native && !native.binding.createCopySymlink)
79
+ throw new FsSafeError("helper-unavailable", "native link copying is unavailable");
80
+ params.assertBeforeMutation?.();
81
+ assertSyncDirectoryGuard(parent);
82
+ if (native?.binding.createCopySymlink) {
83
+ const created = native.binding.createCopySymlink(native.fd, temporaryName, input.target, params.mode, input.preserveMetadata ? input.identity.atimeNs : undefined, input.preserveMetadata ? input.identity.mtimeNs : undefined);
84
+ identity = { dev: statIdentityFromNative(created.dev), ino: statIdentityFromNative(created.ino) };
85
+ if (created.errorCode)
86
+ throw Object.assign(new Error(created.errorMessage), { code: created.errorCode });
87
+ }
88
+ else {
89
+ fs.symlinkSync(input.target, temporaryPath);
90
+ identity = fs.lstatSync(temporaryPath, { bigint: true });
91
+ }
92
+ if (!native && input.preserveMetadata) {
93
+ assertStage();
94
+ fs.lutimesSync(temporaryPath, timestampSeconds(input.identity.atimeNs), timestampSeconds(input.identity.mtimeNs));
95
+ }
96
+ }
97
+ assertStage();
98
+ await input.verifySource();
99
+ params.assertBeforeMutation?.();
100
+ input.signal?.throwIfAborted();
101
+ assertStage();
102
+ if (process.platform === "win32") {
103
+ native.binding.publishCopyLink(native.fd, temporaryName, params.basename, identity.dev, identity.ino);
104
+ }
105
+ else if (native) {
106
+ const renameNoReplace = native.binding.renameNoReplace;
107
+ if (!renameNoReplace)
108
+ throw new FsSafeError("helper-unavailable", "native link publication is unavailable");
109
+ try {
110
+ renameNoReplace(native.fd, temporaryName, native.fd, params.basename);
111
+ }
112
+ catch (error) {
113
+ const unavailable = noReplaceUnavailable(error, "link copy publication", true);
114
+ if (!unavailable || isFsSafeNativeRequired() || process.platform !== "linux")
115
+ throw error;
116
+ // Linux link(2) links the symlink inode itself, never its target.
117
+ assertStage();
118
+ fs.linkSync(temporaryPath, destination);
119
+ }
120
+ }
121
+ else {
122
+ fs.linkSync(temporaryPath, destination);
123
+ }
124
+ params.onPublished?.(identity);
125
+ assertSyncDirectoryGuard(parent);
126
+ const final = inspectFileIdentitySync(() => fs.lstatSync(destination, { bigint: true }), identity);
127
+ if (!final.isSymbolicLink())
128
+ throw new FsSafeError("path-mismatch", "published copy link changed");
129
+ if (params.sync !== false)
130
+ await syncDirectoryBestEffort(parent.realPath);
131
+ await input.verifySource();
132
+ return identity;
133
+ }
134
+ catch (error) {
135
+ failure = { error };
136
+ throw error;
137
+ }
138
+ finally {
139
+ let cleanupFailure;
140
+ try {
141
+ if (identity) {
142
+ assertSyncDirectoryGuard(parent);
143
+ const remaining = fs.lstatSync(temporaryPath, { bigint: true, throwIfNoEntry: false });
144
+ if (remaining) {
145
+ inspectFileIdentitySync(() => remaining, identity);
146
+ if (!remaining.isSymbolicLink())
147
+ throw new FsSafeError("path-mismatch", "copy link cleanup identity changed");
148
+ if (process.platform === "win32")
149
+ native.binding.removeCopyLink(native.fd, temporaryName, identity.dev, identity.ino);
150
+ else
151
+ fs.unlinkSync(temporaryPath);
152
+ }
153
+ }
154
+ }
155
+ catch (error) {
156
+ cleanupFailure = { error };
157
+ }
158
+ try {
159
+ await sourceParent?.close();
160
+ }
161
+ catch (error) {
162
+ cleanupFailure = { error: cleanupFailure
163
+ ? createSuppressedError(error, cleanupFailure.error, "copy link cleanup and close failed") : error };
164
+ }
165
+ if (cleanupFailure)
166
+ throw failure
167
+ ? createSuppressedError(cleanupFailure.error, failure.error, "copy link operation and cleanup failed")
168
+ : cleanupFailure.error;
169
+ }
170
+ }
@@ -0,0 +1,9 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ export type CopyMetadata = Readonly<{
3
+ atimeNs: bigint;
4
+ mtimeNs: bigint;
5
+ restoreNative?: (fd: number) => void;
6
+ }>;
7
+ export declare function timestampSeconds(nanoseconds: bigint): string;
8
+ export declare function captureCopyMetadata(fd: number, stat: BigIntStats): CopyMetadata;
9
+ export declare function restoreCopyMetadata(fd: number, metadata: CopyMetadata | undefined): void;
@@ -0,0 +1,39 @@
1
+ import fs, {} from "node:fs";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { getNativeBinding } from "./native.js";
4
+ import { isFsSafeNativeRequired } from "./native-config.js";
5
+ export function timestampSeconds(nanoseconds) {
6
+ // Numeric strings retain fractional and pre-epoch times in Node's utimes API.
7
+ return String(Number(nanoseconds / 1000000000n) + Number(nanoseconds % 1000000000n) / 1e9);
8
+ }
9
+ export function captureCopyMetadata(fd, stat) {
10
+ let restoreNative;
11
+ const native = getNativeBinding();
12
+ if (process.platform === "win32") {
13
+ if (native?.readCopyMetadata && native.restoreCopyMetadata) {
14
+ const snapshot = native.readCopyMetadata(fd);
15
+ const restore = native.restoreCopyMetadata.bind(native);
16
+ restoreNative = target => restore(target, snapshot);
17
+ }
18
+ else if (isFsSafeNativeRequired()) {
19
+ throw new FsSafeError("helper-unavailable", "native Windows copy metadata preservation is unavailable");
20
+ }
21
+ }
22
+ else if (native?.restoreCopyFileTimes) {
23
+ const restore = native.restoreCopyFileTimes.bind(native);
24
+ const { atimeNs, mtimeNs } = stat;
25
+ restoreNative = target => restore(target, atimeNs, mtimeNs);
26
+ }
27
+ else if (isFsSafeNativeRequired()) {
28
+ throw new FsSafeError("helper-unavailable", "native copy timestamp preservation is unavailable");
29
+ }
30
+ return Object.freeze({ atimeNs: stat.atimeNs, mtimeNs: stat.mtimeNs, restoreNative });
31
+ }
32
+ export function restoreCopyMetadata(fd, metadata) {
33
+ if (!metadata)
34
+ return;
35
+ if (metadata.restoreNative)
36
+ metadata.restoreNative(fd);
37
+ else
38
+ fs.futimesSync(fd, timestampSeconds(metadata.atimeNs), timestampSeconds(metadata.mtimeNs));
39
+ }
@@ -5,6 +5,7 @@ import { FsSafeError } from "./errors.js";
5
5
  import { sameFileIdentity } from "./file-identity.js";
6
6
  import { realpathSync } from "./realpath.js";
7
7
  import { resolveRootPathSync, ROOT_PATH_ALIAS_POLICIES } from "./root-path.js";
8
+ import { hasWindowsPathAlias } from "./windows-path-alias.js";
8
9
  function admitUncachedCopyPath(input, create = false, admittedRoot) {
9
10
  if (!path.isAbsolute(input.rootPath) || !path.isAbsolute(input.absolutePath)) {
10
11
  throw new FsSafeError("invalid-path", "copy roots and paths must be absolute");
@@ -50,15 +51,23 @@ export function createCopyPathAdmissionCache() {
50
51
  const assertParent = ({ root, parent, chain }) => {
51
52
  assertSyncDirectoryGuard(root);
52
53
  for (const guard of chain) {
54
+ // The full parent guard immediately below includes this exact identity.
55
+ if (guard === parent)
56
+ continue;
53
57
  inspectDirectoryIdentitySync(guard.dir, { dev: BigInt(guard.stat.dev), ino: BigInt(guard.stat.ino) });
54
58
  }
55
- assertSyncDirectoryGuard(parent);
59
+ if (parent !== root)
60
+ assertSyncDirectoryGuard(parent);
56
61
  assertSyncDirectoryGuard(root);
57
62
  };
58
63
  const admit = (input, create = false) => {
59
64
  // Keep the public resolver for Windows aliases and noncanonical spellings,
60
65
  // including raw dot segments whose traversal must not be normalized away.
61
- if (process.platform === "win32" || !path.isAbsolute(input.rootPath) ||
66
+ const ordinaryWindowsPath = process.platform !== "win32" || (/^[a-z]:\\/iu.test(input.rootPath) &&
67
+ !hasWindowsPathAlias(input.rootPath, "filesystem") &&
68
+ !hasWindowsPathAlias(input.absolutePath, "filesystem") &&
69
+ !input.absolutePath.slice(3).split("\\").some(segment => /[. ]$/u.test(segment) || segment.includes("~")));
70
+ if (!ordinaryWindowsPath || !path.isAbsolute(input.rootPath) ||
62
71
  !path.isAbsolute(input.absolutePath) || input.rootPath.includes("\0") || input.absolutePath.includes("\0") ||
63
72
  path.resolve(input.rootPath) !== input.rootPath || path.resolve(input.absolutePath) !== input.absolutePath ||
64
73
  !input.absolutePath.startsWith(input.rootPath.endsWith(path.sep) ? input.rootPath : `${input.rootPath}${path.sep}`)) {
@@ -1,13 +1,11 @@
1
+ import { type ExactIdentityReceipt, type ExactFileIdentity } from "./file-identity.js";
1
2
  import { type CopyPath } from "./copy-path-admission.js";
2
3
  import { type CopyCloneMode } from "./copy-policy.js";
3
4
  import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
4
5
  export type CopyRootFileSyncOptions = {
5
6
  source: CopyPath;
6
7
  destination: CopyPath;
7
- expectedSourceIdentity?: {
8
- dev: bigint;
9
- ino: bigint;
10
- };
8
+ expectedSourceIdentity?: ExactFileIdentity | ExactIdentityReceipt;
11
9
  clone?: CopyCloneMode;
12
10
  maxBytes?: number;
13
11
  mode?: number;
@@ -1,3 +1,4 @@
1
+ import { identityFromReceipt } from "./file-identity.js";
1
2
  import fs, {} from "node:fs";
2
3
  import path from "node:path";
3
4
  import { normalizeMaxBytes } from "./byte-budget.js";
@@ -50,7 +51,8 @@ function copyRootFileWithAdmissionSync(options, cache) {
50
51
  const targetInput = options.destination;
51
52
  const targetPath = { rootPath: targetInput.rootPath, absolutePath: targetInput.absolutePath };
52
53
  const expectedInput = options.expectedSourceIdentity;
53
- const expectedSourceIdentity = expectedInput === undefined ? undefined : { dev: expectedInput.dev, ino: expectedInput.ino };
54
+ const expected = expectedInput === undefined ? undefined : identityFromReceipt(expectedInput);
55
+ const expectedSourceIdentity = expected && { dev: expected.dev, ino: expected.ino };
54
56
  const clone = resolveCopyCloneMode(options.clone, "never");
55
57
  const maxBytes = normalizeMaxBytes(options.maxBytes);
56
58
  const mode = options.mode;
@@ -68,7 +70,7 @@ function copyRootFileWithAdmissionSync(options, cache) {
68
70
  let parentOpen = false;
69
71
  let target;
70
72
  let identity;
71
- const native = getNativeBinding();
73
+ const native = process.platform !== "win32" ? getNativeBinding("copyFileExclusiveSync", "removeStagedFile") : getNativeBinding();
72
74
  const nativeCopy = process.platform !== "win32" && native?.copyFileExclusiveSync && native.removeStagedFile
73
75
  ? native.copyFileExclusiveSync.bind(native) : undefined;
74
76
  try {
@@ -4,12 +4,9 @@ import fsp, {} from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
7
+ import { timestampSeconds } from "./copy-metadata.js";
8
+ import { readCopyLinkTarget } from "./copy-link.js";
7
9
  import { assertStagedDirectoryCurrent, exactIdentityMatches, openStagedDirectory, } from "./staged-directory.js";
8
- function timestampSeconds(nanoseconds) {
9
- // Dates discard sub-millisecond precision; negative numbers mean "now" in
10
- // Node's utimes API. Numeric strings retain fractional, pre-epoch timestamps.
11
- return String(Number(nanoseconds / 1000000000n) + Number(nanoseconds % 1000000000n) / 1e9);
12
- }
13
10
  /** Byte-copy adapter for the shared immutable-source, caller-owned namespace contract. */
14
11
  export async function copyOwnedTree(source, destination, options) {
15
12
  const callerSignal = options.signal ? AbortSignal.any([options.signal]) : undefined;
@@ -145,7 +142,7 @@ export async function copyOwnedTree(source, destination, options) {
145
142
  else if (child.isSymbolicLink()) {
146
143
  const link = process.platform === "win32"
147
144
  ? await fsp.readlink(childSource)
148
- : await fsp.readlink(childSource, { encoding: "buffer" });
145
+ : await readCopyLinkTarget(childSource);
149
146
  let type;
150
147
  if (process.platform === "win32") {
151
148
  // Older Node releases infer link type from the destination, where a
@@ -21,7 +21,7 @@ function closeDescriptor(fd, failure) {
21
21
  }
22
22
  /** Inspect an existing real directory without creating probe files. */
23
23
  export function probeTreeClone(parentPath) {
24
- const native = getNativeBinding();
24
+ const native = getNativeBinding("probeTreeClone");
25
25
  if (!native)
26
26
  return undefined;
27
27
  const parent = openStagedDirectory(assertAbsolutePathInput(parentPath));
@@ -64,9 +64,9 @@ async function materializeTree(source, destination, options, policy) {
64
64
  throw new FsSafeError("invalid-path", "clone concurrency must be an integer between 1 and 32");
65
65
  }
66
66
  const native = policy === "always"
67
- ? requireNativeBinding()
67
+ ? requireNativeBinding("probeTreeClone", "cloneTree")
68
68
  : policy === "auto"
69
- ? getNativeBinding()
69
+ ? getNativeBinding("probeTreeClone", "cloneTree")
70
70
  : undefined;
71
71
  const target = assertAbsolutePathInput(destination);
72
72
  const name = path.basename(target);
@@ -204,7 +204,7 @@ function privateDirectoryBackend() {
204
204
  if (getFsSafeNativeConfig().mode === "require") {
205
205
  throw new FsSafeError("helper-unavailable", "private directory creation requires an up-to-date native parent identity capability");
206
206
  }
207
- warnNativeFallback("windows-private-directory", "Private Windows directory creation uses a slower built-in system command.");
207
+ warnNativeFallback("windows-private-directory", "Private Windows directory creation launches the packaged .NET helper for each check or creation.", "createPrivateDirectoryWithParentIdentity");
208
208
  return undefined;
209
209
  }
210
210
  export async function createDirectory(targetPath, options = {}) {
@@ -53,17 +53,17 @@ export function assertPrivateFileCreationAvailable() {
53
53
  typeof native.verifyPrivateWindowsFile !== "function")
54
54
  unavailable();
55
55
  }
56
- function commandFallback() {
56
+ function commandFallback(capability) {
57
57
  if (getFsSafeNativeConfig().mode === "require")
58
58
  unavailable();
59
- warnNativeFallback("windows-private-creation", "Private Windows creation uses a slower built-in system command.");
59
+ warnNativeFallback("windows-private-creation", "Private Windows creation launches the packaged .NET helper for each check or creation.", capability);
60
60
  }
61
61
  function creationOperation(name) {
62
62
  const native = getNativeBinding();
63
63
  const operation = native?.[name];
64
64
  if (typeof operation === "function")
65
65
  return operation.bind(native);
66
- commandFallback();
66
+ commandFallback(name);
67
67
  return undefined;
68
68
  }
69
69
  /** Full Windows identity, with security validation when requested; no repairs. */
@@ -300,7 +300,10 @@ export function assertBeforeCreation(selected, permissions, assertion, kind) {
300
300
  if (permissions.private)
301
301
  assertDarwinCreationDirectoryAcl(selected.parent.dir, selected.parent.stat, kind);
302
302
  assertSynchronousCallbackResult(assertion?.(), "assertBeforeMutation");
303
- selected.assertParent();
303
+ // Ordinary callers enter immediately after prepareCreationPath's identical fence.
304
+ // A callback or private ACL inspection needs a fresh observation afterward.
305
+ if (assertion !== undefined || permissions.private)
306
+ selected.assertParent();
304
307
  if (permissions.private && kind === "directory")
305
308
  assertDarwinPrivateDirectoryMode(permissions.mode);
306
309
  if (permissions.private)
@@ -0,0 +1,12 @@
1
+ export type DarwinAclInspection = {
2
+ kind: "none";
3
+ } | {
4
+ kind: "present";
5
+ inheritsToFiles: boolean;
6
+ inheritsToDirectories: boolean;
7
+ } | {
8
+ kind: "unknown";
9
+ reason: string;
10
+ };
11
+ /** Inspect one no-follow descriptor. Unknown is never evidence of no ACL. */
12
+ export declare function inspectDarwinAcl(path: string): DarwinAclInspection;
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs";
2
+ import { getNativeBinding } from "./native.js";
3
+ /** Inspect one no-follow descriptor. Unknown is never evidence of no ACL. */
4
+ export function inspectDarwinAcl(path) {
5
+ if (process.platform !== "darwin")
6
+ return { kind: "unknown", reason: "unsupported-platform" };
7
+ let fd;
8
+ try {
9
+ const native = getNativeBinding();
10
+ if (!native?.inspectDarwinAcl)
11
+ return { kind: "unknown", reason: "helper-unavailable" };
12
+ fd = fs.openSync(path, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
13
+ const facts = native.inspectDarwinAcl(fd);
14
+ if (facts.state === "absent" || facts.state === "empty")
15
+ return { kind: "none" };
16
+ if (facts.state === "present" && typeof facts.inheritsToFiles === "boolean" &&
17
+ typeof facts.inheritsToDirectories === "boolean") {
18
+ return { kind: "present", inheritsToFiles: facts.inheritsToFiles, inheritsToDirectories: facts.inheritsToDirectories };
19
+ }
20
+ return { kind: "unknown", reason: "incomplete-acl-facts" };
21
+ }
22
+ catch (error) {
23
+ return { kind: "unknown", reason: error instanceof Error ? error.message : String(error) };
24
+ }
25
+ finally {
26
+ if (fd !== undefined)
27
+ fs.closeSync(fd);
28
+ }
29
+ }
@@ -1,5 +1,6 @@
1
+ import type { ExactFileIdentity, ExactIdentityReceipt } from "./exact-identity.js";
1
2
  import { type BigIntStats, type Stats } from "node:fs";
2
- import type { FileIdentityStat } from "./file-identity.js";
3
+ import type { FileIdentityInput } from "./file-identity.js";
3
4
  export type DirectorySyncOutcome = {
4
5
  status: "synced";
5
6
  } | {
@@ -10,14 +11,16 @@ export type DirectoryReceipt<T extends Stats | BigIntStats = Stats> = {
10
11
  path: string;
11
12
  realPath: string;
12
13
  identity: T;
14
+ exactIdentity?: ExactFileIdentity;
13
15
  };
16
+ export type ExactDirectoryReceipt = DirectoryReceipt & ExactIdentityReceipt;
14
17
  export type DurableDirectoryReceipt = DirectoryReceipt & {
15
18
  parentSync: DirectorySyncOutcome | {
16
19
  status: "not-needed";
17
20
  };
18
21
  };
19
22
  export type PinnedDirectory = {
20
- readonly receipt: DirectoryReceipt;
23
+ readonly receipt: ExactDirectoryReceipt;
21
24
  assertCurrent(): Promise<void>;
22
25
  sync(): Promise<DirectorySyncOutcome>;
23
26
  close(): Promise<void>;
@@ -26,7 +29,7 @@ export type EnsureDurableDirectoryOptions = {
26
29
  directoryPath: string;
27
30
  label?: string;
28
31
  mode?: number;
29
- expectedExistingIdentity?: FileIdentityStat;
32
+ expectedExistingIdentity?: FileIdentityInput;
30
33
  create?: (directoryPath: string) => Promise<void>;
31
34
  };
32
35
  export declare function pinDirectory(directory: string | DirectoryReceipt<Stats | BigIntStats>, options?: {
@@ -40,4 +43,4 @@ export declare function syncDirectorySync(directory: string | DirectoryReceipt<S
40
43
  }): DirectorySyncOutcome;
41
44
  export declare function syncDirectoryBestEffort(directoryPath: string): Promise<void>;
42
45
  export declare function syncDirectoryBestEffortSync(directoryPath: string): void;
43
- export declare function ensureDurableDirectory(options: EnsureDurableDirectoryOptions): Promise<DurableDirectoryReceipt>;
46
+ export declare function ensureDurableDirectory(options: EnsureDurableDirectoryOptions): Promise<DurableDirectoryReceipt & ExactIdentityReceipt>;
@@ -1,7 +1,7 @@
1
1
  import fsSync from "node:fs";
2
2
  import path from "node:path";
3
3
  import { FsSafeError } from "./errors.js";
4
- import { sameFileIdentity } from "./file-identity.js";
4
+ import { hasSafeFileIdentity, sameFileIdentity } from "./file-identity.js";
5
5
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
6
6
  import { isNotFoundPathError } from "./path.js";
7
7
  import { directoryComponentNotDirectoryError } from "./root-errors.js";
@@ -9,6 +9,9 @@ import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservi
9
9
  import { realpathSync } from "./realpath.js";
10
10
  import { directoryEntryPath } from "./directory-entry-path.js";
11
11
  import { assertStatObservationSync, inspectStatObservationSync, } from "./stat-observation.js";
12
+ // Numeric metadata is retained for existing internal consumers; rounded IDs
13
+ // must never become authority for a later comparison.
14
+ const exactGuardIdentities = new WeakMap();
12
15
  export async function readDirectoryIdentity(dir) {
13
16
  const guard = await createAsyncDirectoryGuard(dir, { bigint: true });
14
17
  return Object.freeze({ dev: guard.stat.dev, ino: guard.stat.ino, realPath: guard.realPath });
@@ -43,9 +46,19 @@ export function captureDirectoryGuard(dir, mode, options) {
43
46
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
44
47
  throw directoryComponentNotDirectoryError();
45
48
  }
49
+ let exactIdentity;
50
+ if (!hasSafeFileIdentity(stat)) {
51
+ const exact = fsSync.lstatSync(operationPath, { bigint: true });
52
+ if (exact.isSymbolicLink() || !exact.isDirectory())
53
+ throw directoryComponentNotDirectoryError();
54
+ exactIdentity = { dev: exact.dev, ino: exact.ino };
55
+ }
46
56
  const realPath = mode === "native" ? realpathSync.native(operationPath) : realpathSync(operationPath);
47
57
  assertNoWindowsPathAlias(realPath, "filesystem");
48
- return { dir, realPath, stat };
58
+ const guard = { dir, realPath, stat };
59
+ if (exactIdentity)
60
+ exactGuardIdentities.set(guard, exactIdentity);
61
+ return guard;
49
62
  }
50
63
  export async function assertAsyncDirectoryGuard(guard) {
51
64
  assertDirectoryGuard(guard, "native");
@@ -59,9 +72,11 @@ export function assertDirectoryGuard(guard, mode) {
59
72
  const expectedRealPath = guard.realPath;
60
73
  assertNoWindowsPathAlias(expectedRealPath, "filesystem");
61
74
  const expectedStat = guard.stat;
62
- const expectedIdentity = { dev: expectedStat.dev, ino: expectedStat.ino };
75
+ const suppliedIdentity = { dev: expectedStat.dev, ino: expectedStat.ino };
76
+ const retainedIdentity = exactGuardIdentities.get(guard);
77
+ const expectedIdentity = retainedIdentity ?? suppliedIdentity;
63
78
  const stat = typeof expectedIdentity.dev === "bigint" && typeof expectedIdentity.ino === "bigint"
64
- ? inspectDirectoryIdentityAtPathSync(operationPath, {
79
+ ? retainedIdentity ? fsSync.lstatSync(operationPath, { bigint: true }) : inspectDirectoryIdentityAtPathSync(operationPath, {
65
80
  dev: expectedIdentity.dev,
66
81
  ino: expectedIdentity.ino,
67
82
  })
@@ -74,7 +89,7 @@ export function assertDirectoryGuard(guard, mode) {
74
89
  if (mode === "native" && !sameFileIdentity(stat, expectedIdentity)) {
75
90
  throw new FsSafeError("path-mismatch", "directory changed during operation");
76
91
  }
77
- const realPath = mode === "native" || typeof expectedIdentity.ino === "bigint"
92
+ const realPath = mode === "native" || typeof suppliedIdentity.ino === "bigint"
78
93
  ? realpathSync.native(operationPath) : realpathSync(operationPath);
79
94
  assertNoWindowsPathAlias(realPath, "filesystem");
80
95
  if ((mode === "normalized" && !sameFileIdentity(stat, expectedIdentity)) || realPath !== expectedRealPath) {
@@ -5,6 +5,7 @@ export type DirectoryModeChecks = {
5
5
  };
6
6
  export type DirectoryModeOwner = {
7
7
  verify(check?: () => void): Promise<void>;
8
+ verifyCanonical?(): Promise<boolean>;
8
9
  apply(mode: number, checks?: DirectoryModeChecks): Promise<void>;
9
10
  close(): Promise<void>;
10
11
  };
@@ -12,6 +13,7 @@ export declare function assertOwnedDirectory(expected: Stats | BigIntStats, actu
12
13
  /** Serializes use and close: even a queued path-based fd operation retains its descriptor. */
13
14
  export declare function ownDirectoryMode(params: {
14
15
  inspect: () => Promise<number>;
16
+ inspectCanonical?: () => number | undefined;
15
17
  chmod: (mode: number) => Promise<void>;
16
18
  prepareChmod?: () => Promise<void>;
17
19
  verifyChmod?: () => Promise<void>;
@@ -28,6 +30,7 @@ export declare function nodeDirectorySearchOnlyFlags(): {
28
30
  export declare function pinNodeDirectoryForMode(dirPath: string, options?: {
29
31
  expectedIdentity?: BigIntStats;
30
32
  ownerUid?: number;
33
+ canonicalPath?: string;
31
34
  }): Promise<DirectoryModeOwner>;
32
35
  /** Synchronous counterpart with the same no-follow, identity, and owner checks. */
33
36
  export declare function pinNodeDirectoryForModeSync(dirPath: string, options?: {
@@ -4,6 +4,8 @@ import { FsSafeError } from "./errors.js";
4
4
  import { inspectDirectoryIdentity, inspectDirectoryIdentitySync } from "./directory-guard.js";
5
5
  import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
6
6
  import { sameFileIdentityForCleanup } from "./file-identity.js";
7
+ import { getNativeBinding } from "./native.js";
8
+ import { nativePolicyDirectoryObserver } from "./native-policy-directory-observation.js";
7
9
  export function assertOwnedDirectory(expected, actual) {
8
10
  if (actual.isSymbolicLink() || !actual.isDirectory()) {
9
11
  throw new FsSafeError("not-file", "directory mode target must be a real directory");
@@ -23,7 +25,7 @@ export function ownDirectoryMode(params) {
23
25
  pending = operation.catch(() => undefined);
24
26
  return operation;
25
27
  };
26
- return {
28
+ const owner = {
27
29
  verify: (check) => enqueue(async () => {
28
30
  check?.();
29
31
  await params.inspect();
@@ -79,6 +81,14 @@ export function ownDirectoryMode(params) {
79
81
  return closing;
80
82
  },
81
83
  };
84
+ if (params.inspectCanonical) {
85
+ owner.verifyCanonical = async () => {
86
+ let verified = false;
87
+ await enqueue(async () => { verified = params.inspectCanonical() !== undefined; });
88
+ return verified;
89
+ };
90
+ }
91
+ return owner;
82
92
  }
83
93
  /** Darwin descriptor inspection avoids requesting directory-content reads. */
84
94
  export function nodeDarwinDirectoryMetadataFlags() {
@@ -131,7 +141,11 @@ export async function pinNodeDirectoryForMode(dirPath, options = {}) {
131
141
  proc = route.proc;
132
142
  return await fs.open(dirPath, route.flags | flags);
133
143
  });
144
+ let observer;
134
145
  try {
146
+ observer = options.canonicalPath === dirPath && ownerUid === undefined
147
+ ? nativePolicyDirectoryObserver(getNativeBinding(), handle.fd, dirPath)
148
+ : undefined;
135
149
  const inspect = async () => {
136
150
  const opened = await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }), expected);
137
151
  assertOwnedDirectory(expected, opened);
@@ -153,8 +167,18 @@ export async function pinNodeDirectoryForMode(dirPath, options = {}) {
153
167
  assertOwner(opened);
154
168
  assertOwner(followed);
155
169
  };
170
+ const observeCanonical = observer;
156
171
  const owner = ownDirectoryMode({
157
172
  inspect,
173
+ inspectCanonical: observeCanonical ? () => {
174
+ const observed = observeCanonical();
175
+ if (!observed)
176
+ return undefined;
177
+ if (observed.identity.dev !== expected.dev || observed.identity.ino !== expected.ino) {
178
+ throw new FsSafeError("path-mismatch", "directory changed before its mode could be applied");
179
+ }
180
+ return Number(observed.identity.mode & 4095n);
181
+ } : undefined,
158
182
  prepareChmod: proc ? assertProcAuthority : undefined,
159
183
  verifyChmod: proc ? assertProcAuthority : undefined,
160
184
  async chmod(mode) {
@@ -165,12 +189,13 @@ export async function pinNodeDirectoryForMode(dirPath, options = {}) {
165
189
  await handle.chmod(mode);
166
190
  }
167
191
  },
168
- close: () => handle.close(),
192
+ close: () => { observer?.dispose(); return handle.close(); },
169
193
  });
170
194
  await owner.verify();
171
195
  return owner;
172
196
  }
173
197
  catch (error) {
198
+ observer?.dispose();
174
199
  try {
175
200
  await handle.close();
176
201
  }