@openclaw/fs-safe 0.1.2 → 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 (116) hide show
  1. package/CHANGELOG.md +41 -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/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/pinned-write.d.ts.map +1 -1
  70. package/dist/pinned-write.js +44 -19
  71. package/dist/private-temp-workspace.d.ts.map +1 -1
  72. package/dist/private-temp-workspace.js +28 -15
  73. package/dist/regular-file.d.ts.map +1 -1
  74. package/dist/regular-file.js +52 -8
  75. package/dist/replace-directory.d.ts.map +1 -1
  76. package/dist/replace-directory.js +5 -4
  77. package/dist/replace-file.d.ts +1 -1
  78. package/dist/replace-file.d.ts.map +1 -1
  79. package/dist/replace-file.js +93 -31
  80. package/dist/root-impl.d.ts.map +1 -1
  81. package/dist/root-impl.js +57 -51
  82. package/dist/safe-path-segment.d.ts +9 -0
  83. package/dist/safe-path-segment.d.ts.map +1 -0
  84. package/dist/safe-path-segment.js +51 -0
  85. package/dist/secret-file.d.ts.map +1 -1
  86. package/dist/secret-file.js +6 -1
  87. package/dist/sibling-temp.d.ts +1 -0
  88. package/dist/sibling-temp.d.ts.map +1 -1
  89. package/dist/sibling-temp.js +32 -7
  90. package/dist/sidecar-lock.d.ts +2 -0
  91. package/dist/sidecar-lock.d.ts.map +1 -1
  92. package/dist/sidecar-lock.js +93 -13
  93. package/dist/store.d.ts +1 -0
  94. package/dist/store.d.ts.map +1 -1
  95. package/dist/store.js +1 -0
  96. package/dist/temp-target.d.ts.map +1 -1
  97. package/dist/temp-target.js +7 -4
  98. package/dist/test-hooks.d.ts +6 -0
  99. package/dist/test-hooks.d.ts.map +1 -1
  100. package/dist/text-atomic.d.ts +7 -0
  101. package/dist/text-atomic.d.ts.map +1 -1
  102. package/dist/text-atomic.js +3 -2
  103. package/dist/trash.d.ts.map +1 -1
  104. package/dist/trash.js +48 -13
  105. package/docs/advanced.md +10 -0
  106. package/docs/atomic.md +33 -5
  107. package/docs/config.md +32 -3
  108. package/docs/index.md +3 -2
  109. package/docs/json-store.md +4 -0
  110. package/docs/json.md +34 -0
  111. package/docs/output.md +92 -0
  112. package/docs/sidecar-lock.md +13 -12
  113. package/docs/store.md +33 -0
  114. package/docs/temp.md +10 -2
  115. package/docs/testing.md +10 -0
  116. package/package.json +7 -2
