@openclaw/fs-safe 0.19.0 → 0.21.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 +50 -0
- package/README.md +24 -6
- package/dist/advanced.d.ts +4 -0
- package/dist/advanced.js +2 -0
- package/dist/archive-plan.d.ts +2 -7
- package/dist/archive-read.js +9 -18
- 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/atomic.d.ts +1 -1
- 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.js +1 -4
- package/dist/file-store.d.ts +4 -7
- package/dist/json-document-store.d.ts +4 -9
- package/dist/local-file-access.js +2 -5
- package/dist/move-path-cleanup.js +4 -4
- package/dist/native-binding.d.ts +28 -1
- 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-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-buffer.d.ts +4 -0
- package/dist/replace-file-buffer.js +36 -0
- package/dist/replace-file-copy-fallback.d.ts +3 -2
- package/dist/replace-file-copy-fallback.js +66 -38
- package/dist/replace-file-descriptor.d.ts +4 -0
- package/dist/replace-file-descriptor.js +9 -1
- package/dist/replace-file-destination.d.ts +17 -0
- package/dist/replace-file-destination.js +61 -0
- package/dist/replace-file-mutation.d.ts +26 -0
- package/dist/replace-file-mutation.js +47 -0
- package/dist/replace-file-temp-owner.d.ts +2 -2
- package/dist/replace-file-temp-owner.js +16 -4
- package/dist/replace-file-types.d.ts +55 -0
- package/dist/replace-file-types.js +1 -0
- package/dist/replace-file.d.ts +3 -55
- package/dist/replace-file.js +29 -10
- package/dist/retained-file-types.d.ts +61 -0
- package/dist/retained-file-types.js +1 -0
- package/dist/retained-file.d.ts +3 -0
- package/dist/retained-file.js +121 -0
- package/dist/root-directory-entry.d.ts +9 -0
- package/dist/root-directory-entry.js +28 -0
- package/dist/root-directory-list.d.ts +7 -1
- package/dist/root-directory-list.js +48 -23
- package/dist/root-handle-context.d.ts +4 -0
- package/dist/root-handle-context.js +12 -0
- package/dist/root-impl.d.ts +3 -3
- package/dist/root-impl.js +8 -5
- 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-walk.d.ts +19 -12
- package/dist/root-walk.js +49 -18
- package/dist/root-write-admission.js +0 -2
- package/dist/safe-path-segment.d.ts +1 -0
- package/dist/safe-path-segment.js +8 -2
- package/dist/secure-file.js +3 -2
- 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 +5 -2
- package/dist/temp-workspace-admission.js +22 -21
- package/dist/temp-workspace-child-admission.d.ts +1 -1
- package/dist/temp-workspace-child-admission.js +14 -9
- package/dist/temp-workspace-owner.js +4 -9
- package/dist/temp-workspace-ownership.d.ts +8 -0
- package/dist/temp-workspace-ownership.js +52 -0
- package/dist/test-hooks.d.ts +3 -0
- 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/watch-alias.d.ts +6 -0
- package/dist/watch-alias.js +80 -0
- package/dist/watch-hints.d.ts +8 -0
- package/dist/watch-hints.js +77 -0
- package/dist/watch-native.d.ts +32 -0
- package/dist/watch-native.js +56 -0
- package/dist/watch-scan.d.ts +24 -0
- package/dist/watch-scan.js +269 -0
- package/dist/watch-types.d.ts +58 -0
- package/dist/watch-types.js +1 -0
- package/dist/watch.d.ts +5 -0
- package/dist/watch.js +502 -0
- 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 -12
- package/dist/windows-security-facts.d.ts +3 -0
- package/dist/windows-security-facts.js +4 -0
- package/docs/advanced.md +4 -2
- package/docs/archive.md +8 -0
- package/docs/atomic.md +72 -3
- package/docs/contributing.md +35 -0
- package/docs/durability.md +7 -0
- package/docs/index.md +1 -0
- package/docs/install.md +28 -0
- package/docs/native-helper.md +14 -4
- package/docs/native.md +45 -1
- package/docs/permissions.md +66 -0
- package/docs/public-api.md +7 -1
- package/docs/retained-file.md +113 -0
- package/docs/root.md +6 -1
- 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/temp.md +24 -4
- package/docs/testing.md +88 -0
- package/docs/types.md +6 -0
- package/docs/walk.md +22 -1
- package/docs/watch.md +184 -0
- package/docs/writing.md +10 -0
- package/package.json +13 -9
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 {};
|
package/dist/symlink-parents.js
CHANGED
|
@@ -3,24 +3,46 @@ import path from "node:path";
|
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { hasNodeErrorCode, isPathRelativeEscape } from "./path.js";
|
|
5
5
|
import { assertNoWindowsPathAlias, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
|
|
6
|
+
function outsideRootError(params, root) {
|
|
7
|
+
return new Error(`${params.messagePrefix ?? "Path"} must stay under ${root}.`);
|
|
8
|
+
}
|
|
9
|
+
function pathSegments(value) {
|
|
10
|
+
return value.split(process.platform === "win32" ? /[/\\]+/ : /\/+/)
|
|
11
|
+
.filter((segment) => segment.length > 0 && segment !== ".");
|
|
12
|
+
}
|
|
13
|
+
function rawTargetSegments(root, targetPath) {
|
|
14
|
+
// Resolve only the drive/root or cwd, never the caller's dotdot segments.
|
|
15
|
+
const targetRoot = path.parse(targetPath).root;
|
|
16
|
+
const base = resolvePathPreservingWindowsRoot(targetRoot || ".");
|
|
17
|
+
const absoluteTarget = `${base}${path.sep}${targetPath.slice(targetRoot.length)}`;
|
|
18
|
+
const rootSegments = pathSegments(root);
|
|
19
|
+
const targetSegments = pathSegments(absoluteTarget);
|
|
20
|
+
const fold = (segment) => process.platform === "win32" ? segment.toLowerCase() : segment;
|
|
21
|
+
if (rootSegments.some((segment, index) => fold(segment) !== fold(targetSegments[index] ?? ""))) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return targetSegments.slice(rootSegments.length);
|
|
25
|
+
}
|
|
6
26
|
function resolvePathWalk(params) {
|
|
7
27
|
const rawRootDir = params.rootDir;
|
|
8
28
|
assertNoWindowsPathAlias(rawRootDir, "filesystem", "root dir uses a Windows filesystem namespace alias");
|
|
9
29
|
const rawTargetPath = params.targetPath;
|
|
10
30
|
assertNoWindowsPathAlias(rawTargetPath, "filesystem", "target path uses a Windows filesystem namespace alias");
|
|
11
31
|
const root = resolvePathPreservingWindowsRoot(rawRootDir);
|
|
12
|
-
const
|
|
13
|
-
const relative = path.relative(root,
|
|
32
|
+
const lexicalTarget = resolvePathPreservingWindowsRoot(rawTargetPath);
|
|
33
|
+
const relative = path.relative(root, lexicalTarget);
|
|
14
34
|
if (isPathRelativeEscape(relative)) {
|
|
15
35
|
if (params.allowOutsideRoot) {
|
|
16
36
|
return null;
|
|
17
37
|
}
|
|
18
|
-
throw
|
|
38
|
+
throw outsideRootError(params, root);
|
|
19
39
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
const segments = rawTargetSegments(root, rawTargetPath);
|
|
41
|
+
// A spelling outside the root must not fall back to a normalized walk that
|
|
42
|
+
// could erase a symlink before the filesystem receives the original path.
|
|
43
|
+
if (!segments)
|
|
44
|
+
throw outsideRootError(params, root);
|
|
45
|
+
return { root, segments };
|
|
24
46
|
}
|
|
25
47
|
function formatUnsafePath(params, current) {
|
|
26
48
|
return `${params.messagePrefix ?? "Path"} must not traverse symlinked directory: ${current}`;
|
|
@@ -28,18 +50,39 @@ function formatUnsafePath(params, current) {
|
|
|
28
50
|
export async function assertNoSymlinkParents(params) {
|
|
29
51
|
assertNoSymlinkParentsSync(params);
|
|
30
52
|
}
|
|
53
|
+
function isFilesystemRoot(root) {
|
|
54
|
+
return root === path.parse(root).root;
|
|
55
|
+
}
|
|
31
56
|
export function assertNoSymlinkParentsSync(params) {
|
|
32
57
|
const walk = resolvePathWalk(params);
|
|
33
58
|
if (!walk) {
|
|
34
59
|
return;
|
|
35
60
|
}
|
|
36
61
|
let current = walk.root;
|
|
62
|
+
// `..` may only undo a real directory this walk already lstat'd.
|
|
63
|
+
const walked = [];
|
|
37
64
|
for (const [index, segment] of walk.segments.entries()) {
|
|
65
|
+
if (segment === "..") {
|
|
66
|
+
const top = walked[walked.length - 1];
|
|
67
|
+
if (top?.kind === "dir") {
|
|
68
|
+
walked.pop();
|
|
69
|
+
current = walked[walked.length - 1]?.path ?? walk.root;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (top?.kind === "symlink") {
|
|
73
|
+
throw new Error(formatUnsafePath(params, top.path));
|
|
74
|
+
}
|
|
75
|
+
if (!isFilesystemRoot(walk.root)) {
|
|
76
|
+
throw outsideRootError(params, walk.root);
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
38
80
|
current = path.join(current, segment);
|
|
39
81
|
try {
|
|
40
82
|
const stat = fsSync.lstatSync(current);
|
|
41
83
|
if (stat.isSymbolicLink()) {
|
|
42
84
|
if (params.allowRootChildSymlink && path.dirname(current) === walk.root) {
|
|
85
|
+
walked.push({ path: current, kind: "symlink" });
|
|
43
86
|
continue;
|
|
44
87
|
}
|
|
45
88
|
throw new Error(formatUnsafePath(params, current));
|
|
@@ -47,9 +90,17 @@ export function assertNoSymlinkParentsSync(params) {
|
|
|
47
90
|
if ((params.requireDirectories || index < walk.segments.length - 1) && !stat.isDirectory()) {
|
|
48
91
|
throw new FsSafeError("not-file", `${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
|
|
49
92
|
}
|
|
93
|
+
if (stat.isDirectory()) {
|
|
94
|
+
walked.push({ path: current, kind: "dir" });
|
|
95
|
+
}
|
|
50
96
|
}
|
|
51
97
|
catch (err) {
|
|
52
98
|
if (hasNodeErrorCode(err, "ENOENT") && params.allowMissing !== false) {
|
|
99
|
+
// Win32 can cancel a nonexistent component before filesystem lookup.
|
|
100
|
+
// Returning early would leave later, reachable symlinks unchecked.
|
|
101
|
+
if (process.platform === "win32" && walk.segments.slice(index + 1).includes("..")) {
|
|
102
|
+
throw new FsSafeError("invalid-path", `${params.messagePrefix ?? "Path"} must not cancel a missing directory: ${current}`);
|
|
103
|
+
}
|
|
53
104
|
return;
|
|
54
105
|
}
|
|
55
106
|
throw err;
|
package/dist/temp-target.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { suffixWindowsReservedDeviceName } from "./filename.js";
|
|
6
6
|
import { sameFileIdentityForCleanup } from "./file-identity.js";
|
|
7
|
-
import { assertSafePathSegment,
|
|
7
|
+
import { assertSafePathSegment, isSafePathSegment, normalizeSafePathSegment, trimHyphenEdges, } from "./safe-path-segment.js";
|
|
8
8
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
9
9
|
import { hasNodeErrorCode } from "./path.js";
|
|
10
10
|
import { registerTempPathForExit } from "./temp-cleanup.js";
|
|
@@ -60,7 +60,10 @@ function sanitizeExtension(extension) {
|
|
|
60
60
|
return token ? `.${token}` : "";
|
|
61
61
|
}
|
|
62
62
|
export function sanitizeTempFileName(fileName) {
|
|
63
|
-
|
|
63
|
+
// Suffix reserved stems before admission so CON.txt stays CON_.txt.
|
|
64
|
+
const suffixed = suffixWindowsReservedDeviceName(normalizeSafePathSegment(path.basename(fileName)));
|
|
65
|
+
// The suffix only inserts "_", so this value is already normalized.
|
|
66
|
+
return isSafePathSegment(suffixed, { allowDotPrefix: true }) ? suffixed : "download.bin";
|
|
64
67
|
}
|
|
65
68
|
export function buildRandomTempFilePath(params) {
|
|
66
69
|
const rootDir = resolveTempRoot(params.rootDir);
|
|
@@ -36,20 +36,21 @@ function copyExactDirectoryObservation(stat) {
|
|
|
36
36
|
dev: stat.dev,
|
|
37
37
|
ino: stat.ino,
|
|
38
38
|
uid: stat.uid,
|
|
39
|
+
gid: stat.gid,
|
|
39
40
|
mode: stat.mode,
|
|
40
41
|
directory: stat.isDirectory(),
|
|
41
42
|
symbolicLink: stat.isSymbolicLink(),
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
|
-
function snapshotFromExactObservation(dir, uid, realPath, stat) {
|
|
45
|
+
function snapshotFromExactObservation(dir, uid, realPath, stat, privateDirectory = false) {
|
|
45
46
|
if (stat.symbolicLink || !stat.directory) {
|
|
46
47
|
throw new FsSafeError("not-file", "temp workspace root component must be a real directory");
|
|
47
48
|
}
|
|
48
|
-
assertTrustedTempWorkspaceDirectory(stat, uid);
|
|
49
|
+
assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory);
|
|
49
50
|
const identity = Object.freeze({ dev: stat.dev, ino: stat.ino });
|
|
50
51
|
// Retain copied immutable scalars, never a mutable Stats object supplied by
|
|
51
52
|
// an observation hook. Every later permission check uses a fresh snapshot.
|
|
52
|
-
return Object.freeze({ dir, identity, numericIdentity: safeNumericIdentity(stat), realPath });
|
|
53
|
+
return Object.freeze({ dir, identity, numericIdentity: safeNumericIdentity(stat), realPath, privateDirectory });
|
|
53
54
|
}
|
|
54
55
|
function canonicalTempWorkspacePath(dir) {
|
|
55
56
|
assertNoWindowsPathAlias(dir, "filesystem");
|
|
@@ -57,10 +58,10 @@ function canonicalTempWorkspacePath(dir) {
|
|
|
57
58
|
assertNoWindowsPathAlias(canonical, "filesystem");
|
|
58
59
|
return canonical;
|
|
59
60
|
}
|
|
60
|
-
function snapshot(dir, uid, realPath = canonicalTempWorkspacePath(dir)) {
|
|
61
|
+
function snapshot(dir, uid, realPath = canonicalTempWorkspacePath(dir), privateDirectory = false) {
|
|
61
62
|
const stat = inspectDirectoryIdentitySync(dir);
|
|
62
63
|
const observation = copyExactDirectoryObservation(stat);
|
|
63
|
-
return { entry: snapshotFromExactObservation(dir, uid, realPath, observation), stat };
|
|
64
|
+
return { entry: snapshotFromExactObservation(dir, uid, realPath, observation, privateDirectory), stat };
|
|
64
65
|
}
|
|
65
66
|
function hasCompleteExactIdentity(stat) {
|
|
66
67
|
return typeof stat.dev === "bigint" && typeof stat.ino === "bigint" &&
|
|
@@ -105,16 +106,16 @@ function inspectSnapshotIdentity(entry) {
|
|
|
105
106
|
}
|
|
106
107
|
function assertSnapshot(entry, uid) {
|
|
107
108
|
const stat = inspectSnapshotIdentity(entry);
|
|
108
|
-
assertTrustedTempWorkspaceDirectory(stat, uid);
|
|
109
|
+
assertTrustedTempWorkspaceDirectory(stat, uid, entry.privateDirectory);
|
|
109
110
|
assertCanonicalRoot(entry);
|
|
110
111
|
}
|
|
111
|
-
function snapshotMissingRootComponent(parent, dir, ownerUid) {
|
|
112
|
+
function snapshotMissingRootComponent(parent, dir, ownerUid, privateDirectory) {
|
|
112
113
|
if (parent.dir !== parent.realPath) {
|
|
113
114
|
assertSnapshot(parent, ownerUid);
|
|
114
|
-
return snapshot(dir, ownerUid);
|
|
115
|
+
return snapshot(dir, ownerUid, undefined, privateDirectory);
|
|
115
116
|
}
|
|
116
117
|
const current = inspectSnapshotIdentity(parent);
|
|
117
|
-
assertTrustedTempWorkspaceDirectory(current, ownerUid);
|
|
118
|
+
assertTrustedTempWorkspaceDirectory(current, ownerUid, parent.privateDirectory);
|
|
118
119
|
let realPath;
|
|
119
120
|
try {
|
|
120
121
|
realPath = canonicalTempWorkspacePath(dir);
|
|
@@ -126,16 +127,16 @@ function snapshotMissingRootComponent(parent, dir, ownerUid) {
|
|
|
126
127
|
}
|
|
127
128
|
if (path.dirname(realPath) !== parent.realPath) {
|
|
128
129
|
assertCanonicalRoot(parent);
|
|
129
|
-
return snapshot(dir, ownerUid);
|
|
130
|
+
return snapshot(dir, ownerUid, undefined, privateDirectory);
|
|
130
131
|
}
|
|
131
132
|
// The child's canonical parent confirms the same parent name at this phase.
|
|
132
133
|
// Its exact non-symlink/owner observation still precedes mode initialization.
|
|
133
|
-
return snapshot(dir, ownerUid, realPath);
|
|
134
|
+
return snapshot(dir, ownerUid, realPath, privateDirectory);
|
|
134
135
|
}
|
|
135
136
|
function assertChain(chain, uid) {
|
|
136
137
|
for (const entry of chain) {
|
|
137
138
|
const current = inspectSnapshotIdentity(entry);
|
|
138
|
-
assertTrustedTempWorkspaceDirectory(current, uid);
|
|
139
|
+
assertTrustedTempWorkspaceDirectory(current, uid, entry.privateDirectory);
|
|
139
140
|
}
|
|
140
141
|
const last = chain[chain.length - 1];
|
|
141
142
|
assertCanonicalRoot(last);
|
|
@@ -158,7 +159,7 @@ function associateTempWorkspaceRoot(entry, ownerUid, descriptorFd) {
|
|
|
158
159
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
159
160
|
throw new FsSafeError("not-file", "temp workspace cleanup parent must be a real directory");
|
|
160
161
|
}
|
|
161
|
-
assertTrustedTempWorkspaceDirectory(stat, ownerUid);
|
|
162
|
+
assertTrustedTempWorkspaceDirectory(stat, ownerUid, entry.privateDirectory);
|
|
162
163
|
}
|
|
163
164
|
function discoverMissingTempWorkspaceAncestor(ancestor, missing, missingError) {
|
|
164
165
|
// The caller already classified the first ENOENT. Keep this handoff out of
|
|
@@ -219,14 +220,14 @@ function rootPlan(rootDir) {
|
|
|
219
220
|
const existingCanonicalRoot = missing.length === 0 && observedAncestor === ancestor &&
|
|
220
221
|
!initial.symbolicLink && initial.directory && hasCompleteExactIdentity(initial);
|
|
221
222
|
if (existingCanonicalRoot) {
|
|
222
|
-
const discovery = snapshotFromExactObservation(ancestor, ownerUid, ancestor, initial);
|
|
223
|
+
const discovery = snapshotFromExactObservation(ancestor, ownerUid, ancestor, initial, true);
|
|
223
224
|
return {
|
|
224
225
|
admission: canonicalRootAdmission(discovery, ownerUid),
|
|
225
226
|
missing,
|
|
226
227
|
ownerUid,
|
|
227
228
|
};
|
|
228
229
|
}
|
|
229
|
-
const chain = canonicalAncestry(ancestor).map((dir) => snapshot(dir, ownerUid, dir).entry);
|
|
230
|
+
const chain = canonicalAncestry(ancestor).map((dir) => snapshot(dir, ownerUid, dir, missing.length === 0 && dir === ancestor).entry);
|
|
230
231
|
// Missing-component creation needs a replay before its first mutation. When
|
|
231
232
|
// the complete root already exists through an alias, retain the historical
|
|
232
233
|
// guarded route because discovery and mutation use different path spellings.
|
|
@@ -286,10 +287,10 @@ function canonicalRootAdmission(discovery, ownerUid) {
|
|
|
286
287
|
const ancestry = canonicalAncestry(discovery.dir);
|
|
287
288
|
const candidate = ancestry.map((dir, index) => {
|
|
288
289
|
if (!TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY || index !== ancestry.length - 1) {
|
|
289
|
-
return snapshot(dir, ownerUid, dir).entry;
|
|
290
|
+
return snapshot(dir, ownerUid, dir, index === ancestry.length - 1 && discovery.privateDirectory).entry;
|
|
290
291
|
}
|
|
291
292
|
const current = inspectSnapshotIdentity(discovery);
|
|
292
|
-
assertTrustedTempWorkspaceDirectory(current, ownerUid);
|
|
293
|
+
assertTrustedTempWorkspaceDirectory(current, ownerUid, discovery.privateDirectory);
|
|
293
294
|
return discovery;
|
|
294
295
|
});
|
|
295
296
|
const current = candidate[candidate.length - 1];
|
|
@@ -332,7 +333,7 @@ export async function admitTempWorkspaceRoot(rootDir) {
|
|
|
332
333
|
if (admission)
|
|
333
334
|
return admission;
|
|
334
335
|
const guardedChain = chain;
|
|
335
|
-
for (const segment of missing) {
|
|
336
|
+
for (const [index, segment] of missing.entries()) {
|
|
336
337
|
const parentEntry = guardedChain[guardedChain.length - 1];
|
|
337
338
|
const parent = rootAdmission(guardedChain, ownerUid);
|
|
338
339
|
const dir = path.join(parent.dir, segment);
|
|
@@ -347,7 +348,7 @@ export async function admitTempWorkspaceRoot(rootDir) {
|
|
|
347
348
|
if (error.code !== "EEXIST")
|
|
348
349
|
throw error;
|
|
349
350
|
}
|
|
350
|
-
const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid);
|
|
351
|
+
const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid, index === missing.length - 1);
|
|
351
352
|
if (created) {
|
|
352
353
|
const modeInitialization = admitTempWorkspaceChild(dir, observed.stat, parent, 0o700);
|
|
353
354
|
if (modeInitialization)
|
|
@@ -362,7 +363,7 @@ export function admitTempWorkspaceRootSync(rootDir) {
|
|
|
362
363
|
if (admission)
|
|
363
364
|
return admission;
|
|
364
365
|
const guardedChain = chain;
|
|
365
|
-
for (const segment of missing) {
|
|
366
|
+
for (const [index, segment] of missing.entries()) {
|
|
366
367
|
const parentEntry = guardedChain[guardedChain.length - 1];
|
|
367
368
|
const parent = rootAdmission(guardedChain, ownerUid);
|
|
368
369
|
const dir = path.join(parent.dir, segment);
|
|
@@ -377,7 +378,7 @@ export function admitTempWorkspaceRootSync(rootDir) {
|
|
|
377
378
|
if (error.code !== "EEXIST")
|
|
378
379
|
throw error;
|
|
379
380
|
}
|
|
380
|
-
const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid);
|
|
381
|
+
const observed = snapshotMissingRootComponent(parentEntry, dir, ownerUid, index === missing.length - 1);
|
|
381
382
|
if (created)
|
|
382
383
|
admitTempWorkspaceChildSync(dir, observed.stat, parent, 0o700);
|
|
383
384
|
guardedChain.push(observed.entry);
|
|
@@ -14,7 +14,7 @@ export declare function projectTempWorkspaceNumericIdentity(identity: TempWorksp
|
|
|
14
14
|
export declare function inspectTempWorkspaceDescriptorIdentitySync(fd: number, expected: TempWorkspaceIdentity, numeric: TempWorkspaceNumericIdentity | undefined): TempWorkspaceIdentityStat;
|
|
15
15
|
export declare function inspectTempWorkspaceDirectoryIdentitySync(dir: string, expected: TempWorkspaceIdentity, numeric: TempWorkspaceNumericIdentity | undefined): TempWorkspaceIdentityStat;
|
|
16
16
|
export declare function validateTempWorkspaceDirMode(mode: number): void;
|
|
17
|
-
export declare function assertTrustedTempWorkspaceDirectory(stat: Pick<BigIntStats, "uid" | "mode"> | Pick<Stats, "uid" | "mode">, uid: number | undefined,
|
|
17
|
+
export declare function assertTrustedTempWorkspaceDirectory(stat: Pick<BigIntStats, "uid" | "gid" | "mode"> | Pick<Stats, "uid" | "gid" | "mode">, uid: number | undefined, privateDirectory?: boolean): void;
|
|
18
18
|
export declare function assertTempWorkspaceChildState(stat: BigIntStats | Stats, ownerUid: number | undefined): void;
|
|
19
19
|
export declare function validateInitialTempWorkspaceChild(stat: BigIntStats, ownerUid: number | undefined, mode: number): boolean;
|
|
20
20
|
export declare function childHasRequestedMode(stat: BigIntStats | Stats, mode: number): boolean;
|
|
@@ -3,6 +3,7 @@ import { inspectDirectoryIdentitySync, observeDirectoryIdentitySync } from "./di
|
|
|
3
3
|
import { pinNodeDirectoryForMode, pinNodeDirectoryForModeSync } from "./directory-mode-node.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
5
|
import { fileIdentityMismatchError, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
|
+
import { classifyTempWorkspaceOwner, warnUnmappedTempWorkspaceAncestor } from "./temp-workspace-ownership.js";
|
|
6
7
|
export const TEMP_WORKSPACE_NUMERIC_IDENTITY_REPLAY = process.platform === "linux" || process.platform === "darwin";
|
|
7
8
|
export function projectTempWorkspaceNumericIdentity(identity) {
|
|
8
9
|
const dev = Number(identity.dev);
|
|
@@ -42,16 +43,16 @@ export function validateTempWorkspaceDirMode(mode) {
|
|
|
42
43
|
throw new FsSafeError("insecure-permissions", "temp workspace must not be group/world writable");
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
export function assertTrustedTempWorkspaceDirectory(stat, uid,
|
|
46
|
+
export function assertTrustedTempWorkspaceDirectory(stat, uid, privateDirectory = false) {
|
|
46
47
|
if (uid === undefined)
|
|
47
48
|
return;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
throw new FsSafeError("not-owned", "temp workspace directory has an untrusted owner");
|
|
49
|
+
const ownership = classifyTempWorkspaceOwner(stat, uid);
|
|
50
|
+
if (ownership === "foreign" || (privateDirectory && ownership !== "user")) {
|
|
51
|
+
throw new FsSafeError("not-owned", "temp workspace directory has an untrusted owner; use a private temp root owned by the effective user under a trusted directory hierarchy");
|
|
52
52
|
}
|
|
53
|
-
//
|
|
54
|
-
//
|
|
53
|
+
// Unmapped host owners are unverifiable; trust the host directory hierarchy.
|
|
54
|
+
// Admit sticky ancestors so PrivateUsers keeps host /tmp and PrivateTmp usable.
|
|
55
|
+
// Leaf roots stay euid-owned/private; non-sticky writable ancestors are rejected.
|
|
55
56
|
const writable = typeof stat.mode === "bigint"
|
|
56
57
|
? (stat.mode & 18n) !== 0n
|
|
57
58
|
: Number.isSafeInteger(stat.mode) && stat.mode >= 0 && (stat.mode & 0o022) !== 0;
|
|
@@ -61,9 +62,13 @@ export function assertTrustedTempWorkspaceDirectory(stat, uid, child = false) {
|
|
|
61
62
|
if (typeof stat.mode !== "bigint" && (!Number.isSafeInteger(stat.mode) || stat.mode < 0)) {
|
|
62
63
|
throw new FsSafeError("insecure-permissions", "temp workspace directory permissions are invalid");
|
|
63
64
|
}
|
|
64
|
-
if (writable && (
|
|
65
|
-
throw new FsSafeError("insecure-permissions",
|
|
65
|
+
if (writable && (privateDirectory || !sticky)) {
|
|
66
|
+
throw new FsSafeError("insecure-permissions", privateDirectory
|
|
67
|
+
? "temp workspace root and child must not be group/world writable; use a private temp directory"
|
|
68
|
+
: "temp workspace ancestor is group/world writable without sticky protection");
|
|
66
69
|
}
|
|
70
|
+
if (ownership === "unmapped")
|
|
71
|
+
warnUnmappedTempWorkspaceAncestor();
|
|
67
72
|
}
|
|
68
73
|
const WINDOWS = process.platform === "win32";
|
|
69
74
|
export function assertTempWorkspaceChildState(stat, ownerUid) {
|
|
@@ -14,13 +14,6 @@ function isNativeCleanupBinding(binding) {
|
|
|
14
14
|
typeof binding.removeOwnedTreeSync === "function" &&
|
|
15
15
|
typeof binding.ownedTreeRemovalAvailable === "function";
|
|
16
16
|
}
|
|
17
|
-
function nativeRemovalError(result) {
|
|
18
|
-
if (!result.errorCode)
|
|
19
|
-
return undefined;
|
|
20
|
-
return Object.assign(new Error(result.errorMessage ?? "native owned-tree cleanup failed"), {
|
|
21
|
-
code: result.errorCode,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
17
|
export class TempWorkspaceCleanupCapability {
|
|
25
18
|
binding;
|
|
26
19
|
parent;
|
|
@@ -309,8 +302,10 @@ export class TempWorkspaceCleanupOwner {
|
|
|
309
302
|
this.#capability.assertCurrent();
|
|
310
303
|
}
|
|
311
304
|
#mapRemoval(result) {
|
|
312
|
-
|
|
313
|
-
|
|
305
|
+
if (result.errorCode) {
|
|
306
|
+
const error = Object.assign(new Error(result.errorMessage ?? "native owned-tree cleanup failed"), {
|
|
307
|
+
code: result.errorCode,
|
|
308
|
+
});
|
|
314
309
|
if (error.code === "path-mismatch")
|
|
315
310
|
return "indeterminate";
|
|
316
311
|
throw error;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type DirectoryOwner = {
|
|
2
|
+
uid: number | bigint;
|
|
3
|
+
gid: number | bigint;
|
|
4
|
+
};
|
|
5
|
+
export type TempWorkspaceOwnership = "user" | "root" | "unmapped" | "foreign";
|
|
6
|
+
export declare function classifyTempWorkspaceOwner(stat: DirectoryOwner, uid: number): TempWorkspaceOwnership;
|
|
7
|
+
export declare function warnUnmappedTempWorkspaceAncestor(): void;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
let warned = false;
|
|
3
|
+
function overflowId(kind) {
|
|
4
|
+
try {
|
|
5
|
+
const value = Number(fs.readFileSync(`/proc/sys/kernel/overflow${kind}`, "utf8").trim());
|
|
6
|
+
if (Number.isSafeInteger(value) && value >= 0 && value < 0xffffffff)
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Linux defaults when the sysctl files are unavailable in the service mount.
|
|
11
|
+
}
|
|
12
|
+
return 65534;
|
|
13
|
+
}
|
|
14
|
+
function unmappedId(kind) {
|
|
15
|
+
const mappings = fs.readFileSync(`/proc/self/${kind}_map`, "utf8").trim()
|
|
16
|
+
.split("\n").map((line) => line.trim().split(/\s+/).map(Number));
|
|
17
|
+
if (!mappings.every((row) => row.length === 3 && row.every((id) => Number.isSafeInteger(id) && id >= 0 && id <= 0xffffffff) && row[2] > 0))
|
|
18
|
+
return null;
|
|
19
|
+
if (mappings.length === 1 && mappings[0][0] === 0 &&
|
|
20
|
+
mappings[0][1] === 0 && mappings[0][2] === 0xffffffff)
|
|
21
|
+
return null;
|
|
22
|
+
const id = overflowId(kind);
|
|
23
|
+
// Recheck every admission: an explicitly mapped overflow-number ID is real ownership.
|
|
24
|
+
return mappings.some(([start, , count]) => id >= start && id < start + count) ? null : id;
|
|
25
|
+
}
|
|
26
|
+
export function classifyTempWorkspaceOwner(stat, uid) {
|
|
27
|
+
if (stat.uid === uid || stat.uid === BigInt(uid))
|
|
28
|
+
return "user";
|
|
29
|
+
if (stat.uid === 0 || stat.uid === 0n)
|
|
30
|
+
return "root";
|
|
31
|
+
if (process.platform !== "linux")
|
|
32
|
+
return "foreign";
|
|
33
|
+
try {
|
|
34
|
+
const overflowUid = unmappedId("uid");
|
|
35
|
+
const overflowGid = unmappedId("gid");
|
|
36
|
+
return overflowUid !== null && overflowGid !== null &&
|
|
37
|
+
(stat.uid === overflowUid || stat.uid === BigInt(overflowUid)) &&
|
|
38
|
+
(stat.gid === overflowGid || stat.gid === BigInt(overflowGid))
|
|
39
|
+
? "unmapped"
|
|
40
|
+
: "foreign";
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Unavailable mapping evidence cannot authorize the namespace exception.
|
|
44
|
+
return "foreign";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function warnUnmappedTempWorkspaceAncestor() {
|
|
48
|
+
if (warned)
|
|
49
|
+
return;
|
|
50
|
+
warned = true;
|
|
51
|
+
process.emitWarning("Temp workspace ancestor ownership is unverifiable in this user namespace; trusting the host directory hierarchy while enforcing ancestor modes and a process-owned private root.", { code: "FS_SAFE_UNMAPPED_TEMP_ANCESTOR", type: "FsSafeWarning" });
|
|
52
|
+
}
|
package/dist/test-hooks.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { FileHandle } from "node:fs/promises";
|
|
2
2
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
3
3
|
export type FsSafeTestHooks = {
|
|
4
|
+
beforeWatchRegistration?: (path: string) => void | Promise<void>;
|
|
5
|
+
afterWatchRegistration?: (path: string) => void | Promise<void>;
|
|
6
|
+
afterWatchBackendCreated?: (root: string, emit: (batch: import("./watch-native.js").NativeWatchBatch) => void, nativeEvent?: (path: string, flags: number) => void) => void;
|
|
4
7
|
afterPreOpenLstat?: (filePath: string) => Promise<void> | void;
|
|
5
8
|
beforeOpen?: (filePath: string, flags: number) => Promise<void> | void;
|
|
6
9
|
afterOpen?: (filePath: string, handle: FileHandle) => Promise<void> | void;
|
package/dist/text-atomic.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ReplaceFileAtomicOptions } from "./replace-file.js";
|
|
2
|
+
export type WriteTextAtomicOptions = Pick<ReplaceFileAtomicOptions, "beforeRename" | "tempPrefix"> & {
|
|
2
3
|
mode?: number;
|
|
3
4
|
dirMode?: number;
|
|
4
5
|
trailingNewline?: boolean;
|
package/dist/text-atomic.js
CHANGED
package/dist/trash.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { assertSyncDirectoryGuard, createSyncDirectoryGuard } from "./directory-guard.js";
|
|
4
5
|
import { sameFileIdentity } from "./file-identity.js";
|
|
5
6
|
import { guardedRenameSync, guardedRmSync } from "./guarded-mutation.js";
|
|
6
7
|
import { realpathSync } from "./realpath.js";
|
|
@@ -72,6 +73,22 @@ function resolveTrashTargetPath(targetPath) {
|
|
|
72
73
|
assertNoTrashPathAlias(resolvedPath, "target path");
|
|
73
74
|
return { path: resolvedPath, resolved: true };
|
|
74
75
|
}
|
|
76
|
+
function resolveTrashEntryParent(lexicalTarget, targetPath) {
|
|
77
|
+
const lexicalParent = path.dirname(lexicalTarget);
|
|
78
|
+
assertNoTrashPathAlias(lexicalParent, "target path");
|
|
79
|
+
let realParent;
|
|
80
|
+
try {
|
|
81
|
+
// The renamed name lives in this parent. rename follows intermediate
|
|
82
|
+
// symlinks, so a lexical parent inside an allowed root is not enough.
|
|
83
|
+
realParent = realpathSync.native(lexicalParent);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new Error(`Refusing to trash path outside allowed roots: ${targetPath}`);
|
|
87
|
+
}
|
|
88
|
+
const resolvedParent = path.resolve(realParent);
|
|
89
|
+
assertNoTrashPathAlias(resolvedParent, "target path");
|
|
90
|
+
return resolvedParent;
|
|
91
|
+
}
|
|
75
92
|
function assertAllowedTrashTarget(targetPath, allowedRoots) {
|
|
76
93
|
assertNoTrashPathAlias(targetPath, "target path");
|
|
77
94
|
const lexicalTarget = path.resolve(targetPath);
|
|
@@ -79,11 +96,19 @@ function assertAllowedTrashTarget(targetPath, allowedRoots) {
|
|
|
79
96
|
const stat = fs.lstatSync(lexicalTarget);
|
|
80
97
|
const resolvedTarget = resolveTrashTargetPath(targetPath);
|
|
81
98
|
const resolvedTargetPath = resolvedTarget.path;
|
|
82
|
-
const
|
|
99
|
+
const parent = createSyncDirectoryGuard(path.dirname(lexicalTarget));
|
|
100
|
+
// Admit the directory entry only when its parent really stays inside an
|
|
101
|
+
// allowed root. Do not admit it because the symlink target is inside.
|
|
102
|
+
const resolvedParent = resolveTrashEntryParent(lexicalTarget, targetPath);
|
|
103
|
+
const isAllowed = resolveAllowedTrashRoots(allowedRoots).some((root) => isSameOrChildPath(resolvedParent, root));
|
|
83
104
|
if (!isAllowed) {
|
|
84
105
|
throw new Error(`Refusing to trash path outside allowed roots: ${targetPath}`);
|
|
85
106
|
}
|
|
107
|
+
// Sync and native realpath can use different Windows short-name spellings.
|
|
108
|
+
// Recheck the retained guard around native containment instead of comparing them.
|
|
109
|
+
assertSyncDirectoryGuard(parent);
|
|
86
110
|
return {
|
|
111
|
+
parent,
|
|
87
112
|
path: lexicalTarget,
|
|
88
113
|
realPath: resolvedTargetPath,
|
|
89
114
|
realPathResolved: resolvedTarget.resolved,
|
|
@@ -91,6 +116,7 @@ function assertAllowedTrashTarget(targetPath, allowedRoots) {
|
|
|
91
116
|
};
|
|
92
117
|
}
|
|
93
118
|
function assertTrashTargetGuard(guard) {
|
|
119
|
+
assertSyncDirectoryGuard(guard.parent);
|
|
94
120
|
const stat = fs.lstatSync(guard.path);
|
|
95
121
|
if (!sameFileIdentity(stat, guard.stat)) {
|
|
96
122
|
throw new Error(`Refusing to trash path after it changed: ${guard.path}`);
|
package/dist/walk.d.ts
CHANGED
|
@@ -20,12 +20,9 @@ export type AsyncWalkDirectoryOptions = Omit<WalkDirectoryOptions, "include" | "
|
|
|
20
20
|
include?: (entry: WalkDirectoryEntry) => boolean | Promise<boolean>;
|
|
21
21
|
descend?: (entry: WalkDirectoryEntry) => boolean | Promise<boolean>;
|
|
22
22
|
};
|
|
23
|
-
export type WalkDirectoryFailure = {
|
|
24
|
-
path: string;
|
|
25
|
-
relativePath: string;
|
|
26
|
-
depth: number;
|
|
23
|
+
export type WalkDirectoryFailure = Pick<WalkDirectoryEntry & {
|
|
27
24
|
error: unknown;
|
|
28
|
-
}
|
|
25
|
+
}, "path" | "relativePath" | "depth" | "error">;
|
|
29
26
|
export type WalkDirectoryResult = {
|
|
30
27
|
entries: WalkDirectoryEntry[];
|
|
31
28
|
scannedEntryCount: number;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type RootContext } from "./root-context.js";
|
|
2
|
+
import type { NativeWatchBatch } from "./watch-native.js";
|
|
3
|
+
import type { WatchSnapshot } from "./watch-scan.js";
|
|
4
|
+
import type { WatchChange, WatchScope } from "./watch-types.js";
|
|
5
|
+
/** Resolve native spelling aliases without treating case folding as identity. */
|
|
6
|
+
export declare function admittedNativeChanges(root: RootContext, scopes: readonly WatchScope[], before: WatchSnapshot | undefined, after: WatchSnapshot, batch: NativeWatchBatch, signal: AbortSignal, limit: number): Promise<WatchChange[] | undefined>;
|