@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
@@ -1,4 +1,6 @@
1
1
  import type { Readable } from "node:stream";
2
+ import { type FileStorePruneOptions } from "./file-store-prune.js";
3
+ export type { FileStorePruneOptions } from "./file-store-prune.js";
2
4
  import { type JsonFileStoreOptions, type JsonStore } from "./json-document-store.js";
3
5
  import { type OpenResult, type ReadResult, type Root, type RootReadOptions } from "./root.js";
4
6
  export type FileStoreOptions = {
@@ -17,12 +19,6 @@ export type FileStoreWriteOptions = {
17
19
  export type FileStoreReadOptions = RootReadOptions & {
18
20
  encoding?: BufferEncoding;
19
21
  };
20
- export type FileStorePruneOptions = {
21
- ttlMs: number;
22
- recursive?: boolean;
23
- maxDepth?: number;
24
- pruneEmptyDirs?: boolean;
25
- };
26
22
  export type FileStore = {
27
23
  readonly rootDir: string;
28
24
  path(relativePath: string): string;
@@ -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":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAUnE,OAAO,EAAmB,KAAK,oBAAoB,EAAE,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAEtG,OAAO,EAAQ,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,IAAI,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAMpG,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,eAAe,GAAG;IAAE,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,CAAC;AAEnF,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,KAAK,CACH,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,WAAW,CACT,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CACJ,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3E,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3E,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5E,QAAQ,CACN,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/F,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACxF,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAC1B,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACrB,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,SAAS,CACP,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,SAAS,CACP,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE,qBAAqB,GAAG;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAC9D,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;IACtF,YAAY,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,GAAG,IAAI,CAAC;IACvF,gBAAgB,CAAC,CAAC,GAAG,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,CAAC,GAAG,IAAI,CAAC;IAC/F,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IAChG,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IACpG,SAAS,CACP,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE,qBAAqB,GAAG;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAC9D,MAAM,CAAC;CACX,CAAC;AA2FF,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAoM9D;AAyHD,wBAAgB,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAgEtE"}
@@ -2,13 +2,18 @@ import { randomUUID } from "node:crypto";
2
2
  import syncFs from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { pipeline } from "node:stream/promises";
5
+ import { createSyncDirectoryGuard } from "./directory-guard.js";
6
6
  import { FsSafeError } from "./errors.js";
7
+ import { pruneExpiredStoreEntries, } from "./file-store-prune.js";
8
+ import { assertSyncDirectoryGuard, ensureParentInRoot, ensureParentSync, openWritableStoreRoot, writeStreamToTempSource, } from "./file-store-boundary.js";
9
+ import { readFileStoreCopySource } from "./file-store-source.js";
7
10
  import { createJsonStore } from "./json-document-store.js";
8
11
  import { isPathInside, resolveSafeRelativePath } from "./path.js";
9
12
  import { root } from "./root.js";
13
+ import { DEFAULT_ROOT_MAX_BYTES } from "./root-impl.js";
14
+ import { matchRootFileOpenFailure, openRootFileSync } from "./root-file.js";
10
15
  import { writeSecretFileAtomic } from "./secret-file.js";
11
- import { writeSiblingTempFile } from "./sibling-temp.js";
16
+ import { getFsSafeTestHooks } from "./test-hooks.js";
12
17
  function assertRelativePath(relativePath) {
13
18
  const raw = relativePath.trim();
14
19
  if (!raw) {
@@ -24,43 +29,65 @@ function assertStoreFilePath(rootDir, filePath) {
24
29
  throw new FsSafeError("outside-workspace", "file path escapes store root");
25
30
  }
26
31
  }
27
- async function ensureParent(filePath, mode) {
28
- const dir = path.dirname(filePath);
29
- await fs.mkdir(dir, { recursive: true, mode });
30
- await fs.chmod(dir, mode).catch(() => undefined);
31
- }
32
32
  function assertMaxBytes(size, maxBytes) {
33
33
  if (maxBytes !== undefined && size > maxBytes) {
34
34
  throw new FsSafeError("too-large", `file exceeds maximum size of ${maxBytes} bytes`);
35
35
  }
36
36
  }
37
37
  function isNotFound(error) {
38
+ if (!error) {
39
+ return false;
40
+ }
38
41
  return error instanceof FsSafeError
39
42
  ? error.code === "not-found"
40
43
  : error.code === "ENOENT" ||
41
44
  error.code === "ENOTDIR";
42
45
  }
46
+ function handleSyncStoreReadOpenFailure(opened) {
47
+ return matchRootFileOpenFailure(opened, {
48
+ path: (failure) => {
49
+ if (isNotFound(failure.error)) {
50
+ return null;
51
+ }
52
+ throw new FsSafeError("path-mismatch", "store target changed during read", {
53
+ cause: failure.error instanceof Error ? failure.error : undefined,
54
+ });
55
+ },
56
+ validation: (failure) => {
57
+ // Validation failures mean the path existed but violated store policy
58
+ // (directory, hardlink, symlink race). Do not report them as missing.
59
+ throw new FsSafeError("path-mismatch", "store target failed read validation", {
60
+ cause: failure.error instanceof Error ? failure.error : undefined,
61
+ });
62
+ },
63
+ fallback: (failure) => {
64
+ throw new FsSafeError("path-mismatch", "store target changed during read", {
65
+ cause: failure.error instanceof Error ? failure.error : undefined,
66
+ });
67
+ },
68
+ });
69
+ }
43
70
  async function copyIntoRoot(params) {
44
- const destination = resolveStorePath(params.rootDir, params.relativePath);
71
+ const relativePath = assertRelativePath(params.relativePath);
72
+ const destination = resolveStorePath(params.rootDir, relativePath);
45
73
  const sourceStat = await fs.lstat(params.sourcePath);
46
74
  if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
47
75
  throw new FsSafeError("not-file", "source path is not a file");
48
76
  }
49
77
  assertMaxBytes(sourceStat.size, params.maxBytes);
50
- await ensureParent(destination, params.dirMode ?? 0o700);
51
- const result = await writeSiblingTempFile({
52
- dir: path.dirname(destination),
53
- dirMode: params.dirMode ?? 0o700,
78
+ const dirMode = params.dirMode ?? 0o700;
79
+ const scopedRoot = await openWritableStoreRoot({
80
+ rootDir: params.rootDir,
81
+ dirMode,
82
+ maxBytes: params.maxBytes,
83
+ });
84
+ await ensureParentInRoot(scopedRoot, relativePath, dirMode);
85
+ await scopedRoot.copyIn(relativePath, params.sourcePath, {
86
+ maxBytes: params.maxBytes,
87
+ mkdir: false,
54
88
  mode: params.mode ?? 0o600,
55
- tempPrefix: params.tempPrefix ?? `.${path.basename(destination)}`,
56
- writeTemp: async (tempPath) => {
57
- await fs.copyFile(params.sourcePath, tempPath);
58
- },
59
- resolveFinalPath: () => destination,
60
- syncTempFile: true,
61
- syncParentDir: true,
62
89
  });
63
- return result.filePath;
90
+ return destination;
64
91
  }
65
92
  export function fileStore(options) {
66
93
  const rootDir = path.resolve(options.rootDir);
@@ -72,7 +99,8 @@ export function fileStore(options) {
72
99
  return await root(rootDir, { hardlinks: "reject", maxBytes });
73
100
  }
74
101
  async function write(relativePath, data, writeOptions) {
75
- const destination = resolveStorePath(rootDir, relativePath);
102
+ const safeRelativePath = assertRelativePath(relativePath);
103
+ const destination = resolveStorePath(rootDir, safeRelativePath);
76
104
  const content = Buffer.isBuffer(data) ? data : Buffer.from(data);
77
105
  assertMaxBytes(content.byteLength, writeOptions?.maxBytes ?? maxBytes);
78
106
  if (privateMode) {
@@ -85,20 +113,18 @@ export function fileStore(options) {
85
113
  });
86
114
  return destination;
87
115
  }
88
- await ensureParent(destination, writeOptions?.dirMode ?? dirMode);
89
- const result = await writeSiblingTempFile({
90
- dir: path.dirname(destination),
91
- dirMode: writeOptions?.dirMode ?? dirMode,
116
+ const writeDirMode = writeOptions?.dirMode ?? dirMode;
117
+ const scopedRoot = await openWritableStoreRoot({
118
+ rootDir,
119
+ dirMode: writeDirMode,
120
+ maxBytes: writeOptions?.maxBytes ?? maxBytes,
121
+ });
122
+ await ensureParentInRoot(scopedRoot, safeRelativePath, writeDirMode);
123
+ await scopedRoot.write(safeRelativePath, content, {
124
+ mkdir: false,
92
125
  mode: writeOptions?.mode ?? mode,
93
- tempPrefix: writeOptions?.tempPrefix ?? `.${path.basename(destination)}`,
94
- writeTemp: async (tempPath) => {
95
- await fs.writeFile(tempPath, content);
96
- },
97
- resolveFinalPath: () => destination,
98
- syncTempFile: true,
99
- syncParentDir: true,
100
126
  });
101
- return result.filePath;
127
+ return destination;
102
128
  }
103
129
  return {
104
130
  rootDir,
@@ -106,8 +132,9 @@ export function fileStore(options) {
106
132
  root: openRoot,
107
133
  write,
108
134
  writeStream: async (relativePath, stream, writeOptions) => {
109
- const destination = resolveStorePath(rootDir, relativePath);
110
- const limit = writeOptions?.maxBytes ?? maxBytes;
135
+ const safeRelativePath = assertRelativePath(relativePath);
136
+ const destination = resolveStorePath(rootDir, safeRelativePath);
137
+ const limit = writeOptions?.maxBytes ?? maxBytes ?? (privateMode ? DEFAULT_ROOT_MAX_BYTES : undefined);
111
138
  if (privateMode) {
112
139
  const chunks = [];
113
140
  let total = 0;
@@ -126,43 +153,34 @@ export function fileStore(options) {
126
153
  });
127
154
  return destination;
128
155
  }
129
- await ensureParent(destination, writeOptions?.dirMode ?? dirMode);
130
- let total = 0;
131
- const result = await writeSiblingTempFile({
132
- dir: path.dirname(destination),
133
- dirMode: writeOptions?.dirMode ?? dirMode,
156
+ const staged = await writeStreamToTempSource({
157
+ stream,
158
+ maxBytes: limit,
134
159
  mode: writeOptions?.mode ?? mode,
135
- tempPrefix: writeOptions?.tempPrefix ?? `.${path.basename(destination)}`,
136
- writeTemp: async (tempPath) => {
137
- const writable = await fs.open(tempPath, "w", writeOptions?.mode ?? mode);
138
- try {
139
- const out = writable.createWriteStream();
140
- stream.on("data", (chunk) => {
141
- total += Buffer.byteLength(chunk);
142
- if (limit !== undefined && total > limit) {
143
- stream.destroy(new FsSafeError("too-large", `file exceeds maximum size of ${limit} bytes`));
144
- }
145
- });
146
- await pipeline(stream, out);
147
- }
148
- finally {
149
- await writable.close().catch(() => undefined);
150
- }
151
- },
152
- resolveFinalPath: () => destination,
153
- syncTempFile: true,
154
- syncParentDir: true,
155
160
  });
156
- return result.filePath;
161
+ try {
162
+ await copyIntoRoot({
163
+ rootDir,
164
+ relativePath: safeRelativePath,
165
+ sourcePath: staged.path,
166
+ maxBytes: limit,
167
+ mode: writeOptions?.mode ?? mode,
168
+ tempPrefix: writeOptions?.tempPrefix,
169
+ dirMode: writeOptions?.dirMode ?? dirMode,
170
+ });
171
+ }
172
+ finally {
173
+ await staged.cleanup();
174
+ }
175
+ return destination;
157
176
  },
158
177
  copyIn: async (relativePath, sourcePath, writeOptions) => privateMode
159
178
  ? await (async () => {
160
- const sourceStat = await fs.lstat(sourcePath);
161
- if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
162
- throw new FsSafeError("not-file", "source path is not a file");
163
- }
164
- assertMaxBytes(sourceStat.size, writeOptions?.maxBytes ?? maxBytes);
165
- return await write(relativePath, await fs.readFile(sourcePath), writeOptions);
179
+ const buffer = await readFileStoreCopySource({
180
+ sourcePath,
181
+ maxBytes: writeOptions?.maxBytes ?? maxBytes ?? DEFAULT_ROOT_MAX_BYTES,
182
+ });
183
+ return await write(relativePath, buffer, writeOptions);
166
184
  })()
167
185
  : await copyIntoRoot({
168
186
  rootDir,
@@ -240,50 +258,10 @@ export function fileStore(options) {
240
258
  }, jsonOptions);
241
259
  },
242
260
  pruneExpired: async (pruneOptions) => {
243
- const now = Date.now();
244
- const recursive = pruneOptions.recursive ?? false;
245
- const maxDepth = pruneOptions.maxDepth;
246
- const pruneEmptyDirs = (recursive || maxDepth !== undefined) && (pruneOptions.pruneEmptyDirs ?? false);
247
- async function pruneDir(dir, depth) {
248
- const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
249
- for (const entry of entries) {
250
- const fullPath = path.join(dir, entry.name);
251
- const stat = await fs.lstat(fullPath).catch(() => null);
252
- if (!stat || stat.isSymbolicLink()) {
253
- continue;
254
- }
255
- if (stat.isDirectory()) {
256
- const shouldDescend = maxDepth !== undefined ? depth < maxDepth : recursive;
257
- if (shouldDescend && (await pruneDir(fullPath, depth + 1))) {
258
- await fs.rmdir(fullPath).catch(() => undefined);
259
- }
260
- continue;
261
- }
262
- if (stat.isFile() && now - stat.mtimeMs > pruneOptions.ttlMs) {
263
- await fs.rm(fullPath, { force: true }).catch(() => undefined);
264
- }
265
- }
266
- if (!pruneEmptyDirs) {
267
- return false;
268
- }
269
- const remaining = await fs.readdir(dir).catch(() => null);
270
- return remaining !== null && remaining.length === 0;
271
- }
272
- await fs.mkdir(rootDir, { recursive: true, mode: dirMode });
273
- await pruneDir(rootDir, 0);
261
+ await pruneExpiredStoreEntries({ rootDir, dirMode, options: pruneOptions });
274
262
  },
275
263
  };
276
264
  }
277
- function ensureParentSync(filePath, mode) {
278
- const dir = path.dirname(filePath);
279
- syncFs.mkdirSync(dir, { recursive: true, mode });
280
- try {
281
- syncFs.chmodSync(dir, mode);
282
- }
283
- catch {
284
- // Best-effort on platforms that do not enforce POSIX modes.
285
- }
286
- }
287
265
  function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
288
266
  const root = path.resolve(rootDir);
289
267
  const target = path.resolve(targetDir);
@@ -326,12 +304,16 @@ function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
326
304
  // Best-effort on platforms that do not enforce POSIX modes.
327
305
  }
328
306
  }
307
+ const guard = createSyncDirectoryGuard(target);
308
+ assertSyncDirectoryGuard(guard);
309
+ return guard;
329
310
  }
330
311
  function writeFileSyncAtomic(params) {
331
312
  const filePath = path.resolve(params.filePath);
332
313
  assertStoreFilePath(params.rootDir, filePath);
314
+ let parentGuard;
333
315
  if (params.privateMode) {
334
- ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
316
+ parentGuard = ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
335
317
  try {
336
318
  const stat = syncFs.lstatSync(filePath);
337
319
  if (stat.isSymbolicLink() || !stat.isFile()) {
@@ -345,11 +327,19 @@ function writeFileSyncAtomic(params) {
345
327
  }
346
328
  }
347
329
  else {
348
- ensureParentSync(filePath, params.dirMode);
330
+ parentGuard = ensureParentSync({
331
+ rootDir: params.rootDir,
332
+ filePath,
333
+ mode: params.dirMode,
334
+ });
349
335
  }
350
- const tempPath = path.join(path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
336
+ const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
351
337
  let tempExists = false;
352
338
  try {
339
+ getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
340
+ if (parentGuard) {
341
+ assertSyncDirectoryGuard(parentGuard);
342
+ }
353
343
  syncFs.writeFileSync(tempPath, params.content, { flag: "wx", mode: params.mode });
354
344
  tempExists = true;
355
345
  try {
@@ -358,8 +348,14 @@ function writeFileSyncAtomic(params) {
358
348
  catch {
359
349
  // Best-effort on platforms that do not enforce POSIX modes.
360
350
  }
351
+ if (parentGuard) {
352
+ assertSyncDirectoryGuard(parentGuard);
353
+ }
361
354
  syncFs.renameSync(tempPath, filePath);
362
355
  tempExists = false;
356
+ if (parentGuard) {
357
+ assertSyncDirectoryGuard(parentGuard);
358
+ }
363
359
  try {
364
360
  syncFs.chmodSync(filePath, params.mode);
365
361
  }
@@ -403,22 +399,23 @@ export function fileStoreSync(options) {
403
399
  path: (relativePath) => resolveStorePath(rootDir, relativePath),
404
400
  readTextIfExists: (relativePath, readOptions) => {
405
401
  const targetPath = resolveStorePath(rootDir, relativePath);
402
+ const opened = openRootFileSync({
403
+ absolutePath: targetPath,
404
+ rootPath: rootDir,
405
+ boundaryLabel: "store root",
406
+ rejectHardlinks: privateMode,
407
+ });
408
+ if (!opened.ok) {
409
+ return handleSyncStoreReadOpenFailure(opened);
410
+ }
406
411
  try {
407
- const stat = syncFs.lstatSync(targetPath);
408
- if (stat.isSymbolicLink() || !stat.isFile()) {
409
- throw new FsSafeError("not-file", "store target is not a file");
410
- }
411
- assertMaxBytes(stat.size, readOptions?.maxBytes ?? maxBytes);
412
- if (privateMode && stat.nlink > 1) {
413
- throw new FsSafeError("hardlink", "private store target must not be hardlinked");
414
- }
415
- return syncFs.readFileSync(targetPath, "utf8");
412
+ assertMaxBytes(opened.stat.size, readOptions?.maxBytes ?? maxBytes);
413
+ const raw = syncFs.readFileSync(opened.fd, "utf8");
414
+ assertMaxBytes(Buffer.byteLength(raw, "utf8"), readOptions?.maxBytes ?? maxBytes);
415
+ return raw;
416
416
  }
417
- catch (error) {
418
- if (isNotFound(error)) {
419
- return null;
420
- }
421
- throw error;
417
+ finally {
418
+ syncFs.closeSync(opened.fd);
422
419
  }
423
420
  },
424
421
  readJsonIfExists: (relativePath, readOptions) => {
@@ -0,0 +1,6 @@
1
+ export declare function mkdirPathComponentsWithGuards(params: {
2
+ rootReal: string;
3
+ targetPath: string;
4
+ beforeComponent?: (componentPath: string) => Promise<void> | void;
5
+ }): Promise<void>;
6
+ //# sourceMappingURL=guarded-mkdir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AAaA,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,IAAI,CAAC,CAiChB"}
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
4
+ import { FsSafeError } from "./errors.js";
5
+ function isSameOrChildPath(candidate, parent) {
6
+ return candidate === parent || candidate.startsWith(`${parent}${path.sep}`);
7
+ }
8
+ function isPathEscape(relativePath) {
9
+ return relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
10
+ }
11
+ export async function mkdirPathComponentsWithGuards(params) {
12
+ const root = path.resolve(params.rootReal);
13
+ const target = path.resolve(params.targetPath);
14
+ const relative = path.relative(root, target);
15
+ if (isPathEscape(relative)) {
16
+ throw new FsSafeError("outside-workspace", "directory is outside workspace root");
17
+ }
18
+ let current = root;
19
+ for (const part of relative.split(path.sep).filter(Boolean)) {
20
+ const next = path.join(current, part);
21
+ const parentGuard = await createAsyncDirectoryGuard(current);
22
+ await params.beforeComponent?.(next);
23
+ await assertAsyncDirectoryGuard(parentGuard);
24
+ try {
25
+ await fs.mkdir(next);
26
+ }
27
+ catch (error) {
28
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
29
+ throw error;
30
+ }
31
+ }
32
+ const stat = await fs.lstat(next);
33
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
34
+ throw new FsSafeError("not-file", "directory component must be a directory");
35
+ }
36
+ // Node's recursive mkdir follows symlinks in missing components. Build one
37
+ // segment at a time and realpath-check each segment before descending.
38
+ if (!isSameOrChildPath(path.resolve(await fs.realpath(next)), root)) {
39
+ throw new FsSafeError("outside-workspace", "directory escaped workspace root");
40
+ }
41
+ await createAsyncDirectoryGuard(next);
42
+ await assertAsyncDirectoryGuard(parentGuard);
43
+ current = next;
44
+ }
45
+ }
@@ -0,0 +1,33 @@
1
+ import { type AsyncDirectoryGuard, type SyncDirectoryGuard } from "./directory-guard.js";
2
+ export declare function withAsyncDirectoryGuards<T>(guards: readonly AsyncDirectoryGuard[], mutate: () => Promise<T>, options?: {
3
+ verifyAfter?: boolean;
4
+ onPostGuardFailure?: (result: T, error: unknown) => Promise<void> | void;
5
+ }): Promise<T>;
6
+ export declare function withSyncDirectoryGuards<T>(guards: readonly SyncDirectoryGuard[], mutate: () => T, options?: {
7
+ verifyAfter?: boolean;
8
+ }): T;
9
+ export declare function guardedRename(params: {
10
+ from: string;
11
+ to: string;
12
+ targetRoot?: string;
13
+ verifyAfter?: boolean;
14
+ }): Promise<void>;
15
+ export declare function guardedRenameSync(params: {
16
+ from: string;
17
+ to: string;
18
+ targetRoot?: string;
19
+ verifyAfter?: boolean;
20
+ }): void;
21
+ export declare function guardedRm(params: {
22
+ target: string;
23
+ recursive?: boolean;
24
+ force?: boolean;
25
+ verifyAfter?: boolean;
26
+ }): Promise<void>;
27
+ export declare function guardedRmSync(params: {
28
+ target: string;
29
+ recursive?: boolean;
30
+ force?: boolean;
31
+ verifyAfter?: boolean;
32
+ }): void;
33
+ //# sourceMappingURL=guarded-mutation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guarded-mutation.d.ts","sourceRoot":"","sources":["../src/guarded-mutation.ts"],"names":[],"mappings":"AAGA,OAAO,EAOL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAE9B,wBAAsB,wBAAwB,CAAC,CAAC,EAC9C,MAAM,EAAE,SAAS,mBAAmB,EAAE,EACtC,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACxB,OAAO,GAAE;IACP,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACrE,GACL,OAAO,CAAC,CAAC,CAAC,CAyBZ;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,MAAM,EAAE,SAAS,kBAAkB,EAAE,EACrC,MAAM,EAAE,MAAM,CAAC,EACf,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,CAAC,CAWH;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,IAAI,CAUP;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,GAAG,IAAI,CAWP"}
@@ -0,0 +1,76 @@
1
+ import fsSync from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { assertAsyncDirectoryGuard, assertSyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, createNearestExistingSyncDirectoryGuard, createSyncDirectoryGuard, } from "./directory-guard.js";
5
+ export async function withAsyncDirectoryGuards(guards, mutate, options = {}) {
6
+ for (const guard of guards) {
7
+ await assertAsyncDirectoryGuard(guard);
8
+ }
9
+ const result = await mutate();
10
+ if (options.verifyAfter !== false) {
11
+ try {
12
+ for (const guard of guards) {
13
+ await assertAsyncDirectoryGuard(guard);
14
+ }
15
+ }
16
+ catch (error) {
17
+ if (options.onPostGuardFailure) {
18
+ try {
19
+ // The mutation may have returned an owned resource before the post-guard
20
+ // check detected a swapped directory. Give callers one chance to close
21
+ // handles without letting cleanup hide the boundary failure.
22
+ await options.onPostGuardFailure(result, error);
23
+ }
24
+ catch {
25
+ // Preserve the boundary failure. Cleanup is best-effort.
26
+ }
27
+ }
28
+ throw error;
29
+ }
30
+ }
31
+ return result;
32
+ }
33
+ export function withSyncDirectoryGuards(guards, mutate, options = {}) {
34
+ for (const guard of guards) {
35
+ assertSyncDirectoryGuard(guard);
36
+ }
37
+ const result = mutate();
38
+ if (options.verifyAfter !== false) {
39
+ for (const guard of guards) {
40
+ assertSyncDirectoryGuard(guard);
41
+ }
42
+ }
43
+ return result;
44
+ }
45
+ export async function guardedRename(params) {
46
+ const sourceGuard = await createAsyncDirectoryGuard(path.dirname(params.from));
47
+ const targetGuard = params.targetRoot
48
+ ? await createNearestExistingDirectoryGuard(params.targetRoot, path.dirname(params.to))
49
+ : await createAsyncDirectoryGuard(path.dirname(params.to));
50
+ await withAsyncDirectoryGuards([sourceGuard, targetGuard], async () => {
51
+ await fs.rename(params.from, params.to);
52
+ }, { verifyAfter: params.verifyAfter });
53
+ }
54
+ export function guardedRenameSync(params) {
55
+ const sourceGuard = createSyncDirectoryGuard(path.dirname(params.from));
56
+ const targetGuard = params.targetRoot
57
+ ? createNearestExistingSyncDirectoryGuard(params.targetRoot, path.dirname(params.to))
58
+ : createSyncDirectoryGuard(path.dirname(params.to));
59
+ withSyncDirectoryGuards([sourceGuard, targetGuard], () => fsSync.renameSync(params.from, params.to), { verifyAfter: params.verifyAfter });
60
+ }
61
+ export async function guardedRm(params) {
62
+ const guard = await createAsyncDirectoryGuard(path.dirname(params.target));
63
+ await withAsyncDirectoryGuards([guard], async () => {
64
+ await fs.rm(params.target, {
65
+ ...(params.recursive !== undefined ? { recursive: params.recursive } : {}),
66
+ ...(params.force !== undefined ? { force: params.force } : {}),
67
+ });
68
+ }, { verifyAfter: params.verifyAfter });
69
+ }
70
+ export function guardedRmSync(params) {
71
+ const guard = createSyncDirectoryGuard(path.dirname(params.target));
72
+ withSyncDirectoryGuards([guard], () => fsSync.rmSync(params.target, {
73
+ ...(params.recursive !== undefined ? { recursive: params.recursive } : {}),
74
+ ...(params.force !== undefined ? { force: params.force } : {}),
75
+ }), { verifyAfter: params.verifyAfter });
76
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { FsSafeError, categorizeFsSafeError, type FsSafeErrorCategory, type FsSafeErrorCode, } from "./errors.js";
2
2
  export { DEFAULT_ROOT_MAX_BYTES, root, type HardlinkPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root.js";
3
3
  export { configureFsSafePython, getFsSafePythonConfig, type FsSafePythonConfig, type FsSafePythonMode, } from "./pinned-python-config.js";
4
+ export { writeExternalFileWithinRoot, type ExternalFileWriteOptions, type ExternalFileWriteResult, } from "./output.js";
5
+ export { configureFsSafeLocks, getFsSafeLockConfig, type FsSafeLockConfig, } from "./lock-config.js";
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,IAAI,EACJ,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,IAAI,EACJ,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,2BAA2B,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { FsSafeError, categorizeFsSafeError, } from "./errors.js";
2
2
  export { DEFAULT_ROOT_MAX_BYTES, root, } from "./root.js";
3
3
  export { configureFsSafePython, getFsSafePythonConfig, } from "./pinned-python-config.js";
4
+ export { writeExternalFileWithinRoot, } from "./output.js";
5
+ export { configureFsSafeLocks, getFsSafeLockConfig, } from "./lock-config.js";
@@ -1,8 +1,10 @@
1
1
  import type { FileLockRetryOptions } from "./file-lock.js";
2
+ import type { SidecarLockStaleRecovery } from "./sidecar-lock.js";
2
3
  export type JsonStoreLockOptions = {
3
4
  staleMs?: number;
4
5
  timeoutMs?: number;
5
6
  retry?: FileLockRetryOptions;
7
+ staleRecovery?: SidecarLockStaleRecovery;
6
8
  managerKey?: string;
7
9
  };
8
10
  export type JsonFileStoreOptions = {
@@ -1 +1 @@
1
- {"version":3,"file":"json-document-store.d.ts","sourceRoot":"","sources":["../src/json-document-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG3D,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC/B,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAChC,YAAY,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACpE,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,IAAI,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAClC,YAAY,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE,CAAC;AAyBF,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC5B,OAAO,GAAE,oBAAyB,GACjC,SAAS,CAAC,CAAC,CAAC,CA0Dd"}
1
+ {"version":3,"file":"json-document-store.d.ts","sourceRoot":"","sources":["../src/json-document-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAElE,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC/B,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAChC,YAAY,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACpE,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,IAAI,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAClC,YAAY,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE,CAAC;AA2BF,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC5B,OAAO,GAAE,oBAAyB,GACjC,SAAS,CAAC,CAAC,CAAC,CA2Dd"}
@@ -1,3 +1,4 @@
1
+ import { getFsSafeLockConfig } from "./lock-config.js";
1
2
  import { createSidecarLockManager } from "./sidecar-lock.js";
2
3
  function cloneFallback(value) {
3
4
  if (value && typeof value === "object") {
@@ -10,11 +11,13 @@ function resolveLockOptions(filePath, options) {
10
11
  return null;
11
12
  }
12
13
  const lockOptions = options.lock === true ? {} : options.lock;
14
+ const defaults = getFsSafeLockConfig();
13
15
  return {
14
16
  managerKey: lockOptions.managerKey ?? `fs-safe.json-store:${filePath}`,
15
- retry: lockOptions.retry ?? {},
16
- staleMs: lockOptions.staleMs ?? 30_000,
17
- timeoutMs: lockOptions.timeoutMs ?? 30_000,
17
+ retry: lockOptions.retry ?? defaults.retry ?? {},
18
+ staleMs: lockOptions.staleMs ?? defaults.staleMs ?? 30_000,
19
+ staleRecovery: lockOptions.staleRecovery ?? defaults.staleRecovery,
20
+ timeoutMs: lockOptions.timeoutMs ?? defaults.timeoutMs ?? 30_000,
18
21
  };
19
22
  }
20
23
  export function createJsonStore(adapter, options = {}) {
@@ -40,6 +43,7 @@ export function createJsonStore(adapter, options = {}) {
40
43
  staleMs: lockOptions.staleMs,
41
44
  timeoutMs: lockOptions.timeoutMs,
42
45
  retry: lockOptions.retry,
46
+ staleRecovery: lockOptions.staleRecovery,
43
47
  allowReentrant: true,
44
48
  payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
45
49
  }, run);