@gajae-code/ai 0.15.4 → 0.15.6
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 +27 -1
- package/dist/types/auth-broker/client.d.ts +6 -2
- package/dist/types/auth-broker/remote-store.d.ts +14 -2
- package/dist/types/auth-broker/types.d.ts +6 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +19 -0
- package/dist/types/auth-gateway/server.d.ts +39 -5
- package/dist/types/auth-gateway/types.d.ts +16 -2
- package/dist/types/auth-storage.d.ts +116 -34
- package/dist/types/provider-models/openai-compat.d.ts +1 -0
- package/dist/types/provider-models/special.d.ts +2 -1
- package/dist/types/providers/kiro-api-key.d.ts +50 -0
- package/dist/types/providers/kiro-codewhisperer.d.ts +3 -0
- package/dist/types/providers/register-builtins.d.ts +12 -12
- package/dist/types/stream.d.ts +2 -1
- package/dist/types/types.d.ts +35 -24
- package/dist/types/utils/fallback-transport.d.ts +7 -0
- package/dist/types/utils/json-parse.d.ts +5 -3
- package/dist/types/utils/oauth/api-key-login.d.ts +4 -1
- package/dist/types/utils/oauth/api-key-validation.d.ts +12 -6
- package/dist/types/utils/oauth/commandcode.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/dist/types/utils/retry.d.ts +2 -0
- package/dist/types/utils/tool-call-healing.d.ts +4 -4
- package/package.json +3 -3
- package/src/auth-broker/client.ts +41 -13
- package/src/auth-broker/redact.ts +25 -1
- package/src/auth-broker/remote-store.ts +374 -115
- package/src/auth-broker/server.ts +131 -91
- package/src/auth-broker/types.ts +6 -0
- package/src/auth-broker/wire-schemas.ts +6 -0
- package/src/auth-gateway/server.ts +447 -79
- package/src/auth-gateway/types.ts +28 -2
- package/src/auth-storage.ts +742 -157
- package/src/cli.ts +1 -0
- package/src/model-thinking.ts +16 -0
- package/src/models.json +1054 -0
- package/src/models.ts +9 -1
- package/src/provider-models/descriptors.ts +3 -1
- package/src/provider-models/openai-compat.ts +41 -1
- package/src/provider-models/special.ts +15 -3
- package/src/providers/anthropic.ts +7 -1
- package/src/providers/azure-openai-responses.ts +4 -1
- package/src/providers/cursor.ts +256 -101
- package/src/providers/gitlab-duo.ts +18 -1
- package/src/providers/google-gemini-cli.ts +3 -0
- package/src/providers/google-shared.ts +3 -0
- package/src/providers/kiro-api-key.d.ts +50 -0
- package/src/providers/kiro-api-key.ts +786 -0
- package/src/providers/kiro-codewhisperer.d.ts +3 -0
- package/src/providers/kiro-codewhisperer.ts +34 -9
- package/src/providers/ollama.ts +3 -0
- package/src/providers/openai-codex-responses.ts +24 -6
- package/src/providers/openai-completions.ts +11 -1
- package/src/providers/openai-responses-shared.ts +23 -2
- package/src/providers/openai-responses.ts +10 -1
- package/src/providers/pi-native-client.ts +1 -0
- package/src/providers/pi-native-server.ts +24 -0
- package/src/providers/register-builtins.d.ts +12 -12
- package/src/providers/register-builtins.ts +16 -3
- package/src/stream.d.ts +2 -1
- package/src/stream.ts +180 -70
- package/src/types.d.ts +35 -24
- package/src/types.ts +40 -23
- package/src/utils/fallback-transport.d.ts +7 -0
- package/src/utils/fallback-transport.ts +21 -4
- package/src/utils/json-parse.d.ts +5 -3
- package/src/utils/json-parse.ts +6 -6
- package/src/utils/oauth/api-key-login.ts +13 -2
- package/src/utils/oauth/api-key-validation.ts +242 -41
- package/src/utils/oauth/commandcode.ts +17 -0
- package/src/utils/oauth/index.ts +20 -5
- package/src/utils/oauth/types.d.ts +1 -1
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils/retry.d.ts +2 -0
- package/src/utils/retry.ts +15 -2
- package/src/utils/tool-call-healing.d.ts +4 -4
- package/src/utils/tool-call-healing.ts +4 -4
package/src/auth-storage.ts
CHANGED
|
@@ -47,6 +47,20 @@ export type ApiKeyCredential = {
|
|
|
47
47
|
key: string;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Extracts the bearer token from the structured API-key form used by OAuth
|
|
52
|
+
* providers that need to carry token metadata alongside the access token.
|
|
53
|
+
*/
|
|
54
|
+
export function extractStructuredApiKeyToken(apiKey: string): string | undefined {
|
|
55
|
+
if (!apiKey.startsWith("{")) return undefined;
|
|
56
|
+
try {
|
|
57
|
+
const parsed = JSON.parse(apiKey) as { token?: unknown };
|
|
58
|
+
return typeof parsed.token === "string" ? parsed.token : undefined;
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
export interface MCPOAuthBinding {
|
|
51
65
|
/** Exact HTTP(S) origin of the MCP resource endpoint. */
|
|
52
66
|
resourceOrigin: string;
|
|
@@ -369,6 +383,7 @@ export interface AuthCredentialSnapshotEntry {
|
|
|
369
383
|
provider: string;
|
|
370
384
|
credential: SnapshotCredential;
|
|
371
385
|
identityKey: string | null;
|
|
386
|
+
revision?: number;
|
|
372
387
|
}
|
|
373
388
|
|
|
374
389
|
export type AuthCredentialIfAbsentReason =
|
|
@@ -430,8 +445,21 @@ export type OAuthRefreshLeaseClaim =
|
|
|
430
445
|
| { kind: "busy"; expiresAt: number }
|
|
431
446
|
| { kind: "missing" };
|
|
432
447
|
|
|
448
|
+
/**
|
|
449
|
+
* Store-owned ticket that orders a provider admission against remote
|
|
450
|
+
* credential snapshot application. The ticket is intentionally released at
|
|
451
|
+
* provider admission, not response completion.
|
|
452
|
+
*/
|
|
453
|
+
export interface CredentialDispatchTicket {
|
|
454
|
+
release(): void;
|
|
455
|
+
}
|
|
456
|
+
|
|
433
457
|
export interface AuthCredentialStore {
|
|
434
458
|
close(): void;
|
|
459
|
+
refreshSnapshot?(signal?: AbortSignal): Promise<unknown>;
|
|
460
|
+
onSnapshotChanged?(listener: () => void): () => void;
|
|
461
|
+
/** Order provider admission with remote snapshot authority application. */
|
|
462
|
+
acquireCredentialDispatchTicket?(provider: Provider, signal?: AbortSignal): Promise<CredentialDispatchTicket>;
|
|
435
463
|
listAuthCredentials(provider?: string): StoredAuthCredential[];
|
|
436
464
|
/** Payload-free account inventory; active and soft-disabled rows are included. */
|
|
437
465
|
listCredentialInventory?(provider?: string): CredentialInventoryRecord[];
|
|
@@ -445,12 +473,15 @@ export interface AuthCredentialStore {
|
|
|
445
473
|
updateAuthCredential(id: number, credential: AuthCredential): void;
|
|
446
474
|
deleteAuthCredential(id: number, disabledCause: string): void;
|
|
447
475
|
tryDisableAuthCredentialIfMatches(id: number, expectedData: string, disabledCause: string): boolean;
|
|
476
|
+
tryDisableAuthCredentialIfRevision?(id: number, expectedRevision: number, disabledCause: string): boolean;
|
|
448
477
|
replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[];
|
|
449
478
|
upsertAuthCredentialForProvider(provider: string, credential: AuthCredential): StoredAuthCredential[];
|
|
450
479
|
upsertAuthCredentialForProviderIfAbsent(provider: string, credential: AuthCredential): AuthCredentialIfAbsentResult;
|
|
451
480
|
deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void;
|
|
452
481
|
getCache(key: string, options?: { includeExpired?: boolean }): string | null;
|
|
453
482
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
483
|
+
/** Atomically allocate a durable sequence for broker restart epochs. */
|
|
484
|
+
allocateMonotonicSequence(key: string, expiresAtSec: number): number;
|
|
454
485
|
deleteCachePrefix?(prefix: string): void;
|
|
455
486
|
cleanExpiredCache(): void;
|
|
456
487
|
/**
|
|
@@ -514,6 +545,7 @@ export interface AuthCredentialStore {
|
|
|
514
545
|
* `signal` propagates the agent's cancel down to the broker fetch.
|
|
515
546
|
*/
|
|
516
547
|
fetchUsageReports?(signal?: AbortSignal): Promise<UsageReport[] | null>;
|
|
548
|
+
fetchUsageReportsForProvider?(provider: Provider, signal?: AbortSignal): Promise<UsageReport[] | null>;
|
|
517
549
|
/** Synchronous, zero-network usage presentation peek. */
|
|
518
550
|
peekCachedUsagePresentation?(provider: Provider, credentialId: number): CachedUsagePresentation | undefined;
|
|
519
551
|
/** Record a safe usage observation after an explicit fetch/check. */
|
|
@@ -552,6 +584,21 @@ export interface AuthCredentialStore {
|
|
|
552
584
|
* {@link AuthStorage.invalidateCredentialMatching} fall back to `reload()`.
|
|
553
585
|
*/
|
|
554
586
|
markCredentialSuspect?(credentialId: number, opts?: { signal?: AbortSignal }): Promise<void>;
|
|
587
|
+
/**
|
|
588
|
+
* Optional async write hook to disable one credential through an authoritative
|
|
589
|
+
* remote store. Remote clients MUST use this hook instead of the synchronous
|
|
590
|
+
* local delete methods when an OAuth refresh fails definitively.
|
|
591
|
+
*
|
|
592
|
+
* Returns `false` when the row is already absent (for example, a peer
|
|
593
|
+
* disabled it first). Implementations MUST NOT treat a failed remote write as
|
|
594
|
+
* a successful local deletion.
|
|
595
|
+
*/
|
|
596
|
+
disableAuthCredentialRemote?(
|
|
597
|
+
credentialId: number,
|
|
598
|
+
disabledCause: string,
|
|
599
|
+
signal?: AbortSignal,
|
|
600
|
+
expectedRevision?: number,
|
|
601
|
+
): Promise<boolean>;
|
|
555
602
|
/**
|
|
556
603
|
* Optional async write hook for upserting a single credential. When present,
|
|
557
604
|
* `AuthStorage.#upsertOAuthCredential` routes through this instead of the
|
|
@@ -675,6 +722,7 @@ export type AuthStorageOptions = {
|
|
|
675
722
|
* AuthStorage caller surfaces that to its own consumer unchanged.
|
|
676
723
|
*/
|
|
677
724
|
fetchUsageReports?: (signal?: AbortSignal) => Promise<UsageReport[] | null>;
|
|
725
|
+
fetchUsageReportsForProvider?: (provider: Provider, signal?: AbortSignal) => Promise<UsageReport[] | null>;
|
|
678
726
|
};
|
|
679
727
|
|
|
680
728
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -995,9 +1043,11 @@ type UsageRequestDescriptor = {
|
|
|
995
1043
|
baseUrl?: string;
|
|
996
1044
|
};
|
|
997
1045
|
|
|
998
|
-
type AuthApiKeyOptions = {
|
|
1046
|
+
export type AuthApiKeyOptions = {
|
|
999
1047
|
baseUrl?: string;
|
|
1000
1048
|
modelId?: string;
|
|
1049
|
+
/** Select config registrations owned by one caller (for example a ModelRegistry). */
|
|
1050
|
+
owner?: object;
|
|
1001
1051
|
/**
|
|
1002
1052
|
* Caller's cancel signal. Threaded into any broker-bound OAuth refresh so
|
|
1003
1053
|
* `ESC` / request abort actually kills a hung broker fetch instead of
|
|
@@ -1036,6 +1086,7 @@ export interface OAuthAccess {
|
|
|
1036
1086
|
export interface InvalidateCredentialMatchingOptions {
|
|
1037
1087
|
signal?: AbortSignal;
|
|
1038
1088
|
sessionId?: string;
|
|
1089
|
+
owner?: object;
|
|
1039
1090
|
}
|
|
1040
1091
|
|
|
1041
1092
|
function isAbortSignalOption(
|
|
@@ -1054,17 +1105,40 @@ function safeUsageReport(report: UsageReport): SafeUsageReport {
|
|
|
1054
1105
|
}
|
|
1055
1106
|
|
|
1056
1107
|
function scrubHealthReason(reason: unknown, secrets: readonly string[] = []): string {
|
|
1057
|
-
let value = reason instanceof Error ? reason.message : String(reason);
|
|
1108
|
+
let value = (reason instanceof Error ? reason.message : String(reason)).replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, "");
|
|
1058
1109
|
for (const secret of secrets) {
|
|
1059
|
-
if (secret.length > 0)
|
|
1110
|
+
if (secret.length > 0) {
|
|
1111
|
+
value = value.split(secret).join("[redacted]");
|
|
1112
|
+
const escaped = [...secret].map(char => char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&"));
|
|
1113
|
+
value = value.replace(new RegExp(escaped.join("\\s*"), "gu"), "[redacted]");
|
|
1114
|
+
}
|
|
1060
1115
|
}
|
|
1061
1116
|
value = value.replace(/bearer\s+[^\s,;]+/gi, "Bearer [redacted]");
|
|
1062
1117
|
value = value.replace(/(api[_-]?key|token|secret|authorization)[=:]\s*[^\s,;]+/gi, "$1=[redacted]");
|
|
1063
|
-
value = value
|
|
1118
|
+
value = value
|
|
1119
|
+
.replace(/[\x00-\x1f\x7f-\x9f]+/gu, " ")
|
|
1120
|
+
.replace(/[\r\n\t ]+/g, " ")
|
|
1121
|
+
.trim();
|
|
1064
1122
|
if (value.length > 256) value = `${value.slice(0, 253)}...`;
|
|
1065
1123
|
return value || "credential check failed";
|
|
1066
1124
|
}
|
|
1067
1125
|
|
|
1126
|
+
/** Read optional broker error detail without allowing hostile objects to escape classification. */
|
|
1127
|
+
/** @internal Tested directly because hostile accessors must preserve the original error identity. */
|
|
1128
|
+
export function readBrokerErrorBody(error: unknown): string | undefined {
|
|
1129
|
+
if (error === null || (typeof error !== "object" && typeof error !== "function")) return undefined;
|
|
1130
|
+
try {
|
|
1131
|
+
if (!("body" in error)) return undefined;
|
|
1132
|
+
const body = (error as { body?: unknown }).body;
|
|
1133
|
+
return typeof body === "string" ? body : undefined;
|
|
1134
|
+
} catch {
|
|
1135
|
+
// Preserve the original provider error and its terminal behavior. A proxy or
|
|
1136
|
+
// throwing accessor is untrusted provider data, not a reason to replace the
|
|
1137
|
+
// refresh failure with an inspection error.
|
|
1138
|
+
throw error;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1068
1142
|
function requiresOpenAICodexProModel(provider: string, modelId: string | undefined): boolean {
|
|
1069
1143
|
return provider === "openai-codex" && typeof modelId === "string" && modelId.includes("-spark");
|
|
1070
1144
|
}
|
|
@@ -1213,7 +1287,15 @@ class AuthStorageUsageCache implements UsageCache {
|
|
|
1213
1287
|
// In-memory representation
|
|
1214
1288
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1215
1289
|
|
|
1216
|
-
type StoredCredential = { id: number; credential: AuthCredential };
|
|
1290
|
+
type StoredCredential = { id: number; credential: AuthCredential; revision?: number };
|
|
1291
|
+
type IndexedStoredCredential<T extends AuthCredential = AuthCredential> = {
|
|
1292
|
+
id: number;
|
|
1293
|
+
credential: T;
|
|
1294
|
+
index: number;
|
|
1295
|
+
revision?: number;
|
|
1296
|
+
};
|
|
1297
|
+
type OAuthCredentialSelection = IndexedStoredCredential<OAuthCredential>;
|
|
1298
|
+
type ConfigApiKeyRegistration = { apiKey: string; envSourced: boolean; order: number };
|
|
1217
1299
|
|
|
1218
1300
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1219
1301
|
// AuthStorage Class
|
|
@@ -1231,6 +1313,18 @@ export class AuthStorage {
|
|
|
1231
1313
|
#data: Map<string, StoredCredential[]> = new Map();
|
|
1232
1314
|
#runtimeOverrides: Map<string, string> = new Map();
|
|
1233
1315
|
#configOverrides: Map<string, string> = new Map();
|
|
1316
|
+
/** Effective config override registrations, including owner-scoped model registries. */
|
|
1317
|
+
#configOverrideRegistrations: Map<string, Map<object, ConfigApiKeyRegistration>> = new Map();
|
|
1318
|
+
#unownedConfigOverrides: Map<string, ConfigApiKeyRegistration> = new Map();
|
|
1319
|
+
#configOverrideOrder = 0;
|
|
1320
|
+
/**
|
|
1321
|
+
* Providers whose config override was resolved from a models.yml `apiKeyEnv`
|
|
1322
|
+
* indirection rather than a literal `apiKey` pin. An env pointer is not a
|
|
1323
|
+
* pinned secret: the pointed-to value (shell env, trusted env files) can go
|
|
1324
|
+
* stale silently, and the 401 rotation machinery cannot repair it. A stored
|
|
1325
|
+
* api_key credential from `auth login` therefore outranks it.
|
|
1326
|
+
*/
|
|
1327
|
+
#configOverrideEnvSourced: Set<string> = new Set();
|
|
1234
1328
|
#runtimeCredentialSelectors: Map<string, AuthCredentialSelector> = new Map();
|
|
1235
1329
|
/** Soft runtime credential preference per provider; quota failures may rotate away from it. */
|
|
1236
1330
|
#runtimePreferredCredentialSelectors: Map<string, AuthCredentialSelector> = new Map();
|
|
@@ -1256,12 +1350,14 @@ export class AuthStorage {
|
|
|
1256
1350
|
#credentialRankingMode: CredentialRankingMode = "balanced";
|
|
1257
1351
|
#usageLogger?: UsageLogger;
|
|
1258
1352
|
#fallbackResolver?: (provider: string) => string | undefined;
|
|
1353
|
+
#ownedFallbackResolvers: Map<object, (provider: string) => string | undefined> = new Map();
|
|
1259
1354
|
#store: AuthCredentialStore;
|
|
1260
1355
|
#configValueResolver: (config: string, cacheScope?: string) => Promise<string | undefined>;
|
|
1261
1356
|
#resolvedStoredApiKeyValues: Map<string, Map<string, { fingerprint: string; usable: boolean }>> = new Map();
|
|
1262
1357
|
#storedApiKeyResolutionInFlight: Map<string, Map<string, Promise<string | undefined>>> = new Map();
|
|
1263
1358
|
#refreshOAuthCredentialOverride?: AuthStorageOptions["refreshOAuthCredential"];
|
|
1264
1359
|
#fetchUsageReportsOverride?: AuthStorageOptions["fetchUsageReports"];
|
|
1360
|
+
#fetchUsageReportsForProviderOverride?: AuthStorageOptions["fetchUsageReportsForProvider"];
|
|
1265
1361
|
#sourceLabel?: string;
|
|
1266
1362
|
#credentialDisabledListeners: Set<(event: CredentialDisabledEvent) => void | Promise<void>> = new Set();
|
|
1267
1363
|
/**
|
|
@@ -1291,6 +1387,10 @@ export class AuthStorage {
|
|
|
1291
1387
|
|
|
1292
1388
|
constructor(store: AuthCredentialStore, options: AuthStorageOptions = {}) {
|
|
1293
1389
|
this.#store = store;
|
|
1390
|
+
store.onSnapshotChanged?.(() => {
|
|
1391
|
+
this.#reloadCredentialRowsFromStore();
|
|
1392
|
+
void this.reload();
|
|
1393
|
+
});
|
|
1294
1394
|
this.#configValueResolver = options.configValueResolver ?? defaultConfigValueResolver;
|
|
1295
1395
|
this.#usageProviderResolver = options.usageProviderResolver ?? resolveDefaultUsageProvider;
|
|
1296
1396
|
this.#rankingStrategyResolver = options.rankingStrategyResolver ?? resolveDefaultRankingStrategy;
|
|
@@ -1300,6 +1400,7 @@ export class AuthStorage {
|
|
|
1300
1400
|
this.#credentialRankingMode = options.credentialRankingMode ?? "balanced";
|
|
1301
1401
|
this.#refreshOAuthCredentialOverride = options.refreshOAuthCredential;
|
|
1302
1402
|
this.#fetchUsageReportsOverride = options.fetchUsageReports;
|
|
1403
|
+
this.#fetchUsageReportsForProviderOverride = options.fetchUsageReportsForProvider;
|
|
1303
1404
|
this.#sourceLabel = options.sourceLabel;
|
|
1304
1405
|
if (options.onCredentialDisabled) {
|
|
1305
1406
|
// Constructor-registered subscribers are permanent for this AuthStorage's lifetime;
|
|
@@ -1344,6 +1445,15 @@ export class AuthStorage {
|
|
|
1344
1445
|
getGeneration(): number {
|
|
1345
1446
|
return this.#generation;
|
|
1346
1447
|
}
|
|
1448
|
+
getCache(key: string, options?: { includeExpired?: boolean }): string | null {
|
|
1449
|
+
return this.#store.getCache(key, options);
|
|
1450
|
+
}
|
|
1451
|
+
setCache(key: string, value: string, expiresAtSec: number): void {
|
|
1452
|
+
this.#store.setCache(key, value, expiresAtSec);
|
|
1453
|
+
}
|
|
1454
|
+
allocateMonotonicSequence(key: string, expiresAtSec: number): number {
|
|
1455
|
+
return this.#store.allocateMonotonicSequence(key, expiresAtSec);
|
|
1456
|
+
}
|
|
1347
1457
|
getProviderConfigurationGeneration(provider: string): number {
|
|
1348
1458
|
return this.#getProviderConfigurationGeneration(provider);
|
|
1349
1459
|
}
|
|
@@ -1356,13 +1466,29 @@ export class AuthStorage {
|
|
|
1356
1466
|
#getProviderConfigurationGeneration(provider: string): number {
|
|
1357
1467
|
return this.#providerConfigurationGenerations.get(resolveOAuthStorageProvider(provider)) ?? 1;
|
|
1358
1468
|
}
|
|
1359
|
-
|
|
1469
|
+
#configOverrideRegistration(provider: string, owner?: object): ConfigApiKeyRegistration | undefined {
|
|
1360
1470
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1471
|
+
if (owner) {
|
|
1472
|
+
return (
|
|
1473
|
+
this.#configOverrideRegistrations.get(storageProvider)?.get(owner) ??
|
|
1474
|
+
this.#unownedConfigOverrides.get(storageProvider)
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
const apiKey = this.#configOverrides.get(storageProvider);
|
|
1478
|
+
return apiKey === undefined
|
|
1479
|
+
? undefined
|
|
1480
|
+
: { apiKey, envSourced: this.#configOverrideEnvSourced.has(storageProvider), order: 0 };
|
|
1481
|
+
}
|
|
1482
|
+
#hasConfigOverride(provider: string, owner?: object): boolean {
|
|
1483
|
+
return this.#configOverrideRegistration(provider, owner) !== undefined;
|
|
1484
|
+
}
|
|
1485
|
+
getProviderEvidenceGeneration(provider: string, resolvedApiKey?: string, owner?: object): string {
|
|
1486
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1487
|
+
provider = storageProvider;
|
|
1361
1488
|
const evidenceApiKey = resolvedApiKey;
|
|
1489
|
+
const configOverride = this.#configOverrideRegistration(storageProvider, owner);
|
|
1362
1490
|
const storedLiteral =
|
|
1363
|
-
evidenceApiKey === undefined ||
|
|
1364
|
-
this.#runtimeOverrides.has(storageProvider) ||
|
|
1365
|
-
this.#configOverrides.has(storageProvider)
|
|
1491
|
+
evidenceApiKey === undefined || this.#runtimeOverrides.has(storageProvider) || configOverride !== undefined
|
|
1366
1492
|
? undefined
|
|
1367
1493
|
: this.#getCredentialsForProvider(storageProvider).find(
|
|
1368
1494
|
(credential): credential is Extract<AuthCredential, { type: "api_key" }> =>
|
|
@@ -1379,7 +1505,7 @@ export class AuthStorage {
|
|
|
1379
1505
|
}
|
|
1380
1506
|
let selectedCredential: ({ index: number } & StoredCredential) | undefined;
|
|
1381
1507
|
try {
|
|
1382
|
-
selectedCredential = this.#resolveSelectedStoredCredential(provider, undefined, undefined);
|
|
1508
|
+
selectedCredential = this.#resolveSelectedStoredCredential(provider, owner ? { owner } : undefined, undefined);
|
|
1383
1509
|
} catch {
|
|
1384
1510
|
return crypto
|
|
1385
1511
|
.createHash("sha256")
|
|
@@ -1395,7 +1521,7 @@ export class AuthStorage {
|
|
|
1395
1521
|
credential.type === "oauth" && Number.isFinite(credential.expires) && credential.expires > Date.now(),
|
|
1396
1522
|
);
|
|
1397
1523
|
const effectiveEnvKey =
|
|
1398
|
-
this.#runtimeOverrides.get(provider) ||
|
|
1524
|
+
this.#runtimeOverrides.get(provider) || configOverride?.apiKey || hasApiKey || hasUsableOAuth
|
|
1399
1525
|
? undefined
|
|
1400
1526
|
: getEnvApiKey(provider);
|
|
1401
1527
|
const storedApiKeyFingerprint = credentials
|
|
@@ -1543,12 +1669,21 @@ export class AuthStorage {
|
|
|
1543
1669
|
}
|
|
1544
1670
|
}
|
|
1545
1671
|
|
|
1546
|
-
/**
|
|
1547
|
-
|
|
1672
|
+
/**
|
|
1673
|
+
* Set the selector derived from a durable session pin or a session seed.
|
|
1674
|
+
* `owner` scopes config-override validation to one ModelRegistry; omitted
|
|
1675
|
+
* owners retain process-wide caller semantics.
|
|
1676
|
+
*/
|
|
1677
|
+
setSessionCredentialSelector(
|
|
1678
|
+
scopeId: string,
|
|
1679
|
+
provider: string,
|
|
1680
|
+
selector: AuthCredentialSelector,
|
|
1681
|
+
owner?: object,
|
|
1682
|
+
): void {
|
|
1548
1683
|
const scope = scopeId.trim();
|
|
1549
1684
|
if (!scope) throw new Error("Credential scope id must not be empty");
|
|
1550
1685
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1551
|
-
this.#assertCredentialSelectorUsable(storageProvider, selector);
|
|
1686
|
+
this.#assertCredentialSelectorUsable(storageProvider, selector, owner);
|
|
1552
1687
|
const selectors = this.#sessionCredentialSelectors.get(scope) ?? new Map<string, AuthCredentialSelector>();
|
|
1553
1688
|
selectors.set(storageProvider, selector);
|
|
1554
1689
|
this.#sessionCredentialSelectors.set(scope, selectors);
|
|
@@ -1607,23 +1742,32 @@ export class AuthStorage {
|
|
|
1607
1742
|
}
|
|
1608
1743
|
|
|
1609
1744
|
/** @internal Return cache provenance for an exact stored literal API-key row without resolving its value. */
|
|
1610
|
-
getStoredLiteralApiKeyEvidenceGeneration(
|
|
1745
|
+
getStoredLiteralApiKeyEvidenceGeneration(
|
|
1746
|
+
provider: string,
|
|
1747
|
+
selector: AuthCredentialSelector,
|
|
1748
|
+
owner?: object,
|
|
1749
|
+
): string | undefined {
|
|
1611
1750
|
if (selector.kind !== "id") return undefined;
|
|
1612
1751
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1613
|
-
if (this.#runtimeOverrides.has(storageProvider) || this.#
|
|
1752
|
+
if (this.#runtimeOverrides.has(storageProvider) || this.#hasConfigOverride(storageProvider, owner))
|
|
1753
|
+
return undefined;
|
|
1614
1754
|
const selected = this.#findCredentialBySelector(storageProvider, selector);
|
|
1615
1755
|
if (selected?.credential.type !== "api_key") return undefined;
|
|
1616
1756
|
const key = selected.credential.key;
|
|
1617
1757
|
if (!key || key.startsWith("!") || process.env[key] !== undefined) return undefined;
|
|
1618
|
-
return this.getProviderEvidenceGeneration(storageProvider, key);
|
|
1758
|
+
return this.getProviderEvidenceGeneration(storageProvider, key, owner);
|
|
1619
1759
|
}
|
|
1620
1760
|
|
|
1621
|
-
/**
|
|
1622
|
-
|
|
1761
|
+
/**
|
|
1762
|
+
* Validate and canonicalize an OAuth-only selector for account pinning.
|
|
1763
|
+
* `owner` scopes config-override checks to one ModelRegistry; omitted owners
|
|
1764
|
+
* retain process-wide caller semantics.
|
|
1765
|
+
*/
|
|
1766
|
+
resolveOAuthPinTarget(provider: string, selector: AuthCredentialSelector, owner?: object): OAuthPinTarget {
|
|
1623
1767
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1624
1768
|
if (
|
|
1625
1769
|
this.#runtimeOverrides.has(storageProvider) ||
|
|
1626
|
-
this.#
|
|
1770
|
+
this.#hasConfigOverride(storageProvider, owner) ||
|
|
1627
1771
|
getEnvApiKey(storageProvider)
|
|
1628
1772
|
) {
|
|
1629
1773
|
throw new OAuthCredentialSelectorError(
|
|
@@ -1820,8 +1964,8 @@ export class AuthStorage {
|
|
|
1820
1964
|
}
|
|
1821
1965
|
|
|
1822
1966
|
/** Whether a provider is currently authenticated by a config API-key override. */
|
|
1823
|
-
hasConfigApiKey(provider: string): boolean {
|
|
1824
|
-
return Boolean(this.#
|
|
1967
|
+
hasConfigApiKey(provider: string, owner?: object): boolean {
|
|
1968
|
+
return Boolean(this.#configOverrideRegistration(provider, owner)?.apiKey);
|
|
1825
1969
|
}
|
|
1826
1970
|
|
|
1827
1971
|
/**
|
|
@@ -1882,7 +2026,8 @@ export class AuthStorage {
|
|
|
1882
2026
|
* runtime API-key override (`--api-key`), or a config-sourced API key
|
|
1883
2027
|
* (`models.yml` `apiKey`) would each re-decide the credential on the very
|
|
1884
2028
|
* next {@link AuthStorage.getApiKey} call and make this switch appear to
|
|
1885
|
-
* silently do nothing.
|
|
2029
|
+
* silently do nothing. `owner` scopes the config-override check to one
|
|
2030
|
+
* ModelRegistry; omitted owners retain process-wide caller semantics.
|
|
1886
2031
|
*
|
|
1887
2032
|
* Deliberately does not touch credential-blocked state: if the target row
|
|
1888
2033
|
* is still backoff-blocked from a prior quota failure, the existing
|
|
@@ -1890,14 +2035,19 @@ export class AuthStorage {
|
|
|
1890
2035
|
* falls back to a usable account instead of re-issuing a request that would
|
|
1891
2036
|
* just draw another 429/quota error.
|
|
1892
2037
|
*/
|
|
1893
|
-
switchSessionCredential(
|
|
2038
|
+
switchSessionCredential(
|
|
2039
|
+
provider: string,
|
|
2040
|
+
sessionId: string,
|
|
2041
|
+
selector: AuthCredentialSelector,
|
|
2042
|
+
owner?: object,
|
|
2043
|
+
): void {
|
|
1894
2044
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1895
2045
|
if (this.#runtimeOverrides.has(storageProvider)) {
|
|
1896
2046
|
throw new Error(
|
|
1897
2047
|
`Cannot switch credential for ${provider}: a runtime API key override (--api-key) is active and always wins`,
|
|
1898
2048
|
);
|
|
1899
2049
|
}
|
|
1900
|
-
if (this.#
|
|
2050
|
+
if (this.#hasConfigOverride(storageProvider, owner)) {
|
|
1901
2051
|
throw new Error(
|
|
1902
2052
|
`Cannot switch credential for ${provider}: a config API key override (models.yml) is active and always wins`,
|
|
1903
2053
|
);
|
|
@@ -1926,38 +2076,122 @@ export class AuthStorage {
|
|
|
1926
2076
|
*
|
|
1927
2077
|
* Lower priority than {@link setRuntimeApiKey} so a CLI `--api-key`
|
|
1928
2078
|
* still wins for the duration of a single invocation.
|
|
2079
|
+
*
|
|
2080
|
+
* `options.owner` scopes the override to one registry or other caller. The
|
|
2081
|
+
* unscoped form is process-wide and is retained for standalone callers.
|
|
2082
|
+
*
|
|
2083
|
+
* `options.envSourced` marks the value as resolved from a models.yml
|
|
2084
|
+
* `apiKeyEnv` indirection. Unlike a literal pin, an env pointer only says
|
|
2085
|
+
* where to look for a key; when the user has since run `auth login`, the
|
|
2086
|
+
* stored api_key credential is the fresher, actively-managed secret and
|
|
2087
|
+
* wins over the indirection (stored OAuth credentials still yield, so a
|
|
2088
|
+
* custom-endpoint bearer is never replaced by an upstream OAuth token).
|
|
1929
2089
|
*/
|
|
1930
|
-
setConfigApiKey(provider: string, apiKey: string): void {
|
|
2090
|
+
setConfigApiKey(provider: string, apiKey: string, options: { envSourced?: boolean; owner?: object } = {}): void {
|
|
1931
2091
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1932
|
-
|
|
1933
|
-
|
|
2092
|
+
const registration = {
|
|
2093
|
+
apiKey,
|
|
2094
|
+
envSourced: options.envSourced === true,
|
|
2095
|
+
order: ++this.#configOverrideOrder,
|
|
2096
|
+
};
|
|
2097
|
+
if (options.owner) {
|
|
2098
|
+
const registrations = this.#configOverrideRegistrations.get(storageProvider) ?? new Map();
|
|
2099
|
+
registrations.set(options.owner, registration);
|
|
2100
|
+
this.#configOverrideRegistrations.set(storageProvider, registrations);
|
|
2101
|
+
} else {
|
|
2102
|
+
this.#unownedConfigOverrides.set(storageProvider, registration);
|
|
2103
|
+
}
|
|
2104
|
+
this.#reconcileConfigApiKey(storageProvider, "set-config-api-key", true);
|
|
1934
2105
|
}
|
|
1935
2106
|
|
|
1936
2107
|
/**
|
|
1937
2108
|
* Remove a single config-sourced API key override.
|
|
1938
2109
|
*/
|
|
1939
|
-
removeConfigApiKey(provider: string): void {
|
|
2110
|
+
removeConfigApiKey(provider: string, owner?: object): void {
|
|
1940
2111
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1941
|
-
if (
|
|
2112
|
+
if (owner) {
|
|
2113
|
+
const registrations = this.#configOverrideRegistrations.get(storageProvider);
|
|
2114
|
+
if (!registrations?.delete(owner)) return;
|
|
2115
|
+
if (registrations.size === 0) this.#configOverrideRegistrations.delete(storageProvider);
|
|
2116
|
+
} else {
|
|
2117
|
+
if (!this.#unownedConfigOverrides.delete(storageProvider)) return;
|
|
2118
|
+
}
|
|
2119
|
+
this.#reconcileConfigApiKey(storageProvider, "remove-config-api-key", true);
|
|
1942
2120
|
}
|
|
1943
2121
|
|
|
1944
2122
|
/**
|
|
1945
|
-
* Drop
|
|
1946
|
-
*
|
|
2123
|
+
* Drop config-sourced API keys. An owner removes only its own registrations;
|
|
2124
|
+
* the unscoped form remains an explicit global reset for callers that own the
|
|
2125
|
+
* entire AuthStorage instance.
|
|
1947
2126
|
*/
|
|
1948
|
-
clearConfigApiKeys(): void {
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2127
|
+
clearConfigApiKeys(owner?: object): void {
|
|
2128
|
+
if (owner) {
|
|
2129
|
+
const providers = [...this.#configOverrideRegistrations.entries()]
|
|
2130
|
+
.filter(([, registrations]) => registrations.has(owner))
|
|
2131
|
+
.map(([provider]) => provider);
|
|
2132
|
+
for (const provider of providers) this.removeConfigApiKey(provider, owner);
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
const providers = new Set([
|
|
2136
|
+
...this.#configOverrides.keys(),
|
|
2137
|
+
...this.#unownedConfigOverrides.keys(),
|
|
2138
|
+
...this.#configOverrideRegistrations.keys(),
|
|
2139
|
+
]);
|
|
2140
|
+
this.#unownedConfigOverrides.clear();
|
|
2141
|
+
this.#configOverrideRegistrations.clear();
|
|
2142
|
+
for (const provider of providers) this.#reconcileConfigApiKey(provider, "clear-config-api-keys", true);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
#reconcileConfigApiKey(provider: string, reason: string, forceGeneration = false): void {
|
|
2146
|
+
const previous = this.#configOverrides.get(provider);
|
|
2147
|
+
const previousEnvSourced = this.#configOverrideEnvSourced.has(provider);
|
|
2148
|
+
let winner: { apiKey: string; envSourced: boolean; order: number } | undefined =
|
|
2149
|
+
this.#unownedConfigOverrides.get(provider);
|
|
2150
|
+
for (const registration of this.#configOverrideRegistrations.get(provider)?.values() ?? []) {
|
|
2151
|
+
if (!winner || registration.order > winner.order) winner = registration;
|
|
2152
|
+
}
|
|
2153
|
+
if (!winner) {
|
|
2154
|
+
this.#configOverrides.delete(provider);
|
|
2155
|
+
this.#configOverrideEnvSourced.delete(provider);
|
|
2156
|
+
} else {
|
|
2157
|
+
this.#configOverrides.set(provider, winner.apiKey);
|
|
2158
|
+
if (winner.envSourced) this.#configOverrideEnvSourced.add(provider);
|
|
2159
|
+
else this.#configOverrideEnvSourced.delete(provider);
|
|
2160
|
+
}
|
|
2161
|
+
const current = this.#configOverrides.get(provider);
|
|
2162
|
+
const currentEnvSourced = this.#configOverrideEnvSourced.has(provider);
|
|
2163
|
+
if (forceGeneration || previous !== current || previousEnvSourced !== currentEnvSourced) {
|
|
2164
|
+
this.#bumpGeneration(reason, provider);
|
|
2165
|
+
}
|
|
1953
2166
|
}
|
|
1954
2167
|
|
|
1955
2168
|
/**
|
|
1956
2169
|
* Set a fallback resolver for API keys not found in storage or env vars.
|
|
1957
2170
|
* Used for custom provider keys from models.json.
|
|
1958
2171
|
*/
|
|
1959
|
-
setFallbackResolver(resolver: (provider: string) => string | undefined): void {
|
|
2172
|
+
setFallbackResolver(resolver: (provider: string) => string | undefined, owner?: object): () => void {
|
|
2173
|
+
if (owner) {
|
|
2174
|
+
this.#ownedFallbackResolvers.set(owner, resolver);
|
|
2175
|
+
return () => {
|
|
2176
|
+
if (this.#ownedFallbackResolvers.get(owner) !== resolver) return;
|
|
2177
|
+
this.#ownedFallbackResolvers.delete(owner);
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
1960
2180
|
this.#fallbackResolver = resolver;
|
|
2181
|
+
return () => {
|
|
2182
|
+
if (this.#fallbackResolver === resolver) this.#fallbackResolver = undefined;
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
#resolveFallback(provider: string, owner?: object): string | undefined {
|
|
2187
|
+
if (owner) {
|
|
2188
|
+
return this.#ownedFallbackResolvers.get(owner)?.(provider) ?? this.#fallbackResolver?.(provider);
|
|
2189
|
+
}
|
|
2190
|
+
for (const resolver of [...this.#ownedFallbackResolvers.values()].reverse()) {
|
|
2191
|
+
const value = resolver(provider);
|
|
2192
|
+
if (value !== undefined) return value;
|
|
2193
|
+
}
|
|
2194
|
+
return this.#fallbackResolver?.(provider);
|
|
1961
2195
|
}
|
|
1962
2196
|
|
|
1963
2197
|
/**
|
|
@@ -1965,11 +2199,27 @@ export class AuthStorage {
|
|
|
1965
2199
|
*/
|
|
1966
2200
|
async reload(): Promise<void> {
|
|
1967
2201
|
await this.#store.waitForReady?.();
|
|
2202
|
+
this.#reloadCredentialRowsFromStore();
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
/**
|
|
2206
|
+
* Acquire a store-owned provider-admission ticket when the backing store
|
|
2207
|
+
* provides one (for example, a remote broker snapshot store). Local stores
|
|
2208
|
+
* need no additional ordering and return `undefined`.
|
|
2209
|
+
*/
|
|
2210
|
+
async acquireCredentialDispatchTicket(
|
|
2211
|
+
provider: Provider,
|
|
2212
|
+
signal?: AbortSignal,
|
|
2213
|
+
): Promise<CredentialDispatchTicket | undefined> {
|
|
2214
|
+
return this.#store.acquireCredentialDispatchTicket?.(resolveOAuthStorageProvider(provider), signal);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
#reloadCredentialRowsFromStore(): void {
|
|
1968
2218
|
const records = this.#store.listAuthCredentials();
|
|
1969
2219
|
const grouped = new Map<string, StoredCredential[]>();
|
|
1970
2220
|
for (const record of records) {
|
|
1971
2221
|
const list = grouped.get(record.provider) ?? [];
|
|
1972
|
-
list.push({ id: record.id, credential: record.credential });
|
|
2222
|
+
list.push({ id: record.id, credential: record.credential, revision: record.revision });
|
|
1973
2223
|
grouped.set(record.provider, list);
|
|
1974
2224
|
}
|
|
1975
2225
|
|
|
@@ -2010,6 +2260,12 @@ export class AuthStorage {
|
|
|
2010
2260
|
#setStoredCredentials(provider: string, credentials: StoredCredential[]): void {
|
|
2011
2261
|
const current = this.#data.get(provider) ?? [];
|
|
2012
2262
|
if (storedCredentialArraysEqual(current, credentials)) return;
|
|
2263
|
+
const identityOrderChanged =
|
|
2264
|
+
current.length !== credentials.length ||
|
|
2265
|
+
current.some(
|
|
2266
|
+
(entry, index) =>
|
|
2267
|
+
entry.id !== credentials[index]?.id || entry.credential.type !== credentials[index]?.credential.type,
|
|
2268
|
+
);
|
|
2013
2269
|
this.#resolvedStoredApiKeyValues.delete(provider);
|
|
2014
2270
|
this.#storedApiKeyResolutionInFlight.delete(provider);
|
|
2015
2271
|
if (credentials.length === 0) {
|
|
@@ -2017,6 +2273,7 @@ export class AuthStorage {
|
|
|
2017
2273
|
} else {
|
|
2018
2274
|
this.#data.set(provider, credentials);
|
|
2019
2275
|
}
|
|
2276
|
+
if (identityOrderChanged) this.#resetProviderAssignments(resolveOAuthStorageProvider(provider));
|
|
2020
2277
|
this.#bumpGeneration("credentials", provider);
|
|
2021
2278
|
}
|
|
2022
2279
|
|
|
@@ -2217,10 +2474,7 @@ export class AuthStorage {
|
|
|
2217
2474
|
}
|
|
2218
2475
|
}
|
|
2219
2476
|
|
|
2220
|
-
#findCredentialBySelector(
|
|
2221
|
-
provider: string,
|
|
2222
|
-
selector: AuthCredentialSelector,
|
|
2223
|
-
): ({ index: number } & StoredCredential) | undefined {
|
|
2477
|
+
#findCredentialBySelector(provider: string, selector: AuthCredentialSelector): IndexedStoredCredential | undefined {
|
|
2224
2478
|
const stored = this.#getStoredCredentials(provider);
|
|
2225
2479
|
for (let index = 0; index < stored.length; index++) {
|
|
2226
2480
|
const entry = stored[index];
|
|
@@ -2263,13 +2517,13 @@ export class AuthStorage {
|
|
|
2263
2517
|
);
|
|
2264
2518
|
}
|
|
2265
2519
|
|
|
2266
|
-
#assertCredentialSelectorUsable(provider: string, selector: AuthCredentialSelector): void {
|
|
2520
|
+
#assertCredentialSelectorUsable(provider: string, selector: AuthCredentialSelector, owner?: object): void {
|
|
2267
2521
|
if (this.#runtimeOverrides.has(provider)) {
|
|
2268
2522
|
throw new Error(
|
|
2269
2523
|
`Credential selector ${this.#formatCredentialSelector(selector)} cannot be used for ${provider} while a runtime API key override is active`,
|
|
2270
2524
|
);
|
|
2271
2525
|
}
|
|
2272
|
-
if (this.#
|
|
2526
|
+
if (this.#hasConfigOverride(provider, owner)) {
|
|
2273
2527
|
throw new Error(
|
|
2274
2528
|
`Credential selector ${this.#formatCredentialSelector(selector)} cannot be used for ${provider} while a config API key override is active`,
|
|
2275
2529
|
);
|
|
@@ -2285,8 +2539,8 @@ export class AuthStorage {
|
|
|
2285
2539
|
* to an OAuth row specifically — the soft-preference/quota-fallback path is
|
|
2286
2540
|
* meaningless for a single static API key.
|
|
2287
2541
|
*/
|
|
2288
|
-
#assertPreferredCredentialSelectorUsable(provider: string, selector: AuthCredentialSelector): void {
|
|
2289
|
-
if (this.#runtimeOverrides.has(provider) || this.#
|
|
2542
|
+
#assertPreferredCredentialSelectorUsable(provider: string, selector: AuthCredentialSelector, owner?: object): void {
|
|
2543
|
+
if (this.#runtimeOverrides.has(provider) || this.#hasConfigOverride(provider, owner)) {
|
|
2290
2544
|
throw new Error(
|
|
2291
2545
|
`Preferred credential selector ${this.#formatCredentialSelector(selector)} cannot be used for ${provider} while an API key override is active`,
|
|
2292
2546
|
);
|
|
@@ -2303,10 +2557,10 @@ export class AuthStorage {
|
|
|
2303
2557
|
provider: string,
|
|
2304
2558
|
options?: AuthApiKeyOptions,
|
|
2305
2559
|
sessionId?: string,
|
|
2306
|
-
):
|
|
2560
|
+
): IndexedStoredCredential | undefined {
|
|
2307
2561
|
const selector = this.#getCredentialSelector(provider, options, sessionId);
|
|
2308
2562
|
if (!selector) return undefined;
|
|
2309
|
-
this.#assertCredentialSelectorUsable(resolveOAuthStorageProvider(provider), selector);
|
|
2563
|
+
this.#assertCredentialSelectorUsable(resolveOAuthStorageProvider(provider), selector, options?.owner);
|
|
2310
2564
|
const selected = this.#findCredentialBySelector(provider, selector);
|
|
2311
2565
|
if (!selected) {
|
|
2312
2566
|
throw new Error(`No credential found for ${provider} matching ${this.#formatCredentialSelector(selector)}`);
|
|
@@ -2323,13 +2577,23 @@ export class AuthStorage {
|
|
|
2323
2577
|
type: T,
|
|
2324
2578
|
sessionId?: string,
|
|
2325
2579
|
isUsable?: (credential: Extract<AuthCredential, { type: T }>, index: number) => boolean | undefined,
|
|
2326
|
-
):
|
|
2327
|
-
const credentials = this.#
|
|
2328
|
-
.map((
|
|
2580
|
+
): IndexedStoredCredential<Extract<AuthCredential, { type: T }>> | undefined {
|
|
2581
|
+
const credentials = this.#getStoredCredentials(provider)
|
|
2582
|
+
.map((entry, index) => ({ entry, index }))
|
|
2329
2583
|
.filter(
|
|
2330
|
-
(
|
|
2331
|
-
|
|
2332
|
-
|
|
2584
|
+
(
|
|
2585
|
+
item,
|
|
2586
|
+
): item is {
|
|
2587
|
+
entry: StoredCredential & { credential: Extract<AuthCredential, { type: T }> };
|
|
2588
|
+
index: number;
|
|
2589
|
+
} => item.entry.credential.type === type,
|
|
2590
|
+
)
|
|
2591
|
+
.map(({ entry, index }) => ({
|
|
2592
|
+
id: entry.id,
|
|
2593
|
+
credential: entry.credential,
|
|
2594
|
+
index,
|
|
2595
|
+
revision: entry.revision,
|
|
2596
|
+
}));
|
|
2333
2597
|
|
|
2334
2598
|
if (credentials.length === 0) return undefined;
|
|
2335
2599
|
|
|
@@ -2355,12 +2619,14 @@ export class AuthStorage {
|
|
|
2355
2619
|
sessionId?: string,
|
|
2356
2620
|
excludedIndices: ReadonlySet<number> = new Set(),
|
|
2357
2621
|
includeKnownUnusable = false,
|
|
2358
|
-
):
|
|
2359
|
-
const credentials = this.#
|
|
2360
|
-
.map((
|
|
2622
|
+
): IndexedStoredCredential<ApiKeyCredential> | undefined {
|
|
2623
|
+
const credentials = this.#getStoredCredentials(provider)
|
|
2624
|
+
.map((entry, index) => ({ entry, index }))
|
|
2361
2625
|
.filter(
|
|
2362
|
-
(
|
|
2363
|
-
|
|
2626
|
+
(item): item is { entry: StoredCredential & { credential: ApiKeyCredential }; index: number } =>
|
|
2627
|
+
item.entry.credential.type === "api_key",
|
|
2628
|
+
)
|
|
2629
|
+
.map(({ entry, index }) => ({ id: entry.id, credential: entry.credential, index, revision: entry.revision }));
|
|
2364
2630
|
if (credentials.length === 0) return undefined;
|
|
2365
2631
|
|
|
2366
2632
|
const providerKey = this.#getProviderTypeKey(provider, "api_key");
|
|
@@ -2399,11 +2665,20 @@ export class AuthStorage {
|
|
|
2399
2665
|
}
|
|
2400
2666
|
|
|
2401
2667
|
/** Updates a credential at index after OAuth token refresh. */
|
|
2402
|
-
#replaceCredentialAt(
|
|
2668
|
+
#replaceCredentialAt(
|
|
2669
|
+
provider: string,
|
|
2670
|
+
index: number,
|
|
2671
|
+
credential: AuthCredential,
|
|
2672
|
+
persist = true,
|
|
2673
|
+
expectedId?: number,
|
|
2674
|
+
): void {
|
|
2403
2675
|
const entries = this.#getStoredCredentials(provider);
|
|
2404
2676
|
if (index < 0 || index >= entries.length) return;
|
|
2405
2677
|
const target = entries[index];
|
|
2406
|
-
if (
|
|
2678
|
+
if (expectedId !== undefined && target.id !== expectedId) {
|
|
2679
|
+
throw new Error("Credential authority changed during refresh");
|
|
2680
|
+
}
|
|
2681
|
+
if (persist && !this.#store.refreshSnapshot) this.#store.updateAuthCredential(target.id, credential);
|
|
2407
2682
|
const updated = [...entries];
|
|
2408
2683
|
updated[index] = { id: target.id, credential };
|
|
2409
2684
|
this.#setStoredCredentials(provider, updated);
|
|
@@ -2434,15 +2709,18 @@ export class AuthStorage {
|
|
|
2434
2709
|
index: number,
|
|
2435
2710
|
expectedCredential: AuthCredential,
|
|
2436
2711
|
disabledCause: string,
|
|
2712
|
+
expectedId?: number,
|
|
2437
2713
|
): boolean {
|
|
2438
2714
|
const entries = this.#getStoredCredentials(provider);
|
|
2439
|
-
|
|
2440
|
-
|
|
2715
|
+
const targetIndex = expectedId === undefined ? index : entries.findIndex(entry => entry.id === expectedId);
|
|
2716
|
+
if (targetIndex < 0 || targetIndex >= entries.length) return false;
|
|
2717
|
+
const target = entries[targetIndex];
|
|
2718
|
+
if (expectedId !== undefined && target.id !== expectedId) return false;
|
|
2441
2719
|
const serialized = serializeCredential(provider, expectedCredential);
|
|
2442
2720
|
if (!serialized) return false;
|
|
2443
2721
|
const disabled = this.#store.tryDisableAuthCredentialIfMatches(target.id, serialized.data, disabledCause);
|
|
2444
2722
|
if (!disabled) return false;
|
|
2445
|
-
const updated = entries.filter((_value, idx) => idx !==
|
|
2723
|
+
const updated = entries.filter((_value, idx) => idx !== targetIndex);
|
|
2446
2724
|
this.#setStoredCredentials(provider, updated);
|
|
2447
2725
|
this.#clearSelectorsForRemovedCredential(provider, new Set([target.id]), entries);
|
|
2448
2726
|
this.#resetProviderAssignments(provider);
|
|
@@ -2479,6 +2757,35 @@ export class AuthStorage {
|
|
|
2479
2757
|
this.#emitCredentialDisabled({ provider, disabledCause });
|
|
2480
2758
|
}
|
|
2481
2759
|
|
|
2760
|
+
/**
|
|
2761
|
+
* Disable one credential through an authoritative remote store and reconcile
|
|
2762
|
+
* this AuthStorage instance only after that write succeeds. Remote stores
|
|
2763
|
+
* must never be mutated through the synchronous local delete path because
|
|
2764
|
+
* their snapshots do not own persistence authority.
|
|
2765
|
+
*/
|
|
2766
|
+
async #disableCredentialRemotely(
|
|
2767
|
+
provider: string,
|
|
2768
|
+
credentialId: number,
|
|
2769
|
+
disabledCause: string,
|
|
2770
|
+
signal?: AbortSignal,
|
|
2771
|
+
expectedRevision?: number,
|
|
2772
|
+
): Promise<boolean> {
|
|
2773
|
+
const disable = this.#store.disableAuthCredentialRemote?.bind(this.#store);
|
|
2774
|
+
if (!disable) return false;
|
|
2775
|
+
const disabled = await disable(credentialId, disabledCause, signal, expectedRevision);
|
|
2776
|
+
if (!disabled) return false;
|
|
2777
|
+
const entries = this.#getStoredCredentials(provider);
|
|
2778
|
+
if (!entries.some(entry => entry.id === credentialId)) return true;
|
|
2779
|
+
this.#setStoredCredentials(
|
|
2780
|
+
provider,
|
|
2781
|
+
entries.filter(entry => entry.id !== credentialId),
|
|
2782
|
+
);
|
|
2783
|
+
this.#clearSelectorsForRemovedCredential(provider, new Set([credentialId]), entries);
|
|
2784
|
+
this.#resetProviderAssignments(provider);
|
|
2785
|
+
this.#emitCredentialDisabled({ provider, disabledCause });
|
|
2786
|
+
return true;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2482
2789
|
/** Clear every selector whose durable/in-memory target was just removed. */
|
|
2483
2790
|
#clearSelectorsForRemovedCredential(
|
|
2484
2791
|
provider: string,
|
|
@@ -2553,7 +2860,7 @@ export class AuthStorage {
|
|
|
2553
2860
|
: this.#store.replaceAuthCredentialsForProvider(storageProvider, deduped);
|
|
2554
2861
|
this.#setStoredCredentials(
|
|
2555
2862
|
storageProvider,
|
|
2556
|
-
stored.map(record => ({ id: record.id, credential: record.credential })),
|
|
2863
|
+
stored.map(record => ({ id: record.id, credential: record.credential, revision: record.revision })),
|
|
2557
2864
|
);
|
|
2558
2865
|
this.#resetProviderAssignments(storageProvider);
|
|
2559
2866
|
}
|
|
@@ -2568,6 +2875,7 @@ export class AuthStorage {
|
|
|
2568
2875
|
provider: entry.provider,
|
|
2569
2876
|
credential: redacted,
|
|
2570
2877
|
identityKey: resolveCredentialIdentityKey(provider, persisted),
|
|
2878
|
+
...(entry.revision === undefined ? {} : { revision: entry.revision }),
|
|
2571
2879
|
};
|
|
2572
2880
|
});
|
|
2573
2881
|
}
|
|
@@ -2584,14 +2892,15 @@ export class AuthStorage {
|
|
|
2584
2892
|
async importCredentialIfAbsent(
|
|
2585
2893
|
provider: string,
|
|
2586
2894
|
credential: AuthCredential,
|
|
2895
|
+
owner?: object,
|
|
2587
2896
|
): Promise<AuthCredentialIfAbsentSnapshotResult> {
|
|
2588
2897
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2589
2898
|
if (this.#runtimeOverrides.has(storageProvider))
|
|
2590
2899
|
return this.#snapshotSkipResult(storageProvider, "skipped-existing-runtime");
|
|
2591
|
-
if (this.#
|
|
2900
|
+
if (this.#hasConfigOverride(storageProvider, owner))
|
|
2592
2901
|
return this.#snapshotSkipResult(storageProvider, "skipped-existing-config");
|
|
2593
2902
|
if (getEnvApiKey(storageProvider)) return this.#snapshotSkipResult(storageProvider, "skipped-existing-env");
|
|
2594
|
-
if (this.#
|
|
2903
|
+
if (this.#resolveFallback(storageProvider, owner))
|
|
2595
2904
|
return this.#snapshotSkipResult(storageProvider, "skipped-existing-fallback");
|
|
2596
2905
|
|
|
2597
2906
|
const result = this.#store.upsertAuthCredentialRemoteIfAbsent
|
|
@@ -2599,7 +2908,7 @@ export class AuthStorage {
|
|
|
2599
2908
|
: this.#store.upsertAuthCredentialForProviderIfAbsent(storageProvider, credential);
|
|
2600
2909
|
this.#setStoredCredentials(
|
|
2601
2910
|
storageProvider,
|
|
2602
|
-
result.entries.map(entry => ({ id: entry.id, credential: entry.credential })),
|
|
2911
|
+
result.entries.map(entry => ({ id: entry.id, credential: entry.credential, revision: entry.revision })),
|
|
2603
2912
|
);
|
|
2604
2913
|
this.#resetProviderAssignments(storageProvider);
|
|
2605
2914
|
if (result.inserted) this.#invalidateUsageCacheForProvider(storageProvider);
|
|
@@ -2617,7 +2926,7 @@ export class AuthStorage {
|
|
|
2617
2926
|
: this.#store.upsertAuthCredentialForProvider(provider, credential);
|
|
2618
2927
|
this.#setStoredCredentials(
|
|
2619
2928
|
provider,
|
|
2620
|
-
stored.map(record => ({ id: record.id, credential: record.credential })),
|
|
2929
|
+
stored.map(record => ({ id: record.id, credential: record.credential, revision: record.revision })),
|
|
2621
2930
|
);
|
|
2622
2931
|
this.#resetProviderAssignments(provider);
|
|
2623
2932
|
this.#invalidateUsageCacheForProvider(provider);
|
|
@@ -2667,23 +2976,44 @@ export class AuthStorage {
|
|
|
2667
2976
|
* Check if any form of auth is configured for a provider.
|
|
2668
2977
|
* Unlike getApiKey(), this doesn't refresh OAuth tokens.
|
|
2669
2978
|
*/
|
|
2670
|
-
#hasConfiguredAuth(storageProvider: string): boolean {
|
|
2979
|
+
#hasConfiguredAuth(storageProvider: string, owner?: object): boolean {
|
|
2671
2980
|
if (this.hasRuntimeApiKey(storageProvider)) return true;
|
|
2672
|
-
if (this.#
|
|
2981
|
+
if (this.#hasConfigOverride(storageProvider, owner)) return true;
|
|
2673
2982
|
if (this.#getCredentialsForProvider(storageProvider).length > 0) return true;
|
|
2674
2983
|
if (getEnvApiKey(storageProvider)) return true;
|
|
2675
|
-
if (this.#
|
|
2984
|
+
if (this.#resolveFallback(storageProvider, owner)) return true;
|
|
2676
2985
|
return false;
|
|
2677
2986
|
}
|
|
2678
2987
|
|
|
2679
|
-
|
|
2988
|
+
disableCredentialByIdIfRevision(id: number, expectedRevision: number, disabledCause: string): boolean {
|
|
2989
|
+
const cause = normalizeDisabledCause(disabledCause);
|
|
2990
|
+
if (this.#store.tryDisableAuthCredentialIfRevision?.(id, expectedRevision, cause) !== true) return false;
|
|
2991
|
+
for (const [provider, entries] of this.#data) {
|
|
2992
|
+
if (!entries.some(entry => entry.id === id)) continue;
|
|
2993
|
+
this.#setStoredCredentials(
|
|
2994
|
+
provider,
|
|
2995
|
+
entries.filter(entry => entry.id !== id),
|
|
2996
|
+
);
|
|
2997
|
+
this.#clearSelectorsForRemovedCredential(provider, new Set([id]), entries);
|
|
2998
|
+
this.#resetProviderAssignments(provider);
|
|
2999
|
+
this.#emitCredentialDisabled({ provider, disabledCause: cause });
|
|
3000
|
+
return true;
|
|
3001
|
+
}
|
|
3002
|
+
return false;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
hasAuth(provider: string, sessionId?: string, options?: Pick<AuthApiKeyOptions, "owner">): boolean {
|
|
2680
3006
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2681
3007
|
try {
|
|
2682
|
-
this.#resolveSelectedStoredCredential(
|
|
3008
|
+
this.#resolveSelectedStoredCredential(
|
|
3009
|
+
storageProvider,
|
|
3010
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
3011
|
+
sessionId,
|
|
3012
|
+
);
|
|
2683
3013
|
} catch {
|
|
2684
3014
|
return false;
|
|
2685
3015
|
}
|
|
2686
|
-
return this.#hasConfiguredAuth(storageProvider);
|
|
3016
|
+
return this.#hasConfiguredAuth(storageProvider, options?.owner);
|
|
2687
3017
|
}
|
|
2688
3018
|
|
|
2689
3019
|
/**
|
|
@@ -2691,15 +3021,24 @@ export class AuthStorage {
|
|
|
2691
3021
|
* Mirrors getApiKey selector validation, overrides, session OAuth stickiness,
|
|
2692
3022
|
* cached command-key usability, OAuth retry, and environment fallback order.
|
|
2693
3023
|
*/
|
|
2694
|
-
getEffectiveCredentialType(
|
|
3024
|
+
getEffectiveCredentialType(
|
|
3025
|
+
provider: string,
|
|
3026
|
+
sessionId?: string,
|
|
3027
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
3028
|
+
): AuthCredential["type"] | undefined {
|
|
2695
3029
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2696
3030
|
let selected: ({ index: number } & StoredCredential) | undefined;
|
|
2697
3031
|
try {
|
|
2698
|
-
selected = this.#resolveSelectedStoredCredential(
|
|
3032
|
+
selected = this.#resolveSelectedStoredCredential(
|
|
3033
|
+
storageProvider,
|
|
3034
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
3035
|
+
sessionId,
|
|
3036
|
+
);
|
|
2699
3037
|
} catch {
|
|
2700
3038
|
return undefined;
|
|
2701
3039
|
}
|
|
2702
|
-
if (this.hasRuntimeApiKey(storageProvider) || this.#
|
|
3040
|
+
if (this.hasRuntimeApiKey(storageProvider) || this.#hasConfigOverride(storageProvider, options?.owner))
|
|
3041
|
+
return "api_key";
|
|
2703
3042
|
if (selected) return selected.credential.type;
|
|
2704
3043
|
|
|
2705
3044
|
const credentials = this.#getCredentialsForProvider(storageProvider);
|
|
@@ -2712,19 +3051,23 @@ export class AuthStorage {
|
|
|
2712
3051
|
}
|
|
2713
3052
|
if (credentials.some(credential => credential.type === "oauth")) return "oauth";
|
|
2714
3053
|
if (apiKeys.length > 0) return "api_key";
|
|
2715
|
-
if (getEnvApiKey(storageProvider) || this.#
|
|
3054
|
+
if (getEnvApiKey(storageProvider) || this.#resolveFallback(storageProvider, options?.owner)) return "api_key";
|
|
2716
3055
|
return undefined;
|
|
2717
3056
|
}
|
|
2718
3057
|
|
|
2719
3058
|
/**
|
|
2720
3059
|
* Check whether configured auth is currently usable without resolving credentials.
|
|
2721
3060
|
*/
|
|
2722
|
-
hasUsableAuth(provider: string): boolean {
|
|
3061
|
+
hasUsableAuth(provider: string, options?: Pick<AuthApiKeyOptions, "owner">): boolean {
|
|
2723
3062
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2724
3063
|
try {
|
|
2725
|
-
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
3064
|
+
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
3065
|
+
storageProvider,
|
|
3066
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
3067
|
+
undefined,
|
|
3068
|
+
);
|
|
2726
3069
|
if (this.hasRuntimeApiKey(storageProvider)) return true;
|
|
2727
|
-
if (this.#
|
|
3070
|
+
if (this.#hasConfigOverride(storageProvider, options?.owner)) return true;
|
|
2728
3071
|
if (selectedCredential) {
|
|
2729
3072
|
if (selectedCredential.credential.type === "api_key") {
|
|
2730
3073
|
return (
|
|
@@ -2765,7 +3108,7 @@ export class AuthStorage {
|
|
|
2765
3108
|
} catch {
|
|
2766
3109
|
return false;
|
|
2767
3110
|
}
|
|
2768
|
-
return Boolean(getEnvApiKey(storageProvider) || this.#
|
|
3111
|
+
return Boolean(getEnvApiKey(storageProvider) || this.#resolveFallback(storageProvider, options?.owner));
|
|
2769
3112
|
}
|
|
2770
3113
|
|
|
2771
3114
|
/**
|
|
@@ -2778,10 +3121,14 @@ export class AuthStorage {
|
|
|
2778
3121
|
/**
|
|
2779
3122
|
* Get OAuth credentials for a provider.
|
|
2780
3123
|
*/
|
|
2781
|
-
getOAuthCredential(
|
|
3124
|
+
getOAuthCredential(
|
|
3125
|
+
provider: string,
|
|
3126
|
+
sessionId?: string,
|
|
3127
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
3128
|
+
): OAuthCredential | undefined {
|
|
2782
3129
|
const selected = this.#resolveSelectedStoredCredential(
|
|
2783
3130
|
resolveOAuthStorageProvider(provider),
|
|
2784
|
-
undefined,
|
|
3131
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
2785
3132
|
sessionId,
|
|
2786
3133
|
);
|
|
2787
3134
|
if (selected?.credential.type === "oauth") return selected.credential;
|
|
@@ -2798,7 +3145,11 @@ export class AuthStorage {
|
|
|
2798
3145
|
* first call before any `getApiKey` has been issued, or single-credential setups).
|
|
2799
3146
|
* Returns `undefined` when no OAuth credential carries an `accountId`.
|
|
2800
3147
|
*/
|
|
2801
|
-
getOAuthAccountId(
|
|
3148
|
+
getOAuthAccountId(
|
|
3149
|
+
provider: string,
|
|
3150
|
+
sessionId?: string,
|
|
3151
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
3152
|
+
): string | undefined {
|
|
2802
3153
|
provider = resolveOAuthStorageProvider(provider);
|
|
2803
3154
|
const allCredentials = this.#getCredentialsForProvider(provider);
|
|
2804
3155
|
const oauthCredentials = allCredentials.filter((c): c is OAuthCredential => c.type === "oauth");
|
|
@@ -2806,11 +3157,15 @@ export class AuthStorage {
|
|
|
2806
3157
|
|
|
2807
3158
|
// Runtime / config overrides bypass OAuth account_uuid attribution — the
|
|
2808
3159
|
// caller is authenticating with an explicit key, not the broker's OAuth.
|
|
2809
|
-
if (this.#runtimeOverrides.has(provider) || this.#
|
|
3160
|
+
if (this.#runtimeOverrides.has(provider) || this.#hasConfigOverride(provider, options?.owner)) return undefined;
|
|
2810
3161
|
|
|
2811
3162
|
// Prefer the session-sticky credential when available.
|
|
2812
3163
|
|
|
2813
|
-
const scopedSelection = this.#resolveSelectedStoredCredential(
|
|
3164
|
+
const scopedSelection = this.#resolveSelectedStoredCredential(
|
|
3165
|
+
provider,
|
|
3166
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
3167
|
+
sessionId,
|
|
3168
|
+
);
|
|
2814
3169
|
if (scopedSelection?.credential.type === "api_key") return undefined;
|
|
2815
3170
|
if (scopedSelection?.credential.type === "oauth") {
|
|
2816
3171
|
const accountId = scopedSelection.credential.accountId;
|
|
@@ -2826,7 +3181,7 @@ export class AuthStorage {
|
|
|
2826
3181
|
// account_uuid injection would misattribute traffic. Only apply this guard when
|
|
2827
3182
|
// sessionPref is absent; a recorded OAuth sticky (sessionPref.type === "oauth") must
|
|
2828
3183
|
// NOT be blocked even if an env key also happens to exist.
|
|
2829
|
-
if (!sessionPref && (getEnvApiKey(provider) || this.#
|
|
3184
|
+
if (!sessionPref && (getEnvApiKey(provider) || this.#resolveFallback(provider, options?.owner))) return undefined;
|
|
2830
3185
|
// Resolve the sticky index against the full credential list — the index is
|
|
2831
3186
|
// recorded against the unfiltered provider array (by #recordSessionCredential /
|
|
2832
3187
|
// #tryOAuthCredential), not the OAuth-only subset, so dereferencing it into the
|
|
@@ -2951,6 +3306,16 @@ export class AuthStorage {
|
|
|
2951
3306
|
credentials = await loginKimi(ctrl);
|
|
2952
3307
|
break;
|
|
2953
3308
|
}
|
|
3309
|
+
case "kiro": {
|
|
3310
|
+
const { loginKiro } = await import("./utils/oauth/kiro");
|
|
3311
|
+
credentials = await loginKiro({
|
|
3312
|
+
onAuth: (url, instructions) => ctrl.onAuth({ url, instructions }),
|
|
3313
|
+
onPrompt: ctrl.onPrompt,
|
|
3314
|
+
onProgress: ctrl.onProgress,
|
|
3315
|
+
signal: ctrl.signal,
|
|
3316
|
+
});
|
|
3317
|
+
break;
|
|
3318
|
+
}
|
|
2954
3319
|
case "kilo": {
|
|
2955
3320
|
const { loginKilo } = await import("./utils/oauth/kilo");
|
|
2956
3321
|
credentials = await loginKilo(ctrl);
|
|
@@ -2982,6 +3347,12 @@ export class AuthStorage {
|
|
|
2982
3347
|
await saveApiKeyCredential(apiKey);
|
|
2983
3348
|
return;
|
|
2984
3349
|
}
|
|
3350
|
+
case "commandcode-goat": {
|
|
3351
|
+
const { loginCommandCode } = await import("./utils/oauth/commandcode");
|
|
3352
|
+
const apiKey = await loginCommandCode(ctrl);
|
|
3353
|
+
await saveApiKeyCredential(apiKey);
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
2985
3356
|
case "lm-studio": {
|
|
2986
3357
|
const { loginLmStudio } = await import("./utils/oauth/lm-studio");
|
|
2987
3358
|
const apiKey = await loginLmStudio(ctrl);
|
|
@@ -3525,7 +3896,9 @@ export class AuthStorage {
|
|
|
3525
3896
|
}
|
|
3526
3897
|
return lastGood;
|
|
3527
3898
|
})().finally(() => {
|
|
3528
|
-
this.#usageRequestInFlight.
|
|
3899
|
+
if (this.#usageRequestInFlight.get(cacheKey) === promise) {
|
|
3900
|
+
this.#usageRequestInFlight.delete(cacheKey);
|
|
3901
|
+
}
|
|
3529
3902
|
});
|
|
3530
3903
|
|
|
3531
3904
|
this.#usageRequestInFlight.set(cacheKey, promise);
|
|
@@ -3533,6 +3906,7 @@ export class AuthStorage {
|
|
|
3533
3906
|
}
|
|
3534
3907
|
|
|
3535
3908
|
#collectUsageRequests(options?: {
|
|
3909
|
+
provider?: Provider;
|
|
3536
3910
|
baseUrlResolver?: (provider: Provider) => string | undefined;
|
|
3537
3911
|
}): UsageRequestDescriptor[] {
|
|
3538
3912
|
const resolver = this.#usageProviderResolver;
|
|
@@ -3546,6 +3920,7 @@ export class AuthStorage {
|
|
|
3546
3920
|
|
|
3547
3921
|
for (const providerId of providers) {
|
|
3548
3922
|
const provider = providerId as Provider;
|
|
3923
|
+
if (options?.provider && options.provider !== provider) continue;
|
|
3549
3924
|
const providerImpl = resolver(provider);
|
|
3550
3925
|
if (!providerImpl) continue;
|
|
3551
3926
|
const baseUrl = options?.baseUrlResolver?.(provider);
|
|
@@ -3745,6 +4120,7 @@ export class AuthStorage {
|
|
|
3745
4120
|
}
|
|
3746
4121
|
|
|
3747
4122
|
async fetchUsageReports(options?: {
|
|
4123
|
+
provider?: Provider;
|
|
3748
4124
|
baseUrlResolver?: (provider: Provider) => string | undefined;
|
|
3749
4125
|
/** Caller's cancel signal; only rejects this caller, never the shared upstream fetch. */
|
|
3750
4126
|
signal?: AbortSignal;
|
|
@@ -3755,6 +4131,15 @@ export class AuthStorage {
|
|
|
3755
4131
|
// `RemoteAuthCredentialStore` implements the store hook so a gateway
|
|
3756
4132
|
// backed by a broker automatically routes usage to the broker without
|
|
3757
4133
|
// needing the caller to wire it explicitly.
|
|
4134
|
+
const scopedStoreFetch = options?.provider
|
|
4135
|
+
? (this.#fetchUsageReportsForProviderOverride ?? this.#store.fetchUsageReportsForProvider?.bind(this.#store))
|
|
4136
|
+
: undefined;
|
|
4137
|
+
if (scopedStoreFetch && options?.provider) {
|
|
4138
|
+
return raceUsageWithSignal(scopedStoreFetch(options.provider), options.signal);
|
|
4139
|
+
}
|
|
4140
|
+
if (options?.provider && (this.#fetchUsageReportsOverride || this.#store.fetchUsageReports)) {
|
|
4141
|
+
throw new Error("Provider-scoped usage fetch is unavailable");
|
|
4142
|
+
}
|
|
3758
4143
|
const override = this.#fetchUsageReportsOverride ?? this.#store.fetchUsageReports?.bind(this.#store);
|
|
3759
4144
|
if (override) {
|
|
3760
4145
|
// Reuse the in-flight map so concurrent callers (widget poll + format
|
|
@@ -3767,7 +4152,9 @@ export class AuthStorage {
|
|
|
3767
4152
|
// Don't forward the caller signal into the shared fetch — first caller's
|
|
3768
4153
|
// abort would otherwise cancel the upstream for every peer.
|
|
3769
4154
|
shared = override().finally(() => {
|
|
3770
|
-
this.#usageReportsInFlight.
|
|
4155
|
+
if (this.#usageReportsInFlight.get(OVERRIDE_KEY) === shared) {
|
|
4156
|
+
this.#usageReportsInFlight.delete(OVERRIDE_KEY);
|
|
4157
|
+
}
|
|
3771
4158
|
});
|
|
3772
4159
|
this.#usageReportsInFlight.set(OVERRIDE_KEY, shared);
|
|
3773
4160
|
}
|
|
@@ -3836,7 +4223,9 @@ export class AuthStorage {
|
|
|
3836
4223
|
}
|
|
3837
4224
|
return resolved;
|
|
3838
4225
|
})().finally(() => {
|
|
3839
|
-
this.#usageReportsInFlight.
|
|
4226
|
+
if (this.#usageReportsInFlight.get(cacheKey) === promise) {
|
|
4227
|
+
this.#usageReportsInFlight.delete(cacheKey);
|
|
4228
|
+
}
|
|
3840
4229
|
});
|
|
3841
4230
|
|
|
3842
4231
|
this.#usageReportsInFlight.set(cacheKey, promise);
|
|
@@ -4170,9 +4559,11 @@ export class AuthStorage {
|
|
|
4170
4559
|
async markUsageLimitReached(
|
|
4171
4560
|
provider: string,
|
|
4172
4561
|
sessionId: string | undefined,
|
|
4173
|
-
options?: { retryAfterMs?: number; baseUrl?: string; signal?: AbortSignal },
|
|
4562
|
+
options?: { retryAfterMs?: number; baseUrl?: string; signal?: AbortSignal; owner?: object },
|
|
4174
4563
|
): Promise<boolean> {
|
|
4175
4564
|
provider = resolveOAuthStorageProvider(provider);
|
|
4565
|
+
const ownerOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
4566
|
+
if (ownerOverride && !ownerOverride.envSourced) return false;
|
|
4176
4567
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
4177
4568
|
if (!sessionCredential) return false;
|
|
4178
4569
|
|
|
@@ -4272,12 +4663,12 @@ export class AuthStorage {
|
|
|
4272
4663
|
providerKey: string;
|
|
4273
4664
|
provider: string;
|
|
4274
4665
|
order: number[];
|
|
4275
|
-
credentials:
|
|
4666
|
+
credentials: OAuthCredentialSelection[];
|
|
4276
4667
|
options?: AuthApiKeyOptions;
|
|
4277
4668
|
strategy: CredentialRankingStrategy;
|
|
4278
4669
|
}): Promise<
|
|
4279
4670
|
Array<{
|
|
4280
|
-
selection:
|
|
4671
|
+
selection: OAuthCredentialSelection;
|
|
4281
4672
|
usage: UsageReport | null;
|
|
4282
4673
|
usageChecked: boolean;
|
|
4283
4674
|
}>
|
|
@@ -4285,7 +4676,7 @@ export class AuthStorage {
|
|
|
4285
4676
|
const nowMs = Date.now();
|
|
4286
4677
|
const { strategy } = args;
|
|
4287
4678
|
const ranked: Array<{
|
|
4288
|
-
selection:
|
|
4679
|
+
selection: OAuthCredentialSelection;
|
|
4289
4680
|
usage: UsageReport | null;
|
|
4290
4681
|
usageChecked: boolean;
|
|
4291
4682
|
blocked: boolean;
|
|
@@ -4433,18 +4824,29 @@ export class AuthStorage {
|
|
|
4433
4824
|
return undefined;
|
|
4434
4825
|
}
|
|
4435
4826
|
const selectedCredential = this.#resolveSelectedStoredCredential(provider, options, sessionId);
|
|
4436
|
-
const selectedOAuthCredential =
|
|
4827
|
+
const selectedOAuthCredential: OAuthCredentialSelection | undefined =
|
|
4437
4828
|
selectedCredential?.credential.type === "oauth"
|
|
4438
|
-
? {
|
|
4829
|
+
? {
|
|
4830
|
+
id: selectedCredential.id,
|
|
4831
|
+
credential: selectedCredential.credential,
|
|
4832
|
+
index: selectedCredential.index,
|
|
4833
|
+
revision: selectedCredential.revision,
|
|
4834
|
+
}
|
|
4439
4835
|
: undefined;
|
|
4440
4836
|
if (selectedCredential && !selectedOAuthCredential) return undefined;
|
|
4441
4837
|
const credentials = selectedOAuthCredential
|
|
4442
4838
|
? [selectedOAuthCredential]
|
|
4443
|
-
: this.#
|
|
4444
|
-
.map((credential, index) => ({ credential, index }))
|
|
4839
|
+
: this.#getStoredCredentials(provider)
|
|
4445
4840
|
.filter(
|
|
4446
|
-
(entry): entry is { credential: OAuthCredential
|
|
4447
|
-
|
|
4841
|
+
(entry): entry is StoredCredential & { credential: OAuthCredential } =>
|
|
4842
|
+
entry.credential.type === "oauth",
|
|
4843
|
+
)
|
|
4844
|
+
.map((entry, index) => ({
|
|
4845
|
+
id: entry.id,
|
|
4846
|
+
credential: entry.credential,
|
|
4847
|
+
index,
|
|
4848
|
+
revision: entry.revision,
|
|
4849
|
+
}));
|
|
4448
4850
|
|
|
4449
4851
|
if (credentials.length === 0) return undefined;
|
|
4450
4852
|
|
|
@@ -4467,7 +4869,7 @@ export class AuthStorage {
|
|
|
4467
4869
|
? await this.#rankOAuthSelections({ providerKey, provider, order, credentials, options, strategy: strategy! })
|
|
4468
4870
|
: order
|
|
4469
4871
|
.map(idx => credentials[idx])
|
|
4470
|
-
.filter((selection): selection is
|
|
4872
|
+
.filter((selection): selection is OAuthCredentialSelection => Boolean(selection))
|
|
4471
4873
|
.map(selection => ({ selection, usage: null, usageChecked: false }));
|
|
4472
4874
|
|
|
4473
4875
|
// Soft `--prefer-credential` preference: reorder the preferred row to the
|
|
@@ -4477,7 +4879,11 @@ export class AuthStorage {
|
|
|
4477
4879
|
if (!selectedCredential) {
|
|
4478
4880
|
const preferredSelector = this.#getPreferredCredentialSelector(provider, options);
|
|
4479
4881
|
if (preferredSelector) {
|
|
4480
|
-
this.#assertPreferredCredentialSelectorUsable(
|
|
4882
|
+
this.#assertPreferredCredentialSelectorUsable(
|
|
4883
|
+
resolveOAuthStorageProvider(provider),
|
|
4884
|
+
preferredSelector,
|
|
4885
|
+
options?.owner,
|
|
4886
|
+
);
|
|
4481
4887
|
}
|
|
4482
4888
|
const preferredSelection = preferredSelector
|
|
4483
4889
|
? this.#findCredentialBySelector(provider, preferredSelector)
|
|
@@ -4509,14 +4915,15 @@ export class AuthStorage {
|
|
|
4509
4915
|
}
|
|
4510
4916
|
await Promise.all(
|
|
4511
4917
|
candidates.map(async candidate => {
|
|
4918
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, candidate.selection)) return;
|
|
4512
4919
|
if (Date.now() + OAUTH_REFRESH_SKEW_MS < candidate.selection.credential.expires) return;
|
|
4513
|
-
const latestCredential =
|
|
4920
|
+
const latestCredential = candidate.selection.credential;
|
|
4514
4921
|
if (latestCredential?.type === "oauth" && Date.now() + OAUTH_REFRESH_SKEW_MS < latestCredential.expires) {
|
|
4515
4922
|
candidate.selection.credential = latestCredential;
|
|
4516
4923
|
return;
|
|
4517
4924
|
}
|
|
4518
4925
|
try {
|
|
4519
|
-
const credentialId =
|
|
4926
|
+
const credentialId = candidate.selection.id;
|
|
4520
4927
|
const refreshedCredentials = await this.#refreshOAuthCredential(
|
|
4521
4928
|
provider,
|
|
4522
4929
|
candidate.selection.credential,
|
|
@@ -4528,12 +4935,14 @@ export class AuthStorage {
|
|
|
4528
4935
|
...refreshedCredentials,
|
|
4529
4936
|
type: "oauth",
|
|
4530
4937
|
};
|
|
4938
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, candidate.selection)) return;
|
|
4531
4939
|
candidate.selection.credential = updated;
|
|
4532
4940
|
this.#replaceCredentialAt(
|
|
4533
4941
|
provider,
|
|
4534
4942
|
candidate.selection.index,
|
|
4535
4943
|
updated,
|
|
4536
4944
|
!refreshedCredentials.persistedByLease,
|
|
4945
|
+
credentialId,
|
|
4537
4946
|
);
|
|
4538
4947
|
} catch {}
|
|
4539
4948
|
}),
|
|
@@ -4759,6 +5168,15 @@ export class AuthStorage {
|
|
|
4759
5168
|
}
|
|
4760
5169
|
try {
|
|
4761
5170
|
const refreshed = await Promise.race([refreshPromise, cancellation.promise]);
|
|
5171
|
+
let effectiveRefreshed = refreshed;
|
|
5172
|
+
if (this.#refreshOAuthCredentialOverride && this.#store.refreshSnapshot) {
|
|
5173
|
+
await this.#store.refreshSnapshot();
|
|
5174
|
+
const accepted = this.#store.listAuthCredentials(provider).find(row => row.id === credentialId)?.credential;
|
|
5175
|
+
if (accepted?.type !== "oauth") {
|
|
5176
|
+
throw new Error("Credential authority changed during refresh");
|
|
5177
|
+
}
|
|
5178
|
+
effectiveRefreshed = accepted;
|
|
5179
|
+
}
|
|
4762
5180
|
// Return the FULL authority of the effective credential: rotated
|
|
4763
5181
|
// tokens from upstream plus the identity metadata and MCP binding of
|
|
4764
5182
|
// the (possibly guard-adopted) credential that was actually
|
|
@@ -4767,12 +5185,12 @@ export class AuthStorage {
|
|
|
4767
5185
|
// next refresh token to the wrong endpoint — or relabel rotated
|
|
4768
5186
|
// tokens with stale identity.
|
|
4769
5187
|
const authority: RefreshedOAuthCredentials = {
|
|
4770
|
-
...
|
|
4771
|
-
accountId:
|
|
4772
|
-
email:
|
|
4773
|
-
projectId:
|
|
4774
|
-
enterpriseUrl:
|
|
4775
|
-
mcpBinding: (
|
|
5188
|
+
...effectiveRefreshed,
|
|
5189
|
+
accountId: effectiveRefreshed.accountId ?? credential.accountId,
|
|
5190
|
+
email: effectiveRefreshed.email ?? credential.email,
|
|
5191
|
+
projectId: effectiveRefreshed.projectId ?? credential.projectId,
|
|
5192
|
+
enterpriseUrl: effectiveRefreshed.enterpriseUrl ?? credential.enterpriseUrl,
|
|
5193
|
+
mcpBinding: (effectiveRefreshed as RefreshedOAuthCredentials).mcpBinding ?? credential.mcpBinding,
|
|
4776
5194
|
};
|
|
4777
5195
|
if (refreshLease) {
|
|
4778
5196
|
const completeLease = this.#store.completeOAuthRefreshLease?.bind(this.#store);
|
|
@@ -4801,15 +5219,28 @@ export class AuthStorage {
|
|
|
4801
5219
|
}
|
|
4802
5220
|
}
|
|
4803
5221
|
|
|
5222
|
+
#reconcileOAuthCredentialSelection(provider: string, selection: OAuthCredentialSelection): boolean {
|
|
5223
|
+
const entries = this.#getStoredCredentials(provider);
|
|
5224
|
+
const index = entries.findIndex(entry => entry.id === selection.id);
|
|
5225
|
+
if (index === -1) return false;
|
|
5226
|
+
const current = entries[index];
|
|
5227
|
+
if (current?.credential.type !== "oauth") return false;
|
|
5228
|
+
selection.index = index;
|
|
5229
|
+
selection.credential = current.credential;
|
|
5230
|
+
selection.revision = current.revision;
|
|
5231
|
+
return true;
|
|
5232
|
+
}
|
|
5233
|
+
|
|
4804
5234
|
async #prepareOAuthCredentialForRequest(
|
|
4805
5235
|
provider: string,
|
|
4806
|
-
selection:
|
|
5236
|
+
selection: OAuthCredentialSelection,
|
|
4807
5237
|
options: AuthApiKeyOptions | undefined,
|
|
4808
5238
|
): Promise<boolean> {
|
|
5239
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return false;
|
|
4809
5240
|
const prepare = this.#store.prepareForRequest?.bind(this.#store);
|
|
4810
5241
|
if (!prepare) return true;
|
|
4811
5242
|
const stored = this.#getStoredCredentials(provider);
|
|
4812
|
-
const selected = stored
|
|
5243
|
+
const selected = stored.find(entry => entry.id === selection.id);
|
|
4813
5244
|
if (selected?.credential.type !== "oauth") return false;
|
|
4814
5245
|
|
|
4815
5246
|
const prepared = await prepare(selected.id, { signal: options?.signal });
|
|
@@ -4817,21 +5248,22 @@ export class AuthStorage {
|
|
|
4817
5248
|
const latestRows = this.#store.listAuthCredentials(provider);
|
|
4818
5249
|
this.#setStoredCredentials(
|
|
4819
5250
|
provider,
|
|
4820
|
-
latestRows.map(row => ({ id: row.id, credential: row.credential })),
|
|
5251
|
+
latestRows.map(row => ({ id: row.id, credential: row.credential, revision: row.revision })),
|
|
4821
5252
|
);
|
|
4822
|
-
const latestIndex = latestRows.findIndex(row => row.id ===
|
|
5253
|
+
const latestIndex = latestRows.findIndex(row => row.id === selection.id);
|
|
4823
5254
|
if (latestIndex === -1) return false;
|
|
4824
5255
|
const latest = latestRows[latestIndex];
|
|
4825
5256
|
if (latest?.credential.type !== "oauth") return false;
|
|
4826
5257
|
selection.index = latestIndex;
|
|
4827
5258
|
selection.credential = latest.credential;
|
|
5259
|
+
selection.revision = latest.revision;
|
|
4828
5260
|
return true;
|
|
4829
5261
|
}
|
|
4830
5262
|
|
|
4831
5263
|
/** Attempts to use a single OAuth credential, checking usage and refreshing token. */
|
|
4832
5264
|
async #tryOAuthCredential(
|
|
4833
5265
|
provider: Provider,
|
|
4834
|
-
selection:
|
|
5266
|
+
selection: OAuthCredentialSelection,
|
|
4835
5267
|
providerKey: string,
|
|
4836
5268
|
sessionId: string | undefined,
|
|
4837
5269
|
options: AuthApiKeyOptions | undefined,
|
|
@@ -4851,6 +5283,7 @@ export class AuthStorage {
|
|
|
4851
5283
|
usagePrechecked = false,
|
|
4852
5284
|
enforceProRequirement,
|
|
4853
5285
|
} = usageOptions;
|
|
5286
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return undefined;
|
|
4854
5287
|
if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index)) {
|
|
4855
5288
|
return undefined;
|
|
4856
5289
|
}
|
|
@@ -4890,6 +5323,8 @@ export class AuthStorage {
|
|
|
4890
5323
|
}
|
|
4891
5324
|
|
|
4892
5325
|
try {
|
|
5326
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return undefined;
|
|
5327
|
+
const selectionCredentialId = selection.id;
|
|
4893
5328
|
let result: { newCredentials: OAuthCredentials; apiKey: string } | null;
|
|
4894
5329
|
// The refresh result carries the effective (possibly guard-adopted)
|
|
4895
5330
|
// credential's binding; `updated` must persist it or the next refresh
|
|
@@ -4900,7 +5335,7 @@ export class AuthStorage {
|
|
|
4900
5335
|
const refreshedCredentials = await this.#refreshOAuthCredential(
|
|
4901
5336
|
provider,
|
|
4902
5337
|
selection.credential,
|
|
4903
|
-
|
|
5338
|
+
selectionCredentialId,
|
|
4904
5339
|
options?.signal,
|
|
4905
5340
|
);
|
|
4906
5341
|
refreshedAuthority = refreshedCredentials;
|
|
@@ -4917,7 +5352,7 @@ export class AuthStorage {
|
|
|
4917
5352
|
const refreshedCredentials = await this.#refreshOAuthCredential(
|
|
4918
5353
|
provider,
|
|
4919
5354
|
selection.credential,
|
|
4920
|
-
|
|
5355
|
+
selectionCredentialId,
|
|
4921
5356
|
options?.signal,
|
|
4922
5357
|
);
|
|
4923
5358
|
refreshedAuthority = refreshedCredentials;
|
|
@@ -4938,7 +5373,13 @@ export class AuthStorage {
|
|
|
4938
5373
|
enterpriseUrl: result.newCredentials.enterpriseUrl ?? selection.credential.enterpriseUrl,
|
|
4939
5374
|
mcpBinding: refreshedAuthority.mcpBinding,
|
|
4940
5375
|
};
|
|
4941
|
-
this.#replaceCredentialAt(
|
|
5376
|
+
this.#replaceCredentialAt(
|
|
5377
|
+
provider,
|
|
5378
|
+
selection.index,
|
|
5379
|
+
updated,
|
|
5380
|
+
!refreshedAuthority.persistedByLease,
|
|
5381
|
+
selectionCredentialId,
|
|
5382
|
+
);
|
|
4942
5383
|
|
|
4943
5384
|
if ((checkUsage && !allowBlocked) || requiresProModel) {
|
|
4944
5385
|
const sameAccount = selection.credential.accountId === updated.accountId;
|
|
@@ -4962,10 +5403,17 @@ export class AuthStorage {
|
|
|
4962
5403
|
return undefined;
|
|
4963
5404
|
}
|
|
4964
5405
|
}
|
|
5406
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return undefined;
|
|
5407
|
+
if (!authCredentialEquals(selection.credential, updated)) return undefined;
|
|
4965
5408
|
this.#recordSessionCredential(provider, sessionId, "oauth", selection.index);
|
|
4966
5409
|
return { apiKey: result.apiKey, credential: updated };
|
|
4967
5410
|
} catch (error) {
|
|
4968
|
-
|
|
5411
|
+
// Auth-broker errors retain the sanitized upstream body separately from
|
|
5412
|
+
// their transport message. Include that body for failure classification
|
|
5413
|
+
// (the broker's 500 envelope otherwise hides `invalid_grant`) while
|
|
5414
|
+
// preserving the original error object for callers and diagnostics.
|
|
5415
|
+
const brokerBody = readBrokerErrorBody(error);
|
|
5416
|
+
const errorMsg = [String(error), brokerBody].filter((part): part is string => Boolean(part)).join(" ");
|
|
4969
5417
|
// Peer-rotation recovery runs before ANY failure classification: a
|
|
4970
5418
|
// concurrent process may have rotated the refresh token, which
|
|
4971
5419
|
// invalidates the snapshot token we just attempted. Re-read the row —
|
|
@@ -4984,7 +5432,7 @@ export class AuthStorage {
|
|
|
4984
5432
|
// selection snapshot would misread that adoption as a fresh peer
|
|
4985
5433
|
// rotation and loop reload-retry instead of classifying the failure.
|
|
4986
5434
|
const attemptedRefreshToken = getAttemptedRefreshToken(error) ?? selection.credential.refresh;
|
|
4987
|
-
const attemptedCredentialId =
|
|
5435
|
+
const attemptedCredentialId = selection.id;
|
|
4988
5436
|
if (attemptedCredentialId !== undefined) {
|
|
4989
5437
|
const latestRow = this.#store.listAuthCredentials(provider).find(row => row.id === attemptedCredentialId);
|
|
4990
5438
|
const latestCredential = latestRow?.credential;
|
|
@@ -5040,6 +5488,7 @@ export class AuthStorage {
|
|
|
5040
5488
|
selection.index,
|
|
5041
5489
|
selection.credential,
|
|
5042
5490
|
`oauth refresh failed: ${errorMsg}`,
|
|
5491
|
+
attemptedCredentialId,
|
|
5043
5492
|
);
|
|
5044
5493
|
if (!disabled) {
|
|
5045
5494
|
// The CAS predicate compares the row's serialized `data`, so it also
|
|
@@ -5060,7 +5509,35 @@ export class AuthStorage {
|
|
|
5060
5509
|
index: selection.index,
|
|
5061
5510
|
credentialId: attemptedCredentialId,
|
|
5062
5511
|
});
|
|
5063
|
-
|
|
5512
|
+
const disabledCause = `oauth refresh failed: ${errorMsg}`;
|
|
5513
|
+
if (this.#store.disableAuthCredentialRemote) {
|
|
5514
|
+
try {
|
|
5515
|
+
const disabled = await this.#disableCredentialRemotely(
|
|
5516
|
+
provider,
|
|
5517
|
+
attemptedCredentialId,
|
|
5518
|
+
disabledCause,
|
|
5519
|
+
options?.signal,
|
|
5520
|
+
selection.revision,
|
|
5521
|
+
);
|
|
5522
|
+
if (!disabled) {
|
|
5523
|
+
await this.reload();
|
|
5524
|
+
return this.#resolveOAuthSelection(provider, sessionId, options, reloadsUsed + 1);
|
|
5525
|
+
}
|
|
5526
|
+
} catch (disableError) {
|
|
5527
|
+
// A failed broker mutation must not replace the provider's
|
|
5528
|
+
// original authentication failure. We also deliberately do
|
|
5529
|
+
// not remove the row locally: remote authority may still be
|
|
5530
|
+
// active and can only be changed by its broker.
|
|
5531
|
+
logger.warn("OAuth refresh remote disable failed", {
|
|
5532
|
+
provider,
|
|
5533
|
+
credentialId: attemptedCredentialId,
|
|
5534
|
+
error: String(disableError),
|
|
5535
|
+
});
|
|
5536
|
+
throw error;
|
|
5537
|
+
}
|
|
5538
|
+
} else {
|
|
5539
|
+
this.#disableCredentialById(provider, attemptedCredentialId, disabledCause);
|
|
5540
|
+
}
|
|
5064
5541
|
} else {
|
|
5065
5542
|
logger.debug("OAuth refresh disable lost CAS; reloading after peer rotation", {
|
|
5066
5543
|
provider,
|
|
@@ -5159,15 +5636,27 @@ export class AuthStorage {
|
|
|
5159
5636
|
* and get a best-effort token. For GitHub Copilot we preserve enterprise
|
|
5160
5637
|
* routing metadata so discovery can hit the correct host.
|
|
5161
5638
|
*/
|
|
5162
|
-
async peekApiKey(provider: string): Promise<string | undefined> {
|
|
5639
|
+
async peekApiKey(provider: string, options?: Pick<AuthApiKeyOptions, "owner">): Promise<string | undefined> {
|
|
5163
5640
|
provider = resolveOAuthStorageProvider(provider);
|
|
5164
5641
|
const runtimeKey = this.#runtimeOverrides.get(provider);
|
|
5165
5642
|
if (runtimeKey) return runtimeKey;
|
|
5166
5643
|
|
|
5167
|
-
const
|
|
5168
|
-
|
|
5644
|
+
const configOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
5645
|
+
const configKey = configOverride?.apiKey;
|
|
5646
|
+
if (configKey && !configOverride?.envSourced) return configKey;
|
|
5169
5647
|
|
|
5170
|
-
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
5648
|
+
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
5649
|
+
provider,
|
|
5650
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
5651
|
+
undefined,
|
|
5652
|
+
);
|
|
5653
|
+
if (configKey) {
|
|
5654
|
+
// Env-sourced (`apiKeyEnv`) override: same precedence as getApiKey —
|
|
5655
|
+
// a stored api_key credential from `auth login` wins, stored OAuth
|
|
5656
|
+
// still yields to the indirection.
|
|
5657
|
+
const storedApiKey = await this.#resolveStoredApiKeyOverEnvConfig(provider, selectedCredential, undefined);
|
|
5658
|
+
return storedApiKey ?? configKey;
|
|
5659
|
+
}
|
|
5171
5660
|
if (selectedCredential?.credential.type === "api_key") {
|
|
5172
5661
|
return this.#resolveStoredApiKey(provider, selectedCredential.credential.key);
|
|
5173
5662
|
}
|
|
@@ -5210,20 +5699,23 @@ export class AuthStorage {
|
|
|
5210
5699
|
}
|
|
5211
5700
|
}
|
|
5212
5701
|
|
|
5213
|
-
return getEnvApiKey(provider) || this.#
|
|
5702
|
+
return getEnvApiKey(provider) || this.#resolveFallback(provider, options?.owner);
|
|
5214
5703
|
}
|
|
5215
5704
|
|
|
5216
5705
|
/**
|
|
5217
5706
|
* Get API key for a provider.
|
|
5218
5707
|
* Priority:
|
|
5219
5708
|
* 1. Runtime override (CLI --api-key)
|
|
5220
|
-
* 2. Config override (models.yml `providers.<name>.apiKey`)
|
|
5221
|
-
* 3.
|
|
5222
|
-
*
|
|
5223
|
-
*
|
|
5224
|
-
*
|
|
5225
|
-
*
|
|
5226
|
-
*
|
|
5709
|
+
* 2. Config override (models.yml `providers.<name>.apiKey` literal pin)
|
|
5710
|
+
* 3. Stored api_key credential from `auth login`, when the config override
|
|
5711
|
+
* is only an `apiKeyEnv` indirection
|
|
5712
|
+
* 4. Config override sourced from models.yml `providers.<name>.apiKeyEnv`
|
|
5713
|
+
* 5. Session-selected OAuth credential, when present
|
|
5714
|
+
* 6. Usable or unresolved API key from storage
|
|
5715
|
+
* 7. OAuth token from storage (auto-refreshed)
|
|
5716
|
+
* 8. Previously unusable command-backed API key retry
|
|
5717
|
+
* 9. Environment variable
|
|
5718
|
+
* 10. Fallback resolver (models.yml custom providers, last-resort)
|
|
5227
5719
|
*/
|
|
5228
5720
|
async getApiKey(provider: string, sessionId?: string, options?: AuthApiKeyOptions): Promise<string | undefined> {
|
|
5229
5721
|
provider = resolveOAuthStorageProvider(provider);
|
|
@@ -5238,8 +5730,19 @@ export class AuthStorage {
|
|
|
5238
5730
|
// (e.g. an auth-gateway) and supplied the bearer for that endpoint —
|
|
5239
5731
|
// honor it instead of forwarding an upstream OAuth token that the proxy
|
|
5240
5732
|
// won't accept.
|
|
5241
|
-
const
|
|
5242
|
-
|
|
5733
|
+
const configOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
5734
|
+
const configKey = configOverride?.apiKey;
|
|
5735
|
+
if (configKey) {
|
|
5736
|
+
if (!configOverride?.envSourced) return configKey;
|
|
5737
|
+
// The override is an `apiKeyEnv` indirection, not a pinned value. A
|
|
5738
|
+
// stored api_key credential from `auth login` is actively managed
|
|
5739
|
+
// (validated at login, rotated on 401), while the pointed-to env
|
|
5740
|
+
// value can go stale with no recovery path — prefer the stored
|
|
5741
|
+
// credential. Stored OAuth credentials still yield to the override.
|
|
5742
|
+
const storedApiKey = await this.#resolveStoredApiKeyOverEnvConfig(provider, selectedCredential, sessionId);
|
|
5743
|
+
if (storedApiKey) return storedApiKey;
|
|
5744
|
+
return configKey;
|
|
5745
|
+
}
|
|
5243
5746
|
|
|
5244
5747
|
if (selectedCredential?.credential.type === "api_key") {
|
|
5245
5748
|
this.#recordSessionCredential(provider, sessionId, "api_key", selectedCredential.index);
|
|
@@ -5292,7 +5795,39 @@ export class AuthStorage {
|
|
|
5292
5795
|
if (sessionId) this.#sessionLastCredential.get(provider)?.delete(sessionId);
|
|
5293
5796
|
const envKey = getEnvApiKey(provider);
|
|
5294
5797
|
if (envKey) return envKey;
|
|
5295
|
-
return this.#
|
|
5798
|
+
return this.#resolveFallback(provider, options?.owner) ?? undefined;
|
|
5799
|
+
}
|
|
5800
|
+
|
|
5801
|
+
/**
|
|
5802
|
+
* Resolve a stored api_key credential that outranks an env-sourced config
|
|
5803
|
+
* override (`apiKeyEnv`). Mirrors the api_key branches of {@link getApiKey}:
|
|
5804
|
+
* the selector-pinned credential first, then the round-robin/session pool.
|
|
5805
|
+
* Returns undefined when no stored api_key credential resolves, leaving the
|
|
5806
|
+
* env-sourced override in effect.
|
|
5807
|
+
*/
|
|
5808
|
+
async #resolveStoredApiKeyOverEnvConfig(
|
|
5809
|
+
provider: string,
|
|
5810
|
+
selectedCredential: ({ index: number } & StoredCredential) | undefined,
|
|
5811
|
+
sessionId?: string,
|
|
5812
|
+
): Promise<string | undefined> {
|
|
5813
|
+
if (selectedCredential?.credential.type === "api_key") {
|
|
5814
|
+
const resolved = await this.#resolveStoredApiKey(provider, selectedCredential.credential.key);
|
|
5815
|
+
if (resolved) {
|
|
5816
|
+
this.#recordSessionCredential(provider, sessionId, "api_key", selectedCredential.index);
|
|
5817
|
+
return resolved;
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
const attemptedApiKeyIndices = new Set<number>();
|
|
5821
|
+
for (;;) {
|
|
5822
|
+
const apiKeySelection = this.#selectApiKeyCredential(provider, sessionId, attemptedApiKeyIndices);
|
|
5823
|
+
if (!apiKeySelection) return undefined;
|
|
5824
|
+
attemptedApiKeyIndices.add(apiKeySelection.index);
|
|
5825
|
+
const resolved = await this.#resolveStoredApiKey(provider, apiKeySelection.credential.key);
|
|
5826
|
+
if (resolved) {
|
|
5827
|
+
this.#recordSessionCredential(provider, sessionId, "api_key", apiKeySelection.index);
|
|
5828
|
+
return resolved;
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5296
5831
|
}
|
|
5297
5832
|
|
|
5298
5833
|
/**
|
|
@@ -5319,7 +5854,7 @@ export class AuthStorage {
|
|
|
5319
5854
|
// Runtime / config overrides intentionally short-circuit OAuth: when the
|
|
5320
5855
|
// user has pinned an API key, they expect the OAuth identity to be
|
|
5321
5856
|
// suppressed (same contract as `getOAuthAccountId`).
|
|
5322
|
-
if (this.#runtimeOverrides.has(provider) || this.#
|
|
5857
|
+
if (this.#runtimeOverrides.has(provider) || this.#hasConfigOverride(provider, options?.owner)) {
|
|
5323
5858
|
return undefined;
|
|
5324
5859
|
}
|
|
5325
5860
|
const resolved = await this.#resolveOAuthSelection(provider, sessionId, options);
|
|
@@ -5335,13 +5870,7 @@ export class AuthStorage {
|
|
|
5335
5870
|
}
|
|
5336
5871
|
|
|
5337
5872
|
#extractStructuredApiKeyToken(apiKey: string): string | undefined {
|
|
5338
|
-
|
|
5339
|
-
try {
|
|
5340
|
-
const parsed = JSON.parse(apiKey) as { token?: unknown };
|
|
5341
|
-
return typeof parsed.token === "string" ? parsed.token : undefined;
|
|
5342
|
-
} catch {
|
|
5343
|
-
return undefined;
|
|
5344
|
-
}
|
|
5873
|
+
return extractStructuredApiKeyToken(apiKey);
|
|
5345
5874
|
}
|
|
5346
5875
|
|
|
5347
5876
|
async #credentialMatchesApiKey(provider: string, credential: AuthCredential, apiKey: string): Promise<boolean> {
|
|
@@ -5371,6 +5900,9 @@ export class AuthStorage {
|
|
|
5371
5900
|
const signal = isAbortSignalOption(optionsOrSignal) ? optionsOrSignal : optionsOrSignal?.signal;
|
|
5372
5901
|
const sessionId = isAbortSignalOption(optionsOrSignal) ? undefined : optionsOrSignal?.sessionId;
|
|
5373
5902
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
5903
|
+
const owner = isAbortSignalOption(optionsOrSignal) ? undefined : optionsOrSignal?.owner;
|
|
5904
|
+
const ownerOverride = this.#configOverrideRegistration(storageProvider, owner);
|
|
5905
|
+
if (ownerOverride && !ownerOverride.envSourced && ownerOverride.apiKey === apiKey) return false;
|
|
5374
5906
|
const stored = this.#getStoredCredentials(storageProvider);
|
|
5375
5907
|
let matched: { id: number; type: AuthCredential["type"]; index: number } | undefined;
|
|
5376
5908
|
for (let index = 0; index < stored.length; index++) {
|
|
@@ -5403,7 +5935,7 @@ export class AuthStorage {
|
|
|
5403
5935
|
const latestRows = this.#store.listAuthCredentials(storageProvider);
|
|
5404
5936
|
this.#setStoredCredentials(
|
|
5405
5937
|
storageProvider,
|
|
5406
|
-
latestRows.map(row => ({ id: row.id, credential: row.credential })),
|
|
5938
|
+
latestRows.map(row => ({ id: row.id, credential: row.credential, revision: row.revision })),
|
|
5407
5939
|
);
|
|
5408
5940
|
return true;
|
|
5409
5941
|
}
|
|
@@ -5430,6 +5962,7 @@ export class AuthStorage {
|
|
|
5430
5962
|
provider,
|
|
5431
5963
|
credential: redacted,
|
|
5432
5964
|
identityKey: resolveCredentialIdentityKey(provider, credential),
|
|
5965
|
+
...(entry.revision === undefined ? {} : { revision: entry.revision }),
|
|
5433
5966
|
});
|
|
5434
5967
|
}
|
|
5435
5968
|
}
|
|
@@ -5565,12 +6098,14 @@ export class AuthStorage {
|
|
|
5565
6098
|
enterpriseUrl: refreshed.enterpriseUrl ?? target.credential.enterpriseUrl,
|
|
5566
6099
|
mcpBinding: refreshed.mcpBinding,
|
|
5567
6100
|
};
|
|
5568
|
-
this.#replaceCredentialAt(provider, index, updated, !refreshed.persistedByLease);
|
|
6101
|
+
this.#replaceCredentialAt(provider, index, updated, !refreshed.persistedByLease, id);
|
|
6102
|
+
const persisted = this.#store.listAuthCredentials(provider).find(entry => entry.id === id);
|
|
5569
6103
|
return {
|
|
5570
6104
|
id,
|
|
5571
6105
|
provider,
|
|
5572
6106
|
credential: { ...updated, refresh: REMOTE_REFRESH_SENTINEL },
|
|
5573
6107
|
identityKey: resolveCredentialIdentityKey(provider, updated),
|
|
6108
|
+
...(persisted?.revision === undefined ? {} : { revision: persisted.revision }),
|
|
5574
6109
|
};
|
|
5575
6110
|
}
|
|
5576
6111
|
throw new Error(`No credential with id=${id}`);
|
|
@@ -5610,7 +6145,7 @@ export class AuthStorage {
|
|
|
5610
6145
|
const stored = this.#store.upsertAuthCredentialForProvider(provider, credential);
|
|
5611
6146
|
this.#setStoredCredentials(
|
|
5612
6147
|
provider,
|
|
5613
|
-
stored.map(entry => ({ id: entry.id, credential: entry.credential })),
|
|
6148
|
+
stored.map(entry => ({ id: entry.id, credential: entry.credential, revision: entry.revision })),
|
|
5614
6149
|
);
|
|
5615
6150
|
this.#resetProviderAssignments(provider);
|
|
5616
6151
|
return this.#toSnapshotEntries(provider, stored);
|
|
@@ -5621,26 +6156,39 @@ export class AuthStorage {
|
|
|
5621
6156
|
*
|
|
5622
6157
|
* Surfaces four layers, highest precedence first:
|
|
5623
6158
|
* 1. Runtime override (`--api-key`).
|
|
5624
|
-
* 2. Config override (`models.yml` `providers.<name>.apiKey`
|
|
6159
|
+
* 2. Config override (`models.yml` `providers.<name>.apiKey` literal pin,
|
|
6160
|
+
* or an `apiKeyEnv` indirection when no stored api_key credential
|
|
6161
|
+
* outranks it).
|
|
5625
6162
|
* 3. Stored credential (the one this session is currently sticky to, or the
|
|
5626
6163
|
* one round-robin would pick next when no session id is supplied).
|
|
5627
6164
|
* 4. Env var / fallback resolver — when no stored credential exists.
|
|
5628
6165
|
*
|
|
5629
6166
|
* The string is purely informational; consumers must not parse it.
|
|
5630
6167
|
*/
|
|
5631
|
-
describeCredentialSource(
|
|
6168
|
+
describeCredentialSource(
|
|
6169
|
+
provider: string,
|
|
6170
|
+
sessionId?: string,
|
|
6171
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
6172
|
+
): string | undefined {
|
|
6173
|
+
provider = resolveOAuthStorageProvider(provider);
|
|
5632
6174
|
if (this.#runtimeOverrides.has(provider)) {
|
|
5633
6175
|
return "runtime override (--api-key)";
|
|
5634
6176
|
}
|
|
5635
|
-
|
|
5636
|
-
|
|
6177
|
+
const configOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
6178
|
+
if (configOverride) {
|
|
6179
|
+
// An `apiKeyEnv` indirection loses to a stored api_key credential
|
|
6180
|
+
// (see getApiKey); describe the credential that actually wins.
|
|
6181
|
+
const shadowed = this.#getStoredCredentials(provider).some(entry => entry.credential.type === "api_key");
|
|
6182
|
+
if (!configOverride.envSourced || !shadowed) {
|
|
6183
|
+
return "config override (models.yml)";
|
|
6184
|
+
}
|
|
5637
6185
|
}
|
|
5638
6186
|
|
|
5639
6187
|
const baseLabel = this.#sourceLabel ?? "local store";
|
|
5640
6188
|
const stored = this.#getStoredCredentials(provider);
|
|
5641
6189
|
if (stored.length === 0) {
|
|
5642
6190
|
if (getEnvApiKey(provider)) return `env ${baseLabel ? `(fallback over ${baseLabel})` : ""}`.trim();
|
|
5643
|
-
if (this.#
|
|
6191
|
+
if (this.#resolveFallback(provider, options?.owner) !== undefined) return `fallback resolver`;
|
|
5644
6192
|
return undefined;
|
|
5645
6193
|
}
|
|
5646
6194
|
|
|
@@ -5874,6 +6422,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5874
6422
|
#updateStmt: Statement;
|
|
5875
6423
|
#deleteStmt: Statement;
|
|
5876
6424
|
#deleteIfMatchesStmt: Statement;
|
|
6425
|
+
#deleteIfRevisionStmt: Statement;
|
|
5877
6426
|
#deleteByProviderStmt: Statement;
|
|
5878
6427
|
#hardDeleteStmt: Statement;
|
|
5879
6428
|
#getCacheStmt: Statement;
|
|
@@ -5914,6 +6463,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5914
6463
|
this.#deleteIfMatchesStmt = this.#db.prepare(
|
|
5915
6464
|
`UPDATE auth_credentials SET disabled_cause = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ? AND data = ? AND disabled_cause IS NULL`,
|
|
5916
6465
|
);
|
|
6466
|
+
this.#deleteIfRevisionStmt = this.#db.prepare(
|
|
6467
|
+
`UPDATE auth_credentials SET disabled_cause = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ? AND revision = ? AND disabled_cause IS NULL`,
|
|
6468
|
+
);
|
|
5917
6469
|
this.#deleteByProviderStmt = this.#db.prepare(
|
|
5918
6470
|
`UPDATE auth_credentials SET disabled_cause = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE provider = ? AND disabled_cause IS NULL`,
|
|
5919
6471
|
);
|
|
@@ -6350,6 +6902,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6350
6902
|
id: row.id,
|
|
6351
6903
|
credential: deserializeCredential(row),
|
|
6352
6904
|
identityKey: resolveRowCredentialIdentityKey(providerName, row),
|
|
6905
|
+
revision: row.revision,
|
|
6353
6906
|
}));
|
|
6354
6907
|
|
|
6355
6908
|
const result: StoredAuthCredential[] = [];
|
|
@@ -6366,7 +6919,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6366
6919
|
if (match) {
|
|
6367
6920
|
matchedExistingIds.add(match.id);
|
|
6368
6921
|
this.#updateStmt.run(serialized.credentialType, serialized.data, serialized.identityKey, match.id);
|
|
6369
|
-
result.push({
|
|
6922
|
+
result.push({
|
|
6923
|
+
id: match.id,
|
|
6924
|
+
provider: providerName,
|
|
6925
|
+
credential,
|
|
6926
|
+
disabledCause: null,
|
|
6927
|
+
revision: match.revision + 1,
|
|
6928
|
+
});
|
|
6370
6929
|
} else {
|
|
6371
6930
|
const row = this.#insertStmt.get(
|
|
6372
6931
|
providerName,
|
|
@@ -6375,7 +6934,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6375
6934
|
serialized.identityKey,
|
|
6376
6935
|
) as { id?: number } | undefined;
|
|
6377
6936
|
if (row?.id) {
|
|
6378
|
-
result.push({ id: row.id, provider: providerName, credential, disabledCause: null });
|
|
6937
|
+
result.push({ id: row.id, provider: providerName, credential, disabledCause: null, revision: 1 });
|
|
6379
6938
|
}
|
|
6380
6939
|
}
|
|
6381
6940
|
}
|
|
@@ -6468,6 +7027,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6468
7027
|
id: number;
|
|
6469
7028
|
credential: AuthCredential;
|
|
6470
7029
|
identityKey: string | null;
|
|
7030
|
+
revision: number;
|
|
6471
7031
|
}> = [];
|
|
6472
7032
|
for (const row of existingRows) {
|
|
6473
7033
|
const activeCredential = deserializeCredential(row);
|
|
@@ -6476,6 +7036,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6476
7036
|
id: row.id,
|
|
6477
7037
|
credential: activeCredential,
|
|
6478
7038
|
identityKey: resolveRowCredentialIdentityKey(providerName, row),
|
|
7039
|
+
revision: row.revision,
|
|
6479
7040
|
});
|
|
6480
7041
|
}
|
|
6481
7042
|
if (existing.length > 0) {
|
|
@@ -6508,6 +7069,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6508
7069
|
provider: providerName,
|
|
6509
7070
|
credential: row.credential,
|
|
6510
7071
|
disabledCause: null,
|
|
7072
|
+
revision: row.revision,
|
|
6511
7073
|
})),
|
|
6512
7074
|
};
|
|
6513
7075
|
}
|
|
@@ -6595,6 +7157,17 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6595
7157
|
}
|
|
6596
7158
|
}
|
|
6597
7159
|
|
|
7160
|
+
tryDisableAuthCredentialIfRevision(id: number, expectedRevision: number, disabledCause: string): boolean {
|
|
7161
|
+
try {
|
|
7162
|
+
const result = this.#deleteIfRevisionStmt.run(normalizeDisabledCause(disabledCause), id, expectedRevision) as {
|
|
7163
|
+
changes: number;
|
|
7164
|
+
};
|
|
7165
|
+
return result.changes === 1;
|
|
7166
|
+
} catch {
|
|
7167
|
+
return false;
|
|
7168
|
+
}
|
|
7169
|
+
}
|
|
7170
|
+
|
|
6598
7171
|
deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void {
|
|
6599
7172
|
try {
|
|
6600
7173
|
this.#deleteByProviderStmt.run(normalizeDisabledCause(disabledCause), provider);
|
|
@@ -6621,6 +7194,17 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6621
7194
|
}
|
|
6622
7195
|
}
|
|
6623
7196
|
|
|
7197
|
+
allocateMonotonicSequence(key: string, expiresAtSec: number): number {
|
|
7198
|
+
const allocate = this.#db.transaction(() => {
|
|
7199
|
+
const row = this.#getCacheIncludingExpiredStmt.get(key) as { value?: string } | undefined;
|
|
7200
|
+
const current = Number(row?.value);
|
|
7201
|
+
const next = Number.isSafeInteger(current) && current >= 0 ? current + 1 : 1;
|
|
7202
|
+
this.#upsertCacheStmt.run(key, String(next), expiresAtSec);
|
|
7203
|
+
return next;
|
|
7204
|
+
});
|
|
7205
|
+
return allocate();
|
|
7206
|
+
}
|
|
7207
|
+
|
|
6624
7208
|
deleteCachePrefix(prefix: string): void {
|
|
6625
7209
|
if (prefix.length === 0) return;
|
|
6626
7210
|
try {
|
|
@@ -6715,6 +7299,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6715
7299
|
this.#updateStmt.finalize();
|
|
6716
7300
|
this.#deleteStmt.finalize();
|
|
6717
7301
|
this.#deleteIfMatchesStmt.finalize();
|
|
7302
|
+
this.#deleteIfRevisionStmt.finalize();
|
|
6718
7303
|
this.#deleteByProviderStmt.finalize();
|
|
6719
7304
|
this.#hardDeleteStmt.finalize();
|
|
6720
7305
|
this.#getCacheStmt.finalize();
|