@openclaw/feishu 2026.9.8 → 2026.10.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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,8 +1,13 @@
1
1
  export const NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH = "FS_SAFE_INTERNAL_RENAME_SOURCE_IDENTITY_MISMATCH";
2
2
  export function classifyNativeRenameFailure(error) {
3
- const code = error?.code ?? "";
3
+ try {
4
+ if (error?.code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH) {
5
+ return "uncommitted";
6
+ }
7
+ }
8
+ catch {
9
+ // Unreadable diagnostics cannot prove that the rename was uncommitted.
10
+ }
4
11
  // Ordinary errno can follow a committed remote rename whose reply was lost.
5
- return code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH
6
- ? "uncommitted"
7
- : "indeterminate";
12
+ return "indeterminate";
8
13
  }
@@ -57,14 +57,14 @@ import { randomUUID } from "node:crypto";
57
57
  import fs, {} from "node:fs";
58
58
  import path from "node:path";
59
59
  import { FsSafeError } from "./errors.js";
60
- import { assertSynchronousCallbackResult, MutationAuthorityError } from "./mutation-authority.js";
60
+ import { assertSynchronousCallbackResult, isMutationAuthorityError } from "./mutation-authority.js";
61
61
  import { captureNativeFdClose } from "./native-binding.js";
62
62
  import { writePinnedInput } from "./pinned-write-input.js";
63
63
  import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
64
64
  import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
65
65
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
66
66
  import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
