@openclaw/fs-safe 0.1.0 → 0.1.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 (114) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +23 -3
  3. package/dist/archive-staging.d.ts.map +1 -1
  4. package/dist/archive-staging.js +51 -7
  5. package/dist/archive.js +2 -2
  6. package/dist/file-store-boundary.d.ts +27 -0
  7. package/dist/file-store-boundary.d.ts.map +1 -0
  8. package/dist/file-store-boundary.js +164 -0
  9. package/dist/file-store.d.ts.map +1 -1
  10. package/dist/file-store.js +62 -72
  11. package/dist/path.d.ts +1 -0
  12. package/dist/path.d.ts.map +1 -1
  13. package/dist/path.js +6 -3
  14. package/dist/private-file-store.d.ts +5 -7
  15. package/dist/private-file-store.d.ts.map +1 -1
  16. package/dist/private-file-store.js +21 -34
  17. package/dist/root-context.d.ts +23 -0
  18. package/dist/root-context.d.ts.map +1 -0
  19. package/dist/root-context.js +62 -0
  20. package/dist/root-errors.d.ts +4 -0
  21. package/dist/root-errors.d.ts.map +1 -0
  22. package/dist/root-errors.js +21 -0
  23. package/dist/root-impl.d.ts +108 -0
  24. package/dist/root-impl.d.ts.map +1 -0
  25. package/dist/{safe-root.js → root-impl.js} +464 -251
  26. package/dist/root.d.ts +1 -107
  27. package/dist/root.d.ts.map +1 -1
  28. package/dist/root.js +1 -1332
  29. package/docs/advanced.md +100 -0
  30. package/docs/archive.md +199 -0
  31. package/docs/atomic.md +163 -0
  32. package/docs/config.md +65 -0
  33. package/docs/contributing.md +80 -0
  34. package/docs/errors.md +154 -0
  35. package/docs/file-store.md +213 -0
  36. package/docs/filename.md +0 -0
  37. package/docs/index.md +76 -0
  38. package/docs/install-path.md +167 -0
  39. package/docs/install.md +150 -0
  40. package/docs/json-store.md +200 -0
  41. package/docs/json.md +158 -0
  42. package/docs/local-roots.md +164 -0
  43. package/docs/path-scope.md +130 -0
  44. package/docs/path.md +191 -0
  45. package/docs/permissions.md +95 -0
  46. package/docs/private-file-store.md +45 -0
  47. package/docs/python-helper.md +103 -0
  48. package/docs/quickstart.md +147 -0
  49. package/docs/reading.md +174 -0
  50. package/docs/regular-file.md +175 -0
  51. package/docs/root.md +205 -0
  52. package/docs/secret-file.md +154 -0
  53. package/docs/secure-file.md +82 -0
  54. package/docs/security-model.md +99 -0
  55. package/docs/sidecar-lock.md +229 -0
  56. package/docs/store.md +44 -0
  57. package/docs/temp.md +276 -0
  58. package/docs/test-hooks.md +74 -0
  59. package/docs/testing.md +168 -0
  60. package/docs/timing.md +121 -0
  61. package/docs/types.md +155 -0
  62. package/docs/walk.md +63 -0
  63. package/docs/writing.md +222 -0
  64. package/package.json +5 -2
  65. package/dist/archive-utils.d.ts +0 -3
  66. package/dist/archive-utils.d.ts.map +0 -1
  67. package/dist/archive-utils.js +0 -26
  68. package/dist/boundary-file-read.d.ts +0 -44
  69. package/dist/boundary-file-read.d.ts.map +0 -1
  70. package/dist/boundary-file-read.js +0 -129
  71. package/dist/boundary-path.d.ts +0 -39
  72. package/dist/boundary-path.d.ts.map +0 -1
  73. package/dist/boundary-path.js +0 -598
  74. package/dist/fs-pinned-path-helper.d.ts +0 -7
  75. package/dist/fs-pinned-path-helper.d.ts.map +0 -1
  76. package/dist/fs-pinned-path-helper.js +0 -182
  77. package/dist/fs-pinned-write-helper.d.ts +0 -21
  78. package/dist/fs-pinned-write-helper.d.ts.map +0 -1
  79. package/dist/fs-pinned-write-helper.js +0 -263
  80. package/dist/hardlink-guards.d.ts +0 -7
  81. package/dist/hardlink-guards.d.ts.map +0 -1
  82. package/dist/hardlink-guards.js +0 -30
  83. package/dist/install-safe-path.d.ts +0 -20
  84. package/dist/install-safe-path.d.ts.map +0 -1
  85. package/dist/install-safe-path.js +0 -94
  86. package/dist/json-file.d.ts +0 -3
  87. package/dist/json-file.d.ts.map +0 -1
  88. package/dist/json-file.js +0 -123
  89. package/dist/json-files.d.ts +0 -20
  90. package/dist/json-files.d.ts.map +0 -1
  91. package/dist/json-files.js +0 -153
  92. package/dist/path-alias-guards.d.ts +0 -19
  93. package/dist/path-alias-guards.d.ts.map +0 -1
  94. package/dist/path-alias-guards.js +0 -21
  95. package/dist/path-guards.d.ts +0 -7
  96. package/dist/path-guards.d.ts.map +0 -1
  97. package/dist/path-guards.js +0 -49
  98. package/dist/path-safety.d.ts +0 -12
  99. package/dist/path-safety.d.ts.map +0 -1
  100. package/dist/path-safety.js +0 -50
  101. package/dist/safe-open-sync.d.ts +0 -24
  102. package/dist/safe-open-sync.d.ts.map +0 -1
  103. package/dist/safe-open-sync.js +0 -71
  104. package/dist/safe-root.d.ts +0 -123
  105. package/dist/safe-root.d.ts.map +0 -1
  106. package/dist/secure-temp-workspace.d.ts +0 -25
  107. package/dist/secure-temp-workspace.d.ts.map +0 -1
  108. package/dist/secure-temp-workspace.js +0 -136
  109. package/dist/sibling-temp-file.d.ts +0 -16
  110. package/dist/sibling-temp-file.d.ts.map +0 -1
  111. package/dist/sibling-temp-file.js +0 -73
  112. package/dist/sibling-temp-write.d.ts +0 -8
  113. package/dist/sibling-temp-write.d.ts.map +0 -1
  114. package/dist/sibling-temp-write.js +0 -40
