@openclaw/fs-safe 0.1.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 (260) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +390 -0
  4. package/SECURITY.md +12 -0
  5. package/dist/absolute-path.d.ts +19 -0
  6. package/dist/absolute-path.d.ts.map +1 -0
  7. package/dist/absolute-path.js +98 -0
  8. package/dist/advanced.d.ts +22 -0
  9. package/dist/advanced.d.ts.map +1 -0
  10. package/dist/advanced.js +24 -0
  11. package/dist/archive-entry.d.ts +13 -0
  12. package/dist/archive-entry.d.ts.map +1 -0
  13. package/dist/archive-entry.js +47 -0
  14. package/dist/archive-kind.d.ts +8 -0
  15. package/dist/archive-kind.d.ts.map +1 -0
  16. package/dist/archive-kind.js +58 -0
  17. package/dist/archive-limits.d.ts +40 -0
  18. package/dist/archive-limits.d.ts.map +1 -0
  19. package/dist/archive-limits.js +92 -0
  20. package/dist/archive-staging.d.ts +26 -0
  21. package/dist/archive-staging.d.ts.map +1 -0
  22. package/dist/archive-staging.js +171 -0
  23. package/dist/archive-tar.d.ts +14 -0
  24. package/dist/archive-tar.d.ts.map +1 -0
  25. package/dist/archive-tar.js +52 -0
  26. package/dist/archive-utils.d.ts +3 -0
  27. package/dist/archive-utils.d.ts.map +1 -0
  28. package/dist/archive-utils.js +26 -0
  29. package/dist/archive-zip-preflight.d.ts +7 -0
  30. package/dist/archive-zip-preflight.d.ts.map +1 -0
  31. package/dist/archive-zip-preflight.js +170 -0
  32. package/dist/archive.d.ts +24 -0
  33. package/dist/archive.d.ts.map +1 -0
  34. package/dist/archive.js +251 -0
  35. package/dist/async-lock.d.ts +2 -0
  36. package/dist/async-lock.d.ts.map +1 -0
  37. package/dist/async-lock.js +17 -0
  38. package/dist/atomic.d.ts +5 -0
  39. package/dist/atomic.d.ts.map +1 -0
  40. package/dist/atomic.js +4 -0
  41. package/dist/boundary-file-read.d.ts +44 -0
  42. package/dist/boundary-file-read.d.ts.map +1 -0
  43. package/dist/boundary-file-read.js +129 -0
  44. package/dist/boundary-path.d.ts +39 -0
  45. package/dist/boundary-path.d.ts.map +1 -0
  46. package/dist/boundary-path.js +598 -0
  47. package/dist/config.d.ts +2 -0
  48. package/dist/config.d.ts.map +1 -0
  49. package/dist/config.js +1 -0
  50. package/dist/errors.d.ts +11 -0
  51. package/dist/errors.d.ts.map +1 -0
  52. package/dist/errors.js +20 -0
  53. package/dist/file-identity.d.ts +6 -0
  54. package/dist/file-identity.d.ts.map +1 -0
  55. package/dist/file-identity.js +14 -0
  56. package/dist/file-lock.d.ts +20 -0
  57. package/dist/file-lock.d.ts.map +1 -0
  58. package/dist/file-lock.js +34 -0
  59. package/dist/file-store.d.ts +66 -0
  60. package/dist/file-store.d.ts.map +1 -0
  61. package/dist/file-store.js +436 -0
  62. package/dist/file-url.d.ts +2 -0
  63. package/dist/file-url.d.ts.map +1 -0
  64. package/dist/file-url.js +1 -0
  65. package/dist/filename.d.ts +2 -0
  66. package/dist/filename.d.ts.map +1 -0
  67. package/dist/filename.js +25 -0
  68. package/dist/fs-pinned-path-helper.d.ts +7 -0
  69. package/dist/fs-pinned-path-helper.d.ts.map +1 -0
  70. package/dist/fs-pinned-path-helper.js +182 -0
  71. package/dist/fs-pinned-write-helper.d.ts +21 -0
  72. package/dist/fs-pinned-write-helper.d.ts.map +1 -0
  73. package/dist/fs-pinned-write-helper.js +263 -0
  74. package/dist/fs.d.ts +13 -0
  75. package/dist/fs.d.ts.map +1 -0
  76. package/dist/fs.js +30 -0
  77. package/dist/hardlink-guards.d.ts +7 -0
  78. package/dist/hardlink-guards.d.ts.map +1 -0
  79. package/dist/hardlink-guards.js +30 -0
  80. package/dist/home-dir.d.ts +22 -0
  81. package/dist/home-dir.d.ts.map +1 -0
  82. package/dist/home-dir.js +107 -0
  83. package/dist/index.d.ts +4 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +3 -0
  86. package/dist/install-path.d.ts +20 -0
  87. package/dist/install-path.d.ts.map +1 -0
  88. package/dist/install-path.js +94 -0
  89. package/dist/install-safe-path.d.ts +20 -0
  90. package/dist/install-safe-path.d.ts.map +1 -0
  91. package/dist/install-safe-path.js +94 -0
  92. package/dist/json-document-store.d.ts +30 -0
  93. package/dist/json-document-store.d.ts.map +1 -0
  94. package/dist/json-document-store.js +68 -0
  95. package/dist/json-file.d.ts +3 -0
  96. package/dist/json-file.d.ts.map +1 -0
  97. package/dist/json-file.js +123 -0
  98. package/dist/json-files.d.ts +20 -0
  99. package/dist/json-files.d.ts.map +1 -0
  100. package/dist/json-files.js +153 -0
  101. package/dist/json-store.d.ts +9 -0
  102. package/dist/json-store.d.ts.map +1 -0
  103. package/dist/json-store.js +12 -0
  104. package/dist/json.d.ts +17 -0
  105. package/dist/json.d.ts.map +1 -0
  106. package/dist/json.js +198 -0
  107. package/dist/local-file-access.d.ts +8 -0
  108. package/dist/local-file-access.d.ts.map +1 -0
  109. package/dist/local-file-access.js +73 -0
  110. package/dist/local-roots.d.ts +26 -0
  111. package/dist/local-roots.d.ts.map +1 -0
  112. package/dist/local-roots.js +150 -0
  113. package/dist/mode.d.ts +2 -0
  114. package/dist/mode.d.ts.map +1 -0
  115. package/dist/mode.js +3 -0
  116. package/dist/move-path.d.ts +6 -0
  117. package/dist/move-path.d.ts.map +1 -0
  118. package/dist/move-path.js +18 -0
  119. package/dist/path-alias-guards.d.ts +19 -0
  120. package/dist/path-alias-guards.d.ts.map +1 -0
  121. package/dist/path-alias-guards.js +21 -0
  122. package/dist/path-guards.d.ts +7 -0
  123. package/dist/path-guards.d.ts.map +1 -0
  124. package/dist/path-guards.js +49 -0
  125. package/dist/path-policy.d.ts +25 -0
  126. package/dist/path-policy.d.ts.map +1 -0
  127. package/dist/path-policy.js +50 -0
  128. package/dist/path-safety.d.ts +12 -0
  129. package/dist/path-safety.d.ts.map +1 -0
  130. package/dist/path-safety.js +50 -0
  131. package/dist/path-scope.d.ts +2 -0
  132. package/dist/path-scope.d.ts.map +1 -0
  133. package/dist/path-scope.js +1 -0
  134. package/dist/path.d.ts +18 -0
  135. package/dist/path.d.ts.map +1 -0
  136. package/dist/path.js +123 -0
  137. package/dist/permissions-public.d.ts +2 -0
  138. package/dist/permissions-public.d.ts.map +1 -0
  139. package/dist/permissions-public.js +1 -0
  140. package/dist/permissions.d.ts +86 -0
  141. package/dist/permissions.d.ts.map +1 -0
  142. package/dist/permissions.js +435 -0
  143. package/dist/pinned-helper.d.ts +9 -0
  144. package/dist/pinned-helper.d.ts.map +1 -0
  145. package/dist/pinned-helper.js +19 -0
  146. package/dist/pinned-open.d.ts +24 -0
  147. package/dist/pinned-open.d.ts.map +1 -0
  148. package/dist/pinned-open.js +71 -0
  149. package/dist/pinned-path.d.ts +7 -0
  150. package/dist/pinned-path.d.ts.map +1 -0
  151. package/dist/pinned-path.js +21 -0
  152. package/dist/pinned-python-config.d.ts +9 -0
  153. package/dist/pinned-python-config.d.ts.map +1 -0
  154. package/dist/pinned-python-config.js +39 -0
  155. package/dist/pinned-python.d.ts +12 -0
  156. package/dist/pinned-python.d.ts.map +1 -0
  157. package/dist/pinned-python.js +599 -0
  158. package/dist/pinned-write.d.ts +33 -0
  159. package/dist/pinned-write.d.ts.map +1 -0
  160. package/dist/pinned-write.js +198 -0
  161. package/dist/private-file-store.d.ts +60 -0
  162. package/dist/private-file-store.d.ts.map +1 -0
  163. package/dist/private-file-store.js +233 -0
  164. package/dist/private-temp-workspace.d.ts +39 -0
  165. package/dist/private-temp-workspace.d.ts.map +1 -0
  166. package/dist/private-temp-workspace.js +185 -0
  167. package/dist/regular-file.d.ts +38 -0
  168. package/dist/regular-file.d.ts.map +1 -0
  169. package/dist/regular-file.js +239 -0
  170. package/dist/replace-directory.d.ts +7 -0
  171. package/dist/replace-directory.d.ts.map +1 -0
  172. package/dist/replace-directory.js +32 -0
  173. package/dist/replace-file.d.ts +41 -0
  174. package/dist/replace-file.d.ts.map +1 -0
  175. package/dist/replace-file.js +325 -0
  176. package/dist/root-file.d.ts +44 -0
  177. package/dist/root-file.d.ts.map +1 -0
  178. package/dist/root-file.js +129 -0
  179. package/dist/root-path.d.ts +39 -0
  180. package/dist/root-path.d.ts.map +1 -0
  181. package/dist/root-path.js +598 -0
  182. package/dist/root-paths.d.ts +92 -0
  183. package/dist/root-paths.d.ts.map +1 -0
  184. package/dist/root-paths.js +338 -0
  185. package/dist/root.d.ts +108 -0
  186. package/dist/root.d.ts.map +1 -0
  187. package/dist/root.js +1332 -0
  188. package/dist/safe-open-sync.d.ts +24 -0
  189. package/dist/safe-open-sync.d.ts.map +1 -0
  190. package/dist/safe-open-sync.js +71 -0
  191. package/dist/safe-root.d.ts +123 -0
  192. package/dist/safe-root.d.ts.map +1 -0
  193. package/dist/safe-root.js +1060 -0
  194. package/dist/secret-file.d.ts +17 -0
  195. package/dist/secret-file.d.ts.map +1 -0
  196. package/dist/secret-file.js +232 -0
  197. package/dist/secret.d.ts +2 -0
  198. package/dist/secret.d.ts.map +1 -0
  199. package/dist/secret.js +1 -0
  200. package/dist/secure-file.d.ts +32 -0
  201. package/dist/secure-file.d.ts.map +1 -0
  202. package/dist/secure-file.js +163 -0
  203. package/dist/secure-temp-dir.d.ts +27 -0
  204. package/dist/secure-temp-dir.d.ts.map +1 -0
  205. package/dist/secure-temp-dir.js +155 -0
  206. package/dist/secure-temp-workspace.d.ts +25 -0
  207. package/dist/secure-temp-workspace.d.ts.map +1 -0
  208. package/dist/secure-temp-workspace.js +136 -0
  209. package/dist/sibling-temp-file.d.ts +16 -0
  210. package/dist/sibling-temp-file.d.ts.map +1 -0
  211. package/dist/sibling-temp-file.js +73 -0
  212. package/dist/sibling-temp-write.d.ts +8 -0
  213. package/dist/sibling-temp-write.d.ts.map +1 -0
  214. package/dist/sibling-temp-write.js +40 -0
  215. package/dist/sibling-temp.d.ts +23 -0
  216. package/dist/sibling-temp.d.ts.map +1 -0
  217. package/dist/sibling-temp.js +119 -0
  218. package/dist/sidecar-lock.d.ts +50 -0
  219. package/dist/sidecar-lock.d.ts.map +1 -0
  220. package/dist/sidecar-lock.js +235 -0
  221. package/dist/store.d.ts +3 -0
  222. package/dist/store.d.ts.map +1 -0
  223. package/dist/store.js +2 -0
  224. package/dist/string-coerce.d.ts +14 -0
  225. package/dist/string-coerce.d.ts.map +1 -0
  226. package/dist/string-coerce.js +72 -0
  227. package/dist/symlink-parents.d.ts +12 -0
  228. package/dist/symlink-parents.d.ts.map +1 -0
  229. package/dist/symlink-parents.js +78 -0
  230. package/dist/temp-cleanup.d.ts +6 -0
  231. package/dist/temp-cleanup.d.ts.map +1 -0
  232. package/dist/temp-cleanup.js +42 -0
  233. package/dist/temp-target.d.ts +28 -0
  234. package/dist/temp-target.d.ts.map +1 -0
  235. package/dist/temp-target.js +84 -0
  236. package/dist/temp.d.ts +3 -0
  237. package/dist/temp.d.ts.map +1 -0
  238. package/dist/temp.js +2 -0
  239. package/dist/test-hooks.d.ts +9 -0
  240. package/dist/test-hooks.d.ts.map +1 -0
  241. package/dist/test-hooks.js +13 -0
  242. package/dist/text-atomic.d.ts +7 -0
  243. package/dist/text-atomic.d.ts.map +1 -0
  244. package/dist/text-atomic.js +13 -0
  245. package/dist/timing.d.ts +6 -0
  246. package/dist/timing.d.ts.map +1 -0
  247. package/dist/timing.js +24 -0
  248. package/dist/trash.d.ts +5 -0
  249. package/dist/trash.d.ts.map +1 -0
  250. package/dist/trash.js +127 -0
  251. package/dist/types.d.ts +23 -0
  252. package/dist/types.d.ts.map +1 -0
  253. package/dist/types.js +1 -0
  254. package/dist/walk.d.ts +26 -0
  255. package/dist/walk.d.ts.map +1 -0
  256. package/dist/walk.js +169 -0
  257. package/dist/write-queue.d.ts +2 -0
  258. package/dist/write-queue.d.ts.map +1 -0
  259. package/dist/write-queue.js +18 -0
  260. package/package.json +117 -0