@@ -1 +1 @@
1
- {"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"root-impl.d.ts","sourceRoot":"","sources":["../src/root-impl.ts"],"names":[],"mappings":"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"}
package/dist/root-impl.js CHANGED
@@ -2,16 +2,20 @@ import { randomUUID } from "node:crypto";
2
2
  import { constants as fsConstants } from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { Transform } from "node:stream";
6
5
  import { pipeline } from "node:stream/promises";
6
+ import { createBoundedReadStream } from "./bounded-read-stream.js";
7
+ import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
7
8
  import { FsSafeError } from "./errors.js";
8
9
  import { sameFileIdentity } from "./file-identity.js";
10
+ import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
11
+ import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
9
12
  import { isPinnedPathHelperSpawnError, runPinnedPathHelper } from "./pinned-path.js";
10
13
  import { runPinnedCopyHelper, runPinnedWriteHelper } from "./pinned-write.js";
11
14
  import { canFallbackFromPythonError, getFsSafePythonConfig } from "./pinned-python-config.js";
12
15
  import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
13
16
  import { assertNoNulPathInput, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
14
17
  import { helperReaddir, helperStat, runPinnedHelper, } from "./pinned-helper.js";
18
+ import { pathStatFromStats } from "./path-stat.js";
15
19
  import { resolveRootPath } from "./root-path.js";
16
20
  import { assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, } from "./root-context.js";
17
21
  import { isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, } from "./root-errors.js";
@@ -433,24 +437,6 @@ function buildAtomicWriteTempPath(targetPath) {
433
437
  function rootWriteQueueKey(root, relativePath) {
434
438
  return `${root.rootReal}\0${relativePath}`;
435
439
  }
436
- function createMaxBytesTransform(maxBytes) {
437
- let bytes = 0;
438
- return new Transform({
439
- transform(chunk, _encoding, callback) {
440
- const buffer = chunk instanceof Buffer ? chunk : Buffer.from(chunk);
441
- bytes += buffer.byteLength;
442
- if (bytes > maxBytes) {
443
- callback(new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`));
444
- return;
445
- }
446
- callback(null, buffer);
447
- },
448
- });
449
- }
450
- function createBoundedReadStream(opened, maxBytes) {
451
- const stream = opened.handle.createReadStream();
452
- return maxBytes === undefined ? stream : stream.pipe(createMaxBytesTransform(maxBytes));
453
- }
454
440
  async function writeTempFileForAtomicReplace(params) {
455
441
  const tempHandle = await fs.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode);
456
442
  try {
@@ -567,7 +553,10 @@ async function openWritableFileInRoot(root, params) {
567
553
  throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
568
554
  }
569
555
  if (params.mkdir !== false) {
570
- await fs.mkdir(path.dirname(resolved), { recursive: true });
556
+ const parentGuard = await createNearestExistingDirectoryGuard(rootReal, path.dirname(resolved));
557
+ await withAsyncDirectoryGuards([parentGuard], async () => {
558
+ await fs.mkdir(path.dirname(resolved), { recursive: true });
559
+ });
571
560
  }
572
561
  let ioPath = resolved;
573
562
  try {
@@ -965,25 +954,17 @@ async function resolvePinnedRootPathInRoot(root, params) {
965
954
  };
966
955
  }
967
956
  async function removePathFallback(resolved) {
968
- await fs.rm(resolved.resolved);
957
+ const guard = await createAsyncDirectoryGuard(path.dirname(resolved.resolved));
958
+ await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", resolved.resolved);
959
+ await assertAsyncDirectoryGuard(guard);
960
+ await ((await fs.lstat(resolved.resolved)).isDirectory() ? fs.rmdir(resolved.resolved) : fs.rm(resolved.resolved));
961
+ await assertAsyncDirectoryGuard(guard).catch(() => undefined);
969
962
  }
970
963
  async function mkdirPathFallback(resolved) {
971
- await fs.mkdir(resolved.resolved, { recursive: true });
972
- }
973
- function pathStatFromStats(stat) {
974
- return {
975
- dev: Number(stat.dev),
976
- gid: Number(stat.gid),
977
- ino: Number(stat.ino),
978
- isDirectory: stat.isDirectory(),
979
- isFile: stat.isFile(),
980
- isSymbolicLink: stat.isSymbolicLink(),
981
- mode: stat.mode,
982
- mtimeMs: stat.mtimeMs,
983
- nlink: stat.nlink,
984
- size: stat.size,
985
- uid: stat.uid,
986
- };
964
+ await mkdirPathComponentsWithGuards({
965
+ rootReal: resolved.rootReal, targetPath: resolved.resolved,
966
+ beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
967
+ });
987
968
  }
988
969
  async function statPathFallback(root, relativePath) {
989
970
  const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
@@ -1080,6 +1061,11 @@ async function movePathFallback(root, params) {
1080
1061
  }
1081
1062
  }
1082
1063
  }
1064
+ const sourceParentGuard = await createAsyncDirectoryGuard(path.dirname(source.resolved));
1065
+ const targetParentGuard = await createNearestExistingDirectoryGuard(target.rootReal, path.dirname(target.resolved));
1066
+ await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("move", target.resolved);
1067
+ await assertAsyncDirectoryGuard(sourceParentGuard);
1068
+ await assertAsyncDirectoryGuard(targetParentGuard);
1083
1069
  try {
1084
1070
  await fs.rename(source.resolved, target.resolved);
1085
1071
  }
@@ -1096,6 +1082,7 @@ async function movePathFallback(root, params) {
1096
1082
  }
1097
1083
  throw error;
1098
1084
  }
1085
+ await assertAsyncDirectoryGuard(targetParentGuard).catch(() => undefined);
1099
1086
  }
1100
1087
  async function writeFileFallback(root, params) {
1101
1088
  if (params.overwrite === false) {
@@ -1111,6 +1098,7 @@ async function writeFileFallback(root, params) {
1111
1098
  const destinationPath = target.realPath;
1112
1099
  const mode = params.mode ?? (target.stat.mode & 0o777);
1113
1100
  await target.handle.close().catch(() => { });
1101
+ const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath));
1114
1102
  let tempPath = null;
1115
1103
  let unregisterTempPath = null;
1116
1104
  try {
@@ -1122,7 +1110,10 @@ async function writeFileFallback(root, params) {
1122
1110
  encoding: params.encoding,
1123
1111
  mode: mode || 0o600,
1124
1112
  });
1125
- await fs.rename(tempPath, destinationPath);
1113
+ const commitTempPath = tempPath;
1114
+ await withAsyncDirectoryGuards([destinationGuard], async () => {
1115
+ await fs.rename(commitTempPath, destinationPath);
1116
+ });
1126
1117
  tempPath = null;
1127
1118
  unregisterTempPath();
1128
1119
  unregisterTempPath = null;
@@ -1160,20 +1151,32 @@ async function writeMissingFileFallback(root, params) {
1160
1151
  if (params.mkdir !== false) {
1161
1152
  await fs.mkdir(path.dirname(resolved), { recursive: true });
1162
1153
  }
1163
- let handle = null;
1154
+ const parentGuard = await createAsyncDirectoryGuard(path.dirname(resolved));
1164
1155
  let created = false;
1165
1156
  try {
1166
- handle = await fs.open(resolved, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
1167
- created = true;
1168
- if (typeof params.data === "string") {
1169
- await handle.writeFile(params.data, params.encoding ?? "utf8");
1170
- }
1171
- else {
1172
- await handle.writeFile(params.data);
1173
- }
1174
- const writtenStat = await handle.stat();
1157
+ const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
1158
+ const handle = await fs.open(resolved, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
1159
+ created = true;
1160
+ try {
1161
+ if (typeof params.data === "string") {
1162
+ await handle.writeFile(params.data, params.encoding ?? "utf8");
1163
+ }
1164
+ else {
1165
+ await handle.writeFile(params.data);
1166
+ }
1167
+ return { handle, writtenStat: await handle.stat() };
1168
+ }
1169
+ catch (error) {
1170
+ await handle.close().catch(() => undefined);
1171
+ throw error;
1172
+ }
1173
+ }, {
1174
+ onPostGuardFailure: async ({ handle }) => {
1175
+ created = false; // Parent is untrusted now; skip outer path cleanup by name.
1176
+ await handle.close().catch(() => undefined);
1177
+ },
1178
+ });
1175
1179
  await handle.close();
1176
- handle = null;
1177
1180
  await verifyAtomicWriteResult({
1178
1181
  root,
1179
1182
  targetPath: resolved,
@@ -1190,7 +1193,6 @@ async function writeMissingFileFallback(root, params) {
1190
1193
  throw err;
1191
1194
  }
1192
1195
  finally {
1193
- await handle?.close().catch(() => undefined);
1194
1196
  if (created) {
1195
1197
  await fs.rm(resolved, { force: true }).catch(() => undefined);
1196
1198
  }
@@ -1215,6 +1217,7 @@ async function copyFileFallback(root, params, source) {
1215
1217
  const mode = params.mode ?? (target.stat.mode & 0o777);
1216
1218
  await target.handle.close().catch(() => { });
1217
1219
  targetClosedByUs = true;
1220
+ const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath));
1218
1221
  tempPath = buildAtomicWriteTempPath(destinationPath);
1219
1222
  unregisterTempPath = registerTempPathForExit(tempPath);
1220
1223
  tempHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, mode || 0o600);
@@ -1233,7 +1236,10 @@ async function copyFileFallback(root, params, source) {
1233
1236
  tempClosedByStream = true;
1234
1237
  }
1235
1238
  tempHandle = null;
1236
- await fs.rename(tempPath, destinationPath);
1239
+ const commitTempPath = tempPath;
1240
+ await withAsyncDirectoryGuards([destinationGuard], async () => {
1241
+ await fs.rename(commitTempPath, destinationPath);
1242
+ });
1237
1243
  tempPath = null;
1238
1244
  unregisterTempPath();
1239
1245
  unregisterTempPath = null;
@@ -0,0 +1,9 @@
1
+ export type SafePathSegmentOptions = {
2
+ allowDotPrefix?: boolean;
3
+ label?: string;
4
+ };
5
+ export declare function isSafePathSegment(segment: string, options?: SafePathSegmentOptions): boolean;
6
+ export declare function assertSafePathSegment(segment: string, options?: SafePathSegmentOptions): string;
7
+ export declare function sanitizeSafePathSegment(value: string, fallback: string, options?: SafePathSegmentOptions): string;
8
+ export declare function assertSafePathPrefix(prefix: string, options?: SafePathSegmentOptions): string;
9
+ //# sourceMappingURL=safe-path-segment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
@@ -0,0 +1,51 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ const SAFE_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
3
+ const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
4
+ export function isSafePathSegment(segment, options = {}) {
5
+ return (segment !== "" &&
6
+ segment !== "." &&
7
+ segment !== ".." &&
8
+ !segment.includes("/") &&
9
+ !segment.includes("\\") &&
10
+ !segment.includes("\0") &&
11
+ (options.allowDotPrefix === true || !segment.startsWith(".")) &&
12
+ (options.allowDotPrefix === true
13
+ ? SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN.test(segment)
14
+ : SAFE_PATH_SEGMENT_PATTERN.test(segment)));
15
+ }
16
+ export function assertSafePathSegment(segment, options = {}) {
17
+ // Validate the exact value callers will later join into paths; trimming here
18
+ // would let whitespace-padded ids pass and then be used verbatim.
19
+ if (!isSafePathSegment(segment, options)) {
20
+ throw new FsSafeError("invalid-path", `${options.label ?? "path segment"} must be a safe path segment`);
21
+ }
22
+ return segment;
23
+ }
24
+ export function sanitizeSafePathSegment(value, fallback, options = {}) {
25
+ const sanitized = value
26
+ .trim()
27
+ .replace(/[\\/]+/g, "-")
28
+ .replace(/\0/g, "")
29
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
30
+ .replace(/^-+|-+$/g, "");
31
+ if (isSafePathSegment(sanitized, options)) {
32
+ return sanitized;
33
+ }
34
+ return assertSafePathSegment(fallback, { ...options, label: "fallback path segment" });
35
+ }
36
+ export function assertSafePathPrefix(prefix, options = {}) {
37
+ // Prefixes are often derived from safe filenames. Normalize harmless
38
+ // filename characters first, but still reject real path-control bytes.
39
+ if (prefix.includes("/") || prefix.includes("\\") || prefix.includes("\0")) {
40
+ return assertSafePathSegment(prefix, {
41
+ allowDotPrefix: true,
42
+ ...options,
43
+ label: options.label ?? "path prefix",
44
+ });
45
+ }
46
+ return assertSafePathSegment(prefix.replace(/[^A-Za-z0-9._-]+/g, "-"), {
47
+ allowDotPrefix: true,
48
+ ...options,
49
+ label: options.label ?? "path prefix",
50
+ });
51
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAuGF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAQR;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAMpB;AAoFD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDhB"}
1
+ {"version":3,"file":"secret-file.d.ts","sourceRoot":"","sources":["../src/secret-file.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,6BAA6B,QAAY,CAAC;AACvD,eAAO,MAAM,uBAAuB,MAAQ,CAAC;AAC7C,eAAO,MAAM,wBAAwB,MAAQ,CAAC;AAE9C,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAuGF,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAQR;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,qBAA0B,GAClC,MAAM,GAAG,SAAS,CAMpB;AAoFD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyDhB"}
@@ -2,7 +2,9 @@ import { randomBytes } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import fsp from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { createAsyncDirectoryGuard } from "./directory-guard.js";
5
6
  import { FsSafeError } from "./errors.js";
7
+ import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
6
8
  import { resolveHomeRelativePath } from "./home-dir.js";
7
9
  import { openPinnedFileSync } from "./pinned-open.js";
8
10
  export const DEFAULT_SECRET_FILE_MAX_BYTES = 16 * 1024;
@@ -188,6 +190,7 @@ export async function writeSecretFileAtomic(params) {
188
190
  const resolvedRootReal = await fsp.realpath(resolvedRoot);
189
191
  const parentDir = await fsp.realpath(intendedParentDir);
190
192
  assertRealPathWithinRoot(resolvedRootReal, parentDir);
193
+ const parentGuard = await createAsyncDirectoryGuard(parentDir);
191
194
  const fileName = path.basename(resolvedFile);
192
195
  const finalFilePath = path.join(parentDir, fileName);
193
196
  try {
@@ -220,7 +223,9 @@ export async function writeSecretFileAtomic(params) {
220
223
  if (refreshedParentReal !== parentDir) {
221
224
  throw new Error(`Private secret parent directory changed during write for ${finalFilePath}.`);
222
225
  }
223
- await fsp.rename(tempPath, finalFilePath);
226
+ await withAsyncDirectoryGuards([parentGuard], async () => {
227
+ await fsp.rename(tempPath, finalFilePath);
228
+ });
224
229
  createdTemp = false;
225
230
  await enforcePrivatePathMode(finalFilePath, mode, "file");
226
231
  }
@@ -4,6 +4,7 @@ export type WriteSiblingTempFileOptions<T> = {
4
4
  resolveFinalPath: (result: T) => string;
5
5
  tempPrefix?: string;
6
6
  dirMode?: number;
7
+ chmodDir?: boolean;
7
8
  mode?: number;
8
9
  syncTempFile?: boolean;
9
10
  syncParentDir?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAuCF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAoCxC;AAeD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgChB"}
1
+ {"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AA0CF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAyCxC;AAkBD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8ChB"}
@@ -1,12 +1,20 @@
1
1
  import crypto, { randomUUID } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
5
+ import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
4
6
  import { sanitizeUntrustedFileName } from "./filename.js";
5
7
  import { root } from "./root.js";
8
+ import { assertSafePathPrefix } from "./safe-path-segment.js";
9
+ import { resolveSecureTempRoot } from "./secure-temp-dir.js";
6
10
  import { registerTempPathForExit } from "./temp-cleanup.js";
11
+ import { getFsSafeTestHooks } from "./test-hooks.js";
7
12
  import { serializePathWrite } from "./write-queue.js";
8
13
  function buildTempPath(dir, tempPrefix) {
9
- return path.join(dir, `${tempPrefix ?? ".fs-safe-stream"}.${process.pid}.${randomUUID()}.tmp`);
14
+ const safePrefix = assertSafePathPrefix(tempPrefix ?? ".fs-safe-stream", {
15
+ label: "sibling temp prefix",
16
+ });
17
+ return path.join(dir, `${safePrefix}.${process.pid}.${randomUUID()}.tmp`);
10
18
  }
11
19
  async function syncFileBestEffort(filePath) {
12
20
  const handle = await fs.open(filePath, "r+");
@@ -45,7 +53,10 @@ function assertFinalPathIsSibling(dir, filePath) {
45
53
  export async function writeSiblingTempFile(options) {
46
54
  const dir = path.resolve(options.dir);
47
55
  await fs.mkdir(dir, { recursive: true, mode: options.dirMode ?? 0o700 });
48
- await fs.chmod(dir, options.dirMode ?? 0o700).catch(() => undefined);
56
+ if (options.chmodDir !== false) {
57
+ await fs.chmod(dir, options.dirMode ?? 0o700).catch(() => undefined);
58
+ }
59
+ const dirGuard = await createAsyncDirectoryGuard(dir);
49
60
  const tempPath = buildTempPath(dir, options.tempPrefix);
50
61
  const unregisterTempPath = registerTempPathForExit(tempPath);
51
62
  let tempExists = false;
@@ -61,7 +72,9 @@ export async function writeSiblingTempFile(options) {
61
72
  const filePath = path.resolve(options.resolveFinalPath(result));
62
73
  assertFinalPathIsSibling(dir, filePath);
63
74
  await serializePathWrite(filePath, async () => {
64
- await fs.rename(tempPath, filePath);
75
+ await withAsyncDirectoryGuards([dirGuard], async () => {
76
+ await fs.rename(tempPath, filePath);
77
+ });
65
78
  tempExists = false;
66
79
  unregisterTempPath();
67
80
  if (options.mode !== undefined) {
@@ -82,8 +95,11 @@ export async function writeSiblingTempFile(options) {
82
95
  }
83
96
  function buildSiblingTempPath(params) {
84
97
  const id = crypto.randomUUID();
98
+ const safePrefix = assertSafePathPrefix(params.tempPrefix, {
99
+ label: "sibling temp prefix",
100
+ });
85
101
  const safeTail = sanitizeUntrustedFileName(path.basename(params.targetPath), params.fallbackFileName);
86
- return path.join(path.dirname(params.targetPath), `${params.tempPrefix}${id}-${safeTail}.part`);
102
+ return path.join(path.dirname(params.targetPath), `${safePrefix}${id}-${safeTail}.part`);
87
103
  }
88
104
  export async function writeViaSiblingTempPath(params) {
89
105
  const rootDir = await fs
@@ -101,19 +117,28 @@ export async function writeViaSiblingTempPath(params) {
101
117
  path.isAbsolute(relativeTargetPath)) {
102
118
  throw new Error("Target path is outside the allowed root");
103
119
  }
120
+ const rootGuard = await createAsyncDirectoryGuard(rootDir);
121
+ const tempDir = await fs.mkdtemp(path.join(resolveSecureTempRoot({
122
+ fallbackPrefix: "fs-safe-output",
123
+ unsafeFallbackLabel: "sibling temp output dir",
124
+ warn: () => undefined,
125
+ }), "fs-safe-output-"));
104
126
  const tempPath = buildSiblingTempPath({
105
- targetPath,
127
+ targetPath: path.join(tempDir, path.basename(targetPath)),
106
128
  fallbackFileName: params.fallbackFileName ?? "output.bin",
107
129
  tempPrefix: params.tempPrefix ?? ".fs-safe-output-",
108
130
  });
109
- const unregisterTempPath = registerTempPathForExit(tempPath);
131
+ const unregisterTempPath = registerTempPathForExit(tempDir, { recursive: true });
110
132
  try {
133
+ await getFsSafeTestHooks()?.beforeSiblingTempWrite?.(tempPath);
111
134
  await params.writeTemp(tempPath);
135
+ await assertAsyncDirectoryGuard(rootGuard);
112
136
  const targetRoot = await root(rootDir);
113
137
  await targetRoot.copyIn(relativeTargetPath, tempPath, { mkdir: false });
138
+ await assertAsyncDirectoryGuard(rootGuard);
114
139
  }
115
140
  finally {
116
- await fs.rm(tempPath, { force: true }).catch(() => { });
141
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => { });
117
142
  unregisterTempPath();
118
143
  }
119
144
  }
@@ -5,12 +5,14 @@ export type SidecarLockRetryOptions = {
5
5
  maxTimeout?: number;
6
6
  randomize?: boolean;
7
7
  };
8
+ export type SidecarLockStaleRecovery = "fail-closed";
8
9
  export type SidecarLockAcquireOptions<TPayload extends Record<string, unknown>> = {
9
10
  targetPath: string;
10
11
  lockPath?: string;
11
12
  staleMs: number;
12
13
  timeoutMs?: number;
13
14
  retry?: SidecarLockRetryOptions;
15
+ staleRecovery?: SidecarLockStaleRecovery;
14
16
  allowReentrant?: boolean;
15
17
  payload: () => TPayload | Promise<TPayload>;
16
18
  shouldReclaim?: (params: {
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,yBAAyB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI;IAChF,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,uBAAuB,CAAC;IAChC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,EAAE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5C,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACxC,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,iBAAiB,EAAE,OAAO,CAAC;KAC5B,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,YAAY,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,sBAAsB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CACjF,yBAAyB,CAAC,QAAQ,CAAC,EACnC,YAAY,CACb,GAAG;IACF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAuIF,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAW3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACpD,yBAAyB,CAAC,QAAQ,CAAC,KAC3C,OAAO,CAAC,iBAAiB,CAAC;eA6FL,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACxD,yBAAyB,CAAC,QAAQ,CAAC,MACxC,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAC;iBASW,OAAO,CAAC,IAAI,CAAC;iBAQnB,IAAI;uBAIE,oBAAoB,EAAE;EAW/C;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAMZ"}
1
+ {"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { sameFileIdentity } from "./file-identity.js";
4
5
  const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
5
6
  function getGlobalManagers() {
6
7
  const globalWithState = globalThis;
@@ -18,17 +19,63 @@ function resolveManagerState(key) {
18
19
  }
19
20
  return state;
20
21
  }
21
- async function readJsonPayload(lockPath) {
22
+ async function readLockSnapshot(lockPath) {
22
23
  try {
23
- const parsed = JSON.parse(await fs.readFile(lockPath, "utf8"));
24
- return parsed && typeof parsed === "object" && !Array.isArray(parsed)
25
- ? parsed
26
- : null;
24
+ const stat = await fs.lstat(lockPath);
25
+ const raw = await fs.readFile(lockPath, "utf8");
26
+ try {
27
+ const parsed = JSON.parse(raw);
28
+ const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed)
29
+ ? parsed
30
+ : null;
31
+ return { raw, payload, stat };
32
+ }
33
+ catch {
34
+ return { raw, payload: null, stat };
35
+ }
27
36
  }
28
37
  catch {
29
38
  return null;
30
39
  }
31
40
  }
41
+ function snapshotMatches(current, observed) {
42
+ if (observed.stat && current.stat && !sameFileIdentity(observed.stat, current.stat)) {
43
+ return false;
44
+ }
45
+ if (observed.raw !== undefined) {
46
+ return current.raw === observed.raw;
47
+ }
48
+ return observed.stat !== undefined && current.stat !== undefined;
49
+ }
50
+ async function removeLockIfUnchanged(lockPath, observed) {
51
+ const current = await readLockSnapshot(lockPath);
52
+ if (!current || !observed) {
53
+ return false;
54
+ }
55
+ if (!snapshotMatches(current, observed)) {
56
+ // The lock changed after we decided it was stale. Leave the fresh holder's
57
+ // file alone; deleting by path here would break mutual exclusion.
58
+ return false;
59
+ }
60
+ await fs.rm(lockPath, { force: true }).catch(() => undefined);
61
+ return true;
62
+ }
63
+ async function lockSnapshotStillPresent(lockPath, observed) {
64
+ const current = await readLockSnapshot(lockPath);
65
+ return !!current && !!observed && snapshotMatches(current, observed);
66
+ }
67
+ function snapshotMatchesSync(lockPath, observed) {
68
+ try {
69
+ const stat = fsSync.lstatSync(lockPath);
70
+ if (observed.stat && !sameFileIdentity(observed.stat, stat)) {
71
+ return false;
72
+ }
73
+ return observed.raw === undefined || fsSync.readFileSync(lockPath, "utf8") === observed.raw;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ }
32
79
  async function resolveNormalizedTargetPath(targetPath) {
33
80
  const resolved = path.resolve(targetPath);
34
81
  const dir = path.dirname(resolved);
@@ -66,7 +113,9 @@ function releaseAllLocksSync(state) {
66
113
  for (const [normalizedTargetPath, held] of state.held) {
67
114
  void held.handle.close().catch(() => undefined);
68
115
  try {
69
- fsSync.rmSync(held.lockPath, { force: true });
116
+ if (snapshotMatchesSync(held.lockPath, held.snapshot)) {
117
+ fsSync.rmSync(held.lockPath, { force: true });
118
+ }
70
119
  }
71
120
  catch {
72
121
  // Best-effort process-exit cleanup.
@@ -95,7 +144,7 @@ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
95
144
  state.held.delete(normalizedTargetPath);
96
145
  held.releasePromise = (async () => {
97
146
  await held.handle.close().catch(() => undefined);
98
- await fs.rm(held.lockPath, { force: true }).catch(() => undefined);
147
+ await removeLockIfUnchanged(held.lockPath, held.snapshot);
99
148
  })();
100
149
  try {
101
150
  await held.releasePromise;
@@ -137,15 +186,19 @@ export function createSidecarLockManager(key) {
137
186
  let handle = null;
138
187
  try {
139
188
  handle = await fs.open(lockPath, "wx");
189
+ const payload = await options.payload();
190
+ const raw = `${JSON.stringify(payload, null, 2)}\n`;
191
+ await handle.writeFile(raw, "utf8");
192
+ const snapshot = { raw, payload, stat: await handle.stat() };
140
193
  const createdHeld = {
141
194
  count: 1,
142
195
  handle,
143
196
  lockPath,
197
+ snapshot,
144
198
  acquiredAt: Date.now(),
145
199
  metadata: options.metadata ?? {},
146
200
  };
147
201
  state.held.set(normalizedTargetPath, createdHeld);
148
- await handle.writeFile(`${JSON.stringify(await options.payload(), null, 2)}\n`, "utf8");
149
202
  const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
150
203
  return {
151
204
  lockPath,
@@ -156,29 +209,56 @@ export function createSidecarLockManager(key) {
156
209
  }
157
210
  catch (err) {
158
211
  if (handle) {
212
+ const failedSnapshot = { payload: null };
213
+ try {
214
+ failedSnapshot.stat = await handle.stat();
215
+ }
216
+ catch {
217
+ // Best-effort cleanup of a failed exclusive create.
218
+ }
159
219
  const current = state.held.get(normalizedTargetPath);
160
220
  if (current?.handle === handle) {
161
221
  state.held.delete(normalizedTargetPath);
162
222
  }
163
- await handle.close().catch(() => undefined);
223
+ // If payload serialization/write fails, the file may be empty or
224
+ // partial JSON, so remove while our exclusive handle is still open.
164
225
  await fs.rm(lockPath, { force: true }).catch(() => undefined);
226
+ await handle.close().catch(() => undefined);
227
+ // Windows can refuse removing an open file; retry after close but
228
+ // only if the path still points at the file identity we created.
229
+ await removeLockIfUnchanged(lockPath, failedSnapshot);
165
230
  }
166
231
  if (err.code !== "EEXIST") {
167
232
  throw err;
168
233
  }
169
234
  const nowMs = Date.now();
170
- const payload = await readJsonPayload(lockPath);
235
+ const snapshot = await readLockSnapshot(lockPath);
236
+ if (!snapshot) {
237
+ continue;
238
+ }
171
239
  const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
172
240
  if (await shouldReclaim({
173
241
  lockPath,
174
242
  normalizedTargetPath,
175
- payload,
243
+ payload: snapshot?.payload ?? null,
176
244
  staleMs: options.staleMs,
177
245
  nowMs,
178
246
  heldByThisProcess: state.held.has(normalizedTargetPath),
179
247
  })) {
180
- await fs.rm(lockPath, { force: true }).catch(() => undefined);
181
- continue;
248
+ if (!(await lockSnapshotStillPresent(lockPath, snapshot))) {
249
+ continue;
250
+ }
251
+ // Node exposes only path-based unlink/rename here. A stale-lock
252
+ // reclaimer cannot bind the delete to the file it inspected, so a
253
+ // concurrent release+fresh-acquire could otherwise lose its lock.
254
+ // Fail closed and let callers choose a higher-level recovery path.
255
+ if ((options.staleRecovery ?? "fail-closed") === "fail-closed") {
256
+ throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
257
+ code: "file_lock_stale",
258
+ lockPath,
259
+ normalizedTargetPath,
260
+ });
261
+ }
182
262
  }
183
263
  const elapsed = Date.now() - startedAt;
184
264
  if ((options.timeoutMs !== undefined &&
package/dist/store.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { fileStore, fileStoreSync, type FileStore, type FileStoreOptions, type FileStorePruneOptions, type FileStoreReadOptions, type FileStoreSync, type FileStoreWriteOptions, } from "./file-store.js";
2
2
  export { jsonStore, type JsonStore, type JsonFileStoreOptions, type JsonStoreLockOptions, type JsonStoreOptions, } from "./json-store.js";
3
+ export { ackJsonDurableQueueEntry, ensureJsonDurableQueueDirs, jsonDurableQueueEntryExists, loadJsonDurableQueueEntry, loadPendingJsonDurableQueueEntries, moveJsonDurableQueueEntryToFailed, readJsonDurableQueueEntry, resolveJsonDurableQueueEntryPaths, unlinkBestEffort, writeJsonDurableQueueEntry, type JsonDurableQueueEntryPaths, type JsonDurableQueueLoadOptions, type JsonDurableQueueReadResult, } from "./json-durable-queue.js";
3
4
  //# sourceMappingURL=store.d.ts.map