@openclaw/fs-safe 0.7.0 → 0.7.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 +33 -0
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +0 -1
- package/dist/directory-guard.d.ts +2 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +10 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +72 -26
- package/dist/file-observation.d.ts +9 -0
- package/dist/file-observation.d.ts.map +1 -0
- package/dist/file-observation.js +22 -0
- package/dist/guarded-mutation.d.ts +1 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +2 -0
- package/dist/json.js +6 -6
- package/dist/move-path.d.ts +2 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +21 -3
- package/dist/native-pinned-write-windows.js +1 -1
- package/dist/opened-file-failure.d.ts +7 -0
- package/dist/opened-file-failure.d.ts.map +1 -0
- package/dist/opened-file-failure.js +41 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +8 -2
- package/dist/pinned-write.js +2 -1
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +75 -58
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +46 -13
- package/dist/sidecar-lock-reclaim.d.ts +16 -5
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +47 -37
- package/dist/sidecar-lock-root.d.ts +3 -0
- package/dist/sidecar-lock-root.d.ts.map +1 -0
- package/dist/sidecar-lock-root.js +76 -0
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +4 -1
- package/docs/archive.md +12 -4
- package/docs/atomic.md +34 -0
- package/docs/json.md +3 -0
- package/docs/output.md +6 -3
- package/docs/sidecar-lock.md +56 -4
- package/docs/writing.md +6 -0
- package/package.json +8 -8
package/dist/root-impl.js
CHANGED
|
@@ -11,6 +11,7 @@ import { mkdirPathComponentsWithGuards } from "./guarded-mkdir.js";
|
|
|
11
11
|
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
12
12
|
import { assertMutationNotDenied, mergeDenyMutationPolicies, } from "./deny-mutations.js";
|
|
13
13
|
import { resolveOpenedFileRealPathForFd, resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
|
|
14
|
+
import { openedPathResolutionError, recordFileOpenFailure, recordOpenedFileFailure, recordPreOpenFileChange } from "./opened-file-failure.js";
|
|
14
15
|
import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy, } from "./pinned-write.js";
|
|
15
16
|
import { getNativeBinding } from "./native.js";
|
|
16
17
|
import { validatePinnedOperationPayload } from "./pinned-operation.js";
|
|
@@ -102,25 +103,15 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
102
103
|
if (preOpenStat) {
|
|
103
104
|
await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
|
|
104
105
|
}
|
|
106
|
+
const openFlags = options?.symlinks === "follow-within-root"
|
|
107
|
+
? OPEN_READ_FOLLOW_FLAGS
|
|
108
|
+
: OPEN_READ_FLAGS;
|
|
109
|
+
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
105
110
|
let handle;
|
|
106
111
|
try {
|
|
107
|
-
|
|
108
|
-
? OPEN_READ_FOLLOW_FLAGS
|
|
109
|
-
: OPEN_READ_FLAGS;
|
|
110
|
-
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
111
|
-
handle = await fs.open(filePath, openFlags);
|
|
112
|
-
try {
|
|
113
|
-
await fsSafeTestHooks?.afterOpen?.(filePath, handle);
|
|
114
|
-
}
|
|
115
|
-
catch (err) {
|
|
116
|
-
await handle.close().catch(() => { });
|
|
117
|
-
throw err;
|
|
118
|
-
}
|
|
112
|
+
handle = await fs.open(filePath, openFlags).catch((error) => recordFileOpenFailure(isNotFoundPathError(error) ? fileNotFoundError() : error, filePath));
|
|
119
113
|
}
|
|
120
114
|
catch (err) {
|
|
121
|
-
if (isNotFoundPathError(err)) {
|
|
122
|
-
throw fileNotFoundError();
|
|
123
|
-
}
|
|
124
115
|
if (isSymlinkOpenError(err)) {
|
|
125
116
|
throw new FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
126
117
|
}
|
|
@@ -131,16 +122,35 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
131
122
|
throw err;
|
|
132
123
|
}
|
|
133
124
|
try {
|
|
125
|
+
await fsSafeTestHooks?.afterOpen?.(filePath, handle);
|
|
134
126
|
const stat = await handle.stat();
|
|
135
127
|
if (!stat.isFile()) {
|
|
136
128
|
throw new FsSafeError("not-file", "not a file");
|
|
137
129
|
}
|
|
138
130
|
// Keep numeric Stats for the public receipt, never for identity verification.
|
|
139
|
-
|
|
131
|
+
let openedIdentity;
|
|
132
|
+
const identity = await inspectFileIdentity(async () => (openedIdentity = await handle.stat({ bigint: true })), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined).catch(async (error) => {
|
|
133
|
+
if ([0, 1].includes(stat.nlink)) {
|
|
134
|
+
await recordPreOpenFileChange(error, handle, filePath, preOpenStat, openedIdentity);
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
});
|
|
140
138
|
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
141
139
|
throw hardlinkedPathNotAllowedError();
|
|
142
140
|
}
|
|
143
|
-
|
|
141
|
+
const inspectPathIdentity = async (inspect) => {
|
|
142
|
+
try {
|
|
143
|
+
return await inspectFileIdentity(inspect, identity);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
const failure = isNotFoundPathError(error) ? openedPathResolutionError(fileNotFoundError()) : error;
|
|
147
|
+
if (stat.nlink <= 1 && (!preOpenStat || preOpenStat.nlink <= 1n)) {
|
|
148
|
+
await recordOpenedFileFailure(failure, handle, filePath, identity);
|
|
149
|
+
}
|
|
150
|
+
throw failure;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
await inspectPathIdentity(async () => {
|
|
144
154
|
const pathStat = options?.symlinks === "follow-within-root"
|
|
145
155
|
? await fs.stat(filePath, { bigint: true })
|
|
146
156
|
: await fs.lstat(filePath, { bigint: true });
|
|
@@ -148,26 +158,26 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
148
158
|
throw new FsSafeError("symlink", "symlink not allowed");
|
|
149
159
|
}
|
|
150
160
|
return pathStat;
|
|
151
|
-
}
|
|
161
|
+
});
|
|
152
162
|
await fsSafeTestHooks?.afterOpenedPathIdentityCheck?.(filePath, handle);
|
|
153
|
-
const realPath = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath)
|
|
154
|
-
|
|
163
|
+
const realPath = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath)
|
|
164
|
+
.catch(async (error) => {
|
|
165
|
+
if (stat.nlink <= 1 && (!preOpenStat || preOpenStat.nlink <= 1n)) {
|
|
166
|
+
await recordOpenedFileFailure(error, handle, filePath, identity);
|
|
167
|
+
}
|
|
168
|
+
throw error;
|
|
169
|
+
});
|
|
170
|
+
await inspectPathIdentity(async () => {
|
|
155
171
|
const realStat = await fs.stat(realPath, { bigint: true });
|
|
156
172
|
if (options?.hardlinks === "reject" && realStat.nlink > 1n) {
|
|
157
173
|
throw hardlinkedPathNotAllowedError();
|
|
158
174
|
}
|
|
159
175
|
return realStat;
|
|
160
|
-
}
|
|
176
|
+
});
|
|
161
177
|
return { opened: openResult({ handle, realPath, stat }), identity };
|
|
162
178
|
}
|
|
163
179
|
catch (err) {
|
|
164
180
|
await handle.close().catch(() => { });
|
|
165
|
-
if (err instanceof FsSafeError) {
|
|
166
|
-
throw err;
|
|
167
|
-
}
|
|
168
|
-
if (isNotFoundPathError(err)) {
|
|
169
|
-
throw fileNotFoundError();
|
|
170
|
-
}
|
|
171
181
|
throw err;
|
|
172
182
|
}
|
|
173
183
|
}
|
|
@@ -396,19 +406,10 @@ async function openFileInRoot(root, params) {
|
|
|
396
406
|
rejectUnsafeDeviceReads: true,
|
|
397
407
|
rejectSymlinks: params.symlinks !== "follow-within-root",
|
|
398
408
|
});
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
symlinks: params.symlinks,
|
|
404
|
-
}));
|
|
405
|
-
}
|
|
406
|
-
catch (err) {
|
|
407
|
-
if (err instanceof FsSafeError) {
|
|
408
|
-
throw err;
|
|
409
|
-
}
|
|
410
|
-
throw err;
|
|
411
|
-
}
|
|
409
|
+
const { opened } = await openVerifiedLocalFile(resolved, {
|
|
410
|
+
nonBlockingRead: params.nonBlockingRead,
|
|
411
|
+
symlinks: params.symlinks,
|
|
412
|
+
});
|
|
412
413
|
if (params.hardlinks !== "allow" && opened.stat.nlink > 1) {
|
|
413
414
|
await opened.handle.close().catch(() => { });
|
|
414
415
|
throw hardlinkedPathNotAllowedError();
|
|
@@ -460,9 +461,7 @@ function emitWriteBoundaryWarning(reason) {
|
|
|
460
461
|
logWarn(`security: fs-safe write boundary warning (${reason})`);
|
|
461
462
|
}
|
|
462
463
|
function buildAtomicWriteTempPath(targetPath) {
|
|
463
|
-
|
|
464
|
-
const base = path.basename(targetPath);
|
|
465
|
-
return path.join(dir, `.${base}.${process.pid}.${randomUUID()}.tmp`);
|
|
464
|
+
return path.join(path.dirname(targetPath), `.fs-safe-${randomUUID()}.tmp`);
|
|
466
465
|
}
|
|
467
466
|
function rootWriteQueueKey(root, relativePath) {
|
|
468
467
|
return `${root.rootReal}\0${relativePath}`;
|
|
@@ -701,7 +700,7 @@ async function writeFileInRoot(root, params) {
|
|
|
701
700
|
await writeFileFallback(root, params);
|
|
702
701
|
return;
|
|
703
702
|
}
|
|
704
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
703
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations, params.overwrite);
|
|
705
704
|
await serializePathWrite(pinned.targetPath, async () => {
|
|
706
705
|
await commitPinnedWriteInRoot(root, pinned, params);
|
|
707
706
|
});
|
|
@@ -823,7 +822,7 @@ async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
|
823
822
|
await fs.rm(targetPath);
|
|
824
823
|
});
|
|
825
824
|
}
|
|
826
|
-
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
|
|
825
|
+
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true) {
|
|
827
826
|
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
828
827
|
relativePath,
|
|
829
828
|
denyMutations,
|
|
@@ -841,22 +840,40 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
|
|
|
841
840
|
if (!basename || basename === "." || basename === "/") {
|
|
842
841
|
throw new FsSafeError("invalid-path", "invalid target path");
|
|
843
842
|
}
|
|
843
|
+
// Exclusive publication cannot inherit an existing file's mode. Keep type
|
|
844
|
+
// and alias checks, but never open a competing owner's record just to reject it.
|
|
845
|
+
if (!overwrite) {
|
|
846
|
+
try {
|
|
847
|
+
const existing = await fs.stat(resolved);
|
|
848
|
+
if (!existing.isFile())
|
|
849
|
+
throw new FsSafeError("not-file", "not a file");
|
|
850
|
+
if (existing.nlink > 1)
|
|
851
|
+
throw hardlinkedPathNotAllowedError();
|
|
852
|
+
throw new FsSafeError("already-exists", "file already exists");
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
if (!isNotFoundPathError(error))
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
844
859
|
let mode = requestedMode ?? 0o600;
|
|
845
860
|
try {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
861
|
+
if (overwrite) {
|
|
862
|
+
const opened = await openFileInRoot(root, {
|
|
863
|
+
relativePath,
|
|
864
|
+
hardlinks: "reject",
|
|
865
|
+
nonBlockingRead: true,
|
|
866
|
+
symlinks: "follow-within-root",
|
|
867
|
+
});
|
|
868
|
+
try {
|
|
869
|
+
mode = requestedMode ?? (opened.stat.mode & 0o777);
|
|
870
|
+
if (!isPathInside(rootWithSep, opened.realPath)) {
|
|
871
|
+
throw outsideWorkspaceError();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
finally {
|
|
875
|
+
await opened.handle.close().catch(() => { });
|
|
856
876
|
}
|
|
857
|
-
}
|
|
858
|
-
finally {
|
|
859
|
-
await opened.handle.close().catch(() => { });
|
|
860
877
|
}
|
|
861
878
|
}
|
|
862
879
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"AAKA,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAS3C,OAAO,EAWL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG5F,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAEhF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;CAClC,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,2BAA2B,IAAI,IAAI,CAAC;IACpC,iBAAiB,CAAC,oBAAoB,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAC1F,eAAe,CACb,oBAAoB,EAAE,MAAM,EAC5B,IAAI,EAAE,eAAe,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB,CAAC;AAaF,wBAAsB,kBAAkB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,OAAO,EAAE,yBAAyB,CAAC,QAAQ,CAAC,EAC5C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CA4S5B"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { readFileHandleBounded } from "./bounded-read.js";
|
|
5
|
+
import { openSidecarRoot } from "./sidecar-lock-root.js";
|
|
4
6
|
import { createNativeExclusiveFile } from "./native-operations.js";
|
|
5
7
|
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
6
|
-
import {
|
|
8
|
+
import { readSidecarLockRawSnapshot, parseSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
|
|
7
9
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
8
10
|
async function resolveNormalizedTargetPath(targetPath) {
|
|
9
11
|
const resolved = path.resolve(targetPath);
|
|
@@ -93,13 +95,14 @@ export async function acquireSidecarLock(options, context) {
|
|
|
93
95
|
let handle = null;
|
|
94
96
|
let createdSnapshot = null;
|
|
95
97
|
let lockFileCreateDenied = false;
|
|
98
|
+
const payload = await options.payload();
|
|
99
|
+
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
96
100
|
try {
|
|
97
|
-
const payload = await options.payload();
|
|
98
|
-
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
99
101
|
if (options.lockRoot) {
|
|
100
|
-
const
|
|
102
|
+
const lockRoot = options.lockRoot;
|
|
103
|
+
const relativeLockPath = relativeSidecarLockPath(lockRoot, lockPath);
|
|
101
104
|
try {
|
|
102
|
-
await
|
|
105
|
+
await lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
|
|
103
106
|
}
|
|
104
107
|
catch (error) {
|
|
105
108
|
if (error instanceof FsSafeError && error.code === "already-exists") {
|
|
@@ -108,7 +111,23 @@ export async function acquireSidecarLock(options, context) {
|
|
|
108
111
|
throw error;
|
|
109
112
|
}
|
|
110
113
|
createdSnapshot = { raw, payload, ownershipToken };
|
|
111
|
-
|
|
114
|
+
const opened = await openSidecarRoot(lockRoot, relativeLockPath, "unlinked");
|
|
115
|
+
if (!opened) {
|
|
116
|
+
await waitForRetry();
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
// Root.open admits the current file, not necessarily the one we created.
|
|
121
|
+
const currentRaw = await readFileHandleBounded(opened.handle, Buffer.byteLength(raw));
|
|
122
|
+
if (!currentRaw.equals(Buffer.from(raw))) {
|
|
123
|
+
throw new FsSafeError("path-mismatch", "created sidecar lock changed before admission");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
await opened.handle.close().catch(() => undefined);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
handle = opened.handle;
|
|
112
131
|
}
|
|
113
132
|
else {
|
|
114
133
|
try {
|
|
@@ -123,6 +142,12 @@ export async function acquireSidecarLock(options, context) {
|
|
|
123
142
|
await handle.writeFile(raw, "utf8");
|
|
124
143
|
}
|
|
125
144
|
const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
|
|
145
|
+
if (snapshot.stat.nlink === 0) {
|
|
146
|
+
await handle.close();
|
|
147
|
+
handle = null;
|
|
148
|
+
await waitForRetry();
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
126
151
|
const createdHeld = {
|
|
127
152
|
refCount: 1,
|
|
128
153
|
reentrantOwner: options.reentrantOwner,
|
|
@@ -167,7 +192,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
167
192
|
catch (err) {
|
|
168
193
|
try {
|
|
169
194
|
if (handle) {
|
|
170
|
-
const failedSnapshot = { payload: null };
|
|
195
|
+
const failedSnapshot = createdSnapshot ?? { payload: null };
|
|
171
196
|
try {
|
|
172
197
|
failedSnapshot.stat = await handle.stat();
|
|
173
198
|
}
|
|
@@ -179,8 +204,8 @@ export async function acquireSidecarLock(options, context) {
|
|
|
179
204
|
context.held.delete(normalizedTargetPath);
|
|
180
205
|
}
|
|
181
206
|
await handle.close().catch(() => undefined);
|
|
182
|
-
//
|
|
183
|
-
//
|
|
207
|
+
// Root-created records retain the creator's byte/token receipt;
|
|
208
|
+
// partial direct writes use the exclusive descriptor's identity.
|
|
184
209
|
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
185
210
|
lockRoot: options.lockRoot,
|
|
186
211
|
parsePayload: options.parsePayload,
|
|
@@ -206,24 +231,29 @@ export async function acquireSidecarLock(options, context) {
|
|
|
206
231
|
if (ownsReclaimGuard) {
|
|
207
232
|
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
208
233
|
ownsReclaimGuard = false;
|
|
234
|
+
await waitForRetry();
|
|
209
235
|
continue;
|
|
210
236
|
}
|
|
211
237
|
const nowMs = Date.now();
|
|
212
|
-
let
|
|
238
|
+
let rawSnapshot;
|
|
239
|
+
let lockFileOpenDenied = false;
|
|
213
240
|
try {
|
|
214
|
-
|
|
241
|
+
rawSnapshot = await readSidecarLockRawSnapshot(lockPath, {
|
|
215
242
|
lockRoot: options.lockRoot,
|
|
216
|
-
parsePayload: options.parsePayload,
|
|
217
243
|
rejectNonFile: true,
|
|
244
|
+
discardObservation: "changed",
|
|
245
|
+
onOpenFailure: (error) => { lockFileOpenDenied = isTransientLockFileDenial(error, lockPath); },
|
|
218
246
|
});
|
|
219
247
|
}
|
|
220
248
|
catch (readErr) {
|
|
221
|
-
if (!
|
|
249
|
+
if (!lockFileOpenDenied || !withinDenialBudget())
|
|
222
250
|
throw readErr;
|
|
223
251
|
await retryOrRethrowDenial(readErr);
|
|
224
252
|
continue;
|
|
225
253
|
}
|
|
254
|
+
const snapshot = parseSidecarLockSnapshot(rawSnapshot, options.parsePayload);
|
|
226
255
|
if (!snapshot) {
|
|
256
|
+
await waitForRetry();
|
|
227
257
|
continue;
|
|
228
258
|
}
|
|
229
259
|
if (context.held.has(normalizedTargetPath)) {
|
|
@@ -243,6 +273,7 @@ export async function acquireSidecarLock(options, context) {
|
|
|
243
273
|
lockRoot: options.lockRoot,
|
|
244
274
|
parsePayload: options.parsePayload,
|
|
245
275
|
}))) {
|
|
276
|
+
await waitForRetry();
|
|
246
277
|
continue;
|
|
247
278
|
}
|
|
248
279
|
const staleRecovery = options.staleRecovery ?? "fail-closed";
|
|
@@ -261,6 +292,8 @@ export async function acquireSidecarLock(options, context) {
|
|
|
261
292
|
parsePayload: options.parsePayload,
|
|
262
293
|
});
|
|
263
294
|
if (removal === "removed" || removal === "changed") {
|
|
295
|
+
if (removal === "changed")
|
|
296
|
+
await waitForRetry();
|
|
264
297
|
continue;
|
|
265
298
|
}
|
|
266
299
|
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
@@ -12,6 +12,10 @@ export type SidecarLockSnapshot = {
|
|
|
12
12
|
stat?: Stats;
|
|
13
13
|
ownershipToken?: string;
|
|
14
14
|
};
|
|
15
|
+
type SidecarLockRawSnapshot = Omit<SidecarLockSnapshot, "payload"> & {
|
|
16
|
+
raw: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function parseSidecarLockSnapshot(snapshot: SidecarLockRawSnapshot | null, parser?: (raw: string) => unknown): SidecarLockSnapshot | null;
|
|
15
19
|
export declare function readSidecarLockOwnershipToken(raw: string): string | undefined;
|
|
16
20
|
export declare function serializeSidecarLockPayload(payload: Record<string, unknown>): {
|
|
17
21
|
raw: string;
|
|
@@ -19,15 +23,21 @@ export declare function serializeSidecarLockPayload(payload: Record<string, unkn
|
|
|
19
23
|
};
|
|
20
24
|
export declare function relativeSidecarLockPath(lockRoot: Root, lockPath: string): string;
|
|
21
25
|
export declare function parseSidecarLockPayload(raw: string, parser?: (raw: string) => unknown): unknown;
|
|
22
|
-
export declare function readSidecarLockSnapshot(lockPath: string, options?: {
|
|
23
|
-
lockRoot?: Root;
|
|
26
|
+
export declare function readSidecarLockSnapshot(lockPath: string, options?: Parameters<typeof readSidecarLockRawSnapshot>[1] & {
|
|
24
27
|
parsePayload?: (raw: string) => unknown;
|
|
28
|
+
}): Promise<SidecarLockSnapshot | null>;
|
|
29
|
+
export declare function readSidecarLockRawSnapshot(lockPath: string, options?: {
|
|
30
|
+
lockRoot?: Root;
|
|
25
31
|
rejectNonFile?: boolean;
|
|
26
32
|
allowDescriptorIdentityDrift?: boolean;
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
discardObservation?: "unlinked" | "changed";
|
|
34
|
+
onOpenFailure?: (error: unknown) => void;
|
|
35
|
+
}): Promise<SidecarLockRawSnapshot | null>;
|
|
36
|
+
export declare function readSidecarLockSnapshotSync(lockPath: string, parsePayload?: (raw: string) => unknown, options?: Parameters<typeof readSidecarLockRawSnapshotSync>[1]): SidecarLockSnapshot | null;
|
|
37
|
+
export declare function readSidecarLockRawSnapshotSync(lockPath: string, options?: {
|
|
29
38
|
rejectNonFile?: boolean;
|
|
30
|
-
|
|
39
|
+
onOpenFailure?: (error: unknown) => void;
|
|
40
|
+
}): SidecarLockRawSnapshot | null;
|
|
31
41
|
export declare function removeSidecarLockIfUnchangedSync(lockPath: string, observed: SidecarLockSnapshot): boolean;
|
|
32
42
|
export declare function sidecarLockSnapshotMatches(current: SidecarLockSnapshot, observed: SidecarLockSnapshot): boolean;
|
|
33
43
|
export declare function removeSidecarLockIfUnchanged(lockPath: string, observed: SidecarLockSnapshot | null, options?: {
|
|
@@ -49,4 +59,5 @@ export declare function removeStaleSidecarLockIfAllowed(params: {
|
|
|
49
59
|
lockRoot?: Root;
|
|
50
60
|
parsePayload?: (raw: string) => unknown;
|
|
51
61
|
}): Promise<"removed" | "changed" | "not-approved">;
|
|
62
|
+
export {};
|
|
52
63
|
//# sourceMappingURL=sidecar-lock-reclaim.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAO7C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,12 +6,16 @@ import { readFileDescriptorBoundedSync, readFileHandleBounded } from "./bounded-
|
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
7
|
import { sameFileIdentity } from "./file-identity.js";
|
|
8
8
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
9
|
+
import { openSidecarRoot } from "./sidecar-lock-root.js";
|
|
9
10
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
10
11
|
const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
|
|
11
12
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
|
|
12
13
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
|
|
13
14
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX = "\t".repeat(8);
|
|
14
15
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_PATTERN = new RegExp(`\\n(${SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX}[ \\t]{${SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS}})\\n$`);
|
|
16
|
+
export function parseSidecarLockSnapshot(snapshot, parser) {
|
|
17
|
+
return snapshot && { ...snapshot, payload: parseSidecarLockPayload(snapshot.raw, parser) };
|
|
18
|
+
}
|
|
15
19
|
function createSidecarLockOwnershipToken() {
|
|
16
20
|
let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
|
|
17
21
|
for (const byte of randomBytes(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
|
|
@@ -56,34 +60,27 @@ export function parseSidecarLockPayload(raw, parser) {
|
|
|
56
60
|
return null;
|
|
57
61
|
}
|
|
58
62
|
}
|
|
63
|
+
function missingSnapshotPath(error) {
|
|
64
|
+
if (error.code === "ENOENT")
|
|
65
|
+
return null;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
59
68
|
export async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
69
|
+
return parseSidecarLockSnapshot(await readSidecarLockRawSnapshot(lockPath, options), options.parsePayload);
|
|
70
|
+
}
|
|
71
|
+
export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
|
|
60
72
|
let handle;
|
|
61
73
|
try {
|
|
62
74
|
if (options.lockRoot) {
|
|
63
75
|
const lockRoot = options.lockRoot;
|
|
64
76
|
const relative = relativeSidecarLockPath(lockRoot, lockPath);
|
|
65
|
-
const opened = await lockRoot
|
|
66
|
-
if (error instanceof FsSafeError && error.code === "path-mismatch") {
|
|
67
|
-
// An owner can unlink after open. Prove absence through the same root;
|
|
68
|
-
// acquisition retries create-only, so a later replacement stays guarded.
|
|
69
|
-
try {
|
|
70
|
-
await lockRoot.stat(relative);
|
|
71
|
-
}
|
|
72
|
-
catch (probeError) {
|
|
73
|
-
if (probeError instanceof FsSafeError && probeError.code === "not-found") {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
throw error;
|
|
79
|
-
});
|
|
77
|
+
const opened = await openSidecarRoot(lockRoot, relative, options.discardObservation, options.onOpenFailure);
|
|
80
78
|
if (!opened)
|
|
81
79
|
return null;
|
|
82
80
|
try {
|
|
83
81
|
const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
84
82
|
return {
|
|
85
83
|
raw,
|
|
86
|
-
payload: parseSidecarLockPayload(raw, options.parsePayload),
|
|
87
84
|
stat: opened.stat,
|
|
88
85
|
};
|
|
89
86
|
}
|
|
@@ -91,7 +88,9 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
|
91
88
|
await opened.handle.close().catch(() => undefined);
|
|
92
89
|
}
|
|
93
90
|
}
|
|
94
|
-
const before = await fs.lstat(lockPath);
|
|
91
|
+
const before = await fs.lstat(lockPath).catch(missingSnapshotPath);
|
|
92
|
+
if (!before)
|
|
93
|
+
return null;
|
|
95
94
|
if (!before.isFile() || before.isSymbolicLink()) {
|
|
96
95
|
if (options.rejectNonFile) {
|
|
97
96
|
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
@@ -108,11 +107,14 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
|
108
107
|
(typeof fsSync.constants.O_NONBLOCK === "number" ? fsSync.constants.O_NONBLOCK : 0));
|
|
109
108
|
}
|
|
110
109
|
catch (error) {
|
|
110
|
+
if (error.code === "ENOENT")
|
|
111
|
+
return null;
|
|
111
112
|
if (options.rejectNonFile && error.code === "ELOOP") {
|
|
112
113
|
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`, {
|
|
113
114
|
cause: error,
|
|
114
115
|
});
|
|
115
116
|
}
|
|
117
|
+
options.onOpenFailure?.(error);
|
|
116
118
|
throw error;
|
|
117
119
|
}
|
|
118
120
|
const opened = await handle.stat();
|
|
@@ -125,33 +127,47 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
|
125
127
|
if (!options.allowDescriptorIdentityDrift && !sameFileIdentity(before, opened))
|
|
126
128
|
return null;
|
|
127
129
|
const raw = (await readFileHandleBounded(handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
128
|
-
const after = await fs.lstat(lockPath);
|
|
129
|
-
if (!after.isFile() || !sameFileIdentity(before, after))
|
|
130
|
-
return null;
|
|
131
|
-
return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat: after };
|
|
132
|
-
}
|
|
133
|
-
catch (err) {
|
|
134
|
-
if (err.code === "ENOENT" ||
|
|
135
|
-
(err instanceof FsSafeError && err.code === "not-found")) {
|
|
130
|
+
const after = await fs.lstat(lockPath).catch(missingSnapshotPath);
|
|
131
|
+
if (!after || !after.isFile() || !sameFileIdentity(before, after))
|
|
136
132
|
return null;
|
|
137
|
-
}
|
|
138
|
-
throw err;
|
|
133
|
+
return { raw, stat: after };
|
|
139
134
|
}
|
|
140
135
|
finally {
|
|
141
136
|
await handle?.close().catch(() => undefined);
|
|
142
137
|
}
|
|
143
138
|
}
|
|
139
|
+
function lstatSidecarLockSync(lockPath) {
|
|
140
|
+
try {
|
|
141
|
+
return fsSync.lstatSync(lockPath);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return missingSnapshotPath(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
144
147
|
export function readSidecarLockSnapshotSync(lockPath, parsePayload, options = {}) {
|
|
148
|
+
return parseSidecarLockSnapshot(readSidecarLockRawSnapshotSync(lockPath, options), parsePayload);
|
|
149
|
+
}
|
|
150
|
+
export function readSidecarLockRawSnapshotSync(lockPath, options = {}) {
|
|
145
151
|
let fd;
|
|
146
152
|
try {
|
|
147
|
-
const before =
|
|
153
|
+
const before = lstatSidecarLockSync(lockPath);
|
|
154
|
+
if (!before)
|
|
155
|
+
return null;
|
|
148
156
|
if (!before.isFile() || before.isSymbolicLink()) {
|
|
149
157
|
if (options.rejectNonFile) {
|
|
150
158
|
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
151
159
|
}
|
|
152
160
|
return null;
|
|
153
161
|
}
|
|
154
|
-
|
|
162
|
+
try {
|
|
163
|
+
fd = fsSync.openSync(lockPath, resolveReadOpenFlags());
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error.code === "ENOENT")
|
|
167
|
+
return null;
|
|
168
|
+
options.onOpenFailure?.(error);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
155
171
|
const opened = fsSync.fstatSync(fd);
|
|
156
172
|
if (!opened.isFile()) {
|
|
157
173
|
if (options.rejectNonFile) {
|
|
@@ -160,21 +176,15 @@ export function readSidecarLockSnapshotSync(lockPath, parsePayload, options = {}
|
|
|
160
176
|
return null;
|
|
161
177
|
}
|
|
162
178
|
const raw = readFileDescriptorBoundedSync(fd, MAX_LOCK_PAYLOAD_BYTES).toString("utf8");
|
|
163
|
-
const after =
|
|
164
|
-
if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
|
|
179
|
+
const after = lstatSidecarLockSync(lockPath);
|
|
180
|
+
if (!after || !sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
|
|
165
181
|
return null;
|
|
166
182
|
return {
|
|
167
183
|
raw,
|
|
168
|
-
payload: parseSidecarLockPayload(raw, parsePayload),
|
|
169
184
|
stat: after,
|
|
170
185
|
ownershipToken: readSidecarLockOwnershipToken(raw),
|
|
171
186
|
};
|
|
172
187
|
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
if (error.code === "ENOENT")
|
|
175
|
-
return null;
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
178
188
|
finally {
|
|
179
189
|
if (fd !== undefined)
|
|
180
190
|
fsSync.closeSync(fd);
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { OpenResult, Root } from "./root-impl.js";
|
|
2
|
+
export declare function openSidecarRoot(lockRoot: Root, relative: string, discardObservation?: "unlinked" | "changed", onOpenFailure?: (error: unknown) => void): Promise<OpenResult | null>;
|
|
3
|
+
//# sourceMappingURL=sidecar-lock-root.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar-lock-root.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-root.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAGvD,wBAAsB,eAAe,CACnC,QAAQ,EAAE,IAAI,EACd,QAAQ,EAAE,MAAM,EAChB,kBAAkB,CAAC,EAAE,UAAU,GAAG,SAAS,EAC3C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACvC,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAuD5B"}
|