@openclaw/fs-safe 0.1.1 → 0.2.0

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 (207) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +58 -2
  3. package/dist/absolute-path.d.ts +14 -0
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +203 -2
  6. package/dist/advanced.d.ts +2 -1
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +2 -1
  9. package/dist/archive-staging.d.ts.map +1 -1
  10. package/dist/archive-staging.js +81 -7
  11. package/dist/archive.d.ts.map +1 -1
  12. package/dist/archive.js +25 -42
  13. package/dist/bounded-read-stream.d.ts +8 -0
  14. package/dist/bounded-read-stream.d.ts.map +1 -0
  15. package/dist/bounded-read-stream.js +20 -0
  16. package/dist/config.d.ts +1 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +1 -0
  19. package/dist/directory-guard.d.ts +18 -0
  20. package/dist/directory-guard.d.ts.map +1 -0
  21. package/dist/directory-guard.js +70 -0
  22. package/dist/file-lock.d.ts +4 -2
  23. package/dist/file-lock.d.ts.map +1 -1
  24. package/dist/file-lock.js +13 -2
  25. package/dist/file-store-boundary.d.ts +25 -0
  26. package/dist/file-store-boundary.d.ts.map +1 -0
  27. package/dist/file-store-boundary.js +168 -0
  28. package/dist/file-store-prune.d.ts +12 -0
  29. package/dist/file-store-prune.d.ts.map +1 -0
  30. package/dist/file-store-prune.js +86 -0
  31. package/dist/file-store-source.d.ts +5 -0
  32. package/dist/file-store-source.d.ts.map +1 -0
  33. package/dist/file-store-source.js +30 -0
  34. package/dist/file-store.d.ts +2 -6
  35. package/dist/file-store.d.ts.map +1 -1
  36. package/dist/file-store.js +122 -125
  37. package/dist/guarded-mkdir.d.ts +6 -0
  38. package/dist/guarded-mkdir.d.ts.map +1 -0
  39. package/dist/guarded-mkdir.js +45 -0
  40. package/dist/guarded-mutation.d.ts +33 -0
  41. package/dist/guarded-mutation.d.ts.map +1 -0
  42. package/dist/guarded-mutation.js +76 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +2 -0
  46. package/dist/json-document-store.d.ts +2 -0
  47. package/dist/json-document-store.d.ts.map +1 -1
  48. package/dist/json-document-store.js +7 -3
  49. package/dist/json-durable-queue.d.ts +45 -0
  50. package/dist/json-durable-queue.d.ts.map +1 -0
  51. package/dist/json-durable-queue.js +200 -0
  52. package/dist/json.d.ts +35 -5
  53. package/dist/json.d.ts.map +1 -1
  54. package/dist/json.js +69 -2
  55. package/dist/local-roots.d.ts.map +1 -1
  56. package/dist/local-roots.js +17 -3
  57. package/dist/lock-config.d.ts +10 -0
  58. package/dist/lock-config.d.ts.map +1 -0
  59. package/dist/lock-config.js +12 -0
  60. package/dist/move-path.d.ts +1 -0
  61. package/dist/move-path.d.ts.map +1 -1
  62. package/dist/move-path.js +195 -7
  63. package/dist/output.d.ts +13 -0
  64. package/dist/output.d.ts.map +1 -0
  65. package/dist/output.js +69 -0
  66. package/dist/path-stat.d.ts +4 -0
  67. package/dist/path-stat.d.ts.map +1 -0
  68. package/dist/path-stat.js +15 -0
  69. package/dist/path.d.ts +1 -0
  70. package/dist/path.d.ts.map +1 -1
  71. package/dist/path.js +6 -3
  72. package/dist/pinned-write.d.ts.map +1 -1
  73. package/dist/pinned-write.js +44 -19
  74. package/dist/private-file-store.d.ts +5 -7
  75. package/dist/private-file-store.d.ts.map +1 -1
  76. package/dist/private-file-store.js +21 -34
  77. package/dist/private-temp-workspace.d.ts.map +1 -1
  78. package/dist/private-temp-workspace.js +28 -15
  79. package/dist/regular-file.d.ts.map +1 -1
  80. package/dist/regular-file.js +52 -8
  81. package/dist/replace-directory.d.ts.map +1 -1
  82. package/dist/replace-directory.js +5 -4
  83. package/dist/replace-file.d.ts +1 -1
  84. package/dist/replace-file.d.ts.map +1 -1
  85. package/dist/replace-file.js +93 -31
  86. package/dist/root-context.d.ts +23 -0
  87. package/dist/root-context.d.ts.map +1 -0
  88. package/dist/root-context.js +62 -0
  89. package/dist/root-errors.d.ts +4 -0
  90. package/dist/root-errors.d.ts.map +1 -0
  91. package/dist/root-errors.js +21 -0
  92. package/dist/root-impl.d.ts +108 -0
  93. package/dist/root-impl.d.ts.map +1 -0
  94. package/dist/{safe-root.js → root-impl.js} +484 -265
  95. package/dist/root.d.ts +1 -107
  96. package/dist/root.d.ts.map +1 -1
  97. package/dist/root.js +1 -1332
  98. package/dist/safe-path-segment.d.ts +9 -0
  99. package/dist/safe-path-segment.d.ts.map +1 -0
  100. package/dist/safe-path-segment.js +51 -0
  101. package/dist/secret-file.d.ts.map +1 -1
  102. package/dist/secret-file.js +6 -1
  103. package/dist/sibling-temp.d.ts +1 -0
  104. package/dist/sibling-temp.d.ts.map +1 -1
  105. package/dist/sibling-temp.js +32 -7
  106. package/dist/sidecar-lock.d.ts +2 -0
  107. package/dist/sidecar-lock.d.ts.map +1 -1
  108. package/dist/sidecar-lock.js +93 -13
  109. package/dist/store.d.ts +1 -0
  110. package/dist/store.d.ts.map +1 -1
  111. package/dist/store.js +1 -0
  112. package/dist/temp-target.d.ts.map +1 -1
  113. package/dist/temp-target.js +7 -4
  114. package/dist/test-hooks.d.ts +6 -0
  115. package/dist/test-hooks.d.ts.map +1 -1
  116. package/dist/text-atomic.d.ts +7 -0
  117. package/dist/text-atomic.d.ts.map +1 -1
  118. package/dist/text-atomic.js +3 -2
  119. package/dist/trash.d.ts.map +1 -1
  120. package/dist/trash.js +48 -13
  121. package/docs/advanced.md +110 -0
  122. package/docs/archive.md +199 -0
  123. package/docs/atomic.md +191 -0
  124. package/docs/config.md +94 -0
  125. package/docs/contributing.md +80 -0
  126. package/docs/errors.md +154 -0
  127. package/docs/file-store.md +213 -0
  128. package/docs/filename.md +0 -0
  129. package/docs/index.md +77 -0
  130. package/docs/install-path.md +167 -0
  131. package/docs/install.md +150 -0
  132. package/docs/json-store.md +204 -0
  133. package/docs/json.md +192 -0
  134. package/docs/local-roots.md +164 -0
  135. package/docs/output.md +92 -0
  136. package/docs/path-scope.md +130 -0
  137. package/docs/path.md +191 -0
  138. package/docs/permissions.md +95 -0
  139. package/docs/private-file-store.md +45 -0
  140. package/docs/python-helper.md +103 -0
  141. package/docs/quickstart.md +147 -0
  142. package/docs/reading.md +174 -0
  143. package/docs/regular-file.md +175 -0
  144. package/docs/root.md +205 -0
  145. package/docs/secret-file.md +154 -0
  146. package/docs/secure-file.md +82 -0
  147. package/docs/security-model.md +99 -0
  148. package/docs/sidecar-lock.md +230 -0
  149. package/docs/store.md +77 -0
  150. package/docs/temp.md +284 -0
  151. package/docs/test-hooks.md +74 -0
  152. package/docs/testing.md +178 -0
  153. package/docs/timing.md +121 -0
  154. package/docs/types.md +155 -0
  155. package/docs/walk.md +63 -0
  156. package/docs/writing.md +222 -0
  157. package/package.json +10 -2
  158. package/dist/archive-utils.d.ts +0 -3
  159. package/dist/archive-utils.d.ts.map +0 -1
  160. package/dist/archive-utils.js +0 -26
  161. package/dist/boundary-file-read.d.ts +0 -44
  162. package/dist/boundary-file-read.d.ts.map +0 -1
  163. package/dist/boundary-file-read.js +0 -129
  164. package/dist/boundary-path.d.ts +0 -39
  165. package/dist/boundary-path.d.ts.map +0 -1
  166. package/dist/boundary-path.js +0 -598
  167. package/dist/fs-pinned-path-helper.d.ts +0 -7
  168. package/dist/fs-pinned-path-helper.d.ts.map +0 -1
  169. package/dist/fs-pinned-path-helper.js +0 -182
  170. package/dist/fs-pinned-write-helper.d.ts +0 -21
  171. package/dist/fs-pinned-write-helper.d.ts.map +0 -1
  172. package/dist/fs-pinned-write-helper.js +0 -263
  173. package/dist/hardlink-guards.d.ts +0 -7
  174. package/dist/hardlink-guards.d.ts.map +0 -1
  175. package/dist/hardlink-guards.js +0 -30
  176. package/dist/install-safe-path.d.ts +0 -20
  177. package/dist/install-safe-path.d.ts.map +0 -1
  178. package/dist/install-safe-path.js +0 -94
  179. package/dist/json-file.d.ts +0 -3
  180. package/dist/json-file.d.ts.map +0 -1
  181. package/dist/json-file.js +0 -123
  182. package/dist/json-files.d.ts +0 -20
  183. package/dist/json-files.d.ts.map +0 -1
  184. package/dist/json-files.js +0 -153
  185. package/dist/path-alias-guards.d.ts +0 -19
  186. package/dist/path-alias-guards.d.ts.map +0 -1
  187. package/dist/path-alias-guards.js +0 -21
  188. package/dist/path-guards.d.ts +0 -7
  189. package/dist/path-guards.d.ts.map +0 -1
  190. package/dist/path-guards.js +0 -49
  191. package/dist/path-safety.d.ts +0 -12
  192. package/dist/path-safety.d.ts.map +0 -1
  193. package/dist/path-safety.js +0 -50
  194. package/dist/safe-open-sync.d.ts +0 -24
  195. package/dist/safe-open-sync.d.ts.map +0 -1
  196. package/dist/safe-open-sync.js +0 -71
  197. package/dist/safe-root.d.ts +0 -123
  198. package/dist/safe-root.d.ts.map +0 -1
  199. package/dist/secure-temp-workspace.d.ts +0 -25
  200. package/dist/secure-temp-workspace.d.ts.map +0 -1
  201. package/dist/secure-temp-workspace.js +0 -136
  202. package/dist/sibling-temp-file.d.ts +0 -16
  203. package/dist/sibling-temp-file.d.ts.map +0 -1
  204. package/dist/sibling-temp-file.js +0 -73
  205. package/dist/sibling-temp-write.d.ts +0 -8
  206. package/dist/sibling-temp-write.d.ts.map +0 -1
  207. package/dist/sibling-temp-write.js +0 -40
