@openclaw/fs-safe 0.2.1 → 0.2.2

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/archive-utils.d.ts +3 -0
  3. package/dist/archive-utils.d.ts.map +1 -0
  4. package/dist/archive-utils.js +26 -0
  5. package/dist/boundary-file-read.d.ts +44 -0
  6. package/dist/boundary-file-read.d.ts.map +1 -0
  7. package/dist/boundary-file-read.js +129 -0
  8. package/dist/boundary-path.d.ts +39 -0
  9. package/dist/boundary-path.d.ts.map +1 -0
  10. package/dist/boundary-path.js +598 -0
  11. package/dist/fs-pinned-path-helper.d.ts +7 -0
  12. package/dist/fs-pinned-path-helper.d.ts.map +1 -0
  13. package/dist/fs-pinned-path-helper.js +182 -0
  14. package/dist/fs-pinned-write-helper.d.ts +21 -0
  15. package/dist/fs-pinned-write-helper.d.ts.map +1 -0
  16. package/dist/fs-pinned-write-helper.js +263 -0
  17. package/dist/hardlink-guards.d.ts +7 -0
  18. package/dist/hardlink-guards.d.ts.map +1 -0
  19. package/dist/hardlink-guards.js +30 -0
  20. package/dist/install-safe-path.d.ts +20 -0
  21. package/dist/install-safe-path.d.ts.map +1 -0
  22. package/dist/install-safe-path.js +94 -0
  23. package/dist/json-file.d.ts +3 -0
  24. package/dist/json-file.d.ts.map +1 -0
  25. package/dist/json-file.js +123 -0
  26. package/dist/json-files.d.ts +20 -0
  27. package/dist/json-files.d.ts.map +1 -0
  28. package/dist/json-files.js +153 -0
  29. package/dist/path-alias-guards.d.ts +19 -0
  30. package/dist/path-alias-guards.d.ts.map +1 -0
  31. package/dist/path-alias-guards.js +21 -0
  32. package/dist/path-guards.d.ts +7 -0
  33. package/dist/path-guards.d.ts.map +1 -0
  34. package/dist/path-guards.js +49 -0
  35. package/dist/path-safety.d.ts +12 -0
  36. package/dist/path-safety.d.ts.map +1 -0
  37. package/dist/path-safety.js +50 -0
  38. package/dist/pinned-python-config.d.ts.map +1 -1
  39. package/dist/pinned-python-config.js +2 -3
  40. package/dist/private-file-store.d.ts +7 -5
  41. package/dist/private-file-store.d.ts.map +1 -1
  42. package/dist/private-file-store.js +34 -21
  43. package/dist/safe-open-sync.d.ts +24 -0
  44. package/dist/safe-open-sync.d.ts.map +1 -0
  45. package/dist/safe-open-sync.js +71 -0
  46. package/dist/safe-root.d.ts +123 -0
  47. package/dist/safe-root.d.ts.map +1 -0
  48. package/dist/safe-root.js +1060 -0
  49. package/dist/secure-temp-workspace.d.ts +25 -0
  50. package/dist/secure-temp-workspace.d.ts.map +1 -0
  51. package/dist/secure-temp-workspace.js +136 -0
  52. package/dist/sibling-temp-file.d.ts +16 -0
  53. package/dist/sibling-temp-file.d.ts.map +1 -0
  54. package/dist/sibling-temp-file.js +73 -0
  55. package/dist/sibling-temp-write.d.ts +8 -0
  56. package/dist/sibling-temp-write.d.ts.map +1 -0
  57. package/dist/sibling-temp-write.js +40 -0
  58. package/package.json +1 -1
