@cortexkit/anthropic-auth-core 2.4.6 → 2.4.8

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.
@@ -400,6 +400,12 @@ export declare function getScopedQuotaWindowForModel(quota: OAuthQuotaSnapshot |
400
400
  export declare function quotaSnapshotModelScopeIsExhausted(quota: OAuthQuotaSnapshot | undefined, model: unknown): boolean;
401
401
  export declare function quotaSnapshotPassesModelScope(quota: OAuthQuotaSnapshot | undefined, model: unknown): boolean;
402
402
  export declare function quotaSnapshotPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null): boolean;
403
+ /** A poll's omitted session window is evidence, unlike a partial header harvest. */
404
+ export declare function quotaSnapshotOmitsFiveHour(quota: OAuthQuotaSnapshot | undefined): quota is OAuthQuotaSnapshot & {
405
+ checkedAt: number;
406
+ five_hour?: undefined;
407
+ seven_day: AccountQuotaWindow;
408
+ };
403
409
  export declare function quotaSnapshotHasStandardWindows(quota: OAuthQuotaSnapshot | undefined): quota is OAuthQuotaSnapshot;
404
410
  export declare const DEFAULT_KILLSWITCH_THRESHOLDS: Record<QuotaWindowName | 'scoped', number>;
405
411
  export declare function normalizeKillswitchThresholds(thresholds: KillswitchThresholds | undefined): Record<QuotaWindowName | 'scoped', number>;
