@openclaw/feishu 2026.9.8 → 2026.10.1-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 (225) hide show
  1. package/dist/.setup/{channel-DV4hM2Uf.mjs → channel-CX62T71S.mjs} +146 -245
  2. package/dist/.setup/{channel.runtime-CXQcH5zZ.mjs → channel.runtime-COiaq-Rv.mjs} +6 -12
  3. package/dist/.setup/{chat-D8MlFAZi.mjs → chat-BdyvImsY.mjs} +6 -9
  4. package/dist/.setup/{client-BYG-_IZl.mjs → client-BsBVWJzN.mjs} +1 -1
  5. package/dist/.setup/{doctor-contract-3XC0vbIw.mjs → doctor-contract-B1B9ogdd.mjs} +212 -27
  6. package/dist/.setup/{monitor-D_rsHBZ6.mjs → monitor-7DvwUs4Y.mjs} +5 -6
  7. package/dist/.setup/{monitor.account-DvvY7i6k.mjs → monitor.account-7xVu7XWY.mjs} +190 -393
  8. package/dist/.setup/{probe-_ViqSi0j.mjs → probe-B9o7iey6.mjs} +13 -39
  9. package/dist/.setup/{reply-delivery-result-COAjaukc.mjs → reply-delivery-result-Bt-BWBEA.mjs} +151 -297
  10. package/dist/.setup/{setup-api-C4S5I3ac.mjs → setup-api-DmzX9S01.mjs} +1 -1
  11. package/dist/.setup/{subagent-hooks-BaFoMu8W.mjs → subagent-hooks-CUXg0pC-.mjs} +5 -11
  12. package/dist/.setup/{thread-bindings-EdLXPXzw.mjs → thread-bindings-jW2x5VuN.mjs} +23 -41
  13. package/dist/api.js +234 -297
  14. package/dist/channel-plugin-api.js +1 -1
  15. package/dist/doctor-contract-api.js +2 -2
  16. package/dist/session-binding-contract-api.js +1 -1
  17. package/dist/setup-api.js +1 -1
  18. package/dist/setup-entry.js +1 -1
  19. package/dist/subagent-hooks-api.js +1 -1
  20. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +95 -0
  21. package/node_modules/@openclaw/fs-safe/README.md +70 -379
  22. package/node_modules/@openclaw/fs-safe/dist/absolute-path.js +35 -51
  23. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -0
  24. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  25. package/node_modules/@openclaw/fs-safe/dist/archive-input.js +2 -0
  26. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  27. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
  28. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +9 -10
  29. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -5
  30. package/node_modules/@openclaw/fs-safe/dist/atomic-io.d.ts +51 -0
  31. package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +242 -0
  32. package/node_modules/@openclaw/fs-safe/dist/config.d.ts +1 -1
  33. package/node_modules/@openclaw/fs-safe/dist/config.js +1 -1
  34. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
  35. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +2 -2
  36. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +2 -0
  37. package/node_modules/@openclaw/fs-safe/dist/create.js +12 -5
  38. package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +11 -0
  39. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +4 -7
  40. package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.d.ts +55 -0
  41. package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.js +1 -0
  42. package/node_modules/@openclaw/fs-safe/dist/entry-publication.d.ts +8 -0
  43. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +262 -0
  44. package/node_modules/@openclaw/fs-safe/dist/exclusive-create.d.ts +3 -0
  45. package/node_modules/@openclaw/fs-safe/dist/exclusive-create.js +22 -0
  46. package/node_modules/@openclaw/fs-safe/dist/file-cleanup.d.ts +1 -6
  47. package/node_modules/@openclaw/fs-safe/dist/file-cleanup.js +3 -6
  48. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +3 -6
  49. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +2 -2
  50. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-mutation.js +9 -0
  51. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.js +4 -6
  52. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-stale-admission.js +1 -1
  53. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -0
  54. package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +2 -0
  55. package/node_modules/@openclaw/fs-safe/dist/file-store-sync-write.js +20 -18
  56. package/node_modules/@openclaw/fs-safe/dist/file-store.js +4 -10
  57. package/node_modules/@openclaw/fs-safe/dist/fs.d.ts +2 -10
  58. package/node_modules/@openclaw/fs-safe/dist/fs.js +2 -10
  59. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +3 -4
  60. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +6 -18
  61. package/node_modules/@openclaw/fs-safe/dist/guest-dispatch-python.js +1 -1
  62. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +0 -2
  63. package/node_modules/@openclaw/fs-safe/dist/guest.js +3 -9
  64. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
  65. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
  66. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-directory.js +1 -5
  67. package/node_modules/@openclaw/fs-safe/dist/json.js +19 -37
  68. package/node_modules/@openclaw/fs-safe/dist/move-path.js +2 -0
  69. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.d.ts +1 -0
  70. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +9 -1
  71. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +47 -2
  72. package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +1 -9
  73. package/node_modules/@openclaw/fs-safe/dist/native-config.js +6 -40
  74. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +2 -0
  75. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +5 -2
  76. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +11 -316
  77. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.d.ts +7 -7
  78. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.js +28 -175
  79. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +15 -0
  80. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +418 -0
  81. package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +9 -4
  82. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +17 -13
  83. package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +6 -24
  84. package/node_modules/@openclaw/fs-safe/dist/native.js +5 -1
  85. package/node_modules/@openclaw/fs-safe/dist/path-case.js +10 -9
  86. package/node_modules/@openclaw/fs-safe/dist/path-scope-lexical.js +4 -1
  87. package/node_modules/@openclaw/fs-safe/dist/path-segment-route.d.ts +1 -0
  88. package/node_modules/@openclaw/fs-safe/dist/path-segment-route.js +3 -0
  89. package/node_modules/@openclaw/fs-safe/dist/path.js +4 -1
  90. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +0 -1
  91. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +1 -3
  92. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +2 -0
  93. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +9 -10
  94. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +3 -6
  95. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +2 -2
  96. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +4 -3
  97. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +3 -4
  98. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +18 -32
  99. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +5 -17
  100. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +92 -230
  101. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.d.ts +4 -15
  102. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.js +22 -63
  103. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +9 -34
  104. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +78 -83
  105. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +18 -20
  106. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +64 -98
  107. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +16 -33
  108. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +61 -200
  109. package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +1 -6
  110. package/node_modules/@openclaw/fs-safe/dist/replace-file.js +76 -216
  111. package/node_modules/@openclaw/fs-safe/dist/root-boundary.js +14 -24
  112. package/node_modules/@openclaw/fs-safe/dist/root-context.d.ts +1 -4
  113. package/node_modules/@openclaw/fs-safe/dist/root-context.js +4 -19
  114. package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +52 -0
  115. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +350 -0
  116. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +35 -0
  117. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.js +1 -0
  118. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -27
  119. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +31 -28
  120. package/node_modules/@openclaw/fs-safe/dist/root-directory.js +1 -4
  121. package/node_modules/@openclaw/fs-safe/dist/root-errors.js +34 -0
  122. package/node_modules/@openclaw/fs-safe/dist/root-file.js +9 -21
  123. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +105 -37
  124. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +7 -2
  125. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +67 -38
  126. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +1 -1
  127. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -2
  128. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -3
  129. package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +3 -1
  130. package/node_modules/@openclaw/fs-safe/dist/root-path-stat.js +68 -85
  131. package/node_modules/@openclaw/fs-safe/dist/root-path.js +6 -1
  132. package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.d.ts +4 -0
  133. package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.js +1 -1
  134. package/node_modules/@openclaw/fs-safe/dist/root-paths.js +4 -9
  135. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.d.ts +4 -0
  136. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +311 -0
  137. package/node_modules/@openclaw/fs-safe/dist/root-remove.d.ts +11 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-remove.js +3 -1
  139. package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +2 -2
  140. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +1 -3
  141. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.d.ts +3 -3
  142. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -10
  143. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +0 -1
  144. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +0 -4
  145. package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -9
  146. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.d.ts +0 -2
  147. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +2 -0
  148. package/node_modules/@openclaw/fs-safe/dist/sibling-temp.js +7 -4
  149. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +13 -6
  150. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +0 -3
  151. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +5 -23
  152. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.d.ts +2 -1
  153. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +11 -6
  154. package/node_modules/@openclaw/fs-safe/dist/temp-cleanup.js +1 -8
  155. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
  156. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +2 -9
  157. package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +1 -6
  158. package/node_modules/@openclaw/fs-safe/dist/trash.js +6 -2
  159. package/node_modules/@openclaw/fs-safe/dist/walk.js +100 -46
  160. package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +7 -0
  161. package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +235 -19
  162. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +21 -2
  163. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +21 -3
  164. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +6 -0
  165. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +111 -0
  166. package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +13 -0
  167. package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +27 -4
  168. package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +2 -0
  169. package/node_modules/@openclaw/fs-safe/dist/watch.js +131 -40
  170. package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.d.ts +8 -0
  171. package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.js +24 -6
  172. package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.d.ts +13 -0
  173. package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.js +51 -0
  174. package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -0
  175. package/node_modules/@openclaw/fs-safe/docs/atomic.md +4 -1
  176. package/node_modules/@openclaw/fs-safe/docs/config.md +6 -14
  177. package/node_modules/@openclaw/fs-safe/docs/contributing.md +4 -14
  178. package/node_modules/@openclaw/fs-safe/docs/durability.md +6 -29
  179. package/node_modules/@openclaw/fs-safe/docs/entry-publication.md +146 -0
  180. package/node_modules/@openclaw/fs-safe/docs/errors.md +16 -0
  181. package/node_modules/@openclaw/fs-safe/docs/file-store.md +28 -5
  182. package/node_modules/@openclaw/fs-safe/docs/index.md +2 -31
  183. package/node_modules/@openclaw/fs-safe/docs/install.md +10 -31
  184. package/node_modules/@openclaw/fs-safe/docs/json.md +1 -1
  185. package/node_modules/@openclaw/fs-safe/docs/local-roots.md +0 -1
  186. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +12 -12
  187. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +35 -96
  188. package/node_modules/@openclaw/fs-safe/docs/native.md +22 -20
  189. package/node_modules/@openclaw/fs-safe/docs/path.md +1 -1
  190. package/node_modules/@openclaw/fs-safe/docs/permissions.md +1 -2
  191. package/node_modules/@openclaw/fs-safe/docs/public-api.md +1 -2
  192. package/node_modules/@openclaw/fs-safe/docs/reading.md +1 -2
  193. package/node_modules/@openclaw/fs-safe/docs/root.md +26 -184
  194. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +1 -1
  195. package/node_modules/@openclaw/fs-safe/docs/security-model.md +115 -3
  196. package/node_modules/@openclaw/fs-safe/docs/store.md +2 -2
  197. package/node_modules/@openclaw/fs-safe/docs/temp.md +29 -65
  198. package/node_modules/@openclaw/fs-safe/docs/testing.md +90 -58
  199. package/node_modules/@openclaw/fs-safe/docs/types.md +3 -16
  200. package/node_modules/@openclaw/fs-safe/docs/walk.md +33 -5
  201. package/node_modules/@openclaw/fs-safe/docs/watch.md +96 -14
  202. package/node_modules/@openclaw/fs-safe/docs/writing.md +172 -16
  203. package/node_modules/@openclaw/fs-safe/package.json +10 -10
  204. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  205. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  206. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  207. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  208. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  209. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  210. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  211. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  212. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  213. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  214. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  215. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  216. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  217. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  218. package/package.json +5 -5
  219. package/skills/feishu-wiki/SKILL.md +1 -1
  220. package/dist/.setup/accounts-wRqItHug.mjs +0 -206
  221. package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +0 -6
  222. package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +0 -88
  223. package/node_modules/@openclaw/fs-safe/docs/mutation-policy-proof.md +0 -69
  224. package/node_modules/@openclaw/fs-safe/docs/private-file-store.md +0 -68
  225. package/node_modules/@openclaw/fs-safe/docs/test-hooks.md +0 -110
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
2
3
  import fs, {} from "node:fs";