@@ -0,0 +1,123 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ const JSON_FILE_MODE = 0o600;
5
+ const JSON_DIR_MODE = 0o700;
6
+ function trySetSecureMode(pathname) {
7
+ try {
8
+ fs.chmodSync(pathname, JSON_FILE_MODE);
9
+ }
10
+ catch {
11
+ // best-effort on platforms without chmod support
12
+ }
13
+ }
14
+ function trySyncDirectory(pathname) {
15
+ let fd;
16
+ try {
17
+ fd = fs.openSync(path.dirname(pathname), "r");
18
+ fs.fsyncSync(fd);
19
+ }
20
+ catch {
21
+ // best-effort; some platforms/filesystems do not support syncing directories.
22
+ }
23
+ finally {
24
+ if (fd !== undefined) {
25
+ try {
26
+ fs.closeSync(fd);
27
+ }
28
+ catch {
29
+ // best-effort cleanup
30
+ }
31
+ }
32
+ }
33
+ }
34
+ function readSymlinkTargetPath(linkPath) {
35
+ const target = fs.readlinkSync(linkPath);
36
+ return path.resolve(path.dirname(linkPath), target);
37
+ }
38
+ function resolveJsonWriteTarget(pathname) {
39
+ let currentPath = pathname;
40
+ const visited = new Set();
41
+ let followsSymlink = false;
42
+ for (;;) {
43
+ let stat;
44
+ try {
45
+ stat = fs.lstatSync(currentPath);
46
+ }
47
+ catch (error) {
48
+ if (error.code !== "ENOENT") {
49
+ throw error;
50
+ }
51
+ return { targetPath: currentPath, followsSymlink };
52
+ }
53
+ if (!stat.isSymbolicLink()) {
54
+ return { targetPath: currentPath, followsSymlink };
55
+ }
56
+ if (visited.has(currentPath)) {
57
+ const err = new Error(`Too many symlink levels while resolving ${pathname}`);
58
+ err.code = "ELOOP";
59
+ throw err;
60
+ }
61
+ visited.add(currentPath);
62
+ followsSymlink = true;
63
+ currentPath = readSymlinkTargetPath(currentPath);
64
+ }
65
+ }
66
+ function renameJsonFileWithFallback(tmpPath, pathname) {
67
+ try {
68
+ fs.renameSync(tmpPath, pathname);
69
+ return;
70
+ }
71
+ catch (error) {
72
+ const code = error.code;
73
+ // Windows does not reliably support rename-based overwrite for existing files.
74
+ if (code === "EPERM" || code === "EEXIST") {
75
+ fs.copyFileSync(tmpPath, pathname);
76
+ fs.rmSync(tmpPath, { force: true });
77
+ return;
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+ function writeTempJsonFile(pathname, payload) {
83
+ const fd = fs.openSync(pathname, "w", JSON_FILE_MODE);
84
+ try {
85
+ fs.writeFileSync(fd, payload, "utf8");
86
+ fs.fsyncSync(fd);
87
+ }
88
+ finally {
89
+ fs.closeSync(fd);
90
+ }
91
+ }
92
+ export function loadJsonFile(pathname) {
93
+ try {
94
+ const raw = fs.readFileSync(pathname, "utf8");
95
+ return JSON.parse(raw);
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ }
101
+ export function saveJsonFile(pathname, data) {
102
+ const { targetPath, followsSymlink } = resolveJsonWriteTarget(pathname);
103
+ const tmpPath = `${targetPath}.${randomUUID()}.tmp`;
104
+ const payload = `${JSON.stringify(data, null, 2)}\n`;
105
+ if (!followsSymlink) {
106
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: JSON_DIR_MODE });
107
+ }
108
+ try {
109
+ writeTempJsonFile(tmpPath, payload);
110
+ trySetSecureMode(tmpPath);
111
+ renameJsonFileWithFallback(tmpPath, targetPath);
112
+ trySetSecureMode(targetPath);
113
+ trySyncDirectory(targetPath);
114
+ }
115
+ finally {
116
+ try {
117
+ fs.rmSync(tmpPath, { force: true });
118
+ }
119
+ catch {
120
+ // best-effort cleanup when rename does not happen
121
+ }
122
+ }
123
+ }
@@ -0,0 +1,20 @@
1
+ export declare class JsonFileReadError extends Error {
2
+ readonly filePath: string;
3
+ readonly reason: "read" | "parse";
4
+ constructor(filePath: string, reason: "read" | "parse", cause: unknown);
5
+ }
6
+ export declare function readJsonFile<T>(filePath: string): Promise<T | null>;
7
+ export declare function readDurableJsonFile<T>(filePath: string): Promise<T | null>;
8
+ export declare function readJsonFileSync(filePath: string): unknown;
9
+ export declare function writeJsonAtomic(filePath: string, value: unknown, options?: {
10
+ mode?: number;
11
+ trailingNewline?: boolean;
12
+ ensureDirMode?: number;
13
+ }): Promise<void>;
14
+ export declare function writeTextAtomic(filePath: string, content: string, options?: {
15
+ mode?: number;
16
+ ensureDirMode?: number;
17
+ appendTrailingNewline?: boolean;
18
+ }): Promise<void>;
19
+ export declare function createAsyncLock(): <T>(fn: () => Promise<T>) => Promise<T>;
20
+ //# sourceMappingURL=json-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-files.d.ts","sourceRoot":"","sources":["../src/json-files.ts"],"names":[],"mappings":"AASA,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;gBAEtB,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO;CAMvE;AA6BD,wBAAsB,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAOzE;AAED,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAehF;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAO1D;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,iBAQ/E;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAAE,iBA4CrF;AAED,wBAAgB,eAAe,KAEE,CAAC,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC,CAapE"}
@@ -0,0 +1,153 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ function getErrorCode(err) {
6
+ return err instanceof Error ? err.code : undefined;
7
+ }
8
+ export class JsonFileReadError extends Error {
9
+ filePath;
10
+ reason;
11
+ constructor(filePath, reason, cause) {
12
+ super(`Failed to ${reason} JSON file: ${filePath}`, { cause });
13
+ this.name = "JsonFileReadError";
14
+ this.filePath = filePath;
15
+ this.reason = reason;
16
+ }
17
+ }
18
+ async function replaceFileWithWindowsFallback(tempPath, filePath, mode) {
19
+ try {
20
+ await fs.rename(tempPath, filePath);
21
+ return;
22
+ }
23
+ catch (err) {
24
+ const code = getErrorCode(err);
25
+ if (process.platform !== "win32" || (code !== "EPERM" && code !== "EEXIST")) {
26
+ throw err;
27
+ }
28
+ }
29
+ const existing = await fs.lstat(filePath).catch(() => null);
30
+ if (existing?.isSymbolicLink()) {
31
+ await fs.rm(filePath, { force: true });
32
+ await fs.rename(tempPath, filePath);
33
+ return;
34
+ }
35
+ await fs.copyFile(tempPath, filePath);
36
+ try {
37
+ await fs.chmod(filePath, mode);
38
+ }
39
+ catch {
40
+ // best-effort; ignore on platforms without chmod
41
+ }
42
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
43
+ }
44
+ export async function readJsonFile(filePath) {
45
+ try {
46
+ const raw = await fs.readFile(filePath, "utf8");
47
+ return JSON.parse(raw);
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ export async function readDurableJsonFile(filePath) {
54
+ let raw;
55
+ try {
56
+ raw = await fs.readFile(filePath, "utf8");
57
+ }
58
+ catch (err) {
59
+ if (getErrorCode(err) === "ENOENT") {
60
+ return null;
61
+ }
62
+ throw new JsonFileReadError(filePath, "read", err);
63
+ }
64
+ try {
65
+ return JSON.parse(raw);
66
+ }
67
+ catch (err) {
68
+ throw new JsonFileReadError(filePath, "parse", err);
69
+ }
70
+ }
71
+ export function readJsonFileSync(filePath) {
72
+ try {
73
+ const raw = readFileSync(filePath, "utf8");
74
+ return JSON.parse(raw);
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ export async function writeJsonAtomic(filePath, value, options) {
81
+ const text = JSON.stringify(value, null, 2);
82
+ await writeTextAtomic(filePath, text, {
83
+ mode: options?.mode,
84
+ ensureDirMode: options?.ensureDirMode,
85
+ appendTrailingNewline: options?.trailingNewline,
86
+ });
87
+ }
88
+ export async function writeTextAtomic(filePath, content, options) {
89
+ const mode = options?.mode ?? 0o600;
90
+ const payload = options?.appendTrailingNewline && !content.endsWith("\n") ? `${content}\n` : content;
91
+ const mkdirOptions = { recursive: true };
92
+ if (typeof options?.ensureDirMode === "number") {
93
+ mkdirOptions.mode = options.ensureDirMode;
94
+ }
95
+ await fs.mkdir(path.dirname(filePath), mkdirOptions);
96
+ const parentDir = path.dirname(filePath);
97
+ const tmp = `${filePath}.${randomUUID()}.tmp`;
98
+ try {
99
+ const tmpHandle = await fs.open(tmp, "w", mode);
100
+ try {
101
+ await tmpHandle.writeFile(payload, { encoding: "utf8" });
102
+ await tmpHandle.sync();
103
+ }
104
+ finally {
105
+ await tmpHandle.close().catch(() => undefined);
106
+ }
107
+ try {
108
+ await fs.chmod(tmp, mode);
109
+ }
110
+ catch {
111
+ // best-effort; ignore on platforms without chmod
112
+ }
113
+ await replaceFileWithWindowsFallback(tmp, filePath, mode);
114
+ try {
115
+ const dirHandle = await fs.open(parentDir, "r");
116
+ try {
117
+ await dirHandle.sync();
118
+ }
119
+ finally {
120
+ await dirHandle.close().catch(() => undefined);
121
+ }
122
+ }
123
+ catch {
124
+ // best-effort; some platforms/filesystems do not support syncing directories.
125
+ }
126
+ try {
127
+ await fs.chmod(filePath, mode);
128
+ }
129
+ catch {
130
+ // best-effort; ignore on platforms without chmod
131
+ }
132
+ }
133
+ finally {
134
+ await fs.rm(tmp, { force: true }).catch(() => undefined);
135
+ }
136
+ }
137
+ export function createAsyncLock() {
138
+ let lock = Promise.resolve();
139
+ return async function withLock(fn) {
140
+ const prev = lock;
141
+ let release;
142
+ lock = new Promise((resolve) => {
143
+ release = resolve;
144
+ });
145
+ await prev;
146
+ try {
147
+ return await fn();
148
+ }
149
+ finally {
150
+ release?.();
151
+ }
152
+ };
153
+ }
@@ -0,0 +1,19 @@
1
+ import { type RootPathAliasPolicy } from "./root-path.js";
2
+ export type PathAliasPolicy = RootPathAliasPolicy;
3
+ export declare const PATH_ALIAS_POLICIES: {
4
+ readonly strict: Readonly<{
5
+ allowFinalSymlinkForUnlink: false;
6
+ allowFinalHardlinkForUnlink: false;
7
+ }>;
8
+ readonly unlinkTarget: Readonly<{
9
+ allowFinalSymlinkForUnlink: true;
10
+ allowFinalHardlinkForUnlink: true;
11
+ }>;
12
+ };
13
+ export declare function assertNoPathAliasEscape(params: {
14
+ absolutePath: string;
15
+ rootPath: string;
16
+ boundaryLabel: string;
17
+ policy?: PathAliasPolicy;
18
+ }): Promise<void>;
19
+ //# sourceMappingURL=path-alias-guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-alias-guards.d.ts","sourceRoot":"","sources":["../src/path-alias-guards.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,gBAAgB,CAAC;AAGxB,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAElD,eAAO,MAAM,mBAAmB;;;;;;;;;CAA2B,CAAC;AAE5D,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB"}
@@ -0,0 +1,21 @@
1
+ import { ROOT_PATH_ALIAS_POLICIES, resolveRootPath, } from "./root-path.js";
2
+ import { assertNoHardlinkedFinalPath } from "./hardlink-guards.js";
3
+ export const PATH_ALIAS_POLICIES = ROOT_PATH_ALIAS_POLICIES;
4
+ export async function assertNoPathAliasEscape(params) {
5
+ const resolved = await resolveRootPath({
6
+ absolutePath: params.absolutePath,
7
+ rootPath: params.rootPath,
8
+ boundaryLabel: params.boundaryLabel,
9
+ policy: params.policy,
10
+ });
11
+ const allowFinalSymlink = params.policy?.allowFinalSymlinkForUnlink === true;
12
+ if (allowFinalSymlink && resolved.kind === "symlink") {
13
+ return;
14
+ }
15
+ await assertNoHardlinkedFinalPath({
16
+ filePath: resolved.absolutePath,
17
+ root: resolved.rootPath,
18
+ boundaryLabel: params.boundaryLabel,
19
+ allowFinalHardlinkForUnlink: params.policy?.allowFinalHardlinkForUnlink,
20
+ });
21
+ }
@@ -0,0 +1,7 @@
1
+ export declare function normalizeWindowsPathForComparison(input: string): string;
2
+ export declare function isNodeError(value: unknown): value is NodeJS.ErrnoException;
3
+ export declare function hasNodeErrorCode(value: unknown, code: string): boolean;
4
+ export declare function isNotFoundPathError(value: unknown): boolean;
5
+ export declare function isSymlinkOpenError(value: unknown): boolean;
6
+ export declare function isPathInside(root: string, target: string): boolean;
7
+ //# sourceMappingURL=path-guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-guards.d.ts","sourceRoot":"","sources":["../src/path-guards.ts"],"names":[],"mappings":"AAQA,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,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"}
@@ -0,0 +1,49 @@
1
+ import path from "node:path";
2
+ import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
3
+ const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
4
+ const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
5
+ const PARENT_SEGMENT_PREFIX = /^\.\.(?:[\\/]|$)/u;
6
+ const POSIX_SEPARATOR_CHAR_CODE = 0x2f;
7
+ export function normalizeWindowsPathForComparison(input) {
8
+ let normalized = path.win32.normalize(input);
9
+ if (normalized.startsWith("\\\\?\\")) {
10
+ normalized = normalized.slice(4);
11
+ if (normalized.toUpperCase().startsWith("UNC\\")) {
12
+ normalized = `\\\\${normalized.slice(4)}`;
13
+ }
14
+ }
15
+ return normalizeLowercaseStringOrEmpty(normalized.replaceAll("/", "\\"));
16
+ }
17
+ export function isNodeError(value) {
18
+ return Boolean(value && typeof value === "object" && "code" in value);
19
+ }
20
+ export function hasNodeErrorCode(value, code) {
21
+ return isNodeError(value) && value.code === code;
22
+ }
23
+ export function isNotFoundPathError(value) {
24
+ return isNodeError(value) && typeof value.code === "string" && NOT_FOUND_CODES.has(value.code);
25
+ }
26
+ export function isSymlinkOpenError(value) {
27
+ return isNodeError(value) && typeof value.code === "string" && SYMLINK_OPEN_CODES.has(value.code);
28
+ }
29
+ export function isPathInside(root, target) {
30
+ if (process.platform === "win32") {
31
+ const rootForCompare = normalizeWindowsPathForComparison(path.win32.resolve(root));
32
+ const targetForCompare = normalizeWindowsPathForComparison(path.win32.resolve(target));
33
+ const relative = path.win32.relative(rootForCompare, targetForCompare);
34
+ return (relative === "" || (!PARENT_SEGMENT_PREFIX.test(relative) && !path.win32.isAbsolute(relative)));
35
+ }
36
+ if (root.length > 0 &&
37
+ root.charCodeAt(0) === POSIX_SEPARATOR_CHAR_CODE &&
38
+ target.length >= root.length &&
39
+ target.charCodeAt(0) === POSIX_SEPARATOR_CHAR_CODE &&
40
+ !target.includes("/..") &&
41
+ (target === root ||
42
+ (target.startsWith(root) && target.charCodeAt(root.length) === POSIX_SEPARATOR_CHAR_CODE))) {
43
+ return true;
44
+ }
45
+ const resolvedRoot = path.resolve(root);
46
+ const resolvedTarget = path.resolve(target);
47
+ const relative = path.relative(resolvedRoot, resolvedTarget);
48
+ return relative === "" || (!PARENT_SEGMENT_PREFIX.test(relative) && !path.isAbsolute(relative));
49
+ }
@@ -0,0 +1,12 @@
1
+ import fs from "node:fs";
2
+ export declare function resolveSafeBaseDir(rootDir: string): string;
3
+ export declare function isWithinDir(rootDir: string, targetPath: string): boolean;
4
+ export declare function isPathInside(baseDir: string, targetPath: string): boolean;
5
+ export declare function safeRealpathSync(targetPath: string, cache?: Map<string, string>): string | null;
6
+ export declare function isPathInsideWithRealpath(basePath: string, candidatePath: string, opts?: {
7
+ requireRealpath?: boolean;
8
+ cache?: Map<string, string>;
9
+ }): boolean;
10
+ export declare function safeStatSync(targetPath: string): fs.Stats | null;
11
+ export declare function formatPosixMode(mode: number): string;
12
+ //# sourceMappingURL=path-safety.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-safety.d.ts","sourceRoot":"","sources":["../src/path-safety.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAIzB,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,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAEzE;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,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD"}
@@ -0,0 +1,50 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { isPathInside as isRootPathInside } from "./path-guards.js";
4
+ export function resolveSafeBaseDir(rootDir) {
5
+ const resolved = path.resolve(rootDir);
6
+ return resolved.endsWith(path.sep) ? resolved : `${resolved}${path.sep}`;
7
+ }
8
+ export function isWithinDir(rootDir, targetPath) {
9
+ return isRootPathInside(rootDir, targetPath);
10
+ }
11
+ export function isPathInside(baseDir, targetPath) {
12
+ return isRootPathInside(baseDir, targetPath);
13
+ }
14
+ export function safeRealpathSync(targetPath, cache) {
15
+ const cached = cache?.get(targetPath);
16
+ if (cached) {
17
+ return cached;
18
+ }
19
+ try {
20
+ const resolved = fs.realpathSync(targetPath);
21
+ cache?.set(targetPath, resolved);
22
+ cache?.set(resolved, resolved);
23
+ return resolved;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ export function isPathInsideWithRealpath(basePath, candidatePath, opts) {
30
+ if (!isPathInside(basePath, candidatePath)) {
31
+ return false;
32
+ }
33
+ const baseReal = safeRealpathSync(basePath, opts?.cache);
34
+ const candidateReal = safeRealpathSync(candidatePath, opts?.cache);
35
+ if (!baseReal || !candidateReal) {
36
+ return opts?.requireRealpath === false;
37
+ }
38
+ return isPathInside(baseReal, candidateReal);
39
+ }
40
+ export function safeStatSync(targetPath) {
41
+ try {
42
+ return fs.statSync(targetPath);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ export function formatPosixMode(mode) {
49
+ return (mode & 0o777).toString(8).padStart(3, "0");
50
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-python-config.d.ts","sourceRoot":"","sources":["../src/pinned-python-config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAE1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAqBF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAE/E;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAc1D;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAOlE"}
1
+ {"version":3,"file":"pinned-python-config.d.ts","sourceRoot":"","sources":["../src/pinned-python-config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAE1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAqBF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAE/E;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAc1D;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAMlE"}
@@ -32,8 +32,7 @@ export function getFsSafePythonConfig() {
32
32
  };
33
33
  }
34
34
  export function canFallbackFromPythonError(error) {
35
+ const code = error instanceof Error && "code" in error ? error.code : undefined;
35
36
  return (getFsSafePythonConfig().mode !== "require" &&
36
- error instanceof Error &&
37
- "code" in error &&
38
- error.code === "helper-unavailable");
37
+ (code === "helper-unavailable" || code === "unsupported-platform"));
39
38
  }
@@ -1,16 +1,18 @@
1
- export type PrivateFileStore = {
1
+ import { type FileStore } from "./file-store.js";
2
+ export type PrivateStateStoreOptions = {
2
3
  rootDir: string;
3
- path(relativePath: string): string;
4
+ };
5
+ export type PrivateStateStore = Omit<FileStore, "readText" | "readJson" | "writeText" | "writeJson"> & {
4
6
  readText(relativePath: string, options?: {
5
7
  maxBytes?: number;
6
8
  }): Promise<string | null>;
7
9
  readJson<T = unknown>(relativePath: string, options?: {
8
10
  maxBytes?: number;
9
11
  }): Promise<T | null>;
10
- writeText(relativePath: string, content: string | Uint8Array): Promise<void>;
12
+ writeText(relativePath: string, content: string | Uint8Array): Promise<string>;
11
13
  writeJson(relativePath: string, value: unknown, options?: {
12
14
  trailingNewline?: boolean;
13
- }): Promise<void>;
15
+ }): Promise<string>;
14
16
  };
15
17
  export declare function writePrivateTextAtomic(params: {
16
18
  rootDir: string;
@@ -54,5 +56,5 @@ export declare function writePrivateJsonAtomicSync(params: {
54
56
  value: unknown;
55
57
  trailingNewline?: boolean;
56
58
  }): void;
57
- export declare function privateFileStore(rootDir: string): PrivateFileStore;
59
+ export declare function privateStateStore(options: PrivateStateStoreOptions): PrivateStateStore;
58
60
  //# sourceMappingURL=private-file-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"private-file-store.d.ts","sourceRoot":"","sources":["../src/private-file-store.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxF,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAChG,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzG,CAAC;AAgBF,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhB;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBzB;AAoBD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,GAAG,IAAI,CAiBhB;AAED,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAGpB;AAED,wBAAgB,mBAAmB,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,CAAC,GAAG,IAAI,CAGX;AAoCD,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B,GAAG,IAAI,CA4CP;AAED,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,GAAG,IAAI,CAOP;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAiClE"}
1
+ {"version":3,"file":"private-file-store.d.ts","sourceRoot":"","sources":["../src/private-file-store.ts"],"names":[],"mappings":"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"}
@@ -2,10 +2,11 @@ import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
3
  import fs from "node:fs";
4
4
  import { FsSafeError } from "./errors.js";
5
+ import { fileStore } from "./file-store.js";
5
6
  import { isPathInside } from "./path.js";
6
7
  import { readRegularFileSync } from "./regular-file.js";
7
8
  import { root } from "./root.js";
8
- import { writePrivateSecretFileAtomic } from "./secret-file.js";
9
+ import { writeSecretFileAtomic } from "./secret-file.js";
9
10
  function resolvePrivateStorePath(rootDir, relativePath) {
10
11
  const root = path.resolve(rootDir);
11
12
  const raw = relativePath.trim();
@@ -20,7 +21,7 @@ function resolvePrivateStorePath(rootDir, relativePath) {
20
21
  return resolved;
21
22
  }
22
23
  export async function writePrivateTextAtomic(params) {
23
- await writePrivateSecretFileAtomic(params);
24
+ await writeSecretFileAtomic(params);
24
25
  }
25
26
  export async function readPrivateText(params) {
26
27
  const rootDir = path.resolve(params.rootDir);
@@ -172,7 +173,7 @@ export function writePrivateTextAtomicSync(params) {
172
173
  }
173
174
  export async function writePrivateJsonAtomic(params) {
174
175
  const json = JSON.stringify(params.value, null, 2);
175
- await writePrivateSecretFileAtomic({
176
+ await writeSecretFileAtomic({
176
177
  rootDir: params.rootDir,
177
178
  filePath: params.filePath,
178
179
  content: params.trailingNewline && !json.endsWith("\n") ? `${json}\n` : json,
@@ -186,35 +187,47 @@ export function writePrivateJsonAtomicSync(params) {
186
187
  content: params.trailingNewline && !json.endsWith("\n") ? `${json}\n` : json,
187
188
  });
188
189
  }
189
- export function privateFileStore(rootDir) {
190
- const root = path.resolve(rootDir);
190
+ export function privateStateStore(options) {
191
+ const rootDir = path.resolve(options.rootDir);
192
+ const store = fileStore({ rootDir, private: true });
191
193
  return {
192
- rootDir: root,
193
- path: (relativePath) => resolvePrivateStorePath(root, relativePath),
194
- readText: async (relativePath, options) => await readPrivateText({
195
- rootDir: root,
196
- filePath: resolvePrivateStorePath(root, relativePath),
197
- maxBytes: options?.maxBytes,
198
- }),
199
- readJson: async (relativePath, options) => await readPrivateJson({
200
- rootDir: root,
201
- filePath: resolvePrivateStorePath(root, relativePath),
202
- maxBytes: options?.maxBytes,
203
- }),
194
+ ...store,
195
+ rootDir,
196
+ path: (relativePath) => resolvePrivateStorePath(rootDir, relativePath),
197
+ readText: async (relativePath, options) => {
198
+ const safePath = resolvePrivateStorePath(rootDir, relativePath);
199
+ return await readPrivateText({
200
+ rootDir,
201
+ filePath: safePath,
202
+ maxBytes: options?.maxBytes,
203
+ });
204
+ },
205
+ readJson: async (relativePath, options) => {
206
+ const safePath = resolvePrivateStorePath(rootDir, relativePath);
207
+ return await readPrivateJson({
208
+ rootDir,
209
+ filePath: safePath,
210
+ maxBytes: options?.maxBytes,
211
+ });
212
+ },
204
213
  writeText: async (relativePath, content) => {
214
+ const safePath = resolvePrivateStorePath(rootDir, relativePath);
205
215
  await writePrivateTextAtomic({
206
- rootDir: root,
207
- filePath: resolvePrivateStorePath(root, relativePath),
216
+ rootDir,
217
+ filePath: safePath,
208
218
  content,
209
219
  });
220
+ return safePath;
210
221
  },
211
222
  writeJson: async (relativePath, value, options) => {
223
+ const safePath = resolvePrivateStorePath(rootDir, relativePath);
212
224
  await writePrivateJsonAtomic({
213
- rootDir: root,
214
- filePath: resolvePrivateStorePath(root, relativePath),
225
+ rootDir,
226
+ filePath: safePath,
215
227
  value,
216
228
  trailingNewline: options?.trailingNewline,
217
229
  });
230
+ return safePath;
218
231
  },
219
232
  };
220
233
  }
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs";
2
+ export type SafeOpenSyncFailureReason = "path" | "validation" | "io";
3
+ export type SafeOpenSyncResult = {
4
+ ok: true;
5
+ path: string;
6
+ fd: number;
7
+ stat: fs.Stats;
8
+ } | {
9
+ ok: false;
10
+ reason: SafeOpenSyncFailureReason;
11
+ error?: unknown;
12
+ };
13
+ export type SafeOpenSyncAllowedType = "file" | "directory";
14
+ export type SafeOpenSyncFs = Pick<typeof fs, "constants" | "lstatSync" | "realpathSync" | "openSync" | "fstatSync" | "closeSync">;
15
+ export declare function openVerifiedFileSync(params: {
16
+ filePath: string;
17
+ resolvedPath?: string;
18
+ rejectPathSymlink?: boolean;
19
+ rejectHardlinks?: boolean;
20
+ maxBytes?: number;
21
+ allowedType?: SafeOpenSyncAllowedType;
22
+ ioFs?: SafeOpenSyncFs;
23
+ }): SafeOpenSyncResult;
24
+ //# sourceMappingURL=safe-open-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-open-sync.d.ts","sourceRoot":"","sources":["../src/safe-open-sync.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAGzB,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAErE,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,yBAAyB,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE3D,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAYF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,uBAAuB,CAAC;IACtC,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,GAAG,kBAAkB,CA2DrB"}