@openclaw/fs-safe 0.4.7 → 0.5.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 (239) hide show
  1. package/CHANGELOG.md +45 -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/durability.d.ts +2 -0
  56. package/dist/durability.d.ts.map +1 -1
  57. package/dist/durability.js +2 -0
  58. package/dist/errors.d.ts +4 -1
  59. package/dist/errors.d.ts.map +1 -1
  60. package/dist/errors.js +2 -0
  61. package/dist/file-hash.d.ts +10 -0
  62. package/dist/file-hash.d.ts.map +1 -0
  63. package/dist/file-hash.js +73 -0
  64. package/dist/file-lock-sync.d.ts +35 -0
  65. package/dist/file-lock-sync.d.ts.map +1 -0
  66. package/dist/file-lock-sync.js +233 -0
  67. package/dist/file-lock.d.ts +2 -0
  68. package/dist/file-lock.d.ts.map +1 -1
  69. package/dist/file-lock.js +1 -0
  70. package/dist/filename.d.ts.map +1 -1
  71. package/dist/filename.js +4 -1
  72. package/dist/guarded-mkdir.d.ts +7 -1
  73. package/dist/guarded-mkdir.d.ts.map +1 -1
  74. package/dist/guarded-mkdir.js +45 -3
  75. package/dist/index.d.ts +4 -3
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +1 -1
  78. package/dist/json-document-store.d.ts.map +1 -1
  79. package/dist/json-document-store.js +30 -15
  80. package/dist/move-path.d.ts.map +1 -1
  81. package/dist/move-path.js +101 -17
  82. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  83. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  84. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  85. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  86. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  87. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  88. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  89. package/dist/native-binding.d.ts +79 -0
  90. package/dist/native-binding.d.ts.map +1 -0
  91. package/dist/native-binding.js +1 -0
  92. package/dist/native-config.d.ts +17 -0
  93. package/dist/native-config.d.ts.map +1 -0
  94. package/dist/native-config.js +71 -0
  95. package/dist/native-operations.d.ts +21 -0
  96. package/dist/native-operations.d.ts.map +1 -0
  97. package/dist/native-operations.js +115 -0
  98. package/dist/native-pinned-write.d.ts +5 -0
  99. package/dist/native-pinned-write.d.ts.map +1 -0
  100. package/dist/native-pinned-write.js +115 -0
  101. package/dist/native.d.ts +14 -0
  102. package/dist/native.d.ts.map +1 -0
  103. package/dist/native.js +204 -0
  104. package/dist/output-sibling.d.ts +8 -0
  105. package/dist/output-sibling.d.ts.map +1 -0
  106. package/dist/output-sibling.js +122 -0
  107. package/dist/output.d.ts +2 -0
  108. package/dist/output.d.ts.map +1 -1
  109. package/dist/output.js +32 -5
  110. package/dist/owner-dacl.d.ts +31 -0
  111. package/dist/owner-dacl.d.ts.map +1 -0
  112. package/dist/owner-dacl.js +27 -0
  113. package/dist/path-policy.js +1 -1
  114. package/dist/permissions-public.d.ts +2 -0
  115. package/dist/permissions-public.d.ts.map +1 -1
  116. package/dist/permissions-public.js +2 -0
  117. package/dist/permissions.d.ts.map +1 -1
  118. package/dist/permissions.js +6 -0
  119. package/dist/pinned-operation.d.ts +2 -0
  120. package/dist/pinned-operation.d.ts.map +1 -0
  121. package/dist/pinned-operation.js +35 -0
  122. package/dist/pinned-write.d.ts +3 -16
  123. package/dist/pinned-write.d.ts.map +1 -1
  124. package/dist/pinned-write.js +20 -85
  125. package/dist/private-directory.d.ts +5 -0
  126. package/dist/private-directory.d.ts.map +1 -0
  127. package/dist/private-directory.js +13 -0
  128. package/dist/private-temp-workspace.d.ts +6 -2
  129. package/dist/private-temp-workspace.d.ts.map +1 -1
  130. package/dist/private-temp-workspace.js +40 -13
  131. package/dist/publish-file-failure.d.ts +27 -0
  132. package/dist/publish-file-failure.d.ts.map +1 -0
  133. package/dist/publish-file-failure.js +21 -0
  134. package/dist/publish-file.d.ts +21 -0
  135. package/dist/publish-file.d.ts.map +1 -0
  136. package/dist/publish-file.js +351 -0
  137. package/dist/read-opened-file.d.ts +3 -0
  138. package/dist/read-opened-file.d.ts.map +1 -1
  139. package/dist/read-opened-file.js +1 -0
  140. package/dist/replace-file-copy-fallback.d.ts +34 -0
  141. package/dist/replace-file-copy-fallback.d.ts.map +1 -0
  142. package/dist/replace-file-copy-fallback.js +339 -0
  143. package/dist/replace-file.d.ts +6 -2
  144. package/dist/replace-file.d.ts.map +1 -1
  145. package/dist/replace-file.js +39 -90
  146. package/dist/root-context.d.ts +4 -1
  147. package/dist/root-context.d.ts.map +1 -1
  148. package/dist/root-context.js +20 -1
  149. package/dist/root-impl.d.ts +5 -0
  150. package/dist/root-impl.d.ts.map +1 -1
  151. package/dist/root-impl.js +128 -225
  152. package/dist/root-path-existing.d.ts +3 -0
  153. package/dist/root-path-existing.d.ts.map +1 -0
  154. package/dist/root-path-existing.js +69 -0
  155. package/dist/root-path.d.ts +1 -2
  156. package/dist/root-path.d.ts.map +1 -1
  157. package/dist/root-path.js +67 -81
  158. package/dist/root-walk.d.ts +41 -0
  159. package/dist/root-walk.d.ts.map +1 -0
  160. package/dist/root-walk.js +114 -0
  161. package/dist/root.d.ts +2 -0
  162. package/dist/root.d.ts.map +1 -1
  163. package/dist/secret-file.d.ts +5 -2
  164. package/dist/secret-file.d.ts.map +1 -1
  165. package/dist/secret-file.js +20 -2
  166. package/dist/secret-read-async.d.ts +4 -0
  167. package/dist/secret-read-async.d.ts.map +1 -0
  168. package/dist/secret-read-async.js +95 -0
  169. package/dist/secret.d.ts +2 -1
  170. package/dist/secret.d.ts.map +1 -1
  171. package/dist/secret.js +2 -1
  172. package/dist/sibling-temp.d.ts.map +1 -1
  173. package/dist/sibling-temp.js +1 -0
  174. package/dist/sidecar-lock-handle.d.ts +20 -0
  175. package/dist/sidecar-lock-handle.d.ts.map +1 -0
  176. package/dist/sidecar-lock-handle.js +28 -0
  177. package/dist/sidecar-lock-policy.d.ts +10 -0
  178. package/dist/sidecar-lock-policy.d.ts.map +1 -0
  179. package/dist/sidecar-lock-policy.js +29 -0
  180. package/dist/sidecar-lock-reclaim.d.ts +22 -6
  181. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  182. package/dist/sidecar-lock-reclaim.js +106 -20
  183. package/dist/sidecar-lock-types.d.ts +56 -0
  184. package/dist/sidecar-lock-types.d.ts.map +1 -0
  185. package/dist/sidecar-lock-types.js +1 -0
  186. package/dist/sidecar-lock.d.ts +2 -45
  187. package/dist/sidecar-lock.d.ts.map +1 -1
  188. package/dist/sidecar-lock.js +94 -55
  189. package/dist/temp-cleanup.d.ts +7 -1
  190. package/dist/temp-cleanup.d.ts.map +1 -1
  191. package/dist/temp-cleanup.js +35 -5
  192. package/dist/temp.d.ts +2 -1
  193. package/dist/temp.d.ts.map +1 -1
  194. package/dist/test-hooks.d.ts +3 -0
  195. package/dist/test-hooks.d.ts.map +1 -1
  196. package/dist/windows-permissions-native.d.ts +9 -0
  197. package/dist/windows-permissions-native.d.ts.map +1 -0
  198. package/dist/windows-permissions-native.js +31 -0
  199. package/docs/archive.md +131 -13
  200. package/docs/atomic.md +30 -1
  201. package/docs/config.md +51 -21
  202. package/docs/durability.md +211 -1
  203. package/docs/errors.md +36 -6
  204. package/docs/filename.md +0 -0
  205. package/docs/index.md +11 -8
  206. package/docs/install.md +27 -26
  207. package/docs/json-store.md +18 -3
  208. package/docs/migrating-to-0.5.md +196 -0
  209. package/docs/native-helper.md +85 -0
  210. package/docs/native.md +134 -0
  211. package/docs/output.md +37 -11
  212. package/docs/path.md +1 -1
  213. package/docs/permissions.md +84 -1
  214. package/docs/quickstart.md +5 -2
  215. package/docs/reading.md +4 -4
  216. package/docs/root.md +31 -15
  217. package/docs/secret-file.md +50 -1
  218. package/docs/security-model.md +29 -8
  219. package/docs/sidecar-lock.md +92 -7
  220. package/docs/temp.md +24 -1
  221. package/docs/test-hooks.md +22 -2
  222. package/docs/testing.md +18 -7
  223. package/docs/types.md +3 -1
  224. package/docs/walk.md +55 -0
  225. package/docs/writing.md +3 -3
  226. package/package.json +8 -2
  227. package/dist/pinned-helper.d.ts +0 -9
  228. package/dist/pinned-helper.d.ts.map +0 -1
  229. package/dist/pinned-helper.js +0 -19
  230. package/dist/pinned-path.d.ts +0 -7
  231. package/dist/pinned-path.d.ts.map +0 -1
  232. package/dist/pinned-path.js +0 -21
  233. package/dist/pinned-python-config.d.ts +0 -9
  234. package/dist/pinned-python-config.d.ts.map +0 -1
  235. package/dist/pinned-python-config.js +0 -38
  236. package/dist/pinned-python.d.ts +0 -12
  237. package/dist/pinned-python.d.ts.map +0 -1
  238. package/dist/pinned-python.js +0 -688
  239. package/docs/python-helper.md +0 -103
