@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
package/dist/pinned-write.js
CHANGED
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
import { syncFileBestEffort } from "./file-sync.js";
|
|
2
|
-
import {
|
|
2
|
+
import { assertDarwinCreationAcl, privateFileMutationAssertion } from "./creation-darwin.js";
|
|
3
3
|
import fsSync, {} from "node:fs";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { createFileHandle } from "./create.js";
|
|
7
|
+
import { creationAdmissionFromParent } from "./creation-path.js";
|
|
6
8
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
7
9
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
8
10
|
import { FsSafeError } from "./errors.js";
|
|
9
11
|
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
10
|
-
import {
|
|
12
|
+
import { sha256Hex } from "./file-identity.js";
|
|
11
13
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
12
14
|
import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
13
15
|
import { runPinnedWriteNative } from "./native-pinned-write.js";
|
|
14
16
|
import { getNativeBinding } from "./native.js";
|
|
15
17
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
16
|
-
import {
|
|
17
|
-
import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
|
|
18
|
+
import { cleanupPinnedFilePath } from "./file-cleanup.js";
|
|
18
19
|
import { withSidecarLock } from "./sidecar-lock.js";
|
|
19
20
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
20
|
-
import {
|
|
21
|
+
import { writePinnedInput } from "./pinned-write-input.js";
|
|
22
|
+
import { assertPinnedWriteMode, preparePinnedWriteMode } from "./pinned-write-mode.js";
|
|
23
|
+
import { runPinnedStagedWrite } from "./pinned-write-staged.js";
|
|
21
24
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
22
|
-
import { writeCopyFileToFd } from "./copy-file-input.js";
|
|
23
|
-
import { publishCopyStage } from "./publish-copy-stage.js";
|
|
24
25
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
25
|
-
function byteLength(input, encoding) {
|
|
26
|
-
return typeof input === "string"
|
|
27
|
-
? Buffer.byteLength(input, encoding ?? "utf8")
|
|
28
|
-
: input.byteLength;
|
|
29
|
-
}
|
|
30
26
|
function assertSafeBasename(basename) {
|
|
31
27
|
if (!basename ||
|
|
32
28
|
basename === "." ||
|
|
@@ -37,24 +33,10 @@ function assertSafeBasename(basename) {
|
|
|
37
33
|
throw new FsSafeError("invalid-path", "invalid target path");
|
|
38
34
|
}
|
|
39
35
|
}
|
|
40
|
-
function assertWithinMaxBytes(bytes, maxBytes) {
|
|
41
|
-
if (maxBytes !== undefined && bytes > maxBytes) {
|
|
42
|
-
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
36
|
function fastParentGuardDeopt(error) {
|
|
46
37
|
return error instanceof FsSafeError &&
|
|
47
38
|
(error.code === "path-mismatch" || error.code === "not-file");
|
|
48
39
|
}
|
|
49
|
-
async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutation) {
|
|
50
|
-
let bytes = 0;
|
|
51
|
-
for await (const chunk of stream) {
|
|
52
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
53
|
-
bytes += buffer.byteLength;
|
|
54
|
-
assertWithinMaxBytes(bytes, maxBytes);
|
|
55
|
-
await writeAllToFile(handle, buffer, { assertBeforeMutation });
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
40
|
const PINNED_WRITE_SNAPSHOT_KEYS = [
|
|
59
41
|
"rootPath", "relativeParentPath", "basename", "maxBytes", "rootIdentity",
|
|
60
42
|
];
|
|
@@ -142,6 +124,7 @@ export async function withPinnedWriteRenameIdentityLock(params, run) {
|
|
|
142
124
|
}, run);
|
|
143
125
|
}
|
|
144
126
|
async function runPinnedWriteFallback(params) {
|
|
127
|
+
const verifyPosixMode = params.verifyPosixMode === true && process.platform !== "win32";
|
|
145
128
|
const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
|
|
146
129
|
? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
|
|
147
130
|
const mutationAdmission = params.mutationAdmission;
|
|
@@ -246,15 +229,19 @@ async function runPinnedWriteFallback(params) {
|
|
|
246
229
|
}));
|
|
247
230
|
await assertAsyncDirectoryGuard(parentGuard);
|
|
248
231
|
}
|
|
249
|
-
if (params.overwrite === false &&
|
|
250
|
-
(params.input.kind === "stream" && !params.input.stageBeforePublish))) {
|
|
232
|
+
if (params.overwrite === false && params.input.kind !== "file" && !params.input.stageBeforePublish) {
|
|
251
233
|
const assertBeforeMutation = () => {
|
|
252
234
|
assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
|
|
253
235
|
params.assertBeforeMutation?.();
|
|
254
236
|
};
|
|
255
237
|
const handle = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
256
238
|
assertBeforeMutation();
|
|
257
|
-
|
|
239
|
+
if (params.private) {
|
|
240
|
+
return await createFileHandle(targetPath, {
|
|
241
|
+
private: true, mode: 0o600, assertBeforeMutation,
|
|
242
|
+
}, creationAdmissionFromParent(parentGuard));
|
|
243
|
+
}
|
|
244
|
+
return await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, verifyPosixMode ? 0o600 : params.mode);
|
|
258
245
|
}, {
|
|
259
246
|
onPostGuardFailure: async (openedHandle) => {
|
|
260
247
|
// The parent failed verification, so targetPath may now resolve
|
|
@@ -267,19 +254,27 @@ async function runPinnedWriteFallback(params) {
|
|
|
267
254
|
try {
|
|
268
255
|
const verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
269
256
|
createdIdentity = verificationIdentity;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
await writeStreamToHandle(params.input.stream, handle, params.maxBytes, assertBeforeMutation);
|
|
257
|
+
let assertBeforeWrite = params.private
|
|
258
|
+
? privateFileMutationAssertion(handle.fd, assertBeforeMutation) : assertBeforeMutation;
|
|
259
|
+
if (verifyPosixMode) {
|
|
260
|
+
assertBeforeWrite = await preparePinnedWriteMode(handle, Number(createdIdentity.mode & 4095n), assertBeforeWrite);
|
|
261
|
+
assertPinnedWriteMode(handle.fd, 0o600);
|
|
278
262
|
}
|
|
263
|
+
await writePinnedInput(handle, params.input, params.maxBytes, assertBeforeWrite);
|
|
264
|
+
if (verifyPosixMode)
|
|
265
|
+
assertPinnedWriteMode(handle.fd, 0o600);
|
|
266
|
+
if (params.private)
|
|
267
|
+
assertDarwinCreationAcl(handle.fd);
|
|
279
268
|
// Content writes may clear set-ID bits; finalize them through the owned fd.
|
|
280
269
|
await handle.chmod(params.mode);
|
|
281
|
-
if (
|
|
282
|
-
|
|
270
|
+
if (verifyPosixMode)
|
|
271
|
+
assertPinnedWriteMode(handle.fd, params.mode);
|
|
272
|
+
if (params.sync !== false) {
|
|
273
|
+
if (params.strictFileSync)
|
|
274
|
+
await handle.sync();
|
|
275
|
+
else
|
|
276
|
+
await syncFileBestEffort(handle);
|
|
277
|
+
}
|
|
283
278
|
const stat = fsSync.fstatSync(handle.fd);
|
|
284
279
|
if (params.sync !== false)
|
|
285
280
|
await syncDirectoryBestEffort(parentPath);
|
|
@@ -299,110 +294,5 @@ async function runPinnedWriteFallback(params) {
|
|
|
299
294
|
}
|
|
300
295
|
}
|
|
301
296
|
}
|
|
302
|
-
|
|
303
|
-
const tempPath = path.join(parentPath, `.fs-safe-${randomUUID()}.tmp`);
|
|
304
|
-
const tempFlags = fsSync.constants.O_WRONLY |
|
|
305
|
-
fsSync.constants.O_CREAT |
|
|
306
|
-
fsSync.constants.O_EXCL |
|
|
307
|
-
(process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
|
|
308
|
-
? fsSync.constants.O_NOFOLLOW
|
|
309
|
-
: 0);
|
|
310
|
-
let handle;
|
|
311
|
-
let tempStat;
|
|
312
|
-
let tempIdentity;
|
|
313
|
-
let readHandle;
|
|
314
|
-
let renamed = false;
|
|
315
|
-
try {
|
|
316
|
-
params.assertBeforeMutation?.();
|
|
317
|
-
handle = await fs.open(tempPath, tempFlags, params.mode);
|
|
318
|
-
let verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
|
|
319
|
-
tempIdentity = verificationIdentity;
|
|
320
|
-
if (params.input.kind === "buffer") {
|
|
321
|
-
assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
|
|
322
|
-
await writeAllToFile(handle, params.input.data, {
|
|
323
|
-
encoding: params.input.encoding, assertBeforeMutation: params.assertBeforeMutation,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
else if (params.input.kind === "file") {
|
|
327
|
-
await writeCopyFileToFd(handle.fd, params.input, params.maxBytes, params.assertBeforeMutation);
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
await writeStreamToHandle(params.input.stream, handle, params.maxBytes, params.assertBeforeMutation);
|
|
331
|
-
}
|
|
332
|
-
tempStat = fsSync.fstatSync(handle.fd);
|
|
333
|
-
const tempPathStat = fsSync.lstatSync(tempPath);
|
|
334
|
-
if (tempPathStat.isSymbolicLink() || !sameFileIdentity(tempPathStat, tempStat)) {
|
|
335
|
-
throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
|
|
336
|
-
}
|
|
337
|
-
const expectedTempStat = tempStat;
|
|
338
|
-
await handle.chmod(params.mode);
|
|
339
|
-
if (params.sync !== false)
|
|
340
|
-
await syncFileBestEffort(handle);
|
|
341
|
-
if (params.input.kind === "file")
|
|
342
|
-
await params.input.verifySource();
|
|
343
|
-
let verifiedIdentity = expectedTempStat;
|
|
344
|
-
await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
345
|
-
assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
|
|
346
|
-
if (params.overwrite === false) {
|
|
347
|
-
publishCopyStage({
|
|
348
|
-
temporaryPath: tempPath, targetPath, fd: handle.fd,
|
|
349
|
-
identity: tempIdentity, parentGuard,
|
|
350
|
-
assertBeforeMutation: params.assertBeforeMutation,
|
|
351
|
-
onPublished: (identity) => {
|
|
352
|
-
renamed = true;
|
|
353
|
-
params.onPublished?.(identity);
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
else {
|
|
358
|
-
params.assertBeforeMutation?.();
|
|
359
|
-
await fs.rename(tempPath, targetPath);
|
|
360
|
-
renamed = true;
|
|
361
|
-
params.onPublished?.(verificationIdentity);
|
|
362
|
-
}
|
|
363
|
-
await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
|
|
364
|
-
if (params.sync !== false)
|
|
365
|
-
await syncDirectoryBestEffort(parentPath);
|
|
366
|
-
const targetStat = fsSync.lstatSync(targetPath);
|
|
367
|
-
if (targetStat.isSymbolicLink()) {
|
|
368
|
-
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
369
|
-
}
|
|
370
|
-
if (!sameFileIdentity(targetStat, expectedTempStat)) {
|
|
371
|
-
// On filesystems like rclone FUSE, rename(2) can give the destination a
|
|
372
|
-
// different inode from the source temp fd even with zero concurrency. The
|
|
373
|
-
// caller must ensure mutual exclusion before passing "verify-content";
|
|
374
|
-
// fall back to a content hash for this rename-boundary check only.
|
|
375
|
-
if (params.onRenameIdentityMismatch !== "verify-content") {
|
|
376
|
-
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
377
|
-
}
|
|
378
|
-
if (params.input.kind !== "buffer") {
|
|
379
|
-
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
380
|
-
}
|
|
381
|
-
const expectedHash = sha256Hex(params.input.data, params.input.encoding);
|
|
382
|
-
readHandle = await fs.open(targetPath, resolveReadOpenFlags());
|
|
383
|
-
const readHandleStat = fsSync.fstatSync(readHandle.fd, { bigint: true });
|
|
384
|
-
const actualHash = sha256Hex(await readHandle.readFile());
|
|
385
|
-
if (actualHash !== expectedHash) {
|
|
386
|
-
throw new FsSafeError("path-mismatch", "fallback target changed during write");
|
|
387
|
-
}
|
|
388
|
-
// The content-verified destination, not the old temp inode, is now pinned.
|
|
389
|
-
verificationIdentity = readHandleStat;
|
|
390
|
-
// Preserve the helper's legacy numeric return facts, not the private proof.
|
|
391
|
-
verifiedIdentity = { dev: Number(readHandleStat.dev), ino: Number(readHandleStat.ino) };
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
await params.verifyPublished?.((readHandle ?? handle).fd, verificationIdentity, parentGuard);
|
|
395
|
-
return { dev: verifiedIdentity.dev, ino: verifiedIdentity.ino };
|
|
396
|
-
}
|
|
397
|
-
finally {
|
|
398
|
-
try {
|
|
399
|
-
if (!renamed && handle) {
|
|
400
|
-
await cleanupPinnedFilePath({ pathname: tempPath, handle, identity: tempIdentity, parentGuard });
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
finally {
|
|
404
|
-
await readHandle?.close().catch(() => undefined);
|
|
405
|
-
await handle?.close().catch(() => undefined);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
297
|
+
return await runPinnedStagedWrite(params, parentPath, parentGuard);
|
|
408
298
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"private-directory.d.ts","sourceRoot":"","sources":["../src/private-directory.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,6BAA6B,GACtC,OAAO,CAAC,IAAI,CAAC,CAgCf"}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { FsSafeError } from "./errors.js";
|
|
2
3
|
import { getNativeBinding } from "./native.js";
|
|
4
|
+
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
5
|
+
import { warnNativeFallback } from "./native-fallback-warning.js";
|
|
3
6
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
7
|
+
import { createPrivateWindowsDirectoryCommand } from "./windows-security-command.js";
|
|
4
8
|
export async function createPrivateDirectory(targetPath, options) {
|
|
5
9
|
const platform = options?.platform ?? process.platform;
|
|
6
10
|
if (platform !== "win32") {
|
|
@@ -8,9 +12,19 @@ export async function createPrivateDirectory(targetPath, options) {
|
|
|
8
12
|
}
|
|
9
13
|
assertNoWindowsPathAlias(targetPath, "filesystem", "private directory path uses a Windows filesystem namespace alias", platform);
|
|
10
14
|
const native = getNativeBinding();
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
if (typeof native?.createPrivateDirectory === "function") {
|
|
16
|
+
native.createPrivateDirectory(targetPath);
|
|
17
|
+
return;
|
|
14
18
|
}
|
|
15
|
-
|
|
19
|
+
if (getFsSafeNativeConfig().mode === "require") {
|
|
20
|
+
throw new FsSafeError("helper-unavailable", "private Windows directory creation requires an up-to-date native helper");
|
|
21
|
+
}
|
|
22
|
+
// Preserve the native parser's raw component restrictions before resolve()
|
|
23
|
+
// could discard dot components or Win32 could trim trailing aliases.
|
|
24
|
+
const components = targetPath.replaceAll("/", "\\").split("\\");
|
|
25
|
+
if (components.some(component => component.endsWith(".") || component.endsWith(" "))) {
|
|
26
|
+
throw Object.assign(new Error("private directory path has an ambiguous component"), { code: "EINVAL" });
|
|
27
|
+
}
|
|
28
|
+
warnNativeFallback("windows-private-directory", "Private Windows directory creation uses a slower built-in system command.");
|
|
29
|
+
await createPrivateWindowsDirectoryCommand(path.win32.resolve(targetPath));
|
|
16
30
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
|
+
import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
|
|
3
|
+
export declare function handoffCreatedFileSync(params: {
|
|
4
|
+
source: OwnedFileDescriptorSync;
|
|
5
|
+
sourcePath: string;
|
|
6
|
+
targetPath: string;
|
|
7
|
+
identity: BigIntStats;
|
|
8
|
+
assertSourceParent: () => void;
|
|
9
|
+
assertTargetParent: () => void;
|
|
10
|
+
assertBeforeMutation?: () => void;
|
|
11
|
+
verifyDescriptor?: (fd: number, path: string, links: number) => void;
|
|
12
|
+
onPublished?: () => void;
|
|
13
|
+
}): OwnedFileDescriptorSync;
|
|
14
|
+
//# sourceMappingURL=private-producer-handoff-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"private-producer-handoff-sync.d.ts","sourceRoot":"","sources":["../src/private-producer-handoff-sync.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAyB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAW7F,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,MAAM,EAAE,uBAAuB,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,GAAG,uBAAuB,CA0H1B"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs, {} from "node:fs";
|
|
2
|
+
import { ownFileDescriptorSync } from "./create-owned-file.js";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
5
|
+
import { assertInitialSource, assertParents, inspectLinkedFile, normalizeLinkError, } from "./private-producer-handoff.js";
|
|
6
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
7
|
+
export function handoffCreatedFileSync(params) {
|
|
8
|
+
let retained = params.source;
|
|
9
|
+
const owners = new Set([retained]);
|
|
10
|
+
const failures = [];
|
|
11
|
+
let publication = "not-published";
|
|
12
|
+
let cleanup = "preserved";
|
|
13
|
+
let closeFailed = false;
|
|
14
|
+
function close(owner) {
|
|
15
|
+
// A throwing close may already have released the descriptor for reuse.
|
|
16
|
+
owners.delete(owner);
|
|
17
|
+
try {
|
|
18
|
+
owner.close();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
closeFailed = true;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function assertIdentityCurrent(owner, links) {
|
|
26
|
+
assertParents(params);
|
|
27
|
+
inspectLinkedFile(() => fs.fstatSync(owner.fd, { bigint: true }), params.identity, links, "created file descriptor");
|
|
28
|
+
if (cleanup !== "removed") {
|
|
29
|
+
inspectLinkedFile(() => fs.lstatSync(params.sourcePath, { bigint: true }), params.identity, links, "created file stage");
|
|
30
|
+
}
|
|
31
|
+
if (publication === "published") {
|
|
32
|
+
inspectLinkedFile(() => fs.lstatSync(params.targetPath, { bigint: true }), params.identity, links, "created file destination");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function verifyCurrent(owner, descriptorPath, links) {
|
|
36
|
+
assertIdentityCurrent(owner, links);
|
|
37
|
+
params.verifyDescriptor?.(owner.fd, descriptorPath, Number(links));
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
assertInitialSource(params.identity);
|
|
41
|
+
verifyCurrent(retained, params.sourcePath, 1n);
|
|
42
|
+
assertSynchronousCallbackResult(params.assertBeforeMutation?.(), "assertBeforeMutation");
|
|
43
|
+
assertIdentityCurrent(retained, 1n);
|
|
44
|
+
publication = "indeterminate";
|
|
45
|
+
try {
|
|
46
|
+
fs.linkSync(params.sourcePath, params.targetPath);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const normalized = normalizeLinkError(error);
|
|
50
|
+
if (normalized instanceof FsSafeError &&
|
|
51
|
+
(normalized.code === "already-exists" || normalized.code === "helper-unavailable")) {
|
|
52
|
+
publication = "not-published";
|
|
53
|
+
}
|
|
54
|
+
throw normalized;
|
|
55
|
+
}
|
|
56
|
+
publication = "published";
|
|
57
|
+
try {
|
|
58
|
+
params.onPublished?.();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
failures.push(error);
|
|
62
|
+
}
|
|
63
|
+
if (process.platform === "win32") {
|
|
64
|
+
verifyCurrent(retained, params.sourcePath, 2n);
|
|
65
|
+
// Keep the original pin until a verified published-name pin exists, so
|
|
66
|
+
// Windows can retire the staging name without leaving it delete-pending.
|
|
67
|
+
const sibling = ownFileDescriptorSync(fs.openSync(params.targetPath, fs.constants.O_RDWR | resolveReadOpenFlags()));
|
|
68
|
+
owners.add(sibling);
|
|
69
|
+
verifyCurrent(sibling, params.targetPath, 2n);
|
|
70
|
+
inspectLinkedFile(() => fs.fstatSync(retained.fd, { bigint: true }), params.identity, 2n, "created file descriptor");
|
|
71
|
+
close(retained);
|
|
72
|
+
retained = sibling;
|
|
73
|
+
}
|
|
74
|
+
const descriptorPath = process.platform === "win32" ? params.targetPath : params.sourcePath;
|
|
75
|
+
verifyCurrent(retained, descriptorPath, 2n);
|
|
76
|
+
assertSynchronousCallbackResult(params.assertBeforeMutation?.(), "assertBeforeMutation");
|
|
77
|
+
assertIdentityCurrent(retained, 2n);
|
|
78
|
+
cleanup = "failed";
|
|
79
|
+
// Link and unlink stay in one JS turn. Only the verified stage is removed;
|
|
80
|
+
// publication has committed and the destination is never rollback cleanup.
|
|
81
|
+
fs.unlinkSync(params.sourcePath);
|
|
82
|
+
cleanup = "removed";
|
|
83
|
+
verifyCurrent(retained, params.targetPath, 1n);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
failures.push(error);
|
|
87
|
+
}
|
|
88
|
+
if (failures.length === 0)
|
|
89
|
+
return retained;
|
|
90
|
+
for (const owner of owners) {
|
|
91
|
+
try {
|
|
92
|
+
close(owner);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
failures.push(error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const primary = failures[0];
|
|
99
|
+
// Collision-as-no-op callers must still observe a failed settlement.
|
|
100
|
+
const code = closeFailed || failures.length > 1
|
|
101
|
+
? "helper-failed"
|
|
102
|
+
: primary instanceof FsSafeError ? primary.code : "helper-failed";
|
|
103
|
+
throw new FsSafeError(code, "created file publication failed", {
|
|
104
|
+
cause: failures.length === 1 ? primary : new AggregateError(failures, "created file publication and settlement failed"),
|
|
105
|
+
details: {
|
|
106
|
+
publication: { status: publication },
|
|
107
|
+
cleanup,
|
|
108
|
+
resources: closeFailed ? "close-failed" : "closed",
|
|
109
|
+
path: params.targetPath,
|
|
110
|
+
dev: params.identity.dev,
|
|
111
|
+
ino: params.identity.ino,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -6,11 +6,29 @@ export type PrivateProducerHandoff = {
|
|
|
6
6
|
identity: BigIntStats;
|
|
7
7
|
unregister: TempPathRegistration;
|
|
8
8
|
};
|
|
9
|
-
|
|
10
|
-
sourcePath: string;
|
|
11
|
-
targetPath: string;
|
|
9
|
+
type HandoffParents = {
|
|
12
10
|
assertSourceParent: () => void;
|
|
13
11
|
assertTargetParent: () => void;
|
|
12
|
+
};
|
|
13
|
+
type HandoffPaths = HandoffParents & {
|
|
14
|
+
sourcePath: string;
|
|
15
|
+
targetPath: string;
|
|
16
|
+
};
|
|
17
|
+
type ProducerHandoffParams = HandoffPaths & {
|
|
14
18
|
readWrite: boolean;
|
|
15
|
-
}
|
|
19
|
+
};
|
|
20
|
+
type CreatedHandoffParams = HandoffPaths & {
|
|
21
|
+
source: FileHandle;
|
|
22
|
+
identity: BigIntStats;
|
|
23
|
+
assertBeforeMutation?: () => void;
|
|
24
|
+
verifyDescriptor?: (fd: number, path: string, links: number) => Promise<void>;
|
|
25
|
+
onPublished?: () => void;
|
|
26
|
+
};
|
|
27
|
+
export declare function assertInitialSource(stat: BigIntStats): void;
|
|
28
|
+
export declare function inspectLinkedFile(inspect: () => BigIntStats, expected: BigIntStats, links: bigint, label: string): BigIntStats;
|
|
29
|
+
export declare function assertParents(params: HandoffParents): void;
|
|
30
|
+
export declare function normalizeLinkError(error: unknown): unknown;
|
|
31
|
+
export declare function handoffPrivateProducerFile(params: ProducerHandoffParams): Promise<PrivateProducerHandoff>;
|
|
32
|
+
export declare function handoffCreatedFile(params: CreatedHandoffParams): Promise<FileHandle>;
|
|
33
|
+
export {};
|
|
16
34
|
//# sourceMappingURL=private-producer-handoff.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private-producer-handoff.d.ts","sourceRoot":"","sources":["../src/private-producer-handoff.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAW,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"private-producer-handoff.d.ts","sourceRoot":"","sources":["../src/private-producer-handoff.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAW,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMvD,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,UAAU,EAAE,oBAAoB,CAAC;CAClC,CAAC;AASF,KAAK,cAAc,GAAG;IACpB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAC;CAChC,CAAC;AAEF,KAAK,YAAY,GAAG,cAAc,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAChF,KAAK,qBAAqB,GAAG,YAAY,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC;AACnE,KAAK,oBAAoB,GAAG,YAAY,GAAG;IACzC,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAMF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAU3D;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,WAAW,EAC1B,QAAQ,EAAE,WAAW,EACrB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,WAAW,CAQb;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAG1D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAmB1D;AAiUD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAEzG;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,CAEpF"}
|