@openclaw/fs-safe 0.5.1 → 0.5.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 (210) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +18 -6
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +20 -12
  5. package/dist/archive-entry.d.ts +1 -0
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +25 -5
  8. package/dist/archive-input.d.ts +7 -0
  9. package/dist/archive-input.d.ts.map +1 -1
  10. package/dist/archive-input.js +11 -1
  11. package/dist/archive-limits.js +1 -1
  12. package/dist/archive-native.d.ts.map +1 -1
  13. package/dist/archive-native.js +6 -3
  14. package/dist/archive-policy.d.ts.map +1 -1
  15. package/dist/archive-policy.js +2 -1
  16. package/dist/archive-read.d.ts.map +1 -1
  17. package/dist/archive-read.js +30 -12
  18. package/dist/archive-staging.d.ts.map +1 -1
  19. package/dist/archive-staging.js +3 -2
  20. package/dist/archive-tar-meta.d.ts.map +1 -1
  21. package/dist/archive-tar-meta.js +7 -1
  22. package/dist/archive-tar.d.ts.map +1 -1
  23. package/dist/archive-tar.js +7 -4
  24. package/dist/archive-zip-entry.d.ts +1 -0
  25. package/dist/archive-zip-entry.d.ts.map +1 -1
  26. package/dist/archive-zip-integrity.d.ts +4 -0
  27. package/dist/archive-zip-integrity.d.ts.map +1 -0
  28. package/dist/archive-zip-integrity.js +44 -0
  29. package/dist/archive-zip-preflight.d.ts.map +1 -1
  30. package/dist/archive-zip-preflight.js +6 -1
  31. package/dist/archive.d.ts.map +1 -1
  32. package/dist/archive.js +5 -2
  33. package/dist/bounded-read-stream.d.ts.map +1 -1
  34. package/dist/bounded-read-stream.js +13 -2
  35. package/dist/deny-mutations.d.ts +1 -0
  36. package/dist/deny-mutations.d.ts.map +1 -1
  37. package/dist/deny-mutations.js +6 -39
  38. package/dist/device-path.d.ts +1 -0
  39. package/dist/device-path.d.ts.map +1 -1
  40. package/dist/device-path.js +6 -6
  41. package/dist/directory-guard.d.ts.map +1 -1
  42. package/dist/directory-guard.js +5 -4
  43. package/dist/error-detail.d.ts +2 -0
  44. package/dist/error-detail.d.ts.map +1 -0
  45. package/dist/error-detail.js +4 -0
  46. package/dist/errors.d.ts.map +1 -1
  47. package/dist/errors.js +3 -0
  48. package/dist/file-hash.d.ts.map +1 -1
  49. package/dist/file-hash.js +4 -1
  50. package/dist/file-lock-sync.d.ts.map +1 -1
  51. package/dist/file-lock-sync.js +88 -35
  52. package/dist/file-store-boundary.d.ts +6 -0
  53. package/dist/file-store-boundary.d.ts.map +1 -1
  54. package/dist/file-store-boundary.js +20 -5
  55. package/dist/file-store-sync-write.d.ts +9 -0
  56. package/dist/file-store-sync-write.d.ts.map +1 -0
  57. package/dist/file-store-sync-write.js +97 -0
  58. package/dist/file-store.d.ts.map +1 -1
  59. package/dist/file-store.js +36 -128
  60. package/dist/filename.d.ts.map +1 -1
  61. package/dist/filename.js +27 -0
  62. package/dist/guarded-mkdir.d.ts.map +1 -1
  63. package/dist/guarded-mkdir.js +5 -6
  64. package/dist/install-path.js +1 -1
  65. package/dist/json-document-store.d.ts.map +1 -1
  66. package/dist/json-document-store.js +19 -13
  67. package/dist/json.d.ts.map +1 -1
  68. package/dist/json.js +1 -3
  69. package/dist/local-file-access.d.ts +3 -2
  70. package/dist/local-file-access.d.ts.map +1 -1
  71. package/dist/local-file-access.js +12 -6
  72. package/dist/local-roots.d.ts.map +1 -1
  73. package/dist/local-roots.js +35 -30
  74. package/dist/move-path.d.ts.map +1 -1
  75. package/dist/move-path.js +51 -12
  76. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  77. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  78. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  79. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  80. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  81. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  82. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  83. package/dist/native-binding.d.ts +1 -0
  84. package/dist/native-binding.d.ts.map +1 -1
  85. package/dist/native-operations.d.ts.map +1 -1
  86. package/dist/native-operations.js +14 -1
  87. package/dist/native-pinned-write.d.ts.map +1 -1
  88. package/dist/native-pinned-write.js +44 -17
  89. package/dist/output-sibling.d.ts.map +1 -1
  90. package/dist/output-sibling.js +6 -5
  91. package/dist/path-policy.d.ts.map +1 -1
  92. package/dist/path-policy.js +1 -7
  93. package/dist/path.d.ts.map +1 -1
  94. package/dist/path.js +4 -0
  95. package/dist/permission-exec.d.ts +6 -0
  96. package/dist/permission-exec.d.ts.map +1 -0
  97. package/dist/permission-exec.js +28 -0
  98. package/dist/permissions-windows.d.ts +56 -0
  99. package/dist/permissions-windows.d.ts.map +1 -0
  100. package/dist/permissions-windows.js +285 -0
  101. package/dist/permissions.d.ts +3 -46
  102. package/dist/permissions.d.ts.map +1 -1
  103. package/dist/permissions.js +17 -328
  104. package/dist/pinned-write.js +2 -1
  105. package/dist/regular-file.d.ts.map +1 -1
  106. package/dist/regular-file.js +21 -2
  107. package/dist/replace-directory.d.ts.map +1 -1
  108. package/dist/replace-directory.js +10 -1
  109. package/dist/replace-file-copy-fallback.d.ts +3 -0
  110. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  111. package/dist/replace-file-copy-fallback.js +18 -11
  112. package/dist/replace-file-descriptor.d.ts +33 -0
  113. package/dist/replace-file-descriptor.d.ts.map +1 -0
  114. package/dist/replace-file-descriptor.js +96 -0
  115. package/dist/replace-file.d.ts +9 -2
  116. package/dist/replace-file.d.ts.map +1 -1
  117. package/dist/replace-file.js +64 -72
  118. package/dist/root-context.d.ts +8 -0
  119. package/dist/root-context.d.ts.map +1 -1
  120. package/dist/root-context.js +43 -2
  121. package/dist/root-errors.d.ts +7 -0
  122. package/dist/root-errors.d.ts.map +1 -1
  123. package/dist/root-errors.js +46 -1
  124. package/dist/root-file.d.ts +1 -0
  125. package/dist/root-file.d.ts.map +1 -1
  126. package/dist/root-file.js +2 -0
  127. package/dist/root-impl.d.ts.map +1 -1
  128. package/dist/root-impl.js +213 -168
  129. package/dist/root-path-symlink.d.ts +3 -0
  130. package/dist/root-path-symlink.d.ts.map +1 -0
  131. package/dist/root-path-symlink.js +35 -0
  132. package/dist/root-path.d.ts +1 -0
  133. package/dist/root-path.d.ts.map +1 -1
  134. package/dist/root-path.js +129 -190
  135. package/dist/root-walk.d.ts.map +1 -1
  136. package/dist/root-walk.js +15 -0
  137. package/dist/safe-path-segment.d.ts +2 -0
  138. package/dist/safe-path-segment.d.ts.map +1 -1
  139. package/dist/safe-path-segment.js +12 -0
  140. package/dist/secret-file.d.ts.map +1 -1
  141. package/dist/secret-file.js +19 -3
  142. package/dist/secure-temp-dir.d.ts.map +1 -1
  143. package/dist/secure-temp-dir.js +6 -1
  144. package/dist/short-path.d.ts +2 -0
  145. package/dist/short-path.d.ts.map +1 -0
  146. package/dist/short-path.js +7 -0
  147. package/dist/sidecar-lock-acquire.d.ts +30 -0
  148. package/dist/sidecar-lock-acquire.d.ts.map +1 -0
  149. package/dist/sidecar-lock-acquire.js +260 -0
  150. package/dist/sidecar-lock-policy.d.ts +3 -0
  151. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  152. package/dist/sidecar-lock-policy.js +20 -3
  153. package/dist/sidecar-lock-reclaim.d.ts +5 -1
  154. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  155. package/dist/sidecar-lock-reclaim.js +59 -9
  156. package/dist/sidecar-lock.d.ts.map +1 -1
  157. package/dist/sidecar-lock.js +27 -226
  158. package/dist/temp-target.d.ts.map +1 -1
  159. package/dist/temp-target.js +15 -4
  160. package/dist/test-hooks.d.ts +1 -0
  161. package/dist/test-hooks.d.ts.map +1 -1
  162. package/dist/timing.d.ts +2 -0
  163. package/dist/timing.d.ts.map +1 -1
  164. package/dist/timing.js +9 -0
  165. package/dist/walk.d.ts.map +1 -1
  166. package/dist/walk.js +15 -0
  167. package/docs/advanced.md +1 -1
  168. package/docs/archive.md +27 -20
  169. package/docs/assets/readme-banner.jpg +0 -0
  170. package/docs/atomic.md +48 -19
  171. package/docs/errors.md +31 -7
  172. package/docs/file-store.md +10 -1
  173. package/docs/filename.md +17 -16
  174. package/docs/index.md +1 -0
  175. package/docs/install-path.md +9 -6
  176. package/docs/json.md +8 -2
  177. package/docs/local-roots.md +79 -103
  178. package/docs/native-helper.md +11 -5
  179. package/docs/native.md +2 -2
  180. package/docs/output.md +8 -3
  181. package/docs/path-scope.md +4 -1
  182. package/docs/path.md +7 -3
  183. package/docs/permissions.md +8 -0
  184. package/docs/public-api.md +109 -0
  185. package/docs/quickstart.md +6 -2
  186. package/docs/reading.md +13 -10
  187. package/docs/root.md +26 -4
  188. package/docs/secret-file.md +17 -4
  189. package/docs/security-model.md +21 -4
  190. package/docs/sidecar-lock.md +15 -5
  191. package/docs/temp.md +37 -5
  192. package/docs/test-hooks.md +2 -0
  193. package/docs/types.md +32 -15
  194. package/docs/writing.md +25 -12
  195. package/package.json +7 -3
  196. package/dist/archive-file-io.d.ts +0 -9
  197. package/dist/archive-file-io.d.ts.map +0 -1
  198. package/dist/archive-file-io.js +0 -11
  199. package/dist/file-store-source.d.ts +0 -5
  200. package/dist/file-store-source.d.ts.map +0 -1
  201. package/dist/file-store-source.js +0 -30
  202. package/dist/file-url.d.ts +0 -2
  203. package/dist/file-url.d.ts.map +0 -1
  204. package/dist/file-url.js +0 -1
  205. package/dist/path-stat.d.ts +0 -4
  206. package/dist/path-stat.d.ts.map +0 -1
  207. package/dist/path-stat.js +0 -15
  208. package/dist/windows-permissions-native.d.ts +0 -9
  209. package/dist/windows-permissions-native.d.ts.map +0 -1
  210. package/dist/windows-permissions-native.js +0 -31
