@openclaw/fs-safe 0.5.1 → 0.5.2

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 (210) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +18 -6
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +20 -12
  5. package/dist/archive-entry.d.ts +1 -0
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +25 -5
  8. package/dist/archive-input.d.ts +7 -0
  9. package/dist/archive-input.d.ts.map +1 -1
  10. package/dist/archive-input.js +11 -1
  11. package/dist/archive-limits.js +1 -1
  12. package/dist/archive-native.d.ts.map +1 -1
  13. package/dist/archive-native.js +6 -3
  14. package/dist/archive-policy.d.ts.map +1 -1
  15. package/dist/archive-policy.js +2 -1
  16. package/dist/archive-read.d.ts.map +1 -1
  17. package/dist/archive-read.js +30 -12
  18. package/dist/archive-staging.d.ts.map +1 -1
  19. package/dist/archive-staging.js +3 -2
  20. package/dist/archive-tar-meta.d.ts.map +1 -1
  21. package/dist/archive-tar-meta.js +7 -1
  22. package/dist/archive-tar.d.ts.map +1 -1
  23. package/dist/archive-tar.js +7 -4
  24. package/dist/archive-zip-entry.d.ts +1 -0
  25. package/dist/archive-zip-entry.d.ts.map +1 -1
  26. package/dist/archive-zip-integrity.d.ts +4 -0
  27. package/dist/archive-zip-integrity.d.ts.map +1 -0
  28. package/dist/archive-zip-integrity.js +44 -0
  29. package/dist/archive-zip-preflight.d.ts.map +1 -1
  30. package/dist/archive-zip-preflight.js +6 -1
  31. package/dist/archive.d.ts.map +1 -1
  32. package/dist/archive.js +5 -2
  33. package/dist/bounded-read-stream.d.ts.map +1 -1
  34. package/dist/bounded-read-stream.js +13 -2
  35. package/dist/deny-mutations.d.ts +1 -0
  36. package/dist/deny-mutations.d.ts.map +1 -1
  37. package/dist/deny-mutations.js +6 -39
  38. package/dist/device-path.d.ts +1 -0
  39. package/dist/device-path.d.ts.map +1 -1
  40. package/dist/device-path.js +6 -6
  41. package/dist/directory-guard.d.ts.map +1 -1
  42. package/dist/directory-guard.js +5 -4
  43. package/dist/error-detail.d.ts +2 -0
  44. package/dist/error-detail.d.ts.map +1 -0
  45. package/dist/error-detail.js +4 -0
  46. package/dist/errors.d.ts.map +1 -1
  47. package/dist/errors.js +3 -0
  48. package/dist/file-hash.d.ts.map +1 -1
  49. package/dist/file-hash.js +4 -1
  50. package/dist/file-lock-sync.d.ts.map +1 -1
  51. package/dist/file-lock-sync.js +88 -35
  52. package/dist/file-store-boundary.d.ts +6 -0
  53. package/dist/file-store-boundary.d.ts.map +1 -1
  54. package/dist/file-store-boundary.js +20 -5
  55. package/dist/file-store-sync-write.d.ts +9 -0
  56. package/dist/file-store-sync-write.d.ts.map +1 -0
  57. package/dist/file-store-sync-write.js +97 -0
  58. package/dist/file-store.d.ts.map +1 -1
  59. package/dist/file-store.js +36 -128
  60. package/dist/filename.d.ts.map +1 -1
  61. package/dist/filename.js +27 -0
  62. package/dist/guarded-mkdir.d.ts.map +1 -1
  63. package/dist/guarded-mkdir.js +5 -6
  64. package/dist/install-path.js +1 -1
  65. package/dist/json-document-store.d.ts.map +1 -1
  66. package/dist/json-document-store.js +19 -13
  67. package/dist/json.d.ts.map +1 -1
  68. package/dist/json.js +1 -3
  69. package/dist/local-file-access.d.ts +3 -2
  70. package/dist/local-file-access.d.ts.map +1 -1
  71. package/dist/local-file-access.js +12 -6
  72. package/dist/local-roots.d.ts.map +1 -1
  73. package/dist/local-roots.js +35 -30
  74. package/dist/move-path.d.ts.map +1 -1
  75. package/dist/move-path.js +51 -12
  76. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  77. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  78. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  79. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  80. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  81. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  82. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  83. package/dist/native-binding.d.ts +1 -0
  84. package/dist/native-binding.d.ts.map +1 -1
  85. package/dist/native-operations.d.ts.map +1 -1
  86. package/dist/native-operations.js +14 -1
  87. package/dist/native-pinned-write.d.ts.map +1 -1
  88. package/dist/native-pinned-write.js +44 -17
  89. package/dist/output-sibling.d.ts.map +1 -1
  90. package/dist/output-sibling.js +6 -5
  91. package/dist/path-policy.d.ts.map +1 -1
  92. package/dist/path-policy.js +1 -7
  93. package/dist/path.d.ts.map +1 -1
  94. package/dist/path.js +4 -0
  95. package/dist/permission-exec.d.ts +6 -0
  96. package/dist/permission-exec.d.ts.map +1 -0
  97. package/dist/permission-exec.js +28 -0
  98. package/dist/permissions-windows.d.ts +56 -0
  99. package/dist/permissions-windows.d.ts.map +1 -0
  100. package/dist/permissions-windows.js +285 -0
  101. package/dist/permissions.d.ts +3 -46
  102. package/dist/permissions.d.ts.map +1 -1
  103. package/dist/permissions.js +17 -328
  104. package/dist/pinned-write.js +2 -1
  105. package/dist/regular-file.d.ts.map +1 -1
  106. package/dist/regular-file.js +21 -2
  107. package/dist/replace-directory.d.ts.map +1 -1
  108. package/dist/replace-directory.js +10 -1
  109. package/dist/replace-file-copy-fallback.d.ts +3 -0
  110. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  111. package/dist/replace-file-copy-fallback.js +18 -11
  112. package/dist/replace-file-descriptor.d.ts +33 -0
  113. package/dist/replace-file-descriptor.d.ts.map +1 -0
  114. package/dist/replace-file-descriptor.js +96 -0
  115. package/dist/replace-file.d.ts +9 -2
  116. package/dist/replace-file.d.ts.map +1 -1
  117. package/dist/replace-file.js +64 -72
  118. package/dist/root-context.d.ts +8 -0
  119. package/dist/root-context.d.ts.map +1 -1
  120. package/dist/root-context.js +43 -2
  121. package/dist/root-errors.d.ts +7 -0
  122. package/dist/root-errors.d.ts.map +1 -1
  123. package/dist/root-errors.js +46 -1
  124. package/dist/root-file.d.ts +1 -0
  125. package/dist/root-file.d.ts.map +1 -1
  126. package/dist/root-file.js +2 -0
  127. package/dist/root-impl.d.ts.map +1 -1
  128. package/dist/root-impl.js +213 -168
  129. package/dist/root-path-symlink.d.ts +3 -0
  130. package/dist/root-path-symlink.d.ts.map +1 -0
  131. package/dist/root-path-symlink.js +35 -0
  132. package/dist/root-path.d.ts +1 -0
  133. package/dist/root-path.d.ts.map +1 -1
  134. package/dist/root-path.js +129 -190
  135. package/dist/root-walk.d.ts.map +1 -1
  136. package/dist/root-walk.js +15 -0
  137. package/dist/safe-path-segment.d.ts +2 -0
  138. package/dist/safe-path-segment.d.ts.map +1 -1
  139. package/dist/safe-path-segment.js +12 -0
  140. package/dist/secret-file.d.ts.map +1 -1
  141. package/dist/secret-file.js +19 -3
  142. package/dist/secure-temp-dir.d.ts.map +1 -1
  143. package/dist/secure-temp-dir.js +6 -1
  144. package/dist/short-path.d.ts +2 -0
  145. package/dist/short-path.d.ts.map +1 -0
  146. package/dist/short-path.js +7 -0
  147. package/dist/sidecar-lock-acquire.d.ts +30 -0
  148. package/dist/sidecar-lock-acquire.d.ts.map +1 -0
  149. package/dist/sidecar-lock-acquire.js +260 -0
  150. package/dist/sidecar-lock-policy.d.ts +3 -0
  151. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  152. package/dist/sidecar-lock-policy.js +20 -3
  153. package/dist/sidecar-lock-reclaim.d.ts +5 -1
  154. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  155. package/dist/sidecar-lock-reclaim.js +59 -9
  156. package/dist/sidecar-lock.d.ts.map +1 -1
  157. package/dist/sidecar-lock.js +27 -226
  158. package/dist/temp-target.d.ts.map +1 -1
  159. package/dist/temp-target.js +15 -4
  160. package/dist/test-hooks.d.ts +1 -0
  161. package/dist/test-hooks.d.ts.map +1 -1
  162. package/dist/timing.d.ts +2 -0
  163. package/dist/timing.d.ts.map +1 -1
  164. package/dist/timing.js +9 -0
  165. package/dist/walk.d.ts.map +1 -1
  166. package/dist/walk.js +15 -0
  167. package/docs/advanced.md +1 -1
  168. package/docs/archive.md +27 -20
  169. package/docs/assets/readme-banner.jpg +0 -0
  170. package/docs/atomic.md +48 -19
  171. package/docs/errors.md +31 -7
  172. package/docs/file-store.md +10 -1
  173. package/docs/filename.md +17 -16
  174. package/docs/index.md +1 -0
  175. package/docs/install-path.md +9 -6
  176. package/docs/json.md +8 -2
  177. package/docs/local-roots.md +79 -103
  178. package/docs/native-helper.md +11 -5
  179. package/docs/native.md +2 -2
  180. package/docs/output.md +8 -3
  181. package/docs/path-scope.md +4 -1
  182. package/docs/path.md +7 -3
  183. package/docs/permissions.md +8 -0
  184. package/docs/public-api.md +109 -0
  185. package/docs/quickstart.md +6 -2
  186. package/docs/reading.md +13 -10
  187. package/docs/root.md +26 -4
  188. package/docs/secret-file.md +17 -4
  189. package/docs/security-model.md +21 -4
  190. package/docs/sidecar-lock.md +15 -5
  191. package/docs/temp.md +37 -5
  192. package/docs/test-hooks.md +2 -0
  193. package/docs/types.md +32 -15
  194. package/docs/writing.md +25 -12
  195. package/package.json +7 -3
  196. package/dist/archive-file-io.d.ts +0 -9
  197. package/dist/archive-file-io.d.ts.map +0 -1
  198. package/dist/archive-file-io.js +0 -11
  199. package/dist/file-store-source.d.ts +0 -5
  200. package/dist/file-store-source.d.ts.map +0 -1
  201. package/dist/file-store-source.js +0 -30
  202. package/dist/file-url.d.ts +0 -2
  203. package/dist/file-url.d.ts.map +0 -1
  204. package/dist/file-url.js +0 -1
  205. package/dist/path-stat.d.ts +0 -4
  206. package/dist/path-stat.d.ts.map +0 -1
  207. package/dist/path-stat.js +0 -15
  208. package/dist/windows-permissions-native.d.ts +0 -9
  209. package/dist/windows-permissions-native.d.ts.map +0 -1
  210. package/dist/windows-permissions-native.js +0 -31
