@openclaw/fs-safe 0.1.2 → 0.2.1

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 (120) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +39 -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 +2 -4
  26. package/dist/file-store-boundary.d.ts.map +1 -1
  27. package/dist/file-store-boundary.js +11 -7
  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 +60 -53
  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/home-dir.d.ts.map +1 -1
  44. package/dist/home-dir.js +38 -33
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +2 -0
  48. package/dist/json-document-store.d.ts +2 -0
  49. package/dist/json-document-store.d.ts.map +1 -1
  50. package/dist/json-document-store.js +7 -3
  51. package/dist/json-durable-queue.d.ts +45 -0
  52. package/dist/json-durable-queue.d.ts.map +1 -0
  53. package/dist/json-durable-queue.js +200 -0
  54. package/dist/json.d.ts +35 -5
  55. package/dist/json.d.ts.map +1 -1
  56. package/dist/json.js +69 -2
  57. package/dist/local-roots.d.ts.map +1 -1
  58. package/dist/local-roots.js +17 -3
  59. package/dist/lock-config.d.ts +10 -0
  60. package/dist/lock-config.d.ts.map +1 -0
  61. package/dist/lock-config.js +12 -0
  62. package/dist/move-path.d.ts +1 -0
  63. package/dist/move-path.d.ts.map +1 -1
  64. package/dist/move-path.js +195 -7
  65. package/dist/output.d.ts +13 -0
  66. package/dist/output.d.ts.map +1 -0
  67. package/dist/output.js +69 -0
  68. package/dist/path-stat.d.ts +4 -0
  69. package/dist/path-stat.d.ts.map +1 -0
  70. package/dist/path-stat.js +15 -0
  71. package/dist/path.d.ts.map +1 -1
  72. package/dist/path.js +4 -3
  73. package/dist/pinned-write.d.ts.map +1 -1
  74. package/dist/pinned-write.js +44 -19
  75. package/dist/private-temp-workspace.d.ts.map +1 -1
  76. package/dist/private-temp-workspace.js +28 -15
  77. package/dist/regular-file.d.ts.map +1 -1
  78. package/dist/regular-file.js +52 -8
  79. package/dist/replace-directory.d.ts.map +1 -1
  80. package/dist/replace-directory.js +5 -4
  81. package/dist/replace-file.d.ts +1 -1
  82. package/dist/replace-file.d.ts.map +1 -1
  83. package/dist/replace-file.js +93 -31
  84. package/dist/root-impl.d.ts.map +1 -1
  85. package/dist/root-impl.js +62 -53
  86. package/dist/safe-path-segment.d.ts +9 -0
  87. package/dist/safe-path-segment.d.ts.map +1 -0
  88. package/dist/safe-path-segment.js +51 -0
  89. package/dist/secret-file.d.ts.map +1 -1
  90. package/dist/secret-file.js +28 -7
  91. package/dist/sibling-temp.d.ts +1 -0
  92. package/dist/sibling-temp.d.ts.map +1 -1
  93. package/dist/sibling-temp.js +32 -7
  94. package/dist/sidecar-lock.d.ts +2 -0
  95. package/dist/sidecar-lock.d.ts.map +1 -1
  96. package/dist/sidecar-lock.js +93 -13
  97. package/dist/store.d.ts +1 -0
  98. package/dist/store.d.ts.map +1 -1
  99. package/dist/store.js +1 -0
  100. package/dist/temp-target.d.ts.map +1 -1
  101. package/dist/temp-target.js +7 -4
  102. package/dist/test-hooks.d.ts +6 -0
  103. package/dist/test-hooks.d.ts.map +1 -1
  104. package/dist/text-atomic.d.ts +7 -0
  105. package/dist/text-atomic.d.ts.map +1 -1
  106. package/dist/text-atomic.js +3 -2
  107. package/dist/trash.d.ts.map +1 -1
  108. package/dist/trash.js +48 -13
  109. package/docs/advanced.md +10 -0
  110. package/docs/atomic.md +33 -5
  111. package/docs/config.md +32 -3
  112. package/docs/index.md +3 -2
  113. package/docs/json-store.md +4 -0
  114. package/docs/json.md +34 -0
  115. package/docs/output.md +92 -0
  116. package/docs/sidecar-lock.md +13 -12
  117. package/docs/store.md +33 -0
  118. package/docs/temp.md +10 -2
  119. package/docs/testing.md +10 -0
  120. package/package.json +7 -2
@@ -2,12 +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 { createSyncDirectoryGuard } from "./directory-guard.js";
5
6
  import { FsSafeError } from "./errors.js";
7
+ import { pruneExpiredStoreEntries, } from "./file-store-prune.js";
6
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";
16
+ import { getFsSafeTestHooks } from "./test-hooks.js";
11
17
  function assertRelativePath(relativePath) {
12
18
  const raw = relativePath.trim();
13
19
  if (!raw) {
@@ -29,11 +35,38 @@ function assertMaxBytes(size, maxBytes) {
29
35
  }
30
36
  }
31
37
  function isNotFound(error) {
38
+ if (!error) {
39
+ return false;
40
+ }
32
41
  return error instanceof FsSafeError
33
42
  ? error.code === "not-found"
34
43
  : error.code === "ENOENT" ||
35
44
  error.code === "ENOTDIR";
36
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
+ }
37
70
  async function copyIntoRoot(params) {
38
71
  const relativePath = assertRelativePath(params.relativePath);
39
72
  const destination = resolveStorePath(params.rootDir, relativePath);
@@ -101,7 +134,7 @@ export function fileStore(options) {
101
134
  writeStream: async (relativePath, stream, writeOptions) => {
102
135
  const safeRelativePath = assertRelativePath(relativePath);
103
136
  const destination = resolveStorePath(rootDir, safeRelativePath);
104
- const limit = writeOptions?.maxBytes ?? maxBytes;
137
+ const limit = writeOptions?.maxBytes ?? maxBytes ?? (privateMode ? DEFAULT_ROOT_MAX_BYTES : undefined);
105
138
  if (privateMode) {
106
139
  const chunks = [];
107
140
  let total = 0;
@@ -143,12 +176,11 @@ export function fileStore(options) {
143
176
  },
144
177
  copyIn: async (relativePath, sourcePath, writeOptions) => privateMode
145
178
  ? await (async () => {
146
- const sourceStat = await fs.lstat(sourcePath);
147
- if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
148
- throw new FsSafeError("not-file", "source path is not a file");
149
- }
150
- assertMaxBytes(sourceStat.size, writeOptions?.maxBytes ?? maxBytes);
151
- 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);
152
184
  })()
153
185
  : await copyIntoRoot({
154
186
  rootDir,
@@ -226,37 +258,7 @@ export function fileStore(options) {
226
258
  }, jsonOptions);
227
259
  },
228
260
  pruneExpired: async (pruneOptions) => {
229
- const now = Date.now();
230
- const recursive = pruneOptions.recursive ?? false;
231
- const maxDepth = pruneOptions.maxDepth;
232
- const pruneEmptyDirs = (recursive || maxDepth !== undefined) && (pruneOptions.pruneEmptyDirs ?? false);
233
- async function pruneDir(dir, depth) {
234
- const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
235
- for (const entry of entries) {
236
- const fullPath = path.join(dir, entry.name);
237
- const stat = await fs.lstat(fullPath).catch(() => null);
238
- if (!stat || stat.isSymbolicLink()) {
239
- continue;
240
- }
241
- if (stat.isDirectory()) {
242
- const shouldDescend = maxDepth !== undefined ? depth < maxDepth : recursive;
243
- if (shouldDescend && (await pruneDir(fullPath, depth + 1))) {
244
- await fs.rmdir(fullPath).catch(() => undefined);
245
- }
246
- continue;
247
- }
248
- if (stat.isFile() && now - stat.mtimeMs > pruneOptions.ttlMs) {
249
- await fs.rm(fullPath, { force: true }).catch(() => undefined);
250
- }
251
- }
252
- if (!pruneEmptyDirs) {
253
- return false;
254
- }
255
- const remaining = await fs.readdir(dir).catch(() => null);
256
- return remaining !== null && remaining.length === 0;
257
- }
258
- await fs.mkdir(rootDir, { recursive: true, mode: dirMode });
259
- await pruneDir(rootDir, 0);
261
+ await pruneExpiredStoreEntries({ rootDir, dirMode, options: pruneOptions });
260
262
  },
261
263
  };
262
264
  }
@@ -302,13 +304,16 @@ function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
302
304
  // Best-effort on platforms that do not enforce POSIX modes.
303
305
  }
