@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.
- package/dist/.setup/{accounts-B0Pba1-q.mjs → accounts-enJKL3ci.mjs} +4 -8
- package/dist/.setup/api-CrasQEKI.mjs +14 -0
- package/dist/.setup/{api-CbN3mye4.mjs → api-DljZsfUn.mjs} +9 -11
- package/dist/.setup/{approval-auth-DhUKkSkA.mjs → approval-auth-B_5Ytx8K.mjs} +7 -11
- package/dist/.setup/{approval-handler.runtime-CsEcBZoc.mjs → approval-handler.runtime-0RbVmTxx.mjs} +7 -8
- package/dist/.setup/{approval-reactions-JsqCEOHK.mjs → approval-reactions-CvqVSSx8.mjs} +16 -24
- package/dist/.setup/{channel-CM-rLLPN.mjs → channel-ByJj8fSh.mjs} +417 -372
- package/dist/.setup/{client-Duwqbgqi.mjs → client-CesmEjRc.mjs} +170 -186
- package/dist/.setup/{config-schema-DvxZ18SD.mjs → config-schema-BCmW0BLB.mjs} +2 -5
- package/dist/.setup/{doctor-contract-api-BIdVW0PF.mjs → doctor-contract-api-CcLwIhsD.mjs} +88 -95
- package/dist/.setup/{identity-B3JyPqGO.mjs → identity-vKtR3DrM.mjs} +39 -2
- package/dist/.setup/{install-signal-cli-hYVLYGXe.mjs → install-signal-cli-DvLDZfpP.mjs} +95 -109
- package/dist/.setup/{transport-url-CAC0JcuI.mjs → legacy-transport-D_cZC3bS.mjs} +18 -1
- package/dist/.setup/{monitor-C6u1JV2C.mjs → monitor-Cf5sihwl.mjs} +166 -239
- package/dist/.setup/{probe-D_93QCe0.mjs → probe-Caq5Y4kA.mjs} +3 -3
- package/dist/.setup/{send-CM7-016e.mjs → send-BJuOWLky.mjs} +34 -66
- package/dist/.setup/{setup-surface-DvKLwYrl.mjs → setup-surface-DQ1yFZmY.mjs} +3 -3
- package/dist/.setup/{transport-detection-CmlgOybe.mjs → transport-detection-DvR6X_UQ.mjs} +3 -3
- package/dist/.setup/{transport-policy-B6C_LogW.mjs → transport-policy-zIR3lRUj.mjs} +1 -1
- package/dist/.setup/transport-probes.runtime-CoH6UFtc.mjs +1 -0
- package/dist/.source-module-urls/b02c526df8b90083105b63e5b51d5c8bbfe02676df72f6be8dd8f825817b32b9.json +1 -0
- package/dist/api.js +9 -11
- package/dist/channel-config-api.js +1 -2
- package/dist/channel-plugin-api.js +1 -2
- package/dist/contract-api.js +3 -4
- package/dist/doctor-contract-api.js +1 -2
- package/dist/runtime-api.js +1 -2
- package/dist/setup-entry.js +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +94 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/file-open-error.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/file-open-error.js +14 -0
- package/node_modules/@openclaw/fs-safe/dist/file-sync.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/file-sync.js +6 -1
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +14 -3
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +9 -2
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +40 -32
- package/node_modules/@openclaw/fs-safe/dist/output.js +6 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +25 -2
- package/node_modules/@openclaw/fs-safe/dist/publication-handle.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/publication-handle.js +32 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +19 -9
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +23 -24
- package/node_modules/@openclaw/fs-safe/dist/publish-rename-fallback.d.ts +21 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-rename-fallback.js +102 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +8 -5
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +19 -9
- package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-existing.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +27 -13
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +68 -21
- package/node_modules/@openclaw/fs-safe/dist/watch-io-error.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-io-error.js +27 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-io-worker.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-io-worker.js +93 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-io.d.ts +47 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-io.js +236 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/watch-probe.d.ts +16 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-probe.js +135 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +86 -24
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +78 -16
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +23 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/watch.js +60 -13
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +5 -2
- package/node_modules/@openclaw/fs-safe/docs/compiler-qualification.md +392 -0
- package/node_modules/@openclaw/fs-safe/docs/config.md +7 -1
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +25 -5
- package/node_modules/@openclaw/fs-safe/docs/copy.md +10 -0
- package/node_modules/@openclaw/fs-safe/docs/durability.md +9 -3
- package/node_modules/@openclaw/fs-safe/docs/errors.md +13 -0
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -4
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +6 -4
- package/node_modules/@openclaw/fs-safe/docs/output.md +4 -2
- package/node_modules/@openclaw/fs-safe/docs/path.md +5 -2
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +15 -0
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +9 -0
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/testing.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/watch.md +62 -5
- package/node_modules/@openclaw/fs-safe/docs/writing.md +6 -1
- package/node_modules/@openclaw/fs-safe/package.json +18 -16
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/node_modules/zod/package.json +1 -1
- package/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
- package/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
- package/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
- package/node_modules/zod/src/v4/core/compile.ts +9 -0
- package/node_modules/zod/src/v4/core/schemas.ts +35 -1
- package/node_modules/zod/src/v4/core/util.ts +13 -0
- package/node_modules/zod/src/v4/core/versions.ts +1 -1
- package/node_modules/zod/v4/core/compile.cjs +8 -0
- package/node_modules/zod/v4/core/compile.js +8 -0
- package/node_modules/zod/v4/core/schemas.cjs +30 -0
- package/node_modules/zod/v4/core/schemas.d.cts +3 -1
- package/node_modules/zod/v4/core/schemas.d.ts +3 -1
- package/node_modules/zod/v4/core/schemas.js +30 -0
- package/node_modules/zod/v4/core/util.cjs +15 -0
- package/node_modules/zod/v4/core/util.d.cts +2 -0
- package/node_modules/zod/v4/core/util.d.ts +2 -0
- package/node_modules/zod/v4/core/util.js +14 -0
- package/node_modules/zod/v4/core/versions.cjs +1 -1
- package/node_modules/zod/v4/core/versions.js +1 -1
- package/openclaw.plugin.json +0 -30
- package/package.json +6 -8
- package/dist/.setup/api-CJBtD3lT.mjs +0 -16
- package/dist/.setup/client-adapter-Chnbch_z.mjs +0 -30
- package/dist/.setup/legacy-transport-QIpaMNST.mjs +0 -18
- package/dist/.setup/normalize-D7kSomXt.mjs +0 -39
- package/dist/.setup/reaction-runtime-api-mdO4k0yv.mjs +0 -65
- package/dist/.setup/reaction-targets-BgPLDHBf.mjs +0 -72
- package/dist/.setup/rpc-context-BAXcX7dI.mjs +0 -13
- package/dist/.setup/transport-probes.runtime-D-xIMRcn.mjs +0 -2
- package/dist/reaction-runtime-api.js +0 -2
- package/node_modules/ws/LICENSE +0 -20
- package/node_modules/ws/README.md +0 -548
- package/node_modules/ws/browser.js +0 -8
- package/node_modules/ws/index.js +0 -22
- package/node_modules/ws/lib/buffer-util.js +0 -131
- package/node_modules/ws/lib/constants.js +0 -19
- package/node_modules/ws/lib/event-target.js +0 -292
- package/node_modules/ws/lib/extension.js +0 -203
- package/node_modules/ws/lib/limiter.js +0 -55
- package/node_modules/ws/lib/permessage-deflate.js +0 -530
- package/node_modules/ws/lib/receiver.js +0 -743
- package/node_modules/ws/lib/sender.js +0 -607
- package/node_modules/ws/lib/stream.js +0 -161
- package/node_modules/ws/lib/subprotocol.js +0 -62
- package/node_modules/ws/lib/validation.js +0 -152
- package/node_modules/ws/lib/websocket-server.js +0 -562
- package/node_modules/ws/lib/websocket.js +0 -1421
- package/node_modules/ws/package.json +0 -74
- 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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
156
|
+
const publication = exclusive
|
|
156
157
|
? staged.#completePublication(params.basename, false, params.onPublished)
|
|
157
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
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 () => {
|
|
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
|
-
|
|
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
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
!
|
|
71
|
-
|
|
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
|
-
|
|
287
|
+
nativeMoveNoReplace(binding, [
|
|
282
288
|
sourceNativeParent.handle.fd, sourceNativeParent.basename,
|
|
283
289
|
targetNativeParent.handle.fd, targetNativeParent.basename,
|
|
284
|
-
], sourceExactIdentity,
|
|
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
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
+
}
|