package/dist/move-path.js CHANGED
@@ -1,7 +1,186 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
1
3
  import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { guardedRename } from "./guarded-mutation.js";
6
+ function entryIdentity(stat) {
7
+ return {
8
+ ctimeMs: stat.ctimeMs,
9
+ dev: stat.dev,
10
+ ino: stat.ino,
11
+ mode: stat.mode,
12
+ mtimeMs: stat.mtimeMs,
13
+ size: stat.size,
14
+ };
15
+ }
16
+ function sameIdentity(a, b) {
17
+ return (a.dev === b.dev &&
18
+ a.ino === b.ino &&
19
+ a.mode === b.mode &&
20
+ a.size === b.size &&
21
+ a.mtimeMs === b.mtimeMs &&
22
+ a.ctimeMs === b.ctimeMs);
23
+ }
24
+ function sameDirectoryNode(a, b) {
25
+ return a.dev === b.dev && a.ino === b.ino;
26
+ }
27
+ function modeBits(mode) {
28
+ return mode & 0o777;
29
+ }
30
+ function sourceChangedError(sourcePath) {
31
+ return Object.assign(new Error(`Source changed during move fallback: ${sourcePath}`), {
32
+ code: "ESTALE",
33
+ });
34
+ }
35
+ async function assertSourceStillMatches(sourcePath, identity) {
36
+ if (!sameIdentity(identity, entryIdentity(await fs.lstat(sourcePath)))) {
37
+ throw sourceChangedError(sourcePath);
38
+ }
39
+ }
40
+ function regularReadFlags() {
41
+ return (fsConstants.O_RDONLY |
42
+ (typeof fsConstants.O_NOFOLLOW === "number" && process.platform !== "win32"
43
+ ? fsConstants.O_NOFOLLOW
44
+ : 0));
45
+ }
46
+ async function writeAll(handle, buffer, bytesRead) {
47
+ let offset = 0;
48
+ while (offset < bytesRead) {
49
+ const { bytesWritten } = await handle.write(buffer, offset, bytesRead - offset);
50
+ offset += bytesWritten;
51
+ }
52
+ }
53
+ async function copyRegularFilePinned(params) {
54
+ let destinationCreated = false;
55
+ let sourceHandle;
56
+ try {
57
+ sourceHandle = await fs.open(params.from, regularReadFlags());
58
+ }
59
+ catch (error) {
60
+ const code = error?.code;
61
+ if (code === "ELOOP" || code === "ENOENT" || code === "ENOTDIR") {
62
+ throw sourceChangedError(params.from);
63
+ }
64
+ throw error;
65
+ }
66
+ try {
67
+ const openedStat = await sourceHandle.stat();
68
+ if (!openedStat.isFile() || !sameIdentity(params.identity, entryIdentity(openedStat))) {
69
+ throw sourceChangedError(params.from);
70
+ }
71
+ const destinationHandle = await fs.open(params.to, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, modeBits(params.mode) || 0o666);
72
+ destinationCreated = true;
73
+ try {
74
+ const scratch = Buffer.allocUnsafe(64 * 1024);
75
+ while (true) {
76
+ const { bytesRead } = await sourceHandle.read(scratch, 0, scratch.length, null);
77
+ if (bytesRead === 0) {
78
+ break;
79
+ }
80
+ await writeAll(destinationHandle, scratch, bytesRead);
81
+ }
82
+ }
83
+ finally {
84
+ await destinationHandle.close();
85
+ }
86
+ // Re-check the opened handle before the staged tree can be committed. If
87
+ // the source changed while we copied, the caller should retry the move.
88
+ if (!sameIdentity(params.identity, entryIdentity(await sourceHandle.stat()))) {
89
+ throw sourceChangedError(params.from);
90
+ }
91
+ await fs.chmod(params.to, modeBits(params.mode)).catch(() => undefined);
92
+ }
93
+ catch (error) {
94
+ if (destinationCreated) {
95
+ await fs.rm(params.to, { force: true }).catch(() => undefined);
96
+ }
97
+ throw error;
98
+ }
99
+ finally {
100
+ await sourceHandle.close();
101
+ }
102
+ }
103
+ async function copyEntryWithManifest(from, to, options) {
104
+ const sourceStat = await fs.lstat(from);
105
+ const identity = entryIdentity(sourceStat);
106
+ if (sourceStat.isSymbolicLink()) {
107
+ await fs.symlink(await fs.readlink(from), to);
108
+ // readlink() is path-based; verify the symlink we copied is still the one
109
+ // we inspected before letting the staged destination become visible.
110
+ await assertSourceStillMatches(from, identity);
111
+ return { ...identity, kind: "leaf" };
112
+ }
113
+ if (sourceStat.isDirectory()) {
114
+ await fs.mkdir(to, { mode: modeBits(sourceStat.mode) || 0o755 });
115
+ const children = [];
116
+ for (const child of await fs.readdir(from)) {
117
+ children.push({
118
+ name: child,
119
+ manifest: await copyEntryWithManifest(path.join(from, child), path.join(to, child), options),
120
+ });
121
+ }
122
+ // Directory traversal is path-based in Node. Treat a changed parent as a
123
+ // stale move before committing so swapped-in outside trees are not imported.
124
+ await assertSourceStillMatches(from, identity);
125
+ // mkdir() honors process umask. Restore the source mode before commit so
126
+ // EXDEV fallback preserves directory permissions like fs.cp did.
127
+ await fs.chmod(to, modeBits(sourceStat.mode));
128
+ return { ...identity, children, kind: "directory" };
129
+ }
130
+ if (!sourceStat.isFile()) {
131
+ throw new Error(`Refusing to move non-file path with copy fallback: ${from}`);
132
+ }
133
+ if (options.sourceHardlinks === "reject" && sourceStat.nlink > 1) {
134
+ throw new Error(`Refusing to move hardlinked file with copy fallback: ${from}`);
135
+ }
136
+ await copyRegularFilePinned({ from, identity, mode: sourceStat.mode, to });
137
+ return { ...identity, kind: "leaf" };
138
+ }
139
+ function mergeCleanupResults(a, b) {
140
+ return a === "stale" || b === "stale" ? "stale" : "removed";
141
+ }
142
+ async function cleanupCopiedEntry(sourcePath, manifest) {
143
+ let currentStat;
144
+ try {
145
+ currentStat = await fs.lstat(sourcePath);
146
+ }
147
+ catch (error) {
148
+ if (error?.code === "ENOENT") {
149
+ return "removed";
150
+ }
151
+ throw error;
152
+ }
153
+ if (manifest.kind === "directory") {
154
+ if (!currentStat.isDirectory() || !sameDirectoryNode(manifest, entryIdentity(currentStat))) {
155
+ return "stale";
156
+ }
157
+ // A same-inode directory can gain unrelated children after commit. Still
158
+ // clean manifest children so the fallback does not duplicate copied files.
159
+ let result = "removed";
160
+ for (const child of manifest.children) {
161
+ result = mergeCleanupResults(result, await cleanupCopiedEntry(path.join(sourcePath, child.name), child.manifest));
162
+ }
163
+ try {
164
+ await fs.rmdir(sourcePath);
165
+ }
166
+ catch (error) {
167
+ const code = error?.code;
168
+ if (code === "ENOTEMPTY" || code === "EEXIST") {
169
+ return "stale";
170
+ }
171
+ throw error;
172
+ }
173
+ return result;
174
+ }
175
+ if (!sameIdentity(manifest, entryIdentity(currentStat))) {
176
+ return "stale";
177
+ }
178
+ await fs.unlink(sourcePath);
179
+ return "removed";
180
+ }
2
181
  export async function movePathWithCopyFallback(options) {
3
182
  try {
4
- await fs.rename(options.from, options.to);
183
+ await guardedRename({ from: options.from, to: options.to });
5
184
  return;
6
185
  }
7
186
  catch (error) {
@@ -9,10 +188,19 @@ export async function movePathWithCopyFallback(options) {
9
188
  throw error;
10
189
  }
11
190
  }
12
- await fs.cp(options.from, options.to, {
13
- recursive: true,
14
- force: true,
15
- dereference: false,
16
- });
17
- await fs.rm(options.from, { recursive: true, force: true });
191
+ const targetDir = path.dirname(path.resolve(options.to));
192
+ const staged = path.join(targetDir, `.fs-safe-move-${process.pid}-${randomUUID()}.tmp`);
193
+ try {
194
+ const manifest = await copyEntryWithManifest(options.from, staged, {
195
+ sourceHardlinks: options.sourceHardlinks ?? "allow",
196
+ });
197
+ await guardedRename({ from: staged, to: options.to });
198
+ const cleanupResult = await cleanupCopiedEntry(options.from, manifest);
199
+ if (cleanupResult === "stale") {
200
+ throw sourceChangedError(options.from);
201
+ }
202
+ }
203
+ finally {
204
+ await fs.rm(staged, { recursive: true, force: true }).catch(() => undefined);
205
+ }
18
206
  }
