@openclaw/fs-safe 0.4.7 → 0.5.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 (247) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +82 -43
  3. package/dist/archive-entry.d.ts.map +1 -1
  4. package/dist/archive-entry.js +5 -4
  5. package/dist/archive-errors.d.ts +11 -0
  6. package/dist/archive-errors.d.ts.map +1 -0
  7. package/dist/archive-errors.js +16 -0
  8. package/dist/archive-input.d.ts +12 -0
  9. package/dist/archive-input.d.ts.map +1 -0
  10. package/dist/archive-input.js +72 -0
  11. package/dist/archive-kind.d.ts +1 -1
  12. package/dist/archive-kind.d.ts.map +1 -1
  13. package/dist/archive-kind.js +18 -0
  14. package/dist/archive-limits.d.ts +10 -0
  15. package/dist/archive-limits.d.ts.map +1 -1
  16. package/dist/archive-limits.js +18 -0
  17. package/dist/archive-native.d.ts +18 -0
  18. package/dist/archive-native.d.ts.map +1 -0
  19. package/dist/archive-native.js +115 -0
  20. package/dist/archive-options.d.ts +21 -0
  21. package/dist/archive-options.d.ts.map +1 -0
  22. package/dist/archive-options.js +1 -0
  23. package/dist/archive-policy.d.ts +20 -0
  24. package/dist/archive-policy.d.ts.map +1 -0
  25. package/dist/archive-policy.js +29 -0
  26. package/dist/archive-read.d.ts +6 -0
  27. package/dist/archive-read.d.ts.map +1 -0
  28. package/dist/archive-read.js +200 -0
  29. package/dist/archive-staging.d.ts +2 -5
  30. package/dist/archive-staging.d.ts.map +1 -1
  31. package/dist/archive-staging.js +19 -10
  32. package/dist/archive-tar-meta.d.ts +6 -0
  33. package/dist/archive-tar-meta.d.ts.map +1 -0
  34. package/dist/archive-tar-meta.js +143 -0
  35. package/dist/archive-tar-runtime.d.ts +43 -0
  36. package/dist/archive-tar-runtime.d.ts.map +1 -0
  37. package/dist/archive-tar-runtime.js +8 -0
  38. package/dist/archive-tar.d.ts +5 -1
  39. package/dist/archive-tar.d.ts.map +1 -1
  40. package/dist/archive-tar.js +24 -6
  41. package/dist/archive-zip-entry.d.ts +15 -0
  42. package/dist/archive-zip-entry.d.ts.map +1 -0
  43. package/dist/archive-zip-entry.js +17 -0
  44. package/dist/archive.d.ts +7 -17
  45. package/dist/archive.d.ts.map +1 -1
  46. package/dist/archive.js +111 -108
  47. package/dist/atomic.d.ts +1 -1
  48. package/dist/atomic.d.ts.map +1 -1
  49. package/dist/config.d.ts +1 -1
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +1 -1
  52. package/dist/containment.d.ts +2 -0
  53. package/dist/containment.d.ts.map +1 -0
  54. package/dist/containment.js +1 -0
  55. package/dist/device-path.d.ts.map +1 -1
  56. package/dist/device-path.js +24 -2
  57. package/dist/durability.d.ts +2 -0
  58. package/dist/durability.d.ts.map +1 -1
  59. package/dist/durability.js +2 -0
  60. package/dist/errors.d.ts +4 -1
  61. package/dist/errors.d.ts.map +1 -1
  62. package/dist/errors.js +2 -0
  63. package/dist/file-hash.d.ts +10 -0
  64. package/dist/file-hash.d.ts.map +1 -0
  65. package/dist/file-hash.js +73 -0
  66. package/dist/file-lock-sync.d.ts +35 -0
  67. package/dist/file-lock-sync.d.ts.map +1 -0
  68. package/dist/file-lock-sync.js +233 -0
  69. package/dist/file-lock.d.ts +2 -0
  70. package/dist/file-lock.d.ts.map +1 -1
  71. package/dist/file-lock.js +1 -0
  72. package/dist/filename.d.ts.map +1 -1
  73. package/dist/filename.js +4 -1
  74. package/dist/guarded-mkdir.d.ts +7 -1
  75. package/dist/guarded-mkdir.d.ts.map +1 -1
  76. package/dist/guarded-mkdir.js +45 -3
  77. package/dist/index.d.ts +4 -3
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/json-document-store.d.ts.map +1 -1
  81. package/dist/json-document-store.js +30 -15
  82. package/dist/move-path.d.ts.map +1 -1
  83. package/dist/move-path.js +101 -17
  84. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  85. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  86. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  87. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  88. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  89. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  90. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  91. package/dist/native-binding.d.ts +79 -0
  92. package/dist/native-binding.d.ts.map +1 -0
  93. package/dist/native-binding.js +1 -0
  94. package/dist/native-config.d.ts +17 -0
  95. package/dist/native-config.d.ts.map +1 -0
  96. package/dist/native-config.js +71 -0
  97. package/dist/native-operations.d.ts +21 -0
  98. package/dist/native-operations.d.ts.map +1 -0
  99. package/dist/native-operations.js +115 -0
  100. package/dist/native-pinned-write.d.ts +5 -0
  101. package/dist/native-pinned-write.d.ts.map +1 -0
  102. package/dist/native-pinned-write.js +115 -0
  103. package/dist/native.d.ts +14 -0
  104. package/dist/native.d.ts.map +1 -0
  105. package/dist/native.js +204 -0
  106. package/dist/output-sibling.d.ts +8 -0
  107. package/dist/output-sibling.d.ts.map +1 -0
  108. package/dist/output-sibling.js +122 -0
  109. package/dist/output.d.ts +2 -0
  110. package/dist/output.d.ts.map +1 -1
  111. package/dist/output.js +32 -5
  112. package/dist/owner-dacl.d.ts +31 -0
  113. package/dist/owner-dacl.d.ts.map +1 -0
  114. package/dist/owner-dacl.js +27 -0
  115. package/dist/path-policy.js +1 -1
  116. package/dist/path.d.ts.map +1 -1
  117. package/dist/path.js +3 -2
  118. package/dist/permissions-public.d.ts +2 -0
  119. package/dist/permissions-public.d.ts.map +1 -1
  120. package/dist/permissions-public.js +2 -0
  121. package/dist/permissions.d.ts.map +1 -1
  122. package/dist/permissions.js +6 -0
  123. package/dist/pinned-operation.d.ts +2 -0
  124. package/dist/pinned-operation.d.ts.map +1 -0
  125. package/dist/pinned-operation.js +35 -0
  126. package/dist/pinned-write.d.ts +3 -16
  127. package/dist/pinned-write.d.ts.map +1 -1
  128. package/dist/pinned-write.js +20 -85
  129. package/dist/private-directory.d.ts +5 -0
  130. package/dist/private-directory.d.ts.map +1 -0
  131. package/dist/private-directory.js +13 -0
  132. package/dist/private-temp-workspace.d.ts +6 -2
  133. package/dist/private-temp-workspace.d.ts.map +1 -1
  134. package/dist/private-temp-workspace.js +40 -13
  135. package/dist/publish-file-failure.d.ts +27 -0
  136. package/dist/publish-file-failure.d.ts.map +1 -0
  137. package/dist/publish-file-failure.js +21 -0
  138. package/dist/publish-file.d.ts +21 -0
  139. package/dist/publish-file.d.ts.map +1 -0
  140. package/dist/publish-file.js +351 -0
  141. package/dist/read-opened-file.d.ts +3 -0
  142. package/dist/read-opened-file.d.ts.map +1 -1
  143. package/dist/read-opened-file.js +1 -0
  144. package/dist/replace-file-copy-fallback.d.ts +34 -0
  145. package/dist/replace-file-copy-fallback.d.ts.map +1 -0
  146. package/dist/replace-file-copy-fallback.js +339 -0
  147. package/dist/replace-file.d.ts +6 -2
  148. package/dist/replace-file.d.ts.map +1 -1
  149. package/dist/replace-file.js +39 -90
  150. package/dist/root-context.d.ts +4 -1
  151. package/dist/root-context.d.ts.map +1 -1
  152. package/dist/root-context.js +20 -1
  153. package/dist/root-impl.d.ts +5 -0
  154. package/dist/root-impl.d.ts.map +1 -1
  155. package/dist/root-impl.js +128 -225
  156. package/dist/root-path-existing.d.ts +3 -0
  157. package/dist/root-path-existing.d.ts.map +1 -0
  158. package/dist/root-path-existing.js +69 -0
  159. package/dist/root-path.d.ts +1 -2
  160. package/dist/root-path.d.ts.map +1 -1
  161. package/dist/root-path.js +67 -81
  162. package/dist/root-walk.d.ts +41 -0
  163. package/dist/root-walk.d.ts.map +1 -0
  164. package/dist/root-walk.js +114 -0
  165. package/dist/root.d.ts +2 -0
  166. package/dist/root.d.ts.map +1 -1
  167. package/dist/safe-path-segment.d.ts.map +1 -1
  168. package/dist/safe-path-segment.js +16 -4
  169. package/dist/secret-file.d.ts +5 -2
  170. package/dist/secret-file.d.ts.map +1 -1
  171. package/dist/secret-file.js +20 -2
  172. package/dist/secret-read-async.d.ts +4 -0
  173. package/dist/secret-read-async.d.ts.map +1 -0
  174. package/dist/secret-read-async.js +95 -0
  175. package/dist/secret.d.ts +2 -1
  176. package/dist/secret.d.ts.map +1 -1
  177. package/dist/secret.js +2 -1
  178. package/dist/sibling-temp.d.ts.map +1 -1
  179. package/dist/sibling-temp.js +1 -0
  180. package/dist/sidecar-lock-handle.d.ts +20 -0
  181. package/dist/sidecar-lock-handle.d.ts.map +1 -0
  182. package/dist/sidecar-lock-handle.js +28 -0
  183. package/dist/sidecar-lock-policy.d.ts +10 -0
  184. package/dist/sidecar-lock-policy.d.ts.map +1 -0
  185. package/dist/sidecar-lock-policy.js +29 -0
  186. package/dist/sidecar-lock-reclaim.d.ts +22 -6
  187. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  188. package/dist/sidecar-lock-reclaim.js +106 -20
  189. package/dist/sidecar-lock-types.d.ts +56 -0
  190. package/dist/sidecar-lock-types.d.ts.map +1 -0
  191. package/dist/sidecar-lock-types.js +1 -0
  192. package/dist/sidecar-lock.d.ts +2 -45
  193. package/dist/sidecar-lock.d.ts.map +1 -1
  194. package/dist/sidecar-lock.js +94 -55
  195. package/dist/temp-cleanup.d.ts +7 -1
  196. package/dist/temp-cleanup.d.ts.map +1 -1
  197. package/dist/temp-cleanup.js +35 -5
  198. package/dist/temp-target.d.ts.map +1 -1
  199. package/dist/temp-target.js +51 -3
  200. package/dist/temp.d.ts +2 -1
  201. package/dist/temp.d.ts.map +1 -1
  202. package/dist/test-hooks.d.ts +3 -0
  203. package/dist/test-hooks.d.ts.map +1 -1
  204. package/dist/windows-permissions-native.d.ts +9 -0
  205. package/dist/windows-permissions-native.d.ts.map +1 -0
  206. package/dist/windows-permissions-native.js +31 -0
  207. package/docs/archive.md +131 -13
  208. package/docs/atomic.md +30 -1
  209. package/docs/config.md +51 -21
  210. package/docs/durability.md +211 -1
  211. package/docs/errors.md +36 -6
  212. package/docs/filename.md +0 -0
  213. package/docs/index.md +11 -8
  214. package/docs/install.md +27 -26
  215. package/docs/json-store.md +18 -3
  216. package/docs/migrating-to-0.5.md +196 -0
  217. package/docs/native-helper.md +85 -0
  218. package/docs/native.md +134 -0
  219. package/docs/output.md +37 -11
  220. package/docs/path.md +1 -1
  221. package/docs/permissions.md +84 -1
  222. package/docs/quickstart.md +5 -2
  223. package/docs/reading.md +4 -4
  224. package/docs/root.md +31 -15
  225. package/docs/secret-file.md +50 -1
  226. package/docs/security-model.md +29 -8
  227. package/docs/sidecar-lock.md +92 -7
  228. package/docs/temp.md +24 -1
  229. package/docs/test-hooks.md +22 -2
  230. package/docs/testing.md +18 -7
  231. package/docs/types.md +3 -1
  232. package/docs/walk.md +55 -0
  233. package/docs/writing.md +3 -3
  234. package/package.json +10 -4
  235. package/dist/pinned-helper.d.ts +0 -9
  236. package/dist/pinned-helper.d.ts.map +0 -1
  237. package/dist/pinned-helper.js +0 -19
  238. package/dist/pinned-path.d.ts +0 -7
  239. package/dist/pinned-path.d.ts.map +0 -1
  240. package/dist/pinned-path.js +0 -21
  241. package/dist/pinned-python-config.d.ts +0 -9
  242. package/dist/pinned-python-config.d.ts.map +0 -1
  243. package/dist/pinned-python-config.js +0 -38
  244. package/dist/pinned-python.d.ts +0 -12
  245. package/dist/pinned-python.d.ts.map +0 -1
  246. package/dist/pinned-python.js +0 -688
  247. package/docs/python-helper.md +0 -103
