@openclaw/fs-safe 0.15.0 → 0.16.0
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/CHANGELOG.md +38 -0
- package/README.md +28 -6
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-kind.d.ts +0 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +5 -17
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +6 -7
- package/dist/archive-tar-stream.d.ts +3 -0
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +56 -37
- package/dist/archive-tar-wasm.d.ts +16 -4
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +134 -34
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +5 -6
- package/dist/clone-metadata.d.ts +1 -0
- package/dist/clone-metadata.d.ts.map +1 -1
- package/dist/clone-metadata.js +6 -2
- package/dist/create-directory.d.ts +20 -0
- package/dist/create-directory.d.ts.map +1 -0
- package/dist/create-directory.js +130 -0
- package/dist/create-file-async.d.ts +7 -0
- package/dist/create-file-async.d.ts.map +1 -0
- package/dist/create-file-async.js +121 -0
- package/dist/create-file.d.ts +8 -0
- package/dist/create-file.d.ts.map +1 -0
- package/dist/create-file.js +190 -0
- package/dist/create-owned-file.d.ts +8 -0
- package/dist/create-owned-file.d.ts.map +1 -0
- package/dist/create-owned-file.js +16 -0
- package/dist/create.d.ts +4 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +2 -0
- package/dist/creation-darwin.d.ts +7 -0
- package/dist/creation-darwin.d.ts.map +1 -0
- package/dist/creation-darwin.js +79 -0
- package/dist/creation-file-state.d.ts +19 -0
- package/dist/creation-file-state.d.ts.map +1 -0
- package/dist/creation-file-state.js +118 -0
- package/dist/creation-path.d.ts +21 -0
- package/dist/creation-path.d.ts.map +1 -0
- package/dist/creation-path.js +71 -0
- package/dist/creation-permissions.d.ts +19 -0
- package/dist/creation-permissions.d.ts.map +1 -0
- package/dist/creation-permissions.js +125 -0
- package/dist/directory-durability.d.ts +1 -1
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +22 -80
- package/dist/directory-guard.d.ts +3 -0
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-mode-node.d.ts +2 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +8 -0
- package/dist/directory-receipt.d.ts +24 -0
- package/dist/directory-receipt.d.ts.map +1 -0
- package/dist/directory-receipt.js +127 -0
- package/dist/file-cleanup.d.ts +19 -0
- package/dist/file-cleanup.d.ts.map +1 -0
- package/dist/file-cleanup.js +78 -0
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +2 -6
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +3 -9
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +2 -5
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +36 -7
- package/dist/move-path.js +1 -1
- package/dist/native-binding.d.ts +11 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-fallback-warning.d.ts +4 -0
- package/dist/native-fallback-warning.d.ts.map +1 -0
- package/dist/native-fallback-warning.js +11 -0
- package/dist/native-operations.d.ts +0 -2
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +0 -24
- package/dist/native-parent-admission.d.ts +2 -0
- package/dist/native-parent-admission.d.ts.map +1 -1
- package/dist/native-parent-admission.js +3 -2
- package/dist/native-pinned-write-windows.d.ts +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +173 -28
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +19 -3
- package/dist/native-policy-parent-windows.d.ts.map +1 -1
- package/dist/native-policy-parent-windows.js +15 -6
- package/dist/native-staged-file.d.ts +3 -2
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +86 -39
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +10 -4
- package/dist/pinned-write-input.d.ts +4 -0
- package/dist/pinned-write-input.d.ts.map +1 -0
- package/dist/pinned-write-input.js +25 -0
- package/dist/pinned-write-mode.d.ts +5 -0
- package/dist/pinned-write-mode.d.ts.map +1 -0
- package/dist/pinned-write-mode.js +24 -0
- package/dist/pinned-write-staged.d.ts +6 -0
- package/dist/pinned-write-staged.d.ts.map +1 -0
- package/dist/pinned-write-staged.js +187 -0
- package/dist/pinned-write-types.d.ts +3 -0
- package/dist/pinned-write-types.d.ts.map +1 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +35 -145
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +18 -4
- package/dist/private-producer-handoff-sync.d.ts +14 -0
- package/dist/private-producer-handoff-sync.d.ts.map +1 -0
- package/dist/private-producer-handoff-sync.js +114 -0
- package/dist/private-producer-handoff.d.ts +22 -4
- package/dist/private-producer-handoff.d.ts.map +1 -1
- package/dist/private-producer-handoff.js +140 -77
- package/dist/publish-copy-stage.d.ts +2 -1
- package/dist/publish-copy-stage.d.ts.map +1 -1
- package/dist/publish-copy-stage.js +16 -7
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -2
- package/dist/replace-file-temp-owner.d.ts +0 -7
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +3 -54
- package/dist/root-create-input.d.ts +2 -1
- package/dist/root-create-input.d.ts.map +1 -1
- package/dist/root-create-input.js +13 -4
- package/dist/root-directory-creation.d.ts +3 -3
- package/dist/root-directory-creation.d.ts.map +1 -1
- package/dist/root-directory-creation.js +15 -3
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +28 -7
- package/dist/root-move-noreplace.d.ts.map +1 -1
- package/dist/root-move-noreplace.js +22 -13
- package/dist/root-options.d.ts +12 -4
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-stat.d.ts.map +1 -1
- package/dist/root-path-stat.js +59 -7
- package/dist/root-write-publication.js +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +1 -0
- package/dist/secure-file-windows.d.ts +6 -0
- package/dist/secure-file-windows.d.ts.map +1 -1
- package/dist/secure-file-windows.js +34 -117
- package/dist/secure-file.js +2 -2
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +2 -1
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +6 -6
- package/dist/staged-file-settlement.d.ts +17 -0
- package/dist/staged-file-settlement.d.ts.map +1 -0
- package/dist/staged-file-settlement.js +57 -0
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +2 -1
- package/dist/windows-security-bridge.cs +336 -0
- package/dist/windows-security-bridge.ps1 +15 -0
- package/dist/windows-security-command.d.ts +26 -0
- package/dist/windows-security-command.d.ts.map +1 -0
- package/dist/windows-security-command.js +363 -0
- package/dist/windows-security-facts.d.ts +6 -0
- package/dist/windows-security-facts.d.ts.map +1 -0
- package/dist/windows-security-facts.js +108 -0
- package/docs/advanced.md +3 -1
- package/docs/archive.md +61 -37
- package/docs/config.md +6 -2
- package/docs/contributing.md +44 -4
- package/docs/copy.md +2 -0
- package/docs/creation.md +128 -0
- package/docs/durability.md +24 -0
- package/docs/install.md +31 -7
- package/docs/migrating-to-0.5.md +15 -6
- package/docs/migrating-to-0.6.md +9 -4
- package/docs/native-helper.md +22 -9
- package/docs/native.md +38 -7
- package/docs/permissions.md +37 -14
- package/docs/root.md +34 -0
- package/docs/secret-file.md +11 -2
- package/docs/secure-file.md +9 -4
- package/docs/sidecar-lock.md +5 -4
- package/docs/staged-file.md +5 -0
- package/docs/writing.md +71 -5
- package/package.json +18 -15
|
@@ -50,19 +50,22 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
50
50
|
var e = new Error(message);
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
|
+
import { assertDarwinCreationAcl, privateFileMutationAssertion } from "./creation-darwin.js";
|
|
54
|
+
import { assertPrivateCreationFile } from "./creation-file-state.js";
|
|
53
55
|
import { requireNativeBinding } from "./native.js";
|
|
54
56
|
import { syncFileBestEffortSync } from "./file-sync.js";
|
|
55
57
|
import { randomUUID } from "node:crypto";
|
|
56
58
|
import fs from "node:fs";
|
|
57
59
|
import path from "node:path";
|
|
58
60
|
import { FsSafeError } from "./errors.js";
|
|
59
|
-
import { MutationAuthorityError } from "./mutation-authority.js";
|
|
61
|
+
import { assertSynchronousCallbackResult, MutationAuthorityError } from "./mutation-authority.js";
|
|
60
62
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
61
|
-
import {
|
|
63
|
+
import { writePinnedInput } from "./pinned-write-input.js";
|
|
62
64
|
import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
|
|
63
65
|
import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
|
|
64
66
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
65
67
|
import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
|
|
68
|
+
import { createStagedFileReceipt, stagedFileFailure as failure } from "./staged-file-settlement.js";
|
|
66
69
|
export function assertNativeStaging(binding) {
|
|
67
70
|
if ([
|
|
68
71
|
binding.closeOwnedFd,
|
|
@@ -82,12 +85,6 @@ function assertBasename(name, portable) {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
const NOT_PUBLISHED = Object.freeze({ status: "not-published" });
|
|
85
|
-
function failure(error, details) {
|
|
86
|
-
const code = error instanceof FsSafeError
|
|
87
|
-
? error.code
|
|
88
|
-
: error?.code === "EEXIST" ? "already-exists" : "helper-failed";
|
|
89
|
-
return new FsSafeError(code, `staged file ${details.phase} failed`, { cause: error, details });
|
|
90
|
-
}
|
|
91
88
|
class NativeStagedFile {
|
|
92
89
|
#binding;
|
|
93
90
|
#closeFd;
|
|
@@ -97,12 +94,15 @@ class NativeStagedFile {
|
|
|
97
94
|
#portableNames;
|
|
98
95
|
#publishedMode;
|
|
99
96
|
#sync;
|
|
97
|
+
#strictFileSync;
|
|
98
|
+
#private;
|
|
99
|
+
#verifyMode;
|
|
100
100
|
#assertBeforeMutation;
|
|
101
101
|
#name;
|
|
102
102
|
#state = { status: "open", publication: NOT_PUBLISHED };
|
|
103
103
|
#receipt;
|
|
104
104
|
#rejectFinalSymlink = false;
|
|
105
|
-
constructor(binding, parentFd, closeParentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation, name = `.fs-safe-${randomUUID()}.tmp
|
|
105
|
+
constructor(binding, parentFd, closeParentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation, name = `.fs-safe-${randomUUID()}.tmp`, strictFileSync = false, permissionPolicy) {
|
|
106
106
|
assertBasename(name, portableNames);
|
|
107
107
|
this.#name = name;
|
|
108
108
|
this.#closeFd = captureNativeFdClose(binding);
|
|
@@ -113,16 +113,19 @@ class NativeStagedFile {
|
|
|
113
113
|
this.#portableNames = portableNames;
|
|
114
114
|
this.#publishedMode = publishedMode;
|
|
115
115
|
this.#sync = sync;
|
|
116
|
+
this.#strictFileSync = strictFileSync;
|
|
117
|
+
this.#private = permissionPolicy === "private-creation";
|
|
118
|
+
this.#verifyMode = permissionPolicy !== undefined;
|
|
116
119
|
this.#assertBeforeMutation = assertBeforeMutation;
|
|
117
120
|
}
|
|
118
121
|
// Public staging supplies a Node parent; pinned writes supply a native parent.
|
|
119
122
|
// The supplied closer owns parentFd even on construction or preparation failure.
|
|
120
123
|
static async create(binding, parentFd, closeParentFd, directory, input, mode, maxBytes,
|
|
121
124
|
// Public staging uses portable names; existing POSIX writes accept literal names.
|
|
122
|
-
portableNames = true, sync = true, assertBeforeMutation, exclusiveBasename) {
|
|
125
|
+
portableNames = true, sync = true, assertBeforeMutation, exclusiveBasename, strictFileSync = false, permissionPolicy) {
|
|
123
126
|
let staged;
|
|
124
127
|
try {
|
|
125
|
-
staged = new NativeStagedFile(binding, parentFd, closeParentFd, directory, portableNames, mode, sync, assertBeforeMutation, exclusiveBasename);
|
|
128
|
+
staged = new NativeStagedFile(binding, parentFd, closeParentFd, directory, portableNames, mode, sync, assertBeforeMutation, exclusiveBasename, strictFileSync, permissionPolicy);
|
|
126
129
|
}
|
|
127
130
|
catch (error) {
|
|
128
131
|
try {
|
|
@@ -142,19 +145,29 @@ class NativeStagedFile {
|
|
|
142
145
|
const exclusive = params.overwrite === false && params.input.kind === "buffer" && params.input.stageBeforePublish === false;
|
|
143
146
|
// This owner never escapes. Only the internal verifier borrows its fd;
|
|
144
147
|
// public descriptor methods remain await-free and cannot race disposal.
|
|
145
|
-
const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, closeParentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation, exclusive ? params.basename : undefined), true);
|
|
148
|
+
const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, closeParentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation, exclusive ? params.basename : undefined, params.strictFileSync, params.private ? "private-creation" : params.verifyPosixMode === true ? "mode-only" : undefined), true);
|
|
146
149
|
staged.#rejectFinalSymlink = params.rejectFinalSymlink === true;
|
|
147
150
|
if (params.input.kind === "file")
|
|
148
151
|
await params.input.verifySource();
|
|
149
152
|
if (exclusive) {
|
|
150
153
|
staged.#assertCurrent();
|
|
151
154
|
params.assertBeforeMutation?.();
|
|
155
|
+
if (staged.#verifyMode)
|
|
156
|
+
staged.#assertCurrent();
|
|
152
157
|
}
|
|
153
158
|
const published = exclusive
|
|
154
159
|
? staged.#completePublication(params.basename, false, params.onPublished)
|
|
155
160
|
: await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
|
|
156
161
|
const identity = published.staged.identity;
|
|
157
|
-
|
|
162
|
+
try {
|
|
163
|
+
await params.verifyPublished?.(staged.#file(), identity, parentGuard);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (params.overwrite === false && params.input.kind !== "file" && params.input.stageBeforePublish === true) {
|
|
167
|
+
throw failure(error, { phase: "publish", publication: published });
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
158
171
|
return { dev: identity.dev, ino: identity.ino };
|
|
159
172
|
}
|
|
160
173
|
catch (e_1) {
|
|
@@ -186,13 +199,14 @@ class NativeStagedFile {
|
|
|
186
199
|
}
|
|
187
200
|
return state.fileFd;
|
|
188
201
|
}
|
|
189
|
-
#assertNamed(name) {
|
|
202
|
+
#assertNamed(name, expectedMode) {
|
|
190
203
|
const fd = this.#file();
|
|
191
204
|
const stat = this.#binding.stagedFileMatches(this.#parentFd, name, fd)
|
|
192
205
|
? fs.fstatSync(fd, { bigint: true }) : undefined;
|
|
193
206
|
if (!stat || stat.nlink !== 1n) {
|
|
194
207
|
throw new FsSafeError("path-mismatch", "staged entry no longer names the exclusive created file");
|
|
195
208
|
}
|
|
209
|
+
this.#assertPermissions(fd, stat, expectedMode);
|
|
196
210
|
return Number(stat.mode & 4095n);
|
|
197
211
|
}
|
|
198
212
|
#assertCurrent() {
|
|
@@ -200,7 +214,17 @@ class NativeStagedFile {
|
|
|
200
214
|
throw new FsSafeError("helper-failed", "staged file publication has already been attempted");
|
|
201
215
|
}
|
|
202
216
|
assertStagedDirectoryCurrent(this.#directory);
|
|
203
|
-
this.#assertNamed(this.#name);
|
|
217
|
+
this.#assertNamed(this.#name, 0o600);
|
|
218
|
+
}
|
|
219
|
+
#assertPermissions(fd, stat, expectedMode) {
|
|
220
|
+
if (this.#private)
|
|
221
|
+
assertPrivateCreationFile(stat, fd);
|
|
222
|
+
if (this.#verifyMode && Number(stat.mode & 4095n) !== expectedMode) {
|
|
223
|
+
throw new FsSafeError("insecure-permissions", "filesystem did not enforce the staged file mode");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
#assertStagePermissions(fd) {
|
|
227
|
+
this.#assertPermissions(fd, fs.fstatSync(fd, { bigint: true }), 0o600);
|
|
204
228
|
}
|
|
205
229
|
async #prepare(input, maxBytes) {
|
|
206
230
|
try {
|
|
@@ -208,44 +232,60 @@ class NativeStagedFile {
|
|
|
208
232
|
if (!Number.isInteger(mode) || mode < 0 || mode > 0o7777) {
|
|
209
233
|
throw new FsSafeError("invalid-path", "invalid staged file mode");
|
|
210
234
|
}
|
|
235
|
+
const fileSignal = input.kind === "file" && this.#verifyMode ? input.signal : undefined;
|
|
236
|
+
if (input.kind === "file" && this.#verifyMode) {
|
|
237
|
+
fileSignal?.throwIfAborted();
|
|
238
|
+
if (input.clone === "always") {
|
|
239
|
+
throw new FsSafeError("helper-unavailable", "required cloning cannot verify staged permissions before copying");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
211
242
|
assertStagedDirectoryCurrent(this.#directory);
|
|
212
243
|
// The exclusive open performs no fallible post-open checks. Store its fd
|
|
213
244
|
// before every subsequent operation, including the first metadata read.
|
|
214
245
|
const state = this.#open();
|
|
246
|
+
if (this.#private)
|
|
247
|
+
assertDarwinCreationAcl(this.#parentFd, "file");
|
|
215
248
|
this.#assertBeforeMutation?.();
|
|
216
|
-
|
|
249
|
+
if (this.#private)
|
|
250
|
+
assertDarwinCreationAcl(this.#parentFd, "file");
|
|
251
|
+
// Native copying populates its new file before returning its descriptor.
|
|
252
|
+
const copied = input.kind === "file" && !this.#verifyMode
|
|
217
253
|
? await createNativeCopyFile(this.#binding, input, this.#parentFd, this.#name, maxBytes, false)
|
|
218
254
|
: undefined;
|
|
219
255
|
state.fileFd = copied?.fd;
|
|
220
256
|
if (state.fileFd === undefined) {
|
|
221
257
|
this.#assertBeforeMutation?.();
|
|
258
|
+
if (this.#private)
|
|
259
|
+
assertDarwinCreationAcl(this.#parentFd, "file");
|
|
222
260
|
state.fileFd = this.#binding.createStagedFile(this.#parentFd, this.#name);
|
|
223
261
|
}
|
|
224
262
|
const fd = state.fileFd;
|
|
225
263
|
if (input.kind === "file")
|
|
226
264
|
assertNativeCopyCompleted(input, copied);
|
|
227
|
-
this.#
|
|
265
|
+
const assertBeforeChmod = this.#private
|
|
266
|
+
? privateFileMutationAssertion(fd, this.#assertBeforeMutation) : this.#assertBeforeMutation;
|
|
267
|
+
assertBeforeChmod?.();
|
|
228
268
|
fs.fchmodSync(fd, 0o600);
|
|
269
|
+
// Some filesystems report successful chmod without enforcing its mode.
|
|
270
|
+
if (this.#verifyMode)
|
|
271
|
+
this.#assertStagePermissions(fd);
|
|
272
|
+
const assertBeforeMutation = this.#verifyMode ? () => {
|
|
273
|
+
assertSynchronousCallbackResult(this.#assertBeforeMutation?.(), "assertBeforeMutation");
|
|
274
|
+
fileSignal?.throwIfAborted();
|
|
275
|
+
this.#assertStagePermissions(fd);
|
|
276
|
+
} : this.#assertBeforeMutation;
|
|
229
277
|
if (!copied)
|
|
230
|
-
await
|
|
231
|
-
if (this.#
|
|
232
|
-
|
|
278
|
+
await writePinnedInput(fd, input, maxBytes, assertBeforeMutation);
|
|
279
|
+
if (this.#verifyMode)
|
|
280
|
+
this.#assertStagePermissions(fd);
|
|
281
|
+
if (this.#sync) {
|
|
282
|
+
if (this.#strictFileSync)
|
|
283
|
+
fs.fsyncSync(fd);
|
|
284
|
+
else
|
|
285
|
+
syncFileBestEffortSync(fd);
|
|
286
|
+
}
|
|
233
287
|
const stat = fs.fstatSync(fd, { bigint: true });
|
|
234
|
-
this.#receipt =
|
|
235
|
-
directory: this.#directory,
|
|
236
|
-
temporaryBasename: this.#name,
|
|
237
|
-
identity: Object.freeze({
|
|
238
|
-
dev: stat.dev,
|
|
239
|
-
ino: stat.ino,
|
|
240
|
-
mode: Number(stat.mode & 4095n),
|
|
241
|
-
nlink: stat.nlink,
|
|
242
|
-
size: stat.size,
|
|
243
|
-
uid: Number(stat.uid),
|
|
244
|
-
gid: Number(stat.gid),
|
|
245
|
-
mtimeNs: stat.mtimeNs,
|
|
246
|
-
ctimeNs: stat.ctimeNs,
|
|
247
|
-
}),
|
|
248
|
-
});
|
|
288
|
+
this.#receipt = createStagedFileReceipt(this.#directory, this.#name, stat);
|
|
249
289
|
this.#assertCurrent();
|
|
250
290
|
}
|
|
251
291
|
catch (error) {
|
|
@@ -275,6 +315,10 @@ class NativeStagedFile {
|
|
|
275
315
|
this.#assertCurrent();
|
|
276
316
|
assertFinalSymlinkRejected(path.join(this.#directory.realPath, basename), this.#rejectFinalSymlink);
|
|
277
317
|
this.#assertBeforeMutation?.();
|
|
318
|
+
if (this.#assertBeforeMutation) {
|
|
319
|
+
this.#assertCurrent();
|
|
320
|
+
assertFinalSymlinkRejected(path.join(this.#directory.realPath, basename), this.#rejectFinalSymlink);
|
|
321
|
+
}
|
|
278
322
|
try {
|
|
279
323
|
if (overwrite) {
|
|
280
324
|
this.#binding.renameReplace(this.#parentFd, this.#name, this.#parentFd, basename);
|
|
@@ -307,7 +351,7 @@ class NativeStagedFile {
|
|
|
307
351
|
const receipt = Object.freeze({ status: "published", staged: this.receipt, basename, overwrite });
|
|
308
352
|
this.#open().publication = receipt;
|
|
309
353
|
onPublished?.(receipt.staged.identity);
|
|
310
|
-
const stagedMode = this.#assertNamed(basename);
|
|
354
|
+
const stagedMode = this.#assertNamed(basename, 0o600);
|
|
311
355
|
assertStagedDirectoryCurrent(this.#directory);
|
|
312
356
|
// Keep contents private until the name passes its identity fence. Mode
|
|
313
357
|
// changes use the owned fd, including for final mode 000.
|
|
@@ -317,11 +361,14 @@ class NativeStagedFile {
|
|
|
317
361
|
// Content was synced during preparation. A lost chmod leaves 0600, no wider
|
|
318
362
|
// than modes retaining owner rw; restrictive modes still need a durable correction.
|
|
319
363
|
if (this.#sync && ((this.#publishedMode & 0o600) !== 0o600 || (stagedMode & ~this.#publishedMode) !== 0)) {
|
|
320
|
-
|
|
364
|
+
if (this.#strictFileSync)
|
|
365
|
+
fs.fsyncSync(fd);
|
|
366
|
+
else
|
|
367
|
+
syncFileBestEffortSync(fd);
|
|
321
368
|
}
|
|
322
369
|
if (this.#sync)
|
|
323
370
|
syncFileBestEffortSync(this.#parentFd);
|
|
324
|
-
this.#assertNamed(basename);
|
|
371
|
+
this.#assertNamed(basename, this.#publishedMode);
|
|
325
372
|
assertStagedDirectoryCurrent(this.#directory);
|
|
326
373
|
return receipt;
|
|
327
374
|
}
|
|
@@ -397,5 +444,5 @@ export async function stageFileInDirectory(options) {
|
|
|
397
444
|
const input = { kind: "buffer", data: Buffer.from(options.content) };
|
|
398
445
|
const mode = options.mode ?? 0o600;
|
|
399
446
|
const parent = openStagedDirectory(options.directory);
|
|
400
|
-
return await createNativeStage(binding, parent.fd, fs.closeSync, parent.receipt, input, mode);
|
|
447
|
+
return await createNativeStage(binding, parent.fd, fs.closeSync, parent.receipt, input, mode, undefined, true, true, undefined, undefined, false, "mode-only");
|
|
401
448
|
}
|
package/dist/owner-dacl.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"owner-dacl.d.ts","sourceRoot":"","sources":["../src/owner-dacl.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"owner-dacl.d.ts","sourceRoot":"","sources":["../src/owner-dacl.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,KAAK,EAAE,eAAe,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC1B;IACE,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,IAAI,EAAE,yBAAyB,EAAE,CAAC;CACnC,GACD;IACE,MAAM,EAAE,sBAAsB,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC3B,CAAC;AAEN,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,kBAAkB,CAmCvE"}
|
package/dist/owner-dacl.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
2
|
import { getNativeBinding } from "./native.js";
|
|
3
|
+
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
4
|
+
import { warnNativeFallback } from "./native-fallback-warning.js";
|
|
3
5
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
6
|
+
import { readWindowsSecurityFactsCommand } from "./windows-security-command.js";
|
|
4
7
|
export function readOwnerAndDacl(targetPath) {
|
|
5
8
|
if (process.platform !== "win32") {
|
|
6
9
|
return { status: "unsupported-platform", platform: process.platform };
|
|
7
10
|
}
|
|
8
11
|
assertNoWindowsPathAlias(targetPath, "filesystem", "owner and DACL path uses a Windows filesystem namespace alias");
|
|
9
12
|
const native = getNativeBinding();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
const inspect = native?.readOwnerAndDacl;
|
|
14
|
+
if (typeof inspect !== "function") {
|
|
15
|
+
if (getFsSafeNativeConfig().mode === "require") {
|
|
16
|
+
throw new FsSafeError("helper-unavailable", "Windows owner and DACL facts require an up-to-date native helper");
|
|
17
|
+
}
|
|
18
|
+
warnNativeFallback("windows-owner-dacl", "Windows owner and DACL inspection uses a slower built-in system command.");
|
|
13
19
|
}
|
|
14
|
-
const facts =
|
|
20
|
+
const facts = typeof inspect === "function" ? inspect.call(native, targetPath) : readWindowsSecurityFactsCommand(targetPath);
|
|
15
21
|
return {
|
|
16
22
|
status: "supported",
|
|
17
23
|
ownerSid: facts.ownerSid,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
|
+
import type { PinnedWriteInput } from "./pinned-write-types.js";
|
|
3
|
+
export declare function writePinnedInput(target: FileHandle | number, input: PinnedWriteInput, maxBytes?: number, assertBeforeMutation?: () => void): Promise<void>;
|
|
4
|
+
//# sourceMappingURL=pinned-write-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-write-input.d.ts","sourceRoot":"","sources":["../src/pinned-write-input.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,UAAU,GAAG,MAAM,EAC3B,KAAK,EAAE,gBAAgB,EACvB,QAAQ,CAAC,EAAE,MAAM,EACjB,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { writeCopyFileToFd } from "./copy-file-input.js";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { writeAllToFile } from "./write-file-handle.js";
|
|
4
|
+
export async function writePinnedInput(target, input, maxBytes, assertBeforeMutation) {
|
|
5
|
+
if (input.kind === "file") {
|
|
6
|
+
await writeCopyFileToFd(typeof target === "number" ? target : target.fd, input, maxBytes, assertBeforeMutation);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
let bytes = 0;
|
|
10
|
+
const write = async (data) => {
|
|
11
|
+
bytes += data.byteLength;
|
|
12
|
+
if (maxBytes !== undefined && bytes > maxBytes) {
|
|
13
|
+
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
14
|
+
}
|
|
15
|
+
await writeAllToFile(target, data, { assertBeforeMutation });
|
|
16
|
+
};
|
|
17
|
+
if (input.kind === "buffer") {
|
|
18
|
+
await write(typeof input.data === "string" ? Buffer.from(input.data, input.encoding ?? "utf8") : input.data);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
for await (const chunk of input.stream) {
|
|
22
|
+
await write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
|
+
export declare function assertPinnedWriteMode(fd: number, mode: number): void;
|
|
3
|
+
export declare function preparePinnedWriteMode(handle: FileHandle, createdMode: number, assertBeforeMutation?: () => void): Promise<(() => void) | undefined>;
|
|
4
|
+
export declare function pinnedWriteModeAssertion(fd: number, mode: number, assertBeforeMutation?: () => void): () => void;
|
|
5
|
+
//# sourceMappingURL=pinned-write-mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-write-mode.d.ts","sourceRoot":"","sources":["../src/pinned-write-mode.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAKpE;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,MAAM,EACnB,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC,CAKnC;AAED,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,MAAM,IAAI,CAKZ"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
4
|
+
export function assertPinnedWriteMode(fd, mode) {
|
|
5
|
+
if (process.platform === "win32")
|
|
6
|
+
return;
|
|
7
|
+
if (Number(fs.fstatSync(fd, { bigint: true }).mode & 4095n) !== mode) {
|
|
8
|
+
throw new FsSafeError("insecure-permissions", "filesystem did not enforce the secret file mode");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export async function preparePinnedWriteMode(handle, createdMode, assertBeforeMutation) {
|
|
12
|
+
if (process.platform === "win32")
|
|
13
|
+
return assertBeforeMutation;
|
|
14
|
+
assertSynchronousCallbackResult(assertBeforeMutation?.(), "assertBeforeMutation");
|
|
15
|
+
if (createdMode !== 0o600)
|
|
16
|
+
await handle.chmod(0o600);
|
|
17
|
+
return pinnedWriteModeAssertion(handle.fd, 0o600, assertBeforeMutation);
|
|
18
|
+
}
|
|
19
|
+
export function pinnedWriteModeAssertion(fd, mode, assertBeforeMutation) {
|
|
20
|
+
return () => {
|
|
21
|
+
assertSynchronousCallbackResult(assertBeforeMutation?.(), "assertBeforeMutation");
|
|
22
|
+
assertPinnedWriteMode(fd, mode);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
import type { AsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
|
+
import { type FileIdentityStat } from "./file-identity.js";
|
|
4
|
+
import type { PinnedWriteParams } from "./pinned-write-types.js";
|
|
5
|
+
export declare function runPinnedStagedWrite(params: PinnedWriteParams, parentPath: string, parentGuard: AsyncDirectoryGuard<BigIntStats>): Promise<FileIdentityStat>;
|
|
6
|
+
//# sourceMappingURL=pinned-write-staged.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pinned-write-staged.d.ts","sourceRoot":"","sources":["../src/pinned-write-staged.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAMnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGhE,OAAO,EAA+B,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAMxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AASjE,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,iBAAiB,EACzB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,mBAAmB,CAAC,WAAW,CAAC,GAC5C,OAAO,CAAC,gBAAgB,CAAC,CAqJ3B"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fsSync, {} from "node:fs";
|
|
3
|
+
import fs, {} from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { createFileHandle } from "./create.js";
|
|
6
|
+
import { hasPreservedCreationArtifacts } from "./creation-file-state.js";
|
|
7
|
+
import { creationAdmissionFromParent } from "./creation-path.js";
|
|
8
|
+
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
9
|
+
import { FsSafeError } from "./errors.js";
|
|
10
|
+
import { sameFileIdentity, sha256Hex } from "./file-identity.js";
|
|
11
|
+
import { syncFileBestEffort } from "./file-sync.js";
|
|
12
|
+
import { assertDarwinCreationAcl, privateFileMutationAssertion } from "./creation-darwin.js";
|
|
13
|
+
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
14
|
+
import { writePinnedInput } from "./pinned-write-input.js";
|
|
15
|
+
import { assertPinnedWriteMode, pinnedWriteModeAssertion, preparePinnedWriteMode } from "./pinned-write-mode.js";
|
|
16
|
+
import { publishCopyStage } from "./publish-copy-stage.js";
|
|
17
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
18
|
+
import { cleanupPinnedFilePath } from "./file-cleanup.js";
|
|
19
|
+
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
20
|
+
import { createStagedFileReceipt, settleStagedFile } from "./staged-file-settlement.js";
|
|
21
|
+
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
22
|
+
export async function runPinnedStagedWrite(params, parentPath, parentGuard) {
|
|
23
|
+
const verifyPosixMode = params.verifyPosixMode === true && process.platform !== "win32";
|
|
24
|
+
const targetPath = path.join(parentPath, params.basename);
|
|
25
|
+
// Private staging must not consume the destination basename's filename budget.
|
|
26
|
+
const tempPath = path.join(parentPath, `.fs-safe-${randomUUID()}.tmp`);
|
|
27
|
+
const tempFlags = fsSync.constants.O_WRONLY |
|
|
28
|
+
fsSync.constants.O_CREAT |
|
|
29
|
+
fsSync.constants.O_EXCL |
|
|
30
|
+
(process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
|
|
31
|
+
? fsSync.constants.O_NOFOLLOW
|
|
32
|
+
: 0);
|
|
33
|
+
let handle;
|
|
34
|
+
let tempStat;
|
|
35
|
+
let tempIdentity;
|
|
36
|
+
let readHandle;
|
|
37
|
+
let renamed = false;
|
|
38
|
+
const completeCreate = params.overwrite === false && params.input.kind !== "file" && params.input.stageBeforePublish === true;
|
|
39
|
+
let receipt;
|
|
40
|
+
let publication = Object.freeze({ status: "not-published" });
|
|
41
|
+
let phase = "prepare";
|
|
42
|
+
let failure;
|
|
43
|
+
try {
|
|
44
|
+
params.assertBeforeMutation?.();
|
|
45
|
+
handle = params.private
|
|
46
|
+
? await createFileHandle(tempPath, {
|
|
47
|
+
private: true, mode: 0o600, assertBeforeMutation: params.assertBeforeMutation,
|
|
48
|
+
}, creationAdmissionFromParent(parentGuard))
|
|
49
|
+
: await fs.open(tempPath, tempFlags, verifyPosixMode ? 0o600 : params.mode);
|
|
50
|
+
let verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
51
|
+
tempIdentity = verificationIdentity;
|
|
52
|
+
let assertBeforeMutation = params.private
|
|
53
|
+
? privateFileMutationAssertion(handle.fd, params.assertBeforeMutation) : params.assertBeforeMutation;
|
|
54
|
+
const assertBeforeWrite = verifyPosixMode
|
|
55
|
+
? await preparePinnedWriteMode(handle, Number(tempIdentity.mode & 4095n), assertBeforeMutation)
|
|
56
|
+
: assertBeforeMutation;
|
|
57
|
+
if (verifyPosixMode)
|
|
58
|
+
assertPinnedWriteMode(handle.fd, 0o600);
|
|
59
|
+
await writePinnedInput(handle, params.input, params.maxBytes, assertBeforeWrite);
|
|
60
|
+
if (verifyPosixMode)
|
|
61
|
+
assertPinnedWriteMode(handle.fd, 0o600);
|
|
62
|
+
if (params.private)
|
|
63
|
+
assertDarwinCreationAcl(handle.fd);
|
|
64
|
+
tempStat = fsSync.fstatSync(handle.fd);
|
|
65
|
+
const tempPathStat = fsSync.lstatSync(tempPath);
|
|
66
|
+
if (tempPathStat.isSymbolicLink() || !sameFileIdentity(tempPathStat, tempStat)) {
|
|
67
|
+
throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
|
|
68
|
+
}
|
|
69
|
+
const expectedTempStat = tempStat;
|
|
70
|
+
await handle.chmod(params.mode);
|
|
71
|
+
if (verifyPosixMode) {
|
|
72
|
+
assertPinnedWriteMode(handle.fd, params.mode);
|
|
73
|
+
assertBeforeMutation = pinnedWriteModeAssertion(handle.fd, params.mode, assertBeforeMutation);
|
|
74
|
+
}
|
|
75
|
+
if (params.sync !== false) {
|
|
76
|
+
if (params.strictFileSync)
|
|
77
|
+
await handle.sync();
|
|
78
|
+
else
|
|
79
|
+
await syncFileBestEffort(handle);
|
|
80
|
+
}
|
|
81
|
+
if (params.input.kind === "file")
|
|
82
|
+
await params.input.verifySource();
|
|
83
|
+
if (completeCreate) {
|
|
84
|
+
receipt = createStagedFileReceipt(Object.freeze({
|
|
85
|
+
path: parentGuard.dir,
|
|
86
|
+
realPath: parentGuard.realPath,
|
|
87
|
+
identity: Object.freeze({ dev: parentGuard.stat.dev, ino: parentGuard.stat.ino }),
|
|
88
|
+
}), path.basename(tempPath), fsSync.fstatSync(handle.fd, { bigint: true }));
|
|
89
|
+
}
|
|
90
|
+
let verifiedIdentity = expectedTempStat;
|
|
91
|
+
phase = "publish";
|
|
92
|
+
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
93
|
+
assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
|
|
94
|
+
if (params.overwrite === false) {
|
|
95
|
+
publishCopyStage({
|
|
96
|
+
temporaryPath: tempPath, targetPath, fd: handle.fd,
|
|
97
|
+
identity: tempIdentity, parentGuard,
|
|
98
|
+
assertBeforeMutation,
|
|
99
|
+
onPublicationAttempt: () => {
|
|
100
|
+
if (receipt)
|
|
101
|
+
publication = Object.freeze({ status: "indeterminate", basename: params.basename, overwrite: false });
|
|
102
|
+
},
|
|
103
|
+
onPublished: (identity) => {
|
|
104
|
+
renamed = true;
|
|
105
|
+
if (receipt)
|
|
106
|
+
publication = Object.freeze({ status: "published", staged: receipt, basename: params.basename, overwrite: false });
|
|
107
|
+
params.onPublished?.(identity);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
assertBeforeMutation?.();
|
|
113
|
+
await fs.rename(tempPath, targetPath);
|
|
114
|
+
renamed = true;
|
|
115
|
+
params.onPublished?.(verificationIdentity);
|
|
116
|
+
}
|
|
117
|
+
await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
|
|
118
|
+
if (params.sync !== false)
|
|
119
|
+
await syncDirectoryBestEffort(parentPath);
|
|
120
|
+
const targetStat = fsSync.lstatSync(targetPath);
|
|
121
|
+
if (targetStat.isSymbolicLink()) {
|
|
122
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
123
|
+
}
|
|
124
|
+
if (!sameFileIdentity(targetStat, expectedTempStat)) {
|
|
125
|
+
// On filesystems like rclone FUSE, rename(2) can give the destination a
|
|
126
|
+
// different inode from the source temp fd even with zero concurrency. The
|
|
127
|
+
// caller must ensure mutual exclusion before passing "verify-content";
|
|
128
|
+
// fall back to a content hash for this rename-boundary check only.
|
|
129
|
+
if (params.onRenameIdentityMismatch !== "verify-content") {
|
|
130
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
131
|
+
}
|
|
132
|
+
if (params.input.kind !== "buffer") {
|
|
133
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
134
|
+
}
|
|
135
|
+
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
136
|
+
readHandle = await fs.open(targetPath, resolveReadOpenFlags());
|
|
137
|
+
const readHandleStat = fsSync.fstatSync(readHandle.fd, { bigint: true });
|
|
138
|
+
const actualHash = sha256Hex(await readHandle.readFile());
|
|
139
|
+
if (actualHash !== expectedHash) {
|
|
140
|
+
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
141
|
+
}
|
|
142
|
+
// The content-verified destination, not the old temp inode, is now pinned.
|
|
143
|
+
verificationIdentity = readHandleStat;
|
|
144
|
+
// Preserve the helper's legacy numeric return facts, not the private proof.
|
|
145
|
+
verifiedIdentity = { dev: Number(readHandleStat.dev), ino: Number(readHandleStat.ino) };
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
await params.verifyPublished?.((readHandle ?? handle).fd, verificationIdentity, parentGuard);
|
|
149
|
+
return { dev: verifiedIdentity.dev, ino: verifiedIdentity.ino };
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
failure = { error };
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
const preservedPreparation = params.private && !handle && hasPreservedCreationArtifacts(failure?.error);
|
|
157
|
+
if (completeCreate || preservedPreparation) {
|
|
158
|
+
await settleStagedFile({
|
|
159
|
+
temporaryBasename: path.basename(tempPath), publication, phase, failure,
|
|
160
|
+
cleanup: async () => {
|
|
161
|
+
if (preservedPreparation)
|
|
162
|
+
return "preserved";
|
|
163
|
+
if (publication.status === "indeterminate")
|
|
164
|
+
return "preserved";
|
|
165
|
+
if (renamed) {
|
|
166
|
+
return failure?.error instanceof FsSafeError && failure.error.details?.cleanup === "failed"
|
|
167
|
+
? "failed" : "not-needed";
|
|
168
|
+
}
|
|
169
|
+
return handle ? await cleanupPinnedFilePath({
|
|
170
|
+
pathname: tempPath, handle, identity: tempIdentity, parentGuard, throwOnCleanupError: true,
|
|
171
|
+
}) : "not-needed";
|
|
172
|
+
},
|
|
173
|
+
close: [async () => { await readHandle?.close(); }, async () => { await handle?.close(); }],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
try {
|
|
178
|
+
if (!renamed && handle) {
|
|
179
|
+
await cleanupPinnedFilePath({ pathname: tempPath, handle, identity: tempIdentity, parentGuard });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await readHandle?.close().catch(() => undefined);
|
|
184
|
+
await handle?.close().catch(() => undefined);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -62,7 +62,10 @@ export type PinnedWriteParams = {
|
|
|
62
62
|
basename: string;
|
|
63
63
|
mkdir: boolean;
|
|
64
64
|
mode: number;
|
|
65
|
+
private?: boolean;
|
|
66
|
+
verifyPosixMode?: boolean;
|
|
65
67
|
sync?: boolean;
|
|
68
|
+
strictFileSync?: boolean;
|
|
66
69
|
overwrite?: boolean;
|
|
67
70
|
assertBeforeMutation?: () => void;
|
|
68
71
|
rejectFinalSymlink?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write-types.d.ts","sourceRoot":"","sources":["../src/pinned-write-types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pinned-write-types.d.ts","sourceRoot":"","sources":["../src/pinned-write-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAgB3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACvF,YAAY,EACV,gBAAgB,EAAE,oBAAoB,EAAE,sBAAsB,EAC9D,8BAA8B,EAAE,gCAAgC,EAChE,6BAA6B,EAAE,+BAA+B,EAC9D,4BAA4B,EAAE,iBAAiB,GAChD,MAAM,yBAAyB,CAAC;AAkDjC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsD/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CAkB3B;AAED,wBAAsB,iCAAiC,CAAC,CAAC,EACvD,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,EAC5E,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACpB,OAAO,CAAC,CAAC,CAAC,CAiBZ"}
|