@@ -1,13 +1,11 @@
1
1
  import fsSync from "node:fs";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
2
  import { sameFileIdentity } from "./file-identity.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";
3
+ import { removeSidecarLockIfUnchanged, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
4
+ import { acquireSidecarLock } from "./sidecar-lock-acquire.js";
8
5
  import { createHeldSidecarLockHandle } from "./sidecar-lock-handle.js";
9
- import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, } from "./sidecar-lock-policy.js";
10
6
  const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
7
+ const GLOBAL_CLEANUP_KEY = Symbol.for("fsSafe.sidecarLockCleanupRegistered");
8
+ const GLOBAL_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.sidecarLockCleanupHandler");
11
9
  function getGlobalManagers() {
12
10
  const globalWithState = globalThis;
13
11
  if (!globalWithState[GLOBAL_STATE_KEY]) {
@@ -79,17 +77,6 @@ function snapshotMatchesSync(lockPath, observed) {
79
77
  }
80
78
  }
81
79
  }
82
- async function resolveNormalizedTargetPath(targetPath) {
83
- const resolved = path.resolve(targetPath);
84
- const dir = path.dirname(resolved);
85
- await fs.mkdir(dir, { recursive: true });
86
- try {
87
- return path.join(await fs.realpath(dir), path.basename(resolved));
88
- }
89
- catch {
90
- return resolved;
91
- }
92
- }
93
80
  function releaseAllReclaimGuardsSync(state) {
94
81
  for (const reclaimGuardPath of state.reclaimGuards) {
95
82
  try {
@@ -116,6 +103,19 @@ function releaseAllLocksSync(state) {
116
103
  }
117
104
  releaseAllReclaimGuardsSync(state);
118
105
  }
106
+ function ensureGlobalExitCleanupRegistered() {
107
+ const globalWithCleanup = globalThis;
108
+ if (globalWithCleanup[GLOBAL_CLEANUP_KEY])
109
+ return;
110
+ globalWithCleanup[GLOBAL_CLEANUP_KEY] = true;
111
+ const cleanup = () => {
112
+ for (const state of getGlobalManagers().values()) {
113
+ releaseAllLocksSync(state);
114
+ }
115
+ };
116
+ globalWithCleanup[GLOBAL_CLEANUP_HANDLER_KEY] = cleanup;
117
+ process.on("exit", cleanup);
118
+ }
119
119
  async function releaseHeldLock(state, normalizedTargetPath, held, options = {}) {
120
120
  const current = state.held.get(normalizedTargetPath);
121
121
  if (current !== held) {
@@ -164,217 +164,18 @@ function handleForHeldLock(state, normalizedTargetPath, held) {
164
164
  export function createSidecarLockManager(key) {
165
165
  const state = resolveManagerState(key);
166
166
  function ensureExitCleanupRegistered() {
167
- if (!state.cleanupRegistered) {
168
- state.cleanupRegistered = true;
169
- state.reclaimCleanupRegistered = true;
170
- process.on("exit", () => releaseAllLocksSync(state));
171
- return;
172
- }
173
- if (!state.reclaimCleanupRegistered) {
174
- state.reclaimCleanupRegistered = true;
175
- process.on("exit", () => releaseAllReclaimGuardsSync(state));
176
- }
167
+ state.cleanupRegistered = true;
168
+ state.reclaimCleanupRegistered = true;
169
+ ensureGlobalExitCleanupRegistered();
177
170
  }
178
171
  async function acquire(options) {
179
- ensureExitCleanupRegistered();
180
- const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
181
- const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
182
- const held = state.held.get(normalizedTargetPath);
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);
189
- }
190
- const startedAt = Date.now();
191
- const retry = options.retry ?? {};
192
- const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
193
- const reclaimGuardPath = `${lockPath}.reclaim`;
194
- let ownsReclaimGuard = false;
195
- let attempt = 0;
196
- const waitForRetry = async () => {
197
- const elapsed = Date.now() - startedAt;
198
- if ((options.timeoutMs !== undefined &&
199
- options.timeoutMs !== Number.POSITIVE_INFINITY &&
200
- elapsed >= options.timeoutMs) ||
201
- (maxRetries !== undefined && attempt >= maxRetries)) {
202
- throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
203
- code: "file_lock_timeout",
204
- lockPath,
205
- normalizedTargetPath,
206
- });
207
- }
208
- const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
209
- ? Number.POSITIVE_INFINITY
210
- : Math.max(0, options.timeoutMs - elapsed);
211
- const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
212
- attempt += 1;
213
- await new Promise((resolve) => setTimeout(resolve, delay));
214
- };
215
- try {
216
- while (true) {
217
- if (!ownsReclaimGuard && (await sidecarReclaimGuardExists(reclaimGuardPath))) {
218
- await waitForRetry();
219
- continue;
220
- }
221
- let handle = null;
222
- try {
223
- const payload = await options.payload();
224
- const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
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
- }
242
- const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
243
- const createdHeld = {
244
- refCount: 1,
245
- reentrantOwner: options.reentrantOwner,
246
- handle,
247
- lockPath,
248
- snapshot,
249
- acquiredAt: Date.now(),
250
- metadata: options.metadata ?? {},
251
- lockRoot: options.lockRoot,
252
- parsePayload: options.parsePayload,
253
- };
254
- state.held.set(normalizedTargetPath, createdHeld);
255
- if (ownsReclaimGuard) {
256
- try {
257
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
258
- ownsReclaimGuard = false;
259
- }
260
- catch (err) {
261
- await releaseHeldLock(state, normalizedTargetPath, createdHeld, { force: true });
262
- throw err;
263
- }
264
- }
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;
280
- }
281
- catch (err) {
282
- if (handle) {
283
- const failedSnapshot = { payload: null };
284
- try {
285
- failedSnapshot.stat = await handle.stat();
286
- }
287
- catch {
288
- // Best-effort cleanup of a failed exclusive create.
289
- }
290
- const current = state.held.get(normalizedTargetPath);
291
- if (current?.handle === handle) {
292
- state.held.delete(normalizedTargetPath);
293
- }
294
- // If payload serialization/write fails, the file may be empty or
295
- // partial JSON, so remove while our exclusive handle is still open.
296
- if (!options.lockRoot) {
297
- await fs.rm(lockPath, { force: true }).catch(() => undefined);
298
- }
299
- await handle.close().catch(() => undefined);
300
- // Windows can refuse removing an open file; retry after close but
301
- // only if the path still points at the file identity we created.
302
- await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
303
- lockRoot: options.lockRoot,
304
- parsePayload: options.parsePayload,
305
- });
306
- }
307
- if (err.code !== "EEXIST") {
308
- throw err;
309
- }
310
- if (ownsReclaimGuard) {
311
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
312
- ownsReclaimGuard = false;
313
- continue;
314
- }
315
- const nowMs = Date.now();
316
- const snapshot = await readSidecarLockSnapshot(lockPath, {
317
- lockRoot: options.lockRoot,
318
- parsePayload: options.parsePayload,
319
- });
320
- if (!snapshot) {
321
- continue;
322
- }
323
- if (state.held.has(normalizedTargetPath)) {
324
- await waitForRetry();
325
- continue;
326
- }
327
- const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
328
- if (await shouldReclaim({
329
- lockPath,
330
- normalizedTargetPath,
331
- payload: snapshot?.payload ?? null,
332
- staleMs: options.staleMs,
333
- nowMs,
334
- heldByThisProcess: state.held.has(normalizedTargetPath),
335
- })) {
336
- if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
337
- lockRoot: options.lockRoot,
338
- parsePayload: options.parsePayload,
339
- }))) {
340
- continue;
341
- }
342
- const staleRecovery = options.staleRecovery ?? "fail-closed";
343
- if (staleRecovery === "remove-if-unchanged") {
344
- if (!(await tryAcquireSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath))) {
345
- await waitForRetry();
346
- continue;
347
- }
348
- ownsReclaimGuard = true;
349
- const removal = await removeStaleSidecarLockIfAllowed({
350
- lockPath,
351
- normalizedTargetPath,
352
- snapshot,
353
- shouldRemoveStaleLock: options.shouldRemoveStaleLock,
354
- lockRoot: options.lockRoot,
355
- parsePayload: options.parsePayload,
356
- });
357
- if (removal === "removed" || removal === "changed") {
358
- continue;
359
- }
360
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
361
- ownsReclaimGuard = false;
362
- }
363
- throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
364
- code: "file_lock_stale",
365
- lockPath,
366
- normalizedTargetPath,
367
- });
368
- }
369
- await waitForRetry();
370
- }
371
- }
372
- }
373
- finally {
374
- if (ownsReclaimGuard) {
375
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath).catch(() => undefined);
376
- }
377
- }
172
+ return await acquireSidecarLock(options, {
173
+ held: state.held,
174
+ reclaimGuards: state.reclaimGuards,
175
+ ensureExitCleanupRegistered,
176
+ handleForHeldLock: (normalizedTargetPath, held) => handleForHeldLock(state, normalizedTargetPath, held),
177
+ releaseHeldLock: async (normalizedTargetPath, held, releaseOptions) => await releaseHeldLock(state, normalizedTargetPath, held, releaseOptions),
178
+ });
378
179
  }