@@ -2,8 +2,11 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  import { readSidecarLockSnapshotSync, relativeSidecarLockPath, removeSidecarLockIfUnchangedSync, serializeSidecarLockPayload, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
5
- import { computeSidecarLockDelayMs, sidecarLockPayloadIsStale, } from "./sidecar-lock-policy.js";
5
+ import { computeSidecarLockDelayMs, sidecarLockPayloadCreatedAtMs, } from "./sidecar-lock-policy.js";
6
+ import { sleepSync } from "./timing.js";
6
7
  const SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
8
+ const SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
9
+ const SYNC_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupHandler");
7
10
  function getSyncHeldLocks() {
8
11
  const globalWithState = globalThis;
9
12
  if (!globalWithState[SYNC_HELD_LOCKS_KEY]) {
@@ -11,6 +14,36 @@ function getSyncHeldLocks() {
11
14
  }
12
15
  return globalWithState[SYNC_HELD_LOCKS_KEY];
13
16
  }
17
+ function releaseAllSyncHeldLocks() {
18
+ const heldLocks = getSyncHeldLocks();
19
+ for (const [normalizedTargetPath, held] of heldLocks) {
20
+ if (held.timer) {
21
+ clearInterval(held.timer);
22
+ held.timer = undefined;
23
+ }
24
+ try {
25
+ fs.closeSync(held.fd);
26
+ }
27
+ catch {
28
+ // Best-effort process-exit cleanup.
29
+ }
30
+ try {
31
+ removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
32
+ }
33
+ catch {
34
+ // A surviving sidecar fails closed and can be reclaimed by policy.
35
+ }
36
+ heldLocks.delete(normalizedTargetPath);
37
+ }
38
+ }
39
+ function ensureSyncExitCleanupRegistered() {
40
+ const globalWithCleanup = globalThis;
41
+ if (globalWithCleanup[SYNC_CLEANUP_REGISTERED_KEY])
42
+ return;
43
+ globalWithCleanup[SYNC_CLEANUP_REGISTERED_KEY] = true;
44
+ globalWithCleanup[SYNC_CLEANUP_HANDLER_KEY] = releaseAllSyncHeldLocks;
45
+ process.on("exit", releaseAllSyncHeldLocks);
46
+ }
14
47
  function verifySyncHeldLock(held) {
15
48
  const current = readSidecarLockSnapshotSync(held.lockPath, held.parsePayload);
16
49
  return !!current && sidecarLockSnapshotMatches(current, held.snapshot);
@@ -71,8 +104,9 @@ function boundedLockPath(lockPath, lockRoot) {
71
104
  return path.join(parentReal, path.basename(resolved));
72
105
  }
73
106
  function defaultShouldReclaim(payload, lockPath, staleMs, nowMs) {
74
- if (sidecarLockPayloadIsStale(payload, staleMs, nowMs))
75
- return true;
107
+ const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
108
+ if (createdAtMs !== null)
109
+ return nowMs - createdAtMs > staleMs;
76
110
  try {
77
111
  return nowMs - fs.statSync(lockPath).mtimeMs > staleMs;
78
112
  }
@@ -80,8 +114,16 @@ function defaultShouldReclaim(payload, lockPath, staleMs, nowMs) {
80
114
  return true;
81
115
  }
82
116
  }
83
- function sleep(milliseconds) {
84
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
117
+ function reclaimGuardExists(reclaimGuardPath) {
118
+ try {
119
+ fs.lstatSync(reclaimGuardPath);
120
+ return true;
121
+ }
122
+ catch (error) {
123
+ if (error.code === "ENOENT")
124
+ return false;
125
+ throw error;
126
+ }
85
127
  }
86
128
  export function acquireFileLockSync(targetPath, options) {
87
129
  const normalizedTargetPath = normalizeTargetPath(targetPath);
@@ -99,7 +141,25 @@ export function acquireFileLockSync(targetPath, options) {
99
141
  const retry = options.retry ?? {};
100
142
  const startedAt = Date.now();
101
143
  let attempt = 0;
144
+ const reclaimGuardPath = `${lockPath}.reclaim`;
145
+ const waitForRetry = () => {
146
+ const elapsed = Date.now() - startedAt;
147
+ const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
148
+ if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
149
+ throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
150
+ code: "file_lock_timeout",
151
+ lockPath,
152
+ normalizedTargetPath,
153
+ });
154
+ }
155
+ sleepSync(computeSidecarLockDelayMs(retry, attempt));
156
+ attempt += 1;
157
+ };
102
158
  while (true) {
159
+ if (reclaimGuardExists(reclaimGuardPath)) {
160
+ waitForRetry();
161
+ continue;
162
+ }
103
163
  let fd;
104
164
  try {
105
165
  const payload = options.payload();
@@ -126,6 +186,7 @@ export function acquireFileLockSync(targetPath, options) {
126
186
  snapshot,
127
187
  };
128
188
  heldLocks.set(normalizedTargetPath, createdHeld);
189
+ ensureSyncExitCleanupRegistered();
129
190
  const returnedHandle = createSyncHeldLockHandle(createdHeld);
130
191
  if (options.onCompromised && (options.compromiseCheckIntervalMs ?? 0) > 0) {
131
192
  createdHeld.timer = setInterval(() => {
@@ -151,20 +212,12 @@ export function acquireFileLockSync(targetPath, options) {
151
212
  if (error.code !== "EEXIST")
152
213
  throw error;
153
214
  if (heldLocks.has(normalizedTargetPath)) {
154
- const elapsed = Date.now() - startedAt;
155
- const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
156
- if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
157
- throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
158
- code: "file_lock_timeout",
159
- lockPath,
160
- normalizedTargetPath,
161
- });
162
- }
163
- sleep(computeSidecarLockDelayMs(retry, attempt));
164
- attempt += 1;
215
+ waitForRetry();
165
216
  continue;
166
217
  }
167
- const snapshot = readSidecarLockSnapshotSync(lockPath, options.parsePayload);
218
+ const snapshot = readSidecarLockSnapshotSync(lockPath, options.parsePayload, {
219
+ rejectNonFile: true,
220
+ });
168
221
  if (!snapshot)
169
222
  continue;
170
223
  const nowMs = Date.now();
@@ -187,18 +240,28 @@ export function acquireFileLockSync(targetPath, options) {
187
240
  raw: snapshot.raw,
188
241
  payload: snapshot.payload,
189
242
  })) {
190
- const reclaimGuard = `${lockPath}.reclaim`;
243
+ let ownsReclaimGuard = false;
191
244
  try {
192
- fs.mkdirSync(reclaimGuard);
245
+ fs.mkdirSync(reclaimGuardPath);
246
+ ownsReclaimGuard = true;
193
247
  if (removeSidecarLockIfUnchangedSync(lockPath, snapshot))
194
248
  continue;
195
249
  }
196
- finally {
197
- try {
198
- fs.rmdirSync(reclaimGuard);
250
+ catch (reclaimError) {
251
+ if (reclaimError.code !== "EEXIST") {
252
+ throw reclaimError;
199
253
  }
200
- catch {
201
- // A surviving reclaim guard fails closed.
254
+ waitForRetry();
255
+ continue;
256
+ }
257
+ finally {
258
+ if (ownsReclaimGuard) {
259
+ try {
260
+ fs.rmdirSync(reclaimGuardPath);
261
+ }
262
+ catch {
263
+ // A surviving reclaim guard fails closed.
264
+ }
202
265
  }
203
266
  }
204
267
  }
@@ -208,17 +271,7 @@ export function acquireFileLockSync(targetPath, options) {
208
271
  normalizedTargetPath,
209
272
  });
210
273
  }
211
- const elapsed = Date.now() - startedAt;
212
- const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
213
- if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
214
- throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
215
- code: "file_lock_timeout",
216
- lockPath,
217
- normalizedTargetPath,
218
- });
219
- }
220
- sleep(computeSidecarLockDelayMs(retry, attempt));
221
- attempt += 1;
274
+ waitForRetry();
222
275
  }
223
276
  }
