@cortexkit/anthropic-auth-core 2.4.10 → 2.4.11

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.
@@ -105,22 +105,34 @@ export interface HolderLiveness {
105
105
  pidAlive(pid: number): boolean;
106
106
  /** Undefined or null when it can't be read. */
107
107
  incarnation(pid: number): Promise<string | null>;
108
+ /** Wall clock and OS uptime; tests replace both. */
109
+ now?(): number;
110
+ uptime?(): number;
108
111
  }
109
112
  /** The OS's fixed per-process value for `pid`, or null when none is read. */
110
113
  export declare function processIncarnation(pid: number): Promise<string | null>;
111
114
  /**
112
- * Whether an entry's process may still be running. A dead pid is stale. A
113
- * live pid is stale only when the entry recorded an incarnation and the OS
114
- * now reports a different one; an unreadable incarnation counts as alive.
115
- * So pid reuse can keep a stale entry, but never clears a live one.
115
+ * A process recorded before this boot cannot still be running, even if its
116
+ * pid is alive again. Keep a five-minute margin for clock imprecision, and
117
+ * keep blocking when the timestamp is missing, invalid or in the future.
116
118
  */
117
- export declare function holderEntryIsLive(entry: Pick<HolderEntry, 'pid' | 'incarnation'>, liveness?: HolderLiveness): Promise<boolean>;
119
+ export declare function recordPredatesBoot(startedAt: unknown, clock?: Pick<HolderLiveness, 'now' | 'uptime'>): boolean;
120
+ /**
121
+ * Whether an entry's process may still be running. An entry from before
122
+ * this boot or a dead pid is stale. Otherwise an unreadable incarnation
123
+ * counts as alive; only a different incarnation clears a live pid.
124
+ */
125
+ export declare function holderEntryIsLive(entry: Pick<HolderEntry, 'pid' | 'incarnation'> & {
126
+ registeredAt?: unknown;
127
+ }, liveness?: HolderLiveness): Promise<boolean>;
118
128
  export interface HolderFileContents {
119
129
  /** Entries as found, including any this version doesn't understand. */
120
130
  entries: unknown[];
121
131
  }
122
- /** An entry's pid and incarnation when it has a usable shape. */
123
- export declare function holderEntryLiveness(value: unknown): Pick<HolderEntry, 'pid' | 'incarnation'> | undefined;
132
+ /** An entry's liveness fields when its pid and incarnation have a usable shape. */
133
+ export declare function holderEntryLiveness(value: unknown): (Pick<HolderEntry, 'pid' | 'incarnation'> & {
134
+ registeredAt?: unknown;
135
+ }) | undefined;
124
136
  /**
125
137
  * Read a holder file. Undefined when it is absent. A file that exists but
126
138
  * doesn't parse throws: rewriting it would erase other processes' entries.
@@ -185,10 +197,10 @@ export interface NativeHolderRegistration {
185
197
  */
186
198
  export declare function ensureHolderDirectory(dir: string, ownerOnly?: OwnerOnlyOptions): Promise<void>;
187
199
  /**
188
- * Add `entry` to `holderFile`, pruning entries of dead processes, while the
189
- * caller holds that file's `holders` lock. Writes nothing when a live entry
190
- * for this process, kind and files is already there. A holder file that
191
- * doesn't parse throws and is never overwritten.
200
+ * Add `entry` to `holderFile`, pruning stale entries, while the caller holds
201
+ * that file's `holders` lock. Writes nothing when nothing was pruned and a
202
+ * live entry for this process, kind and files is already there. A holder
203
+ * file that doesn't parse throws and is never overwritten.
192
204
  */
