@openclaw/fs-safe 0.18.2 → 0.20.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 +47 -0
- package/README.md +15 -5
- package/dist/advanced.d.ts +2 -0
- package/dist/advanced.js +1 -0
- package/dist/archive-durability.js +1 -1
- package/dist/archive-merge.js +1 -1
- package/dist/archive-plan.d.ts +2 -7
- package/dist/archive-read.js +9 -18
- package/dist/archive-staging.js +3 -1
- package/dist/archive-zip-entry.d.ts +11 -11
- package/dist/archive-zip-entry.js +3 -35
- package/dist/archive-zip-integrity.d.ts +2 -2
- package/dist/archive-zip-integrity.js +2 -12
- package/dist/archive-zip-loader.d.ts +7 -3
- package/dist/archive-zip-loader.js +10 -9
- package/dist/archive-zip-preflight.d.ts +2 -1
- package/dist/archive-zip-preflight.js +16 -7
- package/dist/archive.js +17 -16
- package/dist/directory-receipt.js +5 -7
- package/dist/effective-uid.js +1 -4
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +3 -2
- package/dist/file-lock-sync-root-held.d.ts +4 -11
- package/dist/file-lock-sync-root-held.js +1 -4
- package/dist/file-lock-sync-root-io.d.ts +1 -4
- 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.d.ts +4 -7
- package/dist/file-store.js +19 -21
- package/dist/guest-native-python.js +23 -31
- package/dist/guest.js +21 -12
- package/dist/json-document-store.d.ts +4 -9
- package/dist/json-durable-queue.js +2 -6
- package/dist/local-file-access.js +2 -5
- package/dist/local-file-descriptor.d.ts +2 -5
- package/dist/local-roots.d.ts +2 -7
- package/dist/move-path-cleanup.js +4 -4
- package/dist/native-binding.d.ts +18 -14
- package/dist/native-staged-symlink.d.ts +13 -0
- package/dist/native-staged-symlink.js +303 -0
- package/dist/owner-dacl-batch-worker.d.ts +1 -0
- package/dist/owner-dacl-batch-worker.js +54 -0
- package/dist/owner-dacl-batch.d.ts +5 -0
- package/dist/owner-dacl-batch.js +64 -0
- package/dist/owner-dacl.d.ts +2 -0
- package/dist/owner-dacl.js +3 -0
- package/dist/path.js +17 -1
- package/dist/permission-exec.js +3 -6
- package/dist/permissions-public.d.ts +1 -0
- package/dist/permissions-public.js +1 -0
- package/dist/pinned-mutation-admission.d.ts +0 -1
- package/dist/pinned-mutation-shared-route.d.ts +2 -8
- package/dist/pinned-open.d.ts +0 -1
- package/dist/pinned-open.js +1 -2
- package/dist/publish-copy-stage.js +4 -0
- package/dist/read-opened-file.d.ts +2 -5
- package/dist/regular-file.js +3 -3
- package/dist/replace-file-copy-fallback.d.ts +1 -2
- package/dist/root-context.js +3 -2
- package/dist/root-impl.js +0 -3
- package/dist/root-move-noreplace.d.ts +2 -7
- package/dist/root-observed-path.d.ts +0 -1
- package/dist/root-observed-path.js +0 -3
- package/dist/root-path-observation.d.ts +4 -11
- package/dist/root-path.js +7 -10
- 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 -6
- package/dist/root-write-complete-parent.d.ts +2 -0
- package/dist/root-write-complete-parent.js +1 -1
- package/dist/safe-path-segment.d.ts +1 -0
- package/dist/safe-path-segment.js +8 -2
- package/dist/secret-file.d.ts +6 -2
- package/dist/secret-file.js +1 -0
- package/dist/secure-file-windows.js +1 -5
- package/dist/secure-file.js +3 -2
- 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-stale-admission.d.ts +1 -5
- package/dist/sidecar-lock.js +5 -3
- package/dist/staged-symlink-types.d.ts +49 -0
- package/dist/staged-symlink-types.js +1 -0
- package/dist/symlink-parents.js +58 -7
- package/dist/temp-target.js +4 -2
- package/dist/temp-workspace-owner.js +4 -9
- package/dist/test-hooks.d.ts +1 -1
- package/dist/text-atomic.d.ts +2 -1
- package/dist/text-atomic.js +2 -0
- package/dist/trash.js +27 -1
- package/dist/walk.d.ts +2 -5
- package/dist/windows-owner.d.ts +0 -1
- package/dist/windows-owner.js +0 -1
- package/dist/windows-security-bridge.cs +6 -4
- package/dist/windows-security-bridge.ps1 +78 -3
- package/dist/windows-security-command.d.ts +8 -0
- package/dist/windows-security-command.js +66 -15
- package/dist/windows-security-facts.d.ts +4 -0
- package/dist/windows-security-facts.js +6 -2
- package/docs/advanced.md +3 -2
- package/docs/archive.md +10 -0
- package/docs/atomic.md +11 -3
- package/docs/contributing.md +30 -0
- package/docs/copy.md +2 -0
- package/docs/file-store.md +5 -0
- package/docs/guest.md +7 -1
- package/docs/install.md +28 -0
- package/docs/native-helper.md +11 -4
- package/docs/native.md +45 -1
- package/docs/permissions.md +66 -0
- package/docs/public-api.md +7 -1
- package/docs/root.md +10 -1
- package/docs/secret-file.md +10 -0
- package/docs/security-model.md +4 -1
- package/docs/sidecar-lock.md +2 -0
- package/docs/staged-symlink.md +123 -0
- package/docs/store.md +3 -1
- package/docs/testing.md +28 -0
- package/docs/walk.md +12 -0
- package/docs/writing.md +21 -0
- package/package.json +9 -9
|
@@ -22,6 +22,10 @@ export function publishCopyStage(params) {
|
|
|
22
22
|
// Caller authority checks can synchronously replace a parent or staged entry.
|
|
23
23
|
if (params.assertBeforeMutation)
|
|
24
24
|
assertCurrent();
|
|
25
|
+
// An observed collision needs no dispatch; errno after a link attempt remains ambiguous.
|
|
26
|
+
if (fs.lstatSync(targetPath, { throwIfNoEntry: false })) {
|
|
27
|
+
throw new FsSafeError("already-exists", "destination already exists");
|
|
28
|
+
}
|
|
25
29
|
params.onPublicationAttempt?.();
|
|
26
30
|
fs.linkSync(temporaryPath, targetPath);
|
|
27
31
|
let observerRejected = false;
|
|
@@ -7,12 +7,9 @@ export type ReadResult = {
|
|
|
7
7
|
realPath: string;
|
|
8
8
|
stat: Stats;
|
|
9
9
|
};
|
|
10
|
-
type OpenedFile = {
|
|
10
|
+
type OpenedFile = Omit<ReadResult & {
|
|
11
11
|
handle: FileHandle;
|
|
12
|
-
|
|
13
|
-
realPath: string;
|
|
14
|
-
stat: Stats;
|
|
15
|
-
};
|
|
12
|
+
}, "buffer">;
|
|
16
13
|
export declare function readOpenedFileSafely(params: {
|
|
17
14
|
opened: OpenedFile;
|
|
18
15
|
maxBytes?: number;
|
package/dist/regular-file.js
CHANGED
|
@@ -208,10 +208,10 @@ function inspectAppendPath(filePath) {
|
|
|
208
208
|
}
|
|
209
209
|
function prepareRegularAppend(filePath, rejectSymlinkParents) {
|
|
210
210
|
if (rejectSymlinkParents === true) {
|
|
211
|
-
const
|
|
211
|
+
const unresolvedDir = path.dirname(filePath);
|
|
212
212
|
assertNoSymlinkParentsSync({
|
|
213
|
-
rootDir: path.parse(
|
|
214
|
-
targetPath:
|
|
213
|
+
rootDir: path.parse(resolvePathPreservingWindowsRoot(unresolvedDir)).root,
|
|
214
|
+
targetPath: unresolvedDir,
|
|
215
215
|
allowMissing: false,
|
|
216
216
|
allowRootChildSymlink: true,
|
|
217
217
|
requireDirectories: true,
|
|
@@ -9,9 +9,8 @@ type AsyncFallbackFs = {
|
|
|
9
9
|
lstat: typeof import("node:fs/promises").lstat;
|
|
10
10
|
open: typeof import("node:fs/promises").open;
|
|
11
11
|
rm: typeof import("node:fs/promises").rm;
|
|
12
|
-
unlink: typeof import("node:fs/promises").unlink;
|
|
13
12
|
};
|
|
14
|
-
type SyncFallbackFs = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "ftruncateSync" | "lstatSync" | "openSync" | "readSync" | "rmSync" | "
|
|
13
|
+
type SyncFallbackFs = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "ftruncateSync" | "lstatSync" | "openSync" | "readSync" | "rmSync" | "writeSync">;
|
|
15
14
|
export declare function assertDestinationHardlinkPolicy(fsModule: AsyncFallbackFs, dest: string, policy?: ReplaceFileDestinationHardlinkPolicy): Promise<void>;
|
|
16
15
|
export declare function assertDestinationHardlinkPolicySync(fsModule: SyncFallbackFs, dest: string, policy?: ReplaceFileDestinationHardlinkPolicy): void;
|
|
17
16
|
export declare function copyFallbackReplace(params: {
|
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;
|
package/dist/root-impl.js
CHANGED
|
@@ -798,7 +798,6 @@ async function mkdirPathInRoot(root, params) {
|
|
|
798
798
|
const prepared = policy && resolved.relativePosix !== ""
|
|
799
799
|
? await preparePinnedWriteMutationAdmission({
|
|
800
800
|
rootReal: resolved.rootReal,
|
|
801
|
-
rootWithSep: ensureTrailingSep(resolved.rootReal),
|
|
802
801
|
rootIdentity: root.rootIdentity,
|
|
803
802
|
resolvedTargetPath: resolved.resolved,
|
|
804
803
|
originalPath: params.relativePath,
|
|
@@ -1045,10 +1044,8 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
1045
1044
|
throw err;
|
|
1046
1045
|
throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
1047
1046
|
}
|
|
1048
|
-
const rootWithSep = ensureTrailingSep(resolved.rootCanonicalPath);
|
|
1049
1047
|
return {
|
|
1050
1048
|
rootReal: resolved.rootCanonicalPath,
|
|
1051
|
-
rootWithSep,
|
|
1052
1049
|
canonicalPath: resolved.canonicalPath,
|
|
1053
1050
|
};
|
|
1054
1051
|
}
|
|
@@ -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;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type RootPathObservationKind, type RootPathObservationReceipt } from "./root-path.js";
|
|
2
2
|
import { type RootContext } from "./root-context.js";
|
|
3
3
|
export type PinnedObservedPath = {
|
|
4
|
-
rootReal: string;
|
|
5
4
|
resolved: string;
|
|
6
5
|
receipt?: RootPathObservationReceipt;
|
|
7
6
|
};
|
|
@@ -63,7 +63,6 @@ export async function resolvePinnedObservedPathInRoot(root, relativePath, kind)
|
|
|
63
63
|
// A receipt is emitted only for the straight traversal whose lexical and
|
|
64
64
|
// canonical cursors stayed identical and inside the checked boundary.
|
|
65
65
|
return {
|
|
66
|
-
rootReal: resolved.rootCanonicalPath,
|
|
67
66
|
resolved: resolved.canonicalPath,
|
|
68
67
|
receipt: observed.receipt,
|
|
69
68
|
};
|
|
@@ -71,7 +70,6 @@ export async function resolvePinnedObservedPathInRoot(root, relativePath, kind)
|
|
|
71
70
|
const relativeResolved = path.relative(resolved.rootCanonicalPath, resolved.canonicalPath);
|
|
72
71
|
if (relativeResolved === "" || relativeResolved === ".") {
|
|
73
72
|
return {
|
|
74
|
-
rootReal: resolved.rootCanonicalPath,
|
|
75
73
|
resolved: resolved.canonicalPath,
|
|
76
74
|
};
|
|
77
75
|
}
|
|
@@ -87,7 +85,6 @@ export async function resolvePinnedObservedPathInRoot(root, relativePath, kind)
|
|
|
87
85
|
if (!admittedCanonicalPath)
|
|
88
86
|
throw outsideWorkspaceError();
|
|
89
87
|
return {
|
|
90
|
-
rootReal: resolved.rootCanonicalPath,
|
|
91
88
|
resolved: admittedCanonicalPath.path,
|
|
92
89
|
};
|
|
93
90
|
}
|
|
@@ -7,14 +7,11 @@ export type RootPathObservationKind = "stat" | "directory";
|
|
|
7
7
|
export type RootPathDirectoryObservationGuard = DirectoryObservationGuard | NativeDirectoryObservationGuard;
|
|
8
8
|
export type RootPathTargetObservation = StatObservationReceipt | NativeDirectoryObservationGuard;
|
|
9
9
|
/** An exact receipt owned by one stat/list operation. Never cache it. */
|
|
10
|
-
export type RootPathObservationReceipt = {
|
|
11
|
-
kind: RootPathObservationKind;
|
|
12
|
-
rootGuard: DirectoryObservationGuard;
|
|
10
|
+
export type RootPathObservationReceipt = Pick<RootPathObservationRequest & {
|
|
13
11
|
directoryGuard: RootPathDirectoryObservationGuard;
|
|
14
|
-
directoryObserver?: NativeDirectoryObservationBackend;
|
|
15
12
|
targetPath: string;
|
|
16
13
|
target: RootPathTargetObservation;
|
|
17
|
-
}
|
|
14
|
+
}, "kind" | "rootGuard" | "directoryGuard" | "directoryObserver" | "targetPath" | "target">;
|
|
18
15
|
/** A failed initial target lookup must not discard its already admitted parent. */
|
|
19
16
|
export type RootPathParentObservationReceipt = Omit<RootPathObservationReceipt, "kind" | "target"> & {
|
|
20
17
|
kind: "stat-parent";
|
|
@@ -24,16 +21,12 @@ export type RootPathObservationRequest = {
|
|
|
24
21
|
rootGuard: DirectoryObservationGuard;
|
|
25
22
|
directoryObserver?: NativeDirectoryObservationBackend;
|
|
26
23
|
};
|
|
27
|
-
export type RootPathTraversalObservation = {
|
|
24
|
+
export type RootPathTraversalObservation = Omit<Partial<RootPathObservationReceipt> & {
|
|
28
25
|
enabled: boolean;
|
|
29
26
|
request: RootPathObservationRequest;
|
|
30
27
|
targetIndex: number;
|
|
31
28
|
directoryIndex: number;
|
|
32
|
-
|
|
33
|
-
directoryObserver?: NativeDirectoryObservationBackend;
|
|
34
|
-
targetPath?: string;
|
|
35
|
-
target?: RootPathTargetObservation;
|
|
36
|
-
};
|
|
29
|
+
}, "kind" | "rootGuard">;
|
|
37
30
|
export type RootPathObservedTraversalEntry = StatObservationReceipt | {
|
|
38
31
|
stat: fs.Stats | BigIntStats;
|
|
39
32
|
identity?: undefined;
|
package/dist/root-path.js
CHANGED
|
@@ -214,8 +214,8 @@ function finalizeLexicalResolution(context, kind) {
|
|
|
214
214
|
rootPath: context.rootPath,
|
|
215
215
|
rootCanonicalPath: context.rootCanonicalPath,
|
|
216
216
|
relativePath: relativeInsideRoot(context.rootCanonicalPath, context.state.canonicalCursor),
|
|
217
|
-
exists: kind
|
|
218
|
-
kind
|
|
217
|
+
exists: kind !== "missing",
|
|
218
|
+
kind,
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
221
|
function applyResolvedSymlinkHop(context, linkCanonical) {
|
|
@@ -374,12 +374,9 @@ function traverseRootPath(params, { mode = "native", observation: observationOut
|
|
|
374
374
|
const completeObservation = observation?.enabled === true && observation.directoryGuard !== undefined &&
|
|
375
375
|
observation.targetPath === state.canonicalCursor && observation.target !== undefined;
|
|
376
376
|
const kind = completeObservation
|
|
377
|
-
?
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
? "directory"
|
|
381
|
-
: toResolvedKind(observation.target.stat),
|
|
382
|
-
}
|
|
377
|
+
? isNativeDirectoryObservationGuard(observation.target)
|
|
378
|
+
? "directory"
|
|
379
|
+
: toResolvedKind(observation.target.stat)
|
|
383
380
|
: getPathKindSync(state.canonicalCursor, state.preserveFinalSymlink);
|
|
384
381
|
if (completeObservation && observationOutput) {
|
|
385
382
|
observationOutput.receipt = {
|
|
@@ -399,11 +396,11 @@ function getPathKindSync(absolutePath, preserveFinalSymlink) {
|
|
|
399
396
|
const stat = preserveFinalSymlink
|
|
400
397
|
? fs.lstatSync(operationPath)
|
|
401
398
|
: fs.statSync(operationPath);
|
|
402
|
-
return
|
|
399
|
+
return toResolvedKind(stat);
|
|
403
400
|
}
|
|
404
401
|
catch (error) {
|
|
405
402
|
if (isNotFoundPathError(error)) {
|
|
406
|
-
return
|
|
403
|
+
return "missing";
|
|
407
404
|
}
|
|
408
405
|
throw error;
|
|
409
406
|
}
|
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 {
|
|
@@ -204,7 +201,6 @@ export async function resolveGuardedWriteTargetInRoot(root, params) {
|
|
|
204
201
|
const relativeParent = path.relative(resolvedPath.rootReal, path.dirname(resolvedPath.resolved));
|
|
205
202
|
const prepared = await preparePinnedWriteMutationAdmission({
|
|
206
203
|
rootReal: resolvedPath.rootReal,
|
|
207
|
-
rootWithSep: resolvedPath.rootWithSep,
|
|
208
204
|
rootIdentity: root.rootIdentity,
|
|
209
205
|
resolvedTargetPath: resolvedPath.resolved,
|
|
210
206
|
originalPath: params.relativePath,
|
|
@@ -291,7 +287,6 @@ export async function resolvePinnedWriteTargetInRoot(root, relativePath, request
|
|
|
291
287
|
if (policy) {
|
|
292
288
|
({ relativeParentPath, mutationAdmission } = await preparePinnedWriteMutationAdmission({
|
|
293
289
|
rootReal,
|
|
294
|
-
rootWithSep,
|
|
295
290
|
rootIdentity: root.rootIdentity,
|
|
296
291
|
resolvedTargetPath: resolved,
|
|
297
292
|
originalPath: relativePath,
|
|
@@ -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) {
|
|
@@ -7,5 +7,6 @@ export declare function assertNoDriveRelativePathSegments(value: string, label:
|
|
|
7
7
|
export declare function trimHyphenEdges(value: string): string;
|
|
8
8
|
export declare function isSafePathSegment(segment: string, options?: SafePathSegmentOptions): boolean;
|
|
9
9
|
export declare function assertSafePathSegment(segment: string, options?: SafePathSegmentOptions): string;
|
|
10
|
+
export declare function normalizeSafePathSegment(value: string): string;
|
|
10
11
|
export declare function sanitizeSafePathSegment(value: string): string | undefined;
|
|
11
12
|
export declare function assertSafePathPrefix(prefix: string, options?: SafePathSegmentOptions): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isWindowsReservedDeviceName } from "./device-path.js";
|
|
1
2
|
import { FsSafeError } from "./errors.js";
|
|
2
3
|
const SAFE_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
|
|
3
4
|
const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
@@ -34,6 +35,8 @@ export function isSafePathSegment(segment, options = {}) {
|
|
|
34
35
|
!segment.includes("/") &&
|
|
35
36
|
!segment.includes("\\") &&
|
|
36
37
|
!segment.includes("\0") &&
|
|
38
|
+
// Segments are portable identifiers: CON.json and CON.<pid>.tmp name devices on Windows.
|
|
39
|
+
!isWindowsReservedDeviceName(segment) &&
|
|
37
40
|
(options.allowDotPrefix === true || !segment.startsWith(".")) &&
|
|
38
41
|
(options.allowDotPrefix === true
|
|
39
42
|
? SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN.test(segment)
|
|
@@ -47,13 +50,16 @@ export function assertSafePathSegment(segment, options = {}) {
|
|
|
47
50
|
}
|
|
48
51
|
return segment;
|
|
49
52
|
}
|
|
50
|
-
export function
|
|
53
|
+
export function normalizeSafePathSegment(value) {
|
|
51
54
|
const sanitized = value
|
|
52
55
|
.trim()
|
|
53
56
|
.replace(/[\\/]+/g, "-")
|
|
54
57
|
.replace(/\0/g, "")
|
|
55
58
|
.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
56
|
-
|
|
59
|
+
return trimHyphenEdges(sanitized);
|
|
60
|
+
}
|
|
61
|
+
export function sanitizeSafePathSegment(value) {
|
|
62
|
+
const trimmed = normalizeSafePathSegment(value);
|
|
57
63
|
return isSafePathSegment(trimmed, { allowDotPrefix: true }) ? trimmed : undefined;
|
|
58
64
|
}
|
|
59
65
|
export function assertSafePathPrefix(prefix, options = {}) {
|
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
|
@@ -285,6 +285,7 @@ async function materializeSecretFileAtomic(params, createOnly) {
|
|
|
285
285
|
mode,
|
|
286
286
|
verifyPosixMode: true,
|
|
287
287
|
sync: params.durable !== false,
|
|
288
|
+
strictFileSync: createOnly && params.durable === "file",
|
|
288
289
|
overwrite: !createOnly,
|
|
289
290
|
input: { kind: "buffer", data: typeof params.content === "string" ? params.content : Buffer.from(params.content) },
|
|
290
291
|
rootIdentity: { dev: parentGuard.stat.dev, ino: parentGuard.stat.ino },
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import { FsSafeError } from "./errors.js";
|
|
2
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
9
|
function isRecord(value) {
|
|
14
10
|
return typeof value === "object" && value !== null;
|
|
15
11
|
}
|
package/dist/secure-file.js
CHANGED
|
@@ -202,7 +202,8 @@ function inspectOpenedPermissions(stat, platform) {
|
|
|
202
202
|
groupReadable: isGroupReadable(bits),
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
|
-
async function assertSecurePermissions(options,
|
|
205
|
+
async function assertSecurePermissions(options, opened, fd) {
|
|
206
|
+
const { pathStat: stat, realPath, identity } = opened;
|
|
206
207
|
if (options.permissions?.allowInsecure) {
|
|
207
208
|
return undefined;
|
|
208
209
|
}
|
|
@@ -282,7 +283,7 @@ export async function readSecureFile(options) {
|
|
|
282
283
|
const opened = await openSecureHandle(options, maxBytes);
|
|
283
284
|
try {
|
|
284
285
|
assertTrustedDirs(options, opened.realPath);
|
|
285
|
-
const permissions = await assertSecurePermissions(options, opened
|
|
286
|
+
const permissions = await assertSecurePermissions(options, opened, opened.handle.fd);
|
|
286
287
|
const buffer = await readHandleWithTimeout(opened.handle, options.io?.timeoutMs, maxBytes);
|
|
287
288
|
const finalIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(opened.handle.fd, { bigint: true }), opened.identity);
|
|
288
289
|
if (!finalIdentity.isFile()) {
|
|
@@ -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>;
|
|
@@ -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/sidecar-lock.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
4
4
|
import { removeSidecarLockIfUnchanged, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
|
|
5
5
|
import { acquireSidecarLock } from "./sidecar-lock-acquire.js";
|
|
6
6
|
import { createHeldSidecarLockHandle, stopSidecarLockMonitoring } from "./sidecar-lock-handle.js";
|
|
@@ -61,7 +61,9 @@ function snapshotMatchesSync(lockPath, observed) {
|
|
|
61
61
|
(typeof fsSync.constants.O_NONBLOCK === "number" ? fsSync.constants.O_NONBLOCK : 0);
|
|
62
62
|
fd = fsSync.openSync(lockPath, openFlags);
|
|
63
63
|
const openedStat = fsSync.fstatSync(fd);
|
|
64
|
-
|
|
64
|
+
// Token-owned files can have different descriptor/path identities on VirtioFS.
|
|
65
|
+
// Require a known descriptor identity without rejecting that supported drift.
|
|
66
|
+
if (!openedStat.isFile() || !sameFileIdentityForCleanup(openedStat, openedStat)) {
|
|
65
67
|
return false;
|
|
66
68
|
}
|
|
67
69
|
if (observed.raw !== undefined && openedStat.size !== Buffer.byteLength(observed.raw)) {
|
|
@@ -69,7 +71,7 @@ function snapshotMatchesSync(lockPath, observed) {
|
|
|
69
71
|
}
|
|
70
72
|
const raw = fsSync.readFileSync(fd, "utf8");
|
|
71
73
|
const afterStat = fsSync.lstatSync(lockPath);
|
|
72
|
-
if (!afterStat.isFile() || !
|
|
74
|
+
if (!afterStat.isFile() || !sameFileIdentityForCleanup(beforeStat, afterStat)) {
|
|
73
75
|
return false;
|
|
74
76
|
}
|
|
75
77
|
return sidecarLockSnapshotMatches({ raw, payload: null, stat: afterStat }, observed);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { StagedFileReceipt } from "./staged-file-types.js";
|
|
2
|
+
/** Caller-captured admission evidence, never a same-target ownership heuristic. */
|
|
3
|
+
export type StagedSymlinkExpected = Readonly<{
|
|
4
|
+
dev: bigint;
|
|
5
|
+
ino: bigint;
|
|
6
|
+
uid: number;
|
|
7
|
+
gid: number;
|
|
8
|
+
ctimeNs: bigint;
|
|
9
|
+
target: string;
|
|
10
|
+
}>;
|
|
11
|
+
export type StagedSymlinkReceipt = StagedFileReceipt & Readonly<{
|
|
12
|
+
target: string;
|
|
13
|
+
}>;
|
|
14
|
+
export type PublishedSymlinkReceipt = Readonly<{
|
|
15
|
+
status: "published";
|
|
16
|
+
staged: StagedSymlinkReceipt;
|
|
17
|
+
basename: string;
|
|
18
|
+
overwrite: false;
|
|
19
|
+
}>;
|
|
20
|
+
export type StagedSymlinkPublication = Readonly<{
|
|
21
|
+
status: "not-published";
|
|
22
|
+
}> | PublishedSymlinkReceipt | Readonly<{
|
|
23
|
+
status: "indeterminate";
|
|
24
|
+
basename: string;
|
|
25
|
+
overwrite: false;
|
|
26
|
+
}>;
|
|
27
|
+
export type StagedSymlinkRemoval = "removed" | "name-absent" | "preserved";
|
|
28
|
+
export type StagedSymlinkCleanupReceipt = Readonly<{
|
|
29
|
+
temporaryBasename: string;
|
|
30
|
+
publication: StagedSymlinkPublication;
|
|
31
|
+
status: StagedSymlinkRemoval | "failed" | "not-needed";
|
|
32
|
+
resources: "closed" | "close-failed";
|
|
33
|
+
}>;
|
|
34
|
+
export type StagedSymlinkFailureDetails = Readonly<{
|
|
35
|
+
phase: "prepare" | "publish" | "remove-published" | "cleanup";
|
|
36
|
+
publication: StagedSymlinkPublication;
|
|
37
|
+
cleanup?: StagedSymlinkCleanupReceipt;
|
|
38
|
+
}>;
|
|
39
|
+
export interface StagedSymlink extends AsyncDisposable {
|
|
40
|
+
readonly receipt: StagedSymlinkReceipt;
|
|
41
|
+
assertCurrent(): Promise<void>;
|
|
42
|
+
publish(basename: string): Promise<PublishedSymlinkReceipt>;
|
|
43
|
+
/** Check the published name against the still-retained original symlink. */
|
|
44
|
+
assertPublished(): Promise<void>;
|
|
45
|
+
/** Explicit recovery only. Never removes an observed foreign replacement. */
|
|
46
|
+
removePublished(): Promise<StagedSymlinkRemoval>;
|
|
47
|
+
/** Closes the owner; only an unattempted, still-owned stage is removed. */
|
|
48
|
+
cleanup(): Promise<StagedSymlinkCleanupReceipt>;
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|