224
277
  }
@@ -22,4 +22,10 @@ export declare function ensureParentSync(params: {
22
22
  filePath: string;
23
23
  mode: number;
24
24
  }): SyncParentGuard;
25
+ export declare function ensureStoreDirectorySync(params: {
26
+ rootDir: string;
27
+ targetDir: string;
28
+ mode: number;
29
+ messagePrefix: "private store" | "store";
30
+ }): SyncParentGuard;
25
31
  //# sourceMappingURL=file-store-boundary.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAA4C,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGhF,MAAM,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAOjD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,IAAI,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AA+CD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAuC1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAWrE;AAUD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,eAAe,CAwClB"}
1
+ {"version":3,"file":"file-store-boundary.d.ts","sourceRoot":"","sources":["../src/file-store-boundary.ts"],"names":[],"mappings":"AAGA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAA4C,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAGhF,MAAM,MAAM,eAAe,GAAG,kBAAkB,CAAC;AAOjD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,IAAI,EAChB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AA+CD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAuC1D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAWrE;AAUD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,eAAe,CAOlB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,eAAe,GAAG,OAAO,CAAC;CAC1C,GAAG,eAAe,CAyDlB"}
@@ -5,6 +5,7 @@ import { Transform } from "node:stream";
5
5
  import { pipeline } from "node:stream/promises";