package/dist/index.d.ts CHANGED
@@ -25,7 +25,7 @@ export { CLAUDE_CODE_DEFAULT_BETAS, CLAUDE_MODEL_CAPABILITIES, getClaudeModelCap
25
25
  export * from './model-remap.ts';
26
26
  export * from './models.ts';
27
27
  export * from './native-account-refusal.ts';
28
- export type { NativeAccountRuntime, NativeAccountRuntimeOptions, NativeAccountWriteInput, NativeApiSubject, NativeLocalAuthorizeOptions, NativeRelayUpdate, } from './native-account-runtime.ts';
28
+ export type { NativeAccountPolicy, NativeAccountRuntime, NativeAccountRuntimeOptions, NativeAccountWriteInput, NativeApiSubject, NativeLocalAuthorizeOptions, NativeQuotaPolicyLimit, NativeRelayUpdate, } from './native-account-runtime.ts';
29
29
  export { acquireNativeAccountRuntime, createNativeAccountRuntime, nativeAccountPolicy, } from './native-account-runtime.ts';
30
30
  export type { NativeAccountMetadataPatch, NativeAccountSnapshot, NativeAccountView, } from './native-account-view.ts';
31
31
  export { formatNativeAccountState, nativeAccountCanServe, } from './native-account-view.ts';
@@ -62,6 +62,7 @@ export * from './native-usage-publication.ts';
62
62
  export { NATIVE_VAULT_NO_ACCOUNT_MESSAGE, NativeVaultNoAccountError, readNativeAccountsForRequest, } from './native-vault-admission.ts';
63
63
  export type { NativePiAuthSlot, NativePiAuthSlotErrorCode, NativePiAuthSlotInspection, NativePiAuthSlotOptions, NativeVaultDisconnectOptions, NativeVaultDisconnectResult, } from './native-vault-connection.ts';
64
64
  export { disconnectNativeVaultForHost, NativePiAuthSlotError, nativePiAuthSlotPaths, openNativePiAuthSlot, } from './native-vault-connection.ts';
65
+ export { createNativeVaultNoRouteResponse, NativeVaultQuotaPolicyError, } from './native-vault-refusal.ts';
65
66
  export type { NativeVaultQuotaPurpose, NativeVaultRuntime, NativeVaultRuntimeOptions, NativeVaultWatch, NativeVaultWatchOptions, } from './native-vault-runtime.ts';
66
67
  export { acquireNativeVaultRuntime, createNativeVaultRuntime, discoverNativeVaultInventory, publishNativeVaultRosterSeed, publishNativeVaultRuntimeSeed, } from './native-vault-runtime.ts';
67
68
  export * from './oauth-origin.ts';
package/dist/index.js CHANGED
@@ -4509,6 +4509,8 @@ function mergeHeaderOwnedWindow(existingSnapshot, incomingSnapshot, key) {
4509
4509
  const incoming = incomingSnapshot[key];
4510
4510
  if (!incoming)
4511
4511
  return existing;
4512
+ if (key === "five_hour" && quotaSnapshotOmitsFiveHour(existingSnapshot) && incoming.checkedAt <= (existingSnapshot.checkedAt ?? 0))
4513
+ return;
4512
4514
  if (!existing)
4513
4515
  return incoming;
4514
4516
  if (incoming.checkedAt > existing.checkedAt)
@@ -4545,6 +4547,8 @@ function fieldSourcesForMergedQuota(existing, incoming, merged) {
4545
4547
  if (source)
4546
4548
  fieldSources[field] = source;
4547
4549
  }
4550
+ if (!merged.five_hour && quotaSnapshotOmitsFiveHour(existing))
4551
+ fieldSources.five_hour = "poll";
4548
4552
  return Object.keys(fieldSources).length > 0 ? fieldSources : undefined;
4549
4553
  }
4550
4554
  function mergeHeaderQuotaForPersistence(existing, incoming) {
@@ -4560,7 +4564,10 @@ function mergeHeaderQuotaForPersistence(existing, incoming) {
4560
4564
  extraUsage: existing.extraUsage ?? incoming.extraUsage,
4561
4565
  fallbackAdvised: incoming.fieldSources?.fallbackAdvised !== undefined ? incoming.fallbackAdvised : existing.fallbackAdvised ?? incoming.fallbackAdvised,
4562
4566
  bindingWindow: preservePollBinding ? existing.bindingWindow : incoming.bindingWindow ?? existing.bindingWindow,
4563
- bindingWindowSource: preservePollBinding ? "poll" : incoming.bindingWindowSource ?? existing.bindingWindowSource
4567
+ bindingWindowSource: preservePollBinding ? "poll" : incoming.bindingWindowSource ?? existing.bindingWindowSource,
4568
+ ...quotaSnapshotOmitsFiveHour(existing) && {
4569
+ checkedAt: existing.checkedAt
4570
+ }
4564
4571
  };
4565
4572
  const fieldSources = fieldSourcesForMergedQuota(existing, incoming, merged);
4566
4573
  return {
@@ -4993,20 +5000,25 @@ function quotaSnapshotPassesPolicy(quota, storage) {
4993
5000
  if (!quotaEnabled(storage))
4994
5001
  return true;
4995
5002
  const thresholds = getQuotaMinimumRemainingThresholds(storage);
5003
+ let sawUnknownWindow = false;
4996
5004
  for (const key of ["five_hour", "seven_day"]) {
5005
+ if (key === "five_hour" && quotaSnapshotOmitsFiveHour(quota))
5006
+ continue;
4997
5007
  const window = quota?.[key];
4998
- if (!window)
4999
- return !failClosedOnUnknownQuota(storage);
5000
- if (!Number.isFinite(window.remainingPercent)) {
5001
- return !failClosedOnUnknownQuota(storage);
5008
+ if (!window || !Number.isFinite(window.remainingPercent)) {
5009
+ sawUnknownWindow = true;
5010
+ continue;
5002
5011
  }
5003
5012
  if (window.remainingPercent < thresholds[key])
5004
5013
  return false;
5005
5014
  }
5006
- return true;
5015
+ return !sawUnknownWindow || !failClosedOnUnknownQuota(storage);
5016
+ }
5017
+ function quotaSnapshotOmitsFiveHour(quota) {
5018
+ return Boolean((quota?.source === "poll" || quota?.fieldSources?.five_hour === "poll") && quota && !quota.five_hour && quota.seven_day && Number.isFinite(quota.checkedAt) && (quota.checkedAt ?? 0) > 0);
5007
5019
  }
5008
5020
  function quotaSnapshotHasStandardWindows(quota) {
5009
- return Boolean(quota?.five_hour && quota.seven_day);
5021
+ return Boolean(quota?.seven_day && (quota.five_hour || quotaSnapshotOmitsFiveHour(quota)));
5010
5022
  }
5011
5023
  var DEFAULT_KILLSWITCH_THRESHOLDS = {
5012
5024
  five_hour: 5,
@@ -5040,6 +5052,8 @@ function killswitchPassesPolicy(quota, storage, accountId, modelId) {
5040
5052
  const thresholds = getKillswitchThresholdsForAccount(storage, accountId);
5041
5053
  let sawUnknownWindow = false;
5042
5054
  for (const key of ["five_hour", "seven_day"]) {
5055
+ if (key === "five_hour" && quotaSnapshotOmitsFiveHour(quota))
5056
+ continue;
5043
5057
  const window = quota?.[key];
5044
5058
  if (!window || !Number.isFinite(window.remainingPercent)) {
5045
5059
  if (thresholds[key] > 0)
@@ -5106,6 +5120,8 @@ function getQuotaNextRefreshAt(quota, storage, now) {
5106
5120
  const thresholds = getQuotaMinimumRemainingThresholds(storage);
5107
5121
  const blockedResetTimes = [];
5108
5122
  for (const key of ["five_hour", "seven_day"]) {
5123
+ if (key === "five_hour" && quotaSnapshotOmitsFiveHour(quota))
5124
+ continue;
5109
5125
  const window = quota?.[key];
5110
5126
  if (!window)
5111
5127
  return capAtOldestWindow(now + intervalMs);
@@ -7539,6 +7555,8 @@ class InsulaQuotaSource {
7539
7555
  ...typeof entry.source === "string" && { source: entry.source }
7540
7556
  }
7541
7557
  };
7558
+ if (entry.stale !== undefined && (!record(entry.stale) || entry.stale.class !== "local_source_unavailable"))
7559
+ return { miss: "upstream-failed", reading };
7542
7560
  return now >= fetchedAt + MAX_AGE || entry.stale !== undefined ? { miss: "stale", reading } : { reading };
7543
7561
  }
7544
7562
  if (this.#completeProviders.has(provider))
@@ -7882,6 +7900,7 @@ function combineAbortSignals(signals) {
7882
7900
  }
7883
7901
 
7884
7902
  // ../../node_modules/@cortexkit/common-auth/dist/claustrum/custody.js
7903
+ import { channel } from "node:diagnostics_channel";
7885
7904
  import { ClaustrumCredentialError as ClaustrumCredentialError3 } from "@cortexkit/claustrum-client";
7886
7905
 
7887
7906
  // ../../node_modules/@cortexkit/common-auth/dist/claustrum/host-slot.js
@@ -7941,6 +7960,7 @@ function scopedRetryReason(served, current) {
7941
7960
  return "rotated";
7942
7961
  }
7943
7962
  var defaultLogger = createLogger("claustrum");
7963
+ var phaseDiagnostics = channel("common-auth.claustrum.phases");
7944
7964
  function decideScopedRetryAfter401(site, served, current, logger2 = defaultLogger) {
7945
7965
  const retry = isScopedCredentialRotation(served, current);
7946
7966
  logger2.debug("scoped 401 re-authorized", {
@@ -8140,6 +8160,11 @@ class ClaustrumScopedCustody {
8140
8160
  }
8141
8161
  async#token(signal) {
8142
8162
  this.#check(signal);
8163
+ if (phaseDiagnostics.hasSubscribers)
8164
+ phaseDiagnostics.publish({
8165
+ logger: this.#logger,
8166
+ phase: "token read begin"
8167
+ });
8143
8168
  const pending = this.#readToken();
8144
8169
  this.#tokenReads.add(pending);
8145
8170
  let value;
@@ -8147,6 +8172,11 @@ class ClaustrumScopedCustody {
8147
8172
  value = await pending;
8148
8173
  } finally {
8149
8174
  this.#tokenReads.delete(pending);
8175
+ if (phaseDiagnostics.hasSubscribers)
8176
+ phaseDiagnostics.publish({
8177
+ logger: this.#logger,
8178
+ phase: "token read end"
8179
+ });
8150
8180
  }
8151
8181
  this.#check(signal);
8152
8182
  if (!/^[0-9a-f]{64}$/.test(value.token) || !Number.isSafeInteger(value.token_generation) || value.token_generation < 1) {
@@ -8202,11 +8232,22 @@ class ClaustrumScopedCustody {
8202
8232
  this.#kept.clear();
8203
8233
  throw error;
8204
8234
  }
8235
+ if (phaseDiagnostics.hasSubscribers)
8236
+ phaseDiagnostics.publish({
8237
+ logger: this.#logger,
8238
+ phase: "list scoped begin"
8239
+ });
8205
8240
  try {
8206
8241
  inventory = await this.#call(() => this.#client.listScoped(token), signal);
8207
8242
  } catch (error) {
8208
8243
  this.listFailed(error);
8209
8244
  throw error;
8245
+ } finally {
8246
+ if (phaseDiagnostics.hasSubscribers)
8247
+ phaseDiagnostics.publish({
8248
+ logger: this.#logger,
8249
+ phase: "list scoped end"
8250
+ });
8210
8251
  }
8211
8252
  const skipped = [];
8212
8253
  const refused = [];
@@ -10477,6 +10518,7 @@ class ClaustrumConsumer {
10477
10518
  }
10478
10519
  // ../../node_modules/@cortexkit/common-auth/dist/claustrum/hub.js
10479
10520
  import { createHash as createHash6 } from "node:crypto";
10521
+ import { channel as channel2 } from "node:diagnostics_channel";
10480
10522
  import { readdir as readdir2, stat as stat7 } from "node:fs/promises";
10481
10523
  import { dirname as dirname8, join as join10, resolve as resolve9 } from "node:path";
10482
10524
  var sharedRosters = new Map;
@@ -11074,7 +11116,24 @@ class ClaustrumDiscoveryHub {
11074
11116
  throw closed("Claustrum hub member has no base view");
11075
11117
  if (base instanceof Error)
11076
11118
  throw base;
11077
- const seed = base.file ? undefined : await this.#seed(entry);
11119
+ let seed;
11120
+ if (!base.file) {
11121
+ const phaseDiagnostics2 = channel2("common-auth.claustrum.phases");
11122
+ if (phaseDiagnostics2.hasSubscribers)
11123
+ phaseDiagnostics2.publish({
11124
+ logger: this.#logger,
11125
+ phase: `seed read ${entry.provider} begin`
11126
+ });
11127
+ try {
11128
+ seed = await this.#seed(entry);
11129
+ } finally {
11130
+ if (phaseDiagnostics2.hasSubscribers)
11131
+ phaseDiagnostics2.publish({
11132
+ logger: this.#logger,
11133
+ phase: `seed read ${entry.provider} end`
11134
+ });
11135
+ }
11136
+ }
11078
11137
  const roster = await refreshVaultRoster({
11079
11138
  path: entry.path,
11080
11139
  custody: { discover: async () => share },
@@ -18792,6 +18851,13 @@ var nativeQuotaCodec = {
18792
18851
  return toNativeQuotaMap(mergeHeaderQuotaForPersistence(previous, incoming));
18793
18852
  const previousTime = quotaSnapshotCheckedAt(previous);
18794
18853
  const incomingTime = quotaSnapshotCheckedAt(incoming);
18854
+ if (incoming.source === "poll" && incoming.checkedAt !== undefined && previous.checkedAt !== undefined && (previous.source === "poll" || quotaSnapshotOmitsFiveHour(previous)) && incoming.checkedAt < previous.checkedAt)
18855
+ return toNativeQuotaMap(previous);
18856
+ if (incoming.source === "poll" && incoming.seven_day && !incoming.five_hour && incoming.checkedAt !== undefined && incoming.checkedAt > (previous.five_hour?.checkedAt ?? 0) && (previous.source !== "poll" || incoming.checkedAt >= (previous.checkedAt ?? 0)))
18857
+ return toNativeQuotaMap(previous.source === "headers" ? mergeHeaderQuotaForPersistence(incoming, {
18858
+ ...previous,
18859
+ five_hour: undefined
18860
+ }) : incoming);
18795
18861
  return toNativeQuotaMap(previousTime > incomingTime || previousTime === incomingTime && previous.source === "poll" && incoming.source !== "poll" ? previous : incoming);
18796
18862
  }
18797
18863
  };
@@ -20504,6 +20570,12 @@ function normalizeQuotaHeaders(headers, now = Date.now()) {
20504
20570
  };
20505
20571
  }
20506
20572
  function mergeHeaderQuotaSnapshot(existing, incoming) {
20573
+ if (quotaSnapshotOmitsFiveHour(existing) && incoming.five_hour && incoming.five_hour.checkedAt <= existing.checkedAt) {
20574
+ return mergeHeaderQuotaSnapshot(existing, {
20575
+ ...incoming,
20576
+ five_hour: undefined
20577
+ });
20578
+ }
20507
20579
  const {
20508
20580
  scoped: existingScoped,
20509
20581
  extraUsage: existingExtraUsage,
@@ -20525,6 +20597,8 @@ function mergeHeaderQuotaSnapshot(existing, incoming) {
20525
20597
  if (source2)
20526
20598
  fieldSources2[field] = source2;
20527
20599
  }
20600
+ if (quotaSnapshotOmitsFiveHour(existing) && !incoming.five_hour)
20601
+ fieldSources2.five_hour = "poll";
20528
20602
  return {
20529
20603
  ...existingWithoutPollOwnedFields,
20530
20604
  ...incomingWithoutPollOwnedFields,
@@ -20534,7 +20608,10 @@ function mergeHeaderQuotaSnapshot(existing, incoming) {
20534
20608
  bindingWindow: existing?.bindingWindowSource === "poll" ? existing.bindingWindow : incoming.bindingWindow ?? existing?.bindingWindow,
20535
20609
  bindingWindowSource: existing?.bindingWindowSource === "poll" ? "poll" : incoming.bindingWindowSource ?? existing?.bindingWindowSource,
20536
20610
  ...Object.keys(fieldSources2).length > 0 && { fieldSources: fieldSources2 },
20537
- source: "headers"
20611
+ source: "headers",
20612
+ ...quotaSnapshotOmitsFiveHour(existing) && {
20613
+ checkedAt: existing.checkedAt
20614
+ }
20538
20615
  };
20539
20616
  }
20540
20617
 
@@ -20575,7 +20652,8 @@ function createStickyNoRouteResponse(input) {
20575
20652
  const retryAfter = killswitchRetryAfterSeconds(firstQuota, remainingQuotas.map((quota2) => ({ quota: quota2 })), input.now ?? Date.now(), scopedBlocked ? modelId : undefined);
20576
20653
  const minutes = Math.floor(retryAfter / 60);
20577
20654
  const seconds = retryAfter % 60;
20578
- const reason = scopedBlocked && modelName ? `${modelName} weekly limit reached, no routable OAuth accounts.` : "No OAuth account currently satisfies sticky-balanced quota policy.";
20655
+ const limitNames = input.vaultPolicyLimits?.map((limit) => limit === "model-scoped" ? `${modelName ?? "Model"} scoped limit` : limit === "killswitch" ? "killswitch limit" : "quota floor");
20656
+ const reason = limitNames ? `No vault account currently satisfies quota policy: ${limitNames.join(", ")}. Open /claude → Killswitch / Quota to change these limits.` : scopedBlocked && modelName ? `${modelName} weekly limit reached, no routable OAuth accounts.` : "No OAuth account currently satisfies sticky-balanced quota policy.";
20579
20657
  const message = `${reason} Retry in ${minutes}m ${seconds}s.`;
20580
20658
  return new Response(JSON.stringify({
20581
20659
  type: "error",
@@ -20686,6 +20764,8 @@ function sessionKey(sessionId) {
20686
20764
  function stickyQuotaSnapshotIsFresh(quota2, storage, now = Date.now(), modelId) {
20687
20765
  const maxAge = getQuotaCheckIntervalMs(storage);
20688
20766
  const standardFresh = ["five_hour", "seven_day"].every((key) => {
20767
+ if (key === "five_hour" && quotaSnapshotOmitsFiveHour(quota2))
20768
+ return true;
20689
20769
  const window2 = quota2?.[key];
20690
20770
  return Boolean(window2 && now - window2.checkedAt < maxAge);
20691
20771
  });
@@ -20740,14 +20820,23 @@ function stickyRouteCandidateWeight(input) {
20740
20820
  }
20741
20821
  const weights = [];
20742
20822
  for (const key of ["five_hour", "seven_day"]) {
20823
+ if (key === "five_hour" && quotaSnapshotOmitsFiveHour(quota2))
20824
+ continue;
20743
20825
  const window2 = quota2[key];
20744
20826
  if (!window2 || !Number.isFinite(window2.remainingPercent))
20745
20827
  return 0;
20746
- weights.push(sustainableWindowWeight(window2.remainingPercent, remainingThreshold(storage, candidate.accountId, key), window2.resetsAt, now));
20828
+ const threshold = remainingThreshold(storage, candidate.accountId, key);
20829
+ if (window2.remainingPercent <= threshold) {
20830
+ const quotaThreshold = getQuotaMinimumRemainingThresholds(storage)[key];
20831
+ input.onPolicyBlock?.(isKillswitchEnabled(storage) && threshold > quotaThreshold ? "killswitch" : "quota-floor");
20832
+ }
20833
+ weights.push(sustainableWindowWeight(window2.remainingPercent, threshold, window2.resetsAt, now));
20747
20834
  }
20748
20835
  if (input.family === "fable") {
20749
20836
  const window2 = getScopedQuotaWindowForModel(quota2, input.modelId);
20750
20837
  if (window2 && Number.isFinite(window2.remainingPercent)) {
20838
+ if (window2.remainingPercent <= remainingThreshold(storage, candidate.accountId, "scoped"))
20839
+ input.onPolicyBlock?.("model-scoped");
20751
20840
  weights.push(sustainableWindowWeight(window2.remainingPercent, remainingThreshold(storage, candidate.accountId, "scoped"), window2.resetsAt, now));
20752
20841
  }
20753
20842
  }
@@ -26271,18 +26360,18 @@ function classifySafeErrorForLog(error) {
26271
26360
  } catch {}
26272
26361
  return { name, code };
26273
26362
  }
26274
- function logPublicationFailure(channel, error) {
26363
+ function logPublicationFailure(channel3, error) {
26275
26364
  let level = "warn";
26276
26365
  try {
26277
26366
  if (usagePublicationRefusalIsFatal(error))
26278
26367
  level = "error";
26279
26368
  } catch {}
26280
- logger[level](channel, "Account usage publication unavailable", classifySafeErrorForLog(error));
26369
+ logger[level](channel3, "Account usage publication unavailable", classifySafeErrorForLog(error));
26281
26370
  }
26282
- function createNativeUsagePublisher(channel) {
26371
+ function createNativeUsagePublisher(channel3) {
26283
26372
  const pending = new Set;
26284
26373
  const observe2 = (write) => {
26285
- const work = write.then(() => {}, (error) => logPublicationFailure(channel, error));
26374
+ const work = write.then(() => {}, (error) => logPublicationFailure(channel3, error));
26286
26375
  pending.add(work);
26287
26376
  work.then(() => pending.delete(work), () => pending.delete(work));
26288
26377
  return work;
@@ -26565,19 +26654,19 @@ function normalizeInsulaClaudeQuota(entry2, context) {
26565
26654
  kind: "weekly",
26566
26655
  minutes: 10080
26567
26656
  });
26568
- if (!five_hour || !seven_day)
26657
+ if (!seven_day || usage?.primary != null && !five_hour)
26569
26658
  return;
26570
26659
  const scoped = scopedReadings(usage?.extraRateWindows, checkedAt);
26571
26660
  const extraUsage2 = extraUsageReading(entry2.pools ?? usage?.pools);
26572
26661
  return {
26573
- five_hour,
26662
+ ...five_hour && { five_hour },
26574
26663
  seven_day,
26575
26664
  scoped,
26576
26665
  ...extraUsage2 && { extraUsage: extraUsage2 },
26577
26666
  source: "poll",
26578
26667
  checkedAt,
26579
26668
  fieldSources: {
26580
- five_hour: "poll",
26669
+ ...five_hour && { five_hour: "poll" },
26581
26670
  seven_day: "poll",
26582
26671
  scoped: "poll",
26583
26672
  ...extraUsage2 && { extraUsage: "poll" }
@@ -26594,7 +26683,7 @@ function newer(reading2, stored) {
26594
26683
  function mergeInsulaQuota(reading2, stored) {
26595
26684
  if (!stored || quotaSnapshotCheckedAt(stored) <= reading2.checkedAt)
26596
26685
  return reading2;
26597
- const five_hour = newer(reading2.five_hour, stored.five_hour);
26686
+ const five_hour = quotaSnapshotOmitsFiveHour(stored) ? undefined : !reading2.five_hour && (stored.five_hour?.checkedAt ?? 0) <= reading2.checkedAt ? undefined : newer(reading2.five_hour, stored.five_hour);
26598
26687
  const seven_day = newer(reading2.seven_day, stored.seven_day);
26599
26688
  const scoped = new Map(reading2.scoped.map((window2) => [window2.id, window2]));
26600
26689
  for (const window2 of stored.scoped ?? []) {
@@ -26609,10 +26698,10 @@ function mergeInsulaQuota(reading2, stored) {
26609
26698
  fieldSources2.seven_day = stored.fieldSources.seven_day;
26610
26699
  return {
26611
26700
  ...reading2,
26612
- ...five_hour && { five_hour },
26701
+ five_hour,
26613
26702
  ...seven_day && { seven_day },
26614
26703
  scoped: [...scoped.values()],
26615
- checkedAt: Math.max(reading2.checkedAt, quotaSnapshotCheckedAt(stored)),
26704
+ checkedAt: quotaSnapshotOmitsFiveHour(stored) ? stored.checkedAt : quotaSnapshotOmitsFiveHour(reading2) ? reading2.checkedAt : Math.max(reading2.checkedAt, quotaSnapshotCheckedAt(stored)),
26616
26705
  fieldSources: fieldSources2
26617
26706
  };
26618
26707
  }
@@ -26898,9 +26987,11 @@ function pauseOnRealTimer(ms) {
26898
26987
  class QuotaRefreshDeferred extends Error {
26899
26988
  }
26900
26989
  function mergePollCompletionWithNewerHeaders(current, polled) {
26990
+ if (quotaSnapshotOmitsFiveHour(current) && (polled.checkedAt ?? quotaSnapshotCheckedAt(polled)) < current.checkedAt)
26991
+ return current;
26901
26992
  if (current?.source !== "headers")
26902
26993
  return polled;
26903
- const fiveHourIsNewer = Boolean(current.five_hour && current.five_hour.checkedAt > (polled.five_hour?.checkedAt ?? 0));
26994
+ const fiveHourIsNewer = Boolean(current.five_hour && current.five_hour.checkedAt > (polled.five_hour?.checkedAt ?? polled.checkedAt ?? 0));
26904
26995
  const sevenDayIsNewer = Boolean(current.seven_day && current.seven_day.checkedAt > (polled.seven_day?.checkedAt ?? 0));
26905
26996
  if (!fiveHourIsNewer && !sevenDayIsNewer)
26906
26997
  return polled;
@@ -30239,8 +30330,15 @@ function acquireNativeAccountRuntime(options) {
30239
30330
  function nativeAccountPolicy(account, snapshot, modelId) {
30240
30331
  if (!nativeAccountCanServe(account))
30241
30332
  return { status: "blocked", reason: "account-disabled" };
30242
- if (!quotaSnapshotPassesPolicy(account.quota, snapshot.policyStorage) || !quotaSnapshotPassesModelScope(account.quota, modelId) || !killswitchPassesPolicy(account.quota, snapshot.policyStorage, account.id === "main" ? undefined : account.id, modelId))
30243
- return { status: "blocked", reason: "quota-ineligible" };
30333
+ const quotaPolicyLimits = [];
30334
+ if (!quotaSnapshotPassesPolicy(account.quota, snapshot.policyStorage))
30335
+ quotaPolicyLimits.push("quota-floor");
30336
+ if (!quotaSnapshotPassesModelScope(account.quota, modelId))
30337
+ quotaPolicyLimits.push("model-scoped");
30338
+ if (!killswitchPassesPolicy(account.quota, snapshot.policyStorage, account.id === "main" ? undefined : account.id, modelId))
30339
+ quotaPolicyLimits.push("killswitch");
30340
+ if (quotaPolicyLimits.length)
30341
+ return { status: "blocked", reason: "quota-ineligible", quotaPolicyLimits };
30244
30342
  return { status: "allowed" };
30245
30343
  }
30246
30344
  function createNativeAccountRuntime(options) {
@@ -35238,6 +35336,60 @@ async function disconnectNativeVaultForHost(options) {
35238
35336
  closeNativeVaultAuthority(paths.tokenPath);
35239
35337
  return result;
35240
35338
  }
35339
+ // src/native-vault-refusal.ts
35340
+ function createNativeVaultNoRouteResponse(input) {
35341
+ const { snapshot, modelId } = input;
35342
+ const accounts = snapshot.accounts.filter((account) => account.type === "oauth");
35343
+ if (accounts.some((account) => account.enabled && account.state === "needs_reauth"))
35344
+ throw new NativeVaultNoAccountError;
35345
+ const active2 = accounts.filter(nativeAccountCanServe);
35346
+ const limits = new Set;
35347
+ for (const account of active2) {
35348
+ const policy = input.policies?.get(account.id) ?? nativeAccountPolicy(account, snapshot, modelId);
35349
+ for (const limit of policy.quotaPolicyLimits ?? [])
35350
+ limits.add(limit);
35351
+ if (input.sticky) {
35352
+ stickyRouteCandidateWeight({
35353
+ candidate: {
35354
+ accountId: account.id,
35355
+ order: 0,
35356
+ quota: account.quota ?? { kind: "unknown" }
35357
+ },
35358
+ family: stickyRouteFamilyForModel(modelId),
35359
+ modelId,
35360
+ storage: snapshot.policyStorage,
35361
+ onPolicyBlock: (limit) => limits.add(limit)
35362
+ });
35363
+ }
35364
+ }
35365
+ if (!limits.size)
35366
+ throw new NativeVaultNoAccountError;
35367
+ return createStickyNoRouteResponse({
35368
+ routeQuotas: active2.flatMap((account) => account.quota ? [account.quota] : []),
35369
+ modelId,
35370
+ vaultPolicyLimits: [...limits]
35371
+ });
35372
+ }
35373
+
35374
+ class NativeVaultQuotaPolicyError extends Error {
35375
+ response;
35376
+ body;
35377
+ kind = "quota-policy";
35378
+ status = 429;
35379
+ headers;
35380
+ retryAfter;
35381
+ constructor(response, body) {
35382
+ super(body.error.message);
35383
+ this.response = response;
35384
+ this.body = body;
35385
+ this.name = "NativeVaultQuotaPolicyError";
35386
+ this.headers = new Headers(response.headers);
35387
+ this.retryAfter = Number(this.headers.get("retry-after"));
35388
+ }
35389
+ static async fromResponse(response) {
35390
+ return new NativeVaultQuotaPolicyError(response, await response.clone().json());
35391
+ }
35392
+ }
35241
35393
  // src/oauth-origin.ts
35242
35394
  var ANTHROPIC_OAUTH_ORIGIN = "https://api.anthropic.com";
35243
35395
  var ANTHROPIC_OAUTH_ORIGIN_REFUSAL = "Claude OAuth requests can only go to https://api.anthropic.com. A provider baseURL that points elsewhere is not used for Claude sign-in accounts.";
@@ -35687,6 +35839,12 @@ class PrimeManager {
35687
35839
  });
35688
35840
  return;
35689
35841
  }
35842
+ if (quotaSnapshotOmitsFiveHour(evaluation.storedQuota)) {
35843
+ logger.debug("prime", "no five-hour limit", {
35844
+ account: evaluation.label
35845
+ });
35846
+ return;
35847
+ }
35690
35848
  const storedResetEpoch = storedResetMs(evaluation.storedFiveHour);
35691
35849
  if (storedResetEpoch !== undefined && now < storedResetEpoch + PRIME_DUE_OFFSET_MS) {
35692
35850
  logger.trace("prime", "not due", { account: evaluation.label });
@@ -35744,6 +35902,12 @@ class PrimeManager {
35744
35902
  });
35745
35903
  return;
35746
35904
  }
35905
+ if (quotaSnapshotOmitsFiveHour(fresh)) {
35906
+ logger.debug("prime", "no five-hour limit", {
35907
+ account: evaluation.label
35908
+ });
35909
+ return;
35910
+ }
35747
35911
  if (!killswitchPassesPolicy(fresh, reloaded, evaluation.id, CLAUDE_HAIKU_4_5_MODEL_ID)) {
35748
35912
  logger.debug("prime", "ineligible", {
35749
35913
  account: evaluation.label,
@@ -39153,6 +39317,7 @@ export {
39153
39317
  quotaTextParts,
39154
39318
  quotaSnapshotPassesPolicy,
39155
39319
  quotaSnapshotPassesModelScope,
39320
+ quotaSnapshotOmitsFiveHour,
39156
39321
  quotaSnapshotModelScopeIsExhausted,
39157
39322
  quotaSnapshotHasStandardWindows,
39158
39323
  quotaSnapshotCheckedAt,
@@ -39389,6 +39554,7 @@ export {
39389
39554
  createProviderErrorRedactor,
39390
39555
  createProcessFence,
39391
39556
  createNativeVaultRuntime,
39557
+ createNativeVaultNoRouteResponse,
39392
39558
  createNativeUsagePublisher,
39393
39559
  createNativeUi,
39394
39560
  createNativePoolStore,
@@ -39501,6 +39667,7 @@ export {
39501
39667
  PARAGRAPH_REMOVAL_ANCHORS,
39502
39668
  OPENCODE_IDENTITY_PREFIX,
39503
39669
  OAUTH_SCOPES,
39670
+ NativeVaultQuotaPolicyError,
39504
39671
  NativeVaultNoAccountError,
39505
39672
  NativeTransientAccountError,
39506
39673
  NativeRuntimeError,
@@ -2,7 +2,6 @@ import type { InsulaQuotaContext, InsulaUsageEntry } from "./internal-types/clau
2
2
  import { type AccountQuotaWindow, type AccountScopedQuotaWindow, type OAuthQuotaSnapshot } from './accounts.ts';
3
3
  /** An insula reading in the shape our usage poll produces, before the account identity is attached. */
4
4
  export type InsulaClaudeQuota = OAuthQuotaSnapshot & {
5
- five_hour: AccountQuotaWindow;
6
5
  seven_day: AccountQuotaWindow;
7
6
  scoped: AccountScopedQuotaWindow[];
8
7
  checkedAt: number;
@@ -10,8 +9,8 @@ export type InsulaClaudeQuota = OAuthQuotaSnapshot & {
10
9
  /**
11
10
  * Convert insula's Claude entry into our usage-poll snapshot. Every window's
12
11
  * checkedAt is when insula fetched the reading, never when we read its cache,
13
- * so a newer header or poll reading still wins. Both standard windows must be
14
- * present; otherwise this returns undefined and our own poll stays in charge.
12
+ * so a newer header or poll reading still wins. The weekly window is required;
13
+ * an absent primary slot means the five-hour limit does not apply.
15
14
  * The tertiary slot (legacy seven_day_opus/sonnet) is not a window we model.
16
15
  */
17
16
  export declare function normalizeInsulaClaudeQuota(entry: InsulaUsageEntry, context: InsulaQuotaContext): InsulaClaudeQuota | undefined;
@@ -127,8 +127,12 @@ export interface NativeAccountRuntime {
127
127
  }
128
128
  /** Host instances own leases, while stores and the vault connection live until the last release. */
129
129
  export declare function acquireNativeAccountRuntime(options: NativeAccountRuntimeOptions): NativeAccountRuntime;
130
+ export type NativeQuotaPolicyLimit = 'quota-floor' | 'killswitch' | 'model-scoped';
131
+ export type NativeAccountPolicy = NativeLocalExternalPolicy & {
132
+ quotaPolicyLimits?: readonly NativeQuotaPolicyLimit[];
133
+ };
130
134
  /** Evaluate quota and killswitch policy for this caller's model, outside shared credential refresh work. */
131
- export declare function nativeAccountPolicy(account: NativeAccountView, snapshot: NativeAccountSnapshot, modelId?: string): NativeLocalExternalPolicy;
135
+ export declare function nativeAccountPolicy(account: NativeAccountView, snapshot: NativeAccountSnapshot, modelId?: string): NativeAccountPolicy;
132
136
  /** The local migration journal records completed transfer from host credentials and sidecars to the credential pool. Local use refuses without that record. Host vault enrollment instead selects independent settings and metadata, leaving local credentials, journal and metadata untouched. */
133
137
  export declare function createNativeAccountRuntime(options: NativeAccountRuntimeOptions): NativeAccountRuntime;
134
138
  export declare function primeDelta(delta: PrimeUsageDelta): {
@@ -0,0 +1,28 @@
1
+ import { type NativeAccountPolicy } from './native-account-runtime.ts';
2
+ import { type NativeAccountSnapshot } from './native-account-view.ts';
3
+ /** Preserve authentication precedence, then explain the shared policy's exclusions. */
4
+ export declare function createNativeVaultNoRouteResponse(input: {
5
+ snapshot: NativeAccountSnapshot;
6
+ modelId?: string;
7
+ policies?: ReadonlyMap<string, NativeAccountPolicy>;
8
+ sticky?: boolean;
9
+ }): Response;
10
+ type QuotaPolicyBody = {
11
+ type: 'error';
12
+ error: {
13
+ type: 'rate_limit_error';
14
+ message: string;
15
+ };
16
+ };
17
+ /** OpenCode 2 selection refuses before sending; its chooser cannot return a Response. */
18
+ export declare class NativeVaultQuotaPolicyError extends Error {
19
+ readonly response: Response;
20
+ readonly body: QuotaPolicyBody;
21
+ readonly kind = "quota-policy";
22
+ readonly status = 429;
23
+ readonly headers: Headers;
24
+ readonly retryAfter: number;
25
+ private constructor();
26
+ static fromResponse(response: Response): Promise<NativeVaultQuotaPolicyError>;
27
+ }
28
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { type AccountOperationError, type AccountStorage, type OAuthQuotaSnapshot } from './accounts.ts';
2
+ import type { NativeQuotaPolicyLimit } from './native-account-runtime.ts';
2
3
  import { type NativeLockEvent } from './native-lock-observer.ts';
3
4
  import type { NativeVaultRosterDocument } from './native-roster-seed.ts';
4
5
  import { type NativeVaultRoutingSeed } from './native-vault-seed.ts';
@@ -60,6 +61,7 @@ export declare function createStickyNoRouteResponse(input: {
60
61
  mainRefreshError?: AccountOperationError;
61
62
  fallbackReauthLabels?: readonly string[];
62
63
  routeQuotas: readonly OAuthQuotaSnapshot[];
64
+ vaultPolicyLimits?: readonly NativeQuotaPolicyLimit[];
63
65
  modelId?: string;
64
66
  now?: number;
65
67
  }): Response;
@@ -86,6 +88,7 @@ export declare function stickyRouteCandidateWeight(input: {
86
88
  modelId?: string;
87
89
  storage: AccountStorage | null;
88
90
  now?: number;
91
+ onPolicyBlock?: (limit: NativeQuotaPolicyLimit) => void;
89
92
  }): number;
90
93
  export declare function stickyRouteKnownFableExhausted(candidate: StickyRouteCandidate, storage: AccountStorage | null, now?: number): boolean;
91
94
  export declare function decideStickyQuotaFailure(input: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/anthropic-auth-core",
3
- "version": "2.4.6",
3
+ "version": "2.4.8",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,7 +30,7 @@
30
30
  "prepublishOnly": "bun run build"
31
31
  },
32
32
  "devDependencies": {
33
- "@cortexkit/common-auth": "0.31.0"
33
+ "@cortexkit/common-auth": "0.32.1"
34
34
  },
35
35
  "dependencies": {
36
36
  "@cortexkit/claustrum-client": "0.13.0",