@openclaw/crabline 0.1.14 → 0.1.15

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.
@@ -4,6 +4,7 @@ import { constants as fsConstants, readFileSync } from "node:fs";
4
4
  import fs, {} from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { performance } from "node:perf_hooks";
7
+ import { setTimeout as delay } from "node:timers/promises";
7
8
  import { promisify } from "node:util";
8
9
  import { applyOwnerOnlyWindowsDirectoryAcl as applyOwnerOnlyWindowsDirectoryAclByHandle, createOwnerOnlyWindowsDirectoryAncestry as createOwnerOnlyWindowsDirectoryAncestryByHandle, } from "../platform/windows-acl.js";
9
10
  const execFileAsync = promisify(execFile);
@@ -1101,6 +1102,38 @@ const PRIVATE_MUTATION_CLAIM_ROOT_FILE = ".crabline-private-mutation.claim";
1101
1102
  const PRIVATE_MUTATION_CLAIM_OWNER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
1102
1103
  const PRIVATE_MUTATION_CLAIM_METADATA_MAX_BYTES = 4096;
1103
1104
  const PRIVATE_MUTATION_RESERVED_BASENAME_PATTERN = /^\.crabline-private-mutation(?:\.|$)/iu;
1105
+ const PRIVATE_MUTATION_CLAIM_WAIT_TIMEOUT_MS = 60_000;
1106
+ const PRIVATE_MUTATION_CLAIM_RETRY_DELAY_MS = 50;
1107
+ function preparePrivateMutationClaimWait(options) {
1108
+ const timeoutMs = options.timeoutMs ?? PRIVATE_MUTATION_CLAIM_WAIT_TIMEOUT_MS;
1109
+ const retryDelayMs = options.retryDelayMs ?? PRIVATE_MUTATION_CLAIM_RETRY_DELAY_MS;
1110
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0) {
1111
+ throw new Error("Private mutation claim wait timeout must be a non-negative safe integer.");
1112
+ }
1113
+ if (!Number.isSafeInteger(retryDelayMs) || retryDelayMs <= 0) {
1114
+ throw new Error("Private mutation claim retry delay must be a positive safe integer.");
1115
+ }
1116
+ const now = options.now ?? performance.now.bind(performance);
1117
+ const sleep = options.sleep ??
1118
+ ((delayMs, signal) => delay(delayMs, undefined, { signal }));
1119
+ if (typeof now !== "function" || typeof sleep !== "function") {
1120
+ throw new Error("Private mutation claim wait callbacks must be functions.");
1121
+ }
1122
+ if (options.signal !== undefined && typeof options.signal.throwIfAborted !== "function") {
1123
+ throw new Error("Private mutation claim wait signal is invalid.");
1124
+ }
1125
+ options.signal?.throwIfAborted();
1126
+ if (!Number.isFinite(now())) {
1127
+ throw new Error("Private mutation claim wait clock is invalid.");
1128
+ }
1129
+ return {
1130
+ now,
1131
+ retryDelayMs,
1132
+ ...(options.signal ? { signal: options.signal } : {}),
1133
+ sleep,
1134
+ timeoutMs,
1135
+ };
1136
+ }
1104
1137
  function assertNotReservedPrivateMutationPath(targetPath) {
1105
1138
  if (PRIVATE_MUTATION_RESERVED_BASENAME_PATTERN.test(path.basename(targetPath))) {
1106
1139
  throw new Error("Private path uses Crabline's reserved mutation claim namespace.");
@@ -1345,6 +1378,11 @@ class UnsupportedPrivateMutationHardLinkError extends Error {
1345
1378
  super("Private mutation claims require a non-hard-link fallback.", { cause });
1346
1379
  }
1347
1380
  }
1381
+ class ActivePrivateMutationClaimError extends Error {
1382
+ constructor(cause) {
1383
+ super("Private path mutation is already claimed.", { cause });
1384
+ }
1385
+ }
1348
1386
  function isUnsupportedHardLinkError(error) {
1349
1387
  const code = error.code;
1350
1388
  return code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "ENOSYS";
@@ -1426,15 +1464,15 @@ async function acquireHardLinkPrivateMutationClaim(parent, options) {
1426
1464
  if (runtime.isProcessAlive(observed.owner.pid)) {
1427
1465
  if (observed.owner.pid === runtime.pid &&
1428
1466
  observed.owner.processStartedAtMs === runtime.processStartedAtMs) {
1429
- throw new Error("Private path mutation is already claimed.", { cause: error });
1467
+ throw new ActivePrivateMutationClaimError(error);
1430
1468
  }
1431
1469
  else if (observed.owner.pid !== runtime.pid) {
1432
1470
  if (observed.owner.processIdentity === undefined) {
1433
- throw new Error("Private path mutation is already claimed.", { cause: error });
1471
+ throw new ActivePrivateMutationClaimError(error);
1434
1472
  }
1435
1473
  const actualIdentity = runtime.getProcessIdentity(observed.owner.pid);
1436
1474
  if (actualIdentity === null || actualIdentity === observed.owner.processIdentity) {
1437
- throw new Error("Private path mutation is already claimed.", { cause: error });
1475
+ throw new ActivePrivateMutationClaimError(error);
1438
1476
  }
1439
1477
  }
1440
1478
  }
@@ -1796,7 +1834,7 @@ async function acquireDirectoryPrivateMutationClaim(parent, options) {
1796
1834
  throw error;
1797
1835
  }
1798
1836
  if (privateMutationClaimOwnerIsActive(observed.metadata.owner, runtime)) {
1799
- throw new Error("Private path mutation is already claimed.", { cause: error });
1837
+ throw new ActivePrivateMutationClaimError(error);
1800
1838
  }
1801
1839
  await parent.assertIdentityAt();
1802
1840
  const revalidated = await readPrivateMutationDirectoryClaim(claimPath);
@@ -2131,8 +2169,7 @@ async function ownerOnlyPrivateClaimAncestry(leaf, platform) {
2131
2169
  async function acquirePrivateMutationClaimChain(leaf, options) {
2132
2170
  const platform = options.platform ?? process.platform;
2133
2171
  const ancestry = await ownerOnlyPrivateClaimAncestry(leaf, platform);
2134
- const highestClaimDirectory = ancestry.at(-1);
2135
- const outerBoundary = await captureSafePrivateDirectoryMutationParent(highestClaimDirectory.directoryPath, platform);
2172
+ const outerBoundary = await captureSafePrivateDirectoryMutationParent(ancestry.at(-1).directoryPath, platform);
2136
2173
  const claims = [];
2137
2174
  let leafClaim;
2138
2175
  try {
@@ -2239,6 +2276,56 @@ async function acquirePrivateMutationClaimChain(leaf, options) {
2239
2276
  },
2240
2277
  };
2241
2278
  }
2279
+ function privateMutationClaimTimeoutError(timeoutMs, cause) {
2280
+ return Object.assign(new Error(`Timed out after ${timeoutMs}ms waiting for a private mutation claim.`, { cause }), { code: "ETIMEDOUT" });
2281
+ }
2282
+ async function releaseClaimAfterWaitFailure(claim, error) {
2283
+ try {
2284
+ await claim.release();
2285
+ }
2286
+ catch (releaseError) {
2287
+ const aggregateError = new AggregateError([error, releaseError], "Private mutation claim wait failed and its acquired claim could not be released.");
2288
+ aggregateError.cause = error;
2289
+ throw aggregateError;
2290
+ }
2291
+ throw error;
2292
+ }
2293
+ async function acquirePrivateMutationClaimWithWait(acquire, wait) {
2294
+ if (wait === undefined) {
2295
+ return await acquire();
2296
+ }
2297
+ const deadlineMs = wait.now() + wait.timeoutMs;
2298
+ let lastContentionError;
2299
+ for (;;) {
2300
+ wait.signal?.throwIfAborted();
2301
+ let claim;
2302
+ try {
2303
+ claim = await acquire();
2304
+ }
2305
+ catch (error) {
2306
+ if (!(error instanceof ActivePrivateMutationClaimError) || wait.timeoutMs === 0) {
2307
+ throw error;
2308
+ }
2309
+ lastContentionError = error;
2310
+ const remainingMs = deadlineMs - wait.now();
2311
+ if (remainingMs <= 0) {
2312
+ throw privateMutationClaimTimeoutError(wait.timeoutMs, error);
2313
+ }
2314
+ await wait.sleep(Math.min(wait.retryDelayMs, remainingMs), wait.signal);
2315
+ continue;
2316
+ }
2317
+ try {
2318
+ wait.signal?.throwIfAborted();
2319
+ if (lastContentionError && wait.now() >= deadlineMs) {
2320
+ throw privateMutationClaimTimeoutError(wait.timeoutMs, lastContentionError);
2321
+ }
2322
+ }
2323
+ catch (error) {
2324
+ return await releaseClaimAfterWaitFailure(claim, error);
2325
+ }
2326
+ return claim;
2327
+ }
2328
+ }
2242
2329
  async function secureOwnerOnlyMutationParent(directoryPath) {
2243
2330
  return await securePrivateDirectory(directoryPath, {
2244
2331
  markMutationRoot: false,
@@ -2247,6 +2334,7 @@ async function secureOwnerOnlyMutationParent(directoryPath) {
2247
2334
  }
2248
2335
  export async function publishPrivateFileAtomically(filePath, contents, options = {}) {
2249
2336
  assertNotReservedPrivateMutationPath(filePath);
2337
+ const claimWait = preparePrivateMutationClaimWait(options.claimWait ?? {});
2250
2338
  const temporaryPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
2251
2339
  const parentDirectory = path.resolve(path.dirname(filePath));
2252
2340
  const platform = options.platform ?? process.platform;
@@ -2285,11 +2373,14 @@ export async function publishPrivateFileAtomically(filePath, contents, options =
2285
2373
  await migrationBoundary?.assertIdentityAt();
2286
2374
  const parent = await secureOwnerOnlyMutationParent(parentDirectory);
2287
2375
  await migrationBoundary?.assertIdentityAt();
2288
- const claim = await acquirePrivateMutationClaimChain(parent, {
2376
+ const claimOptions = {
2289
2377
  ...(options.claimRuntime ? { runtime: options.claimRuntime } : {}),
2290
2378
  ...(options.createWindowsFile ? { createWindowsFile: options.createWindowsFile } : {}),
2291
2379
  ...(options.platform ? { platform: options.platform } : {}),
2292
- });
2380
+ };
2381
+ // The full ancestry fence stays held for the temporary file's lifetime so supported removals
2382
+ // cannot quarantine a secret. Active publishers wait here without serializing their whole flow.
2383
+ const claim = await acquirePrivateMutationClaimWithWait(() => acquirePrivateMutationClaimChain(parent, claimOptions), claimWait);
2293
2384
  let handle;
2294
2385
  let identity;
2295
2386
  let publicationFailed = false;