6
6
  import { assertSyncDirectoryGuard as assertDirectoryGuardSync, createSyncDirectoryGuard, } from "./directory-guard.js";
7
7
  import { FsSafeError } from "./errors.js";
8
+ import { sameFileIdentity } from "./file-identity.js";
8
9
  import { isPathInside, isPathRelativeEscape } from "./path.js";
9
10
  import { resolveOpenedFileRealPathForHandle, root } from "./root.js";
10
11
  import { resolveSecureTempRoot } from "./secure-temp-dir.js";
@@ -128,8 +129,16 @@ function chmodDirectorySyncBestEffort(dir, mode) {
128
129
  }
129
130
  }
130
131
  export function ensureParentSync(params) {
132
+ return ensureStoreDirectorySync({
133
+ rootDir: params.rootDir,
134
+ targetDir: path.dirname(path.resolve(params.filePath)),
135
+ mode: params.mode,
136
+ messagePrefix: "store",
137
+ });
138
+ }
139
+ export function ensureStoreDirectorySync(params) {
131
140
  const rootDir = path.resolve(params.rootDir);
132
- const dir = path.dirname(path.resolve(params.filePath));
141
+ const dir = path.resolve(params.targetDir);
133
142
  const relative = path.relative(rootDir, dir);
134
143
  if (isPathRelativeEscape(relative)) {
135
144
  throw new FsSafeError("outside-workspace", "file path escapes store root");
@@ -137,7 +146,7 @@ export function ensureParentSync(params) {
137
146
  syncFs.mkdirSync(rootDir, { recursive: true, mode: params.mode });
138
147
  const rootStat = syncFs.lstatSync(rootDir);
139
148
  if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
140
- throw new FsSafeError("not-file", `store root must be a directory: ${rootDir}`);
149
+ throw new FsSafeError("not-file", `${params.messagePrefix} root must be a directory: ${rootDir}`);
141
150
  }
142
151
  const rootReal = syncFs.realpathSync(rootDir);
143
152
  chmodDirectorySyncBestEffort(rootDir, params.mode);
@@ -147,7 +156,7 @@ export function ensureParentSync(params) {
147
156
  try {
148
157
  const stat = syncFs.lstatSync(current);
149
158
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
150
- throw new FsSafeError("not-file", `store directory component must be a directory: ${current}`);
159
+ throw new FsSafeError("not-file", `${params.messagePrefix} directory component must be a directory: ${current}`);
151
160
  }
152
161
  }
153
162
  catch (error) {
@@ -156,9 +165,15 @@ export function ensureParentSync(params) {
156
165
  }
157
166
  syncFs.mkdirSync(current, { mode: params.mode });
158
167
  }
168
+ const currentRootStat = syncFs.lstatSync(rootDir);
169
+ const currentRootReal = syncFs.realpathSync(rootDir);
159
170
  const currentReal = syncFs.realpathSync(current);
160
- if (!isPathInside(rootReal, currentReal)) {
161
- throw new FsSafeError("outside-workspace", "store directory escapes root");
171
+ if (currentRootStat.isSymbolicLink() ||
172
+ !currentRootStat.isDirectory() ||
173
+ !sameFileIdentity(rootStat, currentRootStat) ||
174
+ currentRootReal !== rootReal ||
175
+ !isPathInside(rootReal, currentReal)) {
176
+ throw new FsSafeError("outside-workspace", `${params.messagePrefix} directory escapes root`);
162
177
  }
163
178
  chmodDirectorySyncBestEffort(current, params.mode);
164
179
  }
@@ -0,0 +1,9 @@
1
+ export declare function writeFileSyncAtomic(params: {
2
+ rootDir: string;
3
+ filePath: string;
4
+ content: string | Uint8Array;
5
+ privateMode: boolean;
6
+ dirMode: number;
7
+ mode: number;
8
+ }): string;
9
+ //# sourceMappingURL=file-store-sync-write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"AAuBA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CA+ET"}
@@ -0,0 +1,97 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { sameFileIdentity } from "./file-identity.js";
6
+ import { assertSyncDirectoryGuard, ensureParentSync, ensureStoreDirectorySync, } from "./file-store-boundary.js";
7
+ import { isPathInside } from "./path.js";
8
+ import { getFsSafeTestHooks } from "./test-hooks.js";
9
+ function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
10
+ return ensureStoreDirectorySync({
11
+ rootDir,
12
+ targetDir,
13
+ mode,
14
+ messagePrefix: "private store",
15
+ });
16
+ }
17
+ export function writeFileSyncAtomic(params) {
18
+ const filePath = path.resolve(params.filePath);
19
+ if (!isPathInside(params.rootDir, filePath)) {
20
+ throw new FsSafeError("outside-workspace", "file path escapes store root");
21
+ }
22
+ let parentGuard;
23
+ if (params.privateMode) {
24
+ parentGuard = ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
25
+ try {
26
+ const stat = fs.lstatSync(filePath);
27
+ if (stat.isSymbolicLink() || !stat.isFile()) {
28
+ throw new FsSafeError("not-file", `private store target must be a regular file: ${filePath}`);
29
+ }
30
+ }
31
+ catch (error) {
32
+ if (error.code !== "ENOENT") {
33
+ throw error;
34
+ }
35
+ }
36
+ }
37
+ else {
38
+ parentGuard = ensureParentSync({
39
+ rootDir: params.rootDir,
40
+ filePath,
41
+ mode: params.dirMode,
42
+ });
43
+ }
44
+ const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
45
+ let tempExists = false;
46
+ try {
47
+ getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
48
+ if (parentGuard) {
49
+ assertSyncDirectoryGuard(parentGuard);
50
+ }
51
+ fs.writeFileSync(tempPath, params.content, { flag: "wx", mode: params.mode });
52
+ tempExists = true;
53
+ try {
54
+ fs.chmodSync(tempPath, params.mode);
55
+ }
56
+ catch {
57
+ // Best-effort on platforms that do not enforce POSIX modes.
58
+ }
59
+ const tempStat = fs.lstatSync(tempPath);
60
+ if (parentGuard) {
61
+ assertSyncDirectoryGuard(parentGuard);
62
+ }
63
+ fs.renameSync(tempPath, filePath);
64
+ tempExists = false;
65
+ if (parentGuard) {
66
+ assertSyncDirectoryGuard(parentGuard);
67
+ }
68
+ try {
69
+ const publishedStat = fs.lstatSync(filePath);
70
+ if (publishedStat.isSymbolicLink() ||
71
+ !publishedStat.isFile() ||
72
+ publishedStat.nlink > 1 ||
73
+ !sameFileIdentity(tempStat, publishedStat)) {
74
+ throw new FsSafeError("path-mismatch", "store target changed after write");
75
+ }
76
+ }
77
+ catch (error) {
78
+ if (error instanceof FsSafeError) {
79
+ throw error;
80
+ }
81
+ throw new FsSafeError("path-mismatch", "store target changed after write", {
82
+ cause: error instanceof Error ? error : undefined,
83
+ });
84
+ }
85
+ return filePath;
86
+ }
87
+ finally {
88
+ if (tempExists) {
89
+ try {
90
+ fs.unlinkSync(tempPath);
91
+ }
92
+ catch {
93
+ // Best-effort cleanup after write failure.
94
+ }
95
+ }
96
+ }
97
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
@@ -1,36 +1,30 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import syncFs from "node:fs";
3
2
  import fs from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { readFileDescriptorBoundedSync } from "./bounded-read.js";
6
- import { createSyncDirectoryGuard } from "./directory-guard.js";
7
5
  import { FsSafeError } from "./errors.js";
8
- import { pruneExpiredStoreEntries, } from "./file-store-prune.js";
9
- import { assertSyncDirectoryGuard, ensureParentInRoot, ensureParentSync, openWritableStoreRoot, writeStreamToTempSource, } from "./file-store-boundary.js";
10
- import { readFileStoreCopySource } from "./file-store-source.js";
6
+ import { pruneExpiredStoreEntries } from "./file-store-prune.js";
7
+ import { ensureParentInRoot, openWritableStoreRoot, writeStreamToTempSource, } from "./file-store-boundary.js";
8
+ import { writeFileSyncAtomic } from "./file-store-sync-write.js";
11
9
  import { createJsonStore } from "./json-document-store.js";
12
10
  import { stringifyJsonDocument } from "./json-stringify.js";
13
- import { isPathInside, resolveSafeRelativePath } from "./path.js";
11
+ import { resolveSafeRelativePath } from "./path.js";
14
12
  import { root } from "./root.js";
15
13
  import { DEFAULT_ROOT_MAX_BYTES } from "./root-impl.js";
14
+ import { readRegularFile } from "./regular-file.js";
16
15
  import { matchRootFileOpenFailure, openRootFileSync } from "./root-file.js";
16
+ import { assertNoDriveRelativePathSegments } from "./safe-path-segment.js";
17
17
  import { writeSecretFileAtomic } from "./secret-file.js";
18
- import { getFsSafeTestHooks } from "./test-hooks.js";
19
18
  function assertRelativePath(relativePath) {
20
19
  const raw = relativePath.trim();
21
- if (!raw) {
22
- throw new FsSafeError("invalid-path", "relative path must be non-empty");
20
+ if (!raw || raw !== relativePath) {
21
+ throw new FsSafeError("invalid-path", "store key must be non-empty and unpadded");
23
22
  }
24
- return raw.replaceAll("\\", "/");
23
+ return assertNoDriveRelativePathSegments(raw.replaceAll("\\", "/"), "store key");
25
24
  }
26
25
  function resolveStorePath(rootDir, relativePath) {
27
26
  return resolveSafeRelativePath(rootDir, assertRelativePath(relativePath));
28
27
  }
29
- function assertStoreFilePath(rootDir, filePath) {
30
- if (!isPathInside(rootDir, filePath)) {
31
- throw new FsSafeError("outside-workspace", "file path escapes store root");
32
- }
33
- }
34
28
  function assertMaxBytes(size, maxBytes) {
35
29
  if (maxBytes !== undefined && size > maxBytes) {
36
30
  throw new FsSafeError("too-large", `file exceeds maximum size of ${maxBytes} bytes`);
@@ -69,6 +63,33 @@ function handleSyncStoreReadOpenFailure(opened) {
69
63
  },
70
64
  });
71
65
  }
66
+ async function readFileStoreCopySource(params) {
67
+ const sourceStat = await fs.lstat(params.sourcePath);
68
+ if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
69
+ throw new FsSafeError("not-file", "source path is not a file");
70
+ }
71
+ if (params.maxBytes !== undefined && sourceStat.size > params.maxBytes) {
72
+ throw new FsSafeError("too-large", `file exceeds maximum size of ${params.maxBytes} bytes`);
73
+ }
74
+ try {
75
+ return (await readRegularFile({ filePath: params.sourcePath, maxBytes: params.maxBytes }))
76
+ .buffer;
77
+ }
78
+ catch (error) {
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ if (message.includes("regular file") || message.includes("not a regular file")) {
81
+ throw new FsSafeError("not-file", "source path is not a file", {
82
+ cause: error instanceof Error ? error : undefined,
83
+ });
84
+ }
85
+ if (params.maxBytes !== undefined && message.includes(`exceeds ${params.maxBytes} bytes`)) {
86
+ throw new FsSafeError("too-large", `file exceeds maximum size of ${params.maxBytes} bytes`, {
87
+ cause: error instanceof Error ? error : undefined,
88
+ });
89
+ }
90
+ throw error;
91
+ }
92
+ }
72
93
  async function copyIntoRoot(params) {
73
94
  const relativePath = assertRelativePath(params.relativePath);
74
95
  const destination = resolveStorePath(params.rootDir, relativePath);
@@ -264,119 +285,6 @@ export function fileStore(options) {
264
285
  },
265
286
  };
266
287
  }