193
205
  export declare function publishHolderEntryLocked(holderFile: string, entry: HolderEntry, options: {
194
206
  liveness?: HolderLiveness;
package/dist/index.d.ts CHANGED
@@ -87,6 +87,7 @@ export * from './quota-manager.ts';
87
87
  export * from './quotas.ts';
88
88
  export * from './relay.ts';
89
89
  export * from './relocation-guard.ts';
90
+ export * from './request-abort.ts';
90
91
  export * from './request-gzip.ts';
91
92
  export * from './request-history.ts';
92
93
  export * from './routing.ts';
package/dist/index.js CHANGED
@@ -4327,6 +4327,73 @@ async function generatePKCE() {
4327
4327
  };
4328
4328
  }
4329
4329
 
4330
+ // src/request-abort.ts
4331
+ import { AsyncLocalStorage } from "node:async_hooks";
4332
+ function raceWithSignal(signal, operation) {
4333
+ if (!signal)
4334
+ return operation();
4335
+ if (signal.aborted)
4336
+ return Promise.reject(signal.reason);
4337
+ return new Promise((resolve5, reject) => {
4338
+ const aborted = () => reject(signal.reason);
4339
+ signal.addEventListener("abort", aborted, { once: true });
4340
+ const finish = () => signal.removeEventListener("abort", aborted);
4341
+ Promise.resolve().then(() => {
4342
+ signal.throwIfAborted();
4343
+ return operation();
4344
+ }).then((value) => {
4345
+ finish();
4346
+ if (signal.aborted)
4347
+ reject(signal.reason);
4348
+ else
4349
+ resolve5(value);
4350
+ }, (error) => {
4351
+ finish();
4352
+ reject(signal.aborted ? signal.reason : error);
4353
+ });
4354
+ });
4355
+ }
4356
+ function abortableFetch(send) {
4357
+ return (input, init) => {
4358
+ const signal = init?.signal !== undefined ? init.signal : input instanceof Request ? input.signal : undefined;
4359
+ return runWithRequestSignal(signal, () => send(input, init));
4360
+ };
4361
+ }
4362
+ function withAbortableFetch(options) {
4363
+ return { ...options, fetch: abortableFetch(options.fetch) };
4364
+ }
4365
+ var AUXILIARY_REQUEST_TIMEOUT_MS = 30000;
4366
+ function auxiliaryRequestSignal(signal) {
4367
+ const deadline = AbortSignal.timeout(AUXILIARY_REQUEST_TIMEOUT_MS);
4368
+ return signal ? AbortSignal.any([signal, deadline]) : deadline;
4369
+ }
4370
+ var requests = new AsyncLocalStorage;
4371
+ function requestSignal() {
4372
+ return requests.getStore()?.signal;
4373
+ }
4374
+ function commitRequestChange(commit) {
4375
+ const request = requests.getStore();
4376
+ request?.signal?.throwIfAborted();
4377
+ if (request)
4378
+ request.commits.push(commit);
4379
+ else
4380
+ commit();
4381
+ }
4382
+ function runWithRequestSignal(signal, operation) {
4383
+ const request = { signal, commits: [] };
4384
+ return raceWithSignal(signal, () => requests.run(request, async () => {
4385
+ try {
4386
+ const result = await operation();
4387
+ signal?.throwIfAborted();
4388
+ for (const commit of request.commits)
4389
+ commit();
4390
+ return result;
4391
+ } finally {
4392
+ request.commits.length = 0;
4393
+ }
4394
+ }));
4395
+ }
4396
+
4330
4397
  // src/auth.ts
4331
4398
  function parseRetryAfterHeader(value) {
4332
4399
  if (!value)
@@ -4370,7 +4437,9 @@ async function refreshClaudeOAuthToken(input) {
4370
4437
  const delay3 = baseDelayMs * 2 ** (attempt - 1);
4371
4438
  await new Promise((resolve5) => setTimeoutImpl(resolve5, delay3));
4372
4439
  }
4373
- const response = await fetchImpl(TOKEN_URL, {
4440
+ const signal = auxiliaryRequestSignal();
4441
+ const response = await raceWithSignal(signal, () => fetchImpl(TOKEN_URL, {
4442
+ signal,
4374
4443
  method: "POST",
4375
4444
  redirect: "error",
4376
4445
  headers: {
@@ -4384,16 +4453,16 @@ async function refreshClaudeOAuthToken(input) {
4384
4453
  client_id: CLIENT_ID,
4385
4454
  scope: REFRESH_SCOPE
4386
4455
  })
4387
- });
4456
+ }));
4388
4457
  if (!response.ok) {
4389
4458
  if (response.status >= 500 && attempt < maxRetries) {
4390
- await response.body?.cancel().catch(() => {});
4459
+ await raceWithSignal(signal, async () => response.body?.cancel()).catch(() => {});
4391
4460
  continue;
4392
4461
  }
4393
- const body = await response.text().catch(() => "");
4462
+ const body = await raceWithSignal(signal, () => response.text()).catch(() => "");
4394
4463
  throw new ClaudeOAuthRefreshError(response.status, body, response.headers.get("retry-after"));
4395
4464
  }
4396
- const json = await response.json();
4465
+ const json = await raceWithSignal(signal, () => response.json());
4397
4466
  const refreshedAt = input.now?.() ?? Date.now();
4398
4467
  return {
4399
4468
  access: json.access_token,
@@ -5368,8 +5437,10 @@ function mapUsageWindow(window, checkedAt) {
5368
5437
  async function fetchOAuthQuotaSnapshot(input) {
5369
5438
  assertNotCustodyTombstone(input.accessToken, "anthropic");
5370
5439
  const fetchImpl = input.fetchImpl ?? fetch;
5371
- const response = await fetchImpl(QUOTA_URL, {
5440
+ const signal = auxiliaryRequestSignal();
5441
+ const response = await raceWithSignal(signal, () => fetchImpl(QUOTA_URL, {
5372
5442
  method: "GET",
5443
+ signal,
5373
5444
  redirect: "error",
5374
5445
  headers: {
5375
5446
  Authorization: `Bearer ${input.accessToken}`,
@@ -5378,9 +5449,9 @@ async function fetchOAuthQuotaSnapshot(input) {
5378
5449
  "anthropic-beta": "oauth-2025-04-20",
5379
5450
  "User-Agent": `claude-code/${CLAUDE_CODE_VERSION}`
5380
5451
  }
5381
- });
5452
+ }));
5382
5453
  if (!response.ok) {
5383
- const body = await response.text().catch(() => "");
5454
+ const body = await raceWithSignal(signal, () => response.text()).catch(() => "");
5384
5455
  const error = Object.assign(new Error(`Claude quota check failed: ${response.status} — ${body}`), {
5385
5456
  status: response.status,
5386
5457
  retryAfter: parseRetryAfterHeader(response.headers.get("Retry-After"))
@@ -5388,7 +5459,7 @@ async function fetchOAuthQuotaSnapshot(input) {
5388
5459
  throw error;
5389
5460
  }
5390
5461
  const checkedAt = input.now?.() ?? Date.now();
5391
- const usage = await response.json();
5462
+ const usage = await raceWithSignal(signal, () => response.json());
5392
5463
  const bindingWindow = mapBindingWindow(usage.limits);
5393
5464
  const snapshot = {
5394
5465
  five_hour: mapUsageWindow(usage.five_hour, checkedAt),
@@ -15013,11 +15084,12 @@ import { execFile } from "node:child_process";
15013
15084
  import { randomUUID as randomUUID14 } from "node:crypto";
15014
15085
  import { readFileSync as readFileSync2 } from "node:fs";
15015
15086
  import { lstat as lstat12, mkdir as mkdir10, readFile as readFile11 } from "node:fs/promises";
15087
+ import { uptime } from "node:os";
15016
15088
  import { basename as basename9, dirname as dirname17, join as join20 } from "node:path";
15017
15089
 
15018
15090
  // src/native-lock-observer.ts
15019
- import { AsyncLocalStorage } from "node:async_hooks";
15020
- var detachedLocks = new AsyncLocalStorage;
15091
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
15092
+ var detachedLocks = new AsyncLocalStorage2;
15021
15093
  function withDetachedNativeLocks(work) {
15022
15094
  return detachedLocks.run(true, work);
15023
15095
  }
@@ -15149,8 +15221,8 @@ function recordIdentityIn(tx, id, identity2) {
15149
15221
  import { setTimeout as snapshotDelay } from "node:timers/promises";
15150
15222
 
15151
15223
  // ../../node_modules/@cortexkit/common-auth/dist/store/hooks.js
15152
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
15153
- var insideHook = new AsyncLocalStorage2;
15224
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
15225
+ var insideHook = new AsyncLocalStorage3;
15154
15226
  function assertNotInsideHook(operation) {
15155
15227
  if (insideHook.getStore() !== undefined)
15156
15228
  throw new PoolReentryError(operation);
@@ -21408,11 +21480,13 @@ class StickySessionRouter {
21408
21480
  })).sort((left, right) => left.score - right.score || left.candidate.order - right.candidate.order || left.candidate.accountId.localeCompare(right.candidate.accountId))[0]?.candidate;
21409
21481
  }
21410
21482
  async resolve(input) {
21483
+ requestSignal()?.throwIfAborted();
21411
21484
  if (!input.sessionId)
21412
21485
  return null;
21413
21486
  this.recoverLocalPins();
21414
21487
  await this.prepareLegacySeed();
21415
21488
  await this.refreshStateIfChanged().catch(() => {});
21489
+ requestSignal()?.throwIfAborted();
21416
21490
  const key = sessionKey(input.sessionId);
21417
21491
  const affinityModelId = input.affinityModelId ?? input.modelId;
21418
21492
  const matchesAffinity = (assignment2) => affinityModelId !== undefined ? assignment2.affinityModelId === affinityModelId : assignment2.family === input.family;
@@ -21429,10 +21503,12 @@ class StickySessionRouter {
21429
21503
  const cached = this.state.assignments[key];
21430
21504
  if (cached && this.assignmentIsActive(cached) && matchesAffinity(cached) && matchesAccountIdentity(cached) && input.retainAccountIds.has(this.logicalAccountId(cached)) && !input.excludeAccountIds?.has(this.logicalAccountId(cached))) {
21431
21505
  if (this.now() - cached.lastSeenAt >= STICKY_ROUTING_TOUCH_INTERVAL_MS) {
21432
- cached.lastSeenAt = this.now();
21433
- this.scheduleTouch(key);
21506
+ commitRequestChange(() => {
21507
+ cached.lastSeenAt = this.now();
21508
+ this.scheduleTouch(key);
21509
+ });
21434
21510
  }
21435
- this.retryChange(key);
21511
+ commitRequestChange(() => this.retryChange(key));
21436
21512
  return {
21437
21513
  accountId: this.logicalAccountId(cached),
21438
21514
  assignment: { ...cached },
@@ -21442,7 +21518,7 @@ class StickySessionRouter {
21442
21518
  }
21443
21519
  const state = structuredClone(this.state);
21444
21520
  const save = (operation) => {
21445
- this.scheduleChange(key, state.assignments[key], operation);
21521
+ commitRequestChange(() => this.scheduleChange(key, state.assignments[key], operation));
21446
21522
  };
21447
21523
  this.prune(state);
21448
21524
  const current = state.assignments[key];
@@ -22502,7 +22578,16 @@ var defaultLiveness = {
22502
22578
  pidAlive,
22503
22579
  incarnation: processIncarnation
22504
22580
  };
22581
+ function recordPredatesBoot(startedAt, clock = {}) {
22582
+ if (typeof startedAt !== "number" || !Number.isFinite(startedAt) || startedAt < 0)
22583
+ return false;
22584
+ const now = (clock.now ?? Date.now)();
22585
+ const uptimeSeconds = (clock.uptime ?? uptime)();
22586
+ return Number.isFinite(now) && Number.isFinite(uptimeSeconds) && uptimeSeconds >= 0 && startedAt <= now && startedAt < now - uptimeSeconds * 1000 - 5 * 60000;
22587
+ }
22505
22588
  async function holderEntryIsLive(entry2, liveness = defaultLiveness) {
22589
+ if (recordPredatesBoot(entry2.registeredAt, liveness))
22590
+ return false;
22506
22591
  if (!liveness.pidAlive(entry2.pid))
22507
22592
  return false;
22508
22593
  if (entry2.incarnation === null)
@@ -22526,7 +22611,7 @@ function holderEntryLiveness(value) {
22526
22611
  return;
22527
22612
  if (incarnation !== null && typeof incarnation !== "string")
22528
22613
  return;
22529
- return { pid, incarnation };
22614
+ return { pid, incarnation, registeredAt: value.registeredAt };
22530
22615
  }
22531
22616
  function sameRegistration(value, entry2) {
22532
22617
  if (!isRecord13(value))
@@ -22645,8 +22730,12 @@ async function publishHolderEntryLocked(holderFile, entry2, options) {
22645
22730
  if (!life || await holderEntryIsLive(life, liveness))
22646
22731
  kept.push(value);
22647
22732
  }
22733
+ let changed = kept.length !== (current?.entries.length ?? 0);
22648
22734
  if (!kept.some((value) => sameRegistration(value, entry2))) {
22649
22735
  kept.push(entry2);
22736
+ changed = true;
22737
+ }
22738
+ if (changed) {
22650
22739
  await writeJsonAtomic(holderFile, { version: 1, entries: kept }, { beforeRename: () => options.assertOwned() });
22651
22740
  }
22652
22741
  published.add(registrationKey(holderFile, entry2));
@@ -22989,20 +23078,21 @@ var PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
22989
23078
  var PROFILE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
22990
23079
  async function fetchOAuthAccountProfile(input) {
22991
23080
  assertNotCustodyTombstone(input.accessToken, "anthropic");
22992
- const response = await (input.fetchImpl ?? fetch)(PROFILE_URL, {
23081
+ const signal = auxiliaryRequestSignal(input.signal);
23082
+ const response = await raceWithSignal(signal, () => (input.fetchImpl ?? fetch)(PROFILE_URL, {
22993
23083
  method: "GET",
22994
23084
  redirect: "error",
22995
- signal: input.signal,
23085
+ signal,
22996
23086
  headers: {
22997
23087
  Authorization: `Bearer ${input.accessToken}`,
22998
23088
  Accept: "application/json",
22999
23089
  "anthropic-beta": "oauth-2025-04-20"
23000
23090
  }
23001
- });
23091
+ }));
23002
23092
  if (!response.ok) {
23003
23093
  throw new Error(`Claude profile check failed: ${response.status}`);
23004
23094
  }
23005
- const value = await response.json();
23095
+ const value = await raceWithSignal(signal, () => response.json());
23006
23096
  const tier = value.organization?.rate_limit_tier;
23007
23097
  const orgType = value.organization?.organization_type;
23008
23098
  if (typeof tier !== "string" || tier.trim() === "" || typeof orgType !== "string" || orgType.trim() === "") {
@@ -28633,7 +28723,10 @@ function createNativeVaultRuntime(options) {
28633
28723
  const combined = signalFor(signal);
28634
28724
  let served = await runtime2.authorize(routeId, combined);
28635
28725
  const send = (input, init) => {
28636
- const physical = () => fetchImpl(input, { ...init, signal: combined });
28726
+ const physical = () => fetchImpl(input, {
28727
+ ...init,
28728
+ signal: init?.signal ? AbortSignal.any([combined, init.signal]) : combined
28729
+ });
28637
28730
  if (!dispatch)
28638
28731
  return physical();
28639
28732
  return dispatch(physical, async () => {
@@ -29013,6 +29106,7 @@ function jsonHeaders(headers) {
29013
29106
  return result;
29014
29107
  }
29015
29108
  async function postRelay(config, payload, signal) {
29109
+ signal?.throwIfAborted();
29016
29110
  const stringifyStart = perfNowMs();
29017
29111
  const body = JSON.stringify(payload);
29018
29112
  relayPerfLog("http_payload_stringify", {
@@ -29266,10 +29360,11 @@ class PersistentRelaySession {
29266
29360
  this.serverState = undefined;
29267
29361
  this.connecting = undefined;
29268
29362
  }
29269
- send(payload, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, responseMode) {
29363
+ send(payload, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, responseMode, signal) {
29364
+ signal?.throwIfAborted();
29270
29365
  this.touch();
29271
29366
  const enqueuedAt = perfNowMs();
29272
- const start = this.queue.catch(() => {}).then(() => this.startQueued(payload, bodyText, enqueuedAt, optimisticResponse, onResponseHeaders, authorizeAttempt, responseMode));
29367
+ const start = this.queue.catch(() => {}).then(() => raceWithSignal(signal, () => this.startQueued(payload, bodyText, enqueuedAt, optimisticResponse, onResponseHeaders, authorizeAttempt, responseMode, signal)));
29273
29368
  const result = start.then(async ({ response, getPayload }) => ({
29274
29369
  response: await response,
29275
29370
  payload: getPayload(),
@@ -29278,12 +29373,14 @@ class PersistentRelaySession {
29278
29373
  usedRelay: true
29279
29374
  }));
29280
29375
  this.queue = start.then(({ done }) => done.catch(() => {}), () => {});
29281
- return result;
29376
+ return raceWithSignal(signal, () => result);
29282
29377
  }
29283
- async startQueued(payload, bodyText, enqueuedAt, optimisticResponse, onResponseHeaders, authorizeAttempt, responseMode) {
29378
+ async startQueued(payload, bodyText, enqueuedAt, optimisticResponse, onResponseHeaders, authorizeAttempt, responseMode, signal) {
29379
+ signal?.throwIfAborted();
29284
29380
  const createScopes = (secrets) => createWebSocketAttemptScopes(secrets, responseMode);
29285
29381
  const connectStart = perfNowMs();
29286
29382
  await this.ensureConnected();
29383
+ signal?.throwIfAborted();
29287
29384
  const connectedAt = perfNowMs();
29288
29385
  const localState = sessionState.get(this.affinity);
29289
29386
  const serverHash = this.serverState?.hash;
@@ -29304,8 +29401,12 @@ class PersistentRelaySession {
29304
29401
  }
29305
29402
  relayLog(`perf websocket send_start session=${shortAffinity2(this.affinity)} request=${requestPayload.id} mode=${requestPayload.mode} queueMs=${formatMs(connectedAt - enqueuedAt)} connectMs=${formatMs(connectedAt - connectStart)} bodyBytes=${bodyText.length} relayBytes=${JSON.stringify(requestPayload).length}`);
29306
29403
  const firstAttempt = await authorizePhysicalAttempt(this.config, requestPayload, authorizeAttempt, createScopes);
29404
+ if (signal?.aborted) {
29405
+ disposeWebSocketAttemptScopes(firstAttempt.scope);
29406
+ signal.throwIfAborted();
29407
+ }
29307
29408
  let activePayload = requestPayload;
29308
- const first = this.sendPayload(requestPayload, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, firstAttempt, responseMode);
29409
+ const first = this.sendPayload(requestPayload, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, firstAttempt, responseMode, signal);
29309
29410
  first.done.catch(() => {});
29310
29411
  let activeDone = first.done;
29311
29412
  const response = first.response.catch(async (error) => {
@@ -29317,13 +29418,18 @@ class PersistentRelaySession {
29317
29418
  fullSync.id = createRequestId();
29318
29419
  fullSync.revision = (this.serverState?.revision ?? 0) + 1;
29319
29420
  const retryAttempt = await authorizePhysicalAttempt(this.config, fullSync, authorizeAttempt, createScopes);
29421
+ if (signal?.aborted) {
29422
+ disposeWebSocketAttemptScopes(retryAttempt.scope);
29423
+ signal.throwIfAborted();
29424
+ }
29320
29425
  activePayload = fullSync;
29321
- const retry = this.sendPayload(fullSync, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, retryAttempt, responseMode);
29426
+ const retry = this.sendPayload(fullSync, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, retryAttempt, responseMode, signal);
29322
29427
  retry.done.catch(() => {});
29323
29428
  activeDone = retry.done;
29324
29429
  return retry.response;
29325
29430
  });
29326
29431
  const done = response.then(() => activeDone, () => activeDone.catch(() => {}));
29432
+ done.catch(() => {});
29327
29433
  return { response, done, getPayload: () => activePayload };
29328
29434
  }
29329
29435
  async ensureConnected() {
@@ -29396,7 +29502,7 @@ class PersistentRelaySession {
29396
29502
  });
29397
29503
  await this.connecting;
29398
29504
  }
29399
- sendPayload(payload, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, attempt, responseMode) {
29505
+ sendPayload(payload, bodyText, optimisticResponse, onResponseHeaders, authorizeAttempt, attempt, responseMode, signal) {
29400
29506
  const socket = this.socket;
29401
29507
  if (!socket || socket.readyState !== WebSocket.OPEN) {
29402
29508
  disposeWebSocketAttemptScopes(attempt.scope);
@@ -29438,6 +29544,24 @@ class PersistentRelaySession {
29438
29544
  responseHeadersDelivered: false
29439
29545
  };
29440
29546
  this.pending = pending;
29547
+ if (signal) {
29548
+ const abort = () => {
29549
+ if (this.pending !== pending)
29550
+ return;
29551
+ this.failPending(signal.reason, false);
29552
+ const socket2 = this.socket;
29553
+ this.socket = undefined;
29554
+ this.serverState = undefined;
29555
+ this.connecting = undefined;
29556
+ socket2?.close();
29557
+ };
29558
+ pending.abortCleanup = () => signal.removeEventListener("abort", abort);
29559
+ signal.addEventListener("abort", abort, { once: true });
29560
+ if (signal.aborted) {
29561
+ abort();
29562
+ return;
29563
+ }
29564
+ }
29441
29565
  this.resetPendingTimeout(pending);
29442
29566
  socket.send(JSON.stringify(payload));
29443
29567
  if (optimisticResponse) {
@@ -29540,6 +29664,8 @@ class PersistentRelaySession {
29540
29664
  }
29541
29665
  retireAttempt(pending) {
29542
29666
  pending.retired = true;
29667
+ pending.abortCleanup?.();
29668
+ pending.abortCleanup = undefined;
29543
29669
  pending.scope?.dispose();
29544
29670
  pending.scope = undefined;
29545
29671
  pending.responseScope?.dispose();
@@ -29936,8 +30062,9 @@ async function sendViaRelay(options) {
29936
30062
  clearTimeout: clearTimeoutImpl
29937
30063
  });
29938
30064
  const optimistic = optimisticResponse === true && requestMode() === "event-stream";
29939
- result = await session.send(payload, bodyText, optimistic, onResponseHeaders, authorizeAttempt, requestMode());
30065
+ result = await session.send(payload, bodyText, optimistic, onResponseHeaders, authorizeAttempt, requestMode(), init?.signal);
29940
30066
  } catch (error) {
30067
+ init?.signal?.throwIfAborted();
29941
30068
  if (error instanceof RelayAuthorizationError || error instanceof RelayResponseMetadataError)
29942
30069
  throw error;
29943
30070
  relayLog(`websocket relay failed session=${shortAffinity2(affinity)}; trying http relay code=relay_transport_error`);
@@ -30013,6 +30140,7 @@ async function sendViaRelay(options) {
30013
30140
  });
30014
30141
  return result.response;
30015
30142
  } catch (error) {
30143
+ init?.signal?.throwIfAborted();
30016
30144
  if (error instanceof RelayAuthorizationError)
30017
30145
  throw error.failure;
30018
30146
  if (error instanceof RelayResponseMetadataError)
@@ -31470,7 +31598,10 @@ function createNativeAccountRuntime(options) {
31470
31598
  throw new NativeCustodyError("route-unavailable");
31471
31599
  let served = first;
31472
31600
  const send = (input, init) => {
31473
- const physical = () => fetchImpl(input, { ...init, signal: combined });
31601
+ const physical = () => fetchImpl(input, {
31602
+ ...init,
31603
+ signal: init?.signal ? AbortSignal.any([combined, init.signal]) : combined
31604
+ });
31474
31605
  if (!dispatch)
31475
31606
  return physical();
31476
31607
  return dispatch(physical, async () => {
@@ -35561,6 +35692,8 @@ function fetchInputUrl(input) {
35561
35692
  function createAnthropicOAuthFetch(options = {}) {
35562
35693
  const current = () => typeof options === "function" ? options() : options;
35563
35694
  return Object.assign(async (input, init) => {
35695
+ const signal = init?.signal !== undefined ? init.signal : input instanceof Request ? input.signal : undefined;
35696
+ signal?.throwIfAborted();
35564
35697
  requireAnthropicOAuthOrigin(fetchInputUrl(input), current());
35565
35698
  return fetch(input, { ...init, redirect: "error" });
35566
35699
  }, {
@@ -37167,7 +37300,7 @@ import { homedir as homedir7 } from "node:os";
37167
37300
  import { basename as basename13, dirname as dirname24, join as join30 } from "node:path";
37168
37301
 
37169
37302
  // src/layout-fence.ts
37170
- import { readdir as readdir6, readFile as readFile15 } from "node:fs/promises";
37303
+ import { open as open13, readdir as readdir6 } from "node:fs/promises";
37171
37304
  import { homedir as homedir5 } from "node:os";
37172
37305
  import { basename as basename11, join as join27, resolve as resolve18 } from "node:path";
37173
37306
  var STATE_PATH_OVERRIDE_VARIABLES = [
@@ -37285,8 +37418,15 @@ function stamped(value) {
37285
37418
  }
37286
37419
  async function readArtifact(path2, blockers) {
37287
37420
  let text6;
37421
+ let mtimeMs;
37288
37422
  try {
37289
- text6 = await readFile15(path2, "utf8");
37423
+ const file = await open13(path2, "r");
37424
+ try {
37425
+ text6 = await file.readFile("utf8");
37426
+ mtimeMs = (await file.stat()).mtimeMs;
37427
+ } finally {
37428
+ await file.close();
37429
+ }
37290
37430
  } catch (error) {
37291
37431
  if (errorCode5(error) === "ENOENT")
37292
37432
  return;
@@ -37294,13 +37434,15 @@ async function readArtifact(path2, blockers) {
37294
37434
  return;
37295
37435
  }
37296
37436
  try {
37297
- return JSON.parse(text6);
37437
+ const value = JSON.parse(text6);
37438
+ const startedAt = typeof value === "object" && value !== null ? "startedAt" in value ? value.startedAt : ("registeredAt" in value) ? value.registeredAt : mtimeMs : mtimeMs;
37439
+ return { value, startedAt };
37298
37440
  } catch {
37299
37441
  blockers.push({ kind: "artifact-scan", path: path2, reason: "invalid" });
37300
37442
  return;
37301
37443
  }
37302
37444
  }
37303
- async function preNArtifactBlockers(locations, pidAlive2 = defaultLiveness2.pidAlive) {
37445
+ async function preNArtifactBlockers(locations, pidAlive2 = defaultLiveness2.pidAlive, clock = {}) {
37304
37446
  const blockers = [];
37305
37447
  const portDirs = [...locations.rpcDirs];
37306
37448
  for (const root of locations.rpcRoots)
@@ -37311,15 +37453,16 @@ async function preNArtifactBlockers(locations, pidAlive2 = defaultLiveness2.pidA
37311
37453
  if (!/^port-\d+\.json$/.test(name))
37312
37454
  continue;
37313
37455
  const path2 = join27(dir, name);
37314
- const value = await readArtifact(path2, blockers);
37315
- if (value === undefined || stamped(value))
37456
+ const record9 = await readArtifact(path2, blockers);
37457
+ if (record9 === undefined || stamped(record9.value))
37316
37458
  continue;
37459
+ const { value, startedAt } = record9;
37317
37460
  const pid = value.pid;
37318
37461
  if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) {
37319
37462
  blockers.push({ kind: "artifact-scan", path: path2, reason: "invalid" });
37320
37463
  continue;
37321
37464
  }
37322
- if (pidAlive2(pid))
37465
+ if (!recordPredatesBoot(startedAt, clock) && pidAlive2(pid))
37323
37466
  blockers.push({ kind: "pre-n-artifact", artifact: path2, pid });
37324
37467
  }
37325
37468
  }
@@ -37329,11 +37472,11 @@ async function preNArtifactBlockers(locations, pidAlive2 = defaultLiveness2.pidA
37329
37472
  if (!match)
37330
37473
  continue;
37331
37474
  const path2 = join27(dir, name);
37332
- const value = await readArtifact(path2, blockers);
37333
- if (value === undefined || stamped(value))
37475
+ const record9 = await readArtifact(path2, blockers);
37476
+ if (record9 === undefined || stamped(record9.value))
37334
37477
  continue;
37335
37478
  const pid = Number(match[1]);
37336
- if (pidAlive2(pid))
37479
+ if (!recordPredatesBoot(record9.startedAt, clock) && pidAlive2(pid))
37337
37480
  blockers.push({ kind: "pre-n-artifact", artifact: path2, pid });
37338
37481
  }
37339
37482
  }
@@ -37356,7 +37499,7 @@ function describeFenceBlocker(blocker) {
37356
37499
 
37357
37500
  // src/state-layout-move.ts
37358
37501
  import { randomUUID as randomUUID20 } from "node:crypto";
37359
- import { lstat as lstat18, readdir as readdir7, readFile as readFile16, stat as stat13, unlink as unlink10 } from "node:fs/promises";
37502
+ import { lstat as lstat18, readdir as readdir7, readFile as readFile15, stat as stat13, unlink as unlink10 } from "node:fs/promises";
37360
37503
  import { basename as basename12, dirname as dirname23, join as join28, sep as sep2 } from "node:path";
37361
37504
  var ACCOUNT_FILE = "anthropic-auth.json";
37362
37505
  var ACCOUNT_STATE_FILE = "anthropic-auth-state.json";
@@ -37826,7 +37969,7 @@ async function rollbackPreparing(record9, dataset, ctx) {
37826
37969
  } else {
37827
37970
  let copy;
37828
37971
  try {
37829
- copy = await readFile16(file.to);
37972
+ copy = await readFile15(file.to);
37830
37973
  } catch {
37831
37974
  throw new StateLayoutRollbackRefused(file.to, `its copy, needed to undo the tombstone over ${file.from}, is missing`);
37832
37975
  }
@@ -38017,12 +38160,12 @@ async function moveDataset(dataset, ctx, checkArtifacts) {
38017
38160
  for (const file of files) {
38018
38161
  if (file.sha256 === null)
38019
38162
  continue;
38020
- const bytes = await readFile16(file.from);
38163
+ const bytes = await readFile15(file.from);
38021
38164
  if (sha256Hex(bytes) !== file.sha256)
38022
38165
  throw new MoveRefused(`${file.from} changed while it was copied`);
38023
38166
  const stage = relocatingPath(file.to, attempt);
38024
38167
  await createOwnerOnlyFile(ctx.fs, stage, bytes);
38025
- const back = await readFile16(stage);
38168
+ const back = await readFile15(stage);
38026
38169
  if (!back.equals(bytes))
38027
38170
  throw new MoveRefused(`the copy of ${file.from} reads back differently`);
38028
38171
  await owned2(ctx, { kind: "rename", from: stage, to: file.to }, expectAbsent(file.to));
@@ -38134,7 +38277,7 @@ async function rollbackAfterFailure(preparing, dataset, ctx) {
38134
38277
 
38135
38278
  // src/state-layout-restore.ts
38136
38279
  import { randomUUID as randomUUID21 } from "node:crypto";
38137
- import { readFile as readFile17 } from "node:fs/promises";
38280
+ import { readFile as readFile16 } from "node:fs/promises";
38138
38281
  import { homedir as homedir6 } from "node:os";
38139
38282
  import { join as join29 } from "node:path";
38140
38283
  var DATA_ROLES = [
@@ -38414,7 +38557,7 @@ async function newRestoreProblems(record9, dataset, options) {
38414
38557
  }
38415
38558
  problems.push(...await holderProblems(join29(dataset.sub, HOLDER_FILE_NAME), options.liveness, true), ...await holderProblems(join29(dataset.oldDir, HOLDER_FILE_NAME), options.liveness, false));
38416
38559
  const artifacts = options.artifacts ?? defaultPreNArtifactLocations(options.env ?? process.env, options.home ?? homedir6());
38417
- for (const blocker of await preNArtifactBlockers(artifacts, options.liveness?.pidAlive))
38560
+ for (const blocker of await preNArtifactBlockers(artifacts, options.liveness?.pidAlive, options.liveness))
38418
38561
  problems.push(`${describeFenceBlocker(blocker)} is still running`);
38419
38562
  for (const slot of dataset.slots) {
38420
38563
  if (slot.role === "localRuntime" || slot.role === "vaultRuntime") {
@@ -38478,12 +38621,12 @@ async function startRestore(record9, dataset, ctx, options) {
38478
38621
  }, ctx.fs);
38479
38622
  await stateLayoutStep(ctx, "restore:restoring-written");
38480
38623
  for (const file of files) {
38481
- const bytes = await readFile17(file.to);
38624
+ const bytes = await readFile16(file.to);
38482
38625
  if (sha256Hex(bytes) !== file.sha256)
38483
38626
  throw refused2(`${file.to} changed while it was being restored; run ${RESTORE_LEGACY_COMMAND} again.`, [file.to]);
38484
38627
  await ctx.stack.assertAll();
38485
38628
  await createOwnerOnlyFile(ctx.fs, file.staging, bytes);
38486
- const back = await readFile17(file.staging);
38629
+ const back = await readFile16(file.staging);
38487
38630
  if (!back.equals(bytes))
38488
38631
  throw refused2(`the restored copy of ${file.to} reads back differently; run ${RESTORE_LEGACY_COMMAND} again.`, [file.staging]);
38489
38632
  await stateLayoutStep(ctx, `restore:staged:${file.role}`);
@@ -39137,7 +39280,7 @@ async function underLayoutLock(dataset, options, ctx, overrides, neverMove) {
39137
39280
  const files = [...dataset.slots.flatMap((slot) => [slot.from, slot.to])];
39138
39281
  const artifacts = options.artifacts ?? defaultPreNArtifactLocations(options.env ?? process.env, options.home ?? homedir7());
39139
39282
  const checkArtifacts = async () => {
39140
- const found = await preNArtifactBlockers(artifacts, options.liveness?.pidAlive);
39283
+ const found = await preNArtifactBlockers(artifacts, options.liveness?.pidAlive, options.liveness);
39141
39284
  return found.length ? found.map(describeFenceBlocker).join("; ") : undefined;
39142
39285
  };
39143
39286
  const blockers = [
@@ -39146,7 +39289,7 @@ async function underLayoutLock(dataset, options, ctx, overrides, neverMove) {
39146
39289
  files,
39147
39290
  liveness: options.liveness
39148
39291
  }),
39149
- ...await preNArtifactBlockers(artifacts, options.liveness?.pidAlive)
39292
+ ...await preNArtifactBlockers(artifacts, options.liveness?.pidAlive, options.liveness)
39150
39293
  ].map(describeFenceBlocker);
39151
39294
  if (!blockers.length) {
39152
39295
  const failure2 = await movePreconditionFailure(dataset, ctx.fs.platform);
@@ -39349,6 +39492,7 @@ export {
39349
39492
  writerStamp,
39350
39493
  writeNativeHostAuth,
39351
39494
  writeDumpFile,
39495
+ withAbortableFetch,
39352
39496
  usesPerTurnControl,
39353
39497
  usagePublicationRefusalIsFatal,
39354
39498
  unmovedRegistrationPolicy,
@@ -39396,6 +39540,7 @@ export {
39396
39540
  sendWithRequestGzip,
39397
39541
  sendViaRelay,
39398
39542
  selectClaudeCodeBetas,
39543
+ runWithRequestSignal,
39399
39544
  runSetup,
39400
39545
  runPiCommandMenu,
39401
39546
  runNativeMigration,
@@ -39419,6 +39564,7 @@ export {
39419
39564
  resetBillingHeaderCCH,
39420
39565
  requireNoSupervisedAuthContentSnapshot,
39421
39566
  requireAnthropicOAuthOrigin,
39567
+ requestSignal,
39422
39568
  requestGzipPadding,
39423
39569
  reportRouting,
39424
39570
  renameWithWindowsSharingRetry,
@@ -39432,6 +39578,7 @@ export {
39432
39578
  refreshBackoffActive,
39433
39579
  redactPayload,
39434
39580
  recordProcessStateLayout,
39581
+ recordPredatesBoot,
39435
39582
  readWindowsOwnerOnly,
39436
39583
  readStateLayoutMarker,
39437
39584
  readRelocationPointer,
@@ -39443,6 +39590,7 @@ export {
39443
39590
  readConfigDirMarker,
39444
39591
  readClaustrumEnrollmentToken2 as readClaustrumEnrollmentToken,
39445
39592
  readClaustrumEnrollmentStatus2 as readClaustrumEnrollmentStatus,
39593
+ raceWithSignal,
39446
39594
  quotaWindowParts,
39447
39595
  quotaTextParts,
39448
39596
  quotaSnapshotPassesPolicy,
@@ -39701,6 +39849,7 @@ export {
39701
39849
  computeXxhash64Hex,
39702
39850
  computeCcVersionSuffix,
39703
39851
  computeCCH,
39852
+ commitRequestChange,
39704
39853
  claudeCodeEntrypoint,
39705
39854
  classifySafeErrorForLog,
39706
39855
  classifyGzipRejection,
@@ -39726,6 +39875,7 @@ export {
39726
39875
  buildCache1hStatusSummary,
39727
39876
  buildBillingHeaderValue,
39728
39877
  buildAccountList,
39878
+ auxiliaryRequestSignal,
39729
39879
  authorize,
39730
39880
  assertUnmanagedInjectedPaths,
39731
39881
  assertNotCustodyTombstone,
@@ -39743,6 +39893,7 @@ export {
39743
39893
  acquireRefreshFileLock2 as acquireRefreshFileLock,
39744
39894
  acquireNativeVaultRuntime,
39745
39895
  acquireNativeAccountRuntime,
39896
+ abortableFetch,
39746
39897
  __setLogTestSink,
39747
39898
  WORKER_SCRIPT,
39748
39899
  USER_AGENT,
@@ -39920,6 +40071,7 @@ export {
39920
40071
  CACHE_1H_COMMAND_NAME,
39921
40072
  AnthropicOAuthOriginError,
39922
40073
  AXIOS_USER_AGENT,
40074
+ AUXILIARY_REQUEST_TIMEOUT_MS,
39923
40075
  AUTHORIZE_URLS,
39924
40076
  ANTHROPIC_OAUTH_ORIGIN_REFUSAL,
39925
40077
  ANTHROPIC_OAUTH_ORIGIN,
@@ -58,8 +58,9 @@ export declare function defaultPreNArtifactLocations(env?: NodeJS.ProcessEnv, ho
58
58
  * The fence's check for older versions: an RPC port file or CacheKeep record without the
59
59
  * `writer` stamp, whose pid is alive, belongs to a version that registers
60
60
  * nothing and blocks. Every entry is read; a folder or file that can't be
61
- * read or parsed blocks too. No timestamp ever clears a live pid.
61
+ * read or parsed blocks too. A record from before this boot is stale even
62
+ * if its pid has been reused; missing or untrustworthy times keep blocking.
62
63
  */
63
- export declare function preNArtifactBlockers(locations: PreNArtifactLocations, pidAlive?: (pid: number) => boolean): Promise<FenceBlocker[]>;
64
+ export declare function preNArtifactBlockers(locations: PreNArtifactLocations, pidAlive?: (pid: number) => boolean, clock?: Pick<HolderLiveness, 'now' | 'uptime'>): Promise<FenceBlocker[]>;
64
65
  /** One line naming a blocker, for the log. */
65
66
  export declare function describeFenceBlocker(blocker: FenceBlocker): string;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * End a caller's wait without cancelling shared work such as a token refresh.
3
+ * The operation still owns its cleanup and any late rejection is observed.
4
+ */
5
+ export declare function raceWithSignal<T>(signal: AbortSignal | null | undefined, operation: () => Promise<T>): Promise<T>;
6
+ export declare function abortableFetch(send: (input: string | URL | Request, init?: RequestInit) => Promise<Response>): (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
7
+ export declare function withAbortableFetch<T extends {
8
+ fetch: Parameters<typeof abortableFetch>[0];
9
+ }>(options: T): T;
10
+ export declare const AUXILIARY_REQUEST_TIMEOUT_MS = 30000;
11
+ /** Account checks must finish even when no host request is waiting on them. */
12
+ export declare function auxiliaryRequestSignal(signal?: AbortSignal | null): AbortSignal;
13
+ export declare function requestSignal(): AbortSignal | null | undefined;
14
+ /** Publish request-owned routing changes only after preparation succeeds. */
15
+ export declare function commitRequestChange(commit: () => void): void;
16
+ export declare function runWithRequestSignal<T>(signal: AbortSignal | null | undefined, operation: () => Promise<T>): Promise<T>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "2.4.10",
3
+ "version": "2.4.11",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",