@@ -0,0 +1,95 @@
1
+ import fsSync from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import { readFileHandleBounded } from "./bounded-read.js";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { sameFileIdentity } from "./file-identity.js";
6
+ import { resolveHomeRelativePath } from "./home-dir.js";
7
+ import { DEFAULT_SECRET_FILE_MAX_BYTES, } from "./secret-file.js";
8
+ function pathErrorCode(error) {
9
+ const code = error.code;
10
+ return code === "ENOENT" || code === "ENOTDIR" ? "not-found" : "invalid-path";
11
+ }
12
+ async function readSecretFileOutcome(filePath, label, options) {
13
+ const resolvedPath = resolveHomeRelativePath(filePath.trim());
14
+ if (!resolvedPath) {
15
+ return { ok: false, code: "invalid-path", message: `${label} file path is empty.` };
16
+ }
17
+ const maxBytes = options.maxBytes ?? DEFAULT_SECRET_FILE_MAX_BYTES;
18
+ let previewStat;
19
+ try {
20
+ previewStat = await fs.lstat(resolvedPath);
21
+ if (previewStat.isSymbolicLink()) {
22
+ if (options.rejectSymlink) {
23
+ return { ok: false, code: "symlink", message: `${label} file at ${resolvedPath} must not be a symlink.` };
24
+ }
25
+ previewStat = await fs.stat(resolvedPath);
26
+ }
27
+ }
28
+ catch (error) {
29
+ const normalized = error instanceof Error ? error : new Error(String(error));
30
+ return {
31
+ ok: false,
32
+ code: pathErrorCode(error),
33
+ error: normalized,
34
+ message: `Failed to inspect ${label} file at ${resolvedPath}: ${String(normalized)}`,
35
+ };
36
+ }
37
+ if (!previewStat.isFile()) {
38
+ return { ok: false, code: "not-file", message: `${label} file at ${resolvedPath} must be a regular file.` };
39
+ }
40
+ if (options.rejectHardlinks !== false && previewStat.nlink > 1) {
41
+ return { ok: false, code: "hardlink", message: `${label} file at ${resolvedPath} must not be hardlinked.` };
42
+ }
43
+ if (previewStat.size > maxBytes) {
44
+ return { ok: false, code: "too-large", message: `${label} file at ${resolvedPath} exceeds ${maxBytes} bytes.` };
45
+ }
46
+ let handle;
47
+ try {
48
+ const realPath = await fs.realpath(resolvedPath);
49
+ const noFollow = process.platform !== "win32" && "O_NOFOLLOW" in fsSync.constants
50
+ ? fsSync.constants.O_NOFOLLOW
51
+ : 0;
52
+ handle = await fs.open(realPath, fsSync.constants.O_RDONLY | noFollow);
53
+ const openedStat = await handle.stat();
54
+ const pathStat = await fs.lstat(realPath);
55
+ if (!openedStat.isFile() ||
56
+ !pathStat.isFile() ||
57
+ !sameFileIdentity(previewStat, openedStat) ||
58
+ !sameFileIdentity(pathStat, openedStat) ||
59
+ (options.rejectHardlinks !== false && openedStat.nlink > 1)) {
60
+ throw new FsSafeError("path-mismatch", "security validation failed");
61
+ }
62
+ const secret = (await readFileHandleBounded(handle, maxBytes)).toString("utf8").trim();
63
+ return secret
64
+ ? { ok: true, secret }
65
+ : { ok: false, code: "invalid-path", message: `${label} file at ${resolvedPath} is empty.` };
66
+ }
67
+ catch (error) {
68
+ const normalized = error instanceof Error ? error : new Error(String(error));
69
+ return {
70
+ ok: false,
71
+ code: error instanceof FsSafeError ? error.code : pathErrorCode(error) === "not-found" ? "not-found" : "path-mismatch",
72
+ error: normalized,
73
+ message: `Failed to read ${label} file at ${resolvedPath}: ${String(normalized)}`,
74
+ };
75
+ }
76
+ finally {
77
+ await handle?.close().catch(() => undefined);
78
+ }
79
+ }
80
+ export async function readSecretFile(filePath, label, options = {}) {
81
+ const result = await readSecretFileOutcome(filePath, label, options);
82
+ if (result.ok)
83
+ return result.secret;
84
+ throw new FsSafeError(result.code, result.message, { cause: result.error });
85
+ }
86
+ export async function tryReadSecretFile(filePath, label, options = {}) {
87
+ if (!filePath?.trim())
88
+ return undefined;
89
+ const result = await readSecretFileOutcome(filePath, label, options);
90
+ if (result.ok)
91
+ return result.secret;
92
+ if (result.code === "not-found")
93
+ return undefined;
94
+ throw new FsSafeError(result.code, result.message, { cause: result.error });
95
+ }
package/dist/secret.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export { DEFAULT_SECRET_FILE_MAX_BYTES, PRIVATE_SECRET_DIR_MODE, PRIVATE_SECRET_FILE_MODE, readSecretFileSync, tryReadSecretFileSync, writeSecretFileAtomic, type SecretFileReadOptions, } from "./secret-file.js";
1
+ export { createSecretFileAtomic, DEFAULT_SECRET_FILE_MAX_BYTES, PRIVATE_SECRET_DIR_MODE, PRIVATE_SECRET_FILE_MODE, readSecretFileSync, tryReadSecretFileSync, writeSecretFileAtomic, type SecretFileReadOptions, } from "./secret-file.js";
2
+ export { readSecretFile, tryReadSecretFile } from "./secret-read-async.js";
2
3
  //# sourceMappingURL=secret.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../src/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,6BAA6B,EAC7B,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"secret.d.ts","sourceRoot":"","sources":["../src/secret.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,sBAAsB,EACtB,6BAA6B,EAC7B,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/secret.js CHANGED
