@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.
- package/CHANGELOG.md +66 -0
- package/README.md +18 -6
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +20 -12
- package/dist/archive-entry.d.ts +1 -0
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +25 -5
- package/dist/archive-input.d.ts +7 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +11 -1
- package/dist/archive-limits.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +6 -3
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +2 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +30 -12
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +3 -2
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +7 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +7 -4
- package/dist/archive-zip-entry.d.ts +1 -0
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-integrity.d.ts +4 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -0
- package/dist/archive-zip-integrity.js +44 -0
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +6 -1
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +5 -2
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +13 -2
- package/dist/deny-mutations.d.ts +1 -0
- package/dist/deny-mutations.d.ts.map +1 -1
- package/dist/deny-mutations.js +6 -39
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +6 -6
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +5 -4
- package/dist/error-detail.d.ts +2 -0
- package/dist/error-detail.d.ts.map +1 -0
- package/dist/error-detail.js +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +4 -1
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +88 -35
- package/dist/file-store-boundary.d.ts +6 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +20 -5
- package/dist/file-store-sync-write.d.ts +9 -0
- package/dist/file-store-sync-write.d.ts.map +1 -0
- package/dist/file-store-sync-write.js +97 -0
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +36 -128
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +27 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +5 -6
- package/dist/install-path.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +19 -13
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +1 -3
- package/dist/local-file-access.d.ts +3 -2
- package/dist/local-file-access.d.ts.map +1 -1
- package/dist/local-file-access.js +12 -6
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +35 -30
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +51 -12
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +1 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +14 -1
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +44 -17
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +6 -5
- package/dist/path-policy.d.ts.map +1 -1
- package/dist/path-policy.js +1 -7
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +4 -0
- package/dist/permission-exec.d.ts +6 -0
- package/dist/permission-exec.d.ts.map +1 -0
- package/dist/permission-exec.js +28 -0
- package/dist/permissions-windows.d.ts +56 -0
- package/dist/permissions-windows.d.ts.map +1 -0
- package/dist/permissions-windows.js +285 -0
- package/dist/permissions.d.ts +3 -46
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +17 -328
- package/dist/pinned-write.js +2 -1
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +21 -2
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +10 -1
- package/dist/replace-file-copy-fallback.d.ts +3 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +18 -11
- package/dist/replace-file-descriptor.d.ts +33 -0
- package/dist/replace-file-descriptor.d.ts.map +1 -0
- package/dist/replace-file-descriptor.js +96 -0
- package/dist/replace-file.d.ts +9 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +64 -72
- package/dist/root-context.d.ts +8 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +43 -2
- package/dist/root-errors.d.ts +7 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +46 -1
- package/dist/root-file.d.ts +1 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +2 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +213 -168
- package/dist/root-path-symlink.d.ts +3 -0
- package/dist/root-path-symlink.d.ts.map +1 -0
- package/dist/root-path-symlink.js +35 -0
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +129 -190
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +15 -0
- package/dist/safe-path-segment.d.ts +2 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +12 -0
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +19 -3
- package/dist/secure-temp-dir.d.ts.map +1 -1
- package/dist/secure-temp-dir.js +6 -1
- package/dist/short-path.d.ts +2 -0
- package/dist/short-path.d.ts.map +1 -0
- package/dist/short-path.js +7 -0
- package/dist/sidecar-lock-acquire.d.ts +30 -0
- package/dist/sidecar-lock-acquire.d.ts.map +1 -0
- package/dist/sidecar-lock-acquire.js +260 -0
- package/dist/sidecar-lock-policy.d.ts +3 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +20 -3
- package/dist/sidecar-lock-reclaim.d.ts +5 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +59 -9
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +27 -226
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +15 -4
- package/dist/test-hooks.d.ts +1 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/timing.d.ts +2 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +9 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +15 -0
- package/docs/advanced.md +1 -1
- package/docs/archive.md +27 -20
- package/docs/assets/readme-banner.jpg +0 -0
- package/docs/atomic.md +48 -19
- package/docs/errors.md +31 -7
- package/docs/file-store.md +10 -1
- package/docs/filename.md +17 -16
- package/docs/index.md +1 -0
- package/docs/install-path.md +9 -6
- package/docs/json.md +8 -2
- package/docs/local-roots.md +79 -103
- package/docs/native-helper.md +11 -5
- package/docs/native.md +2 -2
- package/docs/output.md +8 -3
- package/docs/path-scope.md +4 -1
- package/docs/path.md +7 -3
- package/docs/permissions.md +8 -0
- package/docs/public-api.md +109 -0
- package/docs/quickstart.md +6 -2
- package/docs/reading.md +13 -10
- package/docs/root.md +26 -4
- package/docs/secret-file.md +17 -4
- package/docs/security-model.md +21 -4
- package/docs/sidecar-lock.md +15 -5
- package/docs/temp.md +37 -5
- package/docs/test-hooks.md +2 -0
- package/docs/types.md +32 -15
- package/docs/writing.md +25 -12
- package/package.json +7 -3
- package/dist/archive-file-io.d.ts +0 -9
- package/dist/archive-file-io.d.ts.map +0 -1
- package/dist/archive-file-io.js +0 -11
- package/dist/file-store-source.d.ts +0 -5
- package/dist/file-store-source.d.ts.map +0 -1
- package/dist/file-store-source.js +0 -30
- package/dist/file-url.d.ts +0 -2
- package/dist/file-url.d.ts.map +0 -1
- package/dist/file-url.js +0 -1
- package/dist/path-stat.d.ts +0 -4
- package/dist/path-stat.d.ts.map +0 -1
- package/dist/path-stat.js +0 -15
- package/dist/windows-permissions-native.d.ts +0 -9
- package/dist/windows-permissions-native.d.ts.map +0 -1
- package/dist/windows-permissions-native.js +0 -31
package/dist/sidecar-lock.js
CHANGED
|
@@ -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 {
|
|
6
|
-
import {
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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":"
|
|
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"}
|
package/dist/temp-target.js
CHANGED
|
@@ -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
|
|
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();
|
package/dist/test-hooks.d.ts
CHANGED
|
@@ -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;
|
package/dist/test-hooks.d.ts.map
CHANGED
|
@@ -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
package/dist/timing.d.ts.map
CHANGED
|
@@ -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
|
}) {
|
package/dist/walk.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
|
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;
|
|
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`.
|
|
127
|
+
A limit violation throws `ArchiveLimitError`. Its constant and string code are:
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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"); //
|
|
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 `
|
|
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
|
|
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-
|
|
252
|
-
- `normalizeArchiveEntryPath(raw)` —
|
|
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
|
|
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
|