@openclaw/fs-safe 0.2.0 → 0.2.1
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 +10 -0
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +38 -33
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +4 -3
- package/dist/root-impl.js +5 -2
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +22 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.1 - 2026-05-08
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- Align POSIX and Windows handling for literal `..`-prefixed write targets, preserve whitespace in direct home-relative path inputs, and run the check suite on Windows CI. (#14; thanks @sjf)
|
|
10
|
+
- Keep source prepack builds isolated from parent monorepo ambient type packages such as Bun typings. (#13; thanks @Kaspre)
|
|
11
|
+
- Let secret-file reads follow symlink paths through the pinned real target unless callers opt into `rejectSymlink: true`.
|
|
12
|
+
|
|
3
13
|
## 0.2.0 - 2026-05-07
|
|
4
14
|
|
|
5
15
|
### Features
|
package/dist/home-dir.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"home-dir.d.ts","sourceRoot":"","sources":["../src/home-dir.ts"],"names":[],"mappings":"AAeA,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,GAAG,SAAS,CAGpB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,GAAG,SAAS,CAGpB;
|
|
1
|
+
{"version":3,"file":"home-dir.d.ts","sourceRoot":"","sources":["../src/home-dir.ts"],"names":[],"mappings":"AAeA,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,GAAG,SAAS,CAGpB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,GAAG,SAAS,CAGpB;AAyCD,wBAAgB,sBAAsB,CACpC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,CAER;AAED,wBAAgB,wBAAwB,CACtC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,CAER;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAYR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAcR;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EACN;IACE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACD,MAAM,CAAC,UAAU,EACrB,OAAO,CAAC,EAAE,MAAM,MAAM,GACrB,MAAM,CAMR;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAcR"}
|
package/dist/home-dir.js
CHANGED
|
@@ -21,17 +21,21 @@ export function resolveOsHomeDir(env = process.env, homedir = os.homedir) {
|
|
|
21
21
|
}
|
|
22
22
|
function resolveRawHomeDir(env, homedir) {
|
|
23
23
|
const explicitHome = normalize(env.OPENCLAW_HOME);
|
|
24
|
-
if (explicitHome) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
24
|
+
if (!explicitHome) {
|
|
25
|
+
return resolveRawOsHomeDir(env, homedir);
|
|
26
|
+
}
|
|
27
|
+
const segments = path.normalize(explicitHome).split(path.sep);
|
|
28
|
+
if (segments[0] !== "~") {
|
|
32
29
|
return explicitHome;
|
|
33
30
|
}
|
|
34
|
-
|
|
31
|
+
// OPENCLAW_HOME starts with "~"; expand against the os home dir. Fall
|
|
32
|
+
// back to undefined when there is no os home to expand against rather
|
|
33
|
+
// than returning a raw "~"-prefixed path the caller cannot use.
|
|
34
|
+
const fallbackHome = resolveRawOsHomeDir(env, homedir);
|
|
35
|
+
if (!fallbackHome) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
return expandHomePrefix(explicitHome, { home: fallbackHome });
|
|
35
39
|
}
|
|
36
40
|
function resolveRawOsHomeDir(env, homedir) {
|
|
37
41
|
const envHome = normalize(env.HOME);
|
|
@@ -59,7 +63,8 @@ export function resolveRequiredOsHomeDir(env = process.env, homedir = os.homedir
|
|
|
59
63
|
return resolveOsHomeDir(env, homedir) ?? path.resolve(process.cwd());
|
|
60
64
|
}
|
|
61
65
|
export function expandHomePrefix(input, opts) {
|
|
62
|
-
|
|
66
|
+
const segments = path.normalize(input).split(path.sep);
|
|
67
|
+
if (segments[0] !== "~") {
|
|
63
68
|
return input;
|
|
64
69
|
}
|
|
65
70
|
const home = normalize(opts?.home) ??
|
|
@@ -67,22 +72,22 @@ export function expandHomePrefix(input, opts) {
|
|
|
67
72
|
if (!home) {
|
|
68
73
|
return input;
|
|
69
74
|
}
|
|
70
|
-
return
|
|
75
|
+
return path.join(home, ...segments.slice(1));
|
|
71
76
|
}
|
|
72
77
|
export function resolveHomeRelativePath(input, opts) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return trimmed;
|
|
78
|
+
if (!input) {
|
|
79
|
+
return input;
|
|
76
80
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
env: opts?.env,
|
|
81
|
-
homedir: opts?.homedir,
|
|
82
|
-
});
|
|
83
|
-
return path.resolve(expanded);
|
|
81
|
+
const segments = path.normalize(input).split(path.sep);
|
|
82
|
+
if (segments[0] !== "~") {
|
|
83
|
+
return path.resolve(input);
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
const expanded = expandHomePrefix(input, {
|
|
86
|
+
home: resolveRequiredHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
|
|
87
|
+
env: opts?.env,
|
|
88
|
+
homedir: opts?.homedir,
|
|
89
|
+
});
|
|
90
|
+
return path.resolve(expanded);
|
|
86
91
|
}
|
|
87
92
|
export function resolveUserPath(input, optsOrEnv, homedir) {
|
|
88
93
|
const opts = optsOrEnv && ("env" in optsOrEnv || "homedir" in optsOrEnv)
|
|
@@ -91,17 +96,17 @@ export function resolveUserPath(input, optsOrEnv, homedir) {
|
|
|
91
96
|
return resolveHomeRelativePath(input, opts);
|
|
92
97
|
}
|
|
93
98
|
export function resolveOsHomeRelativePath(input, opts) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return trimmed;
|
|
99
|
+
if (!input) {
|
|
100
|
+
return input;
|
|
97
101
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
env: opts?.env,
|
|
102
|
-
homedir: opts?.homedir,
|
|
103
|
-
});
|
|
104
|
-
return path.resolve(expanded);
|
|
102
|
+
const segments = path.normalize(input).split(path.sep);
|
|
103
|
+
if (segments[0] !== "~") {
|
|
104
|
+
return path.resolve(input);
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
const expanded = expandHomePrefix(input, {
|
|
107
|
+
home: resolveRequiredOsHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
|
|
108
|
+
env: opts?.env,
|
|
109
|
+
homedir: opts?.homedir,
|
|
110
|
+
});
|
|
111
|
+
return path.resolve(expanded);
|
|
107
112
|
}
|
package/dist/path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAUzB,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAA6B,GAAG,IAAI,CAIjG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA4BlE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CAuBpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAOrF"}
|
package/dist/path.js
CHANGED
|
@@ -4,7 +4,6 @@ import { FsSafeError } from "./errors.js";
|
|
|
4
4
|
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
|
|
5
5
|
const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
|
|
6
6
|
const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
|
|
7
|
-
const PARENT_SEGMENT_PREFIX = /^\.\.(?:[\\/]|$)/u;
|
|
8
7
|
const POSIX_SEPARATOR_CHAR_CODE = 0x2f;
|
|
9
8
|
export function normalizeWindowsPathForComparison(input) {
|
|
10
9
|
let normalized = path.win32.normalize(input);
|
|
@@ -38,7 +37,8 @@ export function isPathInside(root, target) {
|
|
|
38
37
|
const rootForCompare = normalizeWindowsPathForComparison(path.win32.resolve(root));
|
|
39
38
|
const targetForCompare = normalizeWindowsPathForComparison(path.win32.resolve(target));
|
|
40
39
|
const relative = path.win32.relative(rootForCompare, targetForCompare);
|
|
41
|
-
|
|
40
|
+
const firstSegment = relative.split(path.win32.sep)[0];
|
|
41
|
+
return (relative === "" || (firstSegment !== ".." && !path.win32.isAbsolute(relative)));
|
|
42
42
|
}
|
|
43
43
|
if (root.length > 0 &&
|
|
44
44
|
root.charCodeAt(0) === POSIX_SEPARATOR_CHAR_CODE &&
|
|
@@ -52,7 +52,8 @@ export function isPathInside(root, target) {
|
|
|
52
52
|
const resolvedRoot = path.resolve(root);
|
|
53
53
|
const resolvedTarget = path.resolve(target);
|
|
54
54
|
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
55
|
-
|
|
55
|
+
const firstSegment = relative.split(path.posix.sep)[0];
|
|
56
|
+
return relative === "" || (firstSegment !== ".." && !path.isAbsolute(relative));
|
|
56
57
|
}
|
|
57
58
|
export function resolveSafeBaseDir(rootDir) {
|
|
58
59
|
const resolved = path.resolve(rootDir);
|
package/dist/root-impl.js
CHANGED
|
@@ -856,8 +856,10 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode)
|
|
|
856
856
|
catch (err) {
|
|
857
857
|
throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
858
858
|
}
|
|
859
|
+
// resolvePathInRoot already enforces isPathInside, so any actual escape
|
|
860
|
+
// is rejected upstream.
|
|
859
861
|
const relativeResolved = path.relative(rootReal, resolved);
|
|
860
|
-
if (
|
|
862
|
+
if (path.isAbsolute(relativeResolved)) {
|
|
861
863
|
throw new FsSafeError("outside-workspace", "file is outside workspace root");
|
|
862
864
|
}
|
|
863
865
|
const relativePosix = relativeResolved
|
|
@@ -919,9 +921,10 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
919
921
|
if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
|
|
920
922
|
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix: "" };
|
|
921
923
|
}
|
|
924
|
+
const firstSegment = relativeResolved.split(path.sep)[0];
|
|
922
925
|
if (relativeResolved === "" ||
|
|
923
926
|
relativeResolved === "." ||
|
|
924
|
-
|
|
927
|
+
firstSegment === ".." ||
|
|
925
928
|
path.isAbsolute(relativeResolved)) {
|
|
926
929
|
throw new FsSafeError("outside-workspace", "file is outside workspace root");
|
|
927
930
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAUA,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;CACzB,CAAC;
|
|
1
|
+
{"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAUA,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;CACzB,CAAC;AAqHF,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,CAMpB;AAoFD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,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,GAAG,OAAO,CAAC,IAAI,CAAC,CAyDhB"}
|
package/dist/secret-file.js
CHANGED
|
@@ -36,12 +36,28 @@ function readSecretFileOutcomeSync(filePath, label, options = {}) {
|
|
|
36
36
|
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
if (previewStat.isSymbolicLink()) {
|
|
40
|
+
if (!options.rejectSymlink) {
|
|
41
|
+
try {
|
|
42
|
+
previewStat = fs.statSync(resolvedPath);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const normalized = normalizeSecretReadError(error);
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
code: error.code === "ENOENT" ? "not-found" : "invalid-path",
|
|
49
|
+
error: normalized,
|
|
50
|
+
message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
code: "symlink",
|
|
58
|
+
message: `${label} file at ${resolvedPath} must not be a symlink.`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
45
61
|
}
|
|
46
62
|
if (!previewStat.isFile()) {
|
|
47
63
|
return {
|