304
306
  }
307
+ const guard = createSyncDirectoryGuard(target);
308
+ assertSyncDirectoryGuard(guard);
309
+ return guard;
305
310
  }
306
311
  function writeFileSyncAtomic(params) {
307
312
  const filePath = path.resolve(params.filePath);
308
313
  assertStoreFilePath(params.rootDir, filePath);
309
314
  let parentGuard;
310
315
  if (params.privateMode) {
311
- ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
316
+ parentGuard = ensurePrivateDirectorySync(params.rootDir, path.dirname(filePath), params.dirMode);
312
317
  try {
313
318
  const stat = syncFs.lstatSync(filePath);
314
319
  if (stat.isSymbolicLink() || !stat.isFile()) {
@@ -331,6 +336,7 @@ function writeFileSyncAtomic(params) {
331
336
  const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
332
337
  let tempExists = false;
333
338
  try {
339
+ getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
334
340
  if (parentGuard) {
335
341
  assertSyncDirectoryGuard(parentGuard);
336
342
  }
@@ -393,22 +399,23 @@ export function fileStoreSync(options) {
393
399
  path: (relativePath) => resolveStorePath(rootDir, relativePath),
394
400
  readTextIfExists: (relativePath, readOptions) => {
395
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
+ }
396
411
  try {
397
- const stat = syncFs.lstatSync(targetPath);
398
- if (stat.isSymbolicLink() || !stat.isFile()) {
399
- throw new FsSafeError("not-file", "store target is not a file");
400
- }
401
- assertMaxBytes(stat.size, readOptions?.maxBytes ?? maxBytes);
402
- if (privateMode && stat.nlink > 1) {
403
- throw new FsSafeError("hardlink", "private store target must not be hardlinked");
404
- }
405
- 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;
406
416
  }
407
- catch (error) {
408
- if (isNotFound(error)) {
409
- return null;
410
- }
411
- throw error;
417
+ finally {
418
+ syncFs.closeSync(opened.fd);
412
419
  }
413
420
  },
414
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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"home-dir.d.ts","sourceRoot":"","sources":["../src/home-dir.ts"],"names":[],"mappings":"AAeA,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,GAAG,SAAS,CAGpB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,GAAG,SAAS,CAGpB;AAsCD,wBAAgB,sBAAsB,CACpC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,CAER;AAED,wBAAgB,wBAAwB,CACtC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,CAER;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAWR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAcR;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EACN;IACE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACD,MAAM,CAAC,UAAU,EACrB,OAAO,CAAC,EAAE,MAAM,MAAM,GACrB,MAAM,CAMR;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAcR"}
1
+ {"version":3,"file":"home-dir.d.ts","sourceRoot":"","sources":["../src/home-dir.ts"],"names":[],"mappings":"AAeA,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,GAAG,SAAS,CAGpB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,GAAG,SAAS,CAGpB;AAyCD,wBAAgB,sBAAsB,CACpC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,CAER;AAED,wBAAgB,wBAAwB,CACtC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,CAER;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAYR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAcR;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,SAAS,CAAC,EACN;IACE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACD,MAAM,CAAC,UAAU,EACrB,OAAO,CAAC,EAAE,MAAM,MAAM,GACrB,MAAM,CAMR;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAcR"}
package/dist/home-dir.js CHANGED
@@ -21,17 +21,21 @@ export function resolveOsHomeDir(env = process.env, homedir = os.homedir) {
21
21
  }
22
22
  function resolveRawHomeDir(env, homedir) {
23
23
  const explicitHome = normalize(env.OPENCLAW_HOME);
24
- if (explicitHome) {
25
- if (explicitHome === "~" || explicitHome.startsWith("~/") || explicitHome.startsWith("~\\")) {
26
- const fallbackHome = resolveRawOsHomeDir(env, homedir);
27
- if (fallbackHome) {
28
- return explicitHome.replace(/^~(?=$|[\\/])/, fallbackHome);
29
- }
30
- return undefined;
31
- }
24
+ if (!explicitHome) {
25
+ return resolveRawOsHomeDir(env, homedir);
26
+ }
27
+ const segments = path.normalize(explicitHome).split(path.sep);
28
+ if (segments[0] !== "~") {
32
29
  return explicitHome;
33
30
  }
34
- return resolveRawOsHomeDir(env, homedir);
31
+ // OPENCLAW_HOME starts with "~"; expand against the os home dir. Fall
32
+ // back to undefined when there is no os home to expand against rather
33
+ // than returning a raw "~"-prefixed path the caller cannot use.
34
+ const fallbackHome = resolveRawOsHomeDir(env, homedir);
35
+ if (!fallbackHome) {
36
+ return undefined;
37
+ }
38
+ return expandHomePrefix(explicitHome, { home: fallbackHome });
35
39
  }
36
40
  function resolveRawOsHomeDir(env, homedir) {
37
41
  const envHome = normalize(env.HOME);
@@ -59,7 +63,8 @@ export function resolveRequiredOsHomeDir(env = process.env, homedir = os.homedir
59
63
  return resolveOsHomeDir(env, homedir) ?? path.resolve(process.cwd());
60
64
  }
61
65
  export function expandHomePrefix(input, opts) {
62
- if (!input.startsWith("~")) {
66
+ const segments = path.normalize(input).split(path.sep);
67
+ if (segments[0] !== "~") {
63
68
  return input;
64
69
  }
65
70
  const home = normalize(opts?.home) ??
@@ -67,22 +72,22 @@ export function expandHomePrefix(input, opts) {
67
72
  if (!home) {
68
73
  return input;
69
74
  }
70
- return input.replace(/^~(?=$|[\\/])/, home);
75
+ return path.join(home, ...segments.slice(1));
71
76
  }
72
77
  export function resolveHomeRelativePath(input, opts) {
73
- const trimmed = input.trim();
74
- if (!trimmed) {
75
- return trimmed;
78
+ if (!input) {
79
+ return input;
76
80
  }
77
- if (trimmed.startsWith("~")) {
78
- const expanded = expandHomePrefix(trimmed, {
79
- home: resolveRequiredHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
80
- env: opts?.env,
81
- homedir: opts?.homedir,
82
- });
83
- return path.resolve(expanded);
81
+ const segments = path.normalize(input).split(path.sep);
82
+ if (segments[0] !== "~") {
83
+ return path.resolve(input);
84
84
  }
85
- return path.resolve(trimmed);
85
+ const expanded = expandHomePrefix(input, {
86
+ home: resolveRequiredHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
87
+ env: opts?.env,
88
+ homedir: opts?.homedir,
89
+ });
90
+ return path.resolve(expanded);
86
91
  }
87
92
  export function resolveUserPath(input, optsOrEnv, homedir) {
88
93
  const opts = optsOrEnv && ("env" in optsOrEnv || "homedir" in optsOrEnv)
@@ -91,17 +96,17 @@ export function resolveUserPath(input, optsOrEnv, homedir) {
91
96
  return resolveHomeRelativePath(input, opts);
92
97
  }
93
98
  export function resolveOsHomeRelativePath(input, opts) {
94
- const trimmed = input.trim();
95
- if (!trimmed) {
96
- return trimmed;
99
+ if (!input) {
100
+ return input;
97
101
  }
98
- if (trimmed.startsWith("~")) {
99
- const expanded = expandHomePrefix(trimmed, {
100
- home: resolveRequiredOsHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
101
- env: opts?.env,
102
- homedir: opts?.homedir,
103
- });
104
- return path.resolve(expanded);
102
+ const segments = path.normalize(input).split(path.sep);
103
+ if (segments[0] !== "~") {
104
+ return path.resolve(input);
105
105
  }
106
- return path.resolve(trimmed);
106
+ const expanded = expandHomePrefix(input, {
107
+ home: resolveRequiredOsHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
108
+ env: opts?.env,
109
+ homedir: opts?.homedir,
110
+ });
111
+ return path.resolve(expanded);
107
112
  }
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);
@@ -0,0 +1,45 @@
1
+ export type JsonDurableQueueEntryPaths = {
2
+ jsonPath: string;
3
+ deliveredPath: string;
4
+ };
5
+ export type JsonDurableQueueReadResult<T> = {
6
+ entry: T;
7
+ migrated?: boolean;
8
+ };
9
+ export type JsonDurableQueueLoadOptions<T> = {
10
+ queueDir: string;
11
+ tempPrefix: string;
12
+ read?: (entry: T, filePath: string) => Promise<JsonDurableQueueReadResult<T>>;
13
+ cleanupTmpMaxAgeMs?: number;
14
+ maxBytes?: number;
15
+ };
16
+ export declare const DEFAULT_JSON_DURABLE_QUEUE_ENTRY_MAX_BYTES: number;
17
+ export declare function unlinkBestEffort(filePath: string): Promise<void>;
18
+ export declare function jsonDurableQueueEntryExists(filePath: string): Promise<boolean>;
19
+ export declare function resolveJsonDurableQueueEntryPaths(queueDir: string, id: string): JsonDurableQueueEntryPaths;
20
+ export declare function ensureJsonDurableQueueDirs(params: {
21
+ queueDir: string;
22
+ failedDir: string;
23
+ }): Promise<void>;
24
+ export declare function writeJsonDurableQueueEntry(params: {
25
+ filePath: string;
26
+ entry: unknown;
27
+ tempPrefix: string;
28
+ }): Promise<void>;
29
+ export declare function readJsonDurableQueueEntry<T>(filePath: string, options?: {
30
+ maxBytes?: number;
31
+ }): Promise<T>;
32
+ export declare function ackJsonDurableQueueEntry(paths: JsonDurableQueueEntryPaths): Promise<void>;
33
+ export declare function loadJsonDurableQueueEntry<T>(params: {
34
+ paths: JsonDurableQueueEntryPaths;
35
+ tempPrefix: string;
36
+ read?: (entry: T, filePath: string) => Promise<JsonDurableQueueReadResult<T>>;
37
+ maxBytes?: number;
38
+ }): Promise<T | null>;
39
+ export declare function loadPendingJsonDurableQueueEntries<T>(options: JsonDurableQueueLoadOptions<T>): Promise<T[]>;
40
+ export declare function moveJsonDurableQueueEntryToFailed(params: {
41
+ queueDir: string;
42
+ failedDir: string;
43
+ id: string;
44
+ }): Promise<void>;
45
+ //# sourceMappingURL=json-durable-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-durable-queue.d.ts","sourceRoot":"","sources":["../src/json-durable-queue.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,0CAA0C,QAAmB,CAAC;AAY3E,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtE;AAED,wBAAsB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUpF;AAmBD,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,GACT,0BAA0B,CAM5B;AAED,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhB;AAED,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAiDD,wBAAsB,yBAAyB,CAAC,CAAC,EAC/C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED,wBAAsB,wBAAwB,CAAC,KAAK,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/F;AAED,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,MAAM,EAAE;IACzD,KAAK,EAAE,0BAA0B,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAwBpB;AAED,wBAAsB,kCAAkC,CAAC,CAAC,EACxD,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,CAAC,EAAE,CAAC,CAkDd;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;CACZ,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB"}