@openclaw/fs-safe 0.2.0 → 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 (65) hide show
  1. package/CHANGELOG.md +16 -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/home-dir.d.ts.map +1 -1
  21. package/dist/home-dir.js +38 -33
  22. package/dist/install-safe-path.d.ts +20 -0
  23. package/dist/install-safe-path.d.ts.map +1 -0
  24. package/dist/install-safe-path.js +94 -0
  25. package/dist/json-file.d.ts +3 -0
  26. package/dist/json-file.d.ts.map +1 -0
  27. package/dist/json-file.js +123 -0
  28. package/dist/json-files.d.ts +20 -0
  29. package/dist/json-files.d.ts.map +1 -0
  30. package/dist/json-files.js +153 -0
  31. package/dist/path-alias-guards.d.ts +19 -0
  32. package/dist/path-alias-guards.d.ts.map +1 -0
  33. package/dist/path-alias-guards.js +21 -0
  34. package/dist/path-guards.d.ts +7 -0
  35. package/dist/path-guards.d.ts.map +1 -0
  36. package/dist/path-guards.js +49 -0
  37. package/dist/path-safety.d.ts +12 -0
  38. package/dist/path-safety.d.ts.map +1 -0
  39. package/dist/path-safety.js +50 -0
  40. package/dist/path.d.ts.map +1 -1
  41. package/dist/path.js +4 -3
  42. package/dist/pinned-python-config.d.ts.map +1 -1
  43. package/dist/pinned-python-config.js +2 -3
  44. package/dist/private-file-store.d.ts +7 -5
  45. package/dist/private-file-store.d.ts.map +1 -1
  46. package/dist/private-file-store.js +34 -21
  47. package/dist/root-impl.js +5 -2
  48. package/dist/safe-open-sync.d.ts +24 -0
  49. package/dist/safe-open-sync.d.ts.map +1 -0
  50. package/dist/safe-open-sync.js +71 -0
  51. package/dist/safe-root.d.ts +123 -0
  52. package/dist/safe-root.d.ts.map +1 -0
  53. package/dist/safe-root.js +1060 -0
  54. package/dist/secret-file.d.ts.map +1 -1
  55. package/dist/secret-file.js +22 -6
  56. package/dist/secure-temp-workspace.d.ts +25 -0
  57. package/dist/secure-temp-workspace.d.ts.map +1 -0
  58. package/dist/secure-temp-workspace.js +136 -0
  59. package/dist/sibling-temp-file.d.ts +16 -0
  60. package/dist/sibling-temp-file.d.ts.map +1 -0
  61. package/dist/sibling-temp-file.js +73 -0
  62. package/dist/sibling-temp-write.d.ts +8 -0
  63. package/dist/sibling-temp-write.d.ts.map +1 -0
  64. package/dist/sibling-temp-write.js +40 -0
  65. package/package.json +1 -1
@@ -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
  }
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
  }