@@ -0,0 +1,13 @@
1
+ export type ExternalFileWriteOptions<T = void> = {
2
+ rootDir: string;
3
+ path: string;
4
+ write: (filePath: string) => Promise<T>;
5
+ maxBytes?: number;
6
+ mode?: number;
7
+ };
8
+ export type ExternalFileWriteResult<T = void> = {
9
+ path: string;
10
+ result: T;
11
+ };
12
+ export declare function writeExternalFileWithinRoot<T = void>(options: ExternalFileWriteOptions<T>): Promise<ExternalFileWriteResult<T>>;
13
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AA6CF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CA8BrC"}
package/dist/output.js ADDED
@@ -0,0 +1,69 @@
1
+ import path from "node:path";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { sanitizeUntrustedFileName } from "./filename.js";
4
+ import { isPathInside } from "./path.js";
5
+ import { root } from "./root.js";
6
+ import { tempFile } from "./temp-target.js";
7
+ function tempFileNameForTarget(targetPath) {
8
+ return sanitizeUntrustedFileName(path.basename(targetPath), "output.bin");
9
+ }
10
+ function ensureTrailingSep(value) {
11
+ return value.endsWith(path.sep) ? value : `${value}${path.sep}`;
12
+ }
13
+ function toRootPathInput(params) {
14
+ if (!path.isAbsolute(params.targetPath)) {
15
+ return params.targetPath;
16
+ }
17
+ const absoluteTarget = path.resolve(params.targetPath);
18
+ const rootDir = path.resolve(params.rootDir);
19
+ if (isPathInside(ensureTrailingSep(rootDir), absoluteTarget)) {
20
+ return path.relative(rootDir, absoluteTarget);
21
+ }
22
+ if (isPathInside(ensureTrailingSep(params.rootReal), absoluteTarget)) {
23
+ return path.relative(params.rootReal, absoluteTarget);
24
+ }
25
+ return params.targetPath;
26
+ }
27
+ function assertFileTargetPath(targetPath) {
28
+ const basename = path.basename(targetPath);
29
+ if (!targetPath ||
30
+ targetPath === "." ||
31
+ targetPath.endsWith("/") ||
32
+ targetPath.endsWith("\\") ||
33
+ !basename ||
34
+ basename === "." ||
35
+ basename === "..") {
36
+ throw new FsSafeError("invalid-path", "target path must name a file");
37
+ }
38
+ }
39
+ export async function writeExternalFileWithinRoot(options) {
40
+ const targetRoot = await root(options.rootDir);
41
+ const requestedTargetPath = options.path;
42
+ if (requestedTargetPath.length === 0) {
43
+ throw new FsSafeError("invalid-path", "target path is required");
44
+ }
45
+ const targetPath = toRootPathInput({
46
+ rootDir: targetRoot.rootDir,
47
+ rootReal: targetRoot.rootReal,
48
+ targetPath: requestedTargetPath,
49
+ });
50
+ assertFileTargetPath(targetPath);
51
+ const finalPath = await targetRoot.resolve(targetPath);
52
+ const staged = await tempFile({
53
+ prefix: "fs-safe-output",
54
+ fileName: tempFileNameForTarget(targetPath),
55
+ });
56
+ try {
57
+ const result = await options.write(staged.path);
58
+ await targetRoot.copyIn(targetPath, staged.path, {
59
+ maxBytes: options.maxBytes,
60
+ mode: options.mode,
61
+ mkdir: true,
62
+ sourceHardlinks: "reject",
63
+ });
64
+ return { path: finalPath, result };
65
+ }
66
+ finally {
67
+ await staged.cleanup();
68
+ }
69
+ }
@@ -0,0 +1,4 @@
1
+ import type { Stats } from "node:fs";
2
+ import type { PathStat } from "./types.js";
3
+ export declare function pathStatFromStats(stat: Stats): PathStat;
4
+ //# sourceMappingURL=path-stat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-stat.d.ts","sourceRoot":"","sources":["../src/path-stat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,KAAK,GAAG,QAAQ,CAcvD"}
@@ -0,0 +1,15 @@
1
+ export function pathStatFromStats(stat) {
2
+ return {
3
+ dev: Number(stat.dev),
4
+ gid: Number(stat.gid),
5
+ ino: Number(stat.ino),
6
+ isDirectory: stat.isDirectory(),
7
+ isFile: stat.isFile(),
8
+ isSymbolicLink: stat.isSymbolicLink(),
9
+ mode: stat.mode,
10
+ mtimeMs: stat.mtimeMs,
11
+ nlink: stat.nlink,
12
+ size: stat.size,
13
+ uid: stat.uid,
14
+ };
15
+ }
package/dist/path.d.ts CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  export declare function normalizeWindowsPathForComparison(input: string): string;
3
3
  export declare function isNodeError(value: unknown): value is NodeJS.ErrnoException;
