@openclaw/fs-safe 0.4.6 → 0.5.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 +69 -0
- package/README.md +110 -41
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -4
- package/dist/archive-errors.d.ts +11 -0
- package/dist/archive-errors.d.ts.map +1 -0
- package/dist/archive-errors.js +16 -0
- package/dist/archive-input.d.ts +12 -0
- package/dist/archive-input.d.ts.map +1 -0
- package/dist/archive-input.js +72 -0
- package/dist/archive-kind.d.ts +1 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +18 -0
- package/dist/archive-limits.d.ts +10 -0
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +18 -0
- package/dist/archive-native.d.ts +18 -0
- package/dist/archive-native.d.ts.map +1 -0
- package/dist/archive-native.js +115 -0
- package/dist/archive-options.d.ts +21 -0
- package/dist/archive-options.d.ts.map +1 -0
- package/dist/archive-options.js +1 -0
- package/dist/archive-policy.d.ts +20 -0
- package/dist/archive-policy.d.ts.map +1 -0
- package/dist/archive-policy.js +29 -0
- package/dist/archive-read.d.ts +6 -0
- package/dist/archive-read.d.ts.map +1 -0
- package/dist/archive-read.js +200 -0
- package/dist/archive-staging.d.ts +2 -5
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +19 -10
- package/dist/archive-tar-meta.d.ts +6 -0
- package/dist/archive-tar-meta.d.ts.map +1 -0
- package/dist/archive-tar-meta.js +143 -0
- package/dist/archive-tar-runtime.d.ts +43 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -0
- package/dist/archive-tar-runtime.js +8 -0
- package/dist/archive-tar.d.ts +5 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +24 -6
- package/dist/archive-zip-entry.d.ts +15 -0
- package/dist/archive-zip-entry.d.ts.map +1 -0
- package/dist/archive-zip-entry.js +17 -0
- package/dist/archive.d.ts +7 -17
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +111 -108
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/containment.d.ts +2 -0
- package/dist/containment.d.ts.map +1 -0
- package/dist/containment.js +1 -0
- package/dist/directory-durability.d.ts +44 -0
- package/dist/directory-durability.d.ts.map +1 -0
- package/dist/directory-durability.js +299 -0
- package/dist/durability.d.ts +4 -0
- package/dist/durability.d.ts.map +1 -0
- package/dist/durability.js +3 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/file-hash.d.ts +10 -0
- package/dist/file-hash.d.ts.map +1 -0
- package/dist/file-hash.js +73 -0
- package/dist/file-lock-sync.d.ts +35 -0
- package/dist/file-lock-sync.d.ts.map +1 -0
- package/dist/file-lock-sync.js +233 -0
- package/dist/file-lock.d.ts +2 -0
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -1
- package/dist/fsync.d.ts +1 -1
- package/dist/fsync.d.ts.map +1 -1
- package/dist/fsync.js +1 -21
- package/dist/guarded-mkdir.d.ts +7 -1
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +45 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +30 -15
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +101 -17
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +79 -0
- package/dist/native-binding.d.ts.map +1 -0
- package/dist/native-binding.js +1 -0
- package/dist/native-config.d.ts +17 -0
- package/dist/native-config.d.ts.map +1 -0
- package/dist/native-config.js +71 -0
- package/dist/native-operations.d.ts +21 -0
- package/dist/native-operations.d.ts.map +1 -0
- package/dist/native-operations.js +115 -0
- package/dist/native-pinned-write.d.ts +5 -0
- package/dist/native-pinned-write.d.ts.map +1 -0
- package/dist/native-pinned-write.js +115 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +204 -0
- package/dist/output-sibling.d.ts +8 -0
- package/dist/output-sibling.d.ts.map +1 -0
- package/dist/output-sibling.js +122 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +32 -5
- package/dist/owner-dacl.d.ts +31 -0
- package/dist/owner-dacl.d.ts.map +1 -0
- package/dist/owner-dacl.js +27 -0
- package/dist/path-policy.js +1 -1
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-public.js +2 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +6 -0
- package/dist/pinned-operation.d.ts +2 -0
- package/dist/pinned-operation.d.ts.map +1 -0
- package/dist/pinned-operation.js +35 -0
- package/dist/pinned-write.d.ts +3 -16
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +20 -85
- package/dist/private-directory.d.ts +5 -0
- package/dist/private-directory.d.ts.map +1 -0
- package/dist/private-directory.js +13 -0
- package/dist/private-temp-workspace.d.ts +6 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +40 -13
- package/dist/publish-file-failure.d.ts +27 -0
- package/dist/publish-file-failure.d.ts.map +1 -0
- package/dist/publish-file-failure.js +21 -0
- package/dist/publish-file.d.ts +21 -0
- package/dist/publish-file.d.ts.map +1 -0
- package/dist/publish-file.js +351 -0
- package/dist/read-opened-file.d.ts +3 -0
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +1 -0
- package/dist/replace-file-copy-fallback.d.ts +34 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -0
- package/dist/replace-file-copy-fallback.js +339 -0
- package/dist/replace-file.d.ts +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +39 -90
- package/dist/root-context.d.ts +4 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +20 -1
- package/dist/root-impl.d.ts +5 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +128 -225
- package/dist/root-path-existing.d.ts +3 -0
- package/dist/root-path-existing.d.ts.map +1 -0
- package/dist/root-path-existing.js +69 -0
- package/dist/root-path.d.ts +1 -2
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +67 -81
- package/dist/root-walk.d.ts +41 -0
- package/dist/root-walk.d.ts.map +1 -0
- package/dist/root-walk.js +114 -0
- package/dist/root.d.ts +2 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +5 -2
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +20 -2
- package/dist/secret-read-async.d.ts +4 -0
- package/dist/secret-read-async.d.ts.map +1 -0
- package/dist/secret-read-async.js +95 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +2 -13
- package/dist/sidecar-lock-handle.d.ts +20 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -0
- package/dist/sidecar-lock-handle.js +28 -0
- package/dist/sidecar-lock-policy.d.ts +10 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -0
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +22 -6
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +106 -20
- package/dist/sidecar-lock-types.d.ts +56 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -0
- package/dist/sidecar-lock-types.js +1 -0
- package/dist/sidecar-lock.d.ts +2 -45
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +94 -55
- package/dist/temp-cleanup.d.ts +7 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +35 -5
- package/dist/temp.d.ts +2 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +3 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-permissions-native.d.ts +9 -0
- package/dist/windows-permissions-native.d.ts.map +1 -0
- package/dist/windows-permissions-native.js +31 -0
- package/docs/advanced.md +1 -1
- package/docs/archive.md +131 -13
- package/docs/atomic.md +30 -1
- package/docs/config.md +51 -21
- package/docs/durability.md +299 -0
- package/docs/errors.md +36 -6
- package/docs/filename.md +0 -0
- package/docs/index.md +11 -7
- package/docs/install.md +27 -26
- package/docs/json-store.md +18 -3
- package/docs/migrating-to-0.5.md +196 -0
- package/docs/native-helper.md +85 -0
- package/docs/native.md +134 -0
- package/docs/output.md +37 -11
- package/docs/path.md +1 -1
- package/docs/permissions.md +84 -1
- package/docs/quickstart.md +5 -2
- package/docs/reading.md +4 -4
- package/docs/root.md +31 -15
- package/docs/secret-file.md +50 -1
- package/docs/security-model.md +42 -8
- package/docs/sidecar-lock.md +92 -7
- package/docs/temp.md +24 -1
- package/docs/test-hooks.md +22 -2
- package/docs/testing.md +18 -7
- package/docs/types.md +3 -1
- package/docs/walk.md +55 -0
- package/docs/writing.md +3 -3
- package/package.json +12 -2
- package/dist/pinned-helper.d.ts +0 -9
- package/dist/pinned-helper.d.ts.map +0 -1
- package/dist/pinned-helper.js +0 -19
- package/dist/pinned-path.d.ts +0 -7
- package/dist/pinned-path.d.ts.map +0 -1
- package/dist/pinned-path.js +0 -21
- package/dist/pinned-python-config.d.ts +0 -9
- package/dist/pinned-python-config.d.ts.map +0 -1
- package/dist/pinned-python-config.js +0 -38
- package/dist/pinned-python.d.ts +0 -12
- package/dist/pinned-python.d.ts.map +0 -1
- package/dist/pinned-python.js +0 -688
- package/docs/python-helper.md +0 -103
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isNotFoundPathError } from "./path.js";
|
|
5
|
+
function isFilesystemRoot(candidate) {
|
|
6
|
+
return path.parse(candidate).root === candidate;
|
|
7
|
+
}
|
|
8
|
+
async function pathExists(targetPath) {
|
|
9
|
+
try {
|
|
10
|
+
await fsp.lstat(targetPath);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (isNotFoundPathError(error)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function resolvePathViaExistingAncestor(targetPath) {
|
|
21
|
+
const normalized = path.resolve(targetPath);
|
|
22
|
+
let cursor = normalized;
|
|
23
|
+
const missingSuffix = [];
|
|
24
|
+
while (!isFilesystemRoot(cursor) && !(await pathExists(cursor))) {
|
|
25
|
+
missingSuffix.unshift(path.basename(cursor));
|
|
26
|
+
const parent = path.dirname(cursor);
|
|
27
|
+
if (parent === cursor) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
cursor = parent;
|
|
31
|
+
}
|
|
32
|
+
if (!(await pathExists(cursor))) {
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const resolvedAncestor = path.resolve(await fsp.realpath(cursor));
|
|
37
|
+
return missingSuffix.length === 0
|
|
38
|
+
? resolvedAncestor
|
|
39
|
+
: path.resolve(resolvedAncestor, ...missingSuffix);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function resolvePathViaExistingAncestorSync(targetPath) {
|
|
46
|
+
const normalized = path.resolve(targetPath);
|
|
47
|
+
let cursor = normalized;
|
|
48
|
+
const missingSuffix = [];
|
|
49
|
+
while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
|
|
50
|
+
missingSuffix.unshift(path.basename(cursor));
|
|
51
|
+
const parent = path.dirname(cursor);
|
|
52
|
+
if (parent === cursor) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
cursor = parent;
|
|
56
|
+
}
|
|
57
|
+
if (!fs.existsSync(cursor)) {
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
|
|
62
|
+
return missingSuffix.length === 0
|
|
63
|
+
? resolvedAncestor
|
|
64
|
+
: path.resolve(resolvedAncestor, ...missingSuffix);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/root-path.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { resolvePathViaExistingAncestorSync } from "./root-path-existing.js";
|
|
1
2
|
type RootPathIntent = "read" | "write" | "create" | "delete" | "stat";
|
|
2
3
|
export type RootPathAliasPolicy = {
|
|
3
4
|
allowFinalSymlinkForUnlink?: boolean;
|
|
@@ -34,6 +35,4 @@ export type ResolvedRootPath = {
|
|
|
34
35
|
};
|
|
35
36
|
export declare function resolveRootPath(params: ResolveRootPathParams): Promise<ResolvedRootPath>;
|
|
36
37
|
export declare function resolveRootPathSync(params: ResolveRootPathParams): ResolvedRootPath;
|
|
37
|
-
export declare function resolvePathViaExistingAncestorSync(targetPath: string): string;
|
|
38
|
-
export {};
|
|
39
38
|
//# sourceMappingURL=root-path.d.ts.map
|
package/dist/root-path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root-path.d.ts","sourceRoot":"","sources":["../src/root-path.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"root-path.d.ts","sourceRoot":"","sources":["../src/root-path.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,kCAAkC,EAAE,MAAM,yBAAyB,CAAC;AAE7E,KAAK,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,wBAAwB;aACnC,MAAM;;;;aAIN,YAAY;;;;CAIJ,CAAC;AAEX,KAAK,qBAAqB,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,KAAK,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,oBAAoB,CAAC;CAC5B,CAAC;AAEF,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CA+B3B;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,gBAAgB,CA+BnF"}
|
package/dist/root-path.js
CHANGED
|
@@ -3,6 +3,8 @@ import fsp from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { isNotFoundPathError, isPathInside, isPathRelativeEscape } from "./path.js";
|
|
6
|
+
import { resolvePathViaExistingAncestor, resolvePathViaExistingAncestorSync, } from "./root-path-existing.js";
|
|
7
|
+
export { resolvePathViaExistingAncestorSync } from "./root-path-existing.js";
|
|
6
8
|
export const ROOT_PATH_ALIAS_POLICIES = {
|
|
7
9
|
strict: Object.freeze({
|
|
8
10
|
allowFinalSymlinkForUnlink: false,
|
|
@@ -80,15 +82,40 @@ function isPromiseLike(value) {
|
|
|
80
82
|
typeof value.then === "function");
|
|
81
83
|
}
|
|
82
84
|
function createLexicalTraversalState(params) {
|
|
83
|
-
const
|
|
85
|
+
const rawAbsolutePath = params.params.absolutePath;
|
|
86
|
+
const rawRelativePath = rawPathRelativeToRoot(params.rootPath, rawAbsolutePath);
|
|
87
|
+
const relative = rawRelativePath ?? path.relative(params.rootPath, params.absolutePath);
|
|
84
88
|
return {
|
|
85
|
-
segments: relative
|
|
89
|
+
segments: splitTraversalSegments(relative),
|
|
86
90
|
allowFinalSymlink: params.params.policy?.allowFinalSymlinkForUnlink === true,
|
|
87
91
|
canonicalCursor: params.rootCanonicalPath,
|
|
88
92
|
lexicalCursor: params.rootPath,
|
|
89
93
|
preserveFinalSymlink: false,
|
|
90
94
|
};
|
|
91
95
|
}
|
|
96
|
+
function splitTraversalSegments(value) {
|
|
97
|
+
return value
|
|
98
|
+
.split(process.platform === "win32" ? /[\\/]+/ : /\/+/)
|
|
99
|
+
.filter((segment) => Boolean(segment) && segment !== ".");
|
|
100
|
+
}
|
|
101
|
+
function rawPathRelativeToRoot(rootPath, candidatePath) {
|
|
102
|
+
if (!path.isAbsolute(candidatePath)) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const root = path.resolve(rootPath);
|
|
106
|
+
const candidate = process.platform === "win32"
|
|
107
|
+
? candidatePath.replaceAll("/", path.sep)
|
|
108
|
+
: candidatePath;
|
|
109
|
+
if (candidate === root) {
|
|
110
|
+
return "";
|
|
111
|
+
}
|
|
112
|
+
const rootWithSep = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
113
|
+
const candidatePrefix = candidate.slice(0, rootWithSep.length);
|
|
114
|
+
const prefixMatches = process.platform === "win32"
|
|
115
|
+
? candidatePrefix.toLowerCase() === rootWithSep.toLowerCase()
|
|
116
|
+
: candidatePrefix === rootWithSep;
|
|
117
|
+
return prefixMatches ? candidate.slice(rootWithSep.length) : undefined;
|
|
118
|
+
}
|
|
92
119
|
function assertLexicalCursorInsideBoundary(params) {
|
|
93
120
|
assertInsideBoundary({
|
|
94
121
|
boundaryLabel: params.params.boundaryLabel,
|
|
@@ -99,13 +126,15 @@ function assertLexicalCursorInsideBoundary(params) {
|
|
|
99
126
|
}
|
|
100
127
|
function applyMissingSuffixToCanonicalCursor(params) {
|
|
101
128
|
const missingSuffix = params.state.segments.slice(params.missingFromIndex);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
129
|
+
for (const segment of missingSuffix) {
|
|
130
|
+
advanceCanonicalCursorForSegment({
|
|
131
|
+
state: params.state,
|
|
132
|
+
segment,
|
|
133
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
134
|
+
params: params.params,
|
|
135
|
+
absolutePath: params.absolutePath,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
109
138
|
}
|
|
110
139
|
function advanceCanonicalCursorForSegment(params) {
|
|
111
140
|
params.state.canonicalCursor = path.resolve(params.state.canonicalCursor, params.segment);
|
|
@@ -221,11 +250,20 @@ function resolveAndApplySymlinkHop(params) {
|
|
|
221
250
|
boundaryLabel: params.boundaryLabel,
|
|
222
251
|
});
|
|
223
252
|
}
|
|
253
|
+
function applyParentTraversalStep(params) {
|
|
254
|
+
params.state.lexicalCursor = path.resolve(params.state.lexicalCursor, "..");
|
|
255
|
+
advanceCanonicalCursorForSegment({
|
|
256
|
+
state: params.state,
|
|
257
|
+
segment: "..",
|
|
258
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
259
|
+
params: params.resolveParams,
|
|
260
|
+
absolutePath: params.absolutePath,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
224
263
|
function* iterateLexicalTraversal(state) {
|
|
225
264
|
for (let idx = 0; idx < state.segments.length; idx += 1) {
|
|
226
265
|
const segment = state.segments[idx] ?? "";
|
|
227
266
|
const isLast = idx === state.segments.length - 1;
|
|
228
|
-
state.lexicalCursor = path.join(state.lexicalCursor, segment);
|
|
229
267
|
yield { idx, segment, isLast };
|
|
230
268
|
}
|
|
231
269
|
}
|
|
@@ -238,6 +276,14 @@ async function resolveRootPathLexicalAsync(params) {
|
|
|
238
276
|
absolutePath: params.absolutePath,
|
|
239
277
|
};
|
|
240
278
|
for (const { idx, segment, isLast } of iterateLexicalTraversal(state)) {
|
|
279
|
+
if (segment === "..") {
|
|
280
|
+
applyParentTraversalStep({
|
|
281
|
+
...sharedStepParams,
|
|
282
|
+
resolveParams: params.params,
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
state.lexicalCursor = path.join(state.lexicalCursor, segment);
|
|
241
287
|
const stat = await readLexicalStat({
|
|
242
288
|
...sharedStepParams,
|
|
243
289
|
missingFromIndex: idx,
|
|
@@ -265,7 +311,7 @@ async function resolveRootPathLexicalAsync(params) {
|
|
|
265
311
|
resolveLinkCanonical: (cursor) => resolveSymlinkHopPath(cursor),
|
|
266
312
|
});
|
|
267
313
|
}
|
|
268
|
-
const kind = await getPathKind(
|
|
314
|
+
const kind = await getPathKind(state.canonicalCursor, state.preserveFinalSymlink);
|
|
269
315
|
return finalizeLexicalResolution({
|
|
270
316
|
...params,
|
|
271
317
|
state,
|
|
@@ -277,6 +323,15 @@ function resolveRootPathLexicalSync(params) {
|
|
|
277
323
|
for (let idx = 0; idx < state.segments.length; idx += 1) {
|
|
278
324
|
const segment = state.segments[idx] ?? "";
|
|
279
325
|
const isLast = idx === state.segments.length - 1;
|
|
326
|
+
if (segment === "..") {
|
|
327
|
+
applyParentTraversalStep({
|
|
328
|
+
state,
|
|
329
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
330
|
+
resolveParams: params.params,
|
|
331
|
+
absolutePath: params.absolutePath,
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
280
335
|
state.lexicalCursor = path.join(state.lexicalCursor, segment);
|
|
281
336
|
const maybeStat = readLexicalStat({
|
|
282
337
|
state,
|
|
@@ -318,7 +373,7 @@ function resolveRootPathLexicalSync(params) {
|
|
|
318
373
|
throw new Error("Unexpected async symlink resolution");
|
|
319
374
|
}
|
|
320
375
|
}
|
|
321
|
-
const kind = getPathKindSync(
|
|
376
|
+
const kind = getPathKindSync(state.canonicalCursor, state.preserveFinalSymlink);
|
|
322
377
|
return finalizeLexicalResolution({
|
|
323
378
|
...params,
|
|
324
379
|
state,
|
|
@@ -434,60 +489,6 @@ function buildResolvedRootPath(params) {
|
|
|
434
489
|
kind: params.kind.kind,
|
|
435
490
|
};
|
|
436
491
|
}
|
|
437
|
-
async function resolvePathViaExistingAncestor(targetPath) {
|
|
438
|
-
const normalized = path.resolve(targetPath);
|
|
439
|
-
let cursor = normalized;
|
|
440
|
-
const missingSuffix = [];
|
|
441
|
-
while (!isFilesystemRoot(cursor) && !(await pathExists(cursor))) {
|
|
442
|
-
missingSuffix.unshift(path.basename(cursor));
|
|
443
|
-
const parent = path.dirname(cursor);
|
|
444
|
-
if (parent === cursor) {
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
cursor = parent;
|
|
448
|
-
}
|
|
449
|
-
if (!(await pathExists(cursor))) {
|
|
450
|
-
return normalized;
|
|
451
|
-
}
|
|
452
|
-
try {
|
|
453
|
-
const resolvedAncestor = path.resolve(await fsp.realpath(cursor));
|
|
454
|
-
if (missingSuffix.length === 0) {
|
|
455
|
-
return resolvedAncestor;
|
|
456
|
-
}
|
|
457
|
-
return path.resolve(resolvedAncestor, ...missingSuffix);
|
|
458
|
-
}
|
|
459
|
-
catch {
|
|
460
|
-
return normalized;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
export function resolvePathViaExistingAncestorSync(targetPath) {
|
|
464
|
-
const normalized = path.resolve(targetPath);
|
|
465
|
-
let cursor = normalized;
|
|
466
|
-
const missingSuffix = [];
|
|
467
|
-
while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
|
|
468
|
-
missingSuffix.unshift(path.basename(cursor));
|
|
469
|
-
const parent = path.dirname(cursor);
|
|
470
|
-
if (parent === cursor) {
|
|
471
|
-
break;
|
|
472
|
-
}
|
|
473
|
-
cursor = parent;
|
|
474
|
-
}
|
|
475
|
-
if (!fs.existsSync(cursor)) {
|
|
476
|
-
return normalized;
|
|
477
|
-
}
|
|
478
|
-
try {
|
|
479
|
-
// Keep sync behavior aligned with async (`fsp.realpath`) to avoid
|
|
480
|
-
// platform-specific canonical alias drift (notably on Windows).
|
|
481
|
-
const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
|
|
482
|
-
if (missingSuffix.length === 0) {
|
|
483
|
-
return resolvedAncestor;
|
|
484
|
-
}
|
|
485
|
-
return path.resolve(resolvedAncestor, ...missingSuffix);
|
|
486
|
-
}
|
|
487
|
-
catch {
|
|
488
|
-
return normalized;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
492
|
async function getPathKind(absolutePath, preserveFinalSymlink) {
|
|
492
493
|
try {
|
|
493
494
|
const stat = preserveFinalSymlink
|
|
@@ -555,21 +556,6 @@ function shortPath(value) {
|
|
|
555
556
|
}
|
|
556
557
|
return value;
|
|
557
558
|
}
|
|
558
|
-
function isFilesystemRoot(candidate) {
|
|
559
|
-
return path.parse(candidate).root === candidate;
|
|
560
|
-
}
|
|
561
|
-
async function pathExists(targetPath) {
|
|
562
|
-
try {
|
|
563
|
-
await fsp.lstat(targetPath);
|
|
564
|
-
return true;
|
|
565
|
-
}
|
|
566
|
-
catch (error) {
|
|
567
|
-
if (isNotFoundPathError(error)) {
|
|
568
|
-
return false;
|
|
569
|
-
}
|
|
570
|
-
throw error;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
559
|
async function resolveSymlinkHopPath(symlinkPath) {
|
|
574
560
|
try {
|
|
575
561
|
return path.resolve(await fsp.realpath(symlinkPath));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DirEntry } from "./types.js";
|
|
2
|
+
export type RootWalkSymlinkPolicy = "skip" | "follow-within-root";
|
|
3
|
+
export type RootWalkLimitBehavior = "truncate" | "throw";
|
|
4
|
+
export type RootWalkDirectoryErrorBehavior = "throw" | "skip-and-report";
|
|
5
|
+
export type RootWalkEntryFilterResult = "include" | "skip" | "skip-subtree";
|
|
6
|
+
export type RootWalkDataEntryKind = "file" | "directory" | "other";
|
|
7
|
+
export type RootWalkEntryKind = RootWalkDataEntryKind | "directory-error" | "truncated";
|
|
8
|
+
export type RootWalkDataEntry = {
|
|
9
|
+
relativePath: string;
|
|
10
|
+
kind: RootWalkDataEntryKind;
|
|
11
|
+
size: number;
|
|
12
|
+
};
|
|
13
|
+
export type RootWalkEntry = RootWalkDataEntry | {
|
|
14
|
+
relativePath: string;
|
|
15
|
+
kind: "truncated";
|
|
16
|
+
size: 0;
|
|
17
|
+
} | {
|
|
18
|
+
relativePath: string;
|
|
19
|
+
kind: "directory-error";
|
|
20
|
+
size: 0;
|
|
21
|
+
error: unknown;
|
|
22
|
+
};
|
|
23
|
+
export type RootWalkEntryFilter = (entry: RootWalkDataEntry) => RootWalkEntryFilterResult;
|
|
24
|
+
export type RootWalkOptions = {
|
|
25
|
+
maxDepth?: number;
|
|
26
|
+
maxEntries?: number;
|
|
27
|
+
symlinkPolicy: RootWalkSymlinkPolicy;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
limitBehavior?: RootWalkLimitBehavior;
|
|
30
|
+
entryFilter?: RootWalkEntryFilter;
|
|
31
|
+
onDirectoryError?: RootWalkDirectoryErrorBehavior;
|
|
32
|
+
};
|
|
33
|
+
type RootWalkCapability = {
|
|
34
|
+
rootReal: string;
|
|
35
|
+
list(relativePath: string, options: {
|
|
36
|
+
withFileTypes: true;
|
|
37
|
+
}): Promise<DirEntry[]>;
|
|
38
|
+
};
|
|
39
|
+
export declare function walkRoot(root: RootWalkCapability, relativePath: string, options: RootWalkOptions): AsyncGenerator<RootWalkEntry>;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=root-walk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-walk.d.ts","sourceRoot":"","sources":["../src/root-walk.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,oBAAoB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,OAAO,CAAC;AACzD,MAAM,MAAM,8BAA8B,GAAG,OAAO,GAAG,iBAAiB,CAAC;AACzE,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;AAC5E,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,WAAW,CAAC;AAExF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACpD;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,yBAAyB,CAAC;AAE1F,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,qBAAqB,CAAC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,gBAAgB,CAAC,EAAE,8BAA8B,CAAC;CACnD,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,aAAa,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;CACnF,CAAC;AAqBF,wBAAuB,QAAQ,CAC7B,IAAI,EAAE,kBAAkB,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC,aAAa,CAAC,CAgG/B"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
import { resolveRootPath } from "./root-path.js";
|
|
4
|
+
function validateBudget(name, value) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return Number.POSITIVE_INFINITY;
|
|
7
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
8
|
+
throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function entryKind(entry) {
|
|
13
|
+
if (entry.isSymbolicLink)
|
|
14
|
+
return "symlink";
|
|
15
|
+
if (entry.isDirectory)
|
|
16
|
+
return "directory";
|
|
17
|
+
if (entry.isFile)
|
|
18
|
+
return "file";
|
|
19
|
+
return "other";
|
|
20
|
+
}
|
|
21
|
+
function limitEntry(relativePath) {
|
|
22
|
+
return { relativePath, kind: "truncated", size: 0 };
|
|
23
|
+
}
|
|
24
|
+
export async function* walkRoot(root, relativePath, options) {
|
|
25
|
+
const maxDepth = validateBudget("maxDepth", options.maxDepth);
|
|
26
|
+
const maxEntries = validateBudget("maxEntries", options.maxEntries);
|
|
27
|
+
const visitedDirectories = new Set();
|
|
28
|
+
let examined = 0;
|
|
29
|
+
const onLimit = (atPath) => {
|
|
30
|
+
if ((options.limitBehavior ?? "truncate") === "throw") {
|
|
31
|
+
throw new FsSafeError("too-large", `root walk budget exceeded at ${atPath || "."}`);
|
|
32
|
+
}
|
|
33
|
+
return limitEntry(atPath);
|
|
34
|
+
};
|
|
35
|
+
async function* visit(directory, depth) {
|
|
36
|
+
options.signal?.throwIfAborted();
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
const resolvedDirectory = await resolveRootPath({
|
|
40
|
+
absolutePath: path.resolve(root.rootReal, directory),
|
|
41
|
+
rootPath: root.rootReal,
|
|
42
|
+
rootCanonicalPath: root.rootReal,
|
|
43
|
+
boundaryLabel: "root walk",
|
|
44
|
+
});
|
|
45
|
+
if (!resolvedDirectory.exists || resolvedDirectory.kind !== "directory") {
|
|
46
|
+
throw new FsSafeError("not-file", `root walk path is not a directory: ${directory || "."}`);
|
|
47
|
+
}
|
|
48
|
+
if (visitedDirectories.has(resolvedDirectory.canonicalPath)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
visitedDirectories.add(resolvedDirectory.canonicalPath);
|
|
52
|
+
const listingDirectory = path
|
|
53
|
+
.relative(root.rootReal, resolvedDirectory.canonicalPath)
|
|
54
|
+
.split(path.sep)
|
|
55
|
+
.join(path.posix.sep);
|
|
56
|
+
entries = await root.list(listingDirectory, { withFileTypes: true });
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if ((options.onDirectoryError ?? "throw") === "throw")
|
|
60
|
+
throw error;
|
|
61
|
+
yield { relativePath: directory, kind: "directory-error", size: 0, error };
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
options.signal?.throwIfAborted();
|
|
66
|
+
const child = directory
|
|
67
|
+
? path.posix.join(directory.split(path.sep).join(path.posix.sep), entry.name)
|
|
68
|
+
: entry.name;
|
|
69
|
+
if (examined >= maxEntries) {
|
|
70
|
+
yield onLimit(child);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
examined += 1;
|
|
74
|
+
let kind = entryKind(entry);
|
|
75
|
+
let size = entry.size;
|
|
76
|
+
if (kind === "symlink") {
|
|
77
|
+
if (options.symlinkPolicy === "skip") {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const resolved = await resolveRootPath({
|
|
81
|
+
absolutePath: path.resolve(root.rootReal, child),
|
|
82
|
+
rootPath: root.rootReal,
|
|
83
|
+
rootCanonicalPath: root.rootReal,
|
|
84
|
+
boundaryLabel: "root walk",
|
|
85
|
+
});
|
|
86
|
+
if (!resolved.exists) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
kind = resolved.kind === "directory" ? "directory" : resolved.kind === "file" ? "file" : "other";
|
|
90
|
+
size = entry.size;
|
|
91
|
+
}
|
|
92
|
+
const walkEntry = { relativePath: child, kind, size };
|
|
93
|
+
const filterResult = options.entryFilter?.(walkEntry) ?? "include";
|
|
94
|
+
if (!["include", "skip", "skip-subtree"].includes(filterResult)) {
|
|
95
|
+
throw new TypeError(`invalid root walk entryFilter result: ${String(filterResult)}`);
|
|
96
|
+
}
|
|
97
|
+
if (filterResult === "include") {
|
|
98
|
+
yield walkEntry;
|
|
99
|
+
}
|
|
100
|
+
if (kind !== "directory") {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (filterResult === "skip-subtree") {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (depth >= maxDepth) {
|
|
107
|
+
yield onLimit(child);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
yield* visit(child, depth + 1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
yield* visit(relativePath, 0);
|
|
114
|
+
}
|
package/dist/root.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { DEFAULT_ROOT_MAX_BYTES, openLocalFileSafely, readLocalFileSafely, resolveOpenedFileRealPathForHandle, root, type DenyMutationPolicy, type HardlinkPolicy, type RenameIdentityPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root-impl.js";
|
|
2
|
+
export type { ContainmentGuarantee } from "./containment.js";
|
|
3
|
+
export type { RootWalkDataEntry, RootWalkDataEntryKind, RootWalkDirectoryErrorBehavior, RootWalkEntry, RootWalkEntryFilter, RootWalkEntryFilterResult, RootWalkEntryKind, RootWalkLimitBehavior, RootWalkOptions, RootWalkSymlinkPolicy, } from "./root-walk.js";
|
|
2
4
|
//# sourceMappingURL=root.d.ts.map
|
package/dist/root.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,EAClC,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"root.d.ts","sourceRoot":"","sources":["../src/root.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,mBAAmB,EACnB,kCAAkC,EAClC,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,YAAY,EACV,iBAAiB,EACjB,qBAAqB,EACrB,8BAA8B,EAC9B,aAAa,EACb,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,eAAe,EACf,qBAAqB,GACtB,MAAM,gBAAgB,CAAC"}
|
package/dist/secret-file.d.ts
CHANGED
|
@@ -8,11 +8,14 @@ export type SecretFileReadOptions = {
|
|
|
8
8
|
};
|
|
9
9
|
export declare function readSecretFileSync(filePath: string, label: string, options?: SecretFileReadOptions): string;
|
|
10
10
|
export declare function tryReadSecretFileSync(filePath: string | undefined, label: string, options?: SecretFileReadOptions): string | undefined;
|
|
11
|
-
|
|
11
|
+
type SecretFileWriteParams = {
|
|
12
12
|
rootDir: string;
|
|
13
13
|
filePath: string;
|
|
14
14
|
content: string | Uint8Array;
|
|
15
15
|
mode?: number;
|
|
16
16
|
dirMode?: number;
|
|
17
|
-
}
|
|
17
|
+
};
|
|
18
|
+
export declare function writeSecretFileAtomic(params: SecretFileWriteParams): Promise<void>;
|
|
19
|
+
export declare function createSecretFileAtomic(params: SecretFileWriteParams): Promise<void>;
|
|
20
|
+
export {};
|
|
18
21
|
//# sourceMappingURL=secret-file.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAqIF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAQR;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAcpB;AAoID,
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAqIF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAQR;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAcpB;AAoID,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAyDF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAExF;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYzF"}
|
package/dist/secret-file.js
CHANGED
|
@@ -257,7 +257,7 @@ async function ensurePrivateDirectory(rootDir, targetDir, mode) {
|
|
|
257
257
|
}
|
|
258
258
|
return { rootGuard, targetReal: await fsp.realpath(resolvedTarget) };
|
|
259
259
|
}
|
|
260
|
-
|
|
260
|
+
async function materializeSecretFileAtomic(params, createOnly) {
|
|
261
261
|
const mode = params.mode ?? PRIVATE_SECRET_FILE_MODE;
|
|
262
262
|
const dirMode = params.dirMode ?? PRIVATE_SECRET_DIR_MODE;
|
|
263
263
|
const resolvedRoot = path.resolve(params.rootDir);
|
|
@@ -272,6 +272,9 @@ export async function writeSecretFileAtomic(params) {
|
|
|
272
272
|
const finalFilePath = path.join(targetReal, fileName);
|
|
273
273
|
try {
|
|
274
274
|
const stat = await fsp.lstat(finalFilePath);
|
|
275
|
+
if (createOnly) {
|
|
276
|
+
throw new FsSafeError("secret-exists", `Private secret file ${finalFilePath} already exists.`);
|
|
277
|
+
}
|
|
275
278
|
if (stat.isSymbolicLink()) {
|
|
276
279
|
throw new Error(`Private secret file ${finalFilePath} must not be a symlink.`);
|
|
277
280
|
}
|
|
@@ -292,7 +295,7 @@ export async function writeSecretFileAtomic(params) {
|
|
|
292
295
|
basename: fileName,
|
|
293
296
|
mkdir: false,
|
|
294
297
|
mode,
|
|
295
|
-
overwrite:
|
|
298
|
+
overwrite: !createOnly,
|
|
296
299
|
input: { kind: "buffer", data: typeof params.content === "string" ? params.content : Buffer.from(params.content) },
|
|
297
300
|
rootIdentity: { dev: parentGuard.stat.dev, ino: parentGuard.stat.ino },
|
|
298
301
|
});
|
|
@@ -300,3 +303,18 @@ export async function writeSecretFileAtomic(params) {
|
|
|
300
303
|
await assertAsyncDirectoryGuard(parentGuard);
|
|
301
304
|
await enforcePrivateFileIdentityAndMode(finalFilePath, identity, mode);
|
|
302
305
|
}
|
|
306
|
+
export async function writeSecretFileAtomic(params) {
|
|
307
|
+
await materializeSecretFileAtomic(params, false);
|
|
308
|
+
}
|
|
309
|
+
export async function createSecretFileAtomic(params) {
|
|
310
|
+
try {
|
|
311
|
+
await materializeSecretFileAtomic(params, true);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if ((error instanceof FsSafeError && error.code === "already-exists") ||
|
|
315
|
+
error.code === "EEXIST") {
|
|
316
|
+
throw new FsSafeError("secret-exists", "Private secret file already exists.", { cause: error });
|
|
317
|
+
}
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type SecretFileReadOptions } from "./secret-file.js";
|
|
2
|
+
export declare function readSecretFile(filePath: string, label: string, options?: SecretFileReadOptions): Promise<string>;
|
|
3
|
+
export declare function tryReadSecretFile(filePath: string | undefined, label: string, options?: SecretFileReadOptions): Promise<string | undefined>;
|
|
4
|
+
//# sourceMappingURL=secret-read-async.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-read-async.d.ts","sourceRoot":"","sources":["../src/secret-read-async.ts"],"names":[],"mappings":"AAMA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,kBAAkB,CAAC;AAqF1B,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAM7B"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { readFileHandleBounded } from "./bounded-read.js";
|
|
4
|
+
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
6
|
+
import { resolveHomeRelativePath } from "./home-dir.js";
|
|
7
|
+
import { DEFAULT_SECRET_FILE_MAX_BYTES, } from "./secret-file.js";
|
|
8
|
+
function pathErrorCode(error) {
|
|
9
|
+
const code = error.code;
|
|
10
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
|
|
11
|
+
}
|
|
12
|
+
async function readSecretFileOutcome(filePath, label, options) {
|
|
13
|
+
const resolvedPath = resolveHomeRelativePath(filePath.trim());
|
|
14
|
+
if (!resolvedPath) {
|
|
15
|
+
return { ok: false, code: "invalid-path", message: `${label} file path is empty.` };
|
|
16
|
+
}
|
|
17
|
+
const maxBytes = options.maxBytes ?? DEFAULT_SECRET_FILE_MAX_BYTES;
|
|
18
|
+
let previewStat;
|
|
19
|
+
try {
|
|
20
|
+
previewStat = await fs.lstat(resolvedPath);
|
|
21
|
+
if (previewStat.isSymbolicLink()) {
|
|
22
|
+
if (options.rejectSymlink) {
|
|
23
|
+
return { ok: false, code: "symlink", message: `${label} file at ${resolvedPath} must not be a symlink.` };
|
|
24
|
+
}
|
|
25
|
+
previewStat = await fs.stat(resolvedPath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
code: pathErrorCode(error),
|
|
33
|
+
error: normalized,
|
|
34
|
+
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (!previewStat.isFile()) {
|
|
38
|
+
return { ok: false, code: "not-file", message: `${label} file at ${resolvedPath} must be a regular file.` };
|
|
39
|
+
}
|
|
40
|
+
if (options.rejectHardlinks !== false && previewStat.nlink > 1) {
|
|
41
|
+
return { ok: false, code: "hardlink", message: `${label} file at ${resolvedPath} must not be hardlinked.` };
|
|
42
|
+
}
|
|
43
|
+
if (previewStat.size > maxBytes) {
|
|
44
|
+
return { ok: false, code: "too-large", message: `${label} file at ${resolvedPath} exceeds ${maxBytes} bytes.` };
|
|
45
|
+
}
|
|
46
|
+
let handle;
|
|
47
|
+
try {
|
|
48
|
+
const realPath = await fs.realpath(resolvedPath);
|
|
49
|
+
const noFollow = process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
|
|
50
|
+
? fsSync.constants.O_NOFOLLOW
|
|
51
|
+
: 0;
|
|
52
|
+
handle = await fs.open(realPath, fsSync.constants.O_RDONLY | noFollow);
|
|
53
|
+
const openedStat = await handle.stat();
|
|
54
|
+
const pathStat = await fs.lstat(realPath);
|
|
55
|
+
if (!openedStat.isFile() ||
|
|
56
|
+
!pathStat.isFile() ||
|
|
57
|
+
!sameFileIdentity(previewStat, openedStat) ||
|
|
58
|
+
!sameFileIdentity(pathStat, openedStat) ||
|
|
59
|
+
(options.rejectHardlinks !== false && openedStat.nlink > 1)) {
|
|
60
|
+
throw new FsSafeError("path-mismatch", "security validation failed");
|
|
61
|
+
}
|
|
62
|
+
const secret = (await readFileHandleBounded(handle, maxBytes)).toString("utf8").trim();
|
|
63
|
+
return secret
|
|
64
|
+
? { ok: true, secret }
|
|
65
|
+
: { ok: false, code: "invalid-path", message: `${label} file at ${resolvedPath} is empty.` };
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
code: error instanceof FsSafeError ? error.code : pathErrorCode(error) === "not-found" ? "not-found" : "path-mismatch",
|
|
72
|
+
error: normalized,
|
|
73
|
+
message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await handle?.close().catch(() => undefined);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function readSecretFile(filePath, label, options = {}) {
|
|
81
|
+
const result = await readSecretFileOutcome(filePath, label, options);
|
|
82
|
+
if (result.ok)
|
|
83
|
+
return result.secret;
|
|
84
|
+
throw new FsSafeError(result.code, result.message, { cause: result.error });
|
|
85
|
+
}
|
|
86
|
+
export async function tryReadSecretFile(filePath, label, options = {}) {
|
|
87
|
+
if (!filePath?.trim())
|
|
88
|
+
return undefined;
|
|
89
|
+
const result = await readSecretFileOutcome(filePath, label, options);
|
|
90
|
+
if (result.ok)
|
|
91
|
+
return result.secret;
|
|
92
|
+
if (result.code === "not-found")
|
|
93
|
+
return undefined;
|
|
94
|
+
throw new FsSafeError(result.code, result.message, { cause: result.error });
|
|
95
|
+
}
|