@cortexkit/anthropic-auth-core 1.20.0 → 1.21.0
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/README.md +1 -1
- package/dist/accounts.d.ts +24 -14
- package/dist/accounts.js +234 -143
- package/dist/claude-code.d.ts +3 -1
- package/dist/claude-code.js +124 -21
- package/dist/dump.d.ts +1 -0
- package/dist/dump.js +6 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/oauth-profile.d.ts +2 -1
- package/dist/oauth-profile.js +10 -4
- package/dist/quota-header-feed.d.ts +40 -0
- package/dist/quota-header-feed.js +248 -0
- package/dist/quota-headers.js +6 -4
- package/dist/quota-manager.d.ts +36 -27
- package/dist/quota-manager.js +375 -144
- package/dist/sticky-routing.d.ts +14 -2
- package/dist/sticky-routing.js +30 -13
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -5,7 +5,6 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { parseRetryAfterHeader, refreshClaudeOAuthToken } from './auth.js';
|
|
6
6
|
import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from './constants.js';
|
|
7
7
|
import { log, logger } from './logger.js';
|
|
8
|
-
import { tokenFingerprint } from './token-fingerprint.js';
|
|
9
8
|
const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
|
|
10
9
|
const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
|
|
11
10
|
export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
|
|
@@ -147,6 +146,10 @@ function normalizeOAuthAccountProfile(value) {
|
|
|
147
146
|
tier: value.tier.trim(),
|
|
148
147
|
orgType: value.orgType.trim(),
|
|
149
148
|
checkedAt: value.checkedAt,
|
|
149
|
+
...(typeof value.accountIdentity === 'string' &&
|
|
150
|
+
value.accountIdentity.trim() && {
|
|
151
|
+
accountIdentity: value.accountIdentity.trim(),
|
|
152
|
+
}),
|
|
150
153
|
...(typeof value.tokenFingerprint === 'string' &&
|
|
151
154
|
value.tokenFingerprint.trim() && {
|
|
152
155
|
tokenFingerprint: value.tokenFingerprint.trim(),
|
|
@@ -169,6 +172,9 @@ function normalizeOperationError(value) {
|
|
|
169
172
|
checkedAt,
|
|
170
173
|
nextRetryAt: Number.isFinite(nextRetryAt) ? nextRetryAt : undefined,
|
|
171
174
|
retryCount: Number.isFinite(retryCount) ? retryCount : undefined,
|
|
175
|
+
accountIdentity: typeof value.accountIdentity === 'string' && value.accountIdentity.trim()
|
|
176
|
+
? value.accountIdentity.trim()
|
|
177
|
+
: undefined,
|
|
172
178
|
tokenHash: typeof value.tokenHash === 'string' ? value.tokenHash : undefined,
|
|
173
179
|
// Preserve the dead-token discriminators across save/load. Without these,
|
|
174
180
|
// a retry-exhausted transient (permanent=false, 24h backoff) would lose its
|
|
@@ -234,6 +240,10 @@ function normalizeQuota(value) {
|
|
|
234
240
|
if (typeof value.checkedAt === 'number' && Number.isFinite(value.checkedAt)) {
|
|
235
241
|
quota.checkedAt = value.checkedAt;
|
|
236
242
|
}
|
|
243
|
+
if (typeof value.accountIdentity === 'string' &&
|
|
244
|
+
value.accountIdentity.trim()) {
|
|
245
|
+
quota.accountIdentity = value.accountIdentity.trim();
|
|
246
|
+
}
|
|
237
247
|
if (Array.isArray(value.scoped)) {
|
|
238
248
|
const scoped = value.scoped
|
|
239
249
|
.map((entry) => {
|
|
@@ -332,6 +342,9 @@ function normalizeStorage(value) {
|
|
|
332
342
|
return null;
|
|
333
343
|
return {
|
|
334
344
|
version: 1,
|
|
345
|
+
mainAccountId: typeof value.mainAccountId === 'string' && value.mainAccountId.trim()
|
|
346
|
+
? value.mainAccountId.trim()
|
|
347
|
+
: undefined,
|
|
335
348
|
main: {
|
|
336
349
|
type: 'opencode',
|
|
337
350
|
provider: 'anthropic',
|
|
@@ -343,6 +356,9 @@ function normalizeStorage(value) {
|
|
|
343
356
|
: undefined,
|
|
344
357
|
refresh: isRecord(value.refresh) ? value.refresh : undefined,
|
|
345
358
|
quota: isRecord(value.quota) ? value.quota : undefined,
|
|
359
|
+
quotaHeaderFeed: isRecord(value.quotaHeaderFeed)
|
|
360
|
+
? value.quotaHeaderFeed
|
|
361
|
+
: undefined,
|
|
346
362
|
claudeCache: isRecord(value.claudeCache) ? value.claudeCache : undefined,
|
|
347
363
|
dump: isRecord(value.dump) ? value.dump : undefined,
|
|
348
364
|
claudeFast: isRecord(value.claudeFast) ? value.claudeFast : undefined,
|
|
@@ -403,6 +419,14 @@ function objectWithDefinedEntries(value) {
|
|
|
403
419
|
function numericField(value) {
|
|
404
420
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
405
421
|
}
|
|
422
|
+
// A clear is a monotonic tombstone; older or absent writes must not reopen backoff.
|
|
423
|
+
export function mergeMainQuotaErrorClearedAt(existing, incoming) {
|
|
424
|
+
if (incoming === undefined || !Number.isFinite(incoming))
|
|
425
|
+
return existing;
|
|
426
|
+
if (existing === undefined || !Number.isFinite(existing))
|
|
427
|
+
return incoming;
|
|
428
|
+
return Math.max(existing, incoming);
|
|
429
|
+
}
|
|
406
430
|
function accountCredentialTimestamp(value) {
|
|
407
431
|
return Math.max(numericField(value.lastRefreshedAt), numericField(value.lastUsed), numericField(value.addedAt));
|
|
408
432
|
}
|
|
@@ -447,6 +471,19 @@ function mergeConfigAndState(configValue, stateValue) {
|
|
|
447
471
|
const refreshConfig = isRecord(configValue.refresh) ? configValue.refresh : {};
|
|
448
472
|
const mainQuotaSource = mainState ?? quotaConfig;
|
|
449
473
|
const mainRefreshSource = mainState ?? refreshConfig;
|
|
474
|
+
const mainQuotaErrorClearedAt = typeof mainState?.quotaErrorClearedAt === 'number' &&
|
|
475
|
+
Number.isFinite(mainState.quotaErrorClearedAt) &&
|
|
476
|
+
mainState.quotaErrorClearedAt >= 0
|
|
477
|
+
? mainState.quotaErrorClearedAt
|
|
478
|
+
: undefined;
|
|
479
|
+
const configQuotaError = quotaConfig.mainLastQuotaApiError;
|
|
480
|
+
const mainLastQuotaApiError = mainState?.lastQuotaApiError ??
|
|
481
|
+
(mainQuotaErrorClearedAt !== undefined &&
|
|
482
|
+
isRecord(configQuotaError) &&
|
|
483
|
+
typeof configQuotaError.checkedAt === 'number' &&
|
|
484
|
+
configQuotaError.checkedAt <= mainQuotaErrorClearedAt
|
|
485
|
+
? undefined
|
|
486
|
+
: configQuotaError);
|
|
450
487
|
const accounts = Array.isArray(configValue.accounts)
|
|
451
488
|
? configValue.accounts.map((account) => {
|
|
452
489
|
if (!isRecord(account))
|
|
@@ -476,7 +513,9 @@ function mergeConfigAndState(configValue, stateValue) {
|
|
|
476
513
|
mainQuota: mainQuotaSource.quota,
|
|
477
514
|
mainQuotaCheckedAt: mainQuotaSource.quotaCheckedAt,
|
|
478
515
|
mainQuotaToken: mainQuotaSource.quotaToken,
|
|
479
|
-
mainLastQuotaApiError
|
|
516
|
+
mainLastQuotaApiError,
|
|
517
|
+
mainQuotaErrorGeneration: mainQuotaSource.quotaErrorGeneration,
|
|
518
|
+
mainQuotaErrorClearedAt: mainQuotaSource.quotaErrorClearedAt,
|
|
480
519
|
}),
|
|
481
520
|
// Carry the main-side prime counters from the state file back into the
|
|
482
521
|
// merged storage so a subsequent read sees the cumulative counters. The
|
|
@@ -610,7 +649,7 @@ function mergeHeaderOwnedWindow(existingSnapshot, incomingSnapshot, key) {
|
|
|
610
649
|
? existing
|
|
611
650
|
: incoming;
|
|
612
651
|
}
|
|
613
|
-
function mergeHeaderQuotaForPersistence(existing, incoming) {
|
|
652
|
+
export function mergeHeaderQuotaForPersistence(existing, incoming) {
|
|
614
653
|
if (!existing || incoming.source !== 'headers')
|
|
615
654
|
return incoming;
|
|
616
655
|
const preservePollBinding = existing.bindingWindowSource === 'poll';
|
|
@@ -730,11 +769,13 @@ function configFromStorage(storage) {
|
|
|
730
769
|
: undefined;
|
|
731
770
|
return omitUndefinedTopLevel({
|
|
732
771
|
version: 1,
|
|
772
|
+
mainAccountId: storage.mainAccountId,
|
|
733
773
|
main: { type: 'opencode', provider: 'anthropic' },
|
|
734
774
|
routing: storage.routing,
|
|
735
775
|
fallbackOn: storage.fallbackOn,
|
|
736
776
|
refresh,
|
|
737
777
|
quota,
|
|
778
|
+
quotaHeaderFeed: storage.quotaHeaderFeed,
|
|
738
779
|
claudeCache: storage.claudeCache,
|
|
739
780
|
dump: storage.dump,
|
|
740
781
|
logging: storage.logging,
|
|
@@ -857,6 +898,27 @@ export function saveAccounts(storage, path = getAccountStoragePath(), options =
|
|
|
857
898
|
const resolvedPath = path;
|
|
858
899
|
return enqueueSave(() => saveAccountsLocked(storage, resolvedPath, options));
|
|
859
900
|
}
|
|
901
|
+
export async function getOrCreateMainAccountId(path = getAccountStoragePath(), createId = randomUUID) {
|
|
902
|
+
return enqueueSave(async () => {
|
|
903
|
+
const lock = await acquireAccountConfigWriteLock(path);
|
|
904
|
+
try {
|
|
905
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
906
|
+
if (storage.mainAccountId)
|
|
907
|
+
return storage.mainAccountId;
|
|
908
|
+
const mainAccountId = createId();
|
|
909
|
+
const nextStorage = { ...storage, mainAccountId };
|
|
910
|
+
const existing = await loadExistingTopLevelFields(path);
|
|
911
|
+
await writeJsonAtomic(path, {
|
|
912
|
+
...existing,
|
|
913
|
+
...configFromStorage(nextStorage),
|
|
914
|
+
});
|
|
915
|
+
return mainAccountId;
|
|
916
|
+
}
|
|
917
|
+
finally {
|
|
918
|
+
await lock.release();
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
}
|
|
860
922
|
async function saveAccountsLocked(storage, path, options) {
|
|
861
923
|
const lock = await acquireAccountConfigWriteLock(path);
|
|
862
924
|
try {
|
|
@@ -892,6 +954,74 @@ function applyMainProfileStatePatch(state, storage) {
|
|
|
892
954
|
}
|
|
893
955
|
function applyMainQuotaStatePatch(state, storage) {
|
|
894
956
|
state.main = state.main ?? {};
|
|
957
|
+
const incomingError = storage.quota?.mainLastQuotaApiError;
|
|
958
|
+
const incomingGeneration = typeof storage.quota?.mainQuotaErrorGeneration === 'number' &&
|
|
959
|
+
Number.isSafeInteger(storage.quota.mainQuotaErrorGeneration) &&
|
|
960
|
+
storage.quota.mainQuotaErrorGeneration >= 0
|
|
961
|
+
? storage.quota.mainQuotaErrorGeneration
|
|
962
|
+
: undefined;
|
|
963
|
+
const incomingClearedAt = typeof storage.quota?.mainQuotaErrorClearedAt === 'number' &&
|
|
964
|
+
Number.isFinite(storage.quota.mainQuotaErrorClearedAt) &&
|
|
965
|
+
storage.quota.mainQuotaErrorClearedAt >= 0
|
|
966
|
+
? storage.quota.mainQuotaErrorClearedAt
|
|
967
|
+
: undefined;
|
|
968
|
+
const existingGeneration = typeof state.main.quotaErrorGeneration === 'number' &&
|
|
969
|
+
Number.isSafeInteger(state.main.quotaErrorGeneration) &&
|
|
970
|
+
state.main.quotaErrorGeneration >= 0
|
|
971
|
+
? state.main.quotaErrorGeneration
|
|
972
|
+
: 0;
|
|
973
|
+
const existingErrorObservedAt = typeof state.main.lastQuotaApiError?.checkedAt === 'number' &&
|
|
974
|
+
Number.isFinite(state.main.lastQuotaApiError.checkedAt) &&
|
|
975
|
+
state.main.lastQuotaApiError.checkedAt >= 0
|
|
976
|
+
? state.main.lastQuotaApiError.checkedAt
|
|
977
|
+
: undefined;
|
|
978
|
+
const existingClearedAt = typeof state.main.quotaErrorClearedAt === 'number' &&
|
|
979
|
+
Number.isFinite(state.main.quotaErrorClearedAt) &&
|
|
980
|
+
state.main.quotaErrorClearedAt >= 0
|
|
981
|
+
? state.main.quotaErrorClearedAt
|
|
982
|
+
: undefined;
|
|
983
|
+
const existingObservedAt = Math.max(existingErrorObservedAt ?? 0, existingClearedAt ?? 0);
|
|
984
|
+
const incomingErrorObservedAt = typeof incomingError?.checkedAt === 'number' &&
|
|
985
|
+
Number.isFinite(incomingError.checkedAt) &&
|
|
986
|
+
incomingError.checkedAt >= 0
|
|
987
|
+
? incomingError.checkedAt
|
|
988
|
+
: undefined;
|
|
989
|
+
// Error transitions are ordered by observation time, not write order. All
|
|
990
|
+
// instances share one host clock, so an error observed before a clear cannot
|
|
991
|
+
// resurrect that clear even when both processes allocated the same generation.
|
|
992
|
+
if (incomingError === undefined &&
|
|
993
|
+
incomingClearedAt !== undefined &&
|
|
994
|
+
incomingClearedAt >= existingObservedAt) {
|
|
995
|
+
state.main.lastQuotaApiError = undefined;
|
|
996
|
+
state.main.quotaErrorClearedAt = mergeMainQuotaErrorClearedAt(existingClearedAt, incomingClearedAt);
|
|
997
|
+
if (incomingGeneration !== undefined &&
|
|
998
|
+
incomingGeneration >= existingGeneration) {
|
|
999
|
+
state.main.quotaErrorGeneration = incomingGeneration;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
else if (incomingError) {
|
|
1003
|
+
const acceptsByObservation = incomingErrorObservedAt !== undefined &&
|
|
1004
|
+
incomingErrorObservedAt > existingObservedAt;
|
|
1005
|
+
const acceptsEqualObservation = incomingErrorObservedAt !== undefined &&
|
|
1006
|
+
incomingErrorObservedAt === existingObservedAt &&
|
|
1007
|
+
existingErrorObservedAt !== undefined &&
|
|
1008
|
+
existingClearedAt === undefined &&
|
|
1009
|
+
incomingGeneration !== undefined &&
|
|
1010
|
+
incomingGeneration >= existingGeneration;
|
|
1011
|
+
const acceptsLegacyGeneration = incomingErrorObservedAt === undefined &&
|
|
1012
|
+
incomingClearedAt === undefined &&
|
|
1013
|
+
incomingGeneration !== undefined &&
|
|
1014
|
+
incomingGeneration >= existingGeneration &&
|
|
1015
|
+
existingClearedAt === undefined;
|
|
1016
|
+
if (acceptsByObservation ||
|
|
1017
|
+
acceptsEqualObservation ||
|
|
1018
|
+
acceptsLegacyGeneration) {
|
|
1019
|
+
state.main.lastQuotaApiError = incomingError;
|
|
1020
|
+
if (incomingGeneration !== undefined) {
|
|
1021
|
+
state.main.quotaErrorGeneration = Math.max(existingGeneration, incomingGeneration);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
895
1025
|
const incomingQuota = storage.quota?.mainQuota;
|
|
896
1026
|
const sameToken = Boolean(state.main.quotaToken &&
|
|
897
1027
|
storage.quota?.mainQuotaToken &&
|
|
@@ -918,7 +1048,6 @@ function applyMainQuotaStatePatch(state, storage) {
|
|
|
918
1048
|
? Math.max(existingCheckedAt, incomingCheckedAt)
|
|
919
1049
|
: storage.quota?.mainQuotaCheckedAt;
|
|
920
1050
|
state.main.quotaToken = storage.quota?.mainQuotaToken;
|
|
921
|
-
state.main.lastQuotaApiError = storage.quota?.mainLastQuotaApiError;
|
|
922
1051
|
}
|
|
923
1052
|
function applyMainRefreshStatePatch(state, storage) {
|
|
924
1053
|
state.main = state.main ?? {};
|
|
@@ -989,38 +1118,41 @@ export function saveOAuthProfileState(input, path = getAccountStoragePath()) {
|
|
|
989
1118
|
const next = isRecord(existing)
|
|
990
1119
|
? { ...existing, version: 1 }
|
|
991
1120
|
: { version: 1 };
|
|
992
|
-
const { accountId,
|
|
993
|
-
if (profile?.tokenFingerprint &&
|
|
994
|
-
profile.tokenFingerprint !== expectedTokenFingerprint) {
|
|
995
|
-
return false;
|
|
996
|
-
}
|
|
1121
|
+
const { accountId, accountIdentity, profile } = input;
|
|
997
1122
|
if (accountId === 'main') {
|
|
998
1123
|
next.main = { ...(next.main ?? {}) };
|
|
999
1124
|
const existingProfile = normalizeOAuthAccountProfile(next.main.profile);
|
|
1000
|
-
|
|
1001
|
-
if (!profile &&
|
|
1002
|
-
existingProfile &&
|
|
1003
|
-
persistedToken === expectedTokenFingerprint) {
|
|
1004
|
-
return false;
|
|
1005
|
-
}
|
|
1006
|
-
if (profile) {
|
|
1007
|
-
if (persistedToken && persistedToken !== expectedTokenFingerprint) {
|
|
1008
|
-
return false;
|
|
1009
|
-
}
|
|
1125
|
+
if (!profile) {
|
|
1010
1126
|
if (existingProfile &&
|
|
1011
|
-
|
|
1127
|
+
(accountIdentity === undefined ||
|
|
1128
|
+
existingProfile.accountIdentity === undefined ||
|
|
1129
|
+
existingProfile.accountIdentity === accountIdentity)) {
|
|
1012
1130
|
return false;
|
|
1013
1131
|
}
|
|
1132
|
+
next.main.profile = undefined;
|
|
1133
|
+
return await writeJsonAtomic(statePath, pruneUndefined(next)).then(() => true);
|
|
1014
1134
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1135
|
+
if (profile.accountIdentity !== undefined &&
|
|
1136
|
+
profile.accountIdentity !== accountIdentity) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
const persistedProfile = accountIdentity === undefined ||
|
|
1140
|
+
profile.accountIdentity === accountIdentity
|
|
1141
|
+
? profile
|
|
1142
|
+
: { ...profile, accountIdentity };
|
|
1143
|
+
if (existingProfile &&
|
|
1144
|
+
(accountIdentity === undefined ||
|
|
1145
|
+
existingProfile.accountIdentity === undefined ||
|
|
1146
|
+
existingProfile.accountIdentity === accountIdentity) &&
|
|
1147
|
+
existingProfile.checkedAt > persistedProfile.checkedAt) {
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
next.main.profile = persistedProfile;
|
|
1017
1151
|
}
|
|
1018
1152
|
else {
|
|
1019
1153
|
const account = current?.accounts.find((candidate) => candidate.id === accountId && isOAuthAccount(candidate));
|
|
1020
|
-
if (!account
|
|
1021
|
-
tokenFingerprint(account.access) !== expectedTokenFingerprint) {
|
|
1154
|
+
if (!account)
|
|
1022
1155
|
return false;
|
|
1023
|
-
}
|
|
1024
1156
|
next.accounts = {
|
|
1025
1157
|
...(isRecord(next.accounts) ? next.accounts : {}),
|
|
1026
1158
|
};
|
|
@@ -1028,16 +1160,33 @@ export function saveOAuthProfileState(input, path = getAccountStoragePath()) {
|
|
|
1028
1160
|
? { ...next.accounts[accountId] }
|
|
1029
1161
|
: {};
|
|
1030
1162
|
const existingProfile = normalizeOAuthAccountProfile(existingEntry.profile);
|
|
1031
|
-
if (!profile
|
|
1032
|
-
existingProfile
|
|
1033
|
-
|
|
1163
|
+
if (!profile) {
|
|
1164
|
+
if (existingProfile &&
|
|
1165
|
+
(accountIdentity === undefined ||
|
|
1166
|
+
existingProfile.accountIdentity === undefined ||
|
|
1167
|
+
existingProfile.accountIdentity === accountIdentity)) {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
existingEntry.profile = undefined;
|
|
1034
1171
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1172
|
+
else {
|
|
1173
|
+
if (profile.accountIdentity !== undefined &&
|
|
1174
|
+
profile.accountIdentity !== accountIdentity) {
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
const persistedProfile = accountIdentity === undefined ||
|
|
1178
|
+
profile.accountIdentity === accountIdentity
|
|
1179
|
+
? profile
|
|
1180
|
+
: { ...profile, accountIdentity };
|
|
1181
|
+
if (existingProfile &&
|
|
1182
|
+
(accountIdentity === undefined ||
|
|
1183
|
+
existingProfile.accountIdentity === undefined ||
|
|
1184
|
+
existingProfile.accountIdentity === accountIdentity) &&
|
|
1185
|
+
existingProfile.checkedAt > persistedProfile.checkedAt) {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
existingEntry.profile = persistedProfile;
|
|
1039
1189
|
}
|
|
1040
|
-
existingEntry.profile = profile;
|
|
1041
1190
|
next.accounts[accountId] = existingEntry;
|
|
1042
1191
|
}
|
|
1043
1192
|
await writeJsonAtomic(statePath, pruneUndefined(next));
|
|
@@ -1421,12 +1570,8 @@ export async function setPrimePersistentEnabled(enabled, path = getAccountStorag
|
|
|
1421
1570
|
await saveAccounts(storage, path);
|
|
1422
1571
|
return storage;
|
|
1423
1572
|
}
|
|
1424
|
-
/**
|
|
1425
|
-
|
|
1426
|
-
* runtime state when older storage has no lineage yet. Main lookups return
|
|
1427
|
-
* undefined when the live refresh token is unavailable and no lineage exists.
|
|
1428
|
-
*/
|
|
1429
|
-
export async function getOrCreatePrimeAuthLineageId(accountId, path = getAccountStoragePath(), mainRefreshToken) {
|
|
1573
|
+
/** Return the stable prime marker identity for an OAuth account. */
|
|
1574
|
+
export async function getOrCreatePrimeAuthLineageId(accountId, path = getAccountStoragePath()) {
|
|
1430
1575
|
return enqueueSave(async () => {
|
|
1431
1576
|
const configLock = await acquireAccountConfigWriteLock(path);
|
|
1432
1577
|
try {
|
|
@@ -1435,52 +1580,23 @@ export async function getOrCreatePrimeAuthLineageId(accountId, path = getAccount
|
|
|
1435
1580
|
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1436
1581
|
if (accountId === 'main') {
|
|
1437
1582
|
const existing = storage.prime?.mainAuthLineageId;
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
storage.refresh.mainRefreshLeaseUntil > Date.now()) {
|
|
1449
|
-
// The owner binds before publishing its rotated credential. An
|
|
1450
|
-
// active lease makes either side of that handoff ambiguous, so
|
|
1451
|
-
// suppress this tick instead of classifying it as a re-login.
|
|
1452
|
-
return existing;
|
|
1453
|
-
}
|
|
1454
|
-
if (!boundFingerprint) {
|
|
1455
|
-
// Legacy lineages predate refresh-token binding. The first
|
|
1456
|
-
// observation attaches the credential without changing markers.
|
|
1457
|
-
storage.prime = {
|
|
1458
|
-
...(storage.prime ?? {}),
|
|
1459
|
-
mainAuthLineageRefreshTokenFingerprint: observedFingerprint,
|
|
1460
|
-
};
|
|
1461
|
-
await saveAccountStateUnlocked(storage, path, {
|
|
1462
|
-
mainPrime: true,
|
|
1463
|
-
});
|
|
1464
|
-
return existing;
|
|
1465
|
-
}
|
|
1583
|
+
if (existing)
|
|
1584
|
+
return existing;
|
|
1585
|
+
const mainAccountId = storage.mainAccountId ?? randomUUID();
|
|
1586
|
+
if (!storage.mainAccountId) {
|
|
1587
|
+
const existingConfig = await loadExistingTopLevelFields(path);
|
|
1588
|
+
await writeJsonAtomic(path, {
|
|
1589
|
+
...existingConfig,
|
|
1590
|
+
...configFromStorage({ ...storage, mainAccountId }),
|
|
1591
|
+
});
|
|
1592
|
+
storage.mainAccountId = mainAccountId;
|
|
1466
1593
|
}
|
|
1467
|
-
// Without the live host credential there is no safe way to bind a
|
|
1468
|
-
// new identity. Skip this tick rather than creating an unbound
|
|
1469
|
-
// namespace that could churn or suppress another account.
|
|
1470
|
-
if (!observedFingerprint)
|
|
1471
|
-
return undefined;
|
|
1472
|
-
// A bound mismatch that did not pass through the owned refresh path
|
|
1473
|
-
// is a host credential replacement and needs a fresh marker identity.
|
|
1474
|
-
const authLineageId = randomUUID();
|
|
1475
1594
|
storage.prime = {
|
|
1476
1595
|
...(storage.prime ?? {}),
|
|
1477
|
-
mainAuthLineageId:
|
|
1478
|
-
...(observedFingerprint && {
|
|
1479
|
-
mainAuthLineageRefreshTokenFingerprint: observedFingerprint,
|
|
1480
|
-
}),
|
|
1596
|
+
mainAuthLineageId: mainAccountId,
|
|
1481
1597
|
};
|
|
1482
1598
|
await saveAccountStateUnlocked(storage, path, { mainPrime: true });
|
|
1483
|
-
return
|
|
1599
|
+
return mainAccountId;
|
|
1484
1600
|
}
|
|
1485
1601
|
const account = storage.accounts.find((candidate) => candidate.id === accountId && isOAuthAccount(candidate));
|
|
1486
1602
|
if (!account) {
|
|
@@ -1507,42 +1623,6 @@ export async function getOrCreatePrimeAuthLineageId(accountId, path = getAccount
|
|
|
1507
1623
|
}
|
|
1508
1624
|
});
|
|
1509
1625
|
}
|
|
1510
|
-
/** Advance a main lineage across a refresh performed by this plugin. */
|
|
1511
|
-
export async function continueMainPrimeAuthLineageAfterRefresh(input, path = getAccountStoragePath()) {
|
|
1512
|
-
return enqueueSave(async () => {
|
|
1513
|
-
const configLock = await acquireAccountConfigWriteLock(path);
|
|
1514
|
-
try {
|
|
1515
|
-
const stateLock = await acquireAccountStateWriteLock(path);
|
|
1516
|
-
try {
|
|
1517
|
-
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1518
|
-
const authLineageId = storage.prime?.mainAuthLineageId;
|
|
1519
|
-
if (!authLineageId)
|
|
1520
|
-
return;
|
|
1521
|
-
const previousFingerprint = tokenFingerprint(input.previousRefreshToken);
|
|
1522
|
-
const currentFingerprint = tokenFingerprint(input.currentRefreshToken);
|
|
1523
|
-
const boundFingerprint = storage.prime?.mainAuthLineageRefreshTokenFingerprint;
|
|
1524
|
-
if (boundFingerprint &&
|
|
1525
|
-
boundFingerprint !== previousFingerprint &&
|
|
1526
|
-
boundFingerprint !== currentFingerprint) {
|
|
1527
|
-
// Another process observed a host replacement after this refresh
|
|
1528
|
-
// started; the stale rotation must not rebind that newer lineage.
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
storage.prime = {
|
|
1532
|
-
...(storage.prime ?? {}),
|
|
1533
|
-
mainAuthLineageRefreshTokenFingerprint: currentFingerprint,
|
|
1534
|
-
};
|
|
1535
|
-
await saveAccountStateUnlocked(storage, path, { mainPrime: true });
|
|
1536
|
-
}
|
|
1537
|
-
finally {
|
|
1538
|
-
await stateLock.release();
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
finally {
|
|
1542
|
-
await configLock.release();
|
|
1543
|
-
}
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
1626
|
/**
|
|
1547
1627
|
* Atomically increment an account's cumulative prime counters and persist via
|
|
1548
1628
|
* the scoped runtime-state path. The `main` account lives at state.main.prime;
|
|
@@ -1659,9 +1739,8 @@ function isTransientRefreshError(error) {
|
|
|
1659
1739
|
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
1660
1740
|
}
|
|
1661
1741
|
export function buildRefreshOperationError(input) {
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
? (input.previous.retryCount ?? 0)
|
|
1742
|
+
const previousRetryCount = input.previous?.accountIdentity === input.accountIdentity
|
|
1743
|
+
? (input.previous?.retryCount ?? 0)
|
|
1665
1744
|
: 0;
|
|
1666
1745
|
const retryCount = previousRetryCount + 1;
|
|
1667
1746
|
const retryAfterFromError = input.error
|
|
@@ -1696,7 +1775,7 @@ export function buildRefreshOperationError(input) {
|
|
|
1696
1775
|
checkedAt: input.now,
|
|
1697
1776
|
nextRetryAt: input.now + delay,
|
|
1698
1777
|
retryCount,
|
|
1699
|
-
|
|
1778
|
+
accountIdentity: input.accountIdentity,
|
|
1700
1779
|
status,
|
|
1701
1780
|
permanent: status === 400 && isInvalidGrant,
|
|
1702
1781
|
};
|
|
@@ -1729,12 +1808,14 @@ export function isPermanentRefreshError(error) {
|
|
|
1729
1808
|
}
|
|
1730
1809
|
return false;
|
|
1731
1810
|
}
|
|
1732
|
-
export function refreshBackoffActive(error,
|
|
1811
|
+
export function refreshBackoffActive(error, accountIdentity, now) {
|
|
1733
1812
|
if (!error?.nextRetryAt || error.nextRetryAt <= now)
|
|
1734
1813
|
return false;
|
|
1735
|
-
if (!
|
|
1814
|
+
if (!error.accountIdentity)
|
|
1815
|
+
return true;
|
|
1816
|
+
if (!accountIdentity)
|
|
1736
1817
|
return true;
|
|
1737
|
-
return error.
|
|
1818
|
+
return error.accountIdentity === accountIdentity;
|
|
1738
1819
|
}
|
|
1739
1820
|
export function formatRefreshBackoffMessage(error, now) {
|
|
1740
1821
|
const seconds = Math.max(1, Math.ceil(((error.nextRetryAt ?? now) - now) / 1000));
|
|
@@ -1768,7 +1849,9 @@ export function isQuotaPolicyAuthError(error) {
|
|
|
1768
1849
|
return /Claude quota check failed: 403\b/.test(formatErrorMessage(error));
|
|
1769
1850
|
}
|
|
1770
1851
|
export function buildQuotaOperationError(input) {
|
|
1771
|
-
const previousRetryCount = input.previous?.
|
|
1852
|
+
const previousRetryCount = input.previous?.accountIdentity === input.accountIdentity
|
|
1853
|
+
? (input.previous?.retryCount ?? 0)
|
|
1854
|
+
: 0;
|
|
1772
1855
|
const retryCount = previousRetryCount + 1;
|
|
1773
1856
|
const delay = isTransientQuotaError(input.error)
|
|
1774
1857
|
? Math.min(MAX_QUOTA_RETRY_DELAY_MS, MIN_QUOTA_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6))
|
|
@@ -1778,6 +1861,9 @@ export function buildQuotaOperationError(input) {
|
|
|
1778
1861
|
checkedAt: input.now,
|
|
1779
1862
|
nextRetryAt: input.now + delay,
|
|
1780
1863
|
retryCount,
|
|
1864
|
+
...(input.accountIdentity !== undefined && {
|
|
1865
|
+
accountIdentity: input.accountIdentity,
|
|
1866
|
+
}),
|
|
1781
1867
|
};
|
|
1782
1868
|
}
|
|
1783
1869
|
export function quotaBackoffActive(error, now) {
|
|
@@ -1813,6 +1899,7 @@ export function getPersistedMainQuota(storage) {
|
|
|
1813
1899
|
quota: storage.quota.mainQuota,
|
|
1814
1900
|
checkedAt: storage.quota.mainQuotaCheckedAt,
|
|
1815
1901
|
tokenFingerprint: storage.quota.mainQuotaToken,
|
|
1902
|
+
accountIdentity: storage.quota.mainQuota.accountIdentity,
|
|
1816
1903
|
};
|
|
1817
1904
|
}
|
|
1818
1905
|
/**
|
|
@@ -1879,6 +1966,9 @@ export function quotaSnapshotPassesPolicy(quota, storage) {
|
|
|
1879
1966
|
}
|
|
1880
1967
|
return true;
|
|
1881
1968
|
}
|
|
1969
|
+
export function quotaSnapshotHasStandardWindows(quota) {
|
|
1970
|
+
return Boolean(quota?.five_hour && quota.seven_day);
|
|
1971
|
+
}
|
|
1882
1972
|
// ---------------------------------------------------------------------------
|
|
1883
1973
|
// Killswitch — hard-block requests when remaining quota drops below per-account
|
|
1884
1974
|
// thresholds, even if the API would still accept them.
|
|
@@ -2351,7 +2441,7 @@ function recordRefreshError(account, error, now) {
|
|
|
2351
2441
|
account.lastRefreshError = buildRefreshOperationError({
|
|
2352
2442
|
error,
|
|
2353
2443
|
now,
|
|
2354
|
-
|
|
2444
|
+
accountIdentity: account.id,
|
|
2355
2445
|
previous: account.lastRefreshError,
|
|
2356
2446
|
});
|
|
2357
2447
|
}
|
|
@@ -2406,7 +2496,7 @@ export class FallbackAccountManager {
|
|
|
2406
2496
|
const checkedAt = quotaSnapshotCheckedAt(account.quota);
|
|
2407
2497
|
if (checkedAt <= 0)
|
|
2408
2498
|
return;
|
|
2409
|
-
const existing = this.quotaManager.getFallback(account.id, account
|
|
2499
|
+
const existing = this.quotaManager.getFallback(account.id, account);
|
|
2410
2500
|
if (existing && existing.checkedAt >= checkedAt)
|
|
2411
2501
|
return;
|
|
2412
2502
|
const checkInterval = getQuotaCheckIntervalMs(storage);
|
|
@@ -2414,7 +2504,7 @@ export class FallbackAccountManager {
|
|
|
2414
2504
|
quota: account.quota,
|
|
2415
2505
|
refreshAfter: checkedAt + checkInterval,
|
|
2416
2506
|
checkedAt,
|
|
2417
|
-
}, account
|
|
2507
|
+
}, account);
|
|
2418
2508
|
}
|
|
2419
2509
|
async load() {
|
|
2420
2510
|
return loadAccounts(this.configPath);
|
|
@@ -2455,12 +2545,12 @@ export class FallbackAccountManager {
|
|
|
2455
2545
|
for (const account of storage.accounts) {
|
|
2456
2546
|
if (account.enabled === false || !isOAuthAccount(account))
|
|
2457
2547
|
continue;
|
|
2548
|
+
let next = account;
|
|
2458
2549
|
try {
|
|
2459
|
-
let next = account;
|
|
2460
2550
|
if (tokenNeedsRefresh(next, storage, this.now())) {
|
|
2461
2551
|
const refreshError = next.lastRefreshError;
|
|
2462
2552
|
if (refreshError &&
|
|
2463
|
-
refreshBackoffActive(refreshError, next.
|
|
2553
|
+
refreshBackoffActive(refreshError, next.id, this.now())) {
|
|
2464
2554
|
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2465
2555
|
}
|
|
2466
2556
|
next = await this.refreshAccount(next, storage);
|
|
@@ -2468,7 +2558,7 @@ export class FallbackAccountManager {
|
|
|
2468
2558
|
}
|
|
2469
2559
|
this.seedFallbackQuota(next, storage);
|
|
2470
2560
|
const stale = this.quotaManager
|
|
2471
|
-
? this.quotaManager.isFallbackStale(next.id, next.access, options.modelId)
|
|
2561
|
+
? this.quotaManager.isFallbackStale(next.id, next.access, options.modelId, next)
|
|
2472
2562
|
: quotaIsStale(next, storage, this.now(), options.modelId);
|
|
2473
2563
|
// Skip the request-time refresh when this account's quota API is
|
|
2474
2564
|
// backed off (recent 429/5xx). Hitting it again would extend the
|
|
@@ -2488,20 +2578,21 @@ export class FallbackAccountManager {
|
|
|
2488
2578
|
usable.push(next);
|
|
2489
2579
|
}
|
|
2490
2580
|
catch (error) {
|
|
2491
|
-
if (canUseCachedQuotaAfterRefreshError(
|
|
2581
|
+
if (canUseCachedQuotaAfterRefreshError(next, storage, error, this.now())) {
|
|
2492
2582
|
log('[refresh] fallback quota using cached quota after refresh error', {
|
|
2493
|
-
accountId:
|
|
2583
|
+
accountId: next.id,
|
|
2494
2584
|
error: formatErrorMessage(error),
|
|
2495
2585
|
});
|
|
2496
|
-
if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(
|
|
2586
|
+
if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(next), storage, {
|
|
2497
2587
|
modelId: options.modelId,
|
|
2498
2588
|
})) {
|
|
2499
|
-
usable.push(
|
|
2589
|
+
usable.push(next);
|
|
2500
2590
|
}
|
|
2501
2591
|
}
|
|
2502
2592
|
else if (!failClosedOnUnknownQuota(storage) &&
|
|
2503
|
-
|
|
2504
|
-
|
|
2593
|
+
!refreshBackoffActive(next.lastRefreshError, next.id, this.now()) &&
|
|
2594
|
+
quotaSnapshotPassesModelScope(next.quota, options.modelId)) {
|
|
2595
|
+
usable.push(next);
|
|
2505
2596
|
}
|
|
2506
2597
|
}
|
|
2507
2598
|
}
|
|
@@ -2532,7 +2623,7 @@ export class FallbackAccountManager {
|
|
|
2532
2623
|
quotaPolicyAccount(account) {
|
|
2533
2624
|
if (!this.quotaManager)
|
|
2534
2625
|
return account;
|
|
2535
|
-
const cached = this.quotaManager.getFallback(account.id, account
|
|
2626
|
+
const cached = this.quotaManager.getFallback(account.id, account)?.quota;
|
|
2536
2627
|
return cached ? { ...account, quota: cached } : account;
|
|
2537
2628
|
}
|
|
2538
2629
|
async refreshDueAccounts() {
|
|
@@ -2545,7 +2636,7 @@ export class FallbackAccountManager {
|
|
|
2545
2636
|
continue;
|
|
2546
2637
|
if (!tokenNeedsRefresh(account, storage, this.now()))
|
|
2547
2638
|
continue;
|
|
2548
|
-
if (refreshBackoffActive(account.lastRefreshError, account.
|
|
2639
|
+
if (refreshBackoffActive(account.lastRefreshError, account.id, this.now())) {
|
|
2549
2640
|
// Backoff skips are steady-state while a fallback account is waiting for
|
|
2550
2641
|
// its next retry. Logging every background tick from every OpenCode
|
|
2551
2642
|
// process creates noise without adding new diagnostic signal; the
|
|
@@ -2588,7 +2679,7 @@ export class FallbackAccountManager {
|
|
|
2588
2679
|
let next = account;
|
|
2589
2680
|
try {
|
|
2590
2681
|
if (tokenNeedsRefresh(next, storage, this.now())) {
|
|
2591
|
-
if (refreshBackoffActive(next.lastRefreshError, next.
|
|
2682
|
+
if (refreshBackoffActive(next.lastRefreshError, next.id, this.now())) {
|
|
2592
2683
|
continue;
|
|
2593
2684
|
}
|
|
2594
2685
|
next = await this.refreshAccount(next, storage);
|
|
@@ -2635,7 +2726,7 @@ export class FallbackAccountManager {
|
|
|
2635
2726
|
if (tokenNeedsRefresh(next, storage, this.now())) {
|
|
2636
2727
|
const refreshError = next.lastRefreshError;
|
|
2637
2728
|
if (refreshError &&
|
|
2638
|
-
refreshBackoffActive(refreshError, next.
|
|
2729
|
+
refreshBackoffActive(refreshError, next.id, this.now())) {
|
|
2639
2730
|
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2640
2731
|
}
|
|
2641
2732
|
next = await this.refreshAccount(next, storage);
|
|
@@ -2709,7 +2800,7 @@ export class FallbackAccountManager {
|
|
|
2709
2800
|
}
|
|
2710
2801
|
const refreshError = latestAccount.lastRefreshError;
|
|
2711
2802
|
if (refreshError &&
|
|
2712
|
-
refreshBackoffActive(refreshError, latestAccount.
|
|
2803
|
+
refreshBackoffActive(refreshError, latestAccount.id, this.now())) {
|
|
2713
2804
|
updateStoredAccount(storage, latestAccount);
|
|
2714
2805
|
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2715
2806
|
}
|
|
@@ -2797,7 +2888,7 @@ export class FallbackAccountManager {
|
|
|
2797
2888
|
// deduplication and 429 backoff gating around the same quota API. Fall back
|
|
2798
2889
|
// to a direct fetch only when no QuotaManager is wired (e.g. in isolation).
|
|
2799
2890
|
const fetchSnapshot = (accessToken) => this.quotaManager
|
|
2800
|
-
? this.quotaManager.refreshFallbackWithMetadata(target.id, accessToken)
|
|
2891
|
+
? this.quotaManager.refreshFallbackWithMetadata(target.id, accessToken, target)
|
|
2801
2892
|
: fetchOAuthQuotaSnapshot({
|
|
2802
2893
|
accessToken,
|
|
2803
2894
|
fetchImpl: this.fetchImpl,
|
|
@@ -2853,7 +2944,7 @@ export class FallbackAccountManager {
|
|
|
2853
2944
|
quota: target.quota,
|
|
2854
2945
|
refreshAfter: now + getQuotaCheckIntervalMs(storage),
|
|
2855
2946
|
checkedAt: now,
|
|
2856
|
-
}, target
|
|
2947
|
+
}, target);
|
|
2857
2948
|
}
|
|
2858
2949
|
return { account: target, fetched };
|
|
2859
2950
|
}
|