4
4
  export declare function hasNodeErrorCode(value: unknown, code: string): boolean;
5
+ export declare function assertNoNulPathInput(filePath: string, message?: string): void;
5
6
  export declare function isNotFoundPathError(value: unknown): boolean;
6
7
  export declare function isSymlinkOpenError(value: unknown): boolean;
7
8
  export declare function isPathInside(root: string, target: string): boolean;
@@ -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,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,CAyBpE;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;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"}
package/dist/path.js CHANGED
@@ -22,6 +22,11 @@ export function isNodeError(value) {
22
22
  export function hasNodeErrorCode(value, code) {
23
23
  return isNodeError(value) && value.code === code;
24
24
  }
25
+ export function assertNoNulPathInput(filePath, message = "path contains a NUL byte") {
26
+ if (filePath.includes("\0")) {
27
+ throw new FsSafeError("invalid-path", message);
28
+ }
29
+ }
25
30
  export function isNotFoundPathError(value) {
26
31
  return isNodeError(value) && typeof value.code === "string" && NOT_FOUND_CODES.has(value.code);
27
32
  }
@@ -94,9 +99,7 @@ export function splitSafeRelativePath(relativePath) {
94
99
  if (relativePath.length === 0 || relativePath === ".") {
95
100
  return [];
96
101
  }
97
- if (relativePath.includes("\0")) {
98
- throw new FsSafeError("invalid-path", "relative path contains a NUL byte");
99
- }
102
+ assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
100
103
  if (relativePath.includes("\\")) {
101
104
  throw new FsSafeError("invalid-path", "relative path must use forward slashes");
102
105
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAQ3D,KAAK,gBAAgB,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAuFzC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;CACzB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuC5B;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAoB5B"}
1
+ {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAIA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAIvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAS3D,KAAK,gBAAgB,GACjB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAuFzC,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;CACzB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuC5B;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAoB5B"}
@@ -1,9 +1,12 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import fsSync from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { Transform } from "node:stream";
5
6
  import { pipeline } from "node:stream/promises";
7
+ import { createNearestExistingDirectoryGuard } from "./directory-guard.js";
6
8
  import { FsSafeError } from "./errors.js";
9
+ import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
7
10
  import { canFallbackFromPythonError, getFsSafePythonConfig } from "./pinned-python-config.js";
8
11
  import { assertPinnedPythonOperationAvailable, runPinnedPythonOperation, validatePinnedOperationPayload, } from "./pinned-python.js";
9
12
  function byteLength(input, encoding) {
@@ -67,13 +70,13 @@ async function inputToBase64(input, maxBytes) {
67
70
  return Buffer.concat(chunks, bytes).toString("base64");
68
71
  }
69
72
  export async function runPinnedWriteHelper(params) {
70
- if (getFsSafePythonConfig().mode === "off") {
71
- return await runPinnedWriteFallback(params);
72
- }
73
73
  assertSafeBasename(params.basename);
74
74
  validatePinnedOperationPayload({
75
75
  relativeParentPath: params.relativeParentPath,
76
76
  });
77
+ if (getFsSafePythonConfig().mode === "off") {
78
+ return await runPinnedWriteFallback(params);
79
+ }
77
80
  if (params.input.kind === "stream") {
78
81
  try {
79
82
  assertPinnedPythonOperationAvailable();
@@ -133,12 +136,21 @@ async function runPinnedWriteFallback(params) {
133
136
  const parentPath = params.relativeParentPath
134
137
  ? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
135
138
  : params.rootPath;
139
+ const parentGuard = await createNearestExistingDirectoryGuard(params.rootPath, parentPath);
136
140
  if (params.mkdir) {
137
- await fs.mkdir(parentPath, { recursive: true });
141
+ await withAsyncDirectoryGuards([parentGuard], async () => {
142
+ await fs.mkdir(parentPath, { recursive: true });
143
+ });
138
144
  }
139
145
  const targetPath = path.join(parentPath, params.basename);
140
146
  if (params.overwrite === false) {
141
- const handle = await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode);
147
+ let handle = await withAsyncDirectoryGuards([parentGuard], async () => await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode), {
148
+ onPostGuardFailure: async (openedHandle) => {
149
+ // The parent failed verification, so targetPath may now resolve
150
+ // somewhere else. Close the fd, but do not clean up by path.
151
+ await openedHandle.close().catch(() => undefined);
152
+ },
153
+ });
142
154
  let created = true;
143
155
  try {
144
156
  if (params.input.kind === "buffer") {
@@ -164,32 +176,45 @@ async function runPinnedWriteFallback(params) {
164
176
  }
165
177
  }
166
178
  }
167
- const tempPath = path.join(parentPath, `.${params.basename}.fallback.tmp`);
179
+ const tempPath = path.join(parentPath, `.${params.basename}.${randomUUID()}.fallback.tmp`);
180
+ const tempFlags = fsSync.constants.O_WRONLY |
181
+ fsSync.constants.O_CREAT |
182
+ fsSync.constants.O_EXCL |
183
+ (process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
184
+ ? fsSync.constants.O_NOFOLLOW
185
+ : 0);
186
+ let handle;
187
+ let handleClosedByStream = false;
168
188
  try {
189
+ handle = await fs.open(tempPath, tempFlags, params.mode);
169
190
  if (params.input.kind === "buffer") {
170
191
  assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
171
192
  if (typeof params.input.data === "string") {
172
- await fs.writeFile(tempPath, params.input.data, {
173
- encoding: params.input.encoding ?? "utf8",
174
- mode: params.mode,
175
- });
193
+ await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
176
194
  }
177
195
  else {
178
- await fs.writeFile(tempPath, params.input.data, { mode: params.mode });
196
+ await handle.writeFile(params.input.data);
179
197
  }
180
198
  }
181
199
  else {
182
- const handle = await fs.open(tempPath, "w", params.mode);
183
- try {
184
- await pipelineWithMaxBytes(params.input.stream, handle.createWriteStream(), params.maxBytes);
185
- }
186
- finally {
187
- await handle.close().catch(() => { });
188
- }
200
+ const writable = handle.createWriteStream();
201
+ writable.once("close", () => {
202
+ handleClosedByStream = true;
203
+ });
204
+ await pipelineWithMaxBytes(params.input.stream, writable, params.maxBytes);
205
+ }
206
+ if (!handleClosedByStream) {
207
+ await handle.close().catch(() => undefined);
208
+ handle = undefined;
189
209
  }
190
- await fs.rename(tempPath, targetPath);
210
+ await withAsyncDirectoryGuards([parentGuard], async () => {
211
+ await fs.rename(tempPath, targetPath);
212
+ });
191
213
  }
192
214
  catch (error) {
215
+ if (handle && !handleClosedByStream) {
216
+ await handle.close().catch(() => undefined);
217
+ }
193
218
  await fs.rm(tempPath, { force: true }).catch(() => undefined);
194
219
  throw error;
195
220
  }
@@ -1,18 +1,16 @@
1
- import { type FileStore } from "./file-store.js";
2
- export type PrivateStateStoreOptions = {
1
+ export type PrivateFileStore = {
3
2
  rootDir: string;
4
- };
5
- export type PrivateStateStore = Omit<FileStore, "readText" | "readJson" | "writeText" | "writeJson"> & {
3
+ path(relativePath: string): string;
6
4
  readText(relativePath: string, options?: {
7
5
  maxBytes?: number;
8
6
  }): Promise<string | null>;
9
7
  readJson<T = unknown>(relativePath: string, options?: {
10
8
  maxBytes?: number;
11
9
  }): Promise<T | null>;
12
- writeText(relativePath: string, content: string | Uint8Array): Promise<string>;
10
+ writeText(relativePath: string, content: string | Uint8Array): Promise<void>;
13
11
  writeJson(relativePath: string, value: unknown, options?: {
14
12
  trailingNewline?: boolean;
15
- }): Promise<string>;
13
+ }): Promise<void>;
16
14
  };
17
15
  export declare function writePrivateTextAtomic(params: {
18
16
  rootDir: string;
@@ -56,5 +54,5 @@ export declare function writePrivateJsonAtomicSync(params: {
56
54
  value: unknown;
57
55
  trailingNewline?: boolean;
58
56
  }): void;
59
- export declare function privateStateStore(options: PrivateStateStoreOptions): PrivateStateStore;
57
+ export declare function privateFileStore(rootDir: string): PrivateFileStore;
60
58
  //# 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":"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"}
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"}
@@ -2,11 +2,10 @@ 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";
6
5
  import { isPathInside } from "./path.js";
7
6
  import { readRegularFileSync } from "./regular-file.js";
8
7
  import { root } from "./root.js";
9
- import { writeSecretFileAtomic } from "./secret-file.js";
8
+ import { writePrivateSecretFileAtomic } from "./secret-file.js";
10
9
  function resolvePrivateStorePath(rootDir, relativePath) {
11
10
  const root = path.resolve(rootDir);
12
11
  const raw = relativePath.trim();
@@ -21,7 +20,7 @@ function resolvePrivateStorePath(rootDir, relativePath) {
21
20
  return resolved;
22
21
  }
23
22
  export async function writePrivateTextAtomic(params) {
24
- await writeSecretFileAtomic(params);
23
+ await writePrivateSecretFileAtomic(params);
25
24
  }
26
25
  export async function readPrivateText(params) {
27
26
  const rootDir = path.resolve(params.rootDir);
@@ -173,7 +172,7 @@ export function writePrivateTextAtomicSync(params) {
173
172
  }
174
173
  export async function writePrivateJsonAtomic(params) {
175
174
  const json = JSON.stringify(params.value, null, 2);
176
- await writeSecretFileAtomic({
175
+ await writePrivateSecretFileAtomic({
177
176
  rootDir: params.rootDir,
178
177
  filePath: params.filePath,
179
178
  content: params.trailingNewline && !json.endsWith("\n") ? `${json}\n` : json,
@@ -187,47 +186,35 @@ export function writePrivateJsonAtomicSync(params) {
187
186
  content: params.trailingNewline && !json.endsWith("\n") ? `${json}\n` : json,
188
187
  });
189
188
  }
190
- export function privateStateStore(options) {
191
- const rootDir = path.resolve(options.rootDir);
192
- const store = fileStore({ rootDir, private: true });
189
+ export function privateFileStore(rootDir) {
190
+ const root = path.resolve(rootDir);
193
191
  return {
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
- },
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
+ }),
213
204
  writeText: async (relativePath, content) => {
214
- const safePath = resolvePrivateStorePath(rootDir, relativePath);
215
205
  await writePrivateTextAtomic({
216
- rootDir,
217
- filePath: safePath,
206
+ rootDir: root,
207
+ filePath: resolvePrivateStorePath(root, relativePath),
218
208
  content,
219
209
  });
220
- return safePath;
221
210
  },
222
211
  writeJson: async (relativePath, value, options) => {
223
- const safePath = resolvePrivateStorePath(rootDir, relativePath);
224
212
  await writePrivateJsonAtomic({
225
- rootDir,
226
- filePath: safePath,
213
+ rootDir: root,
214
+ filePath: resolvePrivateStorePath(root, relativePath),
227
215
  value,
228
216
  trailingNewline: options?.trailingNewline,
229
217
  });
230
- return safePath;
231
218
  },
232
219
  };
233
220
  }
@@ -1 +1 @@
1
- {"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,IAAI,IAAI,CAAC;IAChB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AAuGF,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CA4DnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAUH"}
1
+ {"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAIzB,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,IAAI,IAAI,CAAC;IAChB,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AAsGF,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CAwEnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAUH"}