@@ -0,0 +1,222 @@
1
+ # Writing
2
+
3
+ The `Root` handle exposes a tight set of write verbs. Each one is atomic at the destination — no half-written intermediate state — and goes through the same boundary checks as reads.
4
+
5
+ ```ts
6
+ await fs.write("state.json", body);
7
+ await fs.create("seed.json", initial); // throws if exists
8
+ await fs.writeJson("config.json", state);
9
+ await fs.append("logs/today.log", line);
10
+ await fs.copyIn("inbox/upload.bin", "/tmp/upload.bin");
11
+ await fs.move("notes/draft.md", "notes/published.md");
12
+ await fs.remove("logs/yesterday.log");
13
+ await fs.mkdir("snapshots/2026/05");
14
+ ```
15
+
16
+ ## What every write does
17
+
18
+ 1. Resolve the relative target against the canonical root and reject anything that escapes (`outside-workspace`).
19
+ 2. If `mkdir: true`, create missing parent directories with the parent fd pinned.
20
+ 3. Open the parent directory by fd. Subsequent rename/unlink uses the parent fd, not the path string, so a parent-directory symlink swap mid-call cannot divert the write.
21
+ 4. Write data to a sibling temp file in the same directory.
22
+ 5. Atomically rename the temp file over the destination.
23
+ 6. Stat the resulting fd and verify identity.
24
+
25
+ A failure at any point either leaves the destination at its previous contents or surfaces an `FsSafeError` — never a partially-written file at the destination path.
26
+
27
+ ## Write verbs
28
+
29
+ ### `fs.write(rel, data, options?)`
30
+
31
+ Overwrite or create. Always atomic.
32
+
33
+ ```ts
34
+ await fs.write("state/last-run.json", JSON.stringify(run));
35
+ await fs.write("notes/today.txt", "hello\n", { encoding: "utf8" });
36
+ ```
37
+
38
+ `data` accepts `string | Buffer`. `options` are `{ encoding?: BufferEncoding; mkdir?: boolean; mode?: number; overwrite?: boolean }`. `mode` sets the file's POSIX mode; if omitted, falls back to the `mode` from `RootDefaults` and then to umask. `overwrite` defaults to `true`; set it to `false` for the same no-clobber behavior as `create()`.
39
+
40
+ ### `fs.create(rel, data, options?)`
41
+
42
+ Don't-clobber variant of `write()`. Throws `already-exists` if the target is there.
43
+
44
+ ```ts
45
+ try {
46
+ await fs.create("config/seed.json", initial);
47
+ } catch (err) {
48
+ if (err instanceof FsSafeError && err.code !== "already-exists") throw err;
49
+ }
50
+ ```
51
+
52
+ ### `fs.writeJson(rel, value, options?)`
53
+
54
+ `JSON.stringify(value, replacer, space)` + atomic write. Adds a trailing newline by default.
55
+
56
+ ```ts
57
+ await fs.writeJson("config.json", state, { space: 2 });
58
+ await fs.writeJson("compact.json", state, { trailingNewline: false });
59
+ ```
60
+
61
+ Options:
62
+
63
+ ```ts
64
+ type RootWriteJsonOptions = {
65
+ encoding?: BufferEncoding;
66
+ mkdir?: boolean;
67
+ mode?: number;
68
+ replacer?: (this: any, key: string, value: any) => any | (number | string)[];
69
+ space?: number | string;
70
+ trailingNewline?: boolean; // default true
71
+ };
72
+ ```
73
+
74
+ `createJson(rel, value, options?)` is the don't-clobber variant.
75
+
76
+ ### `fs.append(rel, data, options?)`
77
+
78
+ Open in append mode, write, close. Honors `mkdir` for the parent directory. Pass `prependNewlineIfNeeded: true` to insert a `\n` if the file does not already end in one.
79
+
80
+ ```ts
81
+ await fs.append("logs/today.log", `[${ts}] ${line}\n`);
82
+ await fs.append("notes/scratch.md", "* new bullet", { prependNewlineIfNeeded: true });
83
+ ```
84
+
85
+ For high-volume logging, consider [`openWritable`](#openwritable) and a long-lived append handle.
86
+
87
+ ### `fs.copyIn(rel, sourceAbsPath, options?)`
88
+
89
+ Bring a file from outside the root into the root, atomically. The source path must be absolute. The library streams the source through the boundary, writes to a sibling temp, and renames over the destination.
90
+
91
+ ```ts
92
+ await fs.copyIn("inbox/upload.bin", "/tmp/incoming.bin", {
93
+ maxBytes: 64 * 1024 * 1024,
94
+ });
95
+ ```
96
+
97
+ Options: `{ encoding?, mkdir?, maxBytes?, sourceHardlinks? }`. Use `sourceHardlinks: "reject"` to refuse if the source itself is a hardlinked alias.
98
+
99
+ ### `fs.move(from, to, options?)`
100
+
101
+ Rename one path inside the root to another. Defaults to no clobber:
102
+
103
+ ```ts
104
+ await fs.move("incoming/foo.txt", "archive/foo.txt");
105
+ await fs.move("incoming/foo.txt", "archive/foo.txt", { overwrite: true });
106
+ ```
107
+
108
+ Both `from` and `to` are bounded; `..` in either is rejected.
109
+
110
+ ### `fs.remove(rel)`
111
+
112
+ Unlink a file or `rmdir` an empty directory. Non-empty directories throw `not-empty`. For atomic directory replacement, use [`replaceDirectoryAtomic`](atomic.md#replacedirectoryatomic).
113
+
114
+ ```ts
115
+ await fs.remove("logs/yesterday.log");
116
+ await fs.remove("snapshots/empty-dir"); // ok
117
+ await fs.remove("snapshots/full-dir"); // throws not-empty
118
+ ```
119
+
120
+ ### `fs.mkdir(rel)`
121
+
122
+ `mkdir -p`. Creates missing parents.
123
+
124
+ ```ts
125
+ await fs.mkdir("snapshots/2026/05");
126
+ ```
127
+
128
+ ### `fs.ensureRoot()`
129
+
130
+ Treats `""` / `"."` as the root itself. Useful when a generic helper computes a relative directory and might end up at the root.
131
+
132
+ ```ts
133
+ const targetRel = path.relative(fs.rootReal, candidateAbs); // could be "" if candidateAbs === root
134
+ await fs.ensureRoot(); // accepts "" without throwing
135
+ ```
136
+
137
+ ## `openWritable()` for streaming
138
+
139
+ When `write` doesn't fit (very large outputs, slow producers), open a writable handle:
140
+
141
+ ```ts
142
+ const opened = await fs.openWritable("logs/current.log", { writeMode: "append" });
143
+ try {
144
+ for await (const chunk of source) {
145
+ await opened.handle.appendFile(chunk);
146
+ }
147
+ } finally {
148
+ await opened.handle.close();
149
+ }
150
+ ```
151
+
152
+ Options: `{ mkdir?, mode?, writeMode? }`, where `writeMode` is `"replace"` (default), `"append"`, or `"update"`. `replace` truncates existing files; `update` keeps existing contents. Streaming writes go directly to the destination — there is no atomic-rename step. If you need both streaming and atomicity, write to a sibling temp yourself and rename when done; the [`atomic`](atomic.md) helpers can do this for you.
153
+
154
+ ## Write defaults vs per-call options
155
+
156
+ Set `mkdir: true` once on `root()`; pass text encodings per call when needed:
157
+
158
+ ```ts
159
+ const fs = await root("/srv/workspace", {
160
+ mkdir: true,
161
+ });
162
+
163
+ await fs.write("notes/today.txt", "ascii", { encoding: "utf8" });
164
+ await fs.write("data/blob.bin", buffer); // mkdir true, no encoding because data is Buffer
165
+ await fs.write("data/blob.bin", buffer, { mkdir: false }); // override
166
+ ```
167
+
168
+ ## Errors you'll catch
169
+
170
+ | Code | When |
171
+ |---|---|
172
+ | `outside-workspace` | Target resolves outside the root. |
173
+ | `already-exists` | `create()` / `createJson()` / `move({ overwrite: false })` hit an existing target. |
174
+ | `not-found` | Parent does not exist and `mkdir` is false. |
175
+ | `not-empty` | `remove()` on a non-empty directory. |
176
+ | `not-removable` | `remove()` could not unlink/rmdir (typically permissions or device busy). |
177
+ | `path-mismatch` | Post-write fd identity check did not match. Almost always a parallel writer. |
178
+ | `too-large` | `copyIn()` source exceeded `maxBytes`. |
179
+ | `symlink` | A path component is a symlink and policy is `reject`. |
180
+ | `hardlink` | `sourceHardlinks: "reject"` saw `nlink > 1`. |
181
+
182
+ Full list in [Errors](errors.md).
183
+
184
+ ## Common patterns
185
+
186
+ ### Replace if changed
187
+
188
+ ```ts
189
+ const next = JSON.stringify(state);
190
+ const prev = await fs.readText("state.json").catch(() => "");
191
+ if (prev !== next) await fs.write("state.json", next);
192
+ ```
193
+
194
+ ### Stage many writes, then commit
195
+
196
+ ```ts
197
+ const stagingDir = "snapshots/incoming";
198
+ await fs.mkdir(stagingDir);
199
+ for (const file of files) await fs.write(`${stagingDir}/${file.name}`, file.body);
200
+ await fs.move(stagingDir, "snapshots/2026-05-05", { overwrite: true });
201
+ ```
202
+
203
+ For a true commit-or-rollback over a *directory*, use [`replaceDirectoryAtomic`](atomic.md#replacedirectoryatomic).
204
+
205
+ ### Rotate logs
206
+
207
+ ```ts
208
+ const today = `logs/${formatDate(new Date())}.log`;
209
+ try {
210
+ await fs.create(today, "");
211
+ } catch (err) {
212
+ if (!(err instanceof FsSafeError) || err.code !== "already-exists") throw err;
213
+ }
214
+ await fs.append(today, line);
215
+ ```
216
+
217
+ ## See also
218
+
219
+ - [Atomic writes](atomic.md) — the lower-level `replaceFileAtomic` and friends.
220
+ - [JSON files](json.md) — standalone JSON helpers without going through `root()`.
221
+ - [Reading](reading.md) — companion read API.
222
+ - [Errors](errors.md) — every code, when it fires.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/fs-safe",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,6 +11,7 @@
11
11
  "dist/**/*.js",
12
12
  "dist/**/*.d.ts",
13
13
  "dist/**/*.d.ts.map",
14
+ "docs/**/*.md",
14
15
  "README.md",
15
16
  "CHANGELOG.md",
16
17
  "SECURITY.md",
@@ -95,10 +96,12 @@
95
96
  "scripts": {
96
97
  "benchmark": "node scripts/benchmark.mjs",
97
98
  "build": "tsc -p tsconfig.json",
99
+ "lint:file-size": "node scripts/check-file-size.mjs",
98
100
  "prepack": "node scripts/prepack-build.mjs",
99
101
  "test": "vitest run",
100
102
  "test:coverage": "vitest run --coverage",
101
- "check": "pnpm build && pnpm test",
103
+ "test:security": "vitest run test/fs-safe.test.ts test/openclaw-read-bypass-parity.test.ts test/openclaw-write-bypass-parity.test.ts test/additional-bypass-parity.test.ts test/adversarial-boundary-payloads.test.ts",
104
+ "check": "pnpm lint:file-size && pnpm build && pnpm test",
102
105
  "docs:site": "node scripts/build-docs-site.mjs"
103
106
  },
104
107
  "optionalDependencies": {
@@ -1,3 +0,0 @@
1
- export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T>;
2
- export declare function fileExists(filePath: string): Promise<boolean>;
3
- //# sourceMappingURL=archive-utils.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"archive-utils.d.ts","sourceRoot":"","sources":["../src/archive-utils.ts"],"names":[],"mappings":"AAEA,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,CAAC,CAAC,CAiBZ;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE"}
@@ -1,26 +0,0 @@
1
- import fs from "node:fs/promises";
2
- export async function withTimeout(promise, timeoutMs, label) {
3
- let timeoutId;
4
- try {
5
- return await Promise.race([
6
- promise,
7
- new Promise((_, reject) => {
8
- timeoutId = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
9
- }),
10
- ]);
11
- }
12
- finally {
13
- if (timeoutId) {
14
- clearTimeout(timeoutId);
15
- }
16
- }
17
- }
18
- export async function fileExists(filePath) {
19
- try {
20
- await fs.stat(filePath);
21
- return true;
22
- }
23
- catch {
24
- return false;
25
- }
26
- }
@@ -1,44 +0,0 @@
1
- import fs from "node:fs";
2
- import type { PathAliasPolicy } from "./path-alias-guards.js";
3
- import { type SafeOpenSyncAllowedType, type SafeOpenSyncFailureReason } from "./safe-open-sync.js";
4
- type BoundaryReadFs = Pick<typeof fs, "closeSync" | "constants" | "fstatSync" | "lstatSync" | "openSync" | "readFileSync" | "realpathSync">;
5
- export type BoundaryFileOpenFailureReason = SafeOpenSyncFailureReason | "validation";
6
- export type BoundaryFileOpenResult = {
7
- ok: true;
8
- path: string;
9
- fd: number;
10
- stat: fs.Stats;
11
- rootRealPath: string;
12
- } | {
13
- ok: false;
14
- reason: BoundaryFileOpenFailureReason;
15
- error?: unknown;
16
- };
17
- export type BoundaryFileOpenFailure = Extract<BoundaryFileOpenResult, {
18
- ok: false;
19
- }>;
20
- export type OpenBoundaryFileSyncParams = {
21
- absolutePath: string;
22
- rootPath: string;
23
- boundaryLabel: string;
24
- rootRealPath?: string;
25
- maxBytes?: number;
26
- rejectHardlinks?: boolean;
27
- allowedType?: SafeOpenSyncAllowedType;
28
- skipLexicalRootCheck?: boolean;
29
- ioFs?: BoundaryReadFs;
30
- };
31
- export type OpenBoundaryFileParams = OpenBoundaryFileSyncParams & {
32
- aliasPolicy?: PathAliasPolicy;
33
- };
34
- export declare function canUseBoundaryFileOpen(ioFs: typeof fs): boolean;
35
- export declare function openBoundaryFileSync(params: OpenBoundaryFileSyncParams): BoundaryFileOpenResult;
36
- export declare function matchBoundaryFileOpenFailure<T>(failure: BoundaryFileOpenFailure, handlers: {
37
- path?: (failure: BoundaryFileOpenFailure) => T;
38
- validation?: (failure: BoundaryFileOpenFailure) => T;
39
- io?: (failure: BoundaryFileOpenFailure) => T;
40
- fallback: (failure: BoundaryFileOpenFailure) => T;
41
- }): T;
42
- export declare function openBoundaryFile(params: OpenBoundaryFileParams): Promise<BoundaryFileOpenResult>;
43
- export {};
44
- //# sourceMappingURL=boundary-file-read.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"boundary-file-read.d.ts","sourceRoot":"","sources":["../src/boundary-file-read.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAOzB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC/B,MAAM,qBAAqB,CAAC;AAE7B,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,EAAE,EACP,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,UAAU,GACV,cAAc,GACd,cAAc,CACjB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,yBAAyB,GAAG,YAAY,CAAC;AAErF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,6BAA6B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE1E,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,sBAAsB,EAAE;IAAE,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC;AAErF,MAAM,MAAM,0BAA0B,GAAG;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,uBAAuB,CAAC;IACtC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,0BAA0B,GAAG;IAChE,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAQF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAW/D;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,0BAA0B,GAAG,sBAAsB,CAuB/F;AAED,wBAAgB,4BAA4B,CAAC,CAAC,EAC5C,OAAO,EAAE,uBAAuB,EAChC,QAAQ,EAAE;IACR,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;IAC/C,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;IACrD,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;IAC7C,QAAQ,EAAE,CAAC,OAAO,EAAE,uBAAuB,KAAK,CAAC,CAAC;CACnD,GACA,CAAC,CAUH;AAoDD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,sBAAsB,CAAC,CAsBjC"}
@@ -1,129 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { resolveBoundaryPath, resolveBoundaryPathSync, } from "./boundary-path.js";
4
- import { openVerifiedFileSync, } from "./safe-open-sync.js";
5
- export function canUseBoundaryFileOpen(ioFs) {
6
- return (typeof ioFs.openSync === "function" &&
7
- typeof ioFs.closeSync === "function" &&
8
- typeof ioFs.fstatSync === "function" &&
9
- typeof ioFs.lstatSync === "function" &&
10
- typeof ioFs.realpathSync === "function" &&
11
- typeof ioFs.readFileSync === "function" &&
12
- typeof ioFs.constants === "object" &&
13
- ioFs.constants !== null);
14
- }
15
- export function openBoundaryFileSync(params) {
16
- const ioFs = params.ioFs ?? fs;
17
- const resolved = resolveBoundaryFilePathGeneric({
18
- absolutePath: params.absolutePath,
19
- resolve: (absolutePath) => resolveBoundaryPathSync({
20
- absolutePath,
21
- rootPath: params.rootPath,
22
- rootCanonicalPath: params.rootRealPath,
23
- boundaryLabel: params.boundaryLabel,
24
- skipLexicalRootCheck: params.skipLexicalRootCheck,
25
- }),
26
- });
27
- if (resolved instanceof Promise) {
28
- return toBoundaryValidationError(new Error("Unexpected async boundary resolution"));
29
- }
30
- return finalizeBoundaryFileOpen({
31
- resolved,
32
- maxBytes: params.maxBytes,
33
- rejectHardlinks: params.rejectHardlinks,
34
- allowedType: params.allowedType,
35
- ioFs,
36
- });
37
- }
38
- export function matchBoundaryFileOpenFailure(failure, handlers) {
39
- switch (failure.reason) {
40
- case "path":
41
- return handlers.path ? handlers.path(failure) : handlers.fallback(failure);
42
- case "validation":
43
- return handlers.validation ? handlers.validation(failure) : handlers.fallback(failure);
44
- case "io":
45
- return handlers.io ? handlers.io(failure) : handlers.fallback(failure);
46
- }
47
- return handlers.fallback(failure);
48
- }
49
- function openBoundaryFileResolved(params) {
50
- const opened = openVerifiedFileSync({
51
- filePath: params.absolutePath,
52
- resolvedPath: params.resolvedPath,
53
- rejectHardlinks: params.rejectHardlinks ?? true,
54
- maxBytes: params.maxBytes,
55
- allowedType: params.allowedType,
56
- ioFs: params.ioFs,
57
- });
58
- if (!opened.ok) {
59
- return opened;
60
- }
61
- return {
62
- ok: true,
63
- path: opened.path,
64
- fd: opened.fd,
65
- stat: opened.stat,
66
- rootRealPath: params.rootRealPath,
67
- };
68
- }
69
- function finalizeBoundaryFileOpen(params) {
70
- if ("ok" in params.resolved) {
71
- return params.resolved;
72
- }
73
- return openBoundaryFileResolved({
74
- absolutePath: params.resolved.absolutePath,
75
- resolvedPath: params.resolved.resolvedPath,
76
- rootRealPath: params.resolved.rootRealPath,
77
- maxBytes: params.maxBytes,
78
- rejectHardlinks: params.rejectHardlinks,
79
- allowedType: params.allowedType,
80
- ioFs: params.ioFs,
81
- });
82
- }
83
- export async function openBoundaryFile(params) {
84
- const ioFs = params.ioFs ?? fs;
85
- const maybeResolved = resolveBoundaryFilePathGeneric({
86
- absolutePath: params.absolutePath,
87
- resolve: (absolutePath) => resolveBoundaryPath({
88
- absolutePath,
89
- rootPath: params.rootPath,
90
- rootCanonicalPath: params.rootRealPath,
91
- boundaryLabel: params.boundaryLabel,
92
- policy: params.aliasPolicy,
93
- skipLexicalRootCheck: params.skipLexicalRootCheck,
94
- }),
95
- });
96
- const resolved = maybeResolved instanceof Promise ? await maybeResolved : maybeResolved;
97
- return finalizeBoundaryFileOpen({
98
- resolved,
99
- maxBytes: params.maxBytes,
100
- rejectHardlinks: params.rejectHardlinks,
101
- allowedType: params.allowedType,
102
- ioFs,
103
- });
104
- }
105
- function toBoundaryValidationError(error) {
106
- return { ok: false, reason: "validation", error };
107
- }
108
- function mapResolvedBoundaryPath(absolutePath, resolved) {
109
- return {
110
- absolutePath,
111
- resolvedPath: resolved.canonicalPath,
112
- rootRealPath: resolved.rootCanonicalPath,
113
- };
114
- }
115
- function resolveBoundaryFilePathGeneric(params) {
116
- const absolutePath = path.resolve(params.absolutePath);
117
- try {
118
- const resolved = params.resolve(absolutePath);
119
- if (resolved instanceof Promise) {
120
- return resolved
121
- .then((value) => mapResolvedBoundaryPath(absolutePath, value))
122
- .catch((error) => toBoundaryValidationError(error));
123
- }
124
- return mapResolvedBoundaryPath(absolutePath, resolved);
125
- }
126
- catch (error) {
127
- return toBoundaryValidationError(error);
128
- }
129
- }
@@ -1,39 +0,0 @@
1
- type BoundaryPathIntent = "read" | "write" | "create" | "delete" | "stat";
2
- export type BoundaryPathAliasPolicy = {
3
- allowFinalSymlinkForUnlink?: boolean;
4
- allowFinalHardlinkForUnlink?: boolean;
5
- };
6
- export declare const BOUNDARY_PATH_ALIAS_POLICIES: {
7
- readonly strict: Readonly<{
8
- allowFinalSymlinkForUnlink: false;
9
- allowFinalHardlinkForUnlink: false;
10
- }>;
11
- readonly unlinkTarget: Readonly<{
12
- allowFinalSymlinkForUnlink: true;
13
- allowFinalHardlinkForUnlink: true;
14
- }>;
15
- };
16
- type ResolveBoundaryPathParams = {
17
- absolutePath: string;
18
- rootPath: string;
19
- boundaryLabel: string;
20
- intent?: BoundaryPathIntent;
21
- policy?: BoundaryPathAliasPolicy;
22
- skipLexicalRootCheck?: boolean;
23
- rootCanonicalPath?: string;
24
- };
25
- type ResolvedBoundaryPathKind = "missing" | "file" | "directory" | "symlink" | "other";
26
- export type ResolvedBoundaryPath = {
27
- absolutePath: string;
28
- canonicalPath: string;
29
- rootPath: string;
30
- rootCanonicalPath: string;
31
- relativePath: string;
32
- exists: boolean;
33
- kind: ResolvedBoundaryPathKind;
34
- };
35
- export declare function resolveBoundaryPath(params: ResolveBoundaryPathParams): Promise<ResolvedBoundaryPath>;
36
- export declare function resolveBoundaryPathSync(params: ResolveBoundaryPathParams): ResolvedBoundaryPath;
37
- export declare function resolvePathViaExistingAncestorSync(targetPath: string): string;
38
- export {};
39
- //# sourceMappingURL=boundary-path.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"boundary-path.d.ts","sourceRoot":"","sources":["../src/boundary-path.ts"],"names":[],"mappings":"AAMA,KAAK,kBAAkB,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE1E,MAAM,MAAM,uBAAuB,GAAG;IACpC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,4BAA4B;;;;;;;;;CAS/B,CAAC;AAEX,KAAK,yBAAyB,GAAG;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,KAAK,wBAAwB,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvF,MAAM,MAAM,oBAAoB,GAAG;IACjC,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,wBAAwB,CAAC;CAChC,CAAC;AAEF,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,yBAAyB,GAChC,OAAO,CAAC,oBAAoB,CAAC,CA+B/B;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,yBAAyB,GAAG,oBAAoB,CA+B/F;AAmkBD,wBAAgB,kCAAkC,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA6B7E"}