@openclaw/fs-safe 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
- const openFlags = options?.symlinks === "follow-within-root"
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
- const identity = await inspectFileIdentity(() => handle.stat({ bigint: true }), preOpenStat && !preOpenStat.isSymbolicLink() ? preOpenStat : undefined);
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
- await inspectFileIdentity(async () => {
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
- }, identity);
161
+ });
152
162
  await fsSafeTestHooks?.afterOpenedPathIdentityCheck?.(filePath, handle);
153
- const realPath = await resolveOpenedFileRealPathForFd(handle.fd, identity, filePath);
154
- await inspectFileIdentity(async () => {
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
- }, identity);
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
- let opened;
400
- try {
401
- ({ opened } = await openVerifiedLocalFile(resolved, {
402
- nonBlockingRead: params.nonBlockingRead,
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
- const dir = path.dirname(targetPath);
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
- const opened = await openFileInRoot(root, {
847
- relativePath,
848
- hardlinks: "reject",
849
- nonBlockingRead: true,
850
- symlinks: "follow-within-root",
851
- });
852
- try {
853
- mode = requestedMode ?? (opened.stat.mode & 0o777);
854
- if (!isPathInside(rootWithSep, opened.realPath)) {
855
- throw outsideWorkspaceError();
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":"AAGA,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAS3C,OAAO,EAUL,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,CA+Q5B"}
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 { readSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
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 relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
102
+ const lockRoot = options.lockRoot;
103
+ const relativeLockPath = relativeSidecarLockPath(lockRoot, lockPath);
101
104
  try {
102
- await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
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
- handle = (await options.lockRoot.open(relativeLockPath)).handle;
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
- // The file may be empty or partial JSON, so remove by the identity
183
- // captured from our exclusive handle rather than by pathname alone.
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 snapshot;
238
+ let rawSnapshot;
239
+ let lockFileOpenDenied = false;
213
240
  try {
214
- snapshot = await readSidecarLockSnapshot(lockPath, {
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 (!isTransientLockFileDenial(readErr, lockPath) || !withinDenialBudget())
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
- }): Promise<SidecarLockSnapshot | null>;
28
- export declare function readSidecarLockSnapshotSync(lockPath: string, parsePayload?: (raw: string) => unknown, options?: {
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
- }): SidecarLockSnapshot | null;
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":"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"}
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.open(relative).catch(async (error) => {
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 = fsSync.lstatSync(lockPath);
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
- fd = fsSync.openSync(lockPath, resolveReadOpenFlags());
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 = fsSync.lstatSync(lockPath);
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"}