@@ -2,7 +2,11 @@ import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { sameFileIdentity } from "./file-identity.js";
5
- import { readSidecarLockSnapshot, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotMatches, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
5
+ import { readSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotMatches, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
6
+ import { FsSafeError } from "./errors.js";
7
+ import { createNativeExclusiveFile } from "./native-operations.js";
8
+ import { createHeldSidecarLockHandle } from "./sidecar-lock-handle.js";
9
+ import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, } from "./sidecar-lock-policy.js";
6
10
  const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
7
11
  function getGlobalManagers() {
8
12
  const globalWithState = globalThis;
@@ -28,6 +32,9 @@ function resolveManagerState(key) {
28
32
  // Backfill state created by fs-safe versions that predate reclaim guards.
29
33
  state.reclaimCleanupRegistered ??= false;
30
34
  state.reclaimGuards ??= new Set();
35
+ for (const held of state.held.values()) {
36
+ held.refCount ??= 1;
37
+ }
31
38
  }
32
39
  return state;
33
40
  }
@@ -83,28 +90,6 @@ async function resolveNormalizedTargetPath(targetPath) {
83
90
  return resolved;
84
91
  }
85
92
  }
86
- function computeDelayMs(retry, attempt) {
87
- const minTimeout = retry.minTimeout ?? 50;
88
- const maxTimeout = retry.maxTimeout ?? 1000;
89
- const factor = retry.factor ?? 1;
90
- const base = Math.min(maxTimeout, Math.max(minTimeout, minTimeout * factor ** attempt));
91
- const jitter = retry.randomize ? 1 + Math.random() : 1;
92
- return Math.min(maxTimeout, Math.round(base * jitter));
93
- }
94
- async function defaultShouldReclaim(params) {
95
- const createdAt = typeof params.payload?.createdAt === "string" ? params.payload.createdAt : "";
96
- const createdAtMs = Date.parse(createdAt);
97
- if (Number.isFinite(createdAtMs) && params.nowMs - createdAtMs > params.staleMs) {
98
- return true;
99
- }
100
- try {
101
- const stat = await fs.stat(params.lockPath);
102
- return params.nowMs - stat.mtimeMs > params.staleMs;
103
- }
104
- catch {
105
- return true;
106
- }
107
- }
108
93
  function releaseAllReclaimGuardsSync(state) {
109
94
  for (const reclaimGuardPath of state.reclaimGuards) {
110
95
  try {
@@ -120,7 +105,7 @@ function releaseAllLocksSync(state) {
120
105
  for (const [normalizedTargetPath, held] of state.held) {
121
106
  void held.handle.close().catch(() => undefined);
122
107
  try {
123
- if (snapshotMatchesSync(held.lockPath, held.snapshot)) {
108
+ if (!held.lockRoot && snapshotMatchesSync(held.lockPath, held.snapshot)) {
124
109
  fsSync.rmSync(held.lockPath, { force: true });
125
110
  }
126
111
  }
@@ -131,17 +116,17 @@ function releaseAllLocksSync(state) {
131
116
  }
132
117
  releaseAllReclaimGuardsSync(state);
133
118
  }
134
- async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
119
+ async function releaseHeldLock(state, normalizedTargetPath, held, options = {}) {
135
120
  const current = state.held.get(normalizedTargetPath);
136
121
  if (current !== held) {
137
122
  return false;
138
123
  }
139
- if (opts.force) {
140
- held.count = 0;
124
+ if (options.force) {
125
+ held.refCount = 0;
141
126
  }
142
127
  else {
143
- held.count -= 1;
144
- if (held.count > 0) {
128
+ held.refCount -= 1;
129
+ if (held.refCount > 0) {
145
130
  return false;
146
131
  }
147
132
  }
@@ -150,9 +135,16 @@ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
150
135
  return true;
151
136
  }
152
137
  state.held.delete(normalizedTargetPath);
138
+ if (held.compromiseTimer) {
139
+ clearInterval(held.compromiseTimer);
140
+ held.compromiseTimer = undefined;
141
+ }
153
142
  held.releasePromise = (async () => {
154
143
  await held.handle.close().catch(() => undefined);
155
- await removeSidecarLockIfUnchanged(held.lockPath, held.snapshot);
144
+ await removeSidecarLockIfUnchanged(held.lockPath, held.snapshot, {
145
+ lockRoot: held.lockRoot,
146
+ parsePayload: held.parsePayload,
147
+ });
156
148
  })();
157
149
  try {
158
150
  await held.releasePromise;
@@ -162,6 +154,13 @@ async function releaseHeldLock(state, normalizedTargetPath, held, opts = {}) {
162
154
  held.releasePromise = undefined;
163
155
  }
164
156
  }
157
+ function handleForHeldLock(state, normalizedTargetPath, held) {
158
+ return createHeldSidecarLockHandle({
159
+ normalizedTargetPath,
160
+ held,
161
+ release: async () => await releaseHeldLock(state, normalizedTargetPath, held),
162
+ });
163
+ }
165
164
  export function createSidecarLockManager(key) {
166
165
  const state = resolveManagerState(key);
167
166
  function ensureExitCleanupRegistered() {
@@ -181,15 +180,12 @@ export function createSidecarLockManager(key) {
181
180
  const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
182
181
  const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
183
182
  const held = state.held.get(normalizedTargetPath);
184
- if (held && options.allowReentrant) {
185
- held.count += 1;
186
- const release = () => releaseHeldLock(state, normalizedTargetPath, held).then(() => undefined);
187
- return {
188
- lockPath,
189
- normalizedTargetPath,
190
- release,
191
- [Symbol.asyncDispose]: release,
192
- };
183
+ if (held &&
184
+ options.reentrantOwner !== undefined &&
185
+ held.reentrantOwner !== undefined &&
186
+ options.reentrantOwner === held.reentrantOwner) {
187
+ held.refCount += 1;
188
+ return handleForHeldLock(state, normalizedTargetPath, held);
193
189
  }
194
190
  const startedAt = Date.now();
195
191
  const retry = options.retry ?? {};
@@ -212,7 +208,7 @@ export function createSidecarLockManager(key) {
212
208
  const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
213
209
  ? Number.POSITIVE_INFINITY
214
210
  : Math.max(0, options.timeoutMs - elapsed);
215
- const delay = Math.min(computeDelayMs(retry, attempt), remaining);
211
+ const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
216
212
  attempt += 1;
217
213
  await new Promise((resolve) => setTimeout(resolve, delay));
218
214
  };
@@ -224,18 +220,36 @@ export function createSidecarLockManager(key) {
224
220
  }
225
221
  let handle = null;
226
222
  try {
227
- handle = await fs.open(lockPath, "wx");
228
223
  const payload = await options.payload();
229
224
  const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
230
- await handle.writeFile(raw, "utf8");
225
+ if (options.lockRoot) {
226
+ const relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
227
+ try {
228
+ await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
229
+ }
230
+ catch (error) {
231
+ if (error instanceof FsSafeError && error.code === "already-exists") {
232
+ throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
233
+ }
234
+ throw error;
235
+ }
236
+ handle = (await options.lockRoot.open(relativeLockPath)).handle;
237
+ }
238
+ else {
239
+ handle = (await createNativeExclusiveFile(lockPath, 0o600)) ?? await fs.open(lockPath, "wx");
240
+ await handle.writeFile(raw, "utf8");
241
+ }
231
242
  const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
232
243
  const createdHeld = {
233
- count: 1,
244
+ refCount: 1,
245
+ reentrantOwner: options.reentrantOwner,
234
246
  handle,
235
247
  lockPath,
236
248
  snapshot,
237
249
  acquiredAt: Date.now(),
238
250
  metadata: options.metadata ?? {},
251
+ lockRoot: options.lockRoot,
252
+ parsePayload: options.parsePayload,
239
253
  };
240
254
  state.held.set(normalizedTargetPath, createdHeld);
241
255
  if (ownsReclaimGuard) {
@@ -248,13 +262,21 @@ export function createSidecarLockManager(key) {
248
262
  throw err;
249
263
  }
250
264
  }
251
- const release = () => releaseHeldLock(state, normalizedTargetPath, createdHeld).then(() => undefined);
252
- return {
253
- lockPath,
254
- normalizedTargetPath,
255
- release,
256
- [Symbol.asyncDispose]: release,
257
- };
265
+ const returnedHandle = handleForHeldLock(state, normalizedTargetPath, createdHeld);
266
+ const interval = options.compromiseCheckIntervalMs;
267
+ if (options.onCompromised && interval !== undefined && interval > 0) {
268
+ createdHeld.compromiseTimer = setInterval(() => {
269
+ void returnedHandle.verifyStillHeld().then((stillHeld) => {
270
+ if (!stillHeld && createdHeld.compromiseTimer) {
271
+ clearInterval(createdHeld.compromiseTimer);
272
+ createdHeld.compromiseTimer = undefined;
273
+ options.onCompromised?.({ lockPath, normalizedTargetPath });
274
+ }
275
+ });
276
+ }, interval);
277
+ createdHeld.compromiseTimer.unref();
278
+ }
279
+ return returnedHandle;
258
280
  }
259
281
  catch (err) {
260
282
  if (handle) {
@@ -271,11 +293,16 @@ export function createSidecarLockManager(key) {
271
293
  }
272
294
  // If payload serialization/write fails, the file may be empty or
273
295
  // partial JSON, so remove while our exclusive handle is still open.
274
- await fs.rm(lockPath, { force: true }).catch(() => undefined);
296
+ if (!options.lockRoot) {
297
+ await fs.rm(lockPath, { force: true }).catch(() => undefined);
298
+ }
275
299
  await handle.close().catch(() => undefined);
276
300
  // Windows can refuse removing an open file; retry after close but
277
301
  // only if the path still points at the file identity we created.
278
- await removeSidecarLockIfUnchanged(lockPath, failedSnapshot);
302
+ await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
303
+ lockRoot: options.lockRoot,
304
+ parsePayload: options.parsePayload,
305
+ });
279
306
  }
280
307
  if (err.code !== "EEXIST") {
281
308
  throw err;
@@ -286,11 +313,18 @@ export function createSidecarLockManager(key) {
286
313
  continue;
287
314
  }
288
315
  const nowMs = Date.now();
289
- const snapshot = await readSidecarLockSnapshot(lockPath);
316
+ const snapshot = await readSidecarLockSnapshot(lockPath, {
317
+ lockRoot: options.lockRoot,
318
+ parsePayload: options.parsePayload,
319
+ });
290
320
  if (!snapshot) {
291
321
  continue;
292
322
  }
293
- const shouldReclaim = options.shouldReclaim ?? defaultShouldReclaim;
323
+ if (state.held.has(normalizedTargetPath)) {
324
+ await waitForRetry();
325
+ continue;
326
+ }
327
+ const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
294
328
  if (await shouldReclaim({
295
329
  lockPath,
296
330
  normalizedTargetPath,
@@ -299,7 +333,10 @@ export function createSidecarLockManager(key) {
299
333
  nowMs,
300
334
  heldByThisProcess: state.held.has(normalizedTargetPath),
301
335
  })) {
302
- if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot))) {
336
+ if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
337
+ lockRoot: options.lockRoot,
338
+ parsePayload: options.parsePayload,
339
+ }))) {
303
340
  continue;
304
341
  }
305
342
  const staleRecovery = options.staleRecovery ?? "fail-closed";
@@ -314,6 +351,8 @@ export function createSidecarLockManager(key) {
314
351
  normalizedTargetPath,
315
352
  snapshot,
316
353
  shouldRemoveStaleLock: options.shouldRemoveStaleLock,
354
+ lockRoot: options.lockRoot,
355
+ parsePayload: options.parsePayload,
317
356
  });
318
357
  if (removal === "removed" || removal === "changed") {
319
358
  continue;
@@ -1,6 +1,12 @@
1
+ import { type FileIdentityStat } from "./file-identity.js";
2
+ export type TempPathIdentityReceipt = FileIdentityStat;
3
+ export type TempPathRegistration = (() => void) & {
4
+ setIdentity(identity: FileIdentityStat): void;
5
+ };
1
6
  export declare function registerTempPathForExit(tempPath: string, options?: {
2
7
  recursive?: boolean;
3
- }): () => void;
8
+ identity?: FileIdentityStat;
9
+ }): TempPathRegistration;
4
10
  export declare function __cleanupRegisteredTempPathsForTest(): void;
5
11
  export declare function __cleanupRegisteredTempPathForTest(tempPath: string): void;
6
12
  //# sourceMappingURL=temp-cleanup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"temp-cleanup.d.ts","sourceRoot":"","sources":["../src/temp-cleanup.ts"],"names":[],"mappings":"AAqBA,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAChC,MAAM,IAAI,CAYZ;AAED,wBAAgB,mCAAmC,IAAI,IAAI,CAE1D;AAED,wBAAgB,kCAAkC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAUzE"}
1
+ {"version":3,"file":"temp-cleanup.d.ts","sourceRoot":"","sources":["../src/temp-cleanup.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE7E,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG;IAChD,WAAW,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC/C,CAAC;AAmCF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;CAAE,GAC7D,oBAAoB,CAyBtB;AAED,wBAAgB,mCAAmC,IAAI,IAAI,CAE1D;AAED,wBAAgB,kCAAkC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAYzE"}
@@ -1,10 +1,24 @@
1
1
  import fsSync from "node:fs";
2
+ import { sameFileIdentity } from "./file-identity.js";
2
3
  const tempCleanupEntries = new Map();
3
4
  let cleanupRegistered = false;
5
+ function pathStillMatchesReceipt(entry) {
6
+ if (!entry.identity) {
7
+ return false;
8
+ }
9
+ try {
10
+ return sameFileIdentity(fsSync.lstatSync(entry.path), entry.identity);
11
+ }
12
+ catch (error) {
13
+ return error.code === "ENOENT";
14
+ }
15
+ }
4
16
  function cleanupRegisteredTempPathsSync() {
5
17
  for (const entry of tempCleanupEntries.values()) {
6
18
  try {
7
- fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
19
+ if (pathStillMatchesReceipt(entry)) {
20
+ fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
21
+ }
8
22
  }
9
23
  catch {
10
24
  // Process-exit cleanup is best-effort.
@@ -17,13 +31,27 @@ export function registerTempPathForExit(tempPath, options) {
17
31
  cleanupRegistered = true;
18
32
  process.once("exit", cleanupRegisteredTempPathsSync);
19
33
  }
20
- tempCleanupEntries.set(tempPath, {
34
+ const entry = {
21
35
  path: tempPath,
22
36
  recursive: options?.recursive === true,
23
- });
24
- return () => {
37
+ identity: options?.identity,
38
+ };
39
+ if (!entry.identity) {
40
+ try {
41
+ entry.identity = fsSync.lstatSync(tempPath);
42
+ }
43
+ catch {
44
+ // Callers that register before creation set the identity after opening.
45
+ }
46
+ }
47
+ tempCleanupEntries.set(tempPath, entry);
48
+ const unregister = (() => {
25
49
  tempCleanupEntries.delete(tempPath);
50
+ });
51
+ unregister.setIdentity = (identity) => {
52
+ entry.identity = identity;
26
53
  };
54
+ return unregister;
27
55
  }
28
56
  export function __cleanupRegisteredTempPathsForTest() {
29
57
  cleanupRegisteredTempPathsSync();
@@ -34,7 +62,9 @@ export function __cleanupRegisteredTempPathForTest(tempPath) {
34
62
  return;
35
63
  }
36
64
  try {
37
- fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
65
+ if (pathStillMatchesReceipt(entry)) {
66
+ fsSync.rmSync(entry.path, { force: true, recursive: entry.recursive });
67
+ }
38
68
  }
39
69
  finally {
40
70
  tempCleanupEntries.delete(tempPath);
package/dist/temp.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export { tempWorkspace, type TempWorkspace, type TempWorkspaceOptions, tempWorkspaceSync, type TempWorkspaceSync, withTempWorkspace, withTempWorkspaceSync, } from "./private-temp-workspace.js";
1
+ export { tempWorkspace, type TempWorkspace, type TempWorkspaceOptions, type TempWorkspaceCleanupResult, tempWorkspaceSync, type TempWorkspaceSync, withTempWorkspace, withTempWorkspaceSync, } from "./private-temp-workspace.js";
2
+ export type { TempPathIdentityReceipt } from "./temp-cleanup.js";
2
3
  export { resolveSecureTempRoot, type ResolveSecureTempRootOptions } from "./secure-temp-dir.js";
3
4
  //# sourceMappingURL=temp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"temp.d.ts","sourceRoot":"","sources":["../src/temp.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,qBAAqB,EAAE,KAAK,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"temp.d.ts","sourceRoot":"","sources":["../src/temp.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,KAAK,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import type { FileHandle } from "node:fs/promises";
2
+ import type { FileIdentityStat } from "./file-identity.js";
2
3
  export type FsSafeTestHooks = {
3
4
  afterPreOpenLstat?: (filePath: string) => Promise<void> | void;
4
5
  beforeOpen?: (filePath: string, flags: number) => Promise<void> | void;
@@ -10,6 +11,8 @@ export type FsSafeTestHooks = {
10
11
  afterPinnedWriteFallbackRename?: (targetPath: string) => Promise<void> | void;
11
12
  beforeSiblingTempWrite?: (tempPath: string) => Promise<void> | void;
12
13
  beforeTrashMove?: (targetPath: string, destPath: string) => void;
14
+ afterPublishTargetCreated?: (method: "hardlink" | "exclusive-copy" | "rename-noreplace", targetPath: string, identity: FileIdentityStat) => Promise<void> | void;
15
+ beforePublishDirectorySync?: (method: "hardlink" | "exclusive-copy" | "rename-noreplace", targetPath: string, identity: FileIdentityStat) => Promise<void> | void;
13
16
  };
14
17
  export declare function getFsSafeTestHooks(): FsSafeTestHooks | undefined;
15
18
  export declare function __setFsSafeTestHooksForTest(hooks?: FsSafeTestHooks): void;
@@ -1 +1 @@
1
- {"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,2BAA2B,CAAC,EAAE,CAC5B,SAAS,EAAE,OAAO,GAAG,OAAO,EAC5B,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,2BAA2B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxE,+BAA+B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,0BAA0B,CAAC,EAAE,CAC3B,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACtC,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,8BAA8B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9E,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE,CAAC;AAQF,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,SAAS,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAKzE"}
1
+ {"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,2BAA2B,CAAC,EAAE,CAC5B,SAAS,EAAE,OAAO,GAAG,OAAO,EAC5B,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,2BAA2B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxE,+BAA+B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,0BAA0B,CAAC,EAAE,CAC3B,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACtC,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,8BAA8B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9E,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,yBAAyB,CAAC,EAAE,CAC1B,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,EAC1D,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,0BAA0B,CAAC,EAAE,CAC3B,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,EAC1D,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC3B,CAAC;AAQF,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,SAAS,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAKzE"}
@@ -0,0 +1,9 @@
1
+ import type { PermissionCheck, SafeStatResult } from "./permissions.js";
2
+ export declare function inspectWindowsPermissionsNative(params: {
3
+ targetPath: string;
4
+ stat: SafeStatResult;
5
+ effectiveIsDir: boolean;
6
+ effectiveMode: number | null;
7
+ bits: number | null;
8
+ }): PermissionCheck | undefined;
9
+ //# sourceMappingURL=windows-permissions-native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"windows-permissions-native.d.ts","sourceRoot":"","sources":["../src/windows-permissions-native.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAExE,wBAAgB,+BAA+B,CAAC,MAAM,EAAE;IACtD,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,cAAc,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB,GAAG,eAAe,GAAG,SAAS,CA2B9B"}
@@ -0,0 +1,31 @@
1
+ import { getNativeBinding } from "./native.js";
2
+ export function inspectWindowsPermissionsNative(params) {
3
+ const native = getNativeBinding();
4
+ if (!native)
5
+ return undefined;
6
+ try {
7
+ const facts = native.readOwnerAndDacl(params.targetPath);
8
+ if (facts.fallbackRequired)
9
+ return undefined;
10
+ return {
11
+ ok: true,
12
+ isSymlink: params.stat.isSymlink,
13
+ isDir: params.effectiveIsDir,
14
+ mode: params.effectiveMode,
15
+ bits: params.bits,
16
+ source: "windows-acl",
17
+ worldWritable: facts.worldWritable,
18
+ groupWritable: facts.groupWritable,
19
+ worldReadable: facts.worldReadable,
20
+ groupReadable: facts.groupReadable,
21
+ ownerSid: facts.ownerSid,
22
+ ownerTrusted: facts.ownerClass !== "foreign",
23
+ aclSummary: `native owner=${facts.ownerClass} world=` +
24
+ `${facts.worldReadable ? "r" : "-"}${facts.worldWritable ? "w" : "-"} ` +
25
+ `group=${facts.groupReadable ? "r" : "-"}${facts.groupWritable ? "w" : "-"}`,
26
+ };
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }