@openclaw/fs-safe 0.8.6 → 0.10.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 +93 -0
- package/README.md +17 -0
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts.map +1 -1
- package/dist/archive-crc32.js +6 -1
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +3 -4
- package/dist/archive-durability.d.ts +24 -0
- package/dist/archive-durability.d.ts.map +1 -0
- package/dist/archive-durability.js +180 -0
- package/dist/archive-gzip-tail.d.ts +2 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +22 -4
- package/dist/archive-input.js +4 -4
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +3 -2
- package/dist/archive-merge.d.ts +1 -0
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +42 -9
- package/dist/archive-native.d.ts +1 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +4 -2
- package/dist/archive-options.d.ts +2 -0
- package/dist/archive-options.d.ts.map +1 -1
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +72 -70
- package/dist/archive-staging.d.ts +1 -1
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +24 -19
- package/dist/archive-tar-stream.d.ts +11 -4
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +20 -8
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +2 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +9 -4
- package/dist/bounded-read.d.ts +7 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +73 -43
- package/dist/clone-metadata.d.ts +19 -0
- package/dist/clone-metadata.d.ts.map +1 -0
- package/dist/clone-metadata.js +32 -0
- package/dist/copy-file-input.d.ts +23 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +91 -0
- package/dist/copy-policy.d.ts +3 -0
- package/dist/copy-policy.d.ts.map +1 -0
- package/dist/copy-policy.js +8 -0
- package/dist/copy-publication.d.ts +15 -0
- package/dist/copy-publication.d.ts.map +1 -0
- package/dist/copy-publication.js +30 -0
- package/dist/copy-tree-portable.d.ts +9 -0
- package/dist/copy-tree-portable.d.ts.map +1 -0
- package/dist/copy-tree-portable.js +191 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +123 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +4 -4
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -1
- package/dist/file-hash.d.ts +6 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +49 -12
- package/dist/file-store-sync-write.d.ts +1 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +9 -6
- package/dist/file-store.d.ts +4 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +10 -1
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +2 -12
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +1 -2
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +1 -0
- package/dist/guarded-mutation.d.ts +2 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +9 -5
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +9 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +9 -13
- package/dist/json-document-store.d.ts +3 -0
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +1 -0
- package/dist/json-durable-queue-directory.js +3 -3
- package/dist/json-durable-queue-ownership.js +3 -2
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +31 -27
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +22 -24
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +4 -3
- package/dist/move-path-stage.d.ts +8 -0
- package/dist/move-path-stage.d.ts.map +1 -0
- package/dist/move-path-stage.js +55 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +55 -47
- package/dist/mutation-authority.d.ts +8 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +24 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +11 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +18 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +1 -0
- package/dist/native-staged-file.d.ts +3 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +35 -8
- package/dist/opened-file-failure.d.ts +1 -1
- package/dist/opened-file-failure.d.ts.map +1 -1
- package/dist/opened-file-failure.js +3 -2
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +3 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +38 -48
- package/dist/permissions.js +3 -3
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +5 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +50 -30
- package/dist/positional-read.d.ts +9 -0
- package/dist/positional-read.d.ts.map +1 -0
- package/dist/positional-read.js +36 -0
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +9 -3
- package/dist/publish-copy-stage.d.ts +13 -0
- package/dist/publish-copy-stage.d.ts.map +1 -0
- package/dist/publish-copy-stage.js +47 -0
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +26 -25
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +30 -17
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +10 -5
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +8 -3
- package/dist/root-context.d.ts +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +51 -26
- package/dist/root-directory-list.d.ts +24 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +201 -0
- package/dist/root-errors.d.ts +1 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +3 -0
- package/dist/root-file.d.ts +2 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +12 -4
- package/dist/root-impl.d.ts +6 -50
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +342 -208
- package/dist/root-options.d.ts +66 -0
- package/dist/root-options.d.ts.map +1 -0
- package/dist/root-options.js +18 -0
- package/dist/root-path-existing.d.ts +5 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +56 -1
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +74 -162
- package/dist/root-symlink-policy.d.ts +13 -0
- package/dist/root-symlink-policy.d.ts.map +1 -0
- package/dist/root-symlink-policy.js +34 -0
- package/dist/root-walk.d.ts +5 -4
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +99 -50
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +1 -4
- package/dist/root.d.ts +3 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +1 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +1 -0
- package/dist/secret-read-async.js +5 -5
- package/dist/secure-file.d.ts +1 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +23 -13
- package/dist/sibling-staged-file.js +7 -7
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +16 -7
- package/dist/sidecar-lock-acquire.d.ts +1 -1
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +6 -4
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +5 -3
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +16 -4
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +2 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +9 -4
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.js +2 -2
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +7 -27
- package/dist/windows-owner.d.ts +9 -12
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +24 -58
- package/dist/write-file-handle.d.ts +6 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +25 -0
- package/dist/write-open-flags.d.ts.map +1 -1
- package/dist/write-open-flags.js +1 -2
- package/docs/advanced.md +18 -4
- package/docs/archive.md +57 -9
- package/docs/atomic.md +7 -0
- package/docs/contributing.md +7 -0
- package/docs/copy.md +86 -0
- package/docs/durability.md +17 -2
- package/docs/errors.md +1 -1
- package/docs/file-store.md +48 -3
- package/docs/json-store.md +10 -0
- package/docs/local-roots.md +8 -1
- package/docs/native.md +14 -1
- package/docs/permissions.md +20 -10
- package/docs/positional-read.md +63 -0
- package/docs/reading.md +6 -0
- package/docs/root.md +168 -6
- package/docs/secret-file.md +6 -0
- package/docs/security-model.md +14 -0
- package/docs/sidecar-lock.md +2 -0
- package/docs/timing.md +2 -0
- package/docs/types.md +18 -11
- package/docs/walk.md +54 -5
- package/docs/writing.md +25 -5
- package/package.json +16 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-pinned-write-windows.d.ts","sourceRoot":"","sources":["../src/native-pinned-write-windows.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"native-pinned-write-windows.d.ts","sourceRoot":"","sources":["../src/native-pinned-write-windows.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAM3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,KAAK,CAAC,EAC3C,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,KAAK,CAAC,GAC3C,OAAO,CAET;AAWD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,iBAAiB,EACzB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAyF3B"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { syncFileBestEffortSync } from "./file-sync.js";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import fsSync, {} from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
4
5
|
import { FsSafeError } from "./errors.js";
|
|
5
6
|
import { nativeOpenFlags, removeNativeCreatedFileIfStillPinned, writeNativeInput, } from "./native-operations.js";
|
|
6
7
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
8
|
+
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
7
9
|
export function sameNativeIdentity(left, right) {
|
|
8
10
|
return left.dev === right.dev && left.ino === right.ino;
|
|
9
11
|
}
|
|
@@ -27,6 +29,7 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
27
29
|
let completed = false;
|
|
28
30
|
try {
|
|
29
31
|
tempName = `.fs-safe-${randomUUID()}.tmp`;
|
|
32
|
+
params.assertBeforeMutation?.();
|
|
30
33
|
tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL)).fd;
|
|
31
34
|
const verificationIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(tempFd, { bigint: true }));
|
|
32
35
|
tempIdentity = verificationIdentity;
|
|
@@ -34,9 +37,13 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
34
37
|
// can remove owner access. Keep the unpublished inode private and
|
|
35
38
|
// reopenable until the published name has been identity-fenced.
|
|
36
39
|
fsSync.fchmodSync(tempFd, 0o600);
|
|
37
|
-
await writeNativeInput(tempFd, params.input, params.maxBytes);
|
|
40
|
+
await writeNativeInput(tempFd, params.input, params.maxBytes, params.assertBeforeMutation);
|
|
38
41
|
if (params.sync !== false)
|
|
39
42
|
syncFileBestEffortSync(tempFd);
|
|
43
|
+
if (params.input.kind === "file")
|
|
44
|
+
await params.input.verifySource();
|
|
45
|
+
assertFinalSymlinkRejected(path.join(parentPath, params.basename), params.rejectFinalSymlink);
|
|
46
|
+
params.assertBeforeMutation?.();
|
|
40
47
|
if (params.overwrite === false) {
|
|
41
48
|
binding.renameNoReplace(parentFd, tempName, parentFd, params.basename);
|
|
42
49
|
}
|
|
@@ -44,6 +51,7 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
44
51
|
binding.renameReplace(parentFd, tempName, parentFd, params.basename);
|
|
45
52
|
}
|
|
46
53
|
renamed = true;
|
|
54
|
+
params.onPublished?.(verificationIdentity);
|
|
47
55
|
targetFd = binding.openBeneath(parentFd, params.basename, nativeOpenFlags(fsSync.constants.O_RDONLY)).fd;
|
|
48
56
|
const targetStat = inspectFileIdentitySync(() => fsSync.fstatSync(targetFd, { bigint: true }), verificationIdentity);
|
|
49
57
|
const targetIdentity = binding.fstatIdentity(targetFd);
|
|
@@ -61,12 +69,15 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
61
69
|
catch (error) {
|
|
62
70
|
closeWriteFd(targetFd);
|
|
63
71
|
targetFd = undefined;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
// Copies preserve the published name for caller-owned recovery.
|
|
73
|
+
if (params.input.kind !== "file") {
|
|
74
|
+
removeNativeCreatedFileIfStillPinned({
|
|
75
|
+
parentPath,
|
|
76
|
+
parentFd,
|
|
77
|
+
basename: params.basename,
|
|
78
|
+
created: tempIdentity,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
70
81
|
throw error;
|
|
71
82
|
}
|
|
72
83
|
if (params.sync !== false)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA2FvH"}
|
|
@@ -100,6 +100,7 @@ export async function runPinnedWriteNative(binding, params) {
|
|
|
100
100
|
throw new FsSafeError("path-mismatch", "root path changed during native write");
|
|
101
101
|
}
|
|
102
102
|
if (params.mkdir) {
|
|
103
|
+
params.assertBeforeMutation?.();
|
|
103
104
|
binding.mkdirBeneath(root.fd, params.relativeParentPath, 0o777);
|
|
104
105
|
}
|
|
105
106
|
parentFd = binding.openBeneath(root.fd, params.relativeParentPath, directoryFlags).fd;
|
|
@@ -8,14 +8,14 @@ export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding,
|
|
|
8
8
|
export declare function assertNativeStaging(binding: NativeBinding): asserts binding is NativeStagingBinding;
|
|
9
9
|
declare class NativeStagedFile implements StagedFile {
|
|
10
10
|
#private;
|
|
11
|
-
constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean);
|
|
12
|
-
static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean): Promise<NativeStagedFile>;
|
|
11
|
+
constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
|
|
12
|
+
static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
|
|
13
13
|
static write(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
|
|
14
14
|
get receipt(): StagedFileReceipt;
|
|
15
15
|
assertCurrent(): Promise<void>;
|
|
16
16
|
publish(basename: string, options: {
|
|
17
17
|
overwrite: boolean;
|
|
18
|
-
}): Promise<PublishedFileReceipt>;
|
|
18
|
+
}, onPublished?: PinnedWriteParams["onPublished"]): Promise<PublishedFileReceipt>;
|
|
19
19
|
cleanup(): Promise<StagedFileCleanupReceipt>;
|
|
20
20
|
[Symbol.asyncDispose](): Promise<void>;
|
|
21
21
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAMlE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG7E,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,wBAAwB,EAGxB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,QAAQ,CAAC,IAAI,CAC9D,aAAa,EACb,kBAAkB,GAAG,mBAAmB,GAAG,kBAAkB,CAC9D,CAAC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,IAAI,oBAAoB,CAUnG;AA6BD,cAAM,gBAAiB,YAAW,UAAU;;IAa1C,YACE,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,OAAO,EACb,oBAAoB,CAAC,EAAE,MAAM,IAAI,EASlC;IAGD,OAAa,MAAM,CACjB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EAEjB,aAAa,UAAO,EACpB,IAAI,UAAO,EACX,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,OAAa,KAAK,CAChB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,IAAI,OAAO,IAAI,iBAAiB,CAK/B;IA8FK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAEK,OAAO,CACX,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,EAC/B,WAAW,CAAC,EAAE,iBAAiB,CAAC,aAAa,CAAC,GAC7C,OAAO,CAAC,oBAAoB,CAAC,CAyD/B;IA6CK,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAMjD;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3C;CACF;AAED,eAAO,MAAM,iBAAiB,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,UAAU,CAClF,CAAC;AAC1B,eAAO,MAAM,gBAAgB,+BAAyB,CAAC;AAEvD,YAAY,EACV,oBAAoB,EAAE,UAAU,EAAE,wBAAwB,EAAE,wBAAwB,EACpF,qBAAqB,EAAE,iBAAiB,GACzC,MAAM,wBAAwB,CAAC;AAEhC,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAAC;IACrC,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,UAAU,CAAC,CAUtB"}
|
|
@@ -54,9 +54,13 @@ import { requireNativeBinding } from "./native.js";
|
|
|
54
54
|
import { syncFileBestEffortSync } from "./file-sync.js";
|
|
55
55
|
import { randomUUID } from "node:crypto";
|
|
56
56
|
import fs from "node:fs";
|
|
57
|
+
import path from "node:path";
|
|
57
58
|
import { FsSafeError } from "./errors.js";
|
|
59
|
+
import { MutationAuthorityError } from "./mutation-authority.js";
|
|
58
60
|
import { writeNativeInput } from "./native-operations.js";
|
|
61
|
+
import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
|
|
59
62
|
import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
|
|
63
|
+
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
60
64
|
export function assertNativeStaging(binding) {
|
|
61
65
|
if ([
|
|
62
66
|
binding.createStagedFile,
|
|
@@ -93,24 +97,27 @@ class NativeStagedFile {
|
|
|
93
97
|
#portableNames;
|
|
94
98
|
#publishedMode;
|
|
95
99
|
#sync;
|
|
100
|
+
#assertBeforeMutation;
|
|
96
101
|
#name = `.fs-safe-${randomUUID()}.tmp`;
|
|
97
102
|
#state = { status: "open", publication: NOT_PUBLISHED };
|
|
98
103
|
#receipt;
|
|
99
|
-
|
|
104
|
+
#rejectFinalSymlink = false;
|
|
105
|
+
constructor(binding, parentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation) {
|
|
100
106
|
this.#binding = binding;
|
|
101
107
|
this.#parentFd = parentFd;
|
|
102
108
|
this.#directory = directory;
|
|
103
109
|
this.#portableNames = portableNames;
|
|
104
110
|
this.#publishedMode = publishedMode;
|
|
105
111
|
this.#sync = sync;
|
|
112
|
+
this.#assertBeforeMutation = assertBeforeMutation;
|
|
106
113
|
}
|
|
107
114
|
// Takes ownership of parentFd, including all construction and preparation failures.
|
|
108
115
|
static async create(binding, parentFd, directory, input, mode, maxBytes,
|
|
109
116
|
// Public staging uses portable names; existing POSIX writes accept literal names.
|
|
110
|
-
portableNames = true, sync = true) {
|
|
117
|
+
portableNames = true, sync = true, assertBeforeMutation) {
|
|
111
118
|
let staged;
|
|
112
119
|
try {
|
|
113
|
-
staged = new NativeStagedFile(binding, parentFd, directory, portableNames, mode, sync);
|
|
120
|
+
staged = new NativeStagedFile(binding, parentFd, directory, portableNames, mode, sync, assertBeforeMutation);
|
|
114
121
|
}
|
|
115
122
|
catch (error) {
|
|
116
123
|
try {
|
|
@@ -129,8 +136,11 @@ class NativeStagedFile {
|
|
|
129
136
|
try {
|
|
130
137
|
// This owner never escapes. Only the internal verifier borrows its fd;
|
|
131
138
|
// public descriptor methods remain await-free and cannot race disposal.
|
|
132
|
-
const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync), true);
|
|
133
|
-
|
|
139
|
+
const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation), true);
|
|
140
|
+
staged.#rejectFinalSymlink = params.rejectFinalSymlink === true;
|
|
141
|
+
if (params.input.kind === "file")
|
|
142
|
+
await params.input.verifySource();
|
|
143
|
+
const published = await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
|
|
134
144
|
const identity = published.staged.identity;
|
|
135
145
|
await params.verifyPublished?.(staged.#file(), identity, parentGuard);
|
|
136
146
|
return { dev: identity.dev, ino: identity.ino };
|
|
@@ -190,10 +200,22 @@ class NativeStagedFile {
|
|
|
190
200
|
// The exclusive open performs no fallible post-open checks. Store its fd
|
|
191
201
|
// before every subsequent operation, including the first metadata read.
|
|
192
202
|
const state = this.#open();
|
|
193
|
-
|
|
203
|
+
this.#assertBeforeMutation?.();
|
|
204
|
+
const copied = input.kind === "file"
|
|
205
|
+
? await createNativeCopyFile(this.#binding, input, this.#parentFd, this.#name, maxBytes, false)
|
|
206
|
+
: undefined;
|
|
207
|
+
state.fileFd = copied?.fd;
|
|
208
|
+
if (state.fileFd === undefined) {
|
|
209
|
+
this.#assertBeforeMutation?.();
|
|
210
|
+
state.fileFd = this.#binding.createStagedFile(this.#parentFd, this.#name);
|
|
211
|
+
}
|
|
194
212
|
const fd = state.fileFd;
|
|
213
|
+
if (input.kind === "file")
|
|
214
|
+
assertNativeCopyCompleted(input, copied);
|
|
215
|
+
this.#assertBeforeMutation?.();
|
|
195
216
|
fs.fchmodSync(fd, 0o600);
|
|
196
|
-
|
|
217
|
+
if (!copied)
|
|
218
|
+
await writeNativeInput(fd, input, maxBytes, this.#assertBeforeMutation);
|
|
197
219
|
if (this.#sync)
|
|
198
220
|
syncFileBestEffortSync(fd);
|
|
199
221
|
const stat = fs.fstatSync(fd, { bigint: true });
|
|
@@ -230,7 +252,7 @@ class NativeStagedFile {
|
|
|
230
252
|
async assertCurrent() {
|
|
231
253
|
this.#assertCurrent();
|
|
232
254
|
}
|
|
233
|
-
async publish(basename, options) {
|
|
255
|
+
async publish(basename, options, onPublished) {
|
|
234
256
|
const overwrite = options?.overwrite;
|
|
235
257
|
try {
|
|
236
258
|
const state = this.#open();
|
|
@@ -239,6 +261,8 @@ class NativeStagedFile {
|
|
|
239
261
|
throw new FsSafeError("invalid-path", "publication needs a distinct basename and explicit overwrite policy");
|
|
240
262
|
}
|
|
241
263
|
this.#assertCurrent();
|
|
264
|
+
assertFinalSymlinkRejected(path.join(this.#directory.realPath, basename), this.#rejectFinalSymlink);
|
|
265
|
+
this.#assertBeforeMutation?.();
|
|
242
266
|
try {
|
|
243
267
|
if (overwrite) {
|
|
244
268
|
this.#binding.renameReplace(this.#parentFd, this.#name, this.#parentFd, basename);
|
|
@@ -262,6 +286,7 @@ class NativeStagedFile {
|
|
|
262
286
|
overwrite,
|
|
263
287
|
});
|
|
264
288
|
state.publication = receipt;
|
|
289
|
+
onPublished?.(receipt.staged.identity);
|
|
265
290
|
const stagedMode = this.#assertNamed(basename);
|
|
266
291
|
assertStagedDirectoryCurrent(this.#directory);
|
|
267
292
|
// Keep contents private until the published name passes its identity
|
|
@@ -282,6 +307,8 @@ class NativeStagedFile {
|
|
|
282
307
|
return receipt;
|
|
283
308
|
}
|
|
284
309
|
catch (error) {
|
|
310
|
+
if (error instanceof MutationAuthorityError)
|
|
311
|
+
throw error;
|
|
285
312
|
// Closure rejects further use, not the recorded outcome of an earlier publication.
|
|
286
313
|
const publication = this.#state.status === "closed"
|
|
287
314
|
? this.#state.receipt.publication
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BigIntStats } from "node:fs";
|
|
2
2
|
import type { FileHandle } from "node:fs/promises";
|
|
3
3
|
export declare function recordFileOpenFailure(error: unknown, filePath: string): never;
|
|
4
4
|
export declare function recordExclusiveCreateFailure(error: unknown, filePath: string): never;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opened-file-failure.d.ts","sourceRoot":"","sources":["../src/opened-file-failure.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"opened-file-failure.d.ts","sourceRoot":"","sources":["../src/opened-file-failure.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAG7E;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAGpF;AAED,wBAAgB,yBAAyB,CACvC,KAAK,GAAE,KAA8E,GACpF,KAAK,CAGP;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
1
2
|
import { FsSafeError } from "./errors.js";
|
|
2
3
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
3
4
|
import { isFileObservationFailure, recordFileObservationFailure } from "./file-observation.js";
|
|
@@ -19,7 +20,7 @@ export async function recordPreOpenFileChange(error, handle, filePath, before, o
|
|
|
19
20
|
return;
|
|
20
21
|
try {
|
|
21
22
|
await inspectFileIdentity(async () => before);
|
|
22
|
-
const current = await inspectFileIdentity(() => handle.
|
|
23
|
+
const current = await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }), opened);
|
|
23
24
|
if (current.isFile() && [0n, 1n].includes(current.nlink) &&
|
|
24
25
|
(before.dev !== current.dev || before.ino !== current.ino)) {
|
|
25
26
|
// A stale pathname sample is not proof that its former inode was unlinked.
|
|
@@ -35,7 +36,7 @@ export async function recordOpenedFileFailure(error, handle, filePath, identity)
|
|
|
35
36
|
!identity.isFile() || identity.nlink > 1n)
|
|
36
37
|
return;
|
|
37
38
|
try {
|
|
38
|
-
const current = await inspectFileIdentity(() => handle.
|
|
39
|
+
const current = await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }), identity);
|
|
39
40
|
if (current.isFile() && current.nlink === 0n)
|
|
40
41
|
recordFileObservationFailure(error, `unlinked:${filePath}`);
|
|
41
42
|
}
|
package/dist/path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAMzB,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,yBAAyB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,GAChC,MAAM,kBAAkB,CAAC;AAM1B,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAA6B,GAAG,IAAI,CAIjG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAMzB,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,yBAAyB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,GAChC,MAAM,kBAAkB,CAAC;AAM1B,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAA6B,GAAG,IAAI,CAIjG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA8BlE;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAElE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CA0BpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAOrF"}
|
package/dist/path.js
CHANGED
|
@@ -49,7 +49,9 @@ export function isPathInside(root, target) {
|
|
|
49
49
|
target.charCodeAt(0) === POSIX_SEPARATOR_CHAR_CODE &&
|
|
50
50
|
!target.includes("/..") &&
|
|
51
51
|
(target === root ||
|
|
52
|
-
(target.startsWith(root) &&
|
|
52
|
+
(target.startsWith(root) &&
|
|
53
|
+
(root.charCodeAt(root.length - 1) === POSIX_SEPARATOR_CHAR_CODE ||
|
|
54
|
+
target.charCodeAt(root.length) === POSIX_SEPARATOR_CHAR_CODE)))) {
|
|
53
55
|
return true;
|
|
54
56
|
}
|
|
55
57
|
const resolvedRoot = path.resolve(root);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions-windows.d.ts","sourceRoot":"","sources":["../src/permissions-windows.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"permissions-windows.d.ts","sourceRoot":"","sources":["../src/permissions-windows.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import { getNativeBinding } from "./native.js";
|
|
3
|
-
import { executePermissionCommand
|
|
3
|
+
import { executePermissionCommand } from "./permission-exec.js";
|
|
4
4
|
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
|
|
5
5
|
import { resolveWindowsSystemCommand } from "./windows-command.js";
|
|
6
|
-
import { inspectWindowsOwner
|
|
6
|
+
import { inspectWindowsOwner } from "./windows-owner.js";
|
|
7
7
|
const INHERIT_FLAGS = new Set(["I", "OI", "CI", "IO", "NP"]);
|
|
8
8
|
const WORLD_PRINCIPALS = new Set(["everyone", "users", "builtin\\users", "authenticated users", "nt authority\\authenticated users", "anonymous logon", "nt authority\\anonymous logon", "guests", "builtin\\guests", "interactive", "nt authority\\interactive", "network", "nt authority\\network", "local"]);
|
|
9
9
|
const TRUSTED_BASE = new Set([
|
|
@@ -92,13 +92,7 @@ export async function inspectWindowsPermissions(params) {
|
|
|
92
92
|
const error = `Windows owner inspection failed: ${owner.error}`;
|
|
93
93
|
return { ...unverified, ownerError: owner.error, error, errorDetail: owner.errorDetail, errorCause: owner.errorCause };
|
|
94
94
|
}
|
|
95
|
-
const acl =
|
|
96
|
-
env: params.opts?.env,
|
|
97
|
-
exec: params.opts?.exec,
|
|
98
|
-
currentUserSid: owner.currentUserSid,
|
|
99
|
-
principalSids: owner.principalSids,
|
|
100
|
-
principalTranslationFailed: owner.principalTranslationFailed,
|
|
101
|
-
});
|
|
95
|
+
const acl = summarizeWindowsOwnerAcl(owner);
|
|
102
96
|
const ownerFields = {
|
|
103
97
|
...(owner.sid ? { ownerSid: owner.sid } : {}),
|
|
104
98
|
...(owner.trusted !== undefined ? { ownerTrusted: owner.trusted } : {}),
|
|
@@ -230,49 +224,45 @@ export function summarizeWindowsAcl(entries, env) {
|
|
|
230
224
|
}
|
|
231
225
|
return { trusted, untrustedWorld, untrustedGroup };
|
|
232
226
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (opts?.principalTranslationFailed)
|
|
243
|
-
throw new Error("Windows ACL principal SID translation failed");
|
|
244
|
-
const { stdout, stderr } = await exec(resolveWindowsSystemCommand("icacls.exe", opts?.env), [targetPath]);
|
|
245
|
-
let entries = parseIcaclsOutput(`${stdout}\n${stderr}`.trim(), targetPath);
|
|
246
|
-
if (!entries.length)
|
|
247
|
-
throw new Error("Windows ACL output could not be verified");
|
|
248
|
-
const unresolvedPrincipals = entries.filter((entry) => !entry.sid).map((entry) => entry.principal);
|
|
249
|
-
const principalSids = await resolveWindowsPrincipalSids({ principals: unresolvedPrincipals, known: opts?.principalSids, env: opts?.env, exec });
|
|
250
|
-
entries = entries.map((entry) => {
|
|
251
|
-
const sid = entry.sid ?? principalSids[entry.principal.toLowerCase()];
|
|
252
|
-
if (!sid)
|
|
253
|
-
throw new Error(`Windows ACL principal SID could not be verified: ${entry.principal}`);
|
|
254
|
-
return { ...entry, sid };
|
|
255
|
-
});
|
|
256
|
-
let currentUserSid = normalizeSid(opts?.currentUserSid ?? "");
|
|
257
|
-
let effectiveEnv = currentUserSid ? { USERSID: currentUserSid } : undefined;
|
|
258
|
-
let { trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, effectiveEnv);
|
|
259
|
-
if (!currentUserSid && untrustedGroup.some((entry) => entry.sid && !TRUSTED_SIDS.has(entry.sid))) {
|
|
260
|
-
currentUserSid = (await resolveWindowsCurrentUserSid({ exec, env: opts?.env })) ?? "";
|
|
261
|
-
if (currentUserSid) {
|
|
262
|
-
effectiveEnv = { USERSID: currentUserSid };
|
|
263
|
-
({ trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, effectiveEnv));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return { ok: true, entries, trusted, untrustedWorld, untrustedGroup };
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
227
|
+
const MASK_RIGHTS = [
|
|
228
|
+
[0x1000_0000, "GA"], [0x8000_0000, "GR"], [0x4000_0000, "GW"], [0x2000_0000, "GE"],
|
|
229
|
+
[0x0001_0000, "D"], [0x0002_0000, "RC"], [0x0004_0000, "WDAC"], [0x0008_0000, "WO"],
|
|
230
|
+
[0x0010_0000, "S"], [0x0001, "RD"], [0x0002, "WD"], [0x0004, "AD"],
|
|
231
|
+
[0x0008, "REA"], [0x0010, "WEA"], [0x0020, "X"], [0x0040, "DC"], [0x0080, "RA"], [0x0100, "WA"],
|
|
232
|
+
];
|
|
233
|
+
function summarizeWindowsOwnerAcl(owner) {
|
|
234
|
+
const error = owner.error ?? owner.aclError;
|
|
235
|
+
if (error || owner.daclPresent === undefined || !owner.aces || !owner.currentUserSid) {
|
|
269
236
|
return {
|
|
270
237
|
ok: false, entries: [], trusted: [], untrustedWorld: [], untrustedGroup: [],
|
|
271
|
-
error:
|
|
272
|
-
errorDetail:
|
|
273
|
-
errorCause: err,
|
|
238
|
+
error: error ?? "Windows ACL query returned incomplete descriptor data",
|
|
239
|
+
errorDetail: owner.errorDetail, errorCause: owner.errorCause,
|
|
274
240
|
};
|
|
275
241
|
}
|
|
242
|
+
// A null DACL grants everyone full access; an empty DACL grants nothing.
|
|
243
|
+
const grants = owner.daclPresent ? owner.aces : [{ sid: "s-1-1-0", mask: 0x001f_01ff, deny: false, inheritOnly: false }];
|
|
244
|
+
const entries = grants.filter(ace => !ace.deny && !ace.inheritOnly).map(ace => {
|
|
245
|
+
const rights = ace.mask === 0x001f_01ff ? ["F"] : MASK_RIGHTS.filter(([mask]) => (ace.mask & mask) !== 0).map(([, name]) => name);
|
|
246
|
+
return {
|
|
247
|
+
principal: ace.sid, sid: ace.sid, rights, rawRights: "(" + rights.join(",") + ")",
|
|
248
|
+
// Retain the coarse policy's conservative treatment of read-control and
|
|
249
|
+
// mutation rights. Denies do not subtract grants or claim effective access.
|
|
250
|
+
canRead: (ace.mask & 0x9002_0089) !== 0,
|
|
251
|
+
canWrite: (ace.mask & 0x500d_0156) !== 0,
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
return { ok: true, entries, ...summarizeWindowsAcl(entries, { USERSID: owner.currentUserSid }) };
|
|
255
|
+
}
|
|
256
|
+
export async function inspectWindowsAcl(targetPath, opts) {
|
|
257
|
+
if (opts?.principalTranslationFailed) {
|
|
258
|
+
const error = new Error("Windows ACL principal SID translation failed");
|
|
259
|
+
return summarizeWindowsOwnerAcl({ error: String(error), errorCause: error });
|
|
260
|
+
}
|
|
261
|
+
const owner = await inspectWindowsOwner({ targetPath, env: opts?.env, exec: opts?.exec ?? defaultPermissionExec });
|
|
262
|
+
return summarizeWindowsOwnerAcl({
|
|
263
|
+
...owner,
|
|
264
|
+
currentUserSid: normalizeSid(opts?.currentUserSid ?? "") || owner.currentUserSid,
|
|
265
|
+
});
|
|
276
266
|
}
|
|
277
267
|
export function formatWindowsAclSummary(summary) {
|
|
278
268
|
if (!summary.ok)
|
package/dist/permissions.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
2
|
import { formatIcaclsResetCommand, inspectWindowsPermissions, } from "./permissions-windows.js";
|
|
3
3
|
export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, } from "./permissions-windows.js";
|
|
4
4
|
export async function safeStat(targetPath) {
|
|
5
5
|
try {
|
|
6
|
-
const lst =
|
|
6
|
+
const lst = fsSync.lstatSync(targetPath);
|
|
7
7
|
return {
|
|
8
8
|
ok: true,
|
|
9
9
|
isSymlink: lst.isSymbolicLink(),
|
|
@@ -46,7 +46,7 @@ export async function inspectPathPermissions(targetPath, opts) {
|
|
|
46
46
|
let effectiveIsDir = st.isDir;
|
|
47
47
|
if (st.isSymlink) {
|
|
48
48
|
try {
|
|
49
|
-
const target =
|
|
49
|
+
const target = fsSync.statSync(targetPath);
|
|
50
50
|
effectiveMode = typeof target.mode === "number" ? target.mode : st.mode;
|
|
51
51
|
effectiveIsDir = target.isDirectory();
|
|
52
52
|
}
|
package/dist/pinned-operation.js
CHANGED
|
@@ -27,7 +27,7 @@ function validatePinnedRelativePath(relativePath) {
|
|
|
27
27
|
relativePath.startsWith("..\\")) {
|
|
28
28
|
throw new FsSafeError("invalid-path", "relative path must not escape root");
|
|
29
29
|
}
|
|
30
|
-
for (const segment of relativePath.split("/")) {
|
|
30
|
+
for (const segment of relativePath.split(process.platform === "win32" ? /[\\/]/ : "/")) {
|
|
31
31
|
if (segment === "..") {
|
|
32
32
|
throw new FsSafeError("invalid-path", "relative path must not contain '..'");
|
|
33
33
|
}
|
package/dist/pinned-write.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Readable } from "node:stream";
|
|
2
2
|
import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
3
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
4
|
+
import { type CopyFileInput } from "./copy-file-input.js";
|
|
4
5
|
export type PinnedWriteInput = {
|
|
5
6
|
kind: "buffer";
|
|
6
7
|
data: string | Buffer;
|
|
@@ -8,7 +9,7 @@ export type PinnedWriteInput = {
|
|
|
8
9
|
} | {
|
|
9
10
|
kind: "stream";
|
|
10
11
|
stream: Readable;
|
|
11
|
-
};
|
|
12
|
+
} | CopyFileInput;
|
|
12
13
|
export type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
|
|
13
14
|
export type PublishedWriteIdentity = Readonly<{
|
|
14
15
|
dev: bigint;
|
|
@@ -22,10 +23,13 @@ export type PinnedWriteParams = {
|
|
|
22
23
|
mode: number;
|
|
23
24
|
sync?: boolean;
|
|
24
25
|
overwrite?: boolean;
|
|
26
|
+
assertBeforeMutation?: () => void;
|
|
27
|
+
rejectFinalSymlink?: boolean;
|
|
25
28
|
maxBytes?: number;
|
|
26
29
|
input: PinnedWriteInput;
|
|
27
30
|
rootIdentity?: FileIdentityStat;
|
|
28
31
|
onRenameIdentityMismatch?: "verify-content";
|
|
32
|
+
onPublished?: (identity: PublishedWriteIdentity) => void;
|
|
29
33
|
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
|
|
30
34
|
};
|
|
31
35
|
export declare function runPinnedWriteHelper(params: PinnedWriteParams): Promise<FileIdentityStat>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAA4F,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG7J,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAA4F,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG7J,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAa3D,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AAG7E,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,GACpC,aAAa,CAAC;AA4ClB,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,wBAAwB,CAAC,EAAE,gBAAgB,CAAC;IAC5C,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAEzD,eAAe,CAAC,EAAE,CAChB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgB/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,CA2B3B"}
|