3
4
  import fsAsync, {} from "node:fs/promises";
4
5
  import path from "node:path";
@@ -47,6 +48,7 @@ function createPrivateWindowsFile(selected, options, mode) {
47
48
  }
48
49
  assertSynchronousCallbackResult(assertion?.(), "assertBeforeMutation");
49
50
  assertStage();
51
+ assertExclusiveCreateLeaf(stagePath);
50
52
  file = ownFileDescriptorSync(fs.openSync(stagePath, fs.constants.O_RDWR | fs.constants.O_CREAT |
51
53
  fs.constants.O_EXCL | resolveReadOpenFlags(), 0o600));
52
54
  identity = assertCreationFile(file.fd, stagePath);
@@ -105,10 +107,10 @@ function createPrivateWindowsFile(selected, options, mode) {
105
107
  throw privateFileSettlementFailure({ primary, cleanup, publication, path: selected.target, stageDirectory });
106
108
  }
107
109
  }
108
- function createFileCore(targetPath, options, admission) {
110
+ export function createFileSync(targetPath, options = {}) {
109
111
  const permissions = resolveCreationPermissions(options, false);
110
112
  const assertion = options.assertBeforeMutation;
111
- const selected = prepareCreationPath(targetPath, admission.expectedParentIdentity);
113
+ const selected = prepareCreationPath(targetPath);
112
114
  if (permissions.private && process.platform === "win32") {
113
115
  assertPrivateFileCreationAvailable();
114
116
  return createPrivateWindowsFile(selected, { ...permissions, assertBeforeMutation: assertion }, permissions.mode);
@@ -116,6 +118,7 @@ function createFileCore(targetPath, options, admission) {
116
118
  assertBeforeCreation(selected, permissions, assertion, "file");
117
119
  let file;
118
120
  try {
121
+ assertExclusiveCreateLeaf(selected.target);
119
122
  file = ownFileDescriptorSync(fs.openSync(selected.target, fs.constants.O_RDWR | fs.constants.O_CREAT |
120
123
  fs.constants.O_EXCL | resolveReadOpenFlags(), permissions.mode ?? 0o666));
121
124
  }
@@ -142,9 +145,6 @@ function createFileCore(targetPath, options, admission) {
142
145
  });
143
146
  }
144
147
  }
145
- export function createFileSync(targetPath, options = {}) {
146
- return createFileCore(targetPath, options, {});
147
- }
148
148
  /** Internal async writer; Windows preparation uses the nonblocking stage owner. */
149
149
  export async function createFileHandle(targetPath, options = {}, admission = {}) {
150
150
  const permissions = resolveCreationPermissions(options, false);
@@ -152,6 +152,12 @@ export async function createFileHandle(targetPath, options = {}, admission = {})
152
152
  if (process.platform !== "win32" || !permissions.private) {
153
153
  const selected = prepareCreationPath(targetPath, admission.expectedParentIdentity);
154
154
  assertBeforeCreation(selected, permissions, assertion, "file");
155
+ try {
156
+ assertExclusiveCreateLeaf(selected.target);
157
+ }
158
+ catch (error) {
159
+ throw creationCollision(error);
160
+ }
155
161
  const handle = await fsAsync.open(selected.target, fs.constants.O_RDWR | fs.constants.O_CREAT |
156
162
  fs.constants.O_EXCL | resolveReadOpenFlags(), permissions.mode ?? 0o666)
157
163
  .catch(error => { throw creationCollision(error); });
@@ -316,6 +322,7 @@ async function createPrivateWindowsFileHandle(selected, options) {
316
322
  }
317
323
  assertSynchronousCallbackResult(assertion?.(), "assertBeforeMutation");
318
324
  assertStage();
325
+ assertExclusiveCreateLeaf(stagePath);
319
326
  file = await fsAsync.open(stagePath, fs.constants.O_RDWR | fs.constants.O_CREAT |
320
327
  fs.constants.O_EXCL | resolveReadOpenFlags(), 0o600);
321
328
  assertStage();
@@ -7,6 +7,11 @@ export type AsyncDirectoryGuard<T extends Stats | BigIntStats = Stats> = {
7
7
  };
8
8
  export type AnyAsyncDirectoryGuard = AsyncDirectoryGuard<Stats | BigIntStats>;
9
9
  export type SyncDirectoryGuard = AsyncDirectoryGuard;
10
+ type DirectoryGuardMode = "native" | "normalized";
11
+ type DirectoryGuardOptions = {
12
+ bigint?: boolean;
13
+ initial?: BigIntStats;
14
+ };
10
15
  export type DirectoryIdentity = Readonly<{
11
16
  dev: bigint;
12
17
  ino: bigint;
@@ -27,8 +32,13 @@ export declare function createAsyncDirectoryGuard(dir: string, options: {
27
32
  bigint: boolean;
28
33
  }): Promise<AnyAsyncDirectoryGuard>;
29
34
  export declare function createSyncDirectoryGuard(dir: string): SyncDirectoryGuard;
35
+ export declare function captureDirectoryGuard(dir: string, mode: DirectoryGuardMode, options?: {
36
+ bigint?: false;
37
+ }): SyncDirectoryGuard;
38
+ export declare function captureDirectoryGuard(dir: string, mode: DirectoryGuardMode, options?: DirectoryGuardOptions): AnyAsyncDirectoryGuard;
30
39
  export declare function assertAsyncDirectoryGuard(guard: AnyAsyncDirectoryGuard): Promise<void>;
31
40
  export declare function assertSyncDirectoryGuard(guard: SyncDirectoryGuard | AnyAsyncDirectoryGuard): void;
41
+ export declare function assertDirectoryGuard(guard: AnyAsyncDirectoryGuard, mode: DirectoryGuardMode): void;
32
42
  export declare function createNearestExistingDirectoryGuard(rootReal: string, targetPath: string): Promise<AsyncDirectoryGuard>;
33
43
  export declare function createNearestExistingDirectoryGuard(rootReal: string, targetPath: string, options: {
34
44
  bigint: true;
@@ -52,3 +62,4 @@ export declare function extendDirectoryObservationGuard(observation: StatObserva
52
62
  export declare function inspectDirectoryObservationSync(dir: string, expected?: ExactStatIdentity): StatObservationReceipt;
53
63
  export declare function assertDirectoryObservationSync(dir: string, expected: ExactStatIdentity): Stats | BigIntStats;
54
64
  export declare function assertDirectoryObservationGuardSync(guard: DirectoryObservationGuard): void;
65
+ export {};
@@ -35,7 +35,7 @@ export async function createAsyncDirectoryGuard(dir, options) {
35
35
  export function createSyncDirectoryGuard(dir) {
36
36
  return captureDirectoryGuard(dir, "normalized");
37
37
  }
38
- function captureDirectoryGuard(dir, mode, options) {
38
+ export function captureDirectoryGuard(dir, mode, options) {
39
39
  const operationPath = directoryOperationPath(dir);
40
40
  const stat = options?.bigint
41
41
  ? inspectDirectoryIdentityAtPathSync(operationPath, undefined, options.initial)
@@ -53,7 +53,7 @@ export async function assertAsyncDirectoryGuard(guard) {
53
53
  export function assertSyncDirectoryGuard(guard) {
54
54
  assertDirectoryGuard(guard, "normalized");
55
55
  }
56
- function assertDirectoryGuard(guard, mode) {
56
+ export function assertDirectoryGuard(guard, mode) {
57
57
  const dir = guard.dir;
58
58
  const operationPath = directoryOperationPath(dir);
59
59
  const expectedRealPath = guard.realPath;
@@ -82,16 +82,13 @@ function assertDirectoryGuard(guard, mode) {
82
82
  }
83
83
  }
84
84
  export async function createNearestExistingDirectoryGuard(rootReal, targetPath, options = { bigint: false }) {
85
- return nearestExistingDirectoryGuard(rootReal, targetPath, "native", options);
86
- }
87
- function nearestExistingDirectoryGuard(rootReal, targetPath, mode, options) {
88
85
  assertNoWindowsPathAlias(rootReal, "filesystem");
89
86
  assertNoWindowsPathAlias(targetPath, "filesystem");
90
87
  let current = resolvePathPreservingWindowsRoot(targetPath);
91
88
  const root = resolvePathPreservingWindowsRoot(rootReal);
92
89
  while (current !== root) {
93
90
  try {
94
- return captureDirectoryGuard(current, mode, options);
91
+ return captureDirectoryGuard(current, "native", options);
95
92
  }
96
93
  catch (error) {
97
94
  if (!isNotFoundPathError(error)) {
@@ -100,7 +97,7 @@ function nearestExistingDirectoryGuard(rootReal, targetPath, mode, options) {
100
97
  current = path.dirname(current);
101
98
  }
102
99
  }
103
- return captureDirectoryGuard(root, mode, options);
100
+ return captureDirectoryGuard(root, "native", options);
104
101
  }
105
102
  // Recovery receipts must retain every identity bit, including on Windows.
106
103
  export async function inspectDirectoryIdentity(dir, expected) {
@@ -0,0 +1,55 @@
1
+ /** Exact caller observation. Unknown or rounded identities are not admitted. */
2
+ export type PublicationIdentity = Readonly<{
3
+ dev: bigint;
4
+ ino: bigint;
5
+ }>;
6
+ export type PublicationParent = Readonly<{
7
+ path: string;
8
+ identity: PublicationIdentity;
9
+ }>;
10
+ export type RetainEntryForPublicationOptions = Readonly<{
11
+ source: Readonly<{
12
+ parent: PublicationParent;
13
+ basename: string;
14
+ /** Windows symlink includes file/directory symbolic links and junctions; targets stay opaque. */
15
+ expected: PublicationIdentity & Readonly<{
16
+ kind: "directory" | "file" | "symlink";
17
+ }>;
18
+ }>;
19
+ destination: Readonly<{
20
+ parent: PublicationParent;
21
+ basename: string;
22
+ }>;
23
+ /** Must synchronously throw on loss of caller authority. Does not provide isolation. */
24
+ assertBeforeMutation: () => void;
25
+ }>;
26
+ export type EntryPublicationReceipt = Readonly<{
27
+ source: RetainEntryForPublicationOptions["source"];
28
+ destination: RetainEntryForPublicationOptions["destination"];
29
+ capability: Readonly<{
30
+ destinationAbsence: "atomic";
31
+ sourceIdentity: "observed-under-caller-exclusive-namespace";
32
+ parentBinding: "retained-object";
33
+ sourceFilesystem: string;
34
+ destinationFilesystem: string;
35
+ }>;
36
+ }>;
37
+ export type EntryPublicationIssue = Readonly<{
38
+ phase: "admission" | "authority" | "precheck" | "native" | "postcheck" | "close";
39
+ cause: unknown;
40
+ }>;
41
+ /** In-memory syscall disposition, NOT a crash-durable transaction journal. */
42
+ export type EntryPublicationResult = Readonly<{
43
+ transition: "committed" | "not-published" | "indeterminate";
44
+ verification: "not-performed" | "verified" | "failed";
45
+ resources: "closed" | "close-failed";
46
+ /** First issue is primary; subsequent close failures never replace it. */
47
+ issues: readonly EntryPublicationIssue[];
48
+ }>;
49
+ export interface RetainedEntryPublication extends Disposable {
50
+ readonly receipt: EntryPublicationReceipt;
51
+ /** One-shot, synchronous; returns operation AND close failures without discarding transition. */
52
+ publish(): EntryPublicationResult;
53
+ /** Close only. Never reverses, unlinks, or cleans either name; idempotent. */
54
+ dispose(): EntryPublicationResult;
55
+ }
@@ -0,0 +1,8 @@
1
+ import type { RetainedEntryPublication, RetainEntryForPublicationOptions } from "./entry-publication-types.js";
2
+ /**
3
+ * Retain an existing directory, single-link regular file or symlink for ONE-WAY export.
4
+ * Requires caller-exclusive source namespace and stable admitted topology.
5
+ * Native destination absence is atomic; POSIX source identity is NOT CAS.
6
+ * All operation results settle descriptors, never delete or reverse names.
7
+ */
8
+ export declare function retainEntryForPublication(options: RetainEntryForPublicationOptions): RetainedEntryPublication;
@@ -0,0 +1,262 @@
1
+ import fs, {} from "node:fs";
2
+ import path from "node:path";
3
+ import { assertDirectoryIdentitySync } from "./directory-guard.js";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { assertSynchronousCallbackResult } from "./mutation-authority.js";
6
+ import { captureNativeFdClose } from "./native-binding.js";
7
+ import { requireNativeBinding } from "./native.js";
8
+ import { realpathSync } from "./realpath.js";
9
+ import { describeStagedDirectory, assertStagedDirectoryCurrent } from "./staged-directory.js";
10
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
11
+ function basename(name) {
12
+ if (typeof name !== "string" || !name || name === "." || name === ".." ||
13
+ /[/\\:\u0000-\u001f\u007f-\u009f\u2028\u2029]/u.test(name)) {
14
+ throw new FsSafeError("invalid-path", "publication requires a direct-child basename");
15
+ }
16
+ return name;
17
+ }
18
+ function identity(value) {
19
+ const { dev, ino } = value;
20
+ if (typeof dev !== "bigint" || typeof ino !== "bigint" || dev < 0n || ino <= 0n ||
21
+ dev > 0xffffffffffffffffn || ino > 0xffffffffffffffffn) {
22
+ throw new FsSafeError("path-mismatch", "publication requires an exact known identity");
23
+ }
24
+ return Object.freeze({ dev, ino });
25
+ }
26
+ function parent(value) {
27
+ const pathname = value.path;
28
+ const expected = identity(value.identity);
29
+ if (typeof pathname !== "string" || !path.isAbsolute(pathname) || path.resolve(pathname) !== pathname ||
30
+ realpathSync.native(pathname) !== pathname) {
31
+ throw new FsSafeError("path-alias", "publication parent must use its canonical physical spelling");
32
+ }
33
+ assertDirectoryIdentitySync(pathname, { ...expected, realPath: pathname });
34
+ return Object.freeze({ path: pathname, identity: expected });
35
+ }
36
+ function retainParent(expected, closes) {
37
+ const fd = fs.openSync(expected.path, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
38
+ // Register immediately: outer admission owns *all* close outcomes, including
39
+ // failures in the shared guard. A hidden admission close cannot report closed.
40
+ closes.push(() => fs.closeSync(fd));
41
+ const receipt = describeStagedDirectory(fd, expected.path);
42
+ inspectFileIdentitySync(() => fs.fstatSync(fd, { bigint: true }), expected.identity);
43
+ return { fd, receipt };
44
+ }
45
+ function assertKind(stat, kind) {
46
+ if (kind === "file" ? !stat.isFile() : kind === "symlink" ? !stat.isSymbolicLink() : !stat.isDirectory()) {
47
+ throw new FsSafeError("not-file", "publication source must be the expected file, directory or symlink");
48
+ }
49
+ if (kind !== "directory" && stat.nlink !== 1n) {
50
+ throw new FsSafeError("hardlink", "publication does not admit hardlinked files or symlinks");
51
+ }
52
+ }
53
+ function inspectEntry(name, expected) {
54
+ const stat = inspectFileIdentitySync(() => fs.lstatSync(name, { bigint: true }), expected);
55
+ assertKind(stat, expected.kind);
56
+ // A symlink target is opaque: it may be dangling, relative or inaccessible.
57
+ // Check the directory entry spelling without resolving or opening the target.
58
+ const canonical = expected.kind === "symlink"
59
+ ? fs.readdirSync(path.dirname(name)).includes(path.basename(name))
60
+ : realpathSync.native(name) === name;
61
+ if (!canonical) {
62
+ throw new FsSafeError("path-alias", "publication source must use its canonical physical spelling");
63
+ }
64
+ }
65
+ function settle(closes, issues) {
66
+ let resources = "closed";
67
+ // Consume ownership before each close; a failed close must never be retried.
68
+ for (const close of closes.splice(0).reverse()) {
69
+ try {
70
+ for (const cause of close() ?? []) {
71
+ resources = "close-failed";
72
+ issues.push(Object.freeze({ phase: "close", cause }));
73
+ }
74
+ }
75
+ catch (cause) {
76
+ resources = "close-failed";
77
+ issues.push(Object.freeze({ phase: "close", cause }));
78
+ }
79
+ }
80
+ return resources;
81
+ }
82
+ class Publication {
83
+ #result;
84
+ #busy = false;
85
+ #receipt;
86
+ #backend;
87
+ #closes;
88
+ #assertion;
89
+ constructor(receipt, backend, closes, assertion) {
90
+ this.#receipt = receipt;
91
+ this.#backend = backend;
92
+ this.#closes = closes;
93
+ this.#assertion = assertion;
94
+ }
95
+ get receipt() { return this.#receipt; }
96
+ #idle() {
97
+ if (this.#busy)
98
+ throw new FsSafeError("helper-failed", "reentrant entry publication");
99
+ }
100
+ publish() {
101
+ this.#idle();
102
+ if (this.#result)
103
+ return this.#result;
104
+ this.#busy = true;
105
+ let transition = "not-published";
106
+ let verification = "not-performed";
107
+ let phase = "authority";
108
+ const issues = [];
109
+ try {
110
+ assertSynchronousCallbackResult(this.#assertion(), "assertBeforeMutation");
111
+ phase = "precheck";
112
+ this.#backend.current(false);
113
+ phase = "native";
114
+ // Any thrown/lost/malformed native reply is unknown, even if a later path
115
+ // observation happens to resemble success or failure.
116
+ transition = "indeterminate";
117
+ const native = this.#backend.publish();
118
+ if (native?.outcome !== "committed" && native?.outcome !== "not-published" && native?.outcome !== "indeterminate") {
119
+ throw new FsSafeError("helper-failed", "native publication returned an unknown outcome");
120
+ }
121
+ transition = native.outcome; // Capture BEFORE diagnostics, observations or close.
122
+ if (native.errorCode || transition !== "committed") {
123
+ throw Object.assign(new Error(native.errorMessage ?? "entry publication did not commit"), { code: native.errorCode ?? "helper-failed" });
124
+ }
125
+ phase = "postcheck";
126
+ verification = "failed";
127
+ this.#backend.current(true);
128
+ verification = "verified";
129
+ }
130
+ catch (cause) {
131
+ issues.push(Object.freeze({ phase, cause }));
132
+ }
133
+ const resources = settle(this.#closes, issues);
134
+ this.#result = Object.freeze({ transition, verification, resources, issues: Object.freeze(issues) });
135
+ this.#busy = false;
136
+ return this.#result;
137
+ }
138
+ dispose() {
139
+ this.#idle();
140
+ if (this.#result)
141
+ return this.#result;
142
+ this.#busy = true;
143
+ const issues = [];
144
+ const resources = settle(this.#closes, issues);
145
+ this.#result = Object.freeze({ transition: "not-published", verification: "not-performed",
146
+ resources, issues: Object.freeze(issues) });
147
+ this.#busy = false;
148
+ return this.#result;
149
+ }
150
+ [Symbol.dispose]() {
151
+ const result = this.dispose();
152
+ if (result.resources === "close-failed") {
153
+ throw new FsSafeError("helper-failed", "entry publication descriptor close failed", {
154
+ cause: result.issues[0]?.cause, details: { result },
155
+ });
156
+ }
157
+ }
158
+ }
159
+ function publicationReceipt(source, destination, sourceFilesystem, destinationFilesystem) {
160
+ return Object.freeze({ source, destination, capability: Object.freeze({ destinationAbsence: "atomic",
161
+ sourceIdentity: "observed-under-caller-exclusive-namespace", parentBinding: "retained-object", sourceFilesystem, destinationFilesystem }) });
162
+ }
163
+ /**
164
+ * Retain an existing directory, single-link regular file or symlink for ONE-WAY export.
165
+ * Requires caller-exclusive source namespace and stable admitted topology.
166
+ * Native destination absence is atomic; POSIX source identity is NOT CAS.
167
+ * All operation results settle descriptors, never delete or reverse names.
168
+ */
169
+ export function retainEntryForPublication(options) {
170
+ const closes = [];
171
+ try {
172
+ if (process.platform !== "darwin" && process.platform !== "linux" && process.platform !== "win32") {
173
+ throw new FsSafeError("unsupported-platform", "entry publication requires macOS, Linux or Windows");
174
+ }
175
+ const native = requireNativeBinding();
176
+ const assertion = options.assertBeforeMutation;
177
+ if (typeof assertion !== "function")
178
+ throw new FsSafeError("invalid-path", "synchronous publication authority is required");
179
+ const kind = options.source.expected.kind;
180
+ if (kind !== "directory" && kind !== "file" && kind !== "symlink")
181
+ throw new FsSafeError("not-file", "unsupported publication entry kind");
182
+ const source = Object.freeze({ parent: parent(options.source.parent), basename: basename(options.source.basename),
183
+ expected: Object.freeze({ ...identity(options.source.expected), kind }) });
184
+ const destination = Object.freeze({ parent: parent(options.destination.parent), basename: basename(options.destination.basename) });
185
+ const sourcePath = path.join(source.parent.path, source.basename);
186
+ const targetPath = path.join(destination.parent.path, destination.basename);
187
+ if (targetPath === sourcePath || (kind === "directory" && targetPath.startsWith(`${sourcePath}${path.sep}`))) {
188
+ throw new FsSafeError("invalid-path", "publication source and destination overlap");
189
+ }
190
+ if (process.platform === "win32") {
191
+ if (typeof native.retainWindowsEntryPublication !== "function") {
192
+ throw new FsSafeError("helper-unavailable", "native Windows entry retention is unavailable");
193
+ }
194
+ inspectEntry(sourcePath, source.expected);
195
+ let owner;
196
+ // A lost admission reply cannot prove resource settlement. The native GC
197
+ // backstop closes only; an explicit unknown close must remain visible.
198
+ closes.push(() => {
199
+ if (!owner)
200
+ throw new FsSafeError("helper-failed", "native retention reply lost; closure unknown");
201
+ return owner.close().map(error => Object.assign(new Error(error.message), { code: error.code }));
202
+ });
203
+ owner = native.retainWindowsEntryPublication(source.parent.path, source.basename, source.parent.identity.dev, source.parent.identity.ino, destination.parent.path, destination.basename, destination.parent.identity.dev, destination.parent.identity.ino, source.expected.dev, source.expected.ino, kind);
204
+ const admitted = owner.admission;
205
+ if (admitted.outcome !== "retained" || admitted.errorCode) {
206
+ throw Object.assign(new Error(admitted.errorMessage ?? "native Windows publication admission failed"), { code: admitted.errorCode ?? "helper-failed" });
207
+ }
208
+ const retained = owner;
209
+ const receipt = publicationReceipt(source, destination, "ntfs", "ntfs");
210
+ return new Publication(receipt, { current: published => retained.current(published), publish: () => retained.publish() }, closes, assertion);
211
+ }
212
+ if (typeof native.entryPublicationFilesystem !== "function" ||
213
+ typeof native.publishRetainedEntryNoReplace !== "function" || typeof native.openBeneath !== "function") {
214
+ throw new FsSafeError("helper-unavailable", "native entry publication is unavailable");
215
+ }
216
+ const binding = native;
217
+ const closeSource = captureNativeFdClose(binding);
218
+ if (kind === "symlink" && typeof binding.openStagedSymlink !== "function") {
219
+ throw new FsSafeError("helper-unavailable", "native symlink retention is unavailable");
220
+ }
221
+ const sourceParent = retainParent(source.parent, closes);
222
+ const destinationParent = retainParent(destination.parent, closes);
223
+ const sourceFilesystem = binding.entryPublicationFilesystem(sourceParent.fd);
224
+ const destinationFilesystem = binding.entryPublicationFilesystem(destinationParent.fd);
225
+ if (source.expected.dev !== source.parent.identity.dev || source.expected.dev !== destination.parent.identity.dev) {
226
+ throw Object.assign(new Error("entry publication cannot cross devices"), { code: "EXDEV" });
227
+ }
228
+ inspectEntry(sourcePath, source.expected);
229
+ const sourceFd = kind === "symlink"
230
+ ? binding.openStagedSymlink(sourceParent.fd, source.basename)
231
+ : binding.openBeneath(sourceParent.fd, source.basename, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK |
232
+ (kind === "directory" ? fs.constants.O_DIRECTORY : 0))?.fd;
233
+ if (!Number.isInteger(sourceFd) || sourceFd < 0) {
234
+ throw new FsSafeError("helper-unavailable", "publication source descriptor is unavailable");
235
+ }
236
+ closes.push(() => closeSource(sourceFd));
237
+ assertKind(inspectFileIdentitySync(() => fs.fstatSync(sourceFd, { bigint: true }), source.expected), kind);
238
+ inspectEntry(sourcePath, source.expected);
239
+ assertStagedDirectoryCurrent(sourceParent.receipt);
240
+ assertStagedDirectoryCurrent(destinationParent.receipt);
241
+ const receipt = publicationReceipt(source, destination, sourceFilesystem, destinationFilesystem);
242
+ return new Publication(receipt, {
243
+ current(published) {
244
+ assertStagedDirectoryCurrent(sourceParent.receipt);
245
+ assertStagedDirectoryCurrent(destinationParent.receipt);
246
+ assertKind(inspectFileIdentitySync(() => fs.fstatSync(sourceFd, { bigint: true }), source.expected), kind);
247
+ const location = published ? destination : source;
248
+ inspectEntry(path.join(location.parent.path, location.basename), source.expected);
249
+ },
250
+ publish: () => binding.publishRetainedEntryNoReplace(sourceParent.fd, source.basename, sourceFd, destinationParent.fd, destination.basename),
251
+ }, closes, assertion);
252
+ }
253
+ catch (cause) {
254
+ const issues = [Object.freeze({ phase: "admission", cause })];
255
+ const resources = settle(closes, issues);
256
+ const result = Object.freeze({ transition: "not-published", verification: "not-performed",
257
+ resources, issues: Object.freeze(issues) });
258
+ throw new FsSafeError(cause instanceof FsSafeError ? cause.code : "helper-failed", "entry publication admission failed", {
259
+ cause, details: { result },
260
+ });
261
+ }
262
+ }
@@ -0,0 +1,3 @@
1
+ import fs from "node:fs";
2
+ export declare function assertExclusiveCreateStat(pathname: string, stat: Pick<fs.Stats, "isSymbolicLink">): void;
3
+ export declare function assertExclusiveCreateLeaf(pathname: string): void;
@@ -0,0 +1,22 @@
1
+ import fs from "node:fs";
2
+ import { hasNodeErrorCode } from "./path.js";
3
+ export function assertExclusiveCreateStat(pathname, stat) {
4
+ if (stat.isSymbolicLink()) {
5
+ throw Object.assign(new Error("exclusive creation target already exists"), {
6
+ code: "EEXIST", syscall: "open", path: pathname,
7
+ });
8
+ }
9
+ }
10
+ // Windows O_EXCL can create a dangling file symlink's referent. Keep the
11
+ // exclusive-open collision contract; this preflight cannot prevent a later swap.
12
+ export function assertExclusiveCreateLeaf(pathname) {
13
+ if (process.platform !== "win32")
14
+ return;
15
+ try {
16
+ assertExclusiveCreateStat(pathname, fs.lstatSync(pathname));
17
+ }
18
+ catch (error) {
19
+ if (!hasNodeErrorCode(error, "ENOENT"))
20
+ throw error;
21
+ }
22
+ }
@@ -1,14 +1,9 @@
1
1
  import { type BigIntStats } from "node:fs";
2
- import fs, { type FileHandle } from "node:fs/promises";
2
+ import { type FileHandle } from "node:fs/promises";
3
3
  import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
4
4
  export declare function readErrorCode(error: unknown): unknown;
5
5
  export declare function hasErrorCode(error: unknown, expected: string): boolean;
6
6
  type OwnedPathCleanupStatus = "removed" | "name-absent" | "preserved";
7
- export declare function removeOwnedPath(params: {
8
- fsModule: Pick<typeof fs, "lstat" | "unlink">;
9
- pathname: string;
10
- identity?: BigIntStats;
11
- }): Promise<OwnedPathCleanupStatus>;
12
7
  export declare function cleanupPinnedFilePath(params: {
13
8
  pathname: string;
14
9
  handle: Pick<FileHandle, "fd">;
@@ -17,20 +17,18 @@ export function readErrorCode(error) {
17
17
  export function hasErrorCode(error, expected) {
18
18
  return readErrorCode(error) === expected;
19
19
  }
20
- export async function removeOwnedPath(params) {
20
+ async function removeOwnedPath(params) {
21
21
  if (!params.identity)
22
22
  return "preserved";
23
23
  try {
24
- const current = params.fsModule === fs
25
- ? syncFs.lstatSync(params.pathname, { bigint: true })
26
- : await params.fsModule.lstat(params.pathname, { bigint: true });
24
+ const current = syncFs.lstatSync(params.pathname, { bigint: true });
27
25
  if (current.isSymbolicLink() ||
28
26
  !current.isFile() ||
29
27
  current.nlink !== 1n ||
30
28
  !sameFileIdentityForCleanup(current, params.identity)) {
31
29
  return "preserved";
32
30
  }
33
- await params.fsModule.unlink(params.pathname);
31
+ await fs.unlink(params.pathname);
34
32
  return "removed";
35
33
  }
36
34
  catch (error) {
@@ -68,7 +66,6 @@ export async function cleanupPinnedFilePath(params) {
68
66
  }
69
67
  try {
70
68
  return await removeOwnedPath({
71
- fsModule: fs,
72
69
  pathname: params.pathname,
73
70
  identity: params.identity,
74
71
  });
@@ -34,12 +34,9 @@ export function sameFileIdentityForCleanup(left, right, platform = process.platf
34
34
  if (platform !== "win32") {
35
35
  return sameCleanupStatValue(left.dev, right.dev) && sameCleanupStatValue(left.ino, right.ino);
36
36
  }
37
- // A zero Windows device or inode is unknown, not proof that a pathname still
38
- // names the object we created. Cleanup must fail closed rather than delete a
39
- // replacement that happens to share the known half of the identity.
40
- // Capture each potentially adapter-backed observation once. This strengthens
41
- // the fence against changing accessors; exact legacy getter traces are not a
42
- // compatibility contract.
37
+ // Unknown Windows identity components cannot authorize cleanup: a replacement
38
+ // could share the known component. Snapshot adapter-backed fields once so
39
+ // changing accessors cannot alter the check.
43
40
  const leftDev = left.dev;
44
41
  if (isZero(leftDev))
45
42
  return false;
@@ -4,6 +4,7 @@ import { readFileDescriptorBoundedSync } from "./bounded-read.js";
4
4
  import { inspectDirectoryIdentitySync } from "./directory-guard.js";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { isNotFoundPathError } from "./path.js";
7
+ import { sameAbsolutePath } from "./path-segment-route.js";
7
8
  import { assertRootIdentityCurrentSync } from "./root-context.js";
8
9
  import { hardlinkedPathNotAllowedError } from "./root-errors.js";
9
10
  import { resolveReadOpenFlags } from "./read-open-flags.js";
@@ -14,12 +15,11 @@ import { createSuppressedError } from "./suppressed-error.js";
14
15
  import { assertFileLockSyncRootResolvedPathCurrent, } from "./file-lock-sync-root.js";
15
16
  import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
16
17
  const MAX_SIDECAR_LOCK_PAYLOAD_BYTES = 1024 * 1024;
17
- const samePath = (left, right) => path.relative(path.resolve(left), path.resolve(right)) === "";
18
18
  export function observeDirectory(pathname, initial) {
19
19
  const stat = inspectDirectoryIdentitySync(pathname, undefined, initial);
20
20
  const realPath = realpathSync.native(pathForWindowsFilesystem(pathname));
21
21
  assertNoWindowsPathAlias(realPath, "filesystem", "sidecar lock parent uses a Windows filesystem namespace alias");
22
- if (!samePath(realPath, pathname)) {
22
+ if (!sameAbsolutePath(realPath, pathname)) {
23
23
  throw new FsSafeError("path-mismatch", "sidecar lock parent changed during operation");
24
24
  }
25
25
  return Object.freeze({
@@ -1,4 +1,5 @@
1
1
  import fs, {} from "node:fs";
2
+ import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
2
3
  import path from "node:path";
3
4
  import { inspectDirectoryIdentitySync } from "./directory-guard.js";
4
5
  import { FsSafeError } from "./errors.js";
@@ -89,6 +90,14 @@ export function createFileLockSyncRootFile(pathAuthority, options) {
89
90
  let receipt;
90
91
  try {
91
92
  options.assertBeforeOpen();
93
+ try {
94
+ assertExclusiveCreateLeaf(pathForWindowsFilesystem(pathAuthority.path));
95
+ }
96
+ catch (error) {
97
+ if (error.code === "EEXIST")
98
+ options.onOpenFailure(error);
99
+ throw error;
100
+ }
92
101
  try {
93
102
  fd = fs.openSync(pathForWindowsFilesystem(pathAuthority.path), fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
94
103
  }
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { assertMutationNotDenied } from "./deny-mutations.js";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { assertNoNulPathInput, isNotFoundPathError } from "./path.js";
6
+ import { sameAbsolutePath } from "./path-segment-route.js";
6
7
  import { resolveRootPathSync } from "./root-path.js";
7
8
  import { isRootPathEscapeError } from "./root-path-errors.js";
8
9
  import { admitPathInsideRoot } from "./root-boundary.js";
@@ -91,9 +92,6 @@ export function invokeFileLockSyncRootMutationAuthority(authority) {
91
92
  assertSynchronousCallbackResult(assertion(), "assertBeforeMutation");
92
93
  return true;
93
94
  }
94
- function samePath(left, right) {
95
- return path.relative(path.resolve(left), path.resolve(right)) === "";
96
- }
97
95
  function absoluteRootPath(authority, relativePath) {
98
96
  const segments = relativePath.split(path.posix.sep);
99
97
  const absolute = path.join(authority.context.rootReal, ...segments);
@@ -129,7 +127,7 @@ function resolveAdmittedPath(authority, absolutePath, policy, rootPath) {
129
127
  candidatePath: resolved.canonicalPath,
130
128
  rootIdentity: context.rootIdentity,
131
129
  });
132
- if (!admitted || admitted.relativePath === "" || samePath(admitted.path, context.rootReal)) {
130
+ if (!admitted || admitted.relativePath === "" || sameAbsolutePath(admitted.path, context.rootReal)) {
133
131
  throw outsideWorkspaceError();
134
132
  }
135
133
  return {
@@ -143,7 +141,7 @@ function resolveBothPolicies(authority, absolutePath, rootPath) {
143
141
  const readable = policiesMatch
144
142
  ? mutation
145
143
  : resolveAdmittedPath(authority, absolutePath, readPolicy, rootPath);
146
- if (!samePath(mutation.path, readable.path)) {
144
+ if (!sameAbsolutePath(mutation.path, readable.path)) {
147
145
  throw new FsSafeError("path-mismatch", "sidecar read and mutation paths resolve differently");
148
146
  }
149
147
  assertFileLockSyncRootMutationAllowed(mutation.path, authority.denyMutations);
@@ -190,7 +188,7 @@ export function admitFileLockSyncRootPath(authority, requestedPath) {
190
188
  }
191
189
  export function assertFileLockSyncRootResolvedPathCurrent(pathAuthority) {
192
190
  const resolved = resolveBothPolicies(pathAuthority.authority, absoluteRootPath(pathAuthority.authority, pathAuthority.relativePath), pathAuthority.authority.context.rootReal);
193
- if (!samePath(resolved.path, pathAuthority.path)) {
191
+ if (!sameAbsolutePath(resolved.path, pathAuthority.path)) {
194
192
  throw new FsSafeError("path-mismatch", "sidecar lock path changed during operation");
195
193
  }
196
194
  }