@@ -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"}
@@ -0,0 +1,71 @@
1
+ import fs from "node:fs";
2
+ import { sameFileIdentity as hasSameFileIdentity } from "./file-identity.js";
3
+ function isExpectedPathError(error) {
4
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
5
+ return code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP";
6
+ }
7
+ function sameFileIdentity(left, right) {
8
+ return hasSameFileIdentity(left, right);
9
+ }
10
+ export function openVerifiedFileSync(params) {
11
+ const ioFs = params.ioFs ?? fs;
12
+ const allowedType = params.allowedType ?? "file";
13
+ const openReadFlags = ioFs.constants.O_RDONLY |
14
+ (typeof ioFs.constants.O_NOFOLLOW === "number" ? ioFs.constants.O_NOFOLLOW : 0);
15
+ let fd = null;
16
+ try {
17
+ if (params.rejectPathSymlink) {
18
+ const candidateStat = ioFs.lstatSync(params.filePath);
19
+ if (candidateStat.isSymbolicLink()) {
20
+ return { ok: false, reason: "validation" };
21
+ }
22
+ }
23
+ const realPath = params.resolvedPath ?? ioFs.realpathSync(params.filePath);
24
+ const preOpenStat = ioFs.lstatSync(realPath);
25
+ if (!isAllowedType(preOpenStat, allowedType)) {
26
+ return { ok: false, reason: "validation" };
27
+ }
28
+ if (params.rejectHardlinks && preOpenStat.isFile() && preOpenStat.nlink > 1) {
29
+ return { ok: false, reason: "validation" };
30
+ }
31
+ if (params.maxBytes !== undefined &&
32
+ preOpenStat.isFile() &&
33
+ preOpenStat.size > params.maxBytes) {
34
+ return { ok: false, reason: "validation" };
35
+ }
36
+ fd = ioFs.openSync(realPath, openReadFlags);
37
+ const openedStat = ioFs.fstatSync(fd);
38
+ if (!isAllowedType(openedStat, allowedType)) {
39
+ return { ok: false, reason: "validation" };
40
+ }
41
+ if (params.rejectHardlinks && openedStat.isFile() && openedStat.nlink > 1) {
42
+ return { ok: false, reason: "validation" };
43
+ }
44
+ if (params.maxBytes !== undefined && openedStat.isFile() && openedStat.size > params.maxBytes) {
45
+ return { ok: false, reason: "validation" };
46
+ }
47
+ if (!sameFileIdentity(preOpenStat, openedStat)) {
48
+ return { ok: false, reason: "validation" };
49
+ }
50
+ const opened = { ok: true, path: realPath, fd, stat: openedStat };
51
+ fd = null;
52
+ return opened;
53
+ }
54
+ catch (error) {
55
+ if (isExpectedPathError(error)) {
56
+ return { ok: false, reason: "path", error };
57
+ }
58
+ return { ok: false, reason: "io", error };
59
+ }
60
+ finally {
61
+ if (fd !== null) {
62
+ ioFs.closeSync(fd);
63
+ }
64
+ }
65
+ }
66
+ function isAllowedType(stat, allowedType) {
67
+ if (allowedType === "directory") {
68
+ return stat.isDirectory();
69
+ }
70
+ return stat.isFile();
71
+ }
@@ -0,0 +1,123 @@
1
+ import type { Stats } from "node:fs";
2
+ import type { FileHandle } from "node:fs/promises";
3
+ import type { SafeDirEntry, SafePathStat } from "./types.js";
4
+ export type SafeOpenResult = {
5
+ handle: FileHandle;
6
+ realPath: string;
7
+ stat: Stats;
8
+ };
9
+ export type SafeLocalReadResult = {
10
+ buffer: Buffer;
11
+ realPath: string;
12
+ stat: Stats;
13
+ };
14
+ export type SafeRootContext = {
15
+ rootDir: string;
16
+ rootReal: string;
17
+ rootWithSep: string;
18
+ };
19
+ export type SafeRootOptions = {
20
+ rootDir: string;
21
+ defaults?: SafeRootDefaults;
22
+ };
23
+ export type SafeRootDefaults = {
24
+ append?: SafeRootAppendOptions;
25
+ copy?: SafeRootCopyOptions;
26
+ /** openFile merges read defaults first, then open defaults, then call options. */
27
+ open?: SafeRootOpenOptions;
28
+ openWritable?: SafeRootOpenWritableOptions;
29
+ read?: SafeRootReadOptions;
30
+ write?: SafeRootWriteOptions;
31
+ };
32
+ export type SafeRootSymlinkPolicy = "reject" | "follow-within-root";
33
+ export type SafeRootReadOptions = {
34
+ rejectHardlinks?: boolean;
35
+ nonBlockingRead?: boolean;
36
+ symlinkPolicy?: SafeRootSymlinkPolicy;
37
+ maxBytes?: number;
38
+ };
39
+ export type SafeRootOpenOptions = Omit<SafeRootReadOptions, "maxBytes">;
40
+ export type SafeRootWriteOptions = {
41
+ encoding?: BufferEncoding;
42
+ mkdir?: boolean;
43
+ };
44
+ export type SafeRootOpenWritableOptions = {
45
+ mkdir?: boolean;
46
+ mode?: number;
47
+ truncateExisting?: boolean;
48
+ append?: boolean;
49
+ };
50
+ export type SafeRootCopyOptions = {
51
+ maxBytes?: number;
52
+ mkdir?: boolean;
53
+ rejectSourceHardlinks?: boolean;
54
+ };
55
+ export type SafeRootWriteJsonOptions = SafeRootWriteOptions & {
56
+ replacer?: Parameters<typeof JSON.stringify>[1];
57
+ space?: Parameters<typeof JSON.stringify>[2];
58
+ trailingNewline?: boolean;
59
+ };
60
+ export type SafeRootCreateOptions = SafeRootWriteOptions;
61
+ export type SafeRootCreateJsonOptions = SafeRootWriteJsonOptions;
62
+ export type SafeRootAppendOptions = {
63
+ encoding?: BufferEncoding;
64
+ mkdir?: boolean;
65
+ prependNewlineIfNeeded?: boolean;
66
+ };
67
+ export declare class SafeRoot {
68
+ readonly rootDir: string;
69
+ readonly rootReal: string;
70
+ readonly rootWithSep: string;
71
+ readonly defaults: SafeRootDefaults;
72
+ constructor(context: SafeRootContext, defaults?: SafeRootDefaults);
73
+ private get context();
74
+ resolve(relativePath: string): Promise<string>;
75
+ openFile(relativePath: string, options?: SafeRootOpenOptions): Promise<SafeOpenResult>;
76
+ readFile(relativePath: string, options?: SafeRootReadOptions): Promise<SafeLocalReadResult>;
77
+ readFileBytes(relativePath: string, options?: SafeRootReadOptions): Promise<Buffer>;
78
+ readText(relativePath: string, options?: SafeRootReadOptions & {
79
+ encoding?: BufferEncoding;
80
+ }): Promise<string>;
81
+ readJson<T = unknown>(relativePath: string, options?: SafeRootReadOptions & {
82
+ encoding?: BufferEncoding;
83
+ }): Promise<T>;
84
+ readPath(filePath: string, options?: Pick<SafeRootReadOptions, "rejectHardlinks" | "maxBytes">): Promise<SafeLocalReadResult>;
85
+ reader(options?: Pick<SafeRootReadOptions, "rejectHardlinks" | "maxBytes">): (filePath: string) => Promise<Buffer>;
86
+ openWritable(relativePath: string, options?: SafeRootOpenWritableOptions): Promise<SafeWritableOpenResult>;
87
+ append(relativePath: string, data: string | Buffer, options?: SafeRootAppendOptions): Promise<void>;
88
+ removeFile(relativePath: string): Promise<void>;
89
+ mkdirp(relativePath: string, options?: {
90
+ allowRoot?: boolean;
91
+ }): Promise<void>;
92
+ writeFile(relativePath: string, data: string | Buffer, options?: SafeRootWriteOptions): Promise<void>;
93
+ createFile(relativePath: string, data: string | Buffer, options?: SafeRootCreateOptions): Promise<boolean>;
94
+ writeJson(relativePath: string, data: unknown, options?: SafeRootWriteJsonOptions): Promise<void>;
95
+ createJson(relativePath: string, data: unknown, options?: SafeRootCreateJsonOptions): Promise<boolean>;
96
+ copyFileFrom(sourcePath: string, relativePath: string, options?: SafeRootCopyOptions): Promise<void>;
97
+ statPath(relativePath: string): Promise<SafePathStat>;
98
+ listDir(relativePath: string, options?: {
99
+ withFileTypes?: false;
100
+ }): Promise<string[]>;
101
+ listDir(relativePath: string, options: {
102
+ withFileTypes: true;
103
+ }): Promise<SafeDirEntry[]>;
104
+ move(from: string, to: string, options?: {
105
+ overwrite?: boolean;
106
+ }): Promise<void>;
107
+ }
108
+ export declare function safeRoot(rootDir: string, defaults?: SafeRootDefaults): Promise<SafeRoot>;
109
+ export declare function readLocalFileSafely(params: {
110
+ filePath: string;
111
+ maxBytes?: number;
112
+ }): Promise<SafeLocalReadResult>;
113
+ export declare function openLocalFileSafely(params: {
114
+ filePath: string;
115
+ }): Promise<SafeOpenResult>;
116
+ export type SafeWritableOpenResult = {
117
+ handle: FileHandle;
118
+ createdForWrite: boolean;
119
+ realPath: string;
120
+ stat: Stats;
121
+ };
122
+ export declare function resolveOpenedFileRealPathForHandle(handle: FileHandle, ioPath: string): Promise<string>;
123
+ //# sourceMappingURL=safe-root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-root.d.ts","sourceRoot":"","sources":["../src/safe-root.ts"],"names":[],"mappings":"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"}