@openclaw/signal 2026.11.3 → 2026.11.5-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 (179) hide show
  1. package/dist/.setup/{accounts-B0Pba1-q.mjs → accounts-enJKL3ci.mjs} +4 -8
  2. package/dist/.setup/api-CrasQEKI.mjs +14 -0
  3. package/dist/.setup/{api-CbN3mye4.mjs → api-DljZsfUn.mjs} +9 -11
  4. package/dist/.setup/{approval-auth-DhUKkSkA.mjs → approval-auth-B_5Ytx8K.mjs} +7 -11
  5. package/dist/.setup/{approval-handler.runtime-CsEcBZoc.mjs → approval-handler.runtime-0RbVmTxx.mjs} +7 -8
  6. package/dist/.setup/{approval-reactions-JsqCEOHK.mjs → approval-reactions-CvqVSSx8.mjs} +16 -24
  7. package/dist/.setup/{channel-CM-rLLPN.mjs → channel-ByJj8fSh.mjs} +417 -372
  8. package/dist/.setup/{client-Duwqbgqi.mjs → client-CesmEjRc.mjs} +170 -186
  9. package/dist/.setup/{config-schema-DvxZ18SD.mjs → config-schema-BCmW0BLB.mjs} +2 -5
  10. package/dist/.setup/{doctor-contract-api-BIdVW0PF.mjs → doctor-contract-api-CcLwIhsD.mjs} +88 -95
  11. package/dist/.setup/{identity-B3JyPqGO.mjs → identity-vKtR3DrM.mjs} +39 -2
  12. package/dist/.setup/{install-signal-cli-hYVLYGXe.mjs → install-signal-cli-DvLDZfpP.mjs} +95 -109
  13. package/dist/.setup/{transport-url-CAC0JcuI.mjs → legacy-transport-D_cZC3bS.mjs} +18 -1
  14. package/dist/.setup/{monitor-C6u1JV2C.mjs → monitor-Cf5sihwl.mjs} +166 -239
  15. package/dist/.setup/{probe-D_93QCe0.mjs → probe-Caq5Y4kA.mjs} +3 -3
  16. package/dist/.setup/{send-CM7-016e.mjs → send-BJuOWLky.mjs} +34 -66
  17. package/dist/.setup/{setup-surface-DvKLwYrl.mjs → setup-surface-DQ1yFZmY.mjs} +3 -3
  18. package/dist/.setup/{transport-detection-CmlgOybe.mjs → transport-detection-DvR6X_UQ.mjs} +3 -3
  19. package/dist/.setup/{transport-policy-B6C_LogW.mjs → transport-policy-zIR3lRUj.mjs} +1 -1
  20. package/dist/.setup/transport-probes.runtime-CoH6UFtc.mjs +1 -0
  21. package/dist/.source-module-urls/b02c526df8b90083105b63e5b51d5c8bbfe02676df72f6be8dd8f825817b32b9.json +1 -0
  22. package/dist/api.js +9 -11
  23. package/dist/channel-config-api.js +1 -2
  24. package/dist/channel-plugin-api.js +1 -2
  25. package/dist/contract-api.js +3 -4
  26. package/dist/doctor-contract-api.js +1 -2
  27. package/dist/runtime-api.js +1 -2
  28. package/dist/setup-entry.js +1 -1
  29. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +94 -0
  30. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  31. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
  32. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +3 -1
  33. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +2 -2
  34. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +8 -1
  35. package/node_modules/@openclaw/fs-safe/dist/file-open-error.d.ts +2 -0
  36. package/node_modules/@openclaw/fs-safe/dist/file-open-error.js +14 -0
  37. package/node_modules/@openclaw/fs-safe/dist/file-sync.d.ts +2 -1
  38. package/node_modules/@openclaw/fs-safe/dist/file-sync.js +6 -1
  39. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  40. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +1 -0
  41. package/node_modules/@openclaw/fs-safe/dist/native-config.js +14 -3
  42. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +2 -0
  43. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +9 -2
  44. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +40 -32
  45. package/node_modules/@openclaw/fs-safe/dist/output.js +6 -0
  46. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
  47. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -0
  48. package/node_modules/@openclaw/fs-safe/dist/path.js +5 -1
  49. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +25 -2
  50. package/node_modules/@openclaw/fs-safe/dist/publication-handle.d.ts +4 -0
  51. package/node_modules/@openclaw/fs-safe/dist/publication-handle.js +32 -0
  52. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.d.ts +2 -1
  53. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +19 -9
  54. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +23 -24
  55. package/node_modules/@openclaw/fs-safe/dist/publish-rename-fallback.d.ts +21 -0
  56. package/node_modules/@openclaw/fs-safe/dist/publish-rename-fallback.js +102 -0
  57. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +2 -0
  58. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +3 -1
  59. package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +8 -5
  60. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +3 -0
  61. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -1
  62. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +19 -9
  63. package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +3 -3
  64. package/node_modules/@openclaw/fs-safe/dist/root-path-existing.d.ts +2 -2
  65. package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +27 -13
  66. package/node_modules/@openclaw/fs-safe/dist/root-path.js +2 -2
  67. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.d.ts +3 -0
  68. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
  69. package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +1 -1
  70. package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +68 -21
  71. package/node_modules/@openclaw/fs-safe/dist/watch-io-error.d.ts +2 -0
  72. package/node_modules/@openclaw/fs-safe/dist/watch-io-error.js +27 -0
  73. package/node_modules/@openclaw/fs-safe/dist/watch-io-worker.d.ts +1 -0
  74. package/node_modules/@openclaw/fs-safe/dist/watch-io-worker.js +93 -0
  75. package/node_modules/@openclaw/fs-safe/dist/watch-io.d.ts +47 -0
  76. package/node_modules/@openclaw/fs-safe/dist/watch-io.js +236 -0
  77. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +1 -1
  78. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -3
  79. package/node_modules/@openclaw/fs-safe/dist/watch-probe.d.ts +16 -0
  80. package/node_modules/@openclaw/fs-safe/dist/watch-probe.js +135 -0
  81. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +1 -1
  82. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +86 -24
  83. package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +4 -0
  84. package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +78 -16
  85. package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +2 -0
  86. package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +23 -0
  87. package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +3 -1
  88. package/node_modules/@openclaw/fs-safe/dist/watch.js +60 -13
  89. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +1 -1
  90. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +1 -0
  91. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
  92. package/node_modules/@openclaw/fs-safe/docs/advanced.md +5 -2
  93. package/node_modules/@openclaw/fs-safe/docs/compiler-qualification.md +392 -0
  94. package/node_modules/@openclaw/fs-safe/docs/config.md +7 -1
  95. package/node_modules/@openclaw/fs-safe/docs/contributing.md +25 -5
  96. package/node_modules/@openclaw/fs-safe/docs/copy.md +10 -0
  97. package/node_modules/@openclaw/fs-safe/docs/durability.md +9 -3
  98. package/node_modules/@openclaw/fs-safe/docs/errors.md +13 -0
  99. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -4
  100. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +6 -4
  101. package/node_modules/@openclaw/fs-safe/docs/output.md +4 -2
  102. package/node_modules/@openclaw/fs-safe/docs/path.md +5 -2
  103. package/node_modules/@openclaw/fs-safe/docs/permissions.md +15 -0
  104. package/node_modules/@openclaw/fs-safe/docs/security-model.md +9 -0
  105. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +2 -0
  106. package/node_modules/@openclaw/fs-safe/docs/testing.md +1 -1
  107. package/node_modules/@openclaw/fs-safe/docs/watch.md +62 -5
  108. package/node_modules/@openclaw/fs-safe/docs/writing.md +6 -1
  109. package/node_modules/@openclaw/fs-safe/package.json +18 -16
  110. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  111. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  112. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  113. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  114. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  115. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +1 -1
  116. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  117. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +1 -1
  118. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  119. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  120. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  121. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  122. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  123. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  124. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  125. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  126. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  127. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +1 -1
  128. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  129. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  130. package/node_modules/zod/package.json +1 -1
  131. package/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
  132. package/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
  133. package/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
  134. package/node_modules/zod/src/v4/core/compile.ts +9 -0
  135. package/node_modules/zod/src/v4/core/schemas.ts +35 -1
  136. package/node_modules/zod/src/v4/core/util.ts +13 -0
  137. package/node_modules/zod/src/v4/core/versions.ts +1 -1
  138. package/node_modules/zod/v4/core/compile.cjs +8 -0
  139. package/node_modules/zod/v4/core/compile.js +8 -0
  140. package/node_modules/zod/v4/core/schemas.cjs +30 -0
  141. package/node_modules/zod/v4/core/schemas.d.cts +3 -1
  142. package/node_modules/zod/v4/core/schemas.d.ts +3 -1
  143. package/node_modules/zod/v4/core/schemas.js +30 -0
  144. package/node_modules/zod/v4/core/util.cjs +15 -0
  145. package/node_modules/zod/v4/core/util.d.cts +2 -0
  146. package/node_modules/zod/v4/core/util.d.ts +2 -0
  147. package/node_modules/zod/v4/core/util.js +14 -0
  148. package/node_modules/zod/v4/core/versions.cjs +1 -1
  149. package/node_modules/zod/v4/core/versions.js +1 -1
  150. package/openclaw.plugin.json +0 -30
  151. package/package.json +6 -8
  152. package/dist/.setup/api-CJBtD3lT.mjs +0 -16
  153. package/dist/.setup/client-adapter-Chnbch_z.mjs +0 -30
  154. package/dist/.setup/legacy-transport-QIpaMNST.mjs +0 -18
  155. package/dist/.setup/normalize-D7kSomXt.mjs +0 -39
  156. package/dist/.setup/reaction-runtime-api-mdO4k0yv.mjs +0 -65
  157. package/dist/.setup/reaction-targets-BgPLDHBf.mjs +0 -72
  158. package/dist/.setup/rpc-context-BAXcX7dI.mjs +0 -13
  159. package/dist/.setup/transport-probes.runtime-D-xIMRcn.mjs +0 -2
  160. package/dist/reaction-runtime-api.js +0 -2
  161. package/node_modules/ws/LICENSE +0 -20
  162. package/node_modules/ws/README.md +0 -548
  163. package/node_modules/ws/browser.js +0 -8
  164. package/node_modules/ws/index.js +0 -22
  165. package/node_modules/ws/lib/buffer-util.js +0 -131
  166. package/node_modules/ws/lib/constants.js +0 -19
  167. package/node_modules/ws/lib/event-target.js +0 -292
  168. package/node_modules/ws/lib/extension.js +0 -203
  169. package/node_modules/ws/lib/limiter.js +0 -55
  170. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  171. package/node_modules/ws/lib/receiver.js +0 -743
  172. package/node_modules/ws/lib/sender.js +0 -607
  173. package/node_modules/ws/lib/stream.js +0 -161
  174. package/node_modules/ws/lib/subprotocol.js +0 -62
  175. package/node_modules/ws/lib/validation.js +0 -152
  176. package/node_modules/ws/lib/websocket-server.js +0 -562
  177. package/node_modules/ws/lib/websocket.js +0 -1421
  178. package/node_modules/ws/package.json +0 -74
  179. package/node_modules/ws/wrapper.mjs +0 -21