67
- import { createStagedFileReceipt, stagedFileFailure as failure } from "./staged-file-settlement.js";
67
+ import { createStagedFileReceipt, stagedFailure } from "./staged-file-settlement.js";
68
68
  export function assertNativeStaging(binding) {
69
69
  if ([
70
70
  binding.closeOwnedFd,
@@ -152,7 +152,7 @@ class NativeStagedFile {
152
152
  }
153
153
  catch (error) {
154
154
  if (params.overwrite === false && params.input.kind !== "file" && params.input.stageBeforePublish === true) {
155
- throw failure(error, { phase: "publish", publication: published });
155
+ throw stagedFailure("file", error, { phase: "publish", publication: published });
156
156
  }
157
157
  throw error;
158
158
  }
@@ -175,7 +175,7 @@ class NativeStagedFile {
175
175
  return this.#receipt;
176
176
  }
177
177
  #open() {
178
- if (this.#state.status === "closed") {
178
+ if (this.#state.status !== "open") {
179
179
  throw new FsSafeError("helper-failed", "staged file is closed");
180
180
  }
181
181
  return this.#state;
@@ -238,7 +238,7 @@ class NativeStagedFile {
238
238
  assertDarwinCreationAcl(this.#owner.parentFd, "file");
239
239
  // Native copying populates its new file before returning its descriptor.
240
240
  const copied = input.kind === "file" && !this.#verifyMode
241
- ? await createNativeCopyFile(this.#owner.binding, input, this.#owner.parentFd, this.#owner.name, maxBytes, false)
241
+ ? await createNativeCopyFile(this.#owner.binding, input, this.#owner.parentFd, this.#owner.name, maxBytes)
242
242
  : undefined;
243
243
  state.fileFd = copied?.fd;
244
244
  if (state.fileFd === undefined) {
@@ -282,10 +282,10 @@ class NativeStagedFile {
282
282
  catch (error) {
283
283
  const { receipt: cleanup, error: cleanupError } = this.#finalize();
284
284
  if (cleanupError) {
285
- throw failure(new AggregateError([error, cleanupError], "preparation and cleanup failed"), { phase: "prepare", publication: cleanup.publication, cleanup });
285
+ throw stagedFailure("file", new AggregateError([error, cleanupError], "preparation and cleanup failed"), { phase: "prepare", publication: cleanup.publication, cleanup });
286
286
  }
287
287
  if (cleanup.status === "preserved") {
288
- throw failure(error, { phase: "prepare", publication: cleanup.publication, cleanup });
288
+ throw stagedFailure("file", error, { phase: "prepare", publication: cleanup.publication, cleanup });
289
289
  }
290
290
  throw error;
291
291
  }
@@ -319,22 +319,24 @@ class NativeStagedFile {
319
319
  }
320
320
  }
321
321
  catch (error) {
322
+ // Record before inspecting metadata, whose getters can reenter cleanup.
323
+ state.publication = Object.freeze({ status: "indeterminate", basename, overwrite });
322
324
  // Only explicit pre-dispatch provenance can rule out a committed rename.
323
- if (classifyNativeRenameFailure(error) === "indeterminate") {
324
- state.publication = Object.freeze({ status: "indeterminate", basename, overwrite });
325
+ if (classifyNativeRenameFailure(error) === "uncommitted") {
326
+ state.publication = NOT_PUBLISHED;
325
327
  }
326
328
  throw error;
327
329
  }
328
330
  return this.#completePublication(basename, overwrite, onPublished);
329
331
  }
330
332
  catch (error) {
331
- if (error instanceof MutationAuthorityError)
333
+ if (isMutationAuthorityError(error))
332
334
  throw error;
333
335
  // Closure rejects further use, not the recorded outcome of an earlier publication.
334
336
  const publication = this.#state.status === "closed"
335
337
  ? this.#state.receipt.publication
336
338
  : this.#state.publication;
337
- throw failure(error, { phase: "publish", publication });
339
+ throw stagedFailure("file", error, { phase: "publish", publication });
338
340
  }
339
341
  }
340
342
  #completePublication(basename, overwrite, onPublished) {
@@ -367,7 +369,9 @@ class NativeStagedFile {
367
369
  if (this.#state.status === "closed") {
368
370
  return this.#state;
369
371
  }
370
- const state = this.#state;
372
+ const state = this.#open();
373
+ // Consume descriptor authority before native calls or diagnostic getters can reenter.
374
+ state.status = "closing";
371
375
  let outcome = "not-needed";
372
376
  const errors = [];
373
377
  if (state.publication.status === "indeterminate") {
@@ -404,7 +408,7 @@ class NativeStagedFile {
404
408
  status: outcome,
405
409
  resources,
406
410
  });
407
- const error = errors.length ? failure(errors.length === 1 ? errors[0] : new AggregateError(errors, "staged cleanup failed"), { phase: "cleanup", publication: state.publication, cleanup: receipt }) : undefined;
411
+ const error = errors.length ? stagedFailure("file", errors.length === 1 ? errors[0] : new AggregateError(errors, "staged cleanup failed"), { phase: "cleanup", publication: state.publication, cleanup: receipt }) : undefined;
408
412
  this.#state = { status: "closed", receipt, error };
409
413
  return this.#state;
410
414
  }
@@ -5,7 +5,7 @@ import { captureNativeFdClose } from "./native-binding.js";
5
5
  import { requireNativeBinding } from "./native.js";
6
6
  import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
7
7
  import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
8
- import { createStagedFileReceipt } from "./staged-file-settlement.js";
8
+ import { createStagedFileReceipt, stagedFailure } from "./staged-file-settlement.js";
9
9
  const NOT_PUBLISHED = Object.freeze({ status: "not-published" });
10
10
  function basename(name) {
11
11
  if (typeof name !== "string" || !name || name === "." || name === ".." ||
@@ -13,17 +13,6 @@ function basename(name) {
13
13
  throw new FsSafeError("invalid-path", "staged symlinks require a direct-child basename");
14
14
  }
15
15
  }
16
- function failure(error, details) {
17
- let code = "helper-failed";
18
- try {
19
- code = error instanceof FsSafeError ? error.code
20
- : error?.code === "EEXIST" ? "already-exists" : "helper-failed";
21
- }
22
- catch {
23
- // Uninspectable error metadata must not interrupt terminal settlement.
24
- }
25
- return new FsSafeError(code, `staged symlink ${details.phase} failed`, { cause: error, details });
26
- }
27
16
  function assertAuthority(assertion) {
28
17
  assertSynchronousCallbackResult(assertion(), "assertBeforeMutation");
29
18
  }
@@ -101,14 +90,7 @@ class NativeStagedSymlink {
101
90
  this.#binding.publishStagedSymlink(this.#parentFd, this.#receipt.temporaryBasename, this.#linkFd, name);
102
91
  }
103
92
  catch (error) {
104
- let outcome = "indeterminate";
105
- try {
106
- outcome = classifyNativeRenameFailure(error);
107
- }
108
- catch {
109
- // Unreadable diagnostics cannot prove that the rename was uncommitted.
110
- }
111
- if (outcome === "indeterminate") {
93
+ if (classifyNativeRenameFailure(error) === "indeterminate") {
112
94
  this.#publication = Object.freeze({ status: "indeterminate", basename: name, overwrite: false });
113
95
  }
114
96
  throw error;
@@ -123,7 +105,7 @@ class NativeStagedSymlink {
123
105
  return published;
124
106
  }
125
107
  catch (error) {
126
- throw failure(error, { phase: "publish", publication: this.#publication });
108
+ throw stagedFailure("symlink", error, { phase: "publish", publication: this.#publication });
127
109
  }
128
110
  finally {
129
111
  this.#busy = false;
@@ -181,7 +163,7 @@ class NativeStagedSymlink {
181
163
  return receipt;
182
164
  }
183
165
  catch (error) {
184
- const wrapped = failure(error, { phase: "remove-published", publication: this.#publication });
166
+ const wrapped = stagedFailure("symlink", error, { phase: "remove-published", publication: this.#publication });
185
167
  // A failed unlink is not safe to retry automatically.
186
168
  this.#removal = { error: wrapped };
187
169
  throw wrapped;
@@ -225,7 +207,7 @@ class NativeStagedSymlink {
225
207
  temporaryBasename: this.#receipt.temporaryBasename,
226
208
  publication: this.#publication, status, resources,
227
209
  });
228
- const error = errors.length ? failure(errors.length === 1 ? errors[0] : new AggregateError(errors, "symlink settlement failed"), { phase: "cleanup", publication: this.#publication, cleanup: receipt }) : undefined;
210
+ const error = errors.length ? stagedFailure("symlink", errors.length === 1 ? errors[0] : new AggregateError(errors, "symlink settlement failed"), { phase: "cleanup", publication: this.#publication, cleanup: receipt }) : undefined;
229
211
  this.#closed = { receipt, error };
230
212
  this.#busy = false;
231
213
  if (error)
@@ -298,6 +280,6 @@ export async function retainSymlinkInDirectory(options) {
298
280
  }
299
281
  }
300
282
  // Until admission succeeds, the caller retains every namespace cleanup duty.
301
- throw failure(errors.length === 1 ? error : new AggregateError(errors, "symlink admission and close failed"), { phase: "prepare", publication: NOT_PUBLISHED });
283
+ throw stagedFailure("symlink", errors.length === 1 ? error : new AggregateError(errors, "symlink admission and close failed"), { phase: "prepare", publication: NOT_PUBLISHED });
302
284
  }
303
285
  }
@@ -1,9 +1,13 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { closeSync, openSync, readSync, readdirSync, } from "node:fs";
2
3
  import { createRequire } from "node:module";
3
4
  import { FsSafeError } from "./errors.js";
4
5
  import { captureNativeFdClose } from "./native-binding.js";
5
6
  import { getFsSafeNativeConfig } from "./native-config.js";
6
7
  const require = createRequire(import.meta.url);
8
+ // Native registration creates process-lifetime housekeeping. Use the import
9
+ // context rather than retaining the first operation's AsyncLocalStorage stores.
10
+ const runNativeInitialization = AsyncLocalStorage.snapshot();
7
11
  let binding;
8
12
  let loadError;
9
13
  let attempted = false;
@@ -145,7 +149,7 @@ function loadBundledBinding() {
145
149
  if (!target) {
146
150
  throw new Error(`Unsupported OS or architecture: ${process.platform}-${process.arch}`);
147
151
  }
148
- const loaded = require(nativePackageForTarget(target));
152
+ const loaded = runNativeInitialization(() => require(nativePackageForTarget(target)));
149
153
  captureNativeFdClose(loaded);
150
154
  return loaded;
151
155
  }
@@ -5,8 +5,9 @@ import { assertDirectoryIdentitySync, inspectDirectoryIdentitySync } from "./dir
5
5
  import { sameFileIdentityForCleanup } from "./file-identity.js";
6
6
  import { isNotFoundPathError } from "./path.js";
7
7
  import { realpathSync } from "./realpath.js";
8
- import { writeTempFileSync } from "./replace-file-descriptor.js";
9
- import { SyncAtomicTempOwner } from "./replace-file-temp-owner.js";
8
+ import { writeTempFile } from "./replace-file-descriptor.js";
9
+ import { AtomicIo, runSync } from "./atomic-io.js";
10
+ import { AtomicTempOwner } from "./replace-file-temp-owner.js";
10
11
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
11
12
  function swapAsciiCase(value) {
12
13
  return value.replace(/[A-Za-z]/g, character => {
@@ -54,25 +55,25 @@ function probeEntry(directory, name, names) {
54
55
  }
55
56
  function probeTemporaryEntry(directory) {
56
57
  const name = `.fs-safe-case-probe-${randomUUID()}`;
57
- const owner = new SyncAtomicTempOwner(path.join(directory, name));
58
+ const io = AtomicIo.sync(fs);
59
+ const owner = new AtomicTempOwner(path.join(directory, name), io);
58
60
  try {
59
61
  owner.start();
60
- owner.adopt(writeTempFileSync({
61
- fsModule: fs,
62
+ owner.adopt(runSync(writeTempFile(io, {
62
63
  tempPath: owner.pathname,
63
64
  content: "",
64
65
  mode: 0o600,
65
66
  sync: false,
66
67
  onIdentity: owner.onIdentity,
67
- }));
68
- owner.assertCurrent(fs);
68
+ })));
69
+ runSync(owner.assertCurrent());
69
70
  const observed = probeEntry(directory, name, new Set(fs.readdirSync(directory)));
70
- owner.assertCurrent(fs);
71
+ runSync(owner.assertCurrent());
71
72
  return observed;
72
73
  }
73
74
  finally {
74
75
  // Failed cleanup invalidates the observation; the existing owner retains its exit retry.
75
- owner.finish({ fsModule: fs, throwOnCleanupError: true });
76
+ runSync(owner.finish({ throwOnCleanupError: true }));
76
77
  }
77
78
  }
78
79
  function probeDirectory(directory, allowTemporaryProbe, preferredName) {
@@ -1,10 +1,13 @@
1
1
  import path from "node:path";
2
2
  import { isPathInside, isPathRelativeEscape } from "./path.js";
3
- import { hasWindowsPathAlias, resolvePathFromBasePreservingWindowsRoot } from "./windows-path-alias.js";
3
+ import { hasWindowsPathAlias, isForeignWindowsShareOrDevicePath, resolvePathFromBasePreservingWindowsRoot, } from "./windows-path-alias.js";
4
4
  function pathStaysWithinRoot(rootDir, candidatePath) {
5
5
  if (process.platform !== "win32") {
6
6
  return candidatePath !== rootDir && isPathInside(rootDir, candidatePath);
7
7
  }
8
+ // path.relative folds Unicode case, so it alone could admit another host.
9
+ if (isForeignWindowsShareOrDevicePath(candidatePath, [rootDir]))
10
+ return false;
8
11
  const relative = path.relative(rootDir, candidatePath);
9
12
  return Boolean(relative) && !isPathRelativeEscape(relative);
10
13
  }
@@ -1,3 +1,4 @@
1
+ export declare function sameAbsolutePath(left: string, right: string): boolean;
1
2
  export type PathSegmentRoute = Readonly<{
2
3
  joined: string;
3
4
  offsets: readonly number[];
@@ -1,4 +1,7 @@
1
1
  import path from "node:path";
2
+ export function sameAbsolutePath(left, right) {
3
+ return path.relative(path.resolve(left), path.resolve(right)) === "";
4
+ }
2
5
  export function createPathSegmentRoute(segments) {
3
6
  const offsets = new Array(segments.length + 1);
4
7
  let cursor = 0;
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  import { realpathSync } from "./realpath.js";
5
5
  import { isDriveRelativePath } from "./safe-path-segment.js";
6
- import { assertNoWindowsPathAlias, hasWindowsPathAlias, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
6
+ import { assertNoWindowsPathAlias, hasWindowsPathAlias, isForeignWindowsShareOrDevicePath, pathForWindowsFilesystem, resolvePathFromBasePreservingWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
7
7
  export { assertNoUnsafeDeviceReadPath, isUnsafeDeviceReadPath, matchUnsafeDeviceReadPath, } from "./device-path.js";
8
8
  const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
9
9
  const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
@@ -47,6 +47,9 @@ export function isSymlinkOpenError(value) {
47
47
  }
48
48
  export function isPathInside(root, target) {
49
49
  if (process.platform === "win32") {
50
+ // Comparison folds Unicode case, so it alone could admit another host.
51
+ if (isForeignWindowsShareOrDevicePath(target, [root]))
52
+ return false;
50
53
  const rootForCompare = resolveWindowsPathForComparison(root);
51
54
  const targetForCompare = resolveWindowsPathForComparison(target);
52
55
  // Resolved drive paths already have canonical separators and case. A full
@@ -36,7 +36,6 @@ export declare function inspectWindowsAcl(targetPath: string, opts?: {
36
36
  env?: NodeJS.ProcessEnv;
37
37
  exec?: PermissionExec;
38
38
  currentUserSid?: string;
39
- principalSids?: Record<string, string>;
40
39
  principalTranslationFailed?: boolean;
41
40
  }): Promise<WindowsAclSummary>;
42
41
  export declare function formatWindowsAclSummary(summary: WindowsAclSummary): string;
@@ -3,6 +3,7 @@ import { assertMutationNotDenied } from "./deny-mutations.js";
3
3
  import { createMutationDenyMatcher } from "./deny-mutation-match.js";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { isPathInside } from "./path.js";
6
+ import { sameAbsolutePath } from "./path-segment-route.js";
6
7
  import { admitPathInsideRoot } from "./root-boundary.js";
7
8
  import { ordinarySharedAbsoluteInsideRoot, simpleSharedRoute, } from "./pinned-mutation-shared-route.js";
8
9
  import { resolvePathViaExistingAncestor } from "./root-path-existing.js";
@@ -22,9 +23,6 @@ async function canonicalMutationEntryPath(targetPath) {
22
23
  const canonicalParent = await resolvePathViaExistingAncestor(path.dirname(targetPath));
23
24
  return path.join(canonicalParent, path.basename(targetPath));
24
25
  }
25
- function sameAbsolutePath(left, right) {
26
- return path.relative(path.resolve(left), path.resolve(right)) === "";
27
- }
28
26
  function simplePinnedRoute(rootReal, originalPath) {
29
27
  // These routes can depend on raw traversal, platform aliases, home expansion,
30
28
  // or live native canonicalization. They retain full admission at every step.
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
2
3
  import fsSync, {} from "node:fs";
3
4
  import fs, {} from "node:fs/promises";
4
5
  import path from "node:path";
@@ -41,6 +42,7 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
41
42
  let failure;
42
43
  try {
43
44
  params.assertBeforeMutation?.();
45
+ assertExclusiveCreateLeaf(tempPath);
44
46
  handle = params.private
45
47
  ? await createFileHandle(tempPath, {
46
48
  private: true, mode: 0o600, assertBeforeMutation: params.assertBeforeMutation,
@@ -1,11 +1,12 @@
1
1
  import { syncFileBestEffort } from "./file-sync.js";
2
+ import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
2
3
  import fsSync, {} from "node:fs";
3
4
  import fs from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  import { createFileHandle } from "./create.js";
6
7
  import { creationAdmissionFromParent } from "./creation-boundary.js";
7
8
  import { normalizeMaxBytes } from "./byte-budget.js";
8
- import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
9
+ import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
9
10
  import { FsSafeError } from "./errors.js";
10
11
  import { syncDirectoryBestEffort } from "./directory-durability.js";
11
12
  import { sha256Hex } from "./file-identity.js";
@@ -173,18 +174,17 @@ async function runPinnedWriteFallback(params) {
173
174
  phase: "parent",
174
175
  }));
175
176
  }
176
- if (params.mkdir && !parentGuard) {
177
- // mkdirPathComponentsWithGuards may resolve the final component through
178
- // an in-root symlink (e.g. a skill-bank layout). Use its returned real
179
- // path for the subsequent guard and target path so we don't re-check the
180
- // original, possibly-symlinked, lexical path and reject it outright.
177
+ if (!parentGuard) {
178
+ // Guard the returned real parent; the lexical path may contain an allowed symlink.
181
179
  const mkdirParams = {
182
180
  rootReal: params.rootPath,
183
181
  targetPath: parentPath,
184
182
  rootIdentity: params.rootIdentity,
183
+ createMissing: params.mkdir,
185
184
  assertBeforeMutation: params.assertBeforeMutation,
186
185
  beforeComponent: async (componentPath) => {
187
- await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath);
186
+ if (params.mkdir)
187
+ await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath);
188
188
  },
189
189
  };
190
190
  parentPath = await mkdirPathComponentsWithGuards(mutationAdmission ? {
@@ -215,9 +215,7 @@ async function runPinnedWriteFallback(params) {
215
215
  },
216
216
  } : mkdirParams);
217
217
  }
218
- parentGuard ??= params.mkdir
219
- ? await createAsyncDirectoryGuard(parentPath, { bigint: true })
220
- : await createNearestExistingDirectoryGuard(params.rootPath, parentPath, { bigint: true });
218
+ parentGuard ??= await createAsyncDirectoryGuard(parentPath, { bigint: true });
221
219
  const targetPath = path.join(parentPath, params.basename);
222
220
  if (mutationAdmission && !parentAdmitted) {
223
221
  await mutationAdmission.authorize(Object.freeze({
@@ -239,6 +237,7 @@ async function runPinnedWriteFallback(params) {
239
237
  private: true, mode: 0o600, assertBeforeMutation,
240
238
  }, creationAdmissionFromParent(parentGuard));
241
239
  }
240
+ assertExclusiveCreateLeaf(targetPath);
242
241
  return await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, verifyPosixMode ? 0o600 : params.mode);
243
242
  }, {
244
243
  onPostGuardFailure: async (openedHandle) => {
@@ -4,6 +4,7 @@ import { FsSafeError } from "./errors.js";
4
4
  import { sameFileIdentityForCleanup } from "./file-identity.js";
5
5
  import { assertSynchronousCallbackResult } from "./mutation-authority.js";
6
6
  import { resolveReadOpenFlags } from "./read-open-flags.js";
7
+ import { errorCauseOptions } from "./root-errors.js";
7
8
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
8
9
  import { registerTempPathForExit, } from "./temp-cleanup.js";
9
10
  function pathMismatch(message) {
@@ -36,18 +37,14 @@ export function assertParents(params) {
36
37
  export function normalizeLinkError(error) {
37
38
  const code = error?.code;
38
39
  if (code === "EEXIST") {
39
- return new FsSafeError("already-exists", "isolated producer sibling already exists", {
40
- cause: error instanceof Error ? error : undefined,
41
- });
40
+ return new FsSafeError("already-exists", "isolated producer sibling already exists", errorCauseOptions(error));
42
41
  }
43
42
  if (code === "EXDEV" ||
44
43
  code === "ENOSYS" ||
45
44
  code === "ENOTSUP" ||
46
45
  code === "EOPNOTSUPP" ||
47
46
  code === "EPERM") {
48
- return new FsSafeError("helper-unavailable", "atomic isolated producer handoff is unavailable", {
49
- cause: error instanceof Error ? error : undefined,
50
- });
47
+ return new FsSafeError("helper-unavailable", "atomic isolated producer handoff is unavailable", errorCauseOptions(error));
51
48
  }
52
49
  return error;
53
50
  }
@@ -3,8 +3,8 @@ import path from "node:path";
3
3
  import { assertSyncDirectoryGuard } from "./directory-guard.js";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { sameFileIdentityForCleanup } from "./file-identity.js";
6
- // Node has no portable no-replace rename. Link only our completed private stage,
7
- // then unlink it in the same JS turn so local readers never observe two names.
6
+ // Node has no portable no-replace rename. Publish the completed stage by link,
7
+ // then remove its verified temporary name without yielding.
8
8
  export function publishCopyStage(params) {
9
9
  const { identity, temporaryPath, targetPath } = params;
10
10
  const assertCurrent = () => {
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import fsSync, {} from "node:fs";
3
3
  import fs, {} from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
5
6
  import { pinDirectory, } from "./directory-durability.js";
6
7
  import { FsSafeError } from "./errors.js";
7
8
  import { ownDirectoryReceipt } from "./directory-receipt.js";
@@ -21,7 +22,6 @@ const NATIVE_COPY_FALLBACK_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPN
21
22
  export function isHardlinkFallbackError(error) {
22
23
  return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
23
24
  }
24
- function sourceOpenFlags() { return resolveReadOpenFlags(); }
25
25
  function directoryOpenFlags() {
26
26
  return (fsSync.constants.O_RDONLY |
27
27
  (typeof fsSync.constants.O_DIRECTORY === "number" ? fsSync.constants.O_DIRECTORY : 0));
@@ -105,7 +105,7 @@ async function copyPinnedSource(params) {
105
105
  createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
106
106
  await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, createdIdentity);
107
107
  const identity = fsSync.lstatSync(params.targetPath, { bigint: true });
108
- target = await fs.open(params.targetPath, sourceOpenFlags());
108
+ target = await fs.open(params.targetPath, resolveReadOpenFlags());
109
109
  const opened = fsSync.fstatSync(target.fd, { bigint: true });
110
110
  if (identity.isSymbolicLink() ||
111
111
  !identity.isFile() ||
@@ -134,6 +134,7 @@ async function copyPinnedSource(params) {
134
134
  }
135
135
  }
136
136
  }
137
+ assertExclusiveCreateLeaf(params.targetPath);
137
138
  const target = await fs.open(params.targetPath, "wx+", 0o600);
138
139
  try {
139
140
  const createdIdentity = fsSync.fstatSync(target.fd, { bigint: true });
@@ -216,7 +217,7 @@ export async function publishFileExclusive(params) {
216
217
  if (sourcePathStat.isSymbolicLink() || !sourcePathStat.isFile()) {
217
218
  throw new FsSafeError("not-file", "publication source must be a regular file");
218
219
  }
219
- const source = await fs.open(sourcePath, sourceOpenFlags());
220
+ const source = await fs.open(sourcePath, resolveReadOpenFlags());
220
221
  let parent;
221
222
  let sourceNativeParent;
222
223
  let targetNativeParent;
@@ -1,4 +1,3 @@
1
- import type syncFs from "node:fs";
2
- import type { FileHandle } from "node:fs/promises";
3
- export declare function writeAtomicDestination(handle: FileHandle, data: Buffer, beforeWrite?: () => Promise<void>, assertBeforeMutation?: () => void, onWriting?: () => void): Promise<void>;
4
- export declare function writeAtomicDestinationSync(fsModule: Pick<typeof syncFs, "ftruncateSync" | "writeSync">, fd: number, data: Buffer, beforeWrite?: () => void, onWriting?: () => void): void;
1
+ import type { AtomicFile, Procedure } from "./atomic-io.js";
2
+ import type { AtomicDestination } from "./replace-file-destination.js";
3
+ export declare function writeAtomicDestination(file: AtomicFile, data: Buffer, destination?: AtomicDestination, restore?: boolean): Procedure<void>;
@@ -1,36 +1,22 @@
1
- export async function writeAtomicDestination(handle, data, beforeWrite, assertBeforeMutation, onWriting) {
2
- if (beforeWrite)
3
- await beforeWrite();
4
- assertBeforeMutation?.();
5
- await handle.truncate(0);
6
- onWriting?.();
1
+ export function* writeAtomicDestination(file, data, destination, restore = false) {
2
+ if (destination)
3
+ yield* destination.beforeWrite(restore);
4
+ yield* file.truncate(0);
5
+ destination?.writing();
7
6
  let written = 0;
8
7
  while (written < data.length) {
9
- if (beforeWrite)
10
- await beforeWrite();
11
- assertBeforeMutation?.();
12
- const result = await handle.write(data, written, data.length - written, written);
13
- if (result.bytesWritten === 0)
14
- throw new Error("Copy fallback write made no progress");
15
- written += result.bytesWritten;
8
+ if (destination)
9
+ yield* destination.beforeWrite(restore);
10
+ let result = file.write(data, written, data.length - written, written);
11
+ if (file.io.asynchronous) {
12
+ const completed = (yield result);
13
+ if (completed.bytesWritten === 0)
14
+ throw new Error("Copy fallback write made no progress");
15
+ result = completed.bytesWritten;
16
+ }
17
+ written += result;
16
18
  }
17
- if (beforeWrite)
18
- await beforeWrite();
19
- assertBeforeMutation?.();
20
- await handle.truncate(data.length);
21
- }
22
- export function writeAtomicDestinationSync(fsModule, fd, data, beforeWrite, onWriting) {
23
- beforeWrite?.();
24
- fsModule.ftruncateSync(fd, 0);
25
- onWriting?.();
26
- let written = 0;
27
- while (written < data.length) {
28
- beforeWrite?.();
29
- const bytesWritten = fsModule.writeSync(fd, data, written, data.length - written, written);
30
- if (bytesWritten === 0)
31
- throw new Error("Copy fallback write made no progress");
32
- written += bytesWritten;
33
- }
34
- beforeWrite?.();
35
- fsModule.ftruncateSync(fd, data.length);
19
+ if (destination)
20
+ yield* destination.beforeWrite(restore);
21
+ yield* file.truncate(data.length);
36
22
  }
@@ -1,4 +1,5 @@
1
- import syncFs, { type BigIntStats } from "node:fs";
1
+ import { type BigIntStats } from "node:fs";
2
+ import { type AtomicIo, type Procedure } from "./atomic-io.js";
2
3
  import { AtomicMutation } from "./replace-file-mutation.js";
3
4
  export type ReplaceFileDestinationHardlinkPolicy = "reject";
4
5
  export type ReplaceFileCopyFallbackRestorePolicy = "restore-original" | "none";
@@ -6,16 +7,8 @@ export type ReplaceFileAtomicRestoreCleanup = "restored" | "restore-failed";
6
7
  export type ReplaceFileAtomicRestoreFailureDetails = {
7
8
  cleanup: ReplaceFileAtomicRestoreCleanup;
8
9
  };
9
- type AsyncFallbackFs = {
10
- lstat: typeof import("node:fs/promises").lstat;
11
- open: typeof import("node:fs/promises").open;
12
- rm: typeof import("node:fs/promises").rm;
13
- };
14
- type SyncFallbackFs = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "ftruncateSync" | "lstatSync" | "openSync" | "readSync" | "rmSync" | "writeSync">;
15
- export declare function assertDestinationHardlinkPolicy(fsModule: AsyncFallbackFs, dest: string, policy?: ReplaceFileDestinationHardlinkPolicy): Promise<void>;
16
- export declare function assertDestinationHardlinkPolicySync(fsModule: SyncFallbackFs, dest: string, policy?: ReplaceFileDestinationHardlinkPolicy): void;
17
- export declare function copyFallbackReplace(params: {
18
- fsModule: AsyncFallbackFs;
10
+ export declare function assertDestinationHardlinkPolicy(io: AtomicIo, dest: string, policy?: ReplaceFileDestinationHardlinkPolicy): Procedure<void>;
11
+ export declare function copyFallbackReplace(io: AtomicIo, params: {
19
12
  src: string;
20
13
  dest: string;
21
14
  destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
@@ -24,9 +17,4 @@ export declare function copyFallbackReplace(params: {
24
17
  expectedSourceIdentity?: BigIntStats;
25
18
  sync: boolean;
26
19
  mutation?: AtomicMutation;
27
- }): Promise<void>;
28
- export declare function copyFallbackReplaceSync(params: Omit<Parameters<typeof copyFallbackReplace>[0], "fsModule"> & {
29
- fsModule: SyncFallbackFs;
30
- fchmodSync?: (fd: number, mode: number) => void;
31
- }): void;
32
- export {};
20
+ }): Procedure<void>;