379
180
  async function withLock(options, fn) {
380
181
  const lock = await acquire(options);
@@ -1 +1 @@
1
- {"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AA0EF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAyBD,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqBpB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE;IACN,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,EACD,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
1
+ {"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AA0EF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAsCD,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsBpB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE;IACN,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,EACD,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { lstat, mkdtemp, rm } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { sameFileIdentity } from "./file-identity.js";
4
5
  import { assertSafePathSegment, sanitizeSafePathSegment } from "./safe-path-segment.js";
5
6
  import { resolveSecureTempRoot } from "./secure-temp-dir.js";
6
7
  import { registerTempPathForExit } from "./temp-cleanup.js";
@@ -89,8 +90,17 @@ function isNodeErrorWithCode(err, code) {
89
90
  "code" in err &&
90
91
  err.code === code);
91
92
  }
92
- async function cleanupTempDir(dir, onCleanupError) {
93
+ async function cleanupTempDir(dir, identity, onCleanupError) {
93
94
  try {
95
+ const current = await lstat(dir).catch((error) => {
96
+ if (isNodeErrorWithCode(error, "ENOENT")) {
97
+ return undefined;
98
+ }
99
+ throw error;
100
+ });
101
+ if (!current || !sameFileIdentity(current, identity)) {
102
+ return;
103
+ }
94
104
  await rm(dir, { recursive: true, force: true });
95
105
  }
96
106
  catch (err) {
@@ -106,11 +116,12 @@ export async function tempFile(params) {
106
116
  const rootDir = resolveTempRoot(params.rootDir);
107
117
  const prefix = `${sanitizePrefix(params.prefix)}-`;
108
118
  const dir = await mkdtemp(path.join(rootDir, prefix));
109
- const unregisterTempDir = registerTempPathForExit(dir, { recursive: true });
119
+ const identity = await lstat(dir);
120
+ const unregisterTempDir = registerTempPathForExit(dir, { recursive: true, identity });
110
121
  const file = (fileName) => path.join(dir, sanitizeTempFileName(fileName ?? params.fileName ?? "download.bin"));
111
122
  const cleanup = async () => {
112
123
  try {
113
- await cleanupTempDir(dir, params.onCleanupError);
124
+ await cleanupTempDir(dir, identity, params.onCleanupError);
114
125
  }
115
126
  finally {
116
127
  unregisterTempDir();
@@ -10,6 +10,7 @@ export type FsSafeTestHooks = {
10
10
  beforeRootFallbackMutation?: (operation: "mkdir" | "move" | "remove", targetPath: string) => Promise<void> | void;
11
11
  afterPinnedWriteFallbackRename?: (targetPath: string) => Promise<void> | void;
12
12
  beforeSiblingTempWrite?: (tempPath: string) => Promise<void> | void;
13
+ beforeSidecarLockSnapshotOpen?: (lockPath: string) => Promise<void> | void;
13
14
  beforeTrashMove?: (targetPath: string, destPath: string) => void;
14
15
  afterPublishTargetCreated?: (method: "hardlink" | "exclusive-copy" | "rename-noreplace", targetPath: string, identity: FileIdentityStat) => Promise<void> | void;
15
16
  beforePublishDirectorySync?: (method: "hardlink" | "exclusive-copy" | "rename-noreplace", targetPath: string, identity: FileIdentityStat) => Promise<void> | 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;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"}
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,6BAA6B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,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"}
package/dist/timing.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export declare function sleep(ms: number): Promise<void>;
2
+ export declare function sleepSync(ms: number): void;
1
3
  export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, labelOrOptions?: string | {
2
4
  label?: string;
3
5
  message?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,cAAc,GAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,KAAK,CAAA;CAErF,GACA,OAAO,CAAC,CAAC,CAAC,CA6BZ"}
1
+ {"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAK1C;AAED,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,cAAc,GAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,KAAK,CAAA;CAErF,GACA,OAAO,CAAC,CAAC,CAAC,CA6BZ"}
package/dist/timing.js CHANGED
@@ -1,3 +1,12 @@
1
+ export function sleep(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
4
+ export function sleepSync(ms) {
5
+ if (ms <= 0) {
6
+ return;
7
+ }
8
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
9
+ }
1
10
  export async function withTimeout(promise, timeoutMs, labelOrOptions = {
2
11
  message: "timeout",
3
12
  }) {
@@ -1 +1 @@
1
- {"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAI7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AA8EF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CAwDjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CAwD1C"}
1
+ {"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAI7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AA+FF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CAyDjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CAyD1C"}
package/dist/walk.js CHANGED
@@ -1,6 +1,19 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ function validateWalkBudget(name, value) {
5
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
6
+ throw new RangeError(`${name} must be a non-negative safe integer`);
7
+ }
8
+ }
9
+ function validateWalkOptions(options) {
10
+ validateWalkBudget("maxDepth", options.maxDepth);
11
+ validateWalkBudget("maxEntries", options.maxEntries);
12
+ if (options.symlinks !== undefined &&
13
+ !["skip", "follow", "include"].includes(options.symlinks)) {
14
+ throw new TypeError(`invalid walk symlink policy: ${String(options.symlinks)}`);
15
+ }
16
+ }
4
17
  function kindForDirent(dirent) {
5
18
  if (dirent.isDirectory())
6
19
  return "directory";
@@ -75,6 +88,7 @@ async function resolveAsyncKind(fullPath, dirent, symlinks) {
75
88
  return "other";
76
89
  }
77
90
  export function walkDirectorySync(rootDir, options = {}) {
91
+ validateWalkOptions(options);
78
92
  const root = path.resolve(rootDir);
79
93
  const symlinks = options.symlinks ?? "skip";
80
94
  const result = {
@@ -133,6 +147,7 @@ export function walkDirectorySync(rootDir, options = {}) {
133
147
  return result;
134
148
  }
135
149
  export async function walkDirectory(rootDir, options = {}) {
150
+ validateWalkOptions(options);
136
151
  const root = path.resolve(rootDir);
137
152
  const symlinks = options.symlinks ?? "skip";
138
153
  const result = {
package/docs/advanced.md CHANGED
@@ -56,7 +56,7 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
56
56
  | Export | Page | Notes |
57
57
  |---|---|---|
58
58
  | `readFileDescriptorBounded`, `readFileDescriptorBoundedSync`, `readFileHandleBounded` | – | Incremental whole-file reads for already-open descriptors/handles. They consume at most `maxBytes + 1`, do not close the input, and throw `FsSafeError("too-large")` on overflow. |
59
- | `openRootFile`, `openRootFileSync`, `canUseRootFileOpen`, `matchRootFileOpenFailure`, related types | – | Low-level no-follow open routed through the root-file path. |
59
+ | `openRootFile`, `openRootFileSync`, `canUseRootFileOpen`, `matchRootFileOpenFailure`, related types | – | Low-level root-bounded open; rejects every symlink component by default, with `rejectSymlinks: false` for explicit in-root following. |
60
60
  | `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
61
61
  | `sameFileIdentity`, `FileIdentityStat` | – | Compare two stats for same-inode equality. |
62
62
  | `pathExists`, `pathExistsSync` | – | Boolean existence check that does not throw on `ENOENT`. |
package/docs/archive.md CHANGED
@@ -42,10 +42,10 @@ await extractArchive({
42
42
  ### Parameters
43
43
 
44
44
  ```ts
45
- type ExtractArchiveParams = {
45
+ type ExtractArchiveOptions = {
46
46
  archivePath: string; // absolute path to the archive
47
47
  destDir: string; // absolute destination directory; must already exist
48
- timeoutMs: number; // wall-clock cap; throws on overrun
48
+ timeoutMs: number; // positive wall-clock cap; <= 0/non-finite disables it
49
49
  kind?: ArchiveKind; // "zip" | "tar" | "tar-zstd" | "tar-bzip2"
50
50
  stripComponents?: number; // strip N leading dirs from entry paths
51
51
  tarGzip?: boolean; // when archive is .tar.gz/.tgz
@@ -107,7 +107,7 @@ of leaving a paused parser to drain indefinitely. The native path finishes its
107
107
  bounded manifest read before TypeScript policy evaluation, so a rejected plan
108
108
  never starts the extraction worker.
109
109
 
110
- If `kind` is omitted, the helper calls `resolveArchiveKind(archivePath)` and throws if the extension is not recognized. Pass `kind` explicitly when the archive name doesn't carry the type (e.g. content-addressed names).
110
+ If `kind` is omitted, the helper calls `resolveArchiveKind(archivePath)` and throws if the extension is not recognized. Pass `kind` explicitly when the archive name doesn't carry the type (e.g. content-addressed names). A positive finite `timeoutMs` is a wall-clock budget; zero, negative, `NaN`, and infinity disable the deadline.
111
111
 
112
112
  ### Limits
113
113
 
@@ -122,18 +122,22 @@ type ArchiveExtractLimits = {
122
122
  };
123
123
  ```
124
124
 
125
- Defaults exist for each (`DEFAULT_MAX_ARCHIVE_BYTES_ZIP`, `DEFAULT_MAX_ENTRIES`, `DEFAULT_MAX_EXTRACTED_BYTES`, `DEFAULT_MAX_ENTRY_BYTES`, `DEFAULT_MAX_META_ENTRY_BYTES`, `DEFAULT_MAX_ENTRY_PATH_COMPONENTS`). The path-component default is 256. It is evaluated after `stripComponents` and before TypeScript accepts an entry for either JavaScript or native extraction, so rejected entries cannot cause implicit parent-directory creation. The 1 MiB metadata default matches node-tar's `maxMetaEntrySize`; fs-safe passes the same resolved value to node-tar and the native TAR meter.
125
+ Defaults exist for each (`DEFAULT_MAX_ARCHIVE_BYTES_ZIP`, `DEFAULT_MAX_ENTRIES`, `DEFAULT_MAX_EXTRACTED_BYTES`, `DEFAULT_MAX_ENTRY_BYTES`, `DEFAULT_MAX_META_ENTRY_BYTES`, `DEFAULT_MAX_ENTRY_PATH_COMPONENTS`). An explicit zero remains zero rather than selecting the default. `maxEntries` counts every archive entry, including entries removed by `stripComponents` or an explicit filter. The path-component default is 256. It is evaluated after `stripComponents` and before TypeScript accepts an entry for either JavaScript or native extraction, so rejected entries cannot cause implicit parent-directory creation. The 1 MiB metadata default matches node-tar's `maxMetaEntrySize`; fs-safe passes the same resolved value to node-tar and the native TAR meter.
126
126
 
127
- A limit violation throws `ArchiveLimitError`. The error's code is one of:
127
+ A limit violation throws `ArchiveLimitError`. Its constant and string code are:
128
128
 
129
- ```ts
130
- ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT
131
- ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT
132
- ARCHIVE_LIMIT_ERROR_CODE.EXTRACTED_SIZE_EXCEEDS_LIMIT
133
- ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT
134
- ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT
135
- ARCHIVE_LIMIT_ERROR_CODE.ENTRY_PATH_COMPONENTS_EXCEEDS_LIMIT
136
- ```
129
+ | Constant | Code |
130
+ |---|---|
131
+ | `ARCHIVE_SIZE_EXCEEDS_LIMIT` | `archive-size-exceeds-limit` |
132
+ | `ENTRY_COUNT_EXCEEDS_LIMIT` | `archive-entry-count-exceeds-limit` |
133
+ | `EXTRACTED_SIZE_EXCEEDS_LIMIT` | `archive-extracted-size-exceeds-limit` |
134
+ | `ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT` | `archive-entry-extracted-size-exceeds-limit` |
135
+ | `META_ENTRY_SIZE_EXCEEDS_LIMIT` | `archive-meta-entry-size-exceeds-limit` |
136
+ | `ENTRY_PATH_COMPONENTS_EXCEEDS_LIMIT` | `archive-entry-path-components-exceeds-limit` |
137
+ | `MANIFEST_SIZE_EXCEEDS_LIMIT` | `archive-manifest-size-exceeds-limit` |
138
+
139
+ `MANIFEST_SIZE_EXCEEDS_LIMIT` is retained in the public compatibility union;
140
+ no current public extractor emits it.
137
141
 
138
142
  Catch and branch on the code to surface a meaningful response to the caller.
139
143
 
@@ -144,10 +148,12 @@ codes remain `"destination-not-directory"`, `"destination-symlink"`, and
144
148
 
145
149
  ## What it defends against
146
150
 
147
- - **Path traversal:** entries with `..`, absolute paths, or Windows drive prefixes are rejected (`ArchiveSecurityError`).
151
+ - **Path traversal:** entries with `..`, absolute paths, NUL bytes, or Windows drive-relative segments such as `C:secret` and `nested/C:secret` are rejected (`ArchiveSecurityError`).
148
152
  - **Symlink/hardlink entries:** rejected by default. Some archives ship symlink/hardlink entries that point outside the destination once resolved; `extractArchive` does not follow them.
153
+ - **Ambiguous output names:** duplicate names and distinct names that collide after `stripComponents`, case normalization, or Unicode normalization are rejected instead of relying on backend- or volume-specific overwrite order.
149
154
  - **TOCTOU during merge:** extraction first writes to a private temp dir, then merges into `destDir` using the same boundary checks as `root().write()`. Destination symlink swaps are checked with the selected platform mechanism; non-Linux routes retain the best-effort race window documented in the [security model](security-model.md#containment-guarantees-by-platform).
150
155
  - **Zip bombs:** `maxExtractedBytes` and `maxEntryBytes` apply to *post-decompression* bytes, so highly-compressed payloads hit the cap before they exhaust disk.
156
+ - **Corrupt ZIP payloads:** streamed output must match both the central-directory CRC and declared uncompressed size before it can leave private staging.
151
157
  - **Slow-loris archives:** `timeoutMs` is a hard wall-clock budget. Extraction is aborted on overrun.
152
158
  - **Metadata bombs:** a fixed-header pass-through reader rejects oversized PAX, GNU long-name, and GNU long-link bodies before either TAR implementation buffers them. It understands octal and base-256 size fields without interpreting metadata content.
153
159
 
@@ -168,7 +174,7 @@ import { resolveArchiveKind, type ArchiveKind } from "@openclaw/fs-safe/archive"
168
174
  const kind = resolveArchiveKind("upload.zip"); // "zip"
169
175
  const tar = resolveArchiveKind("upload.tar.gz"); // "tar"
170
176
  const zstd = resolveArchiveKind("upload.tar.zst"); // "tar-zstd" when native is available
171
- const unknown = resolveArchiveKind("upload.bin"); // undefined
177
+ const unknown = resolveArchiveKind("upload.bin"); // null
172
178
  ```
173
179
 
174
180
  Recognizes:
@@ -178,7 +184,7 @@ Recognizes:
178
184
  - `*.tar.zst`, `*.tar.zstd`, `*.tzst` → `"tar-zstd"` (native only)
179
185
  - `*.tar.bz2`, `*.tbz2`, `*.tbz` → `"tar-bzip2"` (native only)
180
186
 
181
- Returns `undefined` for unknown extensions; check the result before calling
187
+ Returns `null` for unknown extensions; check the result before calling
182
188
  `extractArchive` if the filename is caller-controlled. A recognized zstd or
183
189
  bzip2 TAR extension with no native binding throws the typed
184
190
  `FsSafeError("helper-unavailable")` with installation guidance. This includes
@@ -204,7 +210,8 @@ await extractArchive({
204
210
 
205
211
  `readArchiveEntry(archivePath, entryPath, { maxBytes, kind? })` reads one
206
212
  regular-file entry into a bounded `Buffer` without extracting a tree. It pins
207
- and privately stages the archive input, rejects link and directory entries,
213
+ and privately stages the archive input, rejects link, directory, and duplicate
214
+ entries, verifies ZIP CRC and declared size,
208
215
  and throws `ArchiveLimitError` if decompressed bytes exceed `maxBytes`. ZIP
209
216
  inputs retain the archive subpath's 256 MiB compressed-input ceiling.
210
217
  With a native binding it uses the same Rust decoders as extraction, including
@@ -248,11 +255,11 @@ import {
248
255
  } from "@openclaw/fs-safe/archive";
249
256
  ```
250
257
 
251
- - `validateArchiveEntryPath(raw, opts)` — throws `ArchiveSecurityError` for `..`, absolute, drive-prefixed, or otherwise unsafe entry paths.
252
- - `normalizeArchiveEntryPath(raw)` — POSIX-normalizes the entry path (forward slashes, no `.` segments).
258
+ - `validateArchiveEntryPath(raw, opts)` — throws `ArchiveSecurityError` for `..`, absolute, NUL-containing, drive-relative, or otherwise unsafe entry paths.
259
+ - `normalizeArchiveEntryPath(raw)` — converts backslashes in the entry path to forward slashes.
253
260
  - `stripArchivePath(entryPath, n)` — strip the leading N path components, returning `null` if not enough remain.
254
261
  - `resolveArchiveOutputPath({ destDir, entryPath })` — combines the entry path with the destination, after validation.
255
- - `isWindowsDrivePath(value)` — detects `C:\…` style entries that should be rejected.
262
+ - `isWindowsDrivePath(value)` — detects drive-relative segments such as `C:secret` or `nested/C:secret` that should be rejected.
256
263
 
257
264
  ## Common patterns
258
265
 
Binary file