@@ -6,6 +6,7 @@ import { createFileHandle } from "./create.js";
6
6
  import { hasPreservedCreationArtifacts } from "./creation-boundary.js";
7
7
  import { assertSyncDirectoryGuard } from "./directory-guard.js";
8
8
  import { FsSafeError } from "./errors.js";
9
+ import { annotateReadOnlyOpenError } from "./file-open-error.js";
9
10
  import { nativeOpenFlags, removeNativeCreatedFileIfStillPinned, } from "./native-operations.js";
10
11
  import { writePinnedInput } from "./pinned-write-input.js";
11
12
  import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
@@ -83,7 +84,7 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
83
84
  tempName = `.fs-safe-${randomUUID()}.tmp`;
84
85
  params.assertBeforeMutation?.();
85
86
  const copied = params.input.kind === "file" && !params.private
86
- ? await createNativeCopyFile(binding, params.input, parentFd, tempName, params.maxBytes)
87
+ ? await createNativeCopyFile(binding, params.input, parentFd, tempName, params.maxBytes, path.join(parentPath, tempName))
87
88
  : undefined;
88
89
  if (params.private) {
89
90
  const parentIdentity = fsSync.fstatSync(parentFd, { bigint: true });
@@ -102,7 +103,13 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
102
103
  tempFd = copied.fd;
103
104
  }
104
105
  else {
105
- tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL)).fd;
106
+ try {
107
+ tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL)).fd;
108
+ }
109
+ catch (error) {
110
+ annotateReadOnlyOpenError(error, path.join(parentPath, tempName));
111
+ throw error;
112
+ }
106
113
  }
107
114
  const verificationIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(tempFd, { bigint: true }));
108
115
  tempIdentity = verificationIdentity;
@@ -52,7 +52,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
52
52
  });
53
53
  import { assertDarwinCreationAcl, assertPrivateCreationFile } from "./creation-boundary.js";
54
54
  import { requireNativeBinding } from "./native.js";
55
- import { syncFileBestEffortSync } from "./file-sync.js";
55
+ import { syncFile, syncFileBestEffort } from "./file-sync.js";
56
56
  import { randomUUID } from "node:crypto";
57
57
  import fs, {} from "node:fs";
58
58
  import path from "node:path";
@@ -69,6 +69,7 @@ import { createStagedFileReceipt, stagedFailure } from "./staged-file-settlement
69
69
  import { noReplaceUnavailable, rememberNoReplaceUnavailable } from "./native-noreplace.js";
70
70
  import { isFsSafeNativeRequired } from "./native-config.js";
71
71
  import { restoreCopyMetadata } from "./copy-metadata.js";
72
+ import { annotateReadOnlyOpenError } from "./file-open-error.js";
72
73
  export function assertNativeStaging(binding) {
73
74
  if ([
74
75
  binding.closeOwnedFd,
@@ -96,6 +97,7 @@ class NativeStagedFile {
96
97
  #verifyMode;
97
98
  #state = { status: "open", publication: NOT_PUBLISHED };
98
99
  #receipt;
100
+ #publicationFlush;
99
101
  #rejectFinalSymlink = false;
100
102
  #unsupportedNoReplace;
101
103
  #fallbackCleanup;
@@ -132,8 +134,7 @@ class NativeStagedFile {
132
134
  const env_1 = { stack: [], error: void 0, hasError: false };
133
135
  try {
134
136
  const exclusive = params.overwrite === false && params.input.kind === "buffer" && params.input.stageBeforePublish === false;
135
- // This owner never escapes. Only the internal verifier borrows its fd;
136
- // public descriptor methods remain await-free and cannot race disposal.
137
+ // This owner never escapes; only the internal verifier borrows its fd.
137
138
  const staged = __addDisposableResource(env_1, await NativeStagedFile.create({
138
139
  binding, parentFd, closeParentFd, directory, mode: params.mode, sync: params.sync,
139
140
  // Public staging uses portable names; existing POSIX writes accept literal names.
@@ -152,9 +153,10 @@ class NativeStagedFile {
152
153
  }
153
154
  let published;
154
155
  try {
155
- published = exclusive
156
+ const publication = exclusive
156
157
  ? staged.#completePublication(params.basename, false, params.onPublished)
157
- : await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
158
+ : staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
159
+ published = publication instanceof Promise ? await publication : publication;
158
160
  }
159
161
  catch (error) {
160
162
  if (!staged.#unsupportedNoReplace || isFsSafeNativeRequired() ||
@@ -278,14 +280,20 @@ class NativeStagedFile {
278
280
  assertDarwinCreationAcl(this.#owner.parentFd, "file");
279
281
  // Native copying populates its new file before returning its descriptor.
280
282
  const copied = input.kind === "file" && !this.#verifyMode
281
- ? await createNativeCopyFile(this.#owner.binding, input, this.#owner.parentFd, this.#owner.name, maxBytes)
283
+ ? await createNativeCopyFile(this.#owner.binding, input, this.#owner.parentFd, this.#owner.name, maxBytes, path.join(this.#owner.directory.realPath, this.#owner.name))
282
284
  : undefined;
283
285
  state.fileFd = copied?.fd;
284
286
  if (state.fileFd === undefined) {
285
287
  this.#assertBeforeMutation?.();
286
288
  if (this.#private)
287
289
  assertDarwinCreationAcl(this.#owner.parentFd, "file");
288
- state.fileFd = this.#owner.binding.createStagedFile(this.#owner.parentFd, this.#owner.name);
290
+ try {
291
+ state.fileFd = this.#owner.binding.createStagedFile(this.#owner.parentFd, this.#owner.name);
292
+ }
293
+ catch (error) {
294
+ annotateReadOnlyOpenError(error, path.join(this.#owner.directory.realPath, this.#owner.name));
295
+ throw error;
296
+ }
289
297
  }
290
298
  const fd = state.fileFd;
291
299
  if (input.kind === "file")
@@ -311,29 +319,21 @@ class NativeStagedFile {
311
319
  restoreCopyMetadata(fd, input.metadata);
312
320
  if (this.#verifyMode)
313
321
  this.#assertStagePermissions(fd);
314
- if (this.#owner.sync !== false) {
315
- if (this.#owner.strictFileSync)
316
- fs.fsyncSync(fd);
317
- else
318
- syncFileBestEffortSync(fd);
319
- }
322
+ if (this.#owner.sync !== false)
323
+ await (this.#owner.strictFileSync ? syncFile(fd) : syncFileBestEffort(fd));
320
324
  const stat = fs.fstatSync(fd, { bigint: true });
321
325
  this.#receipt = createStagedFileReceipt(this.#owner.directory, this.#owner.name, stat);
322
326
  this.#assertCurrent();
323
327
  }
324
328
  catch (error) {
325
329
  const { receipt: cleanup, error: cleanupError } = this.#finalize();
326
- if (cleanupError) {
327
- throw stagedFailure("file", new AggregateError([error, cleanupError], "preparation and cleanup failed"), { phase: "prepare", publication: cleanup.publication, cleanup });
328
- }
329
- if (cleanup.status === "preserved") {
330
- throw stagedFailure("file", error, { phase: "prepare", publication: cleanup.publication, cleanup });
330
+ if (cleanupError || cleanup.status === "preserved") {
331
+ throw stagedFailure("file", cleanupError
332
+ ? new AggregateError([error, cleanupError], "preparation and cleanup failed") : error, { phase: "prepare", publication: cleanup.publication, cleanup });
331
333
  }
332
334
  throw error;
333
335
  }
334
336
  }
335
- // Keep public descriptor work await-free: each call completes its mutations
336
- // before the next invocation, including closure and cached cleanup failures.
337
337
  async assertCurrent() {
338
338
  this.#assertCurrent();
339
339
  }
@@ -378,7 +378,8 @@ class NativeStagedFile {
378
378
  }
379
379
  throw error;
380
380
  }
381
- return this.#completePublication(basename, overwrite, onPublished);
381
+ const publication = this.#completePublication(basename, overwrite, onPublished);
382
+ return publication instanceof Promise ? await publication : publication;
382
383
  }
383
384
  catch (error) {
384
385
  if (isMutationAuthorityError(error))
@@ -404,17 +405,21 @@ class NativeStagedFile {
404
405
  fs.fchmodSync(fd, this.#owner.mode);
405
406
  // Content was synced during preparation. A lost chmod leaves 0600, no wider
406
407
  // than modes retaining owner rw; restrictive modes still need a durable correction.
407
- if (this.#owner.sync !== false && ((this.#owner.mode & 0o600) !== 0o600 || (stagedMode & ~this.#owner.mode) !== 0)) {
408
- if (this.#owner.strictFileSync)
409
- fs.fsyncSync(fd);
410
- else
411
- syncFileBestEffortSync(fd);
412
- }
413
- if (this.#owner.sync !== false)
414
- syncFileBestEffortSync(this.#owner.parentFd);
415
- this.#assertNamed(basename, this.#owner.mode);
416
- this.#assertDirectory();
417
- return receipt;
408
+ const verify = () => {
409
+ this.#assertNamed(basename, this.#owner.mode);
410
+ this.#assertDirectory();
411
+ return receipt;
412
+ };
413
+ if (this.#owner.sync === false)
414
+ return verify();
415
+ this.#publicationFlush = Promise.resolve().then(async () => {
416
+ if ((this.#owner.mode & 0o600) !== 0o600 || (stagedMode & ~this.#owner.mode) !== 0) {
417
+ await (this.#owner.strictFileSync ? syncFile(fd) : syncFileBestEffort(fd));
418
+ }
419
+ await syncFileBestEffort(this.#owner.parentFd);
420
+ return verify();
421
+ }).finally(() => { this.#publicationFlush = undefined; });
422
+ return this.#publicationFlush;
418
423
  }
419
424
  #finalize() {
420
425
  if (this.#state.status === "closed") {
@@ -469,6 +474,9 @@ class NativeStagedFile {
469
474
  return this.#state;
470
475
  }
471
476
  async cleanup() {
477
+ // Native descriptors cannot close or be recycled while libuv is synchronizing them.
478
+ if (this.#publicationFlush)
479
+ await this.#publicationFlush.catch(() => { });
472
480
  const closed = this.#finalize();
473
481
  if (closed.error) {
474
482
  throw closed.error;
@@ -8,6 +8,7 @@ import { isPathInside } from "./path.js";
8
8
  import { root } from "./root.js";
9
9
  import { tempFile } from "./temp-target.js";
10
10
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
11
+ import { createAsyncDirectoryGuard } from "./directory-guard.js";
11
12
  const NON_PORTABLE_FILE_NAME_CHARACTERS = /[\u0000-\u001f\u007f-\u009f<>:"/\\|?*]/u;
12
13
  function tempFileNameForTarget(targetPath, fallbackFileName) {
13
14
  const fallback = sanitizeUntrustedFileName(fallbackFileName ?? "output.bin", "output.bin");
@@ -62,6 +63,9 @@ export async function writeExternalFileWithinRoot(options) {
62
63
  const staging = options.staging;
63
64
  const fallbackFileName = options.fallbackFileName;
64
65
  const targetRoot = await root(rootDir);
66
+ const rootBoundary = staging === "sibling"
67
+ ? await createAsyncDirectoryGuard(targetRoot.rootReal, { bigint: true })
68
+ : undefined;
65
69
  if (requestedTargetPath.length === 0) {
66
70
  throw new FsSafeError("invalid-path", "target path is required");
67
71
  }
@@ -84,6 +88,7 @@ export async function writeExternalFileWithinRoot(options) {
84
88
  const siblingFinalPath = await targetRoot.resolve(targetPath);
85
89
  const result = await writeExternalFileViaSibling({
86
90
  finalPath: siblingFinalPath,
91
+ rootBoundary,
87
92
  write,
88
93
  producerIsolation,
89
94
  fallbackFileName,
@@ -126,6 +131,7 @@ async function writeExternalFileViaSibling(params) {
126
131
  assertNoWindowsPathAlias(finalPath, "filesystem", "output target uses a Windows filesystem namespace alias");
127
132
  const { result } = await writeCallbackSibling({
128
133
  tempDir: path.dirname(finalPath),
134
+ rootBoundary: params.rootBoundary,
129
135
  tempName: buildSiblingTempName(finalPath, params.fallbackFileName),
130
136
  write: params.write,
131
137
  producerIsolation: params.producerIsolation,
@@ -21,6 +21,8 @@ export type OwnerAndDaclResult = {
21
21
  currentUserSid: string;
22
22
  daclPresent: boolean;
23
23
  isLocal: boolean;
24
+ /** Verified local volume root with a matching non-reparse entry; omitted by older helpers. */
25
+ isVolumeRoot?: boolean;
24
26
  complete: boolean;
25
27
  unsupportedAceTypes: number[];
26
28
  aces: WindowsAccessControlEntry[];
@@ -27,6 +27,7 @@ export function projectOwnerAndDacl(facts) {
27
27
  currentUserSid: facts.currentUserSid,
28
28
  daclPresent: facts.daclPresent,
29
29
  isLocal: facts.isLocal,
30
+ ...(facts.isVolumeRoot === undefined ? {} : { isVolumeRoot: facts.isVolumeRoot }),
30
31
  complete: facts.aceListComplete,
31
32
  unsupportedAceTypes: [...facts.unsupportedAceTypes],
32
33
  aces: facts.aces.map((entry) => ({
@@ -115,7 +115,11 @@ export function safeRealpathSync(targetPath, cache) {
115
115
  return cached;
116
116
  }
117
117
  try {
118
- const resolved = realpathSync(pathForWindowsFilesystem(targetPath));
118
+ const ordinary = realpathSync(pathForWindowsFilesystem(targetPath));
119
+ // Ordinary resolution keeps the caller's letter case. Its result has no symlinks or
120
+ // dot segments left, so native resolution on POSIX only adopts the on-disk spelling
121
+ // that root admission compares against. Windows root admission reconciles aliases.
122
+ const resolved = process.platform === "win32" ? ordinary : realpathSync.native(ordinary);
119
123
  cache?.set(targetPath, resolved);
120
124
  cache?.set(resolved, resolved);
121
125
  return resolved;
@@ -14,6 +14,7 @@ import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
14
14
  import { writePinnedInput } from "./pinned-write-input.js";
15
15
  import { assertPinnedWriteMode, pinnedWriteModeAssertion, preparePinnedWriteMode } from "./pinned-write-mode.js";
16
16
  import { publishCopyStage } from "./publish-copy-stage.js";
17
+ import { openPublicationHandle } from "./publication-handle.js";
17
18
  import { resolveReadOpenFlags } from "./read-open-flags.js";
18
19
  import { cleanupPinnedFilePath } from "./file-cleanup.js";
19
20
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
@@ -35,6 +36,7 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
35
36
  let handle;
36
37
  let tempIdentity;
37
38
  let readHandle;
39
+ let handoffCloseFailure;
38
40
  let renamed = false;
39
41
  const completeCreate = params.overwrite === false && params.input.kind !== "file" && params.input.stageBeforePublish === true;
40
42
  let receipt;
@@ -89,11 +91,15 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
89
91
  }), path.basename(tempPath), fsSync.fstatSync(handle.fd, { bigint: true }));
90
92
  }
91
93
  let verifiedIdentity = expectedTempStat;
94
+ // Keep unreadable modes supported where Node has no read-free file open.
95
+ const canHandoff = process.platform !== "win32" &&
96
+ ((params.mode & 0o400) !== 0 ||
97
+ (process.platform === "linux" && (process.arch === "x64" || process.arch === "arm64")));
92
98
  phase = "publish";
93
99
  await withAsyncDirectoryGuards([parentGuard], async () => {
94
100
  assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
95
101
  if (params.overwrite === false) {
96
- publishCopyStage({
102
+ await publishCopyStage({
97
103
  temporaryPath: tempPath, targetPath, fd: handle.fd,
98
104
  identity: tempIdentity, parentGuard,
99
105
  assertBeforeMutation,
@@ -107,6 +113,18 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
107
113
  publication = Object.freeze({ status: "published", staged: receipt, basename: params.basename, overwrite: false });
108
114
  params.onPublished?.(identity);
109
115
  },
116
+ beforeTemporaryUnlink: !canHandoff ? undefined : async () => {
117
+ readHandle = await openPublicationHandle(targetPath, expectedTempStat);
118
+ // Keep the destination pinned while closing the stage's NFS dentry.
119
+ const stagedHandle = handle;
120
+ handle = undefined;
121
+ try {
122
+ await stagedHandle.close();
123
+ }
124
+ catch (error) {
125
+ handoffCloseFailure = { error };
126
+ }
127
+ },
110
128
  });
111
129
  }
112
130
  else {
@@ -134,6 +152,7 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
134
152
  throw new FsSafeError("path-mismatch", "fallback target changed during write");
135
153
  }
136
154
  const expectedHash = sha256Hex(params.input.data, params.input.encoding);
155
+ await readHandle?.close();
137
156
  readHandle = await fs.open(targetPath, resolveReadOpenFlags());
138
157
  const readHandleStat = fsSync.fstatSync(readHandle.fd, { bigint: true });
139
158
  const actualHash = sha256Hex(await readFileHandleBounded(readHandle, Infinity));
@@ -172,7 +191,11 @@ export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
172
191
  pathname: tempPath, handle, identity: tempIdentity, parentGuard, throwOnCleanupError: true,
173
192
  }) : "not-needed";
174
193
  },
175
- close: [async () => { await readHandle?.close(); }, async () => { await handle?.close(); }],
194
+ close: [async () => { await readHandle?.close(); }, async () => {
195
+ await handle?.close();
196
+ if (handoffCloseFailure)
197
+ throw handoffCloseFailure.error;
198
+ }],
176
199
  });
177
200
  }
178
201
  else
@@ -0,0 +1,4 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ /** Pin the published name before releasing a descriptor opened through its stage. */
4
+ export declare function openPublicationHandle(targetPath: string, identity: BigIntStats): Promise<fs.FileHandle>;
@@ -0,0 +1,32 @@
1
+ import fsSync, {} from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { sameFileIdentityForCleanup } from "./file-identity.js";
5
+ import { hasErrorCode } from "./file-cleanup.js";
6
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
7
+ /** Pin the published name before releasing a descriptor opened through its stage. */
8
+ export async function openPublicationHandle(targetPath, identity) {
9
+ const flags = resolveReadOpenFlags();
10
+ const handle = await fs.open(targetPath, flags).catch(async (error) => {
11
+ // Restrictive final modes must not be widened just to inspect publication.
12
+ if (!hasErrorCode(error, "EACCES") || (process.arch !== "x64" && process.arch !== "arm64"))
13
+ throw error;
14
+ const metadataFlag = process.platform === "linux" ? 0x200000 : undefined; // O_PATH
15
+ if (metadataFlag === undefined)
16
+ throw error;
17
+ return fs.open(targetPath, metadataFlag | fsSync.constants.O_NOFOLLOW);
18
+ });
19
+ try {
20
+ const opened = fsSync.fstatSync(handle.fd, { bigint: true });
21
+ const named = fsSync.lstatSync(targetPath, { bigint: true });
22
+ if (!opened.isFile() || !named.isFile() || named.isSymbolicLink() ||
23
+ !sameFileIdentityForCleanup(opened, identity) || !sameFileIdentityForCleanup(named, identity)) {
24
+ throw new FsSafeError("path-mismatch", "publication descriptor handoff identity changed");
25
+ }
26
+ return handle;
27
+ }
28
+ catch (error) {
29
+ await handle.close().catch(() => undefined);
30
+ throw error;
31
+ }
32
+ }
@@ -10,4 +10,5 @@ export declare function publishCopyStage(params: {
10
10
  assertBeforeMutation?: () => void;
11
11
  onPublicationAttempt?: () => void;
12
12
  onPublished?: (identity: PublishedWriteIdentity) => void;
13
- }): void;
13
+ beforeTemporaryUnlink?: () => Promise<void>;
14
+ }): Promise<void>;
@@ -7,8 +7,8 @@ import { sameFileIdentityForCleanup } from "./file-identity.js";
7
7
  import { hasErrorCode } from "./file-cleanup.js";
8
8
  import { isHardlinkFallbackError } from "./hardlink-fallback.js";
9
9
  // Node has no portable no-replace rename. Publish the completed stage by link,
10
- // then remove its verified temporary name without yielding.
11
- export function publishCopyStage(params) {
10
+ // then remove its verified temporary name after any descriptor handoff.
11
+ export async function publishCopyStage(params) {
12
12
  const { identity, temporaryPath, targetPath } = params;
13
13
  const assertCurrent = () => {
14
14
  assertSyncDirectoryGuard(params.parentGuard);
@@ -62,13 +62,23 @@ export function publishCopyStage(params) {
62
62
  }
63
63
  }
64
64
  else {
65
- const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
66
- const current = fs.lstatSync(temporaryPath, { bigint: true });
67
- if (parent.isSymbolicLink() || !parent.isDirectory() ||
68
- !sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
69
- current.isSymbolicLink() || !current.isFile() ||
70
- !sameFileIdentityForCleanup(current, identity)) {
71
- throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
65
+ const verifyPair = () => {
66
+ assertSyncDirectoryGuard(params.parentGuard);
67
+ const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
68
+ const current = fs.lstatSync(temporaryPath, { bigint: true });
69
+ const target = fs.lstatSync(targetPath, { bigint: true });
70
+ if (parent.isSymbolicLink() || !parent.isDirectory() ||
71
+ !sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
72
+ current.isSymbolicLink() || !current.isFile() || current.nlink !== 2n ||
73
+ target.isSymbolicLink() || !target.isFile() || target.nlink !== 2n ||
74
+ !sameFileIdentityForCleanup(current, identity) || !sameFileIdentityForCleanup(target, identity)) {
75
+ throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
76
+ }
77
+ };
78
+ verifyPair();
79
+ if (params.beforeTemporaryUnlink) {
80
+ await params.beforeTemporaryUnlink();
81
+ verifyPair();
72
82
  }
73
83
  // This removes the stage's name, never the published destination or source.
74
84
  fs.unlinkSync(temporaryPath);
@@ -21,6 +21,8 @@ import { getFsSafeTestHooks } from "./test-hooks.js";
21
21
  import { hasErrorCode } from "./file-cleanup.js";
22
22
  import { writeAllToFile } from "./write-file-handle.js";
23
23
  import { isHardlinkFallbackError } from "./hardlink-fallback.js";
24
+ import { publishRenameFallback } from "./publish-rename-fallback.js";
25
+ import { isNoReplaceUnavailable } from "./native-noreplace.js";
24
26
  export { isHardlinkFallbackError } from "./hardlink-fallback.js";
25
27
  const NATIVE_COPY_FALLBACK_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV"]);
26
28
  function directoryOpenFlags() {
@@ -232,6 +234,7 @@ export async function publishFileExclusive(params) {
232
234
  let sourceNativeParent;
233
235
  let targetNativeParent;
234
236
  let copiedTarget;
237
+ let renamedTarget;
235
238
  const strategy = params.strategy;
236
239
  const failure = {
237
240
  phase: strategy === "rename-noreplace" ? "rename-create" : "hardlink-create",
@@ -264,6 +267,9 @@ export async function publishFileExclusive(params) {
264
267
  const observed = fsSync.fstatSync(handle.fd, { bigint: true });
265
268
  if (!sameFileIdentity(observed, identity))
266
269
  throw new FsSafeError("path-mismatch", "publication descriptor changed");
270
+ if (failure.fallback && (observed.nlink !== 1n || fsSync.lstatSync(targetPath, { bigint: true }).nlink !== 1n)) {
271
+ throw new FsSafeError("hardlink", "no-replace publication link count changed");
272
+ }
267
273
  return { method, ...(failure.fallback ? { fallback: failure.fallback } : {}), identity: numberStats(observed), exactIdentity: exactFileIdentity(observed), directorySync };
268
274
  };
269
275
  await parent.assertCurrent();
@@ -278,36 +284,28 @@ export async function publishFileExclusive(params) {
278
284
  if (strategy === "rename-noreplace") {
279
285
  const binding = requireNativeBinding("renameNoReplace");
280
286
  try {
281
- const fallback = nativeMoveNoReplace(binding, [
287
+ nativeMoveNoReplace(binding, [
282
288
  sourceNativeParent.handle.fd, sourceNativeParent.basename,
283
289
  targetNativeParent.handle.fd, targetNativeParent.basename,
284
- ], sourceExactIdentity, getFsSafeNativeConfig().mode === "auto", "file publication");
285
- if (fallback) {
286
- failure.fallback = fallback;
287
- failure.sourceRemoval = "removed";
288
- }
290
+ ], sourceExactIdentity, false, "file publication");
289
291
  }
290
292
  catch (error) {
291
- if (error instanceof FsSafeError) {
292
- if (error.details?.publication === "published") {
293
- failure.targetCreated = true;
294
- failure.preserveTarget = true;
295
- failure.phase = "rename-verify";
296
- failure.fallback = "link-unlink";
297
- failure.sourceRemoval = error.details.sourceRemoval;
298
- // Changed/unverified pairs cannot establish which inode was published.
299
- if (failure.sourceRemoval === "still-linked")
300
- rememberCreatedTarget(failure, sourceExactIdentity, "rename-verify");
293
+ if (isNoReplaceUnavailable(error) && getFsSafeNativeConfig().mode === "auto" && process.platform === "linux") {
294
+ renamedTarget = await publishRenameFallback({
295
+ binding, source, sourcePath, targetPath, identity: sourceExactIdentity, failure,
296
+ sourceParent: { fd: sourceNativeParent.handle.fd, basename: sourceNativeParent.basename },
297
+ targetParent: { fd: targetNativeParent.handle.fd, basename: targetNativeParent.basename },
298
+ });
299
+ }
300
+ else {
301
+ const code = error?.code;
302
+ if (code === "path-mismatch" || code === "hardlink") {
303
+ throw new FsSafeError(code, "publication source changed before fallback", { cause: error });
301
304
  }
305
+ if (code === "ELOOP")
306
+ throw new FsSafeError("symlink", "publication source became a symlink", { cause: error });
302
307
  throw error;
303
308
  }
304
- const code = error?.code;
305
- if (code === "path-mismatch" || code === "hardlink") {
306
- throw new FsSafeError(code, "publication source changed before fallback", { cause: error });
307
- }
308
- if (code === "ELOOP")
309
- throw new FsSafeError("symlink", "publication source became a symlink", { cause: error });
310
- throw error;
311
309
  }
312
310
  rememberCreatedTarget(failure, sourceExactIdentity, "rename-verify");
313
311
  // A failed post-rename fence must not delete the only remaining name.
@@ -316,7 +314,7 @@ export async function publishFileExclusive(params) {
316
314
  assertPublishedTargetCurrent(targetPath, sourceExactIdentity, "no-replace publication target changed");
317
315
  assertRenamedSourceAbsent(sourcePath);
318
316
  syncFileBestEffortSync(sourceNativeParent.handle.fd);
319
- return await finishPublication("rename-noreplace", source, sourceExactIdentity);
317
+ return await finishPublication("rename-noreplace", renamedTarget ?? source, sourceExactIdentity);
320
318
  }
321
319
  try {
322
320
  if (native?.linkBeneath) {
@@ -363,6 +361,7 @@ export async function publishFileExclusive(params) {
363
361
  throw publicationFailure(error, failure, cleanup);
364
362
  }
365
363
  finally {
364
+ await renamedTarget?.close().catch(() => undefined);
366
365
  await copiedTarget?.close().catch(() => undefined);
367
366
  await sourceNativeParent?.handle.close().catch(() => undefined);
368
367
  await targetNativeParent?.handle.close().catch(() => undefined);
@@ -0,0 +1,21 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import type { FileHandle } from "node:fs/promises";
3
+ import type { NativeBinding } from "./native-binding.js";
4
+ import { type PublishFailureState } from "./publish-file-failure.js";
5
+ /** Retain the target pin while closing the source dentry, before native unlink. */
6
+ export declare function publishRenameFallback(params: {
7
+ binding: NativeBinding;
8
+ sourceParent: {
9
+ fd: number;
10
+ basename: string;
11
+ };
12
+ targetParent: {
13
+ fd: number;
14
+ basename: string;
15
+ };
16
+ sourcePath: string;
17
+ targetPath: string;
18
+ source: FileHandle;
19
+ identity: BigIntStats;
20
+ failure: PublishFailureState;
21
+ }): Promise<FileHandle>;
@@ -0,0 +1,102 @@
1
+ import fs, {} from "node:fs";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { sameFileIdentityForCleanup } from "./file-identity.js";
4
+ import { openPublicationHandle } from "./publication-handle.js";
5
+ import { rememberCreatedTarget } from "./publish-file-failure.js";
6
+ /** Retain the target pin while closing the source dentry, before native unlink. */
7
+ export async function publishRenameFallback(params) {
8
+ const { binding, sourceParent, targetParent, sourcePath, targetPath, source, identity, failure } = params;
9
+ if (!binding.linkBeneath || !binding.removeStagedFile) {
10
+ throw new FsSafeError("helper-unavailable", "no-replace publication needs native link and identity-bound unlink", {
11
+ details: { capability: "rename-noreplace", fallback: "link-unlink", fallbackCapability: "linkat" },
12
+ });
13
+ }
14
+ const verify = (stat, links) => {
15
+ if (!stat.isFile() || stat.isSymbolicLink() || !sameFileIdentityForCleanup(stat, identity)) {
16
+ throw new FsSafeError("path-mismatch", "no-replace publication identity changed");
17
+ }
18
+ if (stat.nlink !== links)
19
+ throw new FsSafeError("hardlink", "no-replace publication link count changed");
20
+ };
21
+ verify(fs.fstatSync(source.fd, { bigint: true }), 1n);
22
+ verify(fs.lstatSync(sourcePath, { bigint: true }), 1n);
23
+ try {
24
+ binding.linkBeneath(sourceParent.fd, sourceParent.basename, targetParent.fd, targetParent.basename);
25
+ }
26
+ catch (error) {
27
+ if (error?.code === "EEXIST") {
28
+ throw new FsSafeError("already-exists", "no-replace fallback destination exists", { cause: error });
29
+ }
30
+ if (["EPERM", "ENOTSUP", "EOPNOTSUPP", "EMLINK", "EXDEV", "ENOSYS"].includes(error?.code ?? "")) {
31
+ throw new FsSafeError("helper-unavailable", "renameat2 RENAME_NOREPLACE unavailable; linkat file publication fallback is unavailable", {
32
+ cause: error, details: { capability: "rename-noreplace", fallback: "link-unlink", fallbackCapability: "linkat" },
33
+ });
34
+ }
35
+ throw error;
36
+ }
37
+ failure.targetCreated = true;
38
+ failure.preserveTarget = true;
39
+ failure.phase = "rename-verify";
40
+ failure.fallback = "link-unlink";
41
+ failure.sourceRemoval = "not-attempted";
42
+ let target;
43
+ const verifyPair = () => {
44
+ try {
45
+ verify(fs.lstatSync(sourcePath, { bigint: true }), 2n);
46
+ verify(fs.lstatSync(targetPath, { bigint: true }), 2n);
47
+ if (target)
48
+ verify(fs.fstatSync(target.fd, { bigint: true }), 2n);
49
+ }
50
+ catch (cause) {
51
+ throw new FsSafeError("path-mismatch", "no-replace publication link pair changed", { cause });
52
+ }
53
+ };
54
+ try {
55
+ verifyPair();
56
+ rememberCreatedTarget(failure, identity, "rename-verify");
57
+ target = await openPublicationHandle(targetPath, identity);
58
+ await source.close();
59
+ verifyPair();
60
+ failure.sourceRemoval = "still-linked";
61
+ let removed;
62
+ try {
63
+ removed = binding.removeStagedFile(sourceParent.fd, sourceParent.basename, target.fd);
64
+ }
65
+ catch (error) {
66
+ try {
67
+ verify(fs.fstatSync(target.fd, { bigint: true }), 1n);
68
+ verify(fs.lstatSync(targetPath, { bigint: true }), 1n);
69
+ if (fs.lstatSync(sourcePath, { bigint: true, throwIfNoEntry: false }))
70
+ throw error;
71
+ removed = "name-absent";
72
+ }
73
+ catch {
74
+ try {
75
+ verifyPair();
76
+ }
77
+ catch {
78
+ failure.sourceRemoval = "unverified";
79
+ }
80
+ const detail = failure.sourceRemoval === "still-linked" ? "source still linked" : "source removal unverified";
81
+ throw new FsSafeError("helper-failed", `linkat file publication fallback published target; ${detail}`, { cause: error });
82
+ }
83
+ }
84
+ if (removed !== "removed" && removed !== "name-absent") {
85
+ failure.sourceRemoval = "unverified";
86
+ throw new FsSafeError("helper-failed", "no-replace publication source removal is unverified");
87
+ }
88
+ failure.sourceRemoval = "removed";
89
+ try {
90
+ verify(fs.fstatSync(target.fd, { bigint: true }), 1n);
91
+ verify(fs.lstatSync(targetPath, { bigint: true }), 1n);
92
+ }
93
+ catch (cause) {
94
+ throw new FsSafeError("helper-failed", "no-replace publication target verification failed", { cause });
95
+ }
96
+ return target;
97
+ }
98
+ catch (error) {
99
+ await target?.close().catch(() => undefined);
100
+ throw error;
101
+ }
102
+ }