@openclaw/fs-safe 0.18.1 → 0.19.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 +28 -0
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.js +1 -1
- package/dist/archive-durability.js +1 -1
- package/dist/archive-merge.js +1 -2
- package/dist/archive-staging.d.ts +1 -2
- package/dist/archive-staging.js +3 -3
- package/dist/archive-zip-preflight.d.ts +0 -2
- package/dist/archive-zip-preflight.js +0 -1
- package/dist/archive.d.ts +6 -3
- package/dist/archive.js +5 -3
- package/dist/directory-guard.d.ts +0 -1
- package/dist/directory-guard.js +0 -3
- package/dist/directory-mode-node.d.ts +20 -2
- package/dist/directory-mode-node.js +77 -1
- package/dist/file-lock-sync-acquisition.js +1 -1
- package/dist/file-lock-sync-root-held.d.ts +4 -11
- package/dist/file-lock-sync-root-io.d.ts +1 -4
- package/dist/file-lock-sync-root-options.d.ts +2 -10
- package/dist/file-lock-sync-root.d.ts +2 -4
- package/dist/file-store-boundary.d.ts +3 -7
- package/dist/file-store-boundary.js +7 -10
- package/dist/file-store-prune.js +3 -2
- package/dist/file-store.js +19 -21
- package/dist/guest-native-python.js +23 -31
- package/dist/guest.js +21 -12
- package/dist/json-durable-queue-ownership.d.ts +1 -5
- package/dist/json-durable-queue.d.ts +0 -1
- package/dist/json-durable-queue.js +2 -7
- package/dist/local-file-descriptor.d.ts +2 -5
- package/dist/local-roots.d.ts +2 -7
- package/dist/native-binding.d.ts +12 -13
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permissions-public.d.ts +2 -1
- package/dist/permissions-windows.d.ts +3 -6
- package/dist/permissions.d.ts +3 -10
- package/dist/permissions.js +0 -1
- package/dist/pinned-mutation-shared-route.d.ts +2 -8
- package/dist/pinned-open.d.ts +1 -0
- package/dist/pinned-open.js +1 -1
- package/dist/replace-directory.js +5 -17
- package/dist/replace-file-copy-fallback.d.ts +1 -8
- package/dist/replace-file-descriptor.d.ts +3 -11
- package/dist/replace-file-descriptor.js +1 -1
- package/dist/retained-directory-replacement.d.ts +1 -0
- package/dist/retained-directory-replacement.js +1 -1
- package/dist/root-context.js +3 -2
- package/dist/root-file-final-admission.d.ts +1 -1
- package/dist/root-file-final-admission.js +1 -6
- package/dist/root-file.js +2 -5
- package/dist/root-move-noreplace.d.ts +2 -7
- package/dist/root-paths.d.ts +2 -6
- package/dist/root-remove-identity.d.ts +1 -3
- package/dist/root-walk.js +8 -3
- package/dist/root-write-admission.js +1 -4
- package/dist/root-write-complete-parent.d.ts +2 -0
- package/dist/root-write-complete-parent.js +1 -1
- package/dist/secret-file.d.ts +6 -2
- package/dist/secret-file.js +12 -16
- package/dist/secret-read-policy.js +1 -1
- package/dist/secure-file-windows.js +5 -14
- package/dist/secure-temp-dir.js +2 -7
- package/dist/sidecar-lock-acquire.js +1 -1
- package/dist/sidecar-lock-admission-parser.d.ts +1 -2
- package/dist/sidecar-lock-handle.d.ts +2 -8
- package/dist/sidecar-lock-policy.d.ts +2 -7
- package/dist/sidecar-lock-reclaim.js +5 -6
- package/dist/sidecar-lock-stale-admission.d.ts +1 -5
- package/dist/stat-observation.js +3 -10
- package/dist/strict-file-identity.d.ts +2 -0
- package/dist/strict-file-identity.js +6 -6
- package/dist/temp-target.js +3 -8
- package/dist/temp-workspace-admission.js +6 -12
- package/dist/temp-workspace-child-admission.js +2 -8
- package/dist/temp-workspace-descriptor.js +1 -2
- package/dist/test-hooks.d.ts +1 -1
- package/dist/windows-owner.d.ts +3 -6
- package/dist/windows-security-bridge.cs +0 -2
- package/dist/windows-security-command.js +1 -4
- package/dist/windows-security-facts.d.ts +1 -0
- package/dist/windows-security-facts.js +2 -2
- package/docs/archive.md +2 -0
- package/docs/copy.md +2 -0
- package/docs/file-store.md +5 -0
- package/docs/guest.md +7 -1
- package/docs/native-helper.md +6 -0
- package/docs/root.md +10 -1
- package/docs/secret-file.md +19 -0
- package/docs/sidecar-lock.md +2 -0
- package/docs/walk.md +12 -0
- package/docs/writing.md +11 -0
- package/package.json +8 -8
- package/dist/directory-mode-owner.d.ts +0 -20
- package/dist/directory-mode-owner.js +0 -78
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import syncFs, { type BigIntStats } from "node:fs";
|
|
2
2
|
import fs, { type FileHandle } from "node:fs/promises";
|
|
3
|
-
import { type DirectoryModeOwner } from "./directory-mode-
|
|
3
|
+
import { type DirectoryModeOwner } from "./directory-mode-node.js";
|
|
4
4
|
type AsyncTempFileSystem = Pick<typeof fs, "lstat" | "open" | "writeFile">;
|
|
5
5
|
type SyncTempFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "lstatSync" | "openSync" | "writeFileSync">;
|
|
6
6
|
export type SyncFchmod = (fd: number, mode: number) => void;
|
|
@@ -12,11 +12,8 @@ export declare function pinDirectoryForMode(params: {
|
|
|
12
12
|
/** Compatibility for best-effort directory modes; admission and close still fail closed. */
|
|
13
13
|
ignoreChmodError?: boolean;
|
|
14
14
|
}): Promise<DirectoryModeOwner | undefined>;
|
|
15
|
-
export declare function applyDirectoryMode(params: {
|
|
16
|
-
fsModule: AsyncTempFileSystem;
|
|
17
|
-
dirPath: string;
|
|
15
|
+
export declare function applyDirectoryMode(params: Parameters<typeof pinDirectoryForMode>[0] & {
|
|
18
16
|
mode: number;
|
|
19
|
-
ignoreChmodError?: boolean;
|
|
20
17
|
}): Promise<void>;
|
|
21
18
|
export declare function applyDirectoryModeSync(params: {
|
|
22
19
|
fsModule: SyncTempFileSystem;
|
|
@@ -35,14 +32,9 @@ export declare function writeTempFile(params: {
|
|
|
35
32
|
handle: FileHandle;
|
|
36
33
|
identity: BigIntStats;
|
|
37
34
|
}>;
|
|
38
|
-
export declare function writeTempFileSync(params: {
|
|
35
|
+
export declare function writeTempFileSync(params: Omit<Parameters<typeof writeTempFile>[0], "fsModule"> & {
|
|
39
36
|
fsModule: SyncTempFileSystem;
|
|
40
|
-
tempPath: string;
|
|
41
|
-
content: string | Uint8Array;
|
|
42
|
-
mode: number;
|
|
43
37
|
fchmodSync?: SyncFchmod;
|
|
44
|
-
sync: boolean;
|
|
45
|
-
onIdentity?: (identity: BigIntStats) => void;
|
|
46
38
|
}): {
|
|
47
39
|
fd: number;
|
|
48
40
|
identity: BigIntStats;
|
|
@@ -3,7 +3,7 @@ import syncFs, {} from "node:fs";
|
|
|
3
3
|
import fs, {} from "node:fs/promises";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
|
-
import { ownDirectoryMode } from "./directory-mode-
|
|
6
|
+
import { ownDirectoryMode } from "./directory-mode-node.js";
|
|
7
7
|
export async function syncDirectoryBestEffort(fsModule, dirPath) {
|
|
8
8
|
let handle;
|
|
9
9
|
try {
|
|
@@ -6,6 +6,7 @@ export type RetainedReplacementParent = Readonly<{
|
|
|
6
6
|
realPath: string;
|
|
7
7
|
identity: Pick<BigIntStats, "dev" | "ino">;
|
|
8
8
|
}>;
|
|
9
|
+
export declare function copyOperationalCode(target: Error, source: unknown): void;
|
|
9
10
|
export declare class RetainedDirectoryReplacement {
|
|
10
11
|
#private;
|
|
11
12
|
private constructor();
|
|
@@ -7,7 +7,7 @@ function helperUnavailable(message, cause) {
|
|
|
7
7
|
...(cause instanceof Error ? { cause } : {}),
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
|
-
function copyOperationalCode(target, source) {
|
|
10
|
+
export function copyOperationalCode(target, source) {
|
|
11
11
|
const code = source?.code;
|
|
12
12
|
if (typeof code === "string") {
|
|
13
13
|
Object.defineProperty(target, "code", { configurable: true, value: code });
|
package/dist/root-context.js
CHANGED
|
@@ -107,7 +107,7 @@ export function rootRelativeReadPath(root, filePath) {
|
|
|
107
107
|
resolveCandidateRoot: base === root.rootDir && root.rootDir !== root.rootReal,
|
|
108
108
|
});
|
|
109
109
|
if (admitted)
|
|
110
|
-
return admitted.relativePath
|
|
110
|
+
return `.${path.sep}${admitted.relativePath}`;
|
|
111
111
|
continue;
|
|
112
112
|
}
|
|
113
113
|
const prefix = ensureTrailingSep(base);
|
|
@@ -116,7 +116,8 @@ export function rootRelativeReadPath(root, filePath) {
|
|
|
116
116
|
let start = prefix.length;
|
|
117
117
|
while (raw[start] === path.sep)
|
|
118
118
|
start += 1;
|
|
119
|
-
|
|
119
|
+
// An admitted absolute tail stays literal, including a leading home marker.
|
|
120
|
+
return `.${path.sep}${raw.slice(start)}`;
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
return raw;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import type
|
|
2
|
+
import { type PinnedOpenSyncFinalAdmission } from "./pinned-open.js";
|
|
3
3
|
export type RootFileAdmissionFs = Pick<typeof fs, "lstatSync" | "realpathSync">;
|
|
4
4
|
type ExactIdentity = Readonly<{
|
|
5
5
|
dev: bigint;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { isExpectedPathError } from "./pinned-open.js";
|
|
3
4
|
import { realpathSync } from "./realpath.js";
|
|
4
5
|
import { admitPathInsideRoot } from "./root-boundary.js";
|
|
5
6
|
import { hardlinkedPathNotAllowedError } from "./root-errors.js";
|
|
@@ -8,12 +9,6 @@ import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-pa
|
|
|
8
9
|
function rootIdentityMismatch(cause) {
|
|
9
10
|
return new FsSafeError("path-mismatch", "canonical root directory identity changed or could not be verified", cause === undefined ? {} : { cause });
|
|
10
11
|
}
|
|
11
|
-
function isExpectedPathError(error) {
|
|
12
|
-
const code = typeof error === "object" && error !== null && "code" in error
|
|
13
|
-
? String(error.code)
|
|
14
|
-
: "";
|
|
15
|
-
return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
|
|
16
|
-
}
|
|
17
12
|
function inspectCanonicalRoot(ioFs, rootPath, expected) {
|
|
18
13
|
try {
|
|
19
14
|
assertNoWindowsPathAlias(rootPath);
|
package/dist/root-file.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveRootPathSyncWithCanonicalRootObservation, resolveRootPathWithCanonicalRootObservation, } from "./root-path.js";
|
|
4
4
|
import { readSymlinkResolution } from "./root-symlink-policy.js";
|
|
5
|
-
import { openPinnedFileSync, } from "./pinned-open.js";
|
|
5
|
+
import { isExpectedPathError, openPinnedFileSync, } from "./pinned-open.js";
|
|
6
6
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
7
7
|
import { FsSafeError } from "./errors.js";
|
|
8
8
|
import { createRootFileFinalAdmission, observeCanonicalRoot, } from "./root-file-final-admission.js";
|
|
@@ -198,10 +198,7 @@ function snapshotAsyncResolution(params, absolutePath) {
|
|
|
198
198
|
function toRootObservationError(error) {
|
|
199
199
|
if (error instanceof FsSafeError)
|
|
200
200
|
return toBoundaryValidationError(error);
|
|
201
|
-
|
|
202
|
-
? String(error.code)
|
|
203
|
-
: "";
|
|
204
|
-
if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP") {
|
|
201
|
+
if (isExpectedPathError(error)) {
|
|
205
202
|
return { ok: false, reason: "path", error };
|
|
206
203
|
}
|
|
207
204
|
return { ok: false, reason: "io", error };
|
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { type BigIntStats, type Stats } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import type { RootMoveOptions } from "./root-options.js";
|
|
3
3
|
import { type RootContext } from "./root-context.js";
|
|
4
|
-
import { type MutationSymlinkPolicy } from "./root-symlink-policy.js";
|
|
5
4
|
export declare function admitMoveSourceStat<T extends Stats | BigIntStats>(stat: T, overwrite?: boolean): T;
|
|
6
|
-
export declare function movePathNoReplaceNative(root: RootContext, params: {
|
|
7
|
-
assertBeforeMutation?: () => void;
|
|
8
|
-
denyMutations?: DenyMutationPolicy;
|
|
9
|
-
mutationSymlinks?: MutationSymlinkPolicy;
|
|
10
|
-
}, paths: {
|
|
5
|
+
export declare function movePathNoReplaceNative(root: RootContext, params: Pick<RootMoveOptions, "assertBeforeMutation" | "denyMutations" | "mutationSymlinks">, paths: {
|
|
11
6
|
sourcePath: string;
|
|
12
7
|
sourceParentPath: string;
|
|
13
8
|
targetPath: string;
|
package/dist/root-paths.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ResolvePathWithinRootParams } from "./path-scope-lexical.js";
|
|
1
2
|
import { type DirectoryResult } from "./root-directory.js";
|
|
2
3
|
export { resolvePathWithinRoot } from "./root-paths-lexical.js";
|
|
3
4
|
export { ensureDirectoryWithinRoot } from "./root-directory.js";
|
|
@@ -44,12 +45,7 @@ export type PathScope = {
|
|
|
44
45
|
mode?: number;
|
|
45
46
|
}): Promise<DirectoryResult>;
|
|
46
47
|
};
|
|
47
|
-
export declare function resolveWritablePathWithinRoot(params: {
|
|
48
|
-
rootDir: string;
|
|
49
|
-
requestedPath: string;
|
|
50
|
-
scopeLabel: string;
|
|
51
|
-
defaultFileName?: string;
|
|
52
|
-
}): Promise<{
|
|
48
|
+
export declare function resolveWritablePathWithinRoot(params: ResolvePathWithinRootParams): Promise<{
|
|
53
49
|
ok: true;
|
|
54
50
|
path: string;
|
|
55
51
|
} | {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { BigIntStats } from "node:fs";
|
|
2
2
|
type ExactDirectoryIdentity = Readonly<Pick<BigIntStats, "dev" | "ino">>;
|
|
3
|
-
export type RemovalDirectoryAssertion = Readonly<{
|
|
3
|
+
export type RemovalDirectoryAssertion = ExactDirectoryIdentity & Readonly<{
|
|
4
4
|
path: string;
|
|
5
|
-
dev: bigint;
|
|
6
|
-
ino: bigint;
|
|
7
5
|
numericDev?: number;
|
|
8
6
|
numericIno?: number;
|
|
9
7
|
platform: NodeJS.Platform;
|
package/dist/root-walk.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { expandRelativePathWithHome } from "./root-context.js";
|
|
3
4
|
import { resolveRootPath, ROOT_PATH_ALIAS_POLICIES } from "./root-path.js";
|
|
4
5
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
5
6
|
function validateBudget(name, value) {
|
|
@@ -65,9 +66,10 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
65
66
|
options.signal?.throwIfAborted();
|
|
66
67
|
let listing;
|
|
67
68
|
try {
|
|
69
|
+
const expandedDirectory = depth === 0 ? await expandRelativePathWithHome(directory) : directory;
|
|
68
70
|
const skipChildSymlinks = depth > 0 && options.symlinkPolicy === "skip";
|
|
69
71
|
const resolvedDirectory = await resolveRootPath({
|
|
70
|
-
absolutePath: path.resolve(root.rootReal,
|
|
72
|
+
absolutePath: path.resolve(root.rootReal, expandedDirectory),
|
|
71
73
|
rootPath: root.rootReal,
|
|
72
74
|
rootCanonicalPath: root.rootReal,
|
|
73
75
|
boundaryLabel: "root walk",
|
|
@@ -87,7 +89,10 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
87
89
|
.relative(root.rootReal, resolvedDirectory.canonicalPath)
|
|
88
90
|
.split(path.sep)
|
|
89
91
|
.join(path.posix.sep);
|
|
90
|
-
|
|
92
|
+
if (depth === 0 && expandedDirectory !== directory)
|
|
93
|
+
directory = listingDirectory;
|
|
94
|
+
// Resolved filesystem names are literal, not caller home-directory shorthand.
|
|
95
|
+
listing = await root.list(`./${listingDirectory}`, {
|
|
91
96
|
order: options.order ?? "sorted",
|
|
92
97
|
signal: options.signal,
|
|
93
98
|
snapshot: maxEntries === Number.POSITIVE_INFINITY,
|
|
@@ -139,7 +144,7 @@ export async function* walkRoot(root, relativePath, options) {
|
|
|
139
144
|
if (!resolved.exists) {
|
|
140
145
|
continue;
|
|
141
146
|
}
|
|
142
|
-
const target = await root.stat(path.relative(root.rootReal, resolved.canonicalPath));
|
|
147
|
+
const target = await root.stat(`./${path.relative(root.rootReal, resolved.canonicalPath)}`);
|
|
143
148
|
kind = target.isDirectory ? "directory" : target.isFile ? "file" : "other";
|
|
144
149
|
size = target.size;
|
|
145
150
|
}
|
|
@@ -14,7 +14,7 @@ import { mutationSymlinkResolution, } from "./root-symlink-policy.js";
|
|
|
14
14
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
15
15
|
import { realpathSync } from "./realpath.js";
|
|
16
16
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
17
|
-
import { assertPreparedRootWriteParentCurrent, } from "./root-write-complete-parent.js";
|
|
17
|
+
import { assertPreparedRootWriteParentCurrent, writeSelectionChanged, } from "./root-write-complete-parent.js";
|
|
18
18
|
export function createRootWriteSelectionForFd(selection, fd) {
|
|
19
19
|
const stat = inspectFileIdentitySync(() => fsSync.fstatSync(fd, { bigint: true }));
|
|
20
20
|
return Object.freeze({
|
|
@@ -22,9 +22,6 @@ export function createRootWriteSelectionForFd(selection, fd) {
|
|
|
22
22
|
identity: Object.freeze({ dev: stat.dev, ino: stat.ino }),
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
-
function writeSelectionChanged(cause) {
|
|
26
|
-
return new FsSafeError("path-mismatch", "write target changed during operation", errorCauseOptions(cause));
|
|
27
|
-
}
|
|
28
25
|
function inspectRegularSelectionPath(pathname, expected, follow) {
|
|
29
26
|
let observed;
|
|
30
27
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BigIntStats } from "node:fs";
|
|
2
2
|
import { type AsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
3
4
|
import type { RootContext } from "./root-context.js";
|
|
4
5
|
import type { GuardedRootWriteTarget } from "./root-write-admission.js";
|
|
5
6
|
type PreparedWriteTargetObservation = Readonly<{
|
|
@@ -36,6 +37,7 @@ export type SharedRootWriteTarget = Readonly<{
|
|
|
36
37
|
mutationAdmission: GuardedRootWriteTarget["mutationAdmission"];
|
|
37
38
|
preparedParent?: PreparedRootWriteParent;
|
|
38
39
|
}>;
|
|
40
|
+
export declare function writeSelectionChanged(cause?: unknown): FsSafeError;
|
|
39
41
|
export declare function assertPreparedRootWriteParentCurrent(prepared: PreparedRootWriteParent, verifyTarget?: boolean): void;
|
|
40
42
|
export declare function prepareSharedRootWriteTarget(root: RootContext, params: SharedRootWriteTargetParams): Promise<SharedRootWriteTarget>;
|
|
41
43
|
export {};
|
|
@@ -12,7 +12,7 @@ import { errorCauseOptions } from "./root-errors.js";
|
|
|
12
12
|
import { canReuseParentWithMutationAssertion } from "./root-write-lock-binding.js";
|
|
13
13
|
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
14
14
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
15
|
-
function writeSelectionChanged(cause) {
|
|
15
|
+
export function writeSelectionChanged(cause) {
|
|
16
16
|
return new FsSafeError("path-mismatch", "write target changed during operation", errorCauseOptions(cause));
|
|
17
17
|
}
|
|
18
18
|
function ordinarySharedWriteRoute(root, relativePath, operationTargetPath) {
|
package/dist/secret-file.d.ts
CHANGED
|
@@ -13,7 +13,11 @@ type SecretFileWriteParams = {
|
|
|
13
13
|
dirMode?: number;
|
|
14
14
|
durable?: boolean;
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
type SecretFileCreateParams = Omit<SecretFileWriteParams, "durable"> & {
|
|
17
|
+
/** "file" requires file synchronization; directory synchronization remains best effort. */
|
|
18
|
+
durable?: boolean | "file";
|
|
19
|
+
};
|
|
20
|
+
export declare function prepareSecretFileWrite(params: Pick<SecretFileWriteParams, "rootDir" | "filePath" | "mode" | "dirMode">): Promise<{
|
|
17
21
|
mode: number;
|
|
18
22
|
rootGuard: AsyncDirectoryGuard<BigIntStats>;
|
|
19
23
|
parentGuard: AsyncDirectoryGuard<BigIntStats>;
|
|
@@ -21,5 +25,5 @@ export declare function prepareSecretFileWrite(params: Omit<SecretFileWriteParam
|
|
|
21
25
|
finalFilePath: string;
|
|
22
26
|
}>;
|
|
23
27
|
export declare function writeSecretFileAtomic(params: SecretFileWriteParams): Promise<void>;
|
|
24
|
-
export declare function createSecretFileAtomic(params:
|
|
28
|
+
export declare function createSecretFileAtomic(params: SecretFileCreateParams): Promise<void>;
|
|
25
29
|
export {};
|
package/dist/secret-file.js
CHANGED
|
@@ -5,8 +5,7 @@ import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
|
5
5
|
import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
6
6
|
import { readFileDescriptorBoundedSync } from "./bounded-read.js";
|
|
7
7
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
|
|
8
|
-
import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
|
|
9
|
-
import { assertOwnedDirectory } from "./directory-mode-owner.js";
|
|
8
|
+
import { pinNodeDirectoryForMode, assertOwnedDirectory } from "./directory-mode-node.js";
|
|
10
9
|
import { FsSafeError } from "./errors.js";
|
|
11
10
|
import { openPinnedFileSync } from "./pinned-open.js";
|
|
12
11
|
import { runPinnedWriteHelper } from "./pinned-write.js";
|
|
@@ -199,7 +198,11 @@ async function ensurePrivateDirectory(rootDir, targetDir, mode) {
|
|
|
199
198
|
}
|
|
200
199
|
return { rootGuard, parentGuard: targetGuard };
|
|
201
200
|
}
|
|
202
|
-
function snapshotSecretFileWriteParams(params
|
|
201
|
+
function snapshotSecretFileWriteParams(params) {
|
|
202
|
+
const rootDir = params.rootDir;
|
|
203
|
+
const filePath = params.filePath;
|
|
204
|
+
assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
205
|
+
assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
203
206
|
return {
|
|
204
207
|
rootDir,
|
|
205
208
|
filePath,
|
|
@@ -282,6 +285,7 @@ async function materializeSecretFileAtomic(params, createOnly) {
|
|
|
282
285
|
mode,
|
|
283
286
|
verifyPosixMode: true,
|
|
284
287
|
sync: params.durable !== false,
|
|
288
|
+
strictFileSync: createOnly && params.durable === "file",
|
|
285
289
|
overwrite: !createOnly,
|
|
286
290
|
input: { kind: "buffer", data: typeof params.content === "string" ? params.content : Buffer.from(params.content) },
|
|
287
291
|
rootIdentity: { dev: parentGuard.stat.dev, ino: parentGuard.stat.ino },
|
|
@@ -305,31 +309,23 @@ async function materializeSecretFileAtomic(params, createOnly) {
|
|
|
305
309
|
});
|
|
306
310
|
}
|
|
307
311
|
export async function writeSecretFileAtomic(params) {
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
311
|
-
assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
312
|
-
const ownedParams = snapshotSecretFileWriteParams(params, rootDir, filePath);
|
|
313
|
-
const canonicalPath = await secretFileWriteQueueKey(filePath);
|
|
312
|
+
const ownedParams = snapshotSecretFileWriteParams(params);
|
|
313
|
+
const canonicalPath = await secretFileWriteQueueKey(ownedParams.filePath);
|
|
314
314
|
await serializePathWrite(canonicalPath, async () => {
|
|
315
315
|
await materializeSecretFileAtomic(ownedParams, false);
|
|
316
316
|
});
|
|
317
317
|
}
|
|
318
318
|
export async function createSecretFileAtomic(params) {
|
|
319
319
|
try {
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
assertNoWindowsPathAlias(rootDir, "filesystem", "private secret root uses a Windows filesystem namespace alias");
|
|
323
|
-
assertNoWindowsPathAlias(filePath, "filesystem", "private secret path uses a Windows filesystem namespace alias");
|
|
324
|
-
const ownedParams = snapshotSecretFileWriteParams(params, rootDir, filePath);
|
|
325
|
-
const canonicalPath = await secretFileWriteQueueKey(filePath);
|
|
320
|
+
const ownedParams = snapshotSecretFileWriteParams(params);
|
|
321
|
+
const canonicalPath = await secretFileWriteQueueKey(ownedParams.filePath);
|
|
326
322
|
await serializePathWrite(canonicalPath, async () => {
|
|
327
323
|
await materializeSecretFileAtomic(ownedParams, true);
|
|
328
324
|
});
|
|
329
325
|
}
|
|
330
326
|
catch (error) {
|
|
331
327
|
if ((error instanceof FsSafeError && error.code === "already-exists") ||
|
|
332
|
-
error
|
|
328
|
+
error?.code === "EEXIST") {
|
|
333
329
|
throw new FsSafeError("secret-exists", "Private secret file already exists.", { cause: error });
|
|
334
330
|
}
|
|
335
331
|
throw error;
|
|
@@ -7,7 +7,7 @@ import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
|
7
7
|
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
8
8
|
export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
|
|
9
9
|
export function secretPathErrorCode(error) {
|
|
10
|
-
const code = error
|
|
10
|
+
const code = error?.code;
|
|
11
11
|
return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
|
|
12
12
|
}
|
|
13
13
|
export function secretReadError(code, action, label, resolvedPath, error) {
|
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { recordFileObservationFailure } from "./file-observation.js";
|
|
1
|
+
import { fileIdentityMismatchError } from "./strict-file-identity.js";
|
|
3
2
|
import { getNativeBinding } from "./native.js";
|
|
4
3
|
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
5
4
|
import { warnNativeFallback } from "./native-fallback-warning.js";
|
|
6
5
|
import { inspectWindowsDescriptorCommand } from "./windows-security-command.js";
|
|
7
|
-
import { validateSecureWindowsSecurityFacts } from "./windows-security-facts.js";
|
|
6
|
+
import { unverified as permissionUnverified, validateSecureWindowsSecurityFacts } from "./windows-security-facts.js";
|
|
8
7
|
const IDENTITY_RE = /^([0-9a-f]{8}):([0-9a-f]{16})$/;
|
|
9
8
|
const TRUSTED_OWNER_CLASSES = new Set(["current-user", "system", "administrators"]);
|
|
10
|
-
function permissionUnverified(message, cause) {
|
|
11
|
-
throw new FsSafeError("permission-unverified", message, cause === undefined ? {} : { cause });
|
|
12
|
-
}
|
|
13
|
-
function identityMismatch() {
|
|
14
|
-
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
15
|
-
recordFileObservationFailure(error, "identity");
|
|
16
|
-
throw error;
|
|
17
|
-
}
|
|
18
9
|
function isRecord(value) {
|
|
19
10
|
return typeof value === "object" && value !== null;
|
|
20
11
|
}
|
|
21
12
|
function parseIdentity(value) {
|
|
22
13
|
if (typeof value !== "string")
|
|
23
|
-
|
|
14
|
+
throw fileIdentityMismatchError();
|
|
24
15
|
const match = IDENTITY_RE.exec(value);
|
|
25
16
|
if (!match)
|
|
26
|
-
|
|
17
|
+
throw fileIdentityMismatchError();
|
|
27
18
|
return {
|
|
28
19
|
dev: BigInt(`0x${match[1]}`),
|
|
29
20
|
ino: BigInt(`0x${match[2]}`),
|
|
@@ -34,7 +25,7 @@ function inspectDescriptorResult(params, result, mechanism) {
|
|
|
34
25
|
permissionUnverified("Windows descriptor ACL facts were malformed");
|
|
35
26
|
const observed = parseIdentity(result.identity);
|
|
36
27
|
if (observed.dev !== params.identity.dev || observed.ino !== params.identity.ino) {
|
|
37
|
-
|
|
28
|
+
throw fileIdentityMismatchError();
|
|
38
29
|
}
|
|
39
30
|
const facts = validateSecureWindowsSecurityFacts(result.security);
|
|
40
31
|
return {
|
package/dist/secure-temp-dir.js
CHANGED
|
@@ -3,15 +3,10 @@ import { tmpdir as getOsTmpDir } from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { directoryEntryPath } from "./directory-entry-path.js";
|
|
5
5
|
import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
|
|
6
|
+
import { hasNodeErrorCode } from "./path.js";
|
|
6
7
|
import { assertSafePathSegment } from "./safe-path-segment.js";
|
|
7
8
|
import { assertNoWindowsPathAlias, pathForWindowsFilesystem } from "./windows-path-alias.js";
|
|
8
9
|
import { captureSecureTempRepairAdapter, repairSecureTempDirectory, secureTempDirectoryReceipt, } from "./secure-temp-repair.js";
|
|
9
|
-
function isNodeErrorWithCode(err, code) {
|
|
10
|
-
return (typeof err === "object" &&
|
|
11
|
-
err !== null &&
|
|
12
|
-
"code" in err &&
|
|
13
|
-
err.code === code);
|
|
14
|
-
}
|
|
15
10
|
export function resolveSecureTempRoot(options) {
|
|
16
11
|
const { fallbackPrefix: prefix, accessSync: suppliedAccess, lstatSync: suppliedLstat, mkdirSync: suppliedMkdir, descriptor: suppliedDescriptor, getuid: suppliedGetuid, tmpdir: suppliedTmpdir, platform: suppliedPlatform, preferredDir, skipPreferredOnWindows, warn: suppliedWarn, warningPrefix: suppliedWarningPrefix, unsafeFallbackLabel: suppliedFallbackLabel, } = options;
|
|
17
12
|
const platform = suppliedPlatform ?? process.platform;
|
|
@@ -87,7 +82,7 @@ export function resolveSecureTempRoot(options) {
|
|
|
87
82
|
candidate = lstatSync(candidatePath);
|
|
88
83
|
}
|
|
89
84
|
catch (error) {
|
|
90
|
-
return { kind:
|
|
85
|
+
return { kind: hasNodeErrorCode(error, "ENOENT") ? "missing" : "invalid", error };
|
|
91
86
|
}
|
|
92
87
|
let receipt;
|
|
93
88
|
try {
|
|
@@ -102,7 +102,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
102
102
|
await waitForRetry();
|
|
103
103
|
}
|
|
104
104
|
catch (waitError) {
|
|
105
|
-
if (waitError
|
|
105
|
+
if (waitError?.code === "file_lock_timeout")
|
|
106
106
|
throw denial;
|
|
107
107
|
throw waitError;
|
|
108
108
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Root } from "./root-impl.js";
|
|
2
2
|
import type { HeldSidecarLock } from "./sidecar-lock-admission.js";
|
|
3
3
|
import { readSidecarLockRawSnapshot, type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
|
|
4
|
-
type SidecarAdmissionRunner = {
|
|
4
|
+
export type SidecarAdmissionRunner = {
|
|
5
5
|
hasToken(): boolean;
|
|
6
6
|
run<T>(callback: () => T): T;
|
|
7
7
|
};
|
|
@@ -39,4 +39,3 @@ export declare function observeHeldSidecarParser(params: {
|
|
|
39
39
|
parserAccessor(): ((raw: string) => unknown) | undefined;
|
|
40
40
|
isTransientDenial(error: unknown): boolean;
|
|
41
41
|
}): Promise<HeldSidecarParserObservation>;
|
|
42
|
-
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
|
|
1
|
+
import type { HeldSidecarLock } from "./sidecar-lock-admission.js";
|
|
3
2
|
import type { SidecarLockHandle } from "./sidecar-lock-types.js";
|
|
4
3
|
export declare function stopSidecarLockMonitoring(held: {
|
|
5
4
|
compromiseTimer?: NodeJS.Timeout;
|
|
@@ -14,12 +13,7 @@ export declare function createSidecarLockHandle(params: {
|
|
|
14
13
|
}): SidecarLockHandle;
|
|
15
14
|
export declare function createHeldSidecarLockHandle(params: {
|
|
16
15
|
normalizedTargetPath: string;
|
|
17
|
-
held:
|
|
18
|
-
lockPath: string;
|
|
19
|
-
snapshot: SidecarLockSnapshot;
|
|
20
|
-
lockRoot?: Root;
|
|
21
|
-
parsePayload?: (raw: string) => unknown;
|
|
22
|
-
};
|
|
16
|
+
held: Pick<HeldSidecarLock, "lockPath" | "snapshot" | "lockRoot" | "parsePayload">;
|
|
23
17
|
release: (options?: {
|
|
24
18
|
retry?: boolean;
|
|
25
19
|
}) => Promise<unknown>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
1
|
+
import type { SidecarLockReclaimParams, SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
2
2
|
export declare function validateSidecarLockRetryOptions(retry: SidecarLockRetryOptions): void;
|
|
3
3
|
export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefined): void;
|
|
4
4
|
export declare function validateSidecarLockStaleMs(staleMs: number | undefined): void;
|
|
@@ -11,9 +11,4 @@ export declare function sidecarLockRetryDelay(retry: SidecarLockRetryOptions, ti
|
|
|
11
11
|
export declare const maxTransientLockDenials = 8;
|
|
12
12
|
export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
|
|
13
13
|
export declare function sidecarLockPayloadCreatedAtMs(payload: unknown): number | null;
|
|
14
|
-
export declare function defaultSidecarLockShouldReclaim(params:
|
|
15
|
-
lockPath: string;
|
|
16
|
-
payload: unknown;
|
|
17
|
-
staleMs: number;
|
|
18
|
-
nowMs: number;
|
|
19
|
-
}): Promise<boolean>;
|
|
14
|
+
export declare function defaultSidecarLockShouldReclaim(params: Pick<SidecarLockReclaimParams, "lockPath" | "payload" | "staleMs" | "nowMs">): Promise<boolean>;
|
|
@@ -357,13 +357,12 @@ export async function removeStaleSidecarLockIfAllowed(params) {
|
|
|
357
357
|
if (params.assertGuardHeld)
|
|
358
358
|
await params.assertGuardHeld();
|
|
359
359
|
params.assertAuthorized?.();
|
|
360
|
+
if (params.lockRoot) {
|
|
361
|
+
await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath), { assertBeforeMutation: params.assertAuthorized });
|
|
362
|
+
return "removed";
|
|
363
|
+
}
|
|
360
364
|
try {
|
|
361
|
-
|
|
362
|
-
await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath), { assertBeforeMutation: params.assertAuthorized });
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
await fs.rm(params.lockPath);
|
|
366
|
-
}
|
|
365
|
+
await fs.rm(params.lockPath);
|
|
367
366
|
return "removed";
|
|
368
367
|
}
|
|
369
368
|
catch (err) {
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { type SidecarReclaimGuard } from "./sidecar-lock-reclaim.js";
|
|
2
|
-
import { type SidecarLockParserState } from "./sidecar-lock-admission-parser.js";
|
|
2
|
+
import { type SidecarLockParserState, type SidecarAdmissionRunner } from "./sidecar-lock-admission-parser.js";
|
|
3
3
|
import type { Root } from "./root-impl.js";
|
|
4
4
|
import type { SidecarLockAcquireOptions } from "./sidecar-lock-types.js";
|
|
5
|
-
type SidecarAdmissionRunner = {
|
|
6
|
-
hasToken(): boolean;
|
|
7
|
-
run<T>(callback: () => T): T;
|
|
8
|
-
};
|
|
9
5
|
type StaleOptions = Pick<SidecarLockAcquireOptions<Record<string, unknown>>, "shouldReclaim" | "shouldRemoveStaleLock" | "staleRecovery">;
|
|
10
6
|
export type SidecarLockStaleOptionsState = {
|
|
11
7
|
shouldReclaimObserved?: boolean;
|
package/dist/stat-observation.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { recordFileObservationFailure } from "./file-observation.js";
|
|
3
|
-
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
4
|
-
function identityMismatch() {
|
|
5
|
-
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
6
|
-
recordFileObservationFailure(error, "identity");
|
|
7
|
-
throw error;
|
|
8
|
-
}
|
|
1
|
+
import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
9
2
|
function safeNumber(value) {
|
|
10
3
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
11
4
|
}
|
|
@@ -35,7 +28,7 @@ function observeStatSync(inspect, expected, initial, platform = process.platform
|
|
|
35
28
|
const ino = numericIno ? BigInt(rawIno) : typeof rawIno === "bigint" ? rawIno : undefined;
|
|
36
29
|
if (expected && ((dev !== undefined && dev !== expected.dev) ||
|
|
37
30
|
(ino !== undefined && ino !== expected.ino)))
|
|
38
|
-
|
|
31
|
+
throw fileIdentityMismatchError();
|
|
39
32
|
if (numericDev && numericIno) {
|
|
40
33
|
return (captureIdentity
|
|
41
34
|
? { stat, identity: expected ?? { dev: dev, ino: ino } }
|
|
@@ -47,7 +40,7 @@ function observeStatSync(inspect, expected, initial, platform = process.platform
|
|
|
47
40
|
const current = inspect(true);
|
|
48
41
|
if ((dev !== undefined && current.dev !== dev) ||
|
|
49
42
|
(ino !== undefined && current.ino !== ino))
|
|
50
|
-
|
|
43
|
+
throw fileIdentityMismatchError();
|
|
51
44
|
return current;
|
|
52
45
|
}, expected, platform);
|
|
53
46
|
return (captureIdentity
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BigIntStats } from "node:fs";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
2
3
|
type ExactFileIdentity = Pick<BigIntStats, "dev" | "ino">;
|
|
4
|
+
export declare function fileIdentityMismatchError(): FsSafeError;
|
|
3
5
|
export declare function inspectFileIdentity<T extends ExactFileIdentity>(inspect: () => T | Promise<T>, expected?: ExactFileIdentity, platform?: NodeJS.Platform): Promise<T>;
|
|
4
6
|
export declare function inspectFileIdentitySync<T extends ExactFileIdentity>(inspect: () => T, expected?: ExactFileIdentity, platform?: NodeJS.Platform, mismatch?: () => Error): T;
|
|
5
7
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
2
|
import { recordFileObservationFailure } from "./file-observation.js";
|
|
3
|
-
function
|
|
3
|
+
export function fileIdentityMismatchError() {
|
|
4
4
|
const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
|
|
5
5
|
recordFileObservationFailure(error, "identity");
|
|
6
6
|
return error;
|
|
@@ -13,20 +13,20 @@ function identityCheck(expected, platform) {
|
|
|
13
13
|
const value = stat[field];
|
|
14
14
|
// Numeric receipts cannot recover identity bits already lost to rounding.
|
|
15
15
|
if (typeof value !== "bigint")
|
|
16
|
-
throw
|
|
16
|
+
throw fileIdentityMismatchError();
|
|
17
17
|
if (platform === "win32" && value === 0n) {
|
|
18
18
|
complete = false;
|
|
19
19
|
}
|
|
20
20
|
else {
|
|
21
21
|
if (known[field] !== undefined && known[field] !== value)
|
|
22
|
-
throw
|
|
22
|
+
throw fileIdentityMismatchError();
|
|
23
23
|
known[field] = value;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
return complete;
|
|
27
27
|
};
|
|
28
28
|
if (expected && !check(expected))
|
|
29
|
-
throw
|
|
29
|
+
throw fileIdentityMismatchError();
|
|
30
30
|
return check;
|
|
31
31
|
}
|
|
32
32
|
// Retry only unknown Windows identities, retaining every known component so a
|
|
@@ -38,9 +38,9 @@ export async function inspectFileIdentity(inspect, expected, platform = process.
|
|
|
38
38
|
if (check(stat))
|
|
39
39
|
return stat;
|
|
40
40
|
}
|
|
41
|
-
throw
|
|
41
|
+
throw fileIdentityMismatchError();
|
|
42
42
|
}
|
|
43
|
-
export function inspectFileIdentitySync(inspect, expected, platform = process.platform, mismatch =
|
|
43
|
+
export function inspectFileIdentitySync(inspect, expected, platform = process.platform, mismatch = fileIdentityMismatchError) {
|
|
44
44
|
let knownDev;
|
|
45
45
|
let knownIno;
|
|
46
46
|
if (expected) {
|