@@ -0,0 +1,198 @@
1
+ import fsSync from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { Transform } from "node:stream";
5
+ import { pipeline } from "node:stream/promises";
6
+ import { FsSafeError } from "./errors.js";
7
+ import { canFallbackFromPythonError, getFsSafePythonConfig } from "./pinned-python-config.js";
8
+ import { assertPinnedPythonOperationAvailable, runPinnedPythonOperation, validatePinnedOperationPayload, } from "./pinned-python.js";
9
+ function byteLength(input, encoding) {
10
+ return typeof input === "string"
11
+ ? Buffer.byteLength(input, encoding ?? "utf8")
12
+ : input.byteLength;
13
+ }
14
+ function assertSafeBasename(basename) {
15
+ if (!basename ||
16
+ basename === "." ||
17
+ basename === ".." ||
18
+ basename.includes("/") ||
19
+ basename.includes("\0")) {
20
+ throw new FsSafeError("invalid-path", "invalid target path");
21
+ }
22
+ }
23
+ function assertWithinMaxBytes(bytes, maxBytes) {
24
+ if (maxBytes !== undefined && bytes > maxBytes) {
25
+ throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
26
+ }
27
+ }
28
+ function createMaxBytesTransform(maxBytes) {
29
+ if (maxBytes === undefined) {
30
+ return undefined;
31
+ }
32
+ let bytes = 0;
33
+ return new Transform({
34
+ transform(chunk, _encoding, callback) {
35
+ bytes += chunk.byteLength;
36
+ if (bytes > maxBytes) {
37
+ callback(new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
38
+ return;
39
+ }
40
+ callback(null, chunk);
41
+ },
42
+ });
43
+ }
44
+ async function pipelineWithMaxBytes(stream, destination, maxBytes) {
45
+ const limiter = createMaxBytesTransform(maxBytes);
46
+ if (limiter) {
47
+ await pipeline(stream, limiter, destination);
48
+ return;
49
+ }
50
+ await pipeline(stream, destination);
51
+ }
52
+ async function inputToBase64(input, maxBytes) {
53
+ if (input.kind === "buffer") {
54
+ assertWithinMaxBytes(byteLength(input.data, input.encoding), maxBytes);
55
+ return (typeof input.data === "string"
56
+ ? Buffer.from(input.data, input.encoding ?? "utf8")
57
+ : input.data).toString("base64");
58
+ }
59
+ const chunks = [];
60
+ let bytes = 0;
61
+ for await (const chunk of input.stream) {
62
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
63
+ bytes += buffer.byteLength;
64
+ assertWithinMaxBytes(bytes, maxBytes);
65
+ chunks.push(buffer);
66
+ }
67
+ return Buffer.concat(chunks, bytes).toString("base64");
68
+ }
69
+ export async function runPinnedWriteHelper(params) {
70
+ if (getFsSafePythonConfig().mode === "off") {
71
+ return await runPinnedWriteFallback(params);
72
+ }
73
+ assertSafeBasename(params.basename);
74
+ validatePinnedOperationPayload({
75
+ relativeParentPath: params.relativeParentPath,
76
+ });
77
+ if (params.input.kind === "stream") {
78
+ try {
79
+ assertPinnedPythonOperationAvailable();
80
+ }
81
+ catch (error) {
82
+ if (canFallbackFromPythonError(error)) {
83
+ return await runPinnedWriteFallback(params);
84
+ }
85
+ throw error;
86
+ }
87
+ }
88
+ const payload = {
89
+ base64: await inputToBase64(params.input, params.maxBytes),
90
+ basename: params.basename,
91
+ maxBytes: params.maxBytes ?? -1,
92
+ mkdir: params.mkdir,
93
+ mode: params.mode || 0o600,
94
+ overwrite: params.overwrite !== false,
95
+ relativeParentPath: params.relativeParentPath,
96
+ };
97
+ try {
98
+ return await runPinnedPythonOperation({
99
+ operation: "write",
100
+ rootPath: params.rootPath,
101
+ payload,
102
+ });
103
+ }
104
+ catch (error) {
105
+ if (canFallbackFromPythonError(error)) {
106
+ return await runPinnedWriteFallback(params);
107
+ }
108
+ throw error;
109
+ }
110
+ }
111
+ export async function runPinnedCopyHelper(params) {
112
+ assertSafeBasename(params.basename);
113
+ validatePinnedOperationPayload({
114
+ relativeParentPath: params.relativeParentPath,
115
+ });
116
+ return await runPinnedPythonOperation({
117
+ operation: "copy",
118
+ rootPath: params.rootPath,
119
+ payload: {
120
+ basename: params.basename,
121
+ maxBytes: params.maxBytes ?? -1,
122
+ mkdir: params.mkdir,
123
+ mode: params.mode || 0o600,
124
+ overwrite: params.overwrite !== false,
125
+ relativeParentPath: params.relativeParentPath,
126
+ sourceDev: params.sourceIdentity.dev,
127
+ sourceIno: params.sourceIdentity.ino,
128
+ sourcePath: params.sourcePath,
129
+ },
130
+ });
131
+ }
132
+ async function runPinnedWriteFallback(params) {
133
+ const parentPath = params.relativeParentPath
134
+ ? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
135
+ : params.rootPath;
136
+ if (params.mkdir) {
137
+ await fs.mkdir(parentPath, { recursive: true });
138
+ }
139
+ const targetPath = path.join(parentPath, params.basename);
140
+ 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);
142
+ let created = true;
143
+ try {
144
+ if (params.input.kind === "buffer") {
145
+ assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
146
+ if (typeof params.input.data === "string") {
147
+ await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
148
+ }
149
+ else {
150
+ await handle.writeFile(params.input.data);
151
+ }
152
+ }
153
+ else {
154
+ await pipelineWithMaxBytes(params.input.stream, handle.createWriteStream(), params.maxBytes);
155
+ }
156
+ const stat = await handle.stat();
157
+ created = false;
158
+ return { dev: stat.dev, ino: stat.ino };
159
+ }
160
+ finally {
161
+ await handle.close().catch(() => undefined);
162
+ if (created) {
163
+ await fs.rm(targetPath, { force: true }).catch(() => undefined);
164
+ }
165
+ }
166
+ }
167
+ const tempPath = path.join(parentPath, `.${params.basename}.fallback.tmp`);
168
+ try {
169
+ if (params.input.kind === "buffer") {
170
+ assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
171
+ 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
+ });
176
+ }
177
+ else {
178
+ await fs.writeFile(tempPath, params.input.data, { mode: params.mode });
179
+ }
180
+ }
181
+ 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
+ }
189
+ }
190
+ await fs.rename(tempPath, targetPath);
191
+ }
192
+ catch (error) {
193
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
194
+ throw error;
195
+ }
196
+ const stat = await fs.stat(targetPath);
197
+ return { dev: stat.dev, ino: stat.ino };
198
+ }
@@ -0,0 +1,60 @@
1
+ import { type FileStore } from "./file-store.js";
2
+ export type PrivateStateStoreOptions = {
3
+ rootDir: string;
4
+ };
5
+ export type PrivateStateStore = Omit<FileStore, "readText" | "readJson" | "writeText" | "writeJson"> & {
6
+ readText(relativePath: string, options?: {
7
+ maxBytes?: number;
8
+ }): Promise<string | null>;
9
+ readJson<T = unknown>(relativePath: string, options?: {
10
+ maxBytes?: number;
11
+ }): Promise<T | null>;
12
+ writeText(relativePath: string, content: string | Uint8Array): Promise<string>;
13
+ writeJson(relativePath: string, value: unknown, options?: {
14
+ trailingNewline?: boolean;
15
+ }): Promise<string>;
16
+ };
17
+ export declare function writePrivateTextAtomic(params: {
18
+ rootDir: string;
19
+ filePath: string;
20
+ content: string | Uint8Array;
21
+ }): Promise<void>;
22
+ export declare function readPrivateText(params: {
23
+ rootDir: string;
24
+ filePath: string;
25
+ maxBytes?: number;
26
+ }): Promise<string | null>;
27
+ export declare function readPrivateTextSync(params: {
28
+ rootDir: string;
29
+ filePath: string;
30
+ maxBytes?: number;
31
+ }): string | null;
32
+ export declare function readPrivateJson<T = unknown>(params: {
33
+ rootDir: string;
34
+ filePath: string;
35
+ maxBytes?: number;
36
+ }): Promise<T | null>;
37
+ export declare function readPrivateJsonSync<T = unknown>(params: {
38
+ rootDir: string;
39
+ filePath: string;
40
+ maxBytes?: number;
41
+ }): T | null;
42
+ export declare function writePrivateTextAtomicSync(params: {
43
+ rootDir: string;
44
+ filePath: string;
45
+ content: string | Uint8Array;
46
+ }): void;
47
+ export declare function writePrivateJsonAtomic(params: {
48
+ rootDir: string;
49
+ filePath: string;
50
+ value: unknown;
51
+ trailingNewline?: boolean;
52
+ }): Promise<void>;
53
+ export declare function writePrivateJsonAtomicSync(params: {
54
+ rootDir: string;
55
+ filePath: string;
56
+ value: unknown;
57
+ trailingNewline?: boolean;
58
+ }): void;
59
+ export declare function privateStateStore(options: PrivateStateStoreOptions): PrivateStateStore;
60
+ //# sourceMappingURL=private-file-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"private-file-store.d.ts","sourceRoot":"","sources":["../src/private-file-store.ts"],"names":[],"mappings":"AAIA,OAAO,EAAa,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAM5D,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC,GAAG;IACrG,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,MAAM,CAAC,CAAC;IAC/E,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3G,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,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,iBAAiB,CA2CtF"}
@@ -0,0 +1,233 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { fileStore } from "./file-store.js";
6
+ import { isPathInside } from "./path.js";
7
+ import { readRegularFileSync } from "./regular-file.js";
8
+ import { root } from "./root.js";
9
+ import { writeSecretFileAtomic } from "./secret-file.js";
10
+ function resolvePrivateStorePath(rootDir, relativePath) {
11
+ const root = path.resolve(rootDir);
12
+ const raw = relativePath.trim();
13
+ if (!raw || path.isAbsolute(raw) || raw.includes("\0")) {
14
+ throw new Error("Private file path must be a relative path.");
15
+ }
16
+ const resolved = path.resolve(root, raw);
17
+ const rel = path.relative(root, resolved);
18
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
19
+ throw new Error("Private file path must stay under the private store root.");
20
+ }
21
+ return resolved;
22
+ }
23
+ export async function writePrivateTextAtomic(params) {
24
+ await writeSecretFileAtomic(params);
25
+ }
26
+ export async function readPrivateText(params) {
27
+ const rootDir = path.resolve(params.rootDir);
28
+ const filePath = path.resolve(params.filePath);
29
+ const relative = path.relative(rootDir, filePath);
30
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
31
+ throw new Error("Private file path must stay under the private store root.");
32
+ }
33
+ try {
34
+ const storeRoot = await root(rootDir, { hardlinks: "reject", maxBytes: params.maxBytes });
35
+ const result = await storeRoot.read(relative);
36
+ return result.buffer.toString("utf8");
37
+ }
38
+ catch (err) {
39
+ if (err instanceof FsSafeError && err.code === "not-found") {
40
+ return null;
41
+ }
42
+ throw err;
43
+ }
44
+ }
45
+ function assertPrivateReadPathSync(params) {
46
+ const rootDir = path.resolve(params.rootDir);
47
+ const filePath = path.resolve(params.filePath);
48
+ const relative = path.relative(rootDir, filePath);
49
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
50
+ throw new Error("Private file path must stay under the private store root.");
51
+ }
52
+ const rootStat = fs.lstatSync(rootDir);
53
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
54
+ throw new Error(`Private file root must be a directory: ${rootDir}`);
55
+ }
56
+ const rootReal = fs.realpathSync(rootDir);
57
+ const fileReal = fs.realpathSync(filePath);
58
+ if (!isPathInside(rootReal, fileReal)) {
59
+ throw new Error("Private file path must stay under the private store root.");
60
+ }
61
+ }
62
+ export function readPrivateTextSync(params) {
63
+ try {
64
+ assertPrivateReadPathSync(params);
65
+ const result = readRegularFileSync({
66
+ filePath: path.resolve(params.filePath),
67
+ maxBytes: params.maxBytes,
68
+ });
69
+ if (result.stat.nlink > 1) {
70
+ throw new Error(`Private file target must not be hardlinked: ${params.filePath}`);
71
+ }
72
+ return result.buffer.toString("utf8");
73
+ }
74
+ catch (err) {
75
+ if (err.code === "ENOENT") {
76
+ return null;
77
+ }
78
+ throw err;
79
+ }
80
+ }
81
+ export async function readPrivateJson(params) {
82
+ const raw = await readPrivateText(params);
83
+ return raw === null ? null : JSON.parse(raw);
84
+ }
85
+ export function readPrivateJsonSync(params) {
86
+ const raw = readPrivateTextSync(params);
87
+ return raw === null ? null : JSON.parse(raw);
88
+ }
89
+ function ensurePrivateDirectorySync(rootDir, targetDir) {
90
+ const root = path.resolve(rootDir);
91
+ const target = path.resolve(targetDir);
92
+ const relative = path.relative(root, target);
93
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
94
+ throw new Error("Private file directory must stay under the private store root.");
95
+ }
96
+ let current = root;
97
+ fs.mkdirSync(current, { recursive: true, mode: 0o700 });
98
+ const rootStat = fs.lstatSync(current);
99
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
100
+ throw new Error(`Private file root must be a directory: ${current}`);
101
+ }
102
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
103
+ current = path.join(current, segment);
104
+ try {
105
+ const stat = fs.lstatSync(current);
106
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
107
+ throw new Error(`Private file directory component must be a directory: ${current}`);
108
+ }
109
+ }
110
+ catch (err) {
111
+ if (err.code !== "ENOENT") {
112
+ throw err;
113
+ }
114
+ fs.mkdirSync(current, { mode: 0o700 });
115
+ }
116
+ try {
117
+ fs.chmodSync(current, 0o700);
118
+ }
119
+ catch {
120
+ // Best-effort on platforms that do not enforce POSIX modes.
121
+ }
122
+ }
123
+ }
124
+ export function writePrivateTextAtomicSync(params) {
125
+ const root = path.resolve(params.rootDir);
126
+ const filePath = path.resolve(params.filePath);
127
+ const relative = path.relative(root, filePath);
128
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
129
+ throw new Error("Private file path must stay under the private store root.");
130
+ }
131
+ ensurePrivateDirectorySync(root, path.dirname(filePath));
132
+ try {
133
+ const stat = fs.lstatSync(filePath);
134
+ if (stat.isSymbolicLink() || !stat.isFile()) {
135
+ throw new Error(`Private file target must be a regular file: ${filePath}`);
136
+ }
137
+ }
138
+ catch (err) {
139
+ if (err.code !== "ENOENT") {
140
+ throw err;
141
+ }
142
+ }
143
+ const tempPath = path.join(path.dirname(filePath), `.tmp-${process.pid}-${randomUUID()}`);
144
+ let created = false;
145
+ try {
146
+ fs.writeFileSync(tempPath, params.content, { mode: 0o600, flag: "wx" });
147
+ created = true;
148
+ try {
149
+ fs.chmodSync(tempPath, 0o600);
150
+ }
151
+ catch {
152
+ // Best-effort on platforms that do not enforce POSIX modes.
153
+ }
154
+ fs.renameSync(tempPath, filePath);
155
+ created = false;
156
+ try {
157
+ fs.chmodSync(filePath, 0o600);
158
+ }
159
+ catch {
160
+ // Best-effort on platforms that do not enforce POSIX modes.
161
+ }
162
+ }
163
+ finally {
164
+ if (created) {
165
+ try {
166
+ fs.unlinkSync(tempPath);
167
+ }
168
+ catch {
169
+ // The temp file is best-effort cleanup after write failure.
170
+ }
171
+ }
172
+ }
173
+ }
174
+ export async function writePrivateJsonAtomic(params) {
175
+ const json = JSON.stringify(params.value, null, 2);
176
+ await writeSecretFileAtomic({
177
+ rootDir: params.rootDir,
178
+ filePath: params.filePath,
179
+ content: params.trailingNewline && !json.endsWith("\n") ? `${json}\n` : json,
180
+ });
181
+ }
182
+ export function writePrivateJsonAtomicSync(params) {
183
+ const json = JSON.stringify(params.value, null, 2);
184
+ writePrivateTextAtomicSync({
185
+ rootDir: params.rootDir,
186
+ filePath: params.filePath,
187
+ content: params.trailingNewline && !json.endsWith("\n") ? `${json}\n` : json,
188
+ });
189
+ }
190
+ export function privateStateStore(options) {
191
+ const rootDir = path.resolve(options.rootDir);
192
+ const store = fileStore({ rootDir, private: true });
193
+ 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
+ },
213
+ writeText: async (relativePath, content) => {
214
+ const safePath = resolvePrivateStorePath(rootDir, relativePath);
215
+ await writePrivateTextAtomic({
216
+ rootDir,
217
+ filePath: safePath,
218
+ content,
219
+ });
220
+ return safePath;
221
+ },
222
+ writeJson: async (relativePath, value, options) => {
223
+ const safePath = resolvePrivateStorePath(rootDir, relativePath);
224
+ await writePrivateJsonAtomic({
225
+ rootDir,
226
+ filePath: safePath,
227
+ value,
228
+ trailingNewline: options?.trailingNewline,
229
+ });
230
+ return safePath;
231
+ },
232
+ };
233
+ }
@@ -0,0 +1,39 @@
1
+ import { type FileStore, type FileStoreSync } from "./file-store.js";
2
+ export type TempWorkspaceOptions = {
3
+ rootDir: string;
4
+ prefix: string;
5
+ dirMode?: number;
6
+ mode?: number;
7
+ };
8
+ export type TempWorkspace = {
9
+ dir: string;
10
+ store: FileStore;
11
+ path(fileName: string): string;
12
+ write(fileName: string, data: string | Uint8Array): Promise<string>;
13
+ writeText(fileName: string, data: string): Promise<string>;
14
+ writeJson(fileName: string, data: unknown, options?: {
15
+ trailingNewline?: boolean;
16
+ }): Promise<string>;
17
+ copyIn(fileName: string, sourcePath: string): Promise<string>;
18
+ read(fileName: string): Promise<Buffer>;
19
+ cleanup(): Promise<void>;
20
+ [Symbol.asyncDispose](): Promise<void>;
21
+ };
22
+ export type TempWorkspaceSync = {
23
+ dir: string;
24
+ store: FileStoreSync;
25
+ path(fileName: string): string;
26
+ write(fileName: string, data: string | Uint8Array): string;
27
+ writeText(fileName: string, data: string): string;
28
+ writeJson(fileName: string, data: unknown, options?: {
29
+ trailingNewline?: boolean;
30
+ }): string;
31
+ read(fileName: string): Buffer;
32
+ cleanup(): void;
33
+ [Symbol.dispose](): void;
34
+ };
35
+ export declare function tempWorkspace(options: TempWorkspaceOptions): Promise<TempWorkspace>;
36
+ export declare function withTempWorkspace<T>(options: TempWorkspaceOptions, run: (workspace: TempWorkspace) => Promise<T>): Promise<T>;
37
+ export declare function tempWorkspaceSync(options: TempWorkspaceOptions): TempWorkspaceSync;
38
+ export declare function withTempWorkspaceSync<T>(options: TempWorkspaceOptions, run: (workspace: TempWorkspaceSync) => T): T;
39
+ //# sourceMappingURL=private-temp-workspace.d.ts.map
@@ -0,0 +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"}