@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 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
@@ -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;AAsCD,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,CAWR;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"}
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
- if (explicitHome === "~" || explicitHome.startsWith("~/") || explicitHome.startsWith("~\\")) {
26
- const fallbackHome = resolveRawOsHomeDir(env, homedir);
27
- if (fallbackHome) {
28
- return explicitHome.replace(/^~(?=$|[\\/])/, fallbackHome);
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
- return resolveRawOsHomeDir(env, homedir);
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
- if (!input.startsWith("~")) {
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 input.replace(/^~(?=$|[\\/])/, home);
75
+ return path.join(home, ...segments.slice(1));
71
76
  }
72
77
  export function resolveHomeRelativePath(input, opts) {
73
- const trimmed = input.trim();
74
- if (!trimmed) {
75
- return trimmed;
78
+ if (!input) {
79
+ return input;
76
80
  }
77
- if (trimmed.startsWith("~")) {
78
- const expanded = expandHomePrefix(trimmed, {
79
- home: resolveRequiredHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
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
- return path.resolve(trimmed);
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
- const trimmed = input.trim();
95
- if (!trimmed) {
96
- return trimmed;
99
+ if (!input) {
100
+ return input;
97
101
  }
98
- if (trimmed.startsWith("~")) {
99
- const expanded = expandHomePrefix(trimmed, {
100
- home: resolveRequiredOsHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
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
- return path.resolve(trimmed);
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAWzB,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,CA0BlE;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"}
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
- return (relative === "" || (!PARENT_SEGMENT_PREFIX.test(relative) && !path.win32.isAbsolute(relative)));
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
- return relative === "" || (!PARENT_SEGMENT_PREFIX.test(relative) && !path.isAbsolute(relative));
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 (relativeResolved.startsWith("..") || path.isAbsolute(relativeResolved)) {
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
- relativeResolved.startsWith("..") ||
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;AAuGF,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"}
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"}
@@ -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 (options.rejectSymlink && previewStat.isSymbolicLink()) {
40
- return {
41
- ok: false,
42
- code: "symlink",
43
- message: `${label} file at ${resolvedPath} must not be a symlink.`,
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "license": "MIT",
6
6
  "repository": {