@gajae-code/ai 0.16.7 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -1
- package/dist/types/auth-storage.d.ts +23 -4
- package/dist/types/models.d.ts +14 -0
- package/dist/types/provider-models/special.d.ts +12 -0
- package/dist/types/providers/anthropic.d.ts +1 -1
- package/dist/types/providers/cursor.d.ts +33 -21
- package/dist/types/providers/devin-acp.d.ts +157 -0
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/dist/types/providers/mock.d.ts +2 -0
- package/dist/types/providers/openai-responses-shared.d.ts +21 -1
- package/dist/types/providers/register-builtins.d.ts +1 -0
- package/dist/types/types.d.ts +52 -11
- package/dist/types/utils/block-symbols.d.ts +15 -5
- package/dist/types/utils/fallback-transport.d.ts +4 -1
- package/dist/types/utils.d.ts +13 -0
- package/package.json +4 -3
- package/src/api-registry.ts +1 -0
- package/src/auth-broker/redact.ts +10 -2
- package/src/auth-gateway/server.ts +56 -3
- package/src/auth-storage.ts +330 -116
- package/src/model-manager.ts +21 -2
- package/src/models.d.ts +14 -0
- package/src/models.json +117 -0
- package/src/models.ts +18 -0
- package/src/provider-models/descriptors.ts +7 -0
- package/src/provider-models/openai-compat.ts +14 -0
- package/src/provider-models/special.ts +39 -0
- package/src/providers/anthropic.d.ts +1 -1
- package/src/providers/anthropic.ts +1 -1
- package/src/providers/azure-openai-responses.ts +10 -1
- package/src/providers/cursor.d.ts +33 -21
- package/src/providers/cursor.ts +2024 -508
- package/src/providers/devin-acp.d.ts +157 -0
- package/src/providers/devin-acp.ts +1103 -0
- package/src/providers/google-gemini-headers.d.ts +1 -1
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/mock.ts +16 -1
- package/src/providers/openai-chat-server.ts +3 -3
- package/src/providers/openai-codex-responses.ts +27 -17
- package/src/providers/openai-responses-server.ts +5 -5
- package/src/providers/openai-responses-shared.d.ts +21 -1
- package/src/providers/openai-responses-shared.ts +60 -6
- package/src/providers/openai-responses.ts +10 -1
- package/src/providers/register-builtins.d.ts +1 -0
- package/src/providers/register-builtins.ts +21 -1
- package/src/stream.ts +14 -0
- package/src/types.d.ts +52 -11
- package/src/types.ts +70 -8
- package/src/utils/block-symbols.d.ts +15 -5
- package/src/utils/block-symbols.ts +16 -6
- package/src/utils/discovery/cursor.ts +3 -2
- package/src/utils/fallback-transport.d.ts +4 -1
- package/src/utils/fallback-transport.ts +12 -5
- package/src/utils.d.ts +13 -0
- package/src/utils.ts +17 -0
- package/dist/types/utils/codex-entitlement.d.ts +0 -22
- package/src/utils/codex-entitlement.d.ts +0 -22
- package/src/utils/codex-entitlement.ts +0 -57
package/src/auth-storage.ts
CHANGED
|
@@ -26,12 +26,6 @@ import type {
|
|
|
26
26
|
UsageReport,
|
|
27
27
|
} from "./usage";
|
|
28
28
|
|
|
29
|
-
import {
|
|
30
|
-
classifyOpenAICodexProEntitlement,
|
|
31
|
-
formatOpenAICodexChatGPTEntitlementError,
|
|
32
|
-
requiresOpenAICodexProModel,
|
|
33
|
-
requiresStrictOpenAICodexProModel,
|
|
34
|
-
} from "./utils/codex-entitlement";
|
|
35
29
|
import { getOAuthApiKey, getOAuthProvider, refreshOAuthToken, resolveOAuthStorageProvider } from "./utils/oauth";
|
|
36
30
|
import { loginDeepInfra } from "./utils/oauth/deepinfra";
|
|
37
31
|
import { loginDeepSeek } from "./utils/oauth/deepseek";
|
|
@@ -1146,28 +1140,6 @@ export function readBrokerErrorBody(error: unknown): string | undefined {
|
|
|
1146
1140
|
}
|
|
1147
1141
|
}
|
|
1148
1142
|
|
|
1149
|
-
function getUsagePlanType(report: UsageReport | null): string | undefined {
|
|
1150
|
-
const metadata = report?.metadata;
|
|
1151
|
-
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return undefined;
|
|
1152
|
-
const planType = (metadata as { planType?: unknown }).planType;
|
|
1153
|
-
return typeof planType === "string" ? planType.toLowerCase() : undefined;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
function getOpenAICodexPlanPriority(report: UsageReport | null): number {
|
|
1157
|
-
const entitlement = classifyOpenAICodexProEntitlement(getUsagePlanType(report));
|
|
1158
|
-
if (entitlement === "entitled") return 0;
|
|
1159
|
-
if (entitlement === "denied") return 2;
|
|
1160
|
-
return 1;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
function hasOpenAICodexProPlan(report: UsageReport | null): boolean {
|
|
1164
|
-
return classifyOpenAICodexProEntitlement(getUsagePlanType(report)) === "entitled";
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
function hasKnownOpenAICodexNonProPlan(report: UsageReport | null): boolean {
|
|
1168
|
-
return classifyOpenAICodexProEntitlement(getUsagePlanType(report)) === "denied";
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
1143
|
function resolveDefaultRankingStrategy(provider: Provider): CredentialRankingStrategy | undefined {
|
|
1172
1144
|
return DEFAULT_RANKING_STRATEGIES.get(provider);
|
|
1173
1145
|
}
|
|
@@ -1210,6 +1182,28 @@ function raceUsageWithSignal<T>(promise: Promise<T>, signal: AbortSignal | undef
|
|
|
1210
1182
|
});
|
|
1211
1183
|
}
|
|
1212
1184
|
|
|
1185
|
+
/**
|
|
1186
|
+
* Distinguish an internal deadline from a caller-owned cancellation.
|
|
1187
|
+
*
|
|
1188
|
+
* `AbortSignal.timeout(...)` (and `AbortSignal.any([...])` when the timeout leg
|
|
1189
|
+
* fires) aborts with a `TimeoutError` reason, whereas a caller's
|
|
1190
|
+
* `AbortController.abort()` surfaces an `AbortError` (or a custom reason). The
|
|
1191
|
+
* probe callers (`#checkCredentialHealth`, `#fetchUsageUncached`) pass a
|
|
1192
|
+
* timeout-derived signal: a refresh cut short by that internal deadline is a
|
|
1193
|
+
* genuine failure — the rotating refresh token may already have been consumed
|
|
1194
|
+
* upstream — and MUST update the replay guard. Only a true caller cancellation
|
|
1195
|
+
* may skip the guard update.
|
|
1196
|
+
*/
|
|
1197
|
+
function isTimeoutAbort(signal: AbortSignal): boolean {
|
|
1198
|
+
const reason: unknown = signal.reason;
|
|
1199
|
+
return (
|
|
1200
|
+
typeof reason === "object" &&
|
|
1201
|
+
reason !== null &&
|
|
1202
|
+
"name" in reason &&
|
|
1203
|
+
(reason as { name?: unknown }).name === "TimeoutError"
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1213
1207
|
function raceCredentialRefreshWithSignal<T>(
|
|
1214
1208
|
promise: Promise<T>,
|
|
1215
1209
|
signal: AbortSignal | undefined,
|
|
@@ -1225,6 +1219,17 @@ function raceCredentialRefreshWithSignal<T>(
|
|
|
1225
1219
|
});
|
|
1226
1220
|
}
|
|
1227
1221
|
|
|
1222
|
+
function oauthNonTokenFieldsEqual(left: OAuthCredential, right: OAuthCredential): boolean {
|
|
1223
|
+
return (
|
|
1224
|
+
left.accountId === right.accountId &&
|
|
1225
|
+
left.email === right.email &&
|
|
1226
|
+
left.projectId === right.projectId &&
|
|
1227
|
+
left.enterpriseUrl === right.enterpriseUrl &&
|
|
1228
|
+
left.mcpBinding?.resourceOrigin === right.mcpBinding?.resourceOrigin &&
|
|
1229
|
+
left.mcpBinding?.tokenEndpoint === right.mcpBinding?.tokenEndpoint
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1228
1233
|
function authCredentialEquals(left: AuthCredential, right: AuthCredential): boolean {
|
|
1229
1234
|
if (left.type !== right.type) return false;
|
|
1230
1235
|
if (left.type === "api_key") {
|
|
@@ -1235,15 +1240,60 @@ function authCredentialEquals(left: AuthCredential, right: AuthCredential): bool
|
|
|
1235
1240
|
left.access === right.access &&
|
|
1236
1241
|
left.refresh === right.refresh &&
|
|
1237
1242
|
left.expires === right.expires &&
|
|
1238
|
-
left
|
|
1239
|
-
left.email === right.email &&
|
|
1240
|
-
left.projectId === right.projectId &&
|
|
1241
|
-
left.enterpriseUrl === right.enterpriseUrl &&
|
|
1242
|
-
left.mcpBinding?.resourceOrigin === right.mcpBinding?.resourceOrigin &&
|
|
1243
|
-
left.mcpBinding?.tokenEndpoint === right.mcpBinding?.tokenEndpoint
|
|
1243
|
+
oauthNonTokenFieldsEqual(left, right)
|
|
1244
1244
|
);
|
|
1245
1245
|
}
|
|
1246
1246
|
|
|
1247
|
+
function parseStructuredOAuthKey(apiKey: string): { token: string; projectId?: string } | undefined {
|
|
1248
|
+
if (!apiKey.startsWith("{")) return undefined;
|
|
1249
|
+
try {
|
|
1250
|
+
const parsed = JSON.parse(apiKey) as { token?: unknown; projectId?: unknown };
|
|
1251
|
+
if (typeof parsed.token !== "string") return undefined;
|
|
1252
|
+
return { token: parsed.token, projectId: typeof parsed.projectId === "string" ? parsed.projectId : undefined };
|
|
1253
|
+
} catch {
|
|
1254
|
+
return undefined;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Identity keys decode token claims and evidence is computed on hot catalog paths. Key the memo by every
|
|
1259
|
+
// field that can feed the identity so an in-place credential mutation can never reuse a stale result.
|
|
1260
|
+
const OAUTH_IDENTITY_MEMO_LIMIT = 256;
|
|
1261
|
+
const oauthIdentityMemo = new Map<string, string | null>();
|
|
1262
|
+
|
|
1263
|
+
function resolveMemoizedOAuthIdentityKey(provider: string, credential: OAuthCredential): string | null {
|
|
1264
|
+
const memoKey = [
|
|
1265
|
+
provider,
|
|
1266
|
+
credential.accountId ?? "",
|
|
1267
|
+
credential.email ?? "",
|
|
1268
|
+
credential.projectId ?? "",
|
|
1269
|
+
credential.enterpriseUrl ?? "",
|
|
1270
|
+
credential.access,
|
|
1271
|
+
credential.refresh,
|
|
1272
|
+
].join("\u0000");
|
|
1273
|
+
const cached = oauthIdentityMemo.get(memoKey);
|
|
1274
|
+
if (cached !== undefined) return cached;
|
|
1275
|
+
const identityKey = resolveCredentialIdentityKey(provider, credential);
|
|
1276
|
+
if (oauthIdentityMemo.size >= OAUTH_IDENTITY_MEMO_LIMIT) oauthIdentityMemo.clear();
|
|
1277
|
+
oauthIdentityMemo.set(memoKey, identityKey);
|
|
1278
|
+
return identityKey;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function isOAuthTokenRotationOnly(provider: string, left: StoredCredential[], right: StoredCredential[]): boolean {
|
|
1282
|
+
if (left.length !== right.length || left.length === 0) return false;
|
|
1283
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
1284
|
+
const leftEntry = left[index];
|
|
1285
|
+
const rightEntry = right[index];
|
|
1286
|
+
if (!leftEntry || !rightEntry || leftEntry.id !== rightEntry.id) return false;
|
|
1287
|
+
if (authCredentialEquals(leftEntry.credential, rightEntry.credential)) continue;
|
|
1288
|
+
const previous = leftEntry.credential;
|
|
1289
|
+
const next = rightEntry.credential;
|
|
1290
|
+
if (previous.type !== "oauth" || next.type !== "oauth" || !oauthNonTokenFieldsEqual(previous, next)) return false;
|
|
1291
|
+
const identityKey = resolveMemoizedOAuthIdentityKey(provider, previous);
|
|
1292
|
+
if (!identityKey || identityKey !== resolveMemoizedOAuthIdentityKey(provider, next)) return false;
|
|
1293
|
+
}
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1247
1297
|
function storedCredentialArraysEqual(left: StoredCredential[], right: StoredCredential[]): boolean {
|
|
1248
1298
|
if (left.length !== right.length) return false;
|
|
1249
1299
|
for (let index = 0; index < left.length; index += 1) {
|
|
@@ -1340,6 +1390,8 @@ export class AuthStorage {
|
|
|
1340
1390
|
#sessionCredentialSelectors: Map<string, Map<string, AuthCredentialSelector>> = new Map();
|
|
1341
1391
|
/** Explicit AUTO masks suppress both scoped and process-global selectors for a scope/provider. */
|
|
1342
1392
|
#sessionCredentialAutoMasks: Map<string, Set<string>> = new Map();
|
|
1393
|
+
/** Hard-pin failures that must remain unavailable until an explicit user choice. */
|
|
1394
|
+
#sessionCredentialUnavailable: Map<string, Map<string, AuthCredentialSelector>> = new Map();
|
|
1343
1395
|
/** Reference counts for sessions sharing one credential scope (top-level + subagents). */
|
|
1344
1396
|
#credentialScopeLeases: Map<string, number> = new Map();
|
|
1345
1397
|
/** Tracks next credential index per provider:type key for round-robin distribution (non-session use). */
|
|
@@ -1381,6 +1433,8 @@ export class AuthStorage {
|
|
|
1381
1433
|
#providerGenerations = new Map<string, number>();
|
|
1382
1434
|
#providerConfigurationGenerations = new Map<string, number>();
|
|
1383
1435
|
#providerOAuthRefreshGenerations = new Map<string, number>();
|
|
1436
|
+
/** Recent access tokens replaced by same-account rotation, keyed by storage provider and row id. */
|
|
1437
|
+
#rotatedOAuthAccessTokens = new Map<string, string[]>();
|
|
1384
1438
|
#generationListeners: Set<(generation: number) => void> = new Set();
|
|
1385
1439
|
#oauthRefreshInFlight: Map<number, Promise<AuthCredentialSnapshotEntry>> = new Map();
|
|
1386
1440
|
#oauthCredentialRefreshInFlight: Map<number, Promise<RefreshedOAuthCredentials>> = new Map();
|
|
@@ -1446,6 +1500,7 @@ export class AuthStorage {
|
|
|
1446
1500
|
this.#credentialScopeLeases.clear();
|
|
1447
1501
|
this.#sessionCredentialSelectors.clear();
|
|
1448
1502
|
this.#sessionCredentialAutoMasks.clear();
|
|
1503
|
+
this.#sessionCredentialUnavailable.clear();
|
|
1449
1504
|
this.#sessionLastCredential.clear();
|
|
1450
1505
|
this.#store.close();
|
|
1451
1506
|
}
|
|
@@ -1520,9 +1575,10 @@ export class AuthStorage {
|
|
|
1520
1575
|
.update(`${this.#getProviderGeneration(storageProvider)}\u0000unavailable-selector`)
|
|
1521
1576
|
.digest("hex");
|
|
1522
1577
|
}
|
|
1523
|
-
const
|
|
1524
|
-
? [selectedCredential
|
|
1525
|
-
: this.#
|
|
1578
|
+
const storedEntries: StoredCredential[] = selectedCredential
|
|
1579
|
+
? [selectedCredential]
|
|
1580
|
+
: this.#getStoredCredentials(provider);
|
|
1581
|
+
const credentials = storedEntries.map(entry => entry.credential);
|
|
1526
1582
|
const hasApiKey = credentials.some(credential => credential.type === "api_key");
|
|
1527
1583
|
const hasUsableOAuth = credentials.some(
|
|
1528
1584
|
credential =>
|
|
@@ -1545,14 +1601,60 @@ export class AuthStorage {
|
|
|
1545
1601
|
}`;
|
|
1546
1602
|
})
|
|
1547
1603
|
.join("\u0001");
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1604
|
+
// Account-backed OAuth rows fingerprint by row, identity, request metadata, and usability rather than token
|
|
1605
|
+
// expiry, so a refresh keeps discovery evidence valid. Rows without a resolvable identity keep the expiry
|
|
1606
|
+
// fingerprint.
|
|
1607
|
+
const now = Date.now();
|
|
1608
|
+
const oauthIdentityKeys = storedEntries.map(entry =>
|
|
1609
|
+
entry.credential.type === "oauth" ? resolveMemoizedOAuthIdentityKey(storageProvider, entry.credential) : null,
|
|
1610
|
+
);
|
|
1611
|
+
const storedOAuthFingerprint = storedEntries
|
|
1612
|
+
.map((entry, index) => {
|
|
1613
|
+
const credential = entry.credential;
|
|
1614
|
+
if (credential.type !== "oauth") return undefined;
|
|
1615
|
+
const usability = credential.expires > now ? "usable" : "expired";
|
|
1616
|
+
const identityKey = oauthIdentityKeys[index];
|
|
1617
|
+
if (!identityKey) return `${credential.expires}\u0000${usability}`;
|
|
1618
|
+
return [
|
|
1619
|
+
entry.id,
|
|
1620
|
+
identityKey,
|
|
1621
|
+
usability,
|
|
1622
|
+
credential.projectId ?? "",
|
|
1623
|
+
credential.enterpriseUrl ?? "",
|
|
1624
|
+
credential.mcpBinding?.resourceOrigin ?? "",
|
|
1625
|
+
credential.mcpBinding?.tokenEndpoint ?? "",
|
|
1626
|
+
].join("\u0000");
|
|
1627
|
+
})
|
|
1628
|
+
.filter(fingerprint => fingerprint !== undefined)
|
|
1551
1629
|
.join("\u0001");
|
|
1630
|
+
// A key resolved from an account-backed OAuth row is covered by that row's fingerprint. Hashing the raw
|
|
1631
|
+
// token, or a structured key carrying it, would make every refresh look like a different credential.
|
|
1632
|
+
const identityBackedEntries = storedEntries.filter((_, index) => oauthIdentityKeys[index]);
|
|
1633
|
+
let oauthKeyEntry: StoredCredential | undefined;
|
|
1634
|
+
if (evidenceApiKey !== undefined && identityBackedEntries.length > 0) {
|
|
1635
|
+
const structuredKey = parseStructuredOAuthKey(evidenceApiKey);
|
|
1636
|
+
const keyToken = structuredKey ? structuredKey.token : evidenceApiKey;
|
|
1637
|
+
// Callers may still hold the key they resolved before the row rotated, so the row's recently rotated
|
|
1638
|
+
// tokens map to it as well. A structured key for another project stays distinct.
|
|
1639
|
+
const matchesRow = (entry: StoredCredential, tokens: readonly string[] | undefined): boolean => {
|
|
1640
|
+
const credential = entry.credential;
|
|
1641
|
+
if (credential.type !== "oauth" || keyToken.length === 0 || !tokens?.includes(keyToken)) return false;
|
|
1642
|
+
return structuredKey?.projectId === undefined || structuredKey.projectId === credential.projectId;
|
|
1643
|
+
};
|
|
1644
|
+
// Current tokens win over rotated ones so a row's old token can never shadow another row's live token.
|
|
1645
|
+
oauthKeyEntry =
|
|
1646
|
+
identityBackedEntries.find(
|
|
1647
|
+
entry => entry.credential.type === "oauth" && matchesRow(entry, [entry.credential.access]),
|
|
1648
|
+
) ??
|
|
1649
|
+
identityBackedEntries.find(entry =>
|
|
1650
|
+
matchesRow(entry, this.#rotatedOAuthAccessTokens.get(`${storageProvider}\u0000${entry.id}`)),
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
const evidenceKeyFingerprint = oauthKeyEntry ? `oauth-row:${oauthKeyEntry.id}` : (evidenceApiKey ?? "");
|
|
1552
1654
|
return crypto
|
|
1553
1655
|
.createHash("sha256")
|
|
1554
1656
|
.update(
|
|
1555
|
-
`${this.#getProviderGeneration(storageProvider)}\u0000${effectiveEnvKey ?? ""}\u0000${storedApiKeyFingerprint}\u0000${storedOAuthFingerprint}\u0000${
|
|
1657
|
+
`${this.#getProviderGeneration(storageProvider)}\u0000${effectiveEnvKey ?? ""}\u0000${storedApiKeyFingerprint}\u0000${storedOAuthFingerprint}\u0000${evidenceKeyFingerprint}`,
|
|
1556
1658
|
)
|
|
1557
1659
|
.digest("hex");
|
|
1558
1660
|
}
|
|
@@ -1672,6 +1774,7 @@ export class AuthStorage {
|
|
|
1672
1774
|
this.#credentialScopeLeases.delete(scope);
|
|
1673
1775
|
this.#sessionCredentialSelectors.delete(scope);
|
|
1674
1776
|
this.#sessionCredentialAutoMasks.delete(scope);
|
|
1777
|
+
this.#sessionCredentialUnavailable.delete(scope);
|
|
1675
1778
|
for (const [provider, sessions] of this.#sessionLastCredential) {
|
|
1676
1779
|
if (!sessions.delete(scope)) continue;
|
|
1677
1780
|
if (sessions.size === 0) this.#sessionLastCredential.delete(provider);
|
|
@@ -1693,6 +1796,7 @@ export class AuthStorage {
|
|
|
1693
1796
|
if (!scope) throw new Error("Credential scope id must not be empty");
|
|
1694
1797
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1695
1798
|
this.#assertCredentialSelectorUsable(storageProvider, selector, owner);
|
|
1799
|
+
this.#sessionCredentialUnavailable.get(scope)?.delete(storageProvider);
|
|
1696
1800
|
const selectors = this.#sessionCredentialSelectors.get(scope) ?? new Map<string, AuthCredentialSelector>();
|
|
1697
1801
|
selectors.set(storageProvider, selector);
|
|
1698
1802
|
this.#sessionCredentialSelectors.set(scope, selectors);
|
|
@@ -1706,6 +1810,7 @@ export class AuthStorage {
|
|
|
1706
1810
|
if (!scope) throw new Error("Credential scope id must not be empty");
|
|
1707
1811
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1708
1812
|
this.#sessionCredentialSelectors.get(scope)?.delete(storageProvider);
|
|
1813
|
+
this.#sessionCredentialUnavailable.get(scope)?.delete(storageProvider);
|
|
1709
1814
|
const masks = this.#sessionCredentialAutoMasks.get(scope) ?? new Set<string>();
|
|
1710
1815
|
masks.add(storageProvider);
|
|
1711
1816
|
this.#sessionCredentialAutoMasks.set(scope, masks);
|
|
@@ -1725,6 +1830,28 @@ export class AuthStorage {
|
|
|
1725
1830
|
if (changed) this.#bumpGeneration("clear-session-credential-selector", storageProvider);
|
|
1726
1831
|
}
|
|
1727
1832
|
|
|
1833
|
+
/** Preserve a failed hard pin as unavailable instead of allowing AUTO fallback. */
|
|
1834
|
+
markSessionCredentialUnavailable(scopeId: string, provider: string, selector: AuthCredentialSelector): void {
|
|
1835
|
+
const scope = scopeId.trim();
|
|
1836
|
+
if (!scope) throw new Error("Credential scope id must not be empty");
|
|
1837
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1838
|
+
const unavailable = this.#sessionCredentialUnavailable.get(scope) ?? new Map<string, AuthCredentialSelector>();
|
|
1839
|
+
unavailable.set(storageProvider, selector);
|
|
1840
|
+
this.#sessionCredentialUnavailable.set(scope, unavailable);
|
|
1841
|
+
this.#sessionCredentialSelectors.get(scope)?.delete(storageProvider);
|
|
1842
|
+
this.#sessionCredentialAutoMasks.get(scope)?.delete(storageProvider);
|
|
1843
|
+
this.#bumpGeneration("mark-session-credential-unavailable", storageProvider);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
/** Return a failed hard pin retained for this scope, if any. */
|
|
1847
|
+
hasSessionCredentialUnavailable(provider: string, scopeId?: string): boolean {
|
|
1848
|
+
const scope = scopeId?.trim();
|
|
1849
|
+
return (
|
|
1850
|
+
scope !== undefined &&
|
|
1851
|
+
this.#sessionCredentialUnavailable.get(scope)?.has(resolveOAuthStorageProvider(provider)) === true
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1728
1855
|
/** Whether the effective selection for a scope is explicitly pinned (AUTO masks are not pins). */
|
|
1729
1856
|
hasSessionCredentialSelector(provider: string, scopeId?: string): boolean {
|
|
1730
1857
|
if (!scopeId) return false;
|
|
@@ -2277,15 +2404,45 @@ export class AuthStorage {
|
|
|
2277
2404
|
(entry, index) =>
|
|
2278
2405
|
entry.id !== credentials[index]?.id || entry.credential.type !== credentials[index]?.credential.type,
|
|
2279
2406
|
);
|
|
2280
|
-
|
|
2281
|
-
|
|
2407
|
+
// Refreshing tokens for the same OAuth account is not a configuration change: keep the provider's
|
|
2408
|
+
// evidence and configuration generations so catalogs discovered for that account stay current.
|
|
2409
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2410
|
+
const tokenRotationOnly =
|
|
2411
|
+
!identityOrderChanged && isOAuthTokenRotationOnly(storageProvider, current, credentials);
|
|
2412
|
+
if (tokenRotationOnly) {
|
|
2413
|
+
this.#rememberRotatedAccessTokens(storageProvider, current, credentials);
|
|
2414
|
+
} else {
|
|
2415
|
+
this.#forgetRotatedAccessTokens(storageProvider);
|
|
2416
|
+
this.#resolvedStoredApiKeyValues.delete(provider);
|
|
2417
|
+
this.#storedApiKeyResolutionInFlight.delete(provider);
|
|
2418
|
+
}
|
|
2282
2419
|
if (credentials.length === 0) {
|
|
2283
2420
|
this.#data.delete(provider);
|
|
2284
2421
|
} else {
|
|
2285
2422
|
this.#data.set(provider, credentials);
|
|
2286
2423
|
}
|
|
2287
|
-
if (identityOrderChanged) this.#resetProviderAssignments(
|
|
2288
|
-
this.#bumpGeneration("
|
|
2424
|
+
if (identityOrderChanged) this.#resetProviderAssignments(storageProvider);
|
|
2425
|
+
if (tokenRotationOnly) this.#bumpGeneration("oauth-token-rotation");
|
|
2426
|
+
else this.#bumpGeneration("credentials", provider);
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
#rememberRotatedAccessTokens(storageProvider: string, previous: StoredCredential[], next: StoredCredential[]): void {
|
|
2430
|
+
for (let index = 0; index < previous.length; index += 1) {
|
|
2431
|
+
const before = previous[index];
|
|
2432
|
+
const after = next[index];
|
|
2433
|
+
if (before?.credential.type !== "oauth" || after?.credential.type !== "oauth") continue;
|
|
2434
|
+
if (!before.credential.access || before.credential.access === after.credential.access) continue;
|
|
2435
|
+
const key = `${storageProvider}\u0000${before.id}`;
|
|
2436
|
+
const history = [before.credential.access, ...(this.#rotatedOAuthAccessTokens.get(key) ?? [])].slice(0, 4);
|
|
2437
|
+
this.#rotatedOAuthAccessTokens.set(key, history);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
#forgetRotatedAccessTokens(storageProvider: string): void {
|
|
2442
|
+
const prefix = `${storageProvider}\u0000`;
|
|
2443
|
+
for (const key of [...this.#rotatedOAuthAccessTokens.keys()]) {
|
|
2444
|
+
if (key.startsWith(prefix)) this.#rotatedOAuthAccessTokens.delete(key);
|
|
2445
|
+
}
|
|
2289
2446
|
}
|
|
2290
2447
|
|
|
2291
2448
|
#resolveOAuthDedupeIdentityKey(provider: string, credential: OAuthCredential): string | null {
|
|
@@ -2692,10 +2849,15 @@ export class AuthStorage {
|
|
|
2692
2849
|
if (persist && !this.#store.refreshSnapshot) this.#store.updateAuthCredential(target.id, credential);
|
|
2693
2850
|
const updated = [...entries];
|
|
2694
2851
|
updated[index] = { id: target.id, credential };
|
|
2852
|
+
const configurationGeneration = this.#getProviderConfigurationGeneration(provider);
|
|
2695
2853
|
this.#setStoredCredentials(provider, updated);
|
|
2854
|
+
// Callers use the refresh generation to discount configuration bumps caused by a refresh, so it only
|
|
2855
|
+
// advances when this refresh bumped configuration; token rotation for a known account does not.
|
|
2696
2856
|
if (
|
|
2857
|
+
configurationGeneration !== this.#getProviderConfigurationGeneration(provider) &&
|
|
2697
2858
|
credential.type === "oauth" &&
|
|
2698
2859
|
target.credential.type === "oauth" &&
|
|
2860
|
+
oauthNonTokenFieldsEqual(credential, target.credential) &&
|
|
2699
2861
|
(credential.access !== target.credential.access ||
|
|
2700
2862
|
credential.refresh !== target.credential.refresh ||
|
|
2701
2863
|
credential.expires !== target.credential.expires)
|
|
@@ -2808,7 +2970,9 @@ export class AuthStorage {
|
|
|
2808
2970
|
const selector = selectors.get(storageProvider);
|
|
2809
2971
|
if (!selector) continue;
|
|
2810
2972
|
const selected = previousEntries.find(entry => this.#credentialMatchesSelector(entry, selector));
|
|
2811
|
-
if (selected && removedIds.has(selected.id))
|
|
2973
|
+
if (selected && removedIds.has(selected.id)) {
|
|
2974
|
+
this.markSessionCredentialUnavailable(scopeId, storageProvider, selector);
|
|
2975
|
+
}
|
|
2812
2976
|
}
|
|
2813
2977
|
for (const [sessionId, sticky] of this.#sessionLastCredential.get(storageProvider) ?? []) {
|
|
2814
2978
|
if (removedIds.has(previousEntries[sticky.index]?.id ?? -1))
|
|
@@ -4563,50 +4727,86 @@ export class AuthStorage {
|
|
|
4563
4727
|
}
|
|
4564
4728
|
|
|
4565
4729
|
/**
|
|
4566
|
-
* Marks the
|
|
4567
|
-
*
|
|
4568
|
-
* Returns
|
|
4730
|
+
* Marks the explicit stored row, or the session row captured at entry, as usage-limited.
|
|
4731
|
+
* Re-finds that same row after the usage lookup; a vanished row marks nothing.
|
|
4732
|
+
* Returns whether another credential of the same type remains unblocked.
|
|
4569
4733
|
*/
|
|
4570
4734
|
async markUsageLimitReached(
|
|
4571
4735
|
provider: string,
|
|
4572
4736
|
sessionId: string | undefined,
|
|
4573
|
-
options?: { retryAfterMs?: number; baseUrl?: string; signal?: AbortSignal; owner?: object },
|
|
4737
|
+
options?: { retryAfterMs?: number; baseUrl?: string; signal?: AbortSignal; owner?: object; rowId?: number },
|
|
4574
4738
|
): Promise<boolean> {
|
|
4575
4739
|
provider = resolveOAuthStorageProvider(provider);
|
|
4576
4740
|
const ownerOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
4577
4741
|
if (ownerOverride && !ownerOverride.envSourced) return false;
|
|
4742
|
+
const entries = this.#getStoredCredentials(provider);
|
|
4578
4743
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4744
|
+
const initial =
|
|
4745
|
+
options?.rowId !== undefined
|
|
4746
|
+
? entries.find(entry => entry.id === options.rowId)
|
|
4747
|
+
: sessionCredential
|
|
4748
|
+
? entries[sessionCredential.index]
|
|
4749
|
+
: undefined;
|
|
4750
|
+
if (!initial) return false;
|
|
4582
4751
|
const now = Date.now();
|
|
4583
4752
|
let blockedUntil = now + (options?.retryAfterMs ?? AuthStorage.#defaultBackoffMs);
|
|
4584
4753
|
|
|
4585
|
-
if (
|
|
4586
|
-
const
|
|
4587
|
-
if (
|
|
4588
|
-
const
|
|
4589
|
-
if (
|
|
4590
|
-
const resetAtMs = this.#getUsageResetAtMs(report, Date.now());
|
|
4591
|
-
if (resetAtMs && resetAtMs > blockedUntil) {
|
|
4592
|
-
blockedUntil = resetAtMs;
|
|
4593
|
-
}
|
|
4594
|
-
}
|
|
4754
|
+
if (initial.credential.type === "oauth" && this.#rankingStrategyResolver?.(provider)) {
|
|
4755
|
+
const report = await this.#getUsageReport(provider, initial.credential, options);
|
|
4756
|
+
if (report && this.#isUsageLimitReached(report)) {
|
|
4757
|
+
const resetAtMs = this.#getUsageResetAtMs(report, Date.now());
|
|
4758
|
+
if (resetAtMs && resetAtMs > blockedUntil) blockedUntil = resetAtMs;
|
|
4595
4759
|
}
|
|
4596
4760
|
}
|
|
4597
4761
|
|
|
4598
|
-
|
|
4762
|
+
// Never consult the possibly reassigned sticky pointer after the await.
|
|
4763
|
+
const current = this.#getStoredCredentials(provider);
|
|
4764
|
+
const targetIndex = current.findIndex(entry => entry.id === initial.id);
|
|
4765
|
+
const target = current[targetIndex];
|
|
4766
|
+
if (!target) return false;
|
|
4767
|
+
const providerKey = this.#getProviderTypeKey(provider, target.credential.type);
|
|
4768
|
+
this.#markCredentialBlocked(providerKey, targetIndex, blockedUntil);
|
|
4599
4769
|
|
|
4600
4770
|
const remainingCredentials = this.#getCredentialsForProvider(provider)
|
|
4601
4771
|
.map((credential, index) => ({ credential, index }))
|
|
4602
4772
|
.filter(
|
|
4603
4773
|
(entry): entry is { credential: AuthCredential; index: number } =>
|
|
4604
|
-
entry.credential.type ===
|
|
4774
|
+
entry.credential.type === target.credential.type && entry.index !== targetIndex,
|
|
4605
4775
|
);
|
|
4606
4776
|
|
|
4607
4777
|
return remainingCredentials.some(candidate => !this.#isCredentialBlocked(providerKey, candidate.index));
|
|
4608
4778
|
}
|
|
4609
4779
|
|
|
4780
|
+
/**
|
|
4781
|
+
* Mark the stored credential whose key matches `apiKey` usage-limited for
|
|
4782
|
+
* `retryAfterMs` (or the default backoff). Used when the caller knows
|
|
4783
|
+
* exactly which leased credential failed (e.g. the auth-gateway leases a
|
|
4784
|
+
* concrete row without a session binding) so bookkeeping cannot land on the
|
|
4785
|
+
* wrong credential. The credential is never invalidated or marked suspect:
|
|
4786
|
+
* the block is a bounded backoff only. Returns false when no stored
|
|
4787
|
+
* credential matches the key.
|
|
4788
|
+
*/
|
|
4789
|
+
async markUsageLimitReachedMatching(
|
|
4790
|
+
provider: string,
|
|
4791
|
+
apiKey: string,
|
|
4792
|
+
options?: { retryAfterMs?: number },
|
|
4793
|
+
): Promise<boolean> {
|
|
4794
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
4795
|
+
const stored = this.#getStoredCredentials(storageProvider);
|
|
4796
|
+
for (let index = 0; index < stored.length; index++) {
|
|
4797
|
+
const entry = stored[index];
|
|
4798
|
+
if (entry && (await this.#credentialMatchesApiKey(storageProvider, entry.credential, apiKey))) {
|
|
4799
|
+
this.#markCredentialBlocked(
|
|
4800
|
+
this.#getProviderTypeKey(storageProvider, entry.credential.type),
|
|
4801
|
+
index,
|
|
4802
|
+
Date.now() + (options?.retryAfterMs ?? AuthStorage.#defaultBackoffMs),
|
|
4803
|
+
);
|
|
4804
|
+
return true;
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
return false;
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4610
4810
|
/**
|
|
4611
4811
|
* Earliest instant at which any currently blocked stored credential for this
|
|
4612
4812
|
* provider becomes usable again. Undefined when nothing is blocked.
|
|
@@ -4787,11 +4987,6 @@ export class AuthStorage {
|
|
|
4787
4987
|
if (leftBlockedUntil !== rightBlockedUntil) return leftBlockedUntil - rightBlockedUntil;
|
|
4788
4988
|
return left.orderPos - right.orderPos;
|
|
4789
4989
|
}
|
|
4790
|
-
if (requiresOpenAICodexProModel(args.provider, args.options?.modelId)) {
|
|
4791
|
-
const leftPlanPriority = getOpenAICodexPlanPriority(left.usage);
|
|
4792
|
-
const rightPlanPriority = getOpenAICodexPlanPriority(right.usage);
|
|
4793
|
-
if (leftPlanPriority !== rightPlanPriority) return leftPlanPriority - rightPlanPriority;
|
|
4794
|
-
}
|
|
4795
4990
|
if (left.hasPriorityBoost !== right.hasPriorityBoost) return left.hasPriorityBoost ? -1 : 1;
|
|
4796
4991
|
if (this.#credentialRankingMode === "earliest-reset" && left.resetAtMs !== right.resetAtMs) {
|
|
4797
4992
|
// Earliest-expiry-first: drain the soonest-to-reset account before
|
|
@@ -4827,6 +5022,15 @@ export class AuthStorage {
|
|
|
4827
5022
|
options?: AuthApiKeyOptions,
|
|
4828
5023
|
reloadsUsed = 0,
|
|
4829
5024
|
): Promise<OAuthResolutionResult | undefined> {
|
|
5025
|
+
const unavailableSelector =
|
|
5026
|
+
sessionId && !options?.credentialSelector
|
|
5027
|
+
? this.#sessionCredentialUnavailable.get(sessionId)?.get(resolveOAuthStorageProvider(provider))
|
|
5028
|
+
: undefined;
|
|
5029
|
+
if (unavailableSelector) {
|
|
5030
|
+
throw new Error(
|
|
5031
|
+
`Selected credential for ${provider} (${this.#formatCredentialSelector(unavailableSelector)}) is unavailable`,
|
|
5032
|
+
);
|
|
5033
|
+
}
|
|
4830
5034
|
if (reloadsUsed > MAX_OAUTH_RESOLUTION_RELOADS) {
|
|
4831
5035
|
logger.warn("OAuth credential resolution exhausted its reload budget", {
|
|
4832
5036
|
provider,
|
|
@@ -4835,6 +5039,10 @@ export class AuthStorage {
|
|
|
4835
5039
|
return undefined;
|
|
4836
5040
|
}
|
|
4837
5041
|
const selectedCredential = this.#resolveSelectedStoredCredential(provider, options, sessionId);
|
|
5042
|
+
const sessionSelector =
|
|
5043
|
+
sessionId && !options?.credentialSelector && selectedCredential?.credential.type === "oauth"
|
|
5044
|
+
? { kind: "id" as const, value: String(selectedCredential.id) }
|
|
5045
|
+
: undefined;
|
|
4838
5046
|
const selectedOAuthCredential: OAuthCredentialSelection | undefined =
|
|
4839
5047
|
selectedCredential?.credential.type === "oauth"
|
|
4840
5048
|
? {
|
|
@@ -4864,9 +5072,7 @@ export class AuthStorage {
|
|
|
4864
5072
|
const providerKey = this.#getProviderTypeKey(provider, "oauth");
|
|
4865
5073
|
const order = selectedCredential ? [0] : this.#getCredentialOrder(providerKey, sessionId, credentials.length);
|
|
4866
5074
|
const strategy = this.#rankingStrategyResolver?.(provider);
|
|
4867
|
-
const
|
|
4868
|
-
const checkUsage =
|
|
4869
|
-
strategy !== undefined && (selectedCredential !== undefined || credentials.length > 1 || requiresProModel);
|
|
5075
|
+
const checkUsage = strategy !== undefined && (selectedCredential !== undefined || credentials.length > 1);
|
|
4870
5076
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
4871
5077
|
const sessionPreferredIndex = sessionCredential?.type === "oauth" ? sessionCredential.index : undefined;
|
|
4872
5078
|
// Skip ranking only when the session already has a working preferred credential — re-ranking
|
|
@@ -4875,7 +5081,7 @@ export class AuthStorage {
|
|
|
4875
5081
|
// with the most headroom proactively and fall back intelligently when rate-limited.
|
|
4876
5082
|
const sessionPreferredIsAvailable =
|
|
4877
5083
|
sessionPreferredIndex !== undefined && !this.#isCredentialBlocked(providerKey, sessionPreferredIndex);
|
|
4878
|
-
const shouldRank = !selectedCredential && checkUsage &&
|
|
5084
|
+
const shouldRank = !selectedCredential && checkUsage && !sessionPreferredIsAvailable;
|
|
4879
5085
|
const candidates = shouldRank
|
|
4880
5086
|
? await this.#rankOAuthSelections({ providerKey, provider, order, credentials, options, strategy: strategy! })
|
|
4881
5087
|
: order
|
|
@@ -4913,7 +5119,7 @@ export class AuthStorage {
|
|
|
4913
5119
|
}
|
|
4914
5120
|
}
|
|
4915
5121
|
|
|
4916
|
-
if (!selectedCredential && sessionPreferredIndex !== undefined
|
|
5122
|
+
if (!selectedCredential && sessionPreferredIndex !== undefined) {
|
|
4917
5123
|
const sessionPreferredCandidate = candidates.findIndex(
|
|
4918
5124
|
candidate =>
|
|
4919
5125
|
!this.#isCredentialBlocked(providerKey, candidate.selection.index) &&
|
|
@@ -4961,24 +5167,6 @@ export class AuthStorage {
|
|
|
4961
5167
|
}),
|
|
4962
5168
|
);
|
|
4963
5169
|
|
|
4964
|
-
// Skip the Pro-plan filter when no candidate is confirmed Pro, so users with only
|
|
4965
|
-
// non-Pro accounts can still attempt Spark requests (e.g. trial/grandfathered access).
|
|
4966
|
-
const enforceProRequirement =
|
|
4967
|
-
requiresProModel && candidates.some(candidate => hasOpenAICodexProPlan(candidate.usage));
|
|
4968
|
-
// Spark retains its historical Plus fallback for grandfathered accounts.
|
|
4969
|
-
// Sol is different: confirmed Free/Plus plans cannot call it, so reject the
|
|
4970
|
-
// model before returning an OAuth bearer and letting the turn fail remotely.
|
|
4971
|
-
// Unknown plan names still reach the provider because the usage endpoint is
|
|
4972
|
-
// authoritative and future tiers must not be denied by a client-side guess.
|
|
4973
|
-
const strictProRequirement = requiresStrictOpenAICodexProModel(provider, options?.modelId);
|
|
4974
|
-
if (
|
|
4975
|
-
strictProRequirement &&
|
|
4976
|
-
candidates.length > 0 &&
|
|
4977
|
-
candidates.every(candidate => hasKnownOpenAICodexNonProPlan(candidate.usage))
|
|
4978
|
-
) {
|
|
4979
|
-
throw new Error(formatOpenAICodexChatGPTEntitlementError(options?.modelId));
|
|
4980
|
-
}
|
|
4981
|
-
|
|
4982
5170
|
const fallback = candidates[0];
|
|
4983
5171
|
|
|
4984
5172
|
for (const candidate of candidates) {
|
|
@@ -4993,9 +5181,9 @@ export class AuthStorage {
|
|
|
4993
5181
|
allowBlocked: false,
|
|
4994
5182
|
prefetchedUsage: candidate.usage,
|
|
4995
5183
|
usagePrechecked: candidate.usageChecked,
|
|
4996
|
-
enforceProRequirement,
|
|
4997
5184
|
},
|
|
4998
5185
|
reloadsUsed,
|
|
5186
|
+
sessionSelector,
|
|
4999
5187
|
);
|
|
5000
5188
|
if (resolved) return resolved;
|
|
5001
5189
|
}
|
|
@@ -5012,9 +5200,9 @@ export class AuthStorage {
|
|
|
5012
5200
|
allowBlocked: true,
|
|
5013
5201
|
prefetchedUsage: fallback.usage,
|
|
5014
5202
|
usagePrechecked: fallback.usageChecked,
|
|
5015
|
-
enforceProRequirement,
|
|
5016
5203
|
},
|
|
5017
5204
|
reloadsUsed,
|
|
5205
|
+
sessionSelector,
|
|
5018
5206
|
);
|
|
5019
5207
|
}
|
|
5020
5208
|
|
|
@@ -5229,6 +5417,18 @@ export class AuthStorage {
|
|
|
5229
5417
|
}
|
|
5230
5418
|
return authority;
|
|
5231
5419
|
} catch (error) {
|
|
5420
|
+
// A genuine caller cancellation (e.g. the agent's ESC) is not a refresh
|
|
5421
|
+
// failure. Rethrow before any failure classification so it never poisons
|
|
5422
|
+
// the replay guard (which would temp-block the credential on the next
|
|
5423
|
+
// request) or otherwise mutate its health state. But an INTERNAL deadline
|
|
5424
|
+
// is a real failure: an `AbortSignal.timeout(...)` from an internal probe
|
|
5425
|
+
// (`#checkCredentialHealth` / `#fetchUsageUncached`) may have cut short a
|
|
5426
|
+
// dial that already consumed the rotating refresh token upstream, so its
|
|
5427
|
+
// failure must still be memoized — otherwise the same (credential, token)
|
|
5428
|
+
// pair is immediately eligible for a second refresh, replaying the token
|
|
5429
|
+
// and tripping provider reuse detection. Skip the guard update only for a
|
|
5430
|
+
// caller-owned abort, never for an internal timeout.
|
|
5431
|
+
if (signal?.aborted && !isTimeoutAbort(signal)) throw error;
|
|
5232
5432
|
if (localDial && credentialId !== undefined) {
|
|
5233
5433
|
for (const [key, entry] of this.#recentOAuthRefreshFailures) {
|
|
5234
5434
|
if (entry.expiresAt <= Date.now()) this.#recentOAuthRefreshFailures.delete(key);
|
|
@@ -5298,17 +5498,11 @@ export class AuthStorage {
|
|
|
5298
5498
|
allowBlocked: boolean;
|
|
5299
5499
|
prefetchedUsage?: UsageReport | null;
|
|
5300
5500
|
usagePrechecked?: boolean;
|
|
5301
|
-
enforceProRequirement?: boolean;
|
|
5302
5501
|
},
|
|
5303
5502
|
reloadsUsed = 0,
|
|
5503
|
+
sessionSelector?: AuthCredentialSelector,
|
|
5304
5504
|
): Promise<OAuthResolutionResult | undefined> {
|
|
5305
|
-
const {
|
|
5306
|
-
checkUsage,
|
|
5307
|
-
allowBlocked,
|
|
5308
|
-
prefetchedUsage = null,
|
|
5309
|
-
usagePrechecked = false,
|
|
5310
|
-
enforceProRequirement,
|
|
5311
|
-
} = usageOptions;
|
|
5505
|
+
const { checkUsage, allowBlocked, prefetchedUsage = null, usagePrechecked = false } = usageOptions;
|
|
5312
5506
|
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return undefined;
|
|
5313
5507
|
if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index)) {
|
|
5314
5508
|
return undefined;
|
|
@@ -5318,12 +5512,10 @@ export class AuthStorage {
|
|
|
5318
5512
|
return undefined;
|
|
5319
5513
|
}
|
|
5320
5514
|
|
|
5321
|
-
const requiresProModel = requiresOpenAICodexProModel(provider, options?.modelId);
|
|
5322
|
-
const applyProFilter = enforceProRequirement ?? requiresProModel;
|
|
5323
5515
|
let usage: UsageReport | null = null;
|
|
5324
5516
|
let usageChecked = false;
|
|
5325
5517
|
|
|
5326
|
-
if (
|
|
5518
|
+
if (checkUsage && !allowBlocked) {
|
|
5327
5519
|
if (usagePrechecked) {
|
|
5328
5520
|
usage = prefetchedUsage;
|
|
5329
5521
|
usageChecked = true;
|
|
@@ -5334,9 +5526,6 @@ export class AuthStorage {
|
|
|
5334
5526
|
});
|
|
5335
5527
|
usageChecked = true;
|
|
5336
5528
|
}
|
|
5337
|
-
if (applyProFilter && !hasOpenAICodexProPlan(usage)) {
|
|
5338
|
-
return undefined;
|
|
5339
|
-
}
|
|
5340
5529
|
if (checkUsage && !allowBlocked && usage && this.#isUsageLimitReached(usage)) {
|
|
5341
5530
|
const resetAtMs = this.#getUsageResetAtMs(usage, Date.now());
|
|
5342
5531
|
this.#markCredentialBlocked(
|
|
@@ -5407,7 +5596,7 @@ export class AuthStorage {
|
|
|
5407
5596
|
selectionCredentialId,
|
|
5408
5597
|
);
|
|
5409
5598
|
|
|
5410
|
-
if (
|
|
5599
|
+
if (checkUsage && !allowBlocked) {
|
|
5411
5600
|
const sameAccount = selection.credential.accountId === updated.accountId;
|
|
5412
5601
|
if (!usageChecked || !sameAccount) {
|
|
5413
5602
|
usage = await this.#getUsageReport(provider, updated, {
|
|
@@ -5416,9 +5605,6 @@ export class AuthStorage {
|
|
|
5416
5605
|
});
|
|
5417
5606
|
usageChecked = true;
|
|
5418
5607
|
}
|
|
5419
|
-
if (applyProFilter && !hasOpenAICodexProPlan(usage)) {
|
|
5420
|
-
return undefined;
|
|
5421
|
-
}
|
|
5422
5608
|
if (checkUsage && !allowBlocked && usage && this.#isUsageLimitReached(usage)) {
|
|
5423
5609
|
const resetAtMs = this.#getUsageResetAtMs(usage, Date.now());
|
|
5424
5610
|
this.#markCredentialBlocked(
|
|
@@ -5434,6 +5620,7 @@ export class AuthStorage {
|
|
|
5434
5620
|
this.#recordSessionCredential(provider, sessionId, "oauth", selection.index);
|
|
5435
5621
|
return { apiKey: result.apiKey, credential: updated };
|
|
5436
5622
|
} catch (error) {
|
|
5623
|
+
if (options?.signal?.aborted) throw error;
|
|
5437
5624
|
if (isSqliteError(error)) throw error;
|
|
5438
5625
|
// Auth-broker errors retain the sanitized upstream body separately from
|
|
5439
5626
|
// their transport message. Include that body for failure classification
|
|
@@ -5517,6 +5704,9 @@ export class AuthStorage {
|
|
|
5517
5704
|
`oauth refresh failed: ${errorMsg}`,
|
|
5518
5705
|
attemptedCredentialId,
|
|
5519
5706
|
);
|
|
5707
|
+
if (sessionSelector && sessionId) {
|
|
5708
|
+
this.markSessionCredentialUnavailable(sessionId, provider, sessionSelector);
|
|
5709
|
+
}
|
|
5520
5710
|
if (!disabled) {
|
|
5521
5711
|
// The CAS predicate compares the row's serialized `data`, so it also
|
|
5522
5712
|
// misses when nothing was rotated: the row may have been replaced by
|
|
@@ -5529,7 +5719,12 @@ export class AuthStorage {
|
|
|
5529
5719
|
// peer rotation to clobber — so apply it directly instead of looping.
|
|
5530
5720
|
const stillHoldsAttemptedToken =
|
|
5531
5721
|
attemptedCredentialId !== undefined &&
|
|
5532
|
-
this.#credentialRowHoldsRefreshToken(provider, attemptedCredentialId, attemptedRefreshToken)
|
|
5722
|
+
(this.#credentialRowHoldsRefreshToken(provider, attemptedCredentialId, attemptedRefreshToken) ||
|
|
5723
|
+
this.#credentialRowHoldsRefreshToken(
|
|
5724
|
+
provider,
|
|
5725
|
+
attemptedCredentialId,
|
|
5726
|
+
selection.credential.refresh,
|
|
5727
|
+
));
|
|
5533
5728
|
if (stillHoldsAttemptedToken && attemptedCredentialId !== undefined) {
|
|
5534
5729
|
logger.warn("OAuth refresh disable CAS mismatched an unrotated row; disabling by id", {
|
|
5535
5730
|
provider,
|
|
@@ -5587,6 +5782,9 @@ export class AuthStorage {
|
|
|
5587
5782
|
}
|
|
5588
5783
|
if (this.#getCredentialSelector(provider, options, sessionId)) {
|
|
5589
5784
|
const selector = this.#getCredentialSelector(provider, options, sessionId);
|
|
5785
|
+
if (selector && sessionId && !options?.credentialSelector) {
|
|
5786
|
+
this.markSessionCredentialUnavailable(sessionId, provider, selector);
|
|
5787
|
+
}
|
|
5590
5788
|
throw new Error(
|
|
5591
5789
|
`Selected credential for ${provider} (${selector ? this.#formatCredentialSelector(selector) : "unknown"}) is unavailable`,
|
|
5592
5790
|
);
|
|
@@ -5663,7 +5861,10 @@ export class AuthStorage {
|
|
|
5663
5861
|
* and get a best-effort token. For GitHub Copilot we preserve enterprise
|
|
5664
5862
|
* routing metadata so discovery can hit the correct host.
|
|
5665
5863
|
*/
|
|
5666
|
-
async peekApiKey(
|
|
5864
|
+
async peekApiKey(
|
|
5865
|
+
provider: string,
|
|
5866
|
+
options?: Pick<AuthApiKeyOptions, "owner"> & { sessionId?: string },
|
|
5867
|
+
): Promise<string | undefined> {
|
|
5667
5868
|
provider = resolveOAuthStorageProvider(provider);
|
|
5668
5869
|
const runtimeKey = this.#runtimeOverrides.get(provider);
|
|
5669
5870
|
if (runtimeKey) return runtimeKey;
|
|
@@ -5671,11 +5872,12 @@ export class AuthStorage {
|
|
|
5671
5872
|
const configOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
5672
5873
|
const configKey = configOverride?.apiKey;
|
|
5673
5874
|
if (configKey && !configOverride?.envSourced) return configKey;
|
|
5875
|
+
if (options?.sessionId && this.hasSessionCredentialUnavailable(provider, options.sessionId)) return undefined;
|
|
5674
5876
|
|
|
5675
5877
|
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
5676
5878
|
provider,
|
|
5677
5879
|
options?.owner ? { owner: options.owner } : undefined,
|
|
5678
|
-
|
|
5880
|
+
options?.sessionId,
|
|
5679
5881
|
);
|
|
5680
5882
|
if (configKey) {
|
|
5681
5883
|
// Env-sourced (`apiKeyEnv`) override: same precedence as getApiKey —
|
|
@@ -5702,6 +5904,10 @@ export class AuthStorage {
|
|
|
5702
5904
|
}
|
|
5703
5905
|
return undefined;
|
|
5704
5906
|
}
|
|
5907
|
+
// A hard selector is an identity boundary. If its selected row cannot
|
|
5908
|
+
// provide a current token, discovery must not continue into the shared
|
|
5909
|
+
// credential pool and silently query another account's catalog.
|
|
5910
|
+
if (this.#getCredentialSelector(provider, undefined, options?.sessionId)) return undefined;
|
|
5705
5911
|
|
|
5706
5912
|
const attemptedApiKeyIndices = new Set<number>();
|
|
5707
5913
|
for (;;) {
|
|
@@ -5770,6 +5976,14 @@ export class AuthStorage {
|
|
|
5770
5976
|
if (storedApiKey) return storedApiKey;
|
|
5771
5977
|
return configKey;
|
|
5772
5978
|
}
|
|
5979
|
+
if (sessionId && !options?.credentialSelector) {
|
|
5980
|
+
const unavailableSelector = this.#sessionCredentialUnavailable.get(sessionId)?.get(provider);
|
|
5981
|
+
if (unavailableSelector) {
|
|
5982
|
+
throw new Error(
|
|
5983
|
+
`Selected credential for ${provider} (${this.#formatCredentialSelector(unavailableSelector)}) is unavailable`,
|
|
5984
|
+
);
|
|
5985
|
+
}
|
|
5986
|
+
}
|
|
5773
5987
|
|
|
5774
5988
|
if (selectedCredential?.credential.type === "api_key") {
|
|
5775
5989
|
this.#recordSessionCredential(provider, sessionId, "api_key", selectedCredential.index);
|