267
- function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
268
- const root = path.resolve(rootDir);
269
- const target = path.resolve(targetDir);
270
- assertStoreFilePath(root, target);
271
- let current = root;
272
- syncFs.mkdirSync(current, { recursive: true, mode });
273
- const rootStat = syncFs.lstatSync(current);
274
- if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
275
- throw new FsSafeError("not-file", `private store root must be a directory: ${current}`);
276
- }
277
- try {
278
- syncFs.chmodSync(current, mode);
279
- }
280
- catch {
281
- // Best-effort on platforms that do not enforce POSIX modes.
282
- }
283
- for (const segment of path.relative(root, target).split(path.sep).filter(Boolean)) {
284
- current = path.join(current, segment);
285
- try {
286
- const stat = syncFs.lstatSync(current);
287
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
288
- throw new FsSafeError("not-file", `private store directory component must be a directory: ${current}`);
289
- }
290
- }
291
- catch (error) {
292
- if (error.code !== "ENOENT") {
293
- throw error;
294
- }
295
- syncFs.mkdirSync(current, { mode });
296
- }
297
- const rootReal = syncFs.realpathSync(root);
298
- const currentReal = syncFs.realpathSync(current);
299
- if (!isPathInside(rootReal, currentReal)) {
300
- throw new FsSafeError("outside-workspace", "private store directory escapes root");
301
- }
302
- try {
303
- syncFs.chmodSync(current, mode);
304
- }
305
- catch {
306
- // Best-effort on platforms that do not enforce POSIX modes.
307
- }
308
- }
309
- const guard = createSyncDirectoryGuard(target);
310
- assertSyncDirectoryGuard(guard);
311
- return guard;
312
- }
313
- function writeFileSyncAtomic(params) {
314
- const filePath = path.resolve(params.filePath);
315
- assertStoreFilePath(params.rootDir, filePath);
316
- let parentGuard;
317
- if (params.privateMode) {
318
- parentGuard = ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
319
- try {
320
- const stat = syncFs.lstatSync(filePath);
321
- if (stat.isSymbolicLink() || !stat.isFile()) {
322
- throw new FsSafeError("not-file", `private store target must be a regular file: ${filePath}`);
323
- }
324
- }
325
- catch (error) {
326
- if (error.code !== "ENOENT") {
327
- throw error;
328
- }
329
- }
330
- }
331
- else {
332
- parentGuard = ensureParentSync({
333
- rootDir: params.rootDir,
334
- filePath,
335
- mode: params.dirMode,
336
- });
337
- }
338
- const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
339
- let tempExists = false;
340
- try {
341
- getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
342
- if (parentGuard) {
343
- assertSyncDirectoryGuard(parentGuard);
344
- }
345
- syncFs.writeFileSync(tempPath, params.content, { flag: "wx", mode: params.mode });
346
- tempExists = true;
347
- try {
348
- syncFs.chmodSync(tempPath, params.mode);
349
- }
350
- catch {
351
- // Best-effort on platforms that do not enforce POSIX modes.
352
- }
353
- if (parentGuard) {
354
- assertSyncDirectoryGuard(parentGuard);
355
- }
356
- syncFs.renameSync(tempPath, filePath);
357
- tempExists = false;
358
- if (parentGuard) {
359
- assertSyncDirectoryGuard(parentGuard);
360
- }
361
- try {
362
- syncFs.chmodSync(filePath, params.mode);
363
- }
364
- catch {
365
- // Best-effort on platforms that do not enforce POSIX modes.
366
- }
367
- return filePath;
368
- }
369
- finally {
370
- if (tempExists) {
371
- try {
372
- syncFs.unlinkSync(tempPath);
373
- }
374
- catch {
375
- // Best-effort cleanup after write failure.
376
- }
377
- }
378
- }
379
- }
380
288
  export function fileStoreSync(options) {
381
289
  const rootDir = path.resolve(options.rootDir);
382
290
  const privateMode = options.private ?? false;
@@ -1 +1 @@
1
- {"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"AAIA,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CA2BxF"}
1
+ {"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"AA4BA,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAgCxF"}
package/dist/filename.js CHANGED
@@ -1,5 +1,27 @@
1
1
  import path from "node:path";
2
+ import { WINDOWS_RESERVED_DEVICE_NAMES } from "./device-path.js";
2
3
  const WINDOWS_INVALID_FILE_NAME_CHARACTERS = new Set('<>:"/\\|?*');
4
+ function trimWindowsIgnoredSuffix(value) {
5
+ let end = value.length;
6
+ while (end > 0) {
7
+ const character = value.charCodeAt(end - 1);
8
+ if (character !== 0x20 && character !== 0x2e) {
9
+ break;
10
+ }
11
+ end -= 1;
12
+ }
13
+ return end === value.length ? value : value.slice(0, end);
14
+ }
15
+ function suffixWindowsReservedDeviceName(fileName) {
16
+ const extensionIndex = fileName.indexOf(".");
17
+ const baseNameEnd = extensionIndex < 0 ? fileName.length : extensionIndex;
18
+ const baseName = fileName.slice(0, baseNameEnd);
19
+ const deviceBaseName = trimWindowsIgnoredSuffix(baseName);
20
+ if (!WINDOWS_RESERVED_DEVICE_NAMES.has(deviceBaseName.toUpperCase())) {
21
+ return fileName;
22
+ }
23
+ return `${baseName}_${fileName.slice(baseNameEnd)}`;
24
+ }
3
25
  export function sanitizeUntrustedFileName(fileName, fallbackName) {
4
26
  const trimmed = typeof fileName === "string" ? fileName.trim() : "";
5
27
  if (!trimmed) {
@@ -21,8 +43,13 @@ export function sanitizeUntrustedFileName(fileName, fallbackName) {
21
43
  if (!base || base === "." || base === "..") {
22
44
  return fallbackName;
23
45
  }
46
+ base = suffixWindowsReservedDeviceName(base);
24
47
  if (base.length > 200) {
25
48
  base = base.slice(0, 200);
49
+ const trailingCodeUnit = base.charCodeAt(base.length - 1);
50
+ if (trailingCodeUnit >= 0xd800 && trailingCodeUnit <= 0xdbff) {
51
+ base = base.slice(0, -1);
52
+ }
26
53
  }
27
54
  return base;
28
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AA0BA;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuDlB"}
1
+ {"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AAyBA;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuDlB"}