@@ -1 +1,2 @@
1
- export { DEFAULT_SECRET_FILE_MAX_BYTES, PRIVATE_SECRET_DIR_MODE, PRIVATE_SECRET_FILE_MODE, readSecretFileSync, tryReadSecretFileSync, writeSecretFileAtomic, } from "./secret-file.js";
1
+ export { createSecretFileAtomic, DEFAULT_SECRET_FILE_MAX_BYTES, PRIVATE_SECRET_DIR_MODE, PRIVATE_SECRET_FILE_MODE, readSecretFileSync, tryReadSecretFileSync, writeSecretFileAtomic, } from "./secret-file.js";
2
+ export { readSecretFile, tryReadSecretFile } from "./secret-read-async.js";
@@ -1 +1 @@
1
- {"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAcA,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;AA8BF,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
+ {"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAcA,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;AA8BF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CA0CxC;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"}
@@ -51,6 +51,7 @@ export async function writeSiblingTempFile(options) {
51
51
  try {
52
52
  tempExists = true;
53
53
  const result = await options.writeTemp(tempPath);
54
+ unregisterTempPath.setIdentity(await fs.lstat(tempPath));
54
55
  if (options.mode !== undefined) {
55
56
  await fs.chmod(tempPath, options.mode).catch(() => undefined);
56
57
  }
@@ -0,0 +1,20 @@
1
+ import type { Root } from "./root-impl.js";
2
+ import { type SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
3
+ import type { SidecarLockHandle } from "./sidecar-lock-types.js";
4
+ export declare function createSidecarLockHandle(params: {
5
+ lockPath: string;
6
+ normalizedTargetPath: string;
7
+ verifyStillHeld: () => Promise<boolean>;
8
+ release: () => Promise<unknown>;
9
+ }): SidecarLockHandle;
10
+ export declare function createHeldSidecarLockHandle(params: {
11
+ normalizedTargetPath: string;
12
+ held: {
13
+ lockPath: string;
14
+ snapshot: SidecarLockSnapshot;
15
+ lockRoot?: Root;
16
+ parsePayload?: (raw: string) => unknown;
17
+ };
18
+ release: () => Promise<unknown>;
19
+ }): SidecarLockHandle;
20
+ //# sourceMappingURL=sidecar-lock-handle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar-lock-handle.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC,GAAG,iBAAiB,CAcpB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,oBAAoB,EAAE,MAAM,CAAC;IAC7B,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,mBAAmB,CAAC;QAC9B,QAAQ,CAAC,EAAE,IAAI,CAAC;QAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;KACzC,CAAC;IACF,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC,GAAG,iBAAiB,CAWpB"}
@@ -0,0 +1,28 @@
1
+ import { sidecarLockSnapshotStillPresent, } from "./sidecar-lock-reclaim.js";
2
+ export function createSidecarLockHandle(params) {
3
+ let released = false;
4
+ const release = async () => {
5
+ if (released)
6
+ return;
7
+ released = true;
8
+ await params.release();
9
+ };
10
+ return {
11
+ lockPath: params.lockPath,
12
+ normalizedTargetPath: params.normalizedTargetPath,
13
+ verifyStillHeld: params.verifyStillHeld,
14
+ release,
15
+ [Symbol.asyncDispose]: release,
16
+ };
17
+ }
18
+ export function createHeldSidecarLockHandle(params) {
19
+ return createSidecarLockHandle({
20
+ lockPath: params.held.lockPath,
21
+ normalizedTargetPath: params.normalizedTargetPath,
22
+ verifyStillHeld: async () => await sidecarLockSnapshotStillPresent(params.held.lockPath, params.held.snapshot, {
23
+ lockRoot: params.held.lockRoot,
24
+ parsePayload: params.held.parsePayload,
25
+ }),
26
+ release: params.release,
27
+ });
28
+ }
@@ -0,0 +1,10 @@
1
+ import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
2
+ export declare function computeSidecarLockDelayMs(retry: SidecarLockRetryOptions, attempt: number): number;
3
+ export declare function sidecarLockPayloadIsStale(payload: unknown, staleMs: number, nowMs: number): boolean;
4
+ export declare function defaultSidecarLockShouldReclaim(params: {
5
+ lockPath: string;
6
+ payload: unknown;
7
+ staleMs: number;
8
+ nowMs: number;
9
+ }): Promise<boolean>;
10
+ //# sourceMappingURL=sidecar-lock-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAEvE,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOjG;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAUT;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnB"}
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs/promises";
2
+ export function computeSidecarLockDelayMs(retry, attempt) {
3
+ const minTimeout = retry.minTimeout ?? 50;
4
+ const maxTimeout = retry.maxTimeout ?? 1000;
5
+ const factor = retry.factor ?? 1;
6
+ const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
7
+ const jitter = retry.randomize ? 1 + Math.random() : 1;
8
+ return Math.min(maxTimeout, Math.round(base * jitter));
9
+ }
10
+ export function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
11
+ const createdAt = payload &&
12
+ typeof payload === "object" &&
13
+ "createdAt" in payload &&
14
+ typeof payload.createdAt === "string"
15
+ ? payload.createdAt
16
+ : "";
17
+ const createdAtMs = Date.parse(createdAt);
18
+ return Number.isFinite(createdAtMs) && nowMs - createdAtMs > staleMs;
19
+ }
20
+ export async function defaultSidecarLockShouldReclaim(params) {
21
+ if (sidecarLockPayloadIsStale(params.payload, params.staleMs, params.nowMs))
22
+ return true;
23
+ try {
24
+ return params.nowMs - (await fs.stat(params.lockPath)).mtimeMs > params.staleMs;
25
+ }
26
+ catch {
27
+ return true;
28
+ }
29
+ }
@@ -1,13 +1,14 @@
1
- import type { Stats } from "node:fs";
1
+ import { type Stats } from "node:fs";
2
+ import type { Root } from "./root-impl.js";
2
3
  export type SidecarLockStaleSnapshot = {
3
4
  lockPath: string;
4
5
  normalizedTargetPath: string;
5
6
  raw: string;
6
- payload: Record<string, unknown> | null;
7
+ payload: unknown;
7
8
  };
8
9
  export type SidecarLockSnapshot = {
9
10
  raw?: string;
10
- payload: Record<string, unknown> | null;
11
+ payload: unknown;
11
12
  stat?: Stats;
12
13
  ownershipToken?: string;
13
14
  };
@@ -16,10 +17,23 @@ export declare function serializeSidecarLockPayload(payload: Record<string, unkn
16
17
  raw: string;
17
18
  ownershipToken: string;
18
19
  };
19
- export declare function readSidecarLockSnapshot(lockPath: string): Promise<SidecarLockSnapshot | null>;
20
+ export declare function relativeSidecarLockPath(lockRoot: Root, lockPath: string): string;
21
+ export declare function parseSidecarLockPayload(raw: string, parser?: (raw: string) => unknown): unknown;
22
+ export declare function readSidecarLockSnapshot(lockPath: string, options?: {
23
+ lockRoot?: Root;
24
+ parsePayload?: (raw: string) => unknown;
25
+ }): Promise<SidecarLockSnapshot | null>;
26
+ export declare function readSidecarLockSnapshotSync(lockPath: string, parsePayload?: (raw: string) => unknown): SidecarLockSnapshot | null;
27
+ export declare function removeSidecarLockIfUnchangedSync(lockPath: string, observed: SidecarLockSnapshot): boolean;
20
28
  export declare function sidecarLockSnapshotMatches(current: SidecarLockSnapshot, observed: SidecarLockSnapshot): boolean;
21
- export declare function removeSidecarLockIfUnchanged(lockPath: string, observed: SidecarLockSnapshot | null): Promise<boolean>;
22
- export declare function sidecarLockSnapshotStillPresent(lockPath: string, observed: SidecarLockSnapshot | null): Promise<boolean>;
29
+ export declare function removeSidecarLockIfUnchanged(lockPath: string, observed: SidecarLockSnapshot | null, options?: {
30
+ lockRoot?: Root;
31
+ parsePayload?: (raw: string) => unknown;
32
+ }): Promise<boolean>;
33
+ export declare function sidecarLockSnapshotStillPresent(lockPath: string, observed: SidecarLockSnapshot | null, options?: {
34
+ lockRoot?: Root;
35
+ parsePayload?: (raw: string) => unknown;
36
+ }): Promise<boolean>;
23
37
  export declare function sidecarReclaimGuardExists(pathname: string): Promise<boolean>;
24
38
  export declare function tryAcquireSidecarReclaimGuard(reclaimGuards: Set<string>, reclaimGuardPath: string): Promise<boolean>;
25
39
  export declare function releaseSidecarReclaimGuard(reclaimGuards: Set<string>, reclaimGuardPath: string): Promise<void>;
@@ -28,5 +42,7 @@ export declare function removeStaleSidecarLockIfAllowed(params: {
28
42
  normalizedTargetPath: string;
29
43
  snapshot: SidecarLockSnapshot;
30
44
  shouldRemoveStaleLock?: (snapshot: SidecarLockStaleSnapshot) => boolean | Promise<boolean>;
45
+ lockRoot?: Root;
46
+ parsePayload?: (raw: string) => unknown;
31
47
  }): Promise<"removed" | "changed" | "not-approved">;
32
48
  //# sourceMappingURL=sidecar-lock-reclaim.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAWrC,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxC,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAYF,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE7E;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;CACxB,CAMA;AAED,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAoBrC;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAkBT;AAED,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,mBAAmB,GAAG,IAAI,GACnC,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,mBAAmB,GAAG,IAAI,GACnC,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUlF;AAED,wBAAsB,6BAA6B,CACjD,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,EAC1B,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,OAAO,CAAC,CAWlB;AAED,wBAAsB,0BAA0B,CAC9C,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,EAC1B,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5F,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,GAAG,cAAc,CAAC,CA6BlD"}
1
+ {"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,11 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import fsSync, {} from "node:fs";
2
3
  import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { readFileHandleBounded } from "./bounded-read.js";
6
+ import { FsSafeError } from "./errors.js";
3
7
  import { sameFileIdentity } from "./file-identity.js";
8
+ const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
4
9
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
5
10
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
6
11
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX = "\t".repeat(8);
@@ -24,28 +29,98 @@ export function serializeSidecarLockPayload(payload) {
24
29
  ownershipToken,
25
30
  };
26
31
  }
27
- export async function readSidecarLockSnapshot(lockPath) {
32
+ export function relativeSidecarLockPath(lockRoot, lockPath) {
33
+ const resolved = path.resolve(lockPath);
34
+ const lexicalRelative = path.relative(lockRoot.rootDir, resolved);
35
+ const relative = lexicalRelative !== ".." &&
36
+ !lexicalRelative.startsWith(`..${path.sep}`) &&
37
+ !path.isAbsolute(lexicalRelative)
38
+ ? lexicalRelative
39
+ : path.relative(lockRoot.rootReal, resolved);
40
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
41
+ throw new FsSafeError("outside-workspace", "sidecar lock path is outside lockRoot");
42
+ }
43
+ return relative.split(path.sep).join(path.posix.sep);
44
+ }
45
+ export function parseSidecarLockPayload(raw, parser) {
46
+ if (parser) {
47
+ return parser(raw);
48
+ }
49
+ try {
50
+ const parsed = JSON.parse(raw);
51
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ export async function readSidecarLockSnapshot(lockPath, options = {}) {
28
58
  try {
59
+ if (options.lockRoot) {
60
+ const opened = await options.lockRoot.open(relativeSidecarLockPath(options.lockRoot, lockPath));
61
+ try {
62
+ const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
63
+ return {
64
+ raw,
65
+ payload: parseSidecarLockPayload(raw, options.parsePayload),
66
+ stat: opened.stat,
67
+ };
68
+ }
69
+ finally {
70
+ await opened.handle.close().catch(() => undefined);
71
+ }
72
+ }
29
73
  const stat = await fs.lstat(lockPath);
30
74
  const raw = await fs.readFile(lockPath, "utf8");
31
- try {
32
- const parsed = JSON.parse(raw);
33
- const payload = parsed && typeof parsed === "object" && !Array.isArray(parsed)
34
- ? parsed
35
- : null;
36
- return { raw, payload, stat };
37
- }
38
- catch {
39
- return { raw, payload: null, stat };
40
- }
75
+ return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat };
41
76
  }
42
77
  catch (err) {
43
- if (err.code === "ENOENT") {
78
+ if (err.code === "ENOENT" ||
79
+ (err instanceof FsSafeError && err.code === "not-found")) {
44
80
  return null;
45
81
  }
46
82
  throw err;
47
83
  }
48
84
  }
85
+ export function readSidecarLockSnapshotSync(lockPath, parsePayload) {
86
+ let fd;
87
+ try {
88
+ const before = fsSync.lstatSync(lockPath);
89
+ if (!before.isFile() || before.isSymbolicLink())
90
+ return null;
91
+ const noFollow = process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
92
+ ? fsSync.constants.O_NOFOLLOW
93
+ : 0;
94
+ fd = fsSync.openSync(lockPath, fsSync.constants.O_RDONLY | noFollow);
95
+ const opened = fsSync.fstatSync(fd);
96
+ const raw = fsSync.readFileSync(fd, "utf8");
97
+ const after = fsSync.lstatSync(lockPath);
98
+ if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
99
+ return null;
100
+ return {
101
+ raw,
102
+ payload: parseSidecarLockPayload(raw, parsePayload),
103
+ stat: after,
104
+ ownershipToken: readSidecarLockOwnershipToken(raw),
105
+ };
106
+ }
107
+ catch (error) {
108
+ if (error.code === "ENOENT")
109
+ return null;
110
+ throw error;
111
+ }
112
+ finally {
113
+ if (fd !== undefined)
114
+ fsSync.closeSync(fd);
115
+ }
116
+ }
117
+ export function removeSidecarLockIfUnchangedSync(lockPath, observed) {
118
+ const current = readSidecarLockSnapshotSync(lockPath);
119
+ if (!current || !sidecarLockSnapshotMatches(current, observed))
120
+ return false;
121
+ fsSync.rmSync(lockPath);
122
+ return true;
123
+ }
49
124
  export function sidecarLockSnapshotMatches(current, observed) {
50
125
  if (observed.ownershipToken !== undefined) {
51
126
  return (current.stat?.isFile() === true &&
@@ -63,16 +138,21 @@ export function sidecarLockSnapshotMatches(current, observed) {
63
138
  }
64
139
  return observed.stat !== undefined && current.stat !== undefined;
65
140
  }
66
- export async function removeSidecarLockIfUnchanged(lockPath, observed) {
67
- const current = await readSidecarLockSnapshot(lockPath);
141
+ export async function removeSidecarLockIfUnchanged(lockPath, observed, options = {}) {
142
+ const current = await readSidecarLockSnapshot(lockPath, options);
68
143
  if (!current || !observed || !sidecarLockSnapshotMatches(current, observed)) {
69
144
  return false;
70
145
  }
71
- await fs.rm(lockPath, { force: true }).catch(() => undefined);
146
+ if (options.lockRoot) {
147
+ await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath)).catch(() => undefined);
148
+ }
149
+ else {
150
+ await fs.rm(lockPath, { force: true }).catch(() => undefined);
151
+ }
72
152
  return true;
73
153
  }
74
- export async function sidecarLockSnapshotStillPresent(lockPath, observed) {
75
- const current = await readSidecarLockSnapshot(lockPath);
154
+ export async function sidecarLockSnapshotStillPresent(lockPath, observed, options = {}) {
155
+ const current = await readSidecarLockSnapshot(lockPath, options);
76
156
  return !!current && !!observed && sidecarLockSnapshotMatches(current, observed);
77
157
  }
78
158
  export async function sidecarReclaimGuardExists(pathname) {
@@ -108,7 +188,8 @@ export async function removeStaleSidecarLockIfAllowed(params) {
108
188
  if (!params.shouldRemoveStaleLock || params.snapshot.raw === undefined) {
109
189
  return "not-approved";
110
190
  }
111
- if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot))) {
191
+ const ioOptions = { lockRoot: params.lockRoot, parsePayload: params.parsePayload };
192
+ if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot, ioOptions))) {
112
193
  return "changed";
113
194
  }
114
195
  if (!(await params.shouldRemoveStaleLock({
@@ -119,11 +200,16 @@ export async function removeStaleSidecarLockIfAllowed(params) {
119
200
  }))) {
120
201
  return "not-approved";
121
202
  }
122
- if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot))) {
203
+ if (!(await sidecarLockSnapshotStillPresent(params.lockPath, params.snapshot, ioOptions))) {
123
204
  return "changed";
124
205
  }
125
206
  try {
126
- await fs.rm(params.lockPath);
207
+ if (params.lockRoot) {
208
+ await params.lockRoot.remove(relativeSidecarLockPath(params.lockRoot, params.lockPath));
209
+ }
210
+ else {
211
+ await fs.rm(params.lockPath);
212
+ }
127
213
  return "removed";
128
214
  }
129
215
  catch (err) {
@@ -0,0 +1,56 @@
1
+ import type { Root } from "./root-impl.js";
2
+ import type { SidecarLockStaleSnapshot } from "./sidecar-lock-reclaim.js";
3
+ export type SidecarLockRetryOptions = {
4
+ retries?: number;
5
+ factor?: number;
6
+ minTimeout?: number;
7
+ maxTimeout?: number;
8
+ randomize?: boolean;
9
+ };
10
+ export type SidecarLockStaleRecovery = "fail-closed" | "remove-if-unchanged";
11
+ export type SidecarLockCompromisedInfo = {
12
+ lockPath: string;
13
+ normalizedTargetPath: string;
14
+ };
15
+ export type SidecarLockAcquireOptions<TPayload extends Record<string, unknown>> = {
16
+ targetPath: string;
17
+ lockPath?: string;
18
+ staleMs: number;
19
+ timeoutMs?: number;
20
+ retry?: SidecarLockRetryOptions;
21
+ staleRecovery?: SidecarLockStaleRecovery;
22
+ reentrantOwner?: string;
23
+ payload: () => TPayload | Promise<TPayload>;
24
+ shouldReclaim?: (params: {
25
+ lockPath: string;
26
+ normalizedTargetPath: string;
27
+ payload: unknown;
28
+ staleMs: number;
29
+ nowMs: number;
30
+ heldByThisProcess: boolean;
31
+ }) => boolean | Promise<boolean>;
32
+ shouldRemoveStaleLock?: (snapshot: SidecarLockStaleSnapshot) => boolean | Promise<boolean>;
33
+ metadata?: Record<string, unknown>;
34
+ parsePayload?: (raw: string) => unknown;
35
+ lockRoot?: Root;
36
+ onCompromised?: (info: SidecarLockCompromisedInfo) => void;
37
+ compromiseCheckIntervalMs?: number;
38
+ };
39
+ export type SidecarLockHandle = {
40
+ lockPath: string;
41
+ normalizedTargetPath: string;
42
+ verifyStillHeld: () => Promise<boolean>;
43
+ release: () => Promise<void>;
44
+ [Symbol.asyncDispose](): Promise<void>;
45
+ };
46
+ export type SidecarLockHeldEntry = {
47
+ normalizedTargetPath: string;
48
+ lockPath: string;
49
+ acquiredAt: number;
50
+ metadata: Record<string, unknown>;
51
+ forceRelease: () => Promise<boolean>;
52
+ };
53
+ export type WithSidecarLockOptions<TPayload extends Record<string, unknown>> = Omit<SidecarLockAcquireOptions<TPayload>, "targetPath"> & {
54
+ managerKey?: string;
55
+ };
56
+ //# sourceMappingURL=sidecar-lock-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar-lock-types.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAE1E,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,wBAAwB,GAAG,aAAa,GAAG,qBAAqB,CAAC;AAE7E,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;CAC9B,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,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,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,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;QACd,iBAAiB,EAAE,OAAO,CAAC;KAC5B,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjC,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,0BAA0B,KAAK,IAAI,CAAC;IAC3D,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,49 +1,6 @@
1
- import { type SidecarLockStaleSnapshot } from "./sidecar-lock-reclaim.js";
1
+ import type { SidecarLockAcquireOptions, SidecarLockHandle, SidecarLockHeldEntry, WithSidecarLockOptions } from "./sidecar-lock-types.js";
2
2
  export type { SidecarLockStaleSnapshot } from "./sidecar-lock-reclaim.js";
3
- export type SidecarLockRetryOptions = {
4
- retries?: number;
5
- factor?: number;
6
- minTimeout?: number;
7
- maxTimeout?: number;
8
- randomize?: boolean;
9
- };
10
- export type SidecarLockStaleRecovery = "fail-closed" | "remove-if-unchanged";
11
- export type SidecarLockAcquireOptions<TPayload extends Record<string, unknown>> = {
12
- targetPath: string;
13
- lockPath?: string;
14
- staleMs: number;
15
- timeoutMs?: number;
16
- retry?: SidecarLockRetryOptions;
17
- staleRecovery?: SidecarLockStaleRecovery;
18
- allowReentrant?: boolean;
19
- payload: () => TPayload | Promise<TPayload>;
20
- shouldReclaim?: (params: {
21
- lockPath: string;
22
- normalizedTargetPath: string;
23
- payload: Record<string, unknown> | null;
24
- staleMs: number;
25
- nowMs: number;
26
- heldByThisProcess: boolean;
27
- }) => boolean | Promise<boolean>;
28
- shouldRemoveStaleLock?: (snapshot: SidecarLockStaleSnapshot) => boolean | Promise<boolean>;
29
- metadata?: Record<string, unknown>;
30
- };
31
- export type SidecarLockHandle = {
32
- lockPath: string;
33
- normalizedTargetPath: string;
34
- release: () => Promise<void>;
35
- [Symbol.asyncDispose](): Promise<void>;
36
- };
37
- export type SidecarLockHeldEntry = {
38
- normalizedTargetPath: string;
39
- lockPath: string;
40
- acquiredAt: number;
41
- metadata: Record<string, unknown>;
42
- forceRelease: () => Promise<boolean>;
43
- };
44
- export type WithSidecarLockOptions<TPayload extends Record<string, unknown>> = Omit<SidecarLockAcquireOptions<TPayload>, "targetPath"> & {
45
- managerKey?: string;
46
- };
3
+ export type { SidecarLockAcquireOptions, SidecarLockCompromisedInfo, SidecarLockHandle, SidecarLockHeldEntry, SidecarLockRetryOptions, SidecarLockStaleRecovery, WithSidecarLockOptions, } from "./sidecar-lock-types.js";
47
4
  export declare function createSidecarLockManager(key: string): {
48
5
  acquire: <TPayload extends Record<string, unknown>>(options: SidecarLockAcquireOptions<TPayload>) => Promise<SidecarLockHandle>;
49
6
  withLock: <T, TPayload extends Record<string, unknown>>(options: SidecarLockAcquireOptions<TPayload>, fn: () => Promise<T>) => Promise<T>;
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EAEpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAKjC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAsLjC,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAgB3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACpD,yBAAyB,CAAC,QAAQ,CAAC,KAC3C,OAAO,CAAC,iBAAiB,CAAC;eAgNL,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"}