@gajae-code/ai 0.15.5 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/dist/types/adapter-internals/aws-region.d.ts +7 -0
- 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 +92 -24
- package/dist/types/core.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/provider-models/openai-compat.d.ts +1 -0
- package/dist/types/providers/anthropic.d.ts +1 -1
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +6 -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 +22 -2
- 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/schema/normalize.d.ts +0 -5
- package/dist/types/utils/sqlite-errors.d.ts +4 -0
- package/package.json +3 -3
- package/src/adapter-internals/aws-region.d.ts +7 -0
- package/src/adapter-internals/aws-region.ts +14 -0
- 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 +672 -168
- package/src/cli.ts +1 -0
- package/src/core.ts +1 -0
- package/src/index.ts +1 -0
- package/src/model-thinking.ts +8 -0
- package/src/models.json +1224 -3
- package/src/provider-models/descriptors.ts +3 -1
- package/src/provider-models/openai-compat.ts +102 -1
- package/src/providers/amazon-bedrock.ts +5 -1
- package/src/providers/anthropic.d.ts +1 -1
- package/src/providers/anthropic.ts +8 -2
- package/src/providers/aws-credentials.ts +6 -0
- 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-gemini-headers.d.ts +1 -1
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/google-shared.ts +3 -0
- package/src/providers/kiro-api-key.ts +33 -8
- package/src/providers/kiro-codewhisperer.ts +28 -9
- package/src/providers/ollama.ts +3 -0
- package/src/providers/openai-codex-responses.d.ts +6 -0
- package/src/providers/openai-codex-responses.ts +37 -8
- package/src/providers/openai-completions.ts +11 -1
- package/src/providers/openai-responses.ts +10 -1
- package/src/providers/pi-native-client.ts +25 -1
- 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 +175 -67
- package/src/types.d.ts +22 -2
- package/src/types.ts +27 -1
- 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/kiro.ts +91 -22
- 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/schema/dereference.ts +169 -49
- package/src/utils/schema/draft.ts +46 -23
- package/src/utils/schema/normalize.d.ts +0 -5
- package/src/utils/schema/normalize.ts +396 -119
- package/src/utils/schema/types.ts +3 -1
- package/src/utils/schema/zod-decontaminate.ts +83 -29
- package/src/utils/sqlite-errors.d.ts +4 -0
- package/src/utils/sqlite-errors.ts +13 -0
- package/src/utils/tool-choice-capability.ts +2 -3
package/src/auth-storage.ts
CHANGED
|
@@ -37,6 +37,7 @@ import type {
|
|
|
37
37
|
OAuthProvider,
|
|
38
38
|
OAuthProviderId,
|
|
39
39
|
} from "./utils/oauth/types";
|
|
40
|
+
import { isSqliteError } from "./utils/sqlite-errors";
|
|
40
41
|
|
|
41
42
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
43
|
// Credential Types
|
|
@@ -47,6 +48,20 @@ export type ApiKeyCredential = {
|
|
|
47
48
|
key: string;
|
|
48
49
|
};
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Extracts the bearer token from the structured API-key form used by OAuth
|
|
53
|
+
* providers that need to carry token metadata alongside the access token.
|
|
54
|
+
*/
|
|
55
|
+
export function extractStructuredApiKeyToken(apiKey: string): string | undefined {
|
|
56
|
+
if (!apiKey.startsWith("{")) return undefined;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(apiKey) as { token?: unknown };
|
|
59
|
+
return typeof parsed.token === "string" ? parsed.token : undefined;
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
export interface MCPOAuthBinding {
|
|
51
66
|
/** Exact HTTP(S) origin of the MCP resource endpoint. */
|
|
52
67
|
resourceOrigin: string;
|
|
@@ -369,6 +384,7 @@ export interface AuthCredentialSnapshotEntry {
|
|
|
369
384
|
provider: string;
|
|
370
385
|
credential: SnapshotCredential;
|
|
371
386
|
identityKey: string | null;
|
|
387
|
+
revision?: number;
|
|
372
388
|
}
|
|
373
389
|
|
|
374
390
|
export type AuthCredentialIfAbsentReason =
|
|
@@ -430,8 +446,21 @@ export type OAuthRefreshLeaseClaim =
|
|
|
430
446
|
| { kind: "busy"; expiresAt: number }
|
|
431
447
|
| { kind: "missing" };
|
|
432
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Store-owned ticket that orders a provider admission against remote
|
|
451
|
+
* credential snapshot application. The ticket is intentionally released at
|
|
452
|
+
* provider admission, not response completion.
|
|
453
|
+
*/
|
|
454
|
+
export interface CredentialDispatchTicket {
|
|
455
|
+
release(): void;
|
|
456
|
+
}
|
|
457
|
+
|
|
433
458
|
export interface AuthCredentialStore {
|
|
434
459
|
close(): void;
|
|
460
|
+
refreshSnapshot?(signal?: AbortSignal): Promise<unknown>;
|
|
461
|
+
onSnapshotChanged?(listener: () => void): () => void;
|
|
462
|
+
/** Order provider admission with remote snapshot authority application. */
|
|
463
|
+
acquireCredentialDispatchTicket?(provider: Provider, signal?: AbortSignal): Promise<CredentialDispatchTicket>;
|
|
435
464
|
listAuthCredentials(provider?: string): StoredAuthCredential[];
|
|
436
465
|
/** Payload-free account inventory; active and soft-disabled rows are included. */
|
|
437
466
|
listCredentialInventory?(provider?: string): CredentialInventoryRecord[];
|
|
@@ -445,12 +474,15 @@ export interface AuthCredentialStore {
|
|
|
445
474
|
updateAuthCredential(id: number, credential: AuthCredential): void;
|
|
446
475
|
deleteAuthCredential(id: number, disabledCause: string): void;
|
|
447
476
|
tryDisableAuthCredentialIfMatches(id: number, expectedData: string, disabledCause: string): boolean;
|
|
477
|
+
tryDisableAuthCredentialIfRevision?(id: number, expectedRevision: number, disabledCause: string): boolean;
|
|
448
478
|
replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[];
|
|
449
479
|
upsertAuthCredentialForProvider(provider: string, credential: AuthCredential): StoredAuthCredential[];
|
|
450
480
|
upsertAuthCredentialForProviderIfAbsent(provider: string, credential: AuthCredential): AuthCredentialIfAbsentResult;
|
|
451
481
|
deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void;
|
|
452
482
|
getCache(key: string, options?: { includeExpired?: boolean }): string | null;
|
|
453
483
|
setCache(key: string, value: string, expiresAtSec: number): void;
|
|
484
|
+
/** Atomically allocate a durable sequence for broker restart epochs. */
|
|
485
|
+
allocateMonotonicSequence(key: string, expiresAtSec: number): number;
|
|
454
486
|
deleteCachePrefix?(prefix: string): void;
|
|
455
487
|
cleanExpiredCache(): void;
|
|
456
488
|
/**
|
|
@@ -514,6 +546,7 @@ export interface AuthCredentialStore {
|
|
|
514
546
|
* `signal` propagates the agent's cancel down to the broker fetch.
|
|
515
547
|
*/
|
|
516
548
|
fetchUsageReports?(signal?: AbortSignal): Promise<UsageReport[] | null>;
|
|
549
|
+
fetchUsageReportsForProvider?(provider: Provider, signal?: AbortSignal): Promise<UsageReport[] | null>;
|
|
517
550
|
/** Synchronous, zero-network usage presentation peek. */
|
|
518
551
|
peekCachedUsagePresentation?(provider: Provider, credentialId: number): CachedUsagePresentation | undefined;
|
|
519
552
|
/** Record a safe usage observation after an explicit fetch/check. */
|
|
@@ -552,6 +585,21 @@ export interface AuthCredentialStore {
|
|
|
552
585
|
* {@link AuthStorage.invalidateCredentialMatching} fall back to `reload()`.
|
|
553
586
|
*/
|
|
554
587
|
markCredentialSuspect?(credentialId: number, opts?: { signal?: AbortSignal }): Promise<void>;
|
|
588
|
+
/**
|
|
589
|
+
* Optional async write hook to disable one credential through an authoritative
|
|
590
|
+
* remote store. Remote clients MUST use this hook instead of the synchronous
|
|
591
|
+
* local delete methods when an OAuth refresh fails definitively.
|
|
592
|
+
*
|
|
593
|
+
* Returns `false` when the row is already absent (for example, a peer
|
|
594
|
+
* disabled it first). Implementations MUST NOT treat a failed remote write as
|
|
595
|
+
* a successful local deletion.
|
|
596
|
+
*/
|
|
597
|
+
disableAuthCredentialRemote?(
|
|
598
|
+
credentialId: number,
|
|
599
|
+
disabledCause: string,
|
|
600
|
+
signal?: AbortSignal,
|
|
601
|
+
expectedRevision?: number,
|
|
602
|
+
): Promise<boolean>;
|
|
555
603
|
/**
|
|
556
604
|
* Optional async write hook for upserting a single credential. When present,
|
|
557
605
|
* `AuthStorage.#upsertOAuthCredential` routes through this instead of the
|
|
@@ -675,6 +723,7 @@ export type AuthStorageOptions = {
|
|
|
675
723
|
* AuthStorage caller surfaces that to its own consumer unchanged.
|
|
676
724
|
*/
|
|
677
725
|
fetchUsageReports?: (signal?: AbortSignal) => Promise<UsageReport[] | null>;
|
|
726
|
+
fetchUsageReportsForProvider?: (provider: Provider, signal?: AbortSignal) => Promise<UsageReport[] | null>;
|
|
678
727
|
};
|
|
679
728
|
|
|
680
729
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -995,9 +1044,11 @@ type UsageRequestDescriptor = {
|
|
|
995
1044
|
baseUrl?: string;
|
|
996
1045
|
};
|
|
997
1046
|
|
|
998
|
-
type AuthApiKeyOptions = {
|
|
1047
|
+
export type AuthApiKeyOptions = {
|
|
999
1048
|
baseUrl?: string;
|
|
1000
1049
|
modelId?: string;
|
|
1050
|
+
/** Select config registrations owned by one caller (for example a ModelRegistry). */
|
|
1051
|
+
owner?: object;
|
|
1001
1052
|
/**
|
|
1002
1053
|
* Caller's cancel signal. Threaded into any broker-bound OAuth refresh so
|
|
1003
1054
|
* `ESC` / request abort actually kills a hung broker fetch instead of
|
|
@@ -1036,6 +1087,7 @@ export interface OAuthAccess {
|
|
|
1036
1087
|
export interface InvalidateCredentialMatchingOptions {
|
|
1037
1088
|
signal?: AbortSignal;
|
|
1038
1089
|
sessionId?: string;
|
|
1090
|
+
owner?: object;
|
|
1039
1091
|
}
|
|
1040
1092
|
|
|
1041
1093
|
function isAbortSignalOption(
|
|
@@ -1054,17 +1106,40 @@ function safeUsageReport(report: UsageReport): SafeUsageReport {
|
|
|
1054
1106
|
}
|
|
1055
1107
|
|
|
1056
1108
|
function scrubHealthReason(reason: unknown, secrets: readonly string[] = []): string {
|
|
1057
|
-
let value = reason instanceof Error ? reason.message : String(reason);
|
|
1109
|
+
let value = (reason instanceof Error ? reason.message : String(reason)).replace(/\u001b\[[0-?]*[ -/]*[@-~]/gu, "");
|
|
1058
1110
|
for (const secret of secrets) {
|
|
1059
|
-
if (secret.length > 0)
|
|
1111
|
+
if (secret.length > 0) {
|
|
1112
|
+
value = value.split(secret).join("[redacted]");
|
|
1113
|
+
const escaped = [...secret].map(char => char.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&"));
|
|
1114
|
+
value = value.replace(new RegExp(escaped.join("\\s*"), "gu"), "[redacted]");
|
|
1115
|
+
}
|
|
1060
1116
|
}
|
|
1061
1117
|
value = value.replace(/bearer\s+[^\s,;]+/gi, "Bearer [redacted]");
|
|
1062
1118
|
value = value.replace(/(api[_-]?key|token|secret|authorization)[=:]\s*[^\s,;]+/gi, "$1=[redacted]");
|
|
1063
|
-
value = value
|
|
1119
|
+
value = value
|
|
1120
|
+
.replace(/[\x00-\x1f\x7f-\x9f]+/gu, " ")
|
|
1121
|
+
.replace(/[\r\n\t ]+/g, " ")
|
|
1122
|
+
.trim();
|
|
1064
1123
|
if (value.length > 256) value = `${value.slice(0, 253)}...`;
|
|
1065
1124
|
return value || "credential check failed";
|
|
1066
1125
|
}
|
|
1067
1126
|
|
|
1127
|
+
/** Read optional broker error detail without allowing hostile objects to escape classification. */
|
|
1128
|
+
/** @internal Tested directly because hostile accessors must preserve the original error identity. */
|
|
1129
|
+
export function readBrokerErrorBody(error: unknown): string | undefined {
|
|
1130
|
+
if (error === null || (typeof error !== "object" && typeof error !== "function")) return undefined;
|
|
1131
|
+
try {
|
|
1132
|
+
if (!("body" in error)) return undefined;
|
|
1133
|
+
const body = (error as { body?: unknown }).body;
|
|
1134
|
+
return typeof body === "string" ? body : undefined;
|
|
1135
|
+
} catch {
|
|
1136
|
+
// Preserve the original provider error and its terminal behavior. A proxy or
|
|
1137
|
+
// throwing accessor is untrusted provider data, not a reason to replace the
|
|
1138
|
+
// refresh failure with an inspection error.
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1068
1143
|
function requiresOpenAICodexProModel(provider: string, modelId: string | undefined): boolean {
|
|
1069
1144
|
return provider === "openai-codex" && typeof modelId === "string" && modelId.includes("-spark");
|
|
1070
1145
|
}
|
|
@@ -1213,7 +1288,15 @@ class AuthStorageUsageCache implements UsageCache {
|
|
|
1213
1288
|
// In-memory representation
|
|
1214
1289
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1215
1290
|
|
|
1216
|
-
type StoredCredential = { id: number; credential: AuthCredential };
|
|
1291
|
+
type StoredCredential = { id: number; credential: AuthCredential; revision?: number };
|
|
1292
|
+
type IndexedStoredCredential<T extends AuthCredential = AuthCredential> = {
|
|
1293
|
+
id: number;
|
|
1294
|
+
credential: T;
|
|
1295
|
+
index: number;
|
|
1296
|
+
revision?: number;
|
|
1297
|
+
};
|
|
1298
|
+
type OAuthCredentialSelection = IndexedStoredCredential<OAuthCredential>;
|
|
1299
|
+
type ConfigApiKeyRegistration = { apiKey: string; envSourced: boolean; order: number };
|
|
1217
1300
|
|
|
1218
1301
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1219
1302
|
// AuthStorage Class
|
|
@@ -1231,6 +1314,10 @@ export class AuthStorage {
|
|
|
1231
1314
|
#data: Map<string, StoredCredential[]> = new Map();
|
|
1232
1315
|
#runtimeOverrides: Map<string, string> = new Map();
|
|
1233
1316
|
#configOverrides: Map<string, string> = new Map();
|
|
1317
|
+
/** Effective config override registrations, including owner-scoped model registries. */
|
|
1318
|
+
#configOverrideRegistrations: Map<string, Map<object, ConfigApiKeyRegistration>> = new Map();
|
|
1319
|
+
#unownedConfigOverrides: Map<string, ConfigApiKeyRegistration> = new Map();
|
|
1320
|
+
#configOverrideOrder = 0;
|
|
1234
1321
|
/**
|
|
1235
1322
|
* Providers whose config override was resolved from a models.yml `apiKeyEnv`
|
|
1236
1323
|
* indirection rather than a literal `apiKey` pin. An env pointer is not a
|
|
@@ -1264,12 +1351,14 @@ export class AuthStorage {
|
|
|
1264
1351
|
#credentialRankingMode: CredentialRankingMode = "balanced";
|
|
1265
1352
|
#usageLogger?: UsageLogger;
|
|
1266
1353
|
#fallbackResolver?: (provider: string) => string | undefined;
|
|
1354
|
+
#ownedFallbackResolvers: Map<object, (provider: string) => string | undefined> = new Map();
|
|
1267
1355
|
#store: AuthCredentialStore;
|
|
1268
1356
|
#configValueResolver: (config: string, cacheScope?: string) => Promise<string | undefined>;
|
|
1269
1357
|
#resolvedStoredApiKeyValues: Map<string, Map<string, { fingerprint: string; usable: boolean }>> = new Map();
|
|
1270
1358
|
#storedApiKeyResolutionInFlight: Map<string, Map<string, Promise<string | undefined>>> = new Map();
|
|
1271
1359
|
#refreshOAuthCredentialOverride?: AuthStorageOptions["refreshOAuthCredential"];
|
|
1272
1360
|
#fetchUsageReportsOverride?: AuthStorageOptions["fetchUsageReports"];
|
|
1361
|
+
#fetchUsageReportsForProviderOverride?: AuthStorageOptions["fetchUsageReportsForProvider"];
|
|
1273
1362
|
#sourceLabel?: string;
|
|
1274
1363
|
#credentialDisabledListeners: Set<(event: CredentialDisabledEvent) => void | Promise<void>> = new Set();
|
|
1275
1364
|
/**
|
|
@@ -1299,6 +1388,10 @@ export class AuthStorage {
|
|
|
1299
1388
|
|
|
1300
1389
|
constructor(store: AuthCredentialStore, options: AuthStorageOptions = {}) {
|
|
1301
1390
|
this.#store = store;
|
|
1391
|
+
store.onSnapshotChanged?.(() => {
|
|
1392
|
+
this.#reloadCredentialRowsFromStore();
|
|
1393
|
+
void this.reload();
|
|
1394
|
+
});
|
|
1302
1395
|
this.#configValueResolver = options.configValueResolver ?? defaultConfigValueResolver;
|
|
1303
1396
|
this.#usageProviderResolver = options.usageProviderResolver ?? resolveDefaultUsageProvider;
|
|
1304
1397
|
this.#rankingStrategyResolver = options.rankingStrategyResolver ?? resolveDefaultRankingStrategy;
|
|
@@ -1308,6 +1401,7 @@ export class AuthStorage {
|
|
|
1308
1401
|
this.#credentialRankingMode = options.credentialRankingMode ?? "balanced";
|
|
1309
1402
|
this.#refreshOAuthCredentialOverride = options.refreshOAuthCredential;
|
|
1310
1403
|
this.#fetchUsageReportsOverride = options.fetchUsageReports;
|
|
1404
|
+
this.#fetchUsageReportsForProviderOverride = options.fetchUsageReportsForProvider;
|
|
1311
1405
|
this.#sourceLabel = options.sourceLabel;
|
|
1312
1406
|
if (options.onCredentialDisabled) {
|
|
1313
1407
|
// Constructor-registered subscribers are permanent for this AuthStorage's lifetime;
|
|
@@ -1352,6 +1446,15 @@ export class AuthStorage {
|
|
|
1352
1446
|
getGeneration(): number {
|
|
1353
1447
|
return this.#generation;
|
|
1354
1448
|
}
|
|
1449
|
+
getCache(key: string, options?: { includeExpired?: boolean }): string | null {
|
|
1450
|
+
return this.#store.getCache(key, options);
|
|
1451
|
+
}
|
|
1452
|
+
setCache(key: string, value: string, expiresAtSec: number): void {
|
|
1453
|
+
this.#store.setCache(key, value, expiresAtSec);
|
|
1454
|
+
}
|
|
1455
|
+
allocateMonotonicSequence(key: string, expiresAtSec: number): number {
|
|
1456
|
+
return this.#store.allocateMonotonicSequence(key, expiresAtSec);
|
|
1457
|
+
}
|
|
1355
1458
|
getProviderConfigurationGeneration(provider: string): number {
|
|
1356
1459
|
return this.#getProviderConfigurationGeneration(provider);
|
|
1357
1460
|
}
|
|
@@ -1364,13 +1467,29 @@ export class AuthStorage {
|
|
|
1364
1467
|
#getProviderConfigurationGeneration(provider: string): number {
|
|
1365
1468
|
return this.#providerConfigurationGenerations.get(resolveOAuthStorageProvider(provider)) ?? 1;
|
|
1366
1469
|
}
|
|
1367
|
-
|
|
1470
|
+
#configOverrideRegistration(provider: string, owner?: object): ConfigApiKeyRegistration | undefined {
|
|
1471
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1472
|
+
if (owner) {
|
|
1473
|
+
return (
|
|
1474
|
+
this.#configOverrideRegistrations.get(storageProvider)?.get(owner) ??
|
|
1475
|
+
this.#unownedConfigOverrides.get(storageProvider)
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
const apiKey = this.#configOverrides.get(storageProvider);
|
|
1479
|
+
return apiKey === undefined
|
|
1480
|
+
? undefined
|
|
1481
|
+
: { apiKey, envSourced: this.#configOverrideEnvSourced.has(storageProvider), order: 0 };
|
|
1482
|
+
}
|
|
1483
|
+
#hasConfigOverride(provider: string, owner?: object): boolean {
|
|
1484
|
+
return this.#configOverrideRegistration(provider, owner) !== undefined;
|
|
1485
|
+
}
|
|
1486
|
+
getProviderEvidenceGeneration(provider: string, resolvedApiKey?: string, owner?: object): string {
|
|
1368
1487
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1488
|
+
provider = storageProvider;
|
|
1369
1489
|
const evidenceApiKey = resolvedApiKey;
|
|
1490
|
+
const configOverride = this.#configOverrideRegistration(storageProvider, owner);
|
|
1370
1491
|
const storedLiteral =
|
|
1371
|
-
evidenceApiKey === undefined ||
|
|
1372
|
-
this.#runtimeOverrides.has(storageProvider) ||
|
|
1373
|
-
this.#configOverrides.has(storageProvider)
|
|
1492
|
+
evidenceApiKey === undefined || this.#runtimeOverrides.has(storageProvider) || configOverride !== undefined
|
|
1374
1493
|
? undefined
|
|
1375
1494
|
: this.#getCredentialsForProvider(storageProvider).find(
|
|
1376
1495
|
(credential): credential is Extract<AuthCredential, { type: "api_key" }> =>
|
|
@@ -1387,7 +1506,7 @@ export class AuthStorage {
|
|
|
1387
1506
|
}
|
|
1388
1507
|
let selectedCredential: ({ index: number } & StoredCredential) | undefined;
|
|
1389
1508
|
try {
|
|
1390
|
-
selectedCredential = this.#resolveSelectedStoredCredential(provider, undefined, undefined);
|
|
1509
|
+
selectedCredential = this.#resolveSelectedStoredCredential(provider, owner ? { owner } : undefined, undefined);
|
|
1391
1510
|
} catch {
|
|
1392
1511
|
return crypto
|
|
1393
1512
|
.createHash("sha256")
|
|
@@ -1403,7 +1522,7 @@ export class AuthStorage {
|
|
|
1403
1522
|
credential.type === "oauth" && Number.isFinite(credential.expires) && credential.expires > Date.now(),
|
|
1404
1523
|
);
|
|
1405
1524
|
const effectiveEnvKey =
|
|
1406
|
-
this.#runtimeOverrides.get(provider) ||
|
|
1525
|
+
this.#runtimeOverrides.get(provider) || configOverride?.apiKey || hasApiKey || hasUsableOAuth
|
|
1407
1526
|
? undefined
|
|
1408
1527
|
: getEnvApiKey(provider);
|
|
1409
1528
|
const storedApiKeyFingerprint = credentials
|
|
@@ -1551,12 +1670,21 @@ export class AuthStorage {
|
|
|
1551
1670
|
}
|
|
1552
1671
|
}
|
|
1553
1672
|
|
|
1554
|
-
/**
|
|
1555
|
-
|
|
1673
|
+
/**
|
|
1674
|
+
* Set the selector derived from a durable session pin or a session seed.
|
|
1675
|
+
* `owner` scopes config-override validation to one ModelRegistry; omitted
|
|
1676
|
+
* owners retain process-wide caller semantics.
|
|
1677
|
+
*/
|
|
1678
|
+
setSessionCredentialSelector(
|
|
1679
|
+
scopeId: string,
|
|
1680
|
+
provider: string,
|
|
1681
|
+
selector: AuthCredentialSelector,
|
|
1682
|
+
owner?: object,
|
|
1683
|
+
): void {
|
|
1556
1684
|
const scope = scopeId.trim();
|
|
1557
1685
|
if (!scope) throw new Error("Credential scope id must not be empty");
|
|
1558
1686
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1559
|
-
this.#assertCredentialSelectorUsable(storageProvider, selector);
|
|
1687
|
+
this.#assertCredentialSelectorUsable(storageProvider, selector, owner);
|
|
1560
1688
|
const selectors = this.#sessionCredentialSelectors.get(scope) ?? new Map<string, AuthCredentialSelector>();
|
|
1561
1689
|
selectors.set(storageProvider, selector);
|
|
1562
1690
|
this.#sessionCredentialSelectors.set(scope, selectors);
|
|
@@ -1615,23 +1743,32 @@ export class AuthStorage {
|
|
|
1615
1743
|
}
|
|
1616
1744
|
|
|
1617
1745
|
/** @internal Return cache provenance for an exact stored literal API-key row without resolving its value. */
|
|
1618
|
-
getStoredLiteralApiKeyEvidenceGeneration(
|
|
1746
|
+
getStoredLiteralApiKeyEvidenceGeneration(
|
|
1747
|
+
provider: string,
|
|
1748
|
+
selector: AuthCredentialSelector,
|
|
1749
|
+
owner?: object,
|
|
1750
|
+
): string | undefined {
|
|
1619
1751
|
if (selector.kind !== "id") return undefined;
|
|
1620
1752
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1621
|
-
if (this.#runtimeOverrides.has(storageProvider) || this.#
|
|
1753
|
+
if (this.#runtimeOverrides.has(storageProvider) || this.#hasConfigOverride(storageProvider, owner))
|
|
1754
|
+
return undefined;
|
|
1622
1755
|
const selected = this.#findCredentialBySelector(storageProvider, selector);
|
|
1623
1756
|
if (selected?.credential.type !== "api_key") return undefined;
|
|
1624
1757
|
const key = selected.credential.key;
|
|
1625
1758
|
if (!key || key.startsWith("!") || process.env[key] !== undefined) return undefined;
|
|
1626
|
-
return this.getProviderEvidenceGeneration(storageProvider, key);
|
|
1759
|
+
return this.getProviderEvidenceGeneration(storageProvider, key, owner);
|
|
1627
1760
|
}
|
|
1628
1761
|
|
|
1629
|
-
/**
|
|
1630
|
-
|
|
1762
|
+
/**
|
|
1763
|
+
* Validate and canonicalize an OAuth-only selector for account pinning.
|
|
1764
|
+
* `owner` scopes config-override checks to one ModelRegistry; omitted owners
|
|
1765
|
+
* retain process-wide caller semantics.
|
|
1766
|
+
*/
|
|
1767
|
+
resolveOAuthPinTarget(provider: string, selector: AuthCredentialSelector, owner?: object): OAuthPinTarget {
|
|
1631
1768
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1632
1769
|
if (
|
|
1633
1770
|
this.#runtimeOverrides.has(storageProvider) ||
|
|
1634
|
-
this.#
|
|
1771
|
+
this.#hasConfigOverride(storageProvider, owner) ||
|
|
1635
1772
|
getEnvApiKey(storageProvider)
|
|
1636
1773
|
) {
|
|
1637
1774
|
throw new OAuthCredentialSelectorError(
|
|
@@ -1828,8 +1965,8 @@ export class AuthStorage {
|
|
|
1828
1965
|
}
|
|
1829
1966
|
|
|
1830
1967
|
/** Whether a provider is currently authenticated by a config API-key override. */
|
|
1831
|
-
hasConfigApiKey(provider: string): boolean {
|
|
1832
|
-
return Boolean(this.#
|
|
1968
|
+
hasConfigApiKey(provider: string, owner?: object): boolean {
|
|
1969
|
+
return Boolean(this.#configOverrideRegistration(provider, owner)?.apiKey);
|
|
1833
1970
|
}
|
|
1834
1971
|
|
|
1835
1972
|
/**
|
|
@@ -1890,7 +2027,8 @@ export class AuthStorage {
|
|
|
1890
2027
|
* runtime API-key override (`--api-key`), or a config-sourced API key
|
|
1891
2028
|
* (`models.yml` `apiKey`) would each re-decide the credential on the very
|
|
1892
2029
|
* next {@link AuthStorage.getApiKey} call and make this switch appear to
|
|
1893
|
-
* silently do nothing.
|
|
2030
|
+
* silently do nothing. `owner` scopes the config-override check to one
|
|
2031
|
+
* ModelRegistry; omitted owners retain process-wide caller semantics.
|
|
1894
2032
|
*
|
|
1895
2033
|
* Deliberately does not touch credential-blocked state: if the target row
|
|
1896
2034
|
* is still backoff-blocked from a prior quota failure, the existing
|
|
@@ -1898,14 +2036,19 @@ export class AuthStorage {
|
|
|
1898
2036
|
* falls back to a usable account instead of re-issuing a request that would
|
|
1899
2037
|
* just draw another 429/quota error.
|
|
1900
2038
|
*/
|
|
1901
|
-
switchSessionCredential(
|
|
2039
|
+
switchSessionCredential(
|
|
2040
|
+
provider: string,
|
|
2041
|
+
sessionId: string,
|
|
2042
|
+
selector: AuthCredentialSelector,
|
|
2043
|
+
owner?: object,
|
|
2044
|
+
): void {
|
|
1902
2045
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1903
2046
|
if (this.#runtimeOverrides.has(storageProvider)) {
|
|
1904
2047
|
throw new Error(
|
|
1905
2048
|
`Cannot switch credential for ${provider}: a runtime API key override (--api-key) is active and always wins`,
|
|
1906
2049
|
);
|
|
1907
2050
|
}
|
|
1908
|
-
if (this.#
|
|
2051
|
+
if (this.#hasConfigOverride(storageProvider, owner)) {
|
|
1909
2052
|
throw new Error(
|
|
1910
2053
|
`Cannot switch credential for ${provider}: a config API key override (models.yml) is active and always wins`,
|
|
1911
2054
|
);
|
|
@@ -1935,6 +2078,9 @@ export class AuthStorage {
|
|
|
1935
2078
|
* Lower priority than {@link setRuntimeApiKey} so a CLI `--api-key`
|
|
1936
2079
|
* still wins for the duration of a single invocation.
|
|
1937
2080
|
*
|
|
2081
|
+
* `options.owner` scopes the override to one registry or other caller. The
|
|
2082
|
+
* unscoped form is process-wide and is retained for standalone callers.
|
|
2083
|
+
*
|
|
1938
2084
|
* `options.envSourced` marks the value as resolved from a models.yml
|
|
1939
2085
|
* `apiKeyEnv` indirection. Unlike a literal pin, an env pointer only says
|
|
1940
2086
|
* where to look for a key; when the user has since run `auth login`, the
|
|
@@ -1942,44 +2088,111 @@ export class AuthStorage {
|
|
|
1942
2088
|
* wins over the indirection (stored OAuth credentials still yield, so a
|
|
1943
2089
|
* custom-endpoint bearer is never replaced by an upstream OAuth token).
|
|
1944
2090
|
*/
|
|
1945
|
-
setConfigApiKey(provider: string, apiKey: string, options: { envSourced?: boolean } = {}): void {
|
|
2091
|
+
setConfigApiKey(provider: string, apiKey: string, options: { envSourced?: boolean; owner?: object } = {}): void {
|
|
1946
2092
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2093
|
+
const registration = {
|
|
2094
|
+
apiKey,
|
|
2095
|
+
envSourced: options.envSourced === true,
|
|
2096
|
+
order: ++this.#configOverrideOrder,
|
|
2097
|
+
};
|
|
2098
|
+
if (options.owner) {
|
|
2099
|
+
const registrations = this.#configOverrideRegistrations.get(storageProvider) ?? new Map();
|
|
2100
|
+
registrations.set(options.owner, registration);
|
|
2101
|
+
this.#configOverrideRegistrations.set(storageProvider, registrations);
|
|
1950
2102
|
} else {
|
|
1951
|
-
this.#
|
|
2103
|
+
this.#unownedConfigOverrides.set(storageProvider, registration);
|
|
1952
2104
|
}
|
|
1953
|
-
this.#
|
|
2105
|
+
this.#reconcileConfigApiKey(storageProvider, "set-config-api-key", true);
|
|
1954
2106
|
}
|
|
1955
2107
|
|
|
1956
2108
|
/**
|
|
1957
2109
|
* Remove a single config-sourced API key override.
|
|
1958
2110
|
*/
|
|
1959
|
-
removeConfigApiKey(provider: string): void {
|
|
2111
|
+
removeConfigApiKey(provider: string, owner?: object): void {
|
|
1960
2112
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1961
|
-
|
|
1962
|
-
|
|
2113
|
+
if (owner) {
|
|
2114
|
+
const registrations = this.#configOverrideRegistrations.get(storageProvider);
|
|
2115
|
+
if (!registrations?.delete(owner)) return;
|
|
2116
|
+
if (registrations.size === 0) this.#configOverrideRegistrations.delete(storageProvider);
|
|
2117
|
+
} else {
|
|
2118
|
+
if (!this.#unownedConfigOverrides.delete(storageProvider)) return;
|
|
2119
|
+
}
|
|
2120
|
+
this.#reconcileConfigApiKey(storageProvider, "remove-config-api-key", true);
|
|
1963
2121
|
}
|
|
1964
2122
|
|
|
1965
2123
|
/**
|
|
1966
|
-
* Drop
|
|
1967
|
-
*
|
|
2124
|
+
* Drop config-sourced API keys. An owner removes only its own registrations;
|
|
2125
|
+
* the unscoped form remains an explicit global reset for callers that own the
|
|
2126
|
+
* entire AuthStorage instance.
|
|
1968
2127
|
*/
|
|
1969
|
-
clearConfigApiKeys(): void {
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2128
|
+
clearConfigApiKeys(owner?: object): void {
|
|
2129
|
+
if (owner) {
|
|
2130
|
+
const providers = [...this.#configOverrideRegistrations.entries()]
|
|
2131
|
+
.filter(([, registrations]) => registrations.has(owner))
|
|
2132
|
+
.map(([provider]) => provider);
|
|
2133
|
+
for (const provider of providers) this.removeConfigApiKey(provider, owner);
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
const providers = new Set([
|
|
2137
|
+
...this.#configOverrides.keys(),
|
|
2138
|
+
...this.#unownedConfigOverrides.keys(),
|
|
2139
|
+
...this.#configOverrideRegistrations.keys(),
|
|
2140
|
+
]);
|
|
2141
|
+
this.#unownedConfigOverrides.clear();
|
|
2142
|
+
this.#configOverrideRegistrations.clear();
|
|
2143
|
+
for (const provider of providers) this.#reconcileConfigApiKey(provider, "clear-config-api-keys", true);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
#reconcileConfigApiKey(provider: string, reason: string, forceGeneration = false): void {
|
|
2147
|
+
const previous = this.#configOverrides.get(provider);
|
|
2148
|
+
const previousEnvSourced = this.#configOverrideEnvSourced.has(provider);
|
|
2149
|
+
let winner: { apiKey: string; envSourced: boolean; order: number } | undefined =
|
|
2150
|
+
this.#unownedConfigOverrides.get(provider);
|
|
2151
|
+
for (const registration of this.#configOverrideRegistrations.get(provider)?.values() ?? []) {
|
|
2152
|
+
if (!winner || registration.order > winner.order) winner = registration;
|
|
2153
|
+
}
|
|
2154
|
+
if (!winner) {
|
|
2155
|
+
this.#configOverrides.delete(provider);
|
|
2156
|
+
this.#configOverrideEnvSourced.delete(provider);
|
|
2157
|
+
} else {
|
|
2158
|
+
this.#configOverrides.set(provider, winner.apiKey);
|
|
2159
|
+
if (winner.envSourced) this.#configOverrideEnvSourced.add(provider);
|
|
2160
|
+
else this.#configOverrideEnvSourced.delete(provider);
|
|
2161
|
+
}
|
|
2162
|
+
const current = this.#configOverrides.get(provider);
|
|
2163
|
+
const currentEnvSourced = this.#configOverrideEnvSourced.has(provider);
|
|
2164
|
+
if (forceGeneration || previous !== current || previousEnvSourced !== currentEnvSourced) {
|
|
2165
|
+
this.#bumpGeneration(reason, provider);
|
|
2166
|
+
}
|
|
1975
2167
|
}
|
|
1976
2168
|
|
|
1977
2169
|
/**
|
|
1978
2170
|
* Set a fallback resolver for API keys not found in storage or env vars.
|
|
1979
2171
|
* Used for custom provider keys from models.json.
|
|
1980
2172
|
*/
|
|
1981
|
-
setFallbackResolver(resolver: (provider: string) => string | undefined): void {
|
|
2173
|
+
setFallbackResolver(resolver: (provider: string) => string | undefined, owner?: object): () => void {
|
|
2174
|
+
if (owner) {
|
|
2175
|
+
this.#ownedFallbackResolvers.set(owner, resolver);
|
|
2176
|
+
return () => {
|
|
2177
|
+
if (this.#ownedFallbackResolvers.get(owner) !== resolver) return;
|
|
2178
|
+
this.#ownedFallbackResolvers.delete(owner);
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
1982
2181
|
this.#fallbackResolver = resolver;
|
|
2182
|
+
return () => {
|
|
2183
|
+
if (this.#fallbackResolver === resolver) this.#fallbackResolver = undefined;
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
#resolveFallback(provider: string, owner?: object): string | undefined {
|
|
2188
|
+
if (owner) {
|
|
2189
|
+
return this.#ownedFallbackResolvers.get(owner)?.(provider) ?? this.#fallbackResolver?.(provider);
|
|
2190
|
+
}
|
|
2191
|
+
for (const resolver of [...this.#ownedFallbackResolvers.values()].reverse()) {
|
|
2192
|
+
const value = resolver(provider);
|
|
2193
|
+
if (value !== undefined) return value;
|
|
2194
|
+
}
|
|
2195
|
+
return this.#fallbackResolver?.(provider);
|
|
1983
2196
|
}
|
|
1984
2197
|
|
|
1985
2198
|
/**
|
|
@@ -1987,11 +2200,27 @@ export class AuthStorage {
|
|
|
1987
2200
|
*/
|
|
1988
2201
|
async reload(): Promise<void> {
|
|
1989
2202
|
await this.#store.waitForReady?.();
|
|
2203
|
+
this.#reloadCredentialRowsFromStore();
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
/**
|
|
2207
|
+
* Acquire a store-owned provider-admission ticket when the backing store
|
|
2208
|
+
* provides one (for example, a remote broker snapshot store). Local stores
|
|
2209
|
+
* need no additional ordering and return `undefined`.
|
|
2210
|
+
*/
|
|
2211
|
+
async acquireCredentialDispatchTicket(
|
|
2212
|
+
provider: Provider,
|
|
2213
|
+
signal?: AbortSignal,
|
|
2214
|
+
): Promise<CredentialDispatchTicket | undefined> {
|
|
2215
|
+
return this.#store.acquireCredentialDispatchTicket?.(resolveOAuthStorageProvider(provider), signal);
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
#reloadCredentialRowsFromStore(): void {
|
|
1990
2219
|
const records = this.#store.listAuthCredentials();
|
|
1991
2220
|
const grouped = new Map<string, StoredCredential[]>();
|
|
1992
2221
|
for (const record of records) {
|
|
1993
2222
|
const list = grouped.get(record.provider) ?? [];
|
|
1994
|
-
list.push({ id: record.id, credential: record.credential });
|
|
2223
|
+
list.push({ id: record.id, credential: record.credential, revision: record.revision });
|
|
1995
2224
|
grouped.set(record.provider, list);
|
|
1996
2225
|
}
|
|
1997
2226
|
|
|
@@ -2032,6 +2261,12 @@ export class AuthStorage {
|
|
|
2032
2261
|
#setStoredCredentials(provider: string, credentials: StoredCredential[]): void {
|
|
2033
2262
|
const current = this.#data.get(provider) ?? [];
|
|
2034
2263
|
if (storedCredentialArraysEqual(current, credentials)) return;
|
|
2264
|
+
const identityOrderChanged =
|
|
2265
|
+
current.length !== credentials.length ||
|
|
2266
|
+
current.some(
|
|
2267
|
+
(entry, index) =>
|
|
2268
|
+
entry.id !== credentials[index]?.id || entry.credential.type !== credentials[index]?.credential.type,
|
|
2269
|
+
);
|
|
2035
2270
|
this.#resolvedStoredApiKeyValues.delete(provider);
|
|
2036
2271
|
this.#storedApiKeyResolutionInFlight.delete(provider);
|
|
2037
2272
|
if (credentials.length === 0) {
|
|
@@ -2039,6 +2274,7 @@ export class AuthStorage {
|
|
|
2039
2274
|
} else {
|
|
2040
2275
|
this.#data.set(provider, credentials);
|
|
2041
2276
|
}
|
|
2277
|
+
if (identityOrderChanged) this.#resetProviderAssignments(resolveOAuthStorageProvider(provider));
|
|
2042
2278
|
this.#bumpGeneration("credentials", provider);
|
|
2043
2279
|
}
|
|
2044
2280
|
|
|
@@ -2239,10 +2475,7 @@ export class AuthStorage {
|
|
|
2239
2475
|
}
|
|
2240
2476
|
}
|
|
2241
2477
|
|
|
2242
|
-
#findCredentialBySelector(
|
|
2243
|
-
provider: string,
|
|
2244
|
-
selector: AuthCredentialSelector,
|
|
2245
|
-
): ({ index: number } & StoredCredential) | undefined {
|
|
2478
|
+
#findCredentialBySelector(provider: string, selector: AuthCredentialSelector): IndexedStoredCredential | undefined {
|
|
2246
2479
|
const stored = this.#getStoredCredentials(provider);
|
|
2247
2480
|
for (let index = 0; index < stored.length; index++) {
|
|
2248
2481
|
const entry = stored[index];
|
|
@@ -2285,13 +2518,13 @@ export class AuthStorage {
|
|
|
2285
2518
|
);
|
|
2286
2519
|
}
|
|
2287
2520
|
|
|
2288
|
-
#assertCredentialSelectorUsable(provider: string, selector: AuthCredentialSelector): void {
|
|
2521
|
+
#assertCredentialSelectorUsable(provider: string, selector: AuthCredentialSelector, owner?: object): void {
|
|
2289
2522
|
if (this.#runtimeOverrides.has(provider)) {
|
|
2290
2523
|
throw new Error(
|
|
2291
2524
|
`Credential selector ${this.#formatCredentialSelector(selector)} cannot be used for ${provider} while a runtime API key override is active`,
|
|
2292
2525
|
);
|
|
2293
2526
|
}
|
|
2294
|
-
if (this.#
|
|
2527
|
+
if (this.#hasConfigOverride(provider, owner)) {
|
|
2295
2528
|
throw new Error(
|
|
2296
2529
|
`Credential selector ${this.#formatCredentialSelector(selector)} cannot be used for ${provider} while a config API key override is active`,
|
|
2297
2530
|
);
|
|
@@ -2307,8 +2540,8 @@ export class AuthStorage {
|
|
|
2307
2540
|
* to an OAuth row specifically — the soft-preference/quota-fallback path is
|
|
2308
2541
|
* meaningless for a single static API key.
|
|
2309
2542
|
*/
|
|
2310
|
-
#assertPreferredCredentialSelectorUsable(provider: string, selector: AuthCredentialSelector): void {
|
|
2311
|
-
if (this.#runtimeOverrides.has(provider) || this.#
|
|
2543
|
+
#assertPreferredCredentialSelectorUsable(provider: string, selector: AuthCredentialSelector, owner?: object): void {
|
|
2544
|
+
if (this.#runtimeOverrides.has(provider) || this.#hasConfigOverride(provider, owner)) {
|
|
2312
2545
|
throw new Error(
|
|
2313
2546
|
`Preferred credential selector ${this.#formatCredentialSelector(selector)} cannot be used for ${provider} while an API key override is active`,
|
|
2314
2547
|
);
|
|
@@ -2325,10 +2558,10 @@ export class AuthStorage {
|
|
|
2325
2558
|
provider: string,
|
|
2326
2559
|
options?: AuthApiKeyOptions,
|
|
2327
2560
|
sessionId?: string,
|
|
2328
|
-
):
|
|
2561
|
+
): IndexedStoredCredential | undefined {
|
|
2329
2562
|
const selector = this.#getCredentialSelector(provider, options, sessionId);
|
|
2330
2563
|
if (!selector) return undefined;
|
|
2331
|
-
this.#assertCredentialSelectorUsable(resolveOAuthStorageProvider(provider), selector);
|
|
2564
|
+
this.#assertCredentialSelectorUsable(resolveOAuthStorageProvider(provider), selector, options?.owner);
|
|
2332
2565
|
const selected = this.#findCredentialBySelector(provider, selector);
|
|
2333
2566
|
if (!selected) {
|
|
2334
2567
|
throw new Error(`No credential found for ${provider} matching ${this.#formatCredentialSelector(selector)}`);
|
|
@@ -2345,13 +2578,23 @@ export class AuthStorage {
|
|
|
2345
2578
|
type: T,
|
|
2346
2579
|
sessionId?: string,
|
|
2347
2580
|
isUsable?: (credential: Extract<AuthCredential, { type: T }>, index: number) => boolean | undefined,
|
|
2348
|
-
):
|
|
2349
|
-
const credentials = this.#
|
|
2350
|
-
.map((
|
|
2581
|
+
): IndexedStoredCredential<Extract<AuthCredential, { type: T }>> | undefined {
|
|
2582
|
+
const credentials = this.#getStoredCredentials(provider)
|
|
2583
|
+
.map((entry, index) => ({ entry, index }))
|
|
2351
2584
|
.filter(
|
|
2352
|
-
(
|
|
2353
|
-
|
|
2354
|
-
|
|
2585
|
+
(
|
|
2586
|
+
item,
|
|
2587
|
+
): item is {
|
|
2588
|
+
entry: StoredCredential & { credential: Extract<AuthCredential, { type: T }> };
|
|
2589
|
+
index: number;
|
|
2590
|
+
} => item.entry.credential.type === type,
|
|
2591
|
+
)
|
|
2592
|
+
.map(({ entry, index }) => ({
|
|
2593
|
+
id: entry.id,
|
|
2594
|
+
credential: entry.credential,
|
|
2595
|
+
index,
|
|
2596
|
+
revision: entry.revision,
|
|
2597
|
+
}));
|
|
2355
2598
|
|
|
2356
2599
|
if (credentials.length === 0) return undefined;
|
|
2357
2600
|
|
|
@@ -2377,12 +2620,14 @@ export class AuthStorage {
|
|
|
2377
2620
|
sessionId?: string,
|
|
2378
2621
|
excludedIndices: ReadonlySet<number> = new Set(),
|
|
2379
2622
|
includeKnownUnusable = false,
|
|
2380
|
-
):
|
|
2381
|
-
const credentials = this.#
|
|
2382
|
-
.map((
|
|
2623
|
+
): IndexedStoredCredential<ApiKeyCredential> | undefined {
|
|
2624
|
+
const credentials = this.#getStoredCredentials(provider)
|
|
2625
|
+
.map((entry, index) => ({ entry, index }))
|
|
2383
2626
|
.filter(
|
|
2384
|
-
(
|
|
2385
|
-
|
|
2627
|
+
(item): item is { entry: StoredCredential & { credential: ApiKeyCredential }; index: number } =>
|
|
2628
|
+
item.entry.credential.type === "api_key",
|
|
2629
|
+
)
|
|
2630
|
+
.map(({ entry, index }) => ({ id: entry.id, credential: entry.credential, index, revision: entry.revision }));
|
|
2386
2631
|
if (credentials.length === 0) return undefined;
|
|
2387
2632
|
|
|
2388
2633
|
const providerKey = this.#getProviderTypeKey(provider, "api_key");
|
|
@@ -2421,11 +2666,20 @@ export class AuthStorage {
|
|
|
2421
2666
|
}
|
|
2422
2667
|
|
|
2423
2668
|
/** Updates a credential at index after OAuth token refresh. */
|
|
2424
|
-
#replaceCredentialAt(
|
|
2669
|
+
#replaceCredentialAt(
|
|
2670
|
+
provider: string,
|
|
2671
|
+
index: number,
|
|
2672
|
+
credential: AuthCredential,
|
|
2673
|
+
persist = true,
|
|
2674
|
+
expectedId?: number,
|
|
2675
|
+
): void {
|
|
2425
2676
|
const entries = this.#getStoredCredentials(provider);
|
|
2426
2677
|
if (index < 0 || index >= entries.length) return;
|
|
2427
2678
|
const target = entries[index];
|
|
2428
|
-
if (
|
|
2679
|
+
if (expectedId !== undefined && target.id !== expectedId) {
|
|
2680
|
+
throw new Error("Credential authority changed during refresh");
|
|
2681
|
+
}
|
|
2682
|
+
if (persist && !this.#store.refreshSnapshot) this.#store.updateAuthCredential(target.id, credential);
|
|
2429
2683
|
const updated = [...entries];
|
|
2430
2684
|
updated[index] = { id: target.id, credential };
|
|
2431
2685
|
this.#setStoredCredentials(provider, updated);
|
|
@@ -2456,15 +2710,18 @@ export class AuthStorage {
|
|
|
2456
2710
|
index: number,
|
|
2457
2711
|
expectedCredential: AuthCredential,
|
|
2458
2712
|
disabledCause: string,
|
|
2713
|
+
expectedId?: number,
|
|
2459
2714
|
): boolean {
|
|
2460
2715
|
const entries = this.#getStoredCredentials(provider);
|
|
2461
|
-
|
|
2462
|
-
|
|
2716
|
+
const targetIndex = expectedId === undefined ? index : entries.findIndex(entry => entry.id === expectedId);
|
|
2717
|
+
if (targetIndex < 0 || targetIndex >= entries.length) return false;
|
|
2718
|
+
const target = entries[targetIndex];
|
|
2719
|
+
if (expectedId !== undefined && target.id !== expectedId) return false;
|
|
2463
2720
|
const serialized = serializeCredential(provider, expectedCredential);
|
|
2464
2721
|
if (!serialized) return false;
|
|
2465
2722
|
const disabled = this.#store.tryDisableAuthCredentialIfMatches(target.id, serialized.data, disabledCause);
|
|
2466
2723
|
if (!disabled) return false;
|
|
2467
|
-
const updated = entries.filter((_value, idx) => idx !==
|
|
2724
|
+
const updated = entries.filter((_value, idx) => idx !== targetIndex);
|
|
2468
2725
|
this.#setStoredCredentials(provider, updated);
|
|
2469
2726
|
this.#clearSelectorsForRemovedCredential(provider, new Set([target.id]), entries);
|
|
2470
2727
|
this.#resetProviderAssignments(provider);
|
|
@@ -2501,6 +2758,35 @@ export class AuthStorage {
|
|
|
2501
2758
|
this.#emitCredentialDisabled({ provider, disabledCause });
|
|
2502
2759
|
}
|
|
2503
2760
|
|
|
2761
|
+
/**
|
|
2762
|
+
* Disable one credential through an authoritative remote store and reconcile
|
|
2763
|
+
* this AuthStorage instance only after that write succeeds. Remote stores
|
|
2764
|
+
* must never be mutated through the synchronous local delete path because
|
|
2765
|
+
* their snapshots do not own persistence authority.
|
|
2766
|
+
*/
|
|
2767
|
+
async #disableCredentialRemotely(
|
|
2768
|
+
provider: string,
|
|
2769
|
+
credentialId: number,
|
|
2770
|
+
disabledCause: string,
|
|
2771
|
+
signal?: AbortSignal,
|
|
2772
|
+
expectedRevision?: number,
|
|
2773
|
+
): Promise<boolean> {
|
|
2774
|
+
const disable = this.#store.disableAuthCredentialRemote?.bind(this.#store);
|
|
2775
|
+
if (!disable) return false;
|
|
2776
|
+
const disabled = await disable(credentialId, disabledCause, signal, expectedRevision);
|
|
2777
|
+
if (!disabled) return false;
|
|
2778
|
+
const entries = this.#getStoredCredentials(provider);
|
|
2779
|
+
if (!entries.some(entry => entry.id === credentialId)) return true;
|
|
2780
|
+
this.#setStoredCredentials(
|
|
2781
|
+
provider,
|
|
2782
|
+
entries.filter(entry => entry.id !== credentialId),
|
|
2783
|
+
);
|
|
2784
|
+
this.#clearSelectorsForRemovedCredential(provider, new Set([credentialId]), entries);
|
|
2785
|
+
this.#resetProviderAssignments(provider);
|
|
2786
|
+
this.#emitCredentialDisabled({ provider, disabledCause });
|
|
2787
|
+
return true;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2504
2790
|
/** Clear every selector whose durable/in-memory target was just removed. */
|
|
2505
2791
|
#clearSelectorsForRemovedCredential(
|
|
2506
2792
|
provider: string,
|
|
@@ -2575,7 +2861,7 @@ export class AuthStorage {
|
|
|
2575
2861
|
: this.#store.replaceAuthCredentialsForProvider(storageProvider, deduped);
|
|
2576
2862
|
this.#setStoredCredentials(
|
|
2577
2863
|
storageProvider,
|
|
2578
|
-
stored.map(record => ({ id: record.id, credential: record.credential })),
|
|
2864
|
+
stored.map(record => ({ id: record.id, credential: record.credential, revision: record.revision })),
|
|
2579
2865
|
);
|
|
2580
2866
|
this.#resetProviderAssignments(storageProvider);
|
|
2581
2867
|
}
|
|
@@ -2590,6 +2876,7 @@ export class AuthStorage {
|
|
|
2590
2876
|
provider: entry.provider,
|
|
2591
2877
|
credential: redacted,
|
|
2592
2878
|
identityKey: resolveCredentialIdentityKey(provider, persisted),
|
|
2879
|
+
...(entry.revision === undefined ? {} : { revision: entry.revision }),
|
|
2593
2880
|
};
|
|
2594
2881
|
});
|
|
2595
2882
|
}
|
|
@@ -2606,14 +2893,15 @@ export class AuthStorage {
|
|
|
2606
2893
|
async importCredentialIfAbsent(
|
|
2607
2894
|
provider: string,
|
|
2608
2895
|
credential: AuthCredential,
|
|
2896
|
+
owner?: object,
|
|
2609
2897
|
): Promise<AuthCredentialIfAbsentSnapshotResult> {
|
|
2610
2898
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2611
2899
|
if (this.#runtimeOverrides.has(storageProvider))
|
|
2612
2900
|
return this.#snapshotSkipResult(storageProvider, "skipped-existing-runtime");
|
|
2613
|
-
if (this.#
|
|
2901
|
+
if (this.#hasConfigOverride(storageProvider, owner))
|
|
2614
2902
|
return this.#snapshotSkipResult(storageProvider, "skipped-existing-config");
|
|
2615
2903
|
if (getEnvApiKey(storageProvider)) return this.#snapshotSkipResult(storageProvider, "skipped-existing-env");
|
|
2616
|
-
if (this.#
|
|
2904
|
+
if (this.#resolveFallback(storageProvider, owner))
|
|
2617
2905
|
return this.#snapshotSkipResult(storageProvider, "skipped-existing-fallback");
|
|
2618
2906
|
|
|
2619
2907
|
const result = this.#store.upsertAuthCredentialRemoteIfAbsent
|
|
@@ -2621,7 +2909,7 @@ export class AuthStorage {
|
|
|
2621
2909
|
: this.#store.upsertAuthCredentialForProviderIfAbsent(storageProvider, credential);
|
|
2622
2910
|
this.#setStoredCredentials(
|
|
2623
2911
|
storageProvider,
|
|
2624
|
-
result.entries.map(entry => ({ id: entry.id, credential: entry.credential })),
|
|
2912
|
+
result.entries.map(entry => ({ id: entry.id, credential: entry.credential, revision: entry.revision })),
|
|
2625
2913
|
);
|
|
2626
2914
|
this.#resetProviderAssignments(storageProvider);
|
|
2627
2915
|
if (result.inserted) this.#invalidateUsageCacheForProvider(storageProvider);
|
|
@@ -2639,7 +2927,7 @@ export class AuthStorage {
|
|
|
2639
2927
|
: this.#store.upsertAuthCredentialForProvider(provider, credential);
|
|
2640
2928
|
this.#setStoredCredentials(
|
|
2641
2929
|
provider,
|
|
2642
|
-
stored.map(record => ({ id: record.id, credential: record.credential })),
|
|
2930
|
+
stored.map(record => ({ id: record.id, credential: record.credential, revision: record.revision })),
|
|
2643
2931
|
);
|
|
2644
2932
|
this.#resetProviderAssignments(provider);
|
|
2645
2933
|
this.#invalidateUsageCacheForProvider(provider);
|
|
@@ -2689,23 +2977,44 @@ export class AuthStorage {
|
|
|
2689
2977
|
* Check if any form of auth is configured for a provider.
|
|
2690
2978
|
* Unlike getApiKey(), this doesn't refresh OAuth tokens.
|
|
2691
2979
|
*/
|
|
2692
|
-
#hasConfiguredAuth(storageProvider: string): boolean {
|
|
2980
|
+
#hasConfiguredAuth(storageProvider: string, owner?: object): boolean {
|
|
2693
2981
|
if (this.hasRuntimeApiKey(storageProvider)) return true;
|
|
2694
|
-
if (this.#
|
|
2982
|
+
if (this.#hasConfigOverride(storageProvider, owner)) return true;
|
|
2695
2983
|
if (this.#getCredentialsForProvider(storageProvider).length > 0) return true;
|
|
2696
2984
|
if (getEnvApiKey(storageProvider)) return true;
|
|
2697
|
-
if (this.#
|
|
2985
|
+
if (this.#resolveFallback(storageProvider, owner)) return true;
|
|
2698
2986
|
return false;
|
|
2699
2987
|
}
|
|
2700
2988
|
|
|
2701
|
-
|
|
2989
|
+
disableCredentialByIdIfRevision(id: number, expectedRevision: number, disabledCause: string): boolean {
|
|
2990
|
+
const cause = normalizeDisabledCause(disabledCause);
|
|
2991
|
+
if (this.#store.tryDisableAuthCredentialIfRevision?.(id, expectedRevision, cause) !== true) return false;
|
|
2992
|
+
for (const [provider, entries] of this.#data) {
|
|
2993
|
+
if (!entries.some(entry => entry.id === id)) continue;
|
|
2994
|
+
this.#setStoredCredentials(
|
|
2995
|
+
provider,
|
|
2996
|
+
entries.filter(entry => entry.id !== id),
|
|
2997
|
+
);
|
|
2998
|
+
this.#clearSelectorsForRemovedCredential(provider, new Set([id]), entries);
|
|
2999
|
+
this.#resetProviderAssignments(provider);
|
|
3000
|
+
this.#emitCredentialDisabled({ provider, disabledCause: cause });
|
|
3001
|
+
return true;
|
|
3002
|
+
}
|
|
3003
|
+
return false;
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
hasAuth(provider: string, sessionId?: string, options?: Pick<AuthApiKeyOptions, "owner">): boolean {
|
|
2702
3007
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2703
3008
|
try {
|
|
2704
|
-
this.#resolveSelectedStoredCredential(
|
|
3009
|
+
this.#resolveSelectedStoredCredential(
|
|
3010
|
+
storageProvider,
|
|
3011
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
3012
|
+
sessionId,
|
|
3013
|
+
);
|
|
2705
3014
|
} catch {
|
|
2706
3015
|
return false;
|
|
2707
3016
|
}
|
|
2708
|
-
return this.#hasConfiguredAuth(storageProvider);
|
|
3017
|
+
return this.#hasConfiguredAuth(storageProvider, options?.owner);
|
|
2709
3018
|
}
|
|
2710
3019
|
|
|
2711
3020
|
/**
|
|
@@ -2713,15 +3022,24 @@ export class AuthStorage {
|
|
|
2713
3022
|
* Mirrors getApiKey selector validation, overrides, session OAuth stickiness,
|
|
2714
3023
|
* cached command-key usability, OAuth retry, and environment fallback order.
|
|
2715
3024
|
*/
|
|
2716
|
-
getEffectiveCredentialType(
|
|
3025
|
+
getEffectiveCredentialType(
|
|
3026
|
+
provider: string,
|
|
3027
|
+
sessionId?: string,
|
|
3028
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
3029
|
+
): AuthCredential["type"] | undefined {
|
|
2717
3030
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2718
3031
|
let selected: ({ index: number } & StoredCredential) | undefined;
|
|
2719
3032
|
try {
|
|
2720
|
-
selected = this.#resolveSelectedStoredCredential(
|
|
3033
|
+
selected = this.#resolveSelectedStoredCredential(
|
|
3034
|
+
storageProvider,
|
|
3035
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
3036
|
+
sessionId,
|
|
3037
|
+
);
|
|
2721
3038
|
} catch {
|
|
2722
3039
|
return undefined;
|
|
2723
3040
|
}
|
|
2724
|
-
if (this.hasRuntimeApiKey(storageProvider) || this.#
|
|
3041
|
+
if (this.hasRuntimeApiKey(storageProvider) || this.#hasConfigOverride(storageProvider, options?.owner))
|
|
3042
|
+
return "api_key";
|
|
2725
3043
|
if (selected) return selected.credential.type;
|
|
2726
3044
|
|
|
2727
3045
|
const credentials = this.#getCredentialsForProvider(storageProvider);
|
|
@@ -2734,19 +3052,23 @@ export class AuthStorage {
|
|
|
2734
3052
|
}
|
|
2735
3053
|
if (credentials.some(credential => credential.type === "oauth")) return "oauth";
|
|
2736
3054
|
if (apiKeys.length > 0) return "api_key";
|
|
2737
|
-
if (getEnvApiKey(storageProvider) || this.#
|
|
3055
|
+
if (getEnvApiKey(storageProvider) || this.#resolveFallback(storageProvider, options?.owner)) return "api_key";
|
|
2738
3056
|
return undefined;
|
|
2739
3057
|
}
|
|
2740
3058
|
|
|
2741
3059
|
/**
|
|
2742
3060
|
* Check whether configured auth is currently usable without resolving credentials.
|
|
2743
3061
|
*/
|
|
2744
|
-
hasUsableAuth(provider: string): boolean {
|
|
3062
|
+
hasUsableAuth(provider: string, options?: Pick<AuthApiKeyOptions, "owner">): boolean {
|
|
2745
3063
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2746
3064
|
try {
|
|
2747
|
-
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
3065
|
+
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
3066
|
+
storageProvider,
|
|
3067
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
3068
|
+
undefined,
|
|
3069
|
+
);
|
|
2748
3070
|
if (this.hasRuntimeApiKey(storageProvider)) return true;
|
|
2749
|
-
if (this.#
|
|
3071
|
+
if (this.#hasConfigOverride(storageProvider, options?.owner)) return true;
|
|
2750
3072
|
if (selectedCredential) {
|
|
2751
3073
|
if (selectedCredential.credential.type === "api_key") {
|
|
2752
3074
|
return (
|
|
@@ -2787,7 +3109,7 @@ export class AuthStorage {
|
|
|
2787
3109
|
} catch {
|
|
2788
3110
|
return false;
|
|
2789
3111
|
}
|
|
2790
|
-
return Boolean(getEnvApiKey(storageProvider) || this.#
|
|
3112
|
+
return Boolean(getEnvApiKey(storageProvider) || this.#resolveFallback(storageProvider, options?.owner));
|
|
2791
3113
|
}
|
|
2792
3114
|
|
|
2793
3115
|
/**
|
|
@@ -2800,10 +3122,14 @@ export class AuthStorage {
|
|
|
2800
3122
|
/**
|
|
2801
3123
|
* Get OAuth credentials for a provider.
|
|
2802
3124
|
*/
|
|
2803
|
-
getOAuthCredential(
|
|
3125
|
+
getOAuthCredential(
|
|
3126
|
+
provider: string,
|
|
3127
|
+
sessionId?: string,
|
|
3128
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
3129
|
+
): OAuthCredential | undefined {
|
|
2804
3130
|
const selected = this.#resolveSelectedStoredCredential(
|
|
2805
3131
|
resolveOAuthStorageProvider(provider),
|
|
2806
|
-
undefined,
|
|
3132
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
2807
3133
|
sessionId,
|
|
2808
3134
|
);
|
|
2809
3135
|
if (selected?.credential.type === "oauth") return selected.credential;
|
|
@@ -2820,7 +3146,11 @@ export class AuthStorage {
|
|
|
2820
3146
|
* first call before any `getApiKey` has been issued, or single-credential setups).
|
|
2821
3147
|
* Returns `undefined` when no OAuth credential carries an `accountId`.
|
|
2822
3148
|
*/
|
|
2823
|
-
getOAuthAccountId(
|
|
3149
|
+
getOAuthAccountId(
|
|
3150
|
+
provider: string,
|
|
3151
|
+
sessionId?: string,
|
|
3152
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
3153
|
+
): string | undefined {
|
|
2824
3154
|
provider = resolveOAuthStorageProvider(provider);
|
|
2825
3155
|
const allCredentials = this.#getCredentialsForProvider(provider);
|
|
2826
3156
|
const oauthCredentials = allCredentials.filter((c): c is OAuthCredential => c.type === "oauth");
|
|
@@ -2828,11 +3158,15 @@ export class AuthStorage {
|
|
|
2828
3158
|
|
|
2829
3159
|
// Runtime / config overrides bypass OAuth account_uuid attribution — the
|
|
2830
3160
|
// caller is authenticating with an explicit key, not the broker's OAuth.
|
|
2831
|
-
if (this.#runtimeOverrides.has(provider) || this.#
|
|
3161
|
+
if (this.#runtimeOverrides.has(provider) || this.#hasConfigOverride(provider, options?.owner)) return undefined;
|
|
2832
3162
|
|
|
2833
3163
|
// Prefer the session-sticky credential when available.
|
|
2834
3164
|
|
|
2835
|
-
const scopedSelection = this.#resolveSelectedStoredCredential(
|
|
3165
|
+
const scopedSelection = this.#resolveSelectedStoredCredential(
|
|
3166
|
+
provider,
|
|
3167
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
3168
|
+
sessionId,
|
|
3169
|
+
);
|
|
2836
3170
|
if (scopedSelection?.credential.type === "api_key") return undefined;
|
|
2837
3171
|
if (scopedSelection?.credential.type === "oauth") {
|
|
2838
3172
|
const accountId = scopedSelection.credential.accountId;
|
|
@@ -2848,7 +3182,7 @@ export class AuthStorage {
|
|
|
2848
3182
|
// account_uuid injection would misattribute traffic. Only apply this guard when
|
|
2849
3183
|
// sessionPref is absent; a recorded OAuth sticky (sessionPref.type === "oauth") must
|
|
2850
3184
|
// NOT be blocked even if an env key also happens to exist.
|
|
2851
|
-
if (!sessionPref && (getEnvApiKey(provider) || this.#
|
|
3185
|
+
if (!sessionPref && (getEnvApiKey(provider) || this.#resolveFallback(provider, options?.owner))) return undefined;
|
|
2852
3186
|
// Resolve the sticky index against the full credential list — the index is
|
|
2853
3187
|
// recorded against the unfiltered provider array (by #recordSessionCredential /
|
|
2854
3188
|
// #tryOAuthCredential), not the OAuth-only subset, so dereferencing it into the
|
|
@@ -2973,6 +3307,16 @@ export class AuthStorage {
|
|
|
2973
3307
|
credentials = await loginKimi(ctrl);
|
|
2974
3308
|
break;
|
|
2975
3309
|
}
|
|
3310
|
+
case "kiro": {
|
|
3311
|
+
const { loginKiro } = await import("./utils/oauth/kiro");
|
|
3312
|
+
credentials = await loginKiro({
|
|
3313
|
+
onAuth: (url, instructions) => ctrl.onAuth({ url, instructions }),
|
|
3314
|
+
onPrompt: ctrl.onPrompt,
|
|
3315
|
+
onProgress: ctrl.onProgress,
|
|
3316
|
+
signal: ctrl.signal,
|
|
3317
|
+
});
|
|
3318
|
+
break;
|
|
3319
|
+
}
|
|
2976
3320
|
case "kilo": {
|
|
2977
3321
|
const { loginKilo } = await import("./utils/oauth/kilo");
|
|
2978
3322
|
credentials = await loginKilo(ctrl);
|
|
@@ -3004,6 +3348,12 @@ export class AuthStorage {
|
|
|
3004
3348
|
await saveApiKeyCredential(apiKey);
|
|
3005
3349
|
return;
|
|
3006
3350
|
}
|
|
3351
|
+
case "commandcode-goat": {
|
|
3352
|
+
const { loginCommandCode } = await import("./utils/oauth/commandcode");
|
|
3353
|
+
const apiKey = await loginCommandCode(ctrl);
|
|
3354
|
+
await saveApiKeyCredential(apiKey);
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3007
3357
|
case "lm-studio": {
|
|
3008
3358
|
const { loginLmStudio } = await import("./utils/oauth/lm-studio");
|
|
3009
3359
|
const apiKey = await loginLmStudio(ctrl);
|
|
@@ -3547,7 +3897,9 @@ export class AuthStorage {
|
|
|
3547
3897
|
}
|
|
3548
3898
|
return lastGood;
|
|
3549
3899
|
})().finally(() => {
|
|
3550
|
-
this.#usageRequestInFlight.
|
|
3900
|
+
if (this.#usageRequestInFlight.get(cacheKey) === promise) {
|
|
3901
|
+
this.#usageRequestInFlight.delete(cacheKey);
|
|
3902
|
+
}
|
|
3551
3903
|
});
|
|
3552
3904
|
|
|
3553
3905
|
this.#usageRequestInFlight.set(cacheKey, promise);
|
|
@@ -3555,6 +3907,7 @@ export class AuthStorage {
|
|
|
3555
3907
|
}
|
|
3556
3908
|
|
|
3557
3909
|
#collectUsageRequests(options?: {
|
|
3910
|
+
provider?: Provider;
|
|
3558
3911
|
baseUrlResolver?: (provider: Provider) => string | undefined;
|
|
3559
3912
|
}): UsageRequestDescriptor[] {
|
|
3560
3913
|
const resolver = this.#usageProviderResolver;
|
|
@@ -3568,6 +3921,7 @@ export class AuthStorage {
|
|
|
3568
3921
|
|
|
3569
3922
|
for (const providerId of providers) {
|
|
3570
3923
|
const provider = providerId as Provider;
|
|
3924
|
+
if (options?.provider && options.provider !== provider) continue;
|
|
3571
3925
|
const providerImpl = resolver(provider);
|
|
3572
3926
|
if (!providerImpl) continue;
|
|
3573
3927
|
const baseUrl = options?.baseUrlResolver?.(provider);
|
|
@@ -3767,6 +4121,7 @@ export class AuthStorage {
|
|
|
3767
4121
|
}
|
|
3768
4122
|
|
|
3769
4123
|
async fetchUsageReports(options?: {
|
|
4124
|
+
provider?: Provider;
|
|
3770
4125
|
baseUrlResolver?: (provider: Provider) => string | undefined;
|
|
3771
4126
|
/** Caller's cancel signal; only rejects this caller, never the shared upstream fetch. */
|
|
3772
4127
|
signal?: AbortSignal;
|
|
@@ -3777,6 +4132,15 @@ export class AuthStorage {
|
|
|
3777
4132
|
// `RemoteAuthCredentialStore` implements the store hook so a gateway
|
|
3778
4133
|
// backed by a broker automatically routes usage to the broker without
|
|
3779
4134
|
// needing the caller to wire it explicitly.
|
|
4135
|
+
const scopedStoreFetch = options?.provider
|
|
4136
|
+
? (this.#fetchUsageReportsForProviderOverride ?? this.#store.fetchUsageReportsForProvider?.bind(this.#store))
|
|
4137
|
+
: undefined;
|
|
4138
|
+
if (scopedStoreFetch && options?.provider) {
|
|
4139
|
+
return raceUsageWithSignal(scopedStoreFetch(options.provider), options.signal);
|
|
4140
|
+
}
|
|
4141
|
+
if (options?.provider && (this.#fetchUsageReportsOverride || this.#store.fetchUsageReports)) {
|
|
4142
|
+
throw new Error("Provider-scoped usage fetch is unavailable");
|
|
4143
|
+
}
|
|
3780
4144
|
const override = this.#fetchUsageReportsOverride ?? this.#store.fetchUsageReports?.bind(this.#store);
|
|
3781
4145
|
if (override) {
|
|
3782
4146
|
// Reuse the in-flight map so concurrent callers (widget poll + format
|
|
@@ -3789,7 +4153,9 @@ export class AuthStorage {
|
|
|
3789
4153
|
// Don't forward the caller signal into the shared fetch — first caller's
|
|
3790
4154
|
// abort would otherwise cancel the upstream for every peer.
|
|
3791
4155
|
shared = override().finally(() => {
|
|
3792
|
-
this.#usageReportsInFlight.
|
|
4156
|
+
if (this.#usageReportsInFlight.get(OVERRIDE_KEY) === shared) {
|
|
4157
|
+
this.#usageReportsInFlight.delete(OVERRIDE_KEY);
|
|
4158
|
+
}
|
|
3793
4159
|
});
|
|
3794
4160
|
this.#usageReportsInFlight.set(OVERRIDE_KEY, shared);
|
|
3795
4161
|
}
|
|
@@ -3858,7 +4224,9 @@ export class AuthStorage {
|
|
|
3858
4224
|
}
|
|
3859
4225
|
return resolved;
|
|
3860
4226
|
})().finally(() => {
|
|
3861
|
-
this.#usageReportsInFlight.
|
|
4227
|
+
if (this.#usageReportsInFlight.get(cacheKey) === promise) {
|
|
4228
|
+
this.#usageReportsInFlight.delete(cacheKey);
|
|
4229
|
+
}
|
|
3862
4230
|
});
|
|
3863
4231
|
|
|
3864
4232
|
this.#usageReportsInFlight.set(cacheKey, promise);
|
|
@@ -4192,9 +4560,11 @@ export class AuthStorage {
|
|
|
4192
4560
|
async markUsageLimitReached(
|
|
4193
4561
|
provider: string,
|
|
4194
4562
|
sessionId: string | undefined,
|
|
4195
|
-
options?: { retryAfterMs?: number; baseUrl?: string; signal?: AbortSignal },
|
|
4563
|
+
options?: { retryAfterMs?: number; baseUrl?: string; signal?: AbortSignal; owner?: object },
|
|
4196
4564
|
): Promise<boolean> {
|
|
4197
4565
|
provider = resolveOAuthStorageProvider(provider);
|
|
4566
|
+
const ownerOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
4567
|
+
if (ownerOverride && !ownerOverride.envSourced) return false;
|
|
4198
4568
|
const sessionCredential = this.#getSessionCredential(provider, sessionId);
|
|
4199
4569
|
if (!sessionCredential) return false;
|
|
4200
4570
|
|
|
@@ -4294,12 +4664,12 @@ export class AuthStorage {
|
|
|
4294
4664
|
providerKey: string;
|
|
4295
4665
|
provider: string;
|
|
4296
4666
|
order: number[];
|
|
4297
|
-
credentials:
|
|
4667
|
+
credentials: OAuthCredentialSelection[];
|
|
4298
4668
|
options?: AuthApiKeyOptions;
|
|
4299
4669
|
strategy: CredentialRankingStrategy;
|
|
4300
4670
|
}): Promise<
|
|
4301
4671
|
Array<{
|
|
4302
|
-
selection:
|
|
4672
|
+
selection: OAuthCredentialSelection;
|
|
4303
4673
|
usage: UsageReport | null;
|
|
4304
4674
|
usageChecked: boolean;
|
|
4305
4675
|
}>
|
|
@@ -4307,7 +4677,7 @@ export class AuthStorage {
|
|
|
4307
4677
|
const nowMs = Date.now();
|
|
4308
4678
|
const { strategy } = args;
|
|
4309
4679
|
const ranked: Array<{
|
|
4310
|
-
selection:
|
|
4680
|
+
selection: OAuthCredentialSelection;
|
|
4311
4681
|
usage: UsageReport | null;
|
|
4312
4682
|
usageChecked: boolean;
|
|
4313
4683
|
blocked: boolean;
|
|
@@ -4455,18 +4825,29 @@ export class AuthStorage {
|
|
|
4455
4825
|
return undefined;
|
|
4456
4826
|
}
|
|
4457
4827
|
const selectedCredential = this.#resolveSelectedStoredCredential(provider, options, sessionId);
|
|
4458
|
-
const selectedOAuthCredential =
|
|
4828
|
+
const selectedOAuthCredential: OAuthCredentialSelection | undefined =
|
|
4459
4829
|
selectedCredential?.credential.type === "oauth"
|
|
4460
|
-
? {
|
|
4830
|
+
? {
|
|
4831
|
+
id: selectedCredential.id,
|
|
4832
|
+
credential: selectedCredential.credential,
|
|
4833
|
+
index: selectedCredential.index,
|
|
4834
|
+
revision: selectedCredential.revision,
|
|
4835
|
+
}
|
|
4461
4836
|
: undefined;
|
|
4462
4837
|
if (selectedCredential && !selectedOAuthCredential) return undefined;
|
|
4463
4838
|
const credentials = selectedOAuthCredential
|
|
4464
4839
|
? [selectedOAuthCredential]
|
|
4465
|
-
: this.#
|
|
4466
|
-
.map((credential, index) => ({ credential, index }))
|
|
4840
|
+
: this.#getStoredCredentials(provider)
|
|
4467
4841
|
.filter(
|
|
4468
|
-
(entry): entry is { credential: OAuthCredential
|
|
4469
|
-
|
|
4842
|
+
(entry): entry is StoredCredential & { credential: OAuthCredential } =>
|
|
4843
|
+
entry.credential.type === "oauth",
|
|
4844
|
+
)
|
|
4845
|
+
.map((entry, index) => ({
|
|
4846
|
+
id: entry.id,
|
|
4847
|
+
credential: entry.credential,
|
|
4848
|
+
index,
|
|
4849
|
+
revision: entry.revision,
|
|
4850
|
+
}));
|
|
4470
4851
|
|
|
4471
4852
|
if (credentials.length === 0) return undefined;
|
|
4472
4853
|
|
|
@@ -4489,7 +4870,7 @@ export class AuthStorage {
|
|
|
4489
4870
|
? await this.#rankOAuthSelections({ providerKey, provider, order, credentials, options, strategy: strategy! })
|
|
4490
4871
|
: order
|
|
4491
4872
|
.map(idx => credentials[idx])
|
|
4492
|
-
.filter((selection): selection is
|
|
4873
|
+
.filter((selection): selection is OAuthCredentialSelection => Boolean(selection))
|
|
4493
4874
|
.map(selection => ({ selection, usage: null, usageChecked: false }));
|
|
4494
4875
|
|
|
4495
4876
|
// Soft `--prefer-credential` preference: reorder the preferred row to the
|
|
@@ -4499,7 +4880,11 @@ export class AuthStorage {
|
|
|
4499
4880
|
if (!selectedCredential) {
|
|
4500
4881
|
const preferredSelector = this.#getPreferredCredentialSelector(provider, options);
|
|
4501
4882
|
if (preferredSelector) {
|
|
4502
|
-
this.#assertPreferredCredentialSelectorUsable(
|
|
4883
|
+
this.#assertPreferredCredentialSelectorUsable(
|
|
4884
|
+
resolveOAuthStorageProvider(provider),
|
|
4885
|
+
preferredSelector,
|
|
4886
|
+
options?.owner,
|
|
4887
|
+
);
|
|
4503
4888
|
}
|
|
4504
4889
|
const preferredSelection = preferredSelector
|
|
4505
4890
|
? this.#findCredentialBySelector(provider, preferredSelector)
|
|
@@ -4531,14 +4916,15 @@ export class AuthStorage {
|
|
|
4531
4916
|
}
|
|
4532
4917
|
await Promise.all(
|
|
4533
4918
|
candidates.map(async candidate => {
|
|
4919
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, candidate.selection)) return;
|
|
4534
4920
|
if (Date.now() + OAUTH_REFRESH_SKEW_MS < candidate.selection.credential.expires) return;
|
|
4535
|
-
const latestCredential =
|
|
4921
|
+
const latestCredential = candidate.selection.credential;
|
|
4536
4922
|
if (latestCredential?.type === "oauth" && Date.now() + OAUTH_REFRESH_SKEW_MS < latestCredential.expires) {
|
|
4537
4923
|
candidate.selection.credential = latestCredential;
|
|
4538
4924
|
return;
|
|
4539
4925
|
}
|
|
4540
4926
|
try {
|
|
4541
|
-
const credentialId =
|
|
4927
|
+
const credentialId = candidate.selection.id;
|
|
4542
4928
|
const refreshedCredentials = await this.#refreshOAuthCredential(
|
|
4543
4929
|
provider,
|
|
4544
4930
|
candidate.selection.credential,
|
|
@@ -4550,14 +4936,18 @@ export class AuthStorage {
|
|
|
4550
4936
|
...refreshedCredentials,
|
|
4551
4937
|
type: "oauth",
|
|
4552
4938
|
};
|
|
4939
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, candidate.selection)) return;
|
|
4553
4940
|
candidate.selection.credential = updated;
|
|
4554
4941
|
this.#replaceCredentialAt(
|
|
4555
4942
|
provider,
|
|
4556
4943
|
candidate.selection.index,
|
|
4557
4944
|
updated,
|
|
4558
4945
|
!refreshedCredentials.persistedByLease,
|
|
4946
|
+
credentialId,
|
|
4559
4947
|
);
|
|
4560
|
-
} catch {
|
|
4948
|
+
} catch (error) {
|
|
4949
|
+
if (isSqliteError(error)) throw error;
|
|
4950
|
+
}
|
|
4561
4951
|
}),
|
|
4562
4952
|
);
|
|
4563
4953
|
|
|
@@ -4781,6 +5171,15 @@ export class AuthStorage {
|
|
|
4781
5171
|
}
|
|
4782
5172
|
try {
|
|
4783
5173
|
const refreshed = await Promise.race([refreshPromise, cancellation.promise]);
|
|
5174
|
+
let effectiveRefreshed = refreshed;
|
|
5175
|
+
if (this.#refreshOAuthCredentialOverride && this.#store.refreshSnapshot) {
|
|
5176
|
+
await this.#store.refreshSnapshot();
|
|
5177
|
+
const accepted = this.#store.listAuthCredentials(provider).find(row => row.id === credentialId)?.credential;
|
|
5178
|
+
if (accepted?.type !== "oauth") {
|
|
5179
|
+
throw new Error("Credential authority changed during refresh");
|
|
5180
|
+
}
|
|
5181
|
+
effectiveRefreshed = accepted;
|
|
5182
|
+
}
|
|
4784
5183
|
// Return the FULL authority of the effective credential: rotated
|
|
4785
5184
|
// tokens from upstream plus the identity metadata and MCP binding of
|
|
4786
5185
|
// the (possibly guard-adopted) credential that was actually
|
|
@@ -4789,12 +5188,12 @@ export class AuthStorage {
|
|
|
4789
5188
|
// next refresh token to the wrong endpoint — or relabel rotated
|
|
4790
5189
|
// tokens with stale identity.
|
|
4791
5190
|
const authority: RefreshedOAuthCredentials = {
|
|
4792
|
-
...
|
|
4793
|
-
accountId:
|
|
4794
|
-
email:
|
|
4795
|
-
projectId:
|
|
4796
|
-
enterpriseUrl:
|
|
4797
|
-
mcpBinding: (
|
|
5191
|
+
...effectiveRefreshed,
|
|
5192
|
+
accountId: effectiveRefreshed.accountId ?? credential.accountId,
|
|
5193
|
+
email: effectiveRefreshed.email ?? credential.email,
|
|
5194
|
+
projectId: effectiveRefreshed.projectId ?? credential.projectId,
|
|
5195
|
+
enterpriseUrl: effectiveRefreshed.enterpriseUrl ?? credential.enterpriseUrl,
|
|
5196
|
+
mcpBinding: (effectiveRefreshed as RefreshedOAuthCredentials).mcpBinding ?? credential.mcpBinding,
|
|
4798
5197
|
};
|
|
4799
5198
|
if (refreshLease) {
|
|
4800
5199
|
const completeLease = this.#store.completeOAuthRefreshLease?.bind(this.#store);
|
|
@@ -4823,15 +5222,28 @@ export class AuthStorage {
|
|
|
4823
5222
|
}
|
|
4824
5223
|
}
|
|
4825
5224
|
|
|
5225
|
+
#reconcileOAuthCredentialSelection(provider: string, selection: OAuthCredentialSelection): boolean {
|
|
5226
|
+
const entries = this.#getStoredCredentials(provider);
|
|
5227
|
+
const index = entries.findIndex(entry => entry.id === selection.id);
|
|
5228
|
+
if (index === -1) return false;
|
|
5229
|
+
const current = entries[index];
|
|
5230
|
+
if (current?.credential.type !== "oauth") return false;
|
|
5231
|
+
selection.index = index;
|
|
5232
|
+
selection.credential = current.credential;
|
|
5233
|
+
selection.revision = current.revision;
|
|
5234
|
+
return true;
|
|
5235
|
+
}
|
|
5236
|
+
|
|
4826
5237
|
async #prepareOAuthCredentialForRequest(
|
|
4827
5238
|
provider: string,
|
|
4828
|
-
selection:
|
|
5239
|
+
selection: OAuthCredentialSelection,
|
|
4829
5240
|
options: AuthApiKeyOptions | undefined,
|
|
4830
5241
|
): Promise<boolean> {
|
|
5242
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return false;
|
|
4831
5243
|
const prepare = this.#store.prepareForRequest?.bind(this.#store);
|
|
4832
5244
|
if (!prepare) return true;
|
|
4833
5245
|
const stored = this.#getStoredCredentials(provider);
|
|
4834
|
-
const selected = stored
|
|
5246
|
+
const selected = stored.find(entry => entry.id === selection.id);
|
|
4835
5247
|
if (selected?.credential.type !== "oauth") return false;
|
|
4836
5248
|
|
|
4837
5249
|
const prepared = await prepare(selected.id, { signal: options?.signal });
|
|
@@ -4839,21 +5251,22 @@ export class AuthStorage {
|
|
|
4839
5251
|
const latestRows = this.#store.listAuthCredentials(provider);
|
|
4840
5252
|
this.#setStoredCredentials(
|
|
4841
5253
|
provider,
|
|
4842
|
-
latestRows.map(row => ({ id: row.id, credential: row.credential })),
|
|
5254
|
+
latestRows.map(row => ({ id: row.id, credential: row.credential, revision: row.revision })),
|
|
4843
5255
|
);
|
|
4844
|
-
const latestIndex = latestRows.findIndex(row => row.id ===
|
|
5256
|
+
const latestIndex = latestRows.findIndex(row => row.id === selection.id);
|
|
4845
5257
|
if (latestIndex === -1) return false;
|
|
4846
5258
|
const latest = latestRows[latestIndex];
|
|
4847
5259
|
if (latest?.credential.type !== "oauth") return false;
|
|
4848
5260
|
selection.index = latestIndex;
|
|
4849
5261
|
selection.credential = latest.credential;
|
|
5262
|
+
selection.revision = latest.revision;
|
|
4850
5263
|
return true;
|
|
4851
5264
|
}
|
|
4852
5265
|
|
|
4853
5266
|
/** Attempts to use a single OAuth credential, checking usage and refreshing token. */
|
|
4854
5267
|
async #tryOAuthCredential(
|
|
4855
5268
|
provider: Provider,
|
|
4856
|
-
selection:
|
|
5269
|
+
selection: OAuthCredentialSelection,
|
|
4857
5270
|
providerKey: string,
|
|
4858
5271
|
sessionId: string | undefined,
|
|
4859
5272
|
options: AuthApiKeyOptions | undefined,
|
|
@@ -4873,6 +5286,7 @@ export class AuthStorage {
|
|
|
4873
5286
|
usagePrechecked = false,
|
|
4874
5287
|
enforceProRequirement,
|
|
4875
5288
|
} = usageOptions;
|
|
5289
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return undefined;
|
|
4876
5290
|
if (!allowBlocked && this.#isCredentialBlocked(providerKey, selection.index)) {
|
|
4877
5291
|
return undefined;
|
|
4878
5292
|
}
|
|
@@ -4912,6 +5326,8 @@ export class AuthStorage {
|
|
|
4912
5326
|
}
|
|
4913
5327
|
|
|
4914
5328
|
try {
|
|
5329
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return undefined;
|
|
5330
|
+
const selectionCredentialId = selection.id;
|
|
4915
5331
|
let result: { newCredentials: OAuthCredentials; apiKey: string } | null;
|
|
4916
5332
|
// The refresh result carries the effective (possibly guard-adopted)
|
|
4917
5333
|
// credential's binding; `updated` must persist it or the next refresh
|
|
@@ -4922,7 +5338,7 @@ export class AuthStorage {
|
|
|
4922
5338
|
const refreshedCredentials = await this.#refreshOAuthCredential(
|
|
4923
5339
|
provider,
|
|
4924
5340
|
selection.credential,
|
|
4925
|
-
|
|
5341
|
+
selectionCredentialId,
|
|
4926
5342
|
options?.signal,
|
|
4927
5343
|
);
|
|
4928
5344
|
refreshedAuthority = refreshedCredentials;
|
|
@@ -4939,7 +5355,7 @@ export class AuthStorage {
|
|
|
4939
5355
|
const refreshedCredentials = await this.#refreshOAuthCredential(
|
|
4940
5356
|
provider,
|
|
4941
5357
|
selection.credential,
|
|
4942
|
-
|
|
5358
|
+
selectionCredentialId,
|
|
4943
5359
|
options?.signal,
|
|
4944
5360
|
);
|
|
4945
5361
|
refreshedAuthority = refreshedCredentials;
|
|
@@ -4960,7 +5376,13 @@ export class AuthStorage {
|
|
|
4960
5376
|
enterpriseUrl: result.newCredentials.enterpriseUrl ?? selection.credential.enterpriseUrl,
|
|
4961
5377
|
mcpBinding: refreshedAuthority.mcpBinding,
|
|
4962
5378
|
};
|
|
4963
|
-
this.#replaceCredentialAt(
|
|
5379
|
+
this.#replaceCredentialAt(
|
|
5380
|
+
provider,
|
|
5381
|
+
selection.index,
|
|
5382
|
+
updated,
|
|
5383
|
+
!refreshedAuthority.persistedByLease,
|
|
5384
|
+
selectionCredentialId,
|
|
5385
|
+
);
|
|
4964
5386
|
|
|
4965
5387
|
if ((checkUsage && !allowBlocked) || requiresProModel) {
|
|
4966
5388
|
const sameAccount = selection.credential.accountId === updated.accountId;
|
|
@@ -4984,10 +5406,18 @@ export class AuthStorage {
|
|
|
4984
5406
|
return undefined;
|
|
4985
5407
|
}
|
|
4986
5408
|
}
|
|
5409
|
+
if (!this.#reconcileOAuthCredentialSelection(provider, selection)) return undefined;
|
|
5410
|
+
if (!authCredentialEquals(selection.credential, updated)) return undefined;
|
|
4987
5411
|
this.#recordSessionCredential(provider, sessionId, "oauth", selection.index);
|
|
4988
5412
|
return { apiKey: result.apiKey, credential: updated };
|
|
4989
5413
|
} catch (error) {
|
|
4990
|
-
|
|
5414
|
+
if (isSqliteError(error)) throw error;
|
|
5415
|
+
// Auth-broker errors retain the sanitized upstream body separately from
|
|
5416
|
+
// their transport message. Include that body for failure classification
|
|
5417
|
+
// (the broker's 500 envelope otherwise hides `invalid_grant`) while
|
|
5418
|
+
// preserving the original error object for callers and diagnostics.
|
|
5419
|
+
const brokerBody = readBrokerErrorBody(error);
|
|
5420
|
+
const errorMsg = [String(error), brokerBody].filter((part): part is string => Boolean(part)).join(" ");
|
|
4991
5421
|
// Peer-rotation recovery runs before ANY failure classification: a
|
|
4992
5422
|
// concurrent process may have rotated the refresh token, which
|
|
4993
5423
|
// invalidates the snapshot token we just attempted. Re-read the row —
|
|
@@ -5006,7 +5436,7 @@ export class AuthStorage {
|
|
|
5006
5436
|
// selection snapshot would misread that adoption as a fresh peer
|
|
5007
5437
|
// rotation and loop reload-retry instead of classifying the failure.
|
|
5008
5438
|
const attemptedRefreshToken = getAttemptedRefreshToken(error) ?? selection.credential.refresh;
|
|
5009
|
-
const attemptedCredentialId =
|
|
5439
|
+
const attemptedCredentialId = selection.id;
|
|
5010
5440
|
if (attemptedCredentialId !== undefined) {
|
|
5011
5441
|
const latestRow = this.#store.listAuthCredentials(provider).find(row => row.id === attemptedCredentialId);
|
|
5012
5442
|
const latestCredential = latestRow?.credential;
|
|
@@ -5062,6 +5492,7 @@ export class AuthStorage {
|
|
|
5062
5492
|
selection.index,
|
|
5063
5493
|
selection.credential,
|
|
5064
5494
|
`oauth refresh failed: ${errorMsg}`,
|
|
5495
|
+
attemptedCredentialId,
|
|
5065
5496
|
);
|
|
5066
5497
|
if (!disabled) {
|
|
5067
5498
|
// The CAS predicate compares the row's serialized `data`, so it also
|
|
@@ -5082,7 +5513,35 @@ export class AuthStorage {
|
|
|
5082
5513
|
index: selection.index,
|
|
5083
5514
|
credentialId: attemptedCredentialId,
|
|
5084
5515
|
});
|
|
5085
|
-
|
|
5516
|
+
const disabledCause = `oauth refresh failed: ${errorMsg}`;
|
|
5517
|
+
if (this.#store.disableAuthCredentialRemote) {
|
|
5518
|
+
try {
|
|
5519
|
+
const disabled = await this.#disableCredentialRemotely(
|
|
5520
|
+
provider,
|
|
5521
|
+
attemptedCredentialId,
|
|
5522
|
+
disabledCause,
|
|
5523
|
+
options?.signal,
|
|
5524
|
+
selection.revision,
|
|
5525
|
+
);
|
|
5526
|
+
if (!disabled) {
|
|
5527
|
+
await this.reload();
|
|
5528
|
+
return this.#resolveOAuthSelection(provider, sessionId, options, reloadsUsed + 1);
|
|
5529
|
+
}
|
|
5530
|
+
} catch (disableError) {
|
|
5531
|
+
// A failed broker mutation must not replace the provider's
|
|
5532
|
+
// original authentication failure. We also deliberately do
|
|
5533
|
+
// not remove the row locally: remote authority may still be
|
|
5534
|
+
// active and can only be changed by its broker.
|
|
5535
|
+
logger.warn("OAuth refresh remote disable failed", {
|
|
5536
|
+
provider,
|
|
5537
|
+
credentialId: attemptedCredentialId,
|
|
5538
|
+
error: String(disableError),
|
|
5539
|
+
});
|
|
5540
|
+
throw error;
|
|
5541
|
+
}
|
|
5542
|
+
} else {
|
|
5543
|
+
this.#disableCredentialById(provider, attemptedCredentialId, disabledCause);
|
|
5544
|
+
}
|
|
5086
5545
|
} else {
|
|
5087
5546
|
logger.debug("OAuth refresh disable lost CAS; reloading after peer rotation", {
|
|
5088
5547
|
provider,
|
|
@@ -5181,15 +5640,20 @@ export class AuthStorage {
|
|
|
5181
5640
|
* and get a best-effort token. For GitHub Copilot we preserve enterprise
|
|
5182
5641
|
* routing metadata so discovery can hit the correct host.
|
|
5183
5642
|
*/
|
|
5184
|
-
async peekApiKey(provider: string): Promise<string | undefined> {
|
|
5643
|
+
async peekApiKey(provider: string, options?: Pick<AuthApiKeyOptions, "owner">): Promise<string | undefined> {
|
|
5185
5644
|
provider = resolveOAuthStorageProvider(provider);
|
|
5186
5645
|
const runtimeKey = this.#runtimeOverrides.get(provider);
|
|
5187
5646
|
if (runtimeKey) return runtimeKey;
|
|
5188
5647
|
|
|
5189
|
-
const
|
|
5190
|
-
|
|
5648
|
+
const configOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
5649
|
+
const configKey = configOverride?.apiKey;
|
|
5650
|
+
if (configKey && !configOverride?.envSourced) return configKey;
|
|
5191
5651
|
|
|
5192
|
-
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
5652
|
+
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
5653
|
+
provider,
|
|
5654
|
+
options?.owner ? { owner: options.owner } : undefined,
|
|
5655
|
+
undefined,
|
|
5656
|
+
);
|
|
5193
5657
|
if (configKey) {
|
|
5194
5658
|
// Env-sourced (`apiKeyEnv`) override: same precedence as getApiKey —
|
|
5195
5659
|
// a stored api_key credential from `auth login` wins, stored OAuth
|
|
@@ -5239,7 +5703,7 @@ export class AuthStorage {
|
|
|
5239
5703
|
}
|
|
5240
5704
|
}
|
|
5241
5705
|
|
|
5242
|
-
return getEnvApiKey(provider) || this.#
|
|
5706
|
+
return getEnvApiKey(provider) || this.#resolveFallback(provider, options?.owner);
|
|
5243
5707
|
}
|
|
5244
5708
|
|
|
5245
5709
|
/**
|
|
@@ -5270,9 +5734,10 @@ export class AuthStorage {
|
|
|
5270
5734
|
// (e.g. an auth-gateway) and supplied the bearer for that endpoint —
|
|
5271
5735
|
// honor it instead of forwarding an upstream OAuth token that the proxy
|
|
5272
5736
|
// won't accept.
|
|
5273
|
-
const
|
|
5737
|
+
const configOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
5738
|
+
const configKey = configOverride?.apiKey;
|
|
5274
5739
|
if (configKey) {
|
|
5275
|
-
if (!
|
|
5740
|
+
if (!configOverride?.envSourced) return configKey;
|
|
5276
5741
|
// The override is an `apiKeyEnv` indirection, not a pinned value. A
|
|
5277
5742
|
// stored api_key credential from `auth login` is actively managed
|
|
5278
5743
|
// (validated at login, rotated on 401), while the pointed-to env
|
|
@@ -5334,7 +5799,7 @@ export class AuthStorage {
|
|
|
5334
5799
|
if (sessionId) this.#sessionLastCredential.get(provider)?.delete(sessionId);
|
|
5335
5800
|
const envKey = getEnvApiKey(provider);
|
|
5336
5801
|
if (envKey) return envKey;
|
|
5337
|
-
return this.#
|
|
5802
|
+
return this.#resolveFallback(provider, options?.owner) ?? undefined;
|
|
5338
5803
|
}
|
|
5339
5804
|
|
|
5340
5805
|
/**
|
|
@@ -5393,7 +5858,7 @@ export class AuthStorage {
|
|
|
5393
5858
|
// Runtime / config overrides intentionally short-circuit OAuth: when the
|
|
5394
5859
|
// user has pinned an API key, they expect the OAuth identity to be
|
|
5395
5860
|
// suppressed (same contract as `getOAuthAccountId`).
|
|
5396
|
-
if (this.#runtimeOverrides.has(provider) || this.#
|
|
5861
|
+
if (this.#runtimeOverrides.has(provider) || this.#hasConfigOverride(provider, options?.owner)) {
|
|
5397
5862
|
return undefined;
|
|
5398
5863
|
}
|
|
5399
5864
|
const resolved = await this.#resolveOAuthSelection(provider, sessionId, options);
|
|
@@ -5409,13 +5874,7 @@ export class AuthStorage {
|
|
|
5409
5874
|
}
|
|
5410
5875
|
|
|
5411
5876
|
#extractStructuredApiKeyToken(apiKey: string): string | undefined {
|
|
5412
|
-
|
|
5413
|
-
try {
|
|
5414
|
-
const parsed = JSON.parse(apiKey) as { token?: unknown };
|
|
5415
|
-
return typeof parsed.token === "string" ? parsed.token : undefined;
|
|
5416
|
-
} catch {
|
|
5417
|
-
return undefined;
|
|
5418
|
-
}
|
|
5877
|
+
return extractStructuredApiKeyToken(apiKey);
|
|
5419
5878
|
}
|
|
5420
5879
|
|
|
5421
5880
|
async #credentialMatchesApiKey(provider: string, credential: AuthCredential, apiKey: string): Promise<boolean> {
|
|
@@ -5445,6 +5904,9 @@ export class AuthStorage {
|
|
|
5445
5904
|
const signal = isAbortSignalOption(optionsOrSignal) ? optionsOrSignal : optionsOrSignal?.signal;
|
|
5446
5905
|
const sessionId = isAbortSignalOption(optionsOrSignal) ? undefined : optionsOrSignal?.sessionId;
|
|
5447
5906
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
5907
|
+
const owner = isAbortSignalOption(optionsOrSignal) ? undefined : optionsOrSignal?.owner;
|
|
5908
|
+
const ownerOverride = this.#configOverrideRegistration(storageProvider, owner);
|
|
5909
|
+
if (ownerOverride && !ownerOverride.envSourced && ownerOverride.apiKey === apiKey) return false;
|
|
5448
5910
|
const stored = this.#getStoredCredentials(storageProvider);
|
|
5449
5911
|
let matched: { id: number; type: AuthCredential["type"]; index: number } | undefined;
|
|
5450
5912
|
for (let index = 0; index < stored.length; index++) {
|
|
@@ -5477,7 +5939,7 @@ export class AuthStorage {
|
|
|
5477
5939
|
const latestRows = this.#store.listAuthCredentials(storageProvider);
|
|
5478
5940
|
this.#setStoredCredentials(
|
|
5479
5941
|
storageProvider,
|
|
5480
|
-
latestRows.map(row => ({ id: row.id, credential: row.credential })),
|
|
5942
|
+
latestRows.map(row => ({ id: row.id, credential: row.credential, revision: row.revision })),
|
|
5481
5943
|
);
|
|
5482
5944
|
return true;
|
|
5483
5945
|
}
|
|
@@ -5504,6 +5966,7 @@ export class AuthStorage {
|
|
|
5504
5966
|
provider,
|
|
5505
5967
|
credential: redacted,
|
|
5506
5968
|
identityKey: resolveCredentialIdentityKey(provider, credential),
|
|
5969
|
+
...(entry.revision === undefined ? {} : { revision: entry.revision }),
|
|
5507
5970
|
});
|
|
5508
5971
|
}
|
|
5509
5972
|
}
|
|
@@ -5639,12 +6102,14 @@ export class AuthStorage {
|
|
|
5639
6102
|
enterpriseUrl: refreshed.enterpriseUrl ?? target.credential.enterpriseUrl,
|
|
5640
6103
|
mcpBinding: refreshed.mcpBinding,
|
|
5641
6104
|
};
|
|
5642
|
-
this.#replaceCredentialAt(provider, index, updated, !refreshed.persistedByLease);
|
|
6105
|
+
this.#replaceCredentialAt(provider, index, updated, !refreshed.persistedByLease, id);
|
|
6106
|
+
const persisted = this.#store.listAuthCredentials(provider).find(entry => entry.id === id);
|
|
5643
6107
|
return {
|
|
5644
6108
|
id,
|
|
5645
6109
|
provider,
|
|
5646
6110
|
credential: { ...updated, refresh: REMOTE_REFRESH_SENTINEL },
|
|
5647
6111
|
identityKey: resolveCredentialIdentityKey(provider, updated),
|
|
6112
|
+
...(persisted?.revision === undefined ? {} : { revision: persisted.revision }),
|
|
5648
6113
|
};
|
|
5649
6114
|
}
|
|
5650
6115
|
throw new Error(`No credential with id=${id}`);
|
|
@@ -5684,7 +6149,7 @@ export class AuthStorage {
|
|
|
5684
6149
|
const stored = this.#store.upsertAuthCredentialForProvider(provider, credential);
|
|
5685
6150
|
this.#setStoredCredentials(
|
|
5686
6151
|
provider,
|
|
5687
|
-
stored.map(entry => ({ id: entry.id, credential: entry.credential })),
|
|
6152
|
+
stored.map(entry => ({ id: entry.id, credential: entry.credential, revision: entry.revision })),
|
|
5688
6153
|
);
|
|
5689
6154
|
this.#resetProviderAssignments(provider);
|
|
5690
6155
|
return this.#toSnapshotEntries(provider, stored);
|
|
@@ -5704,15 +6169,21 @@ export class AuthStorage {
|
|
|
5704
6169
|
*
|
|
5705
6170
|
* The string is purely informational; consumers must not parse it.
|
|
5706
6171
|
*/
|
|
5707
|
-
describeCredentialSource(
|
|
6172
|
+
describeCredentialSource(
|
|
6173
|
+
provider: string,
|
|
6174
|
+
sessionId?: string,
|
|
6175
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
6176
|
+
): string | undefined {
|
|
6177
|
+
provider = resolveOAuthStorageProvider(provider);
|
|
5708
6178
|
if (this.#runtimeOverrides.has(provider)) {
|
|
5709
6179
|
return "runtime override (--api-key)";
|
|
5710
6180
|
}
|
|
5711
|
-
|
|
6181
|
+
const configOverride = this.#configOverrideRegistration(provider, options?.owner);
|
|
6182
|
+
if (configOverride) {
|
|
5712
6183
|
// An `apiKeyEnv` indirection loses to a stored api_key credential
|
|
5713
6184
|
// (see getApiKey); describe the credential that actually wins.
|
|
5714
6185
|
const shadowed = this.#getStoredCredentials(provider).some(entry => entry.credential.type === "api_key");
|
|
5715
|
-
if (!
|
|
6186
|
+
if (!configOverride.envSourced || !shadowed) {
|
|
5716
6187
|
return "config override (models.yml)";
|
|
5717
6188
|
}
|
|
5718
6189
|
}
|
|
@@ -5721,7 +6192,7 @@ export class AuthStorage {
|
|
|
5721
6192
|
const stored = this.#getStoredCredentials(provider);
|
|
5722
6193
|
if (stored.length === 0) {
|
|
5723
6194
|
if (getEnvApiKey(provider)) return `env ${baseLabel ? `(fallback over ${baseLabel})` : ""}`.trim();
|
|
5724
|
-
if (this.#
|
|
6195
|
+
if (this.#resolveFallback(provider, options?.owner) !== undefined) return `fallback resolver`;
|
|
5725
6196
|
return undefined;
|
|
5726
6197
|
}
|
|
5727
6198
|
|
|
@@ -5955,6 +6426,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5955
6426
|
#updateStmt: Statement;
|
|
5956
6427
|
#deleteStmt: Statement;
|
|
5957
6428
|
#deleteIfMatchesStmt: Statement;
|
|
6429
|
+
#deleteIfRevisionStmt: Statement;
|
|
5958
6430
|
#deleteByProviderStmt: Statement;
|
|
5959
6431
|
#hardDeleteStmt: Statement;
|
|
5960
6432
|
#getCacheStmt: Statement;
|
|
@@ -5995,6 +6467,9 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5995
6467
|
this.#deleteIfMatchesStmt = this.#db.prepare(
|
|
5996
6468
|
`UPDATE auth_credentials SET disabled_cause = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ? AND data = ? AND disabled_cause IS NULL`,
|
|
5997
6469
|
);
|
|
6470
|
+
this.#deleteIfRevisionStmt = this.#db.prepare(
|
|
6471
|
+
`UPDATE auth_credentials SET disabled_cause = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ? AND revision = ? AND disabled_cause IS NULL`,
|
|
6472
|
+
);
|
|
5998
6473
|
this.#deleteByProviderStmt = this.#db.prepare(
|
|
5999
6474
|
`UPDATE auth_credentials SET disabled_cause = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE provider = ? AND disabled_cause IS NULL`,
|
|
6000
6475
|
);
|
|
@@ -6431,6 +6906,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6431
6906
|
id: row.id,
|
|
6432
6907
|
credential: deserializeCredential(row),
|
|
6433
6908
|
identityKey: resolveRowCredentialIdentityKey(providerName, row),
|
|
6909
|
+
revision: row.revision,
|
|
6434
6910
|
}));
|
|
6435
6911
|
|
|
6436
6912
|
const result: StoredAuthCredential[] = [];
|
|
@@ -6447,7 +6923,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6447
6923
|
if (match) {
|
|
6448
6924
|
matchedExistingIds.add(match.id);
|
|
6449
6925
|
this.#updateStmt.run(serialized.credentialType, serialized.data, serialized.identityKey, match.id);
|
|
6450
|
-
result.push({
|
|
6926
|
+
result.push({
|
|
6927
|
+
id: match.id,
|
|
6928
|
+
provider: providerName,
|
|
6929
|
+
credential,
|
|
6930
|
+
disabledCause: null,
|
|
6931
|
+
revision: match.revision + 1,
|
|
6932
|
+
});
|
|
6451
6933
|
} else {
|
|
6452
6934
|
const row = this.#insertStmt.get(
|
|
6453
6935
|
providerName,
|
|
@@ -6456,7 +6938,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6456
6938
|
serialized.identityKey,
|
|
6457
6939
|
) as { id?: number } | undefined;
|
|
6458
6940
|
if (row?.id) {
|
|
6459
|
-
result.push({ id: row.id, provider: providerName, credential, disabledCause: null });
|
|
6941
|
+
result.push({ id: row.id, provider: providerName, credential, disabledCause: null, revision: 1 });
|
|
6460
6942
|
}
|
|
6461
6943
|
}
|
|
6462
6944
|
}
|
|
@@ -6549,6 +7031,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6549
7031
|
id: number;
|
|
6550
7032
|
credential: AuthCredential;
|
|
6551
7033
|
identityKey: string | null;
|
|
7034
|
+
revision: number;
|
|
6552
7035
|
}> = [];
|
|
6553
7036
|
for (const row of existingRows) {
|
|
6554
7037
|
const activeCredential = deserializeCredential(row);
|
|
@@ -6557,6 +7040,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6557
7040
|
id: row.id,
|
|
6558
7041
|
credential: activeCredential,
|
|
6559
7042
|
identityKey: resolveRowCredentialIdentityKey(providerName, row),
|
|
7043
|
+
revision: row.revision,
|
|
6560
7044
|
});
|
|
6561
7045
|
}
|
|
6562
7046
|
if (existing.length > 0) {
|
|
@@ -6589,6 +7073,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6589
7073
|
provider: providerName,
|
|
6590
7074
|
credential: row.credential,
|
|
6591
7075
|
disabledCause: null,
|
|
7076
|
+
revision: row.revision,
|
|
6592
7077
|
})),
|
|
6593
7078
|
};
|
|
6594
7079
|
}
|
|
@@ -6635,19 +7120,15 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6635
7120
|
}
|
|
6636
7121
|
|
|
6637
7122
|
updateAuthCredential(id: number, credential: AuthCredential): void {
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
this.#purgeSupersededDisabledRows(provider, this.listAuthCredentials(provider));
|
|
6648
|
-
}
|
|
6649
|
-
} catch {
|
|
6650
|
-
// Ignore update failures
|
|
7123
|
+
const providerRow = this.#db.prepare("SELECT provider FROM auth_credentials WHERE id = ?").get(id) as
|
|
7124
|
+
| { provider?: string }
|
|
7125
|
+
| undefined;
|
|
7126
|
+
const provider = providerRow?.provider ?? "";
|
|
7127
|
+
const serialized = serializeCredential(provider, credential);
|
|
7128
|
+
if (!serialized) return;
|
|
7129
|
+
this.#updateStmt.run(serialized.credentialType, serialized.data, serialized.identityKey, id);
|
|
7130
|
+
if (provider) {
|
|
7131
|
+
this.#purgeSupersededDisabledRows(provider, this.listAuthCredentials(provider));
|
|
6651
7132
|
}
|
|
6652
7133
|
}
|
|
6653
7134
|
|
|
@@ -6676,6 +7157,17 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6676
7157
|
}
|
|
6677
7158
|
}
|
|
6678
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
|
+
|
|
6679
7171
|
deleteAuthCredentialsForProvider(provider: string, disabledCause: string): void {
|
|
6680
7172
|
try {
|
|
6681
7173
|
this.#deleteByProviderStmt.run(normalizeDisabledCause(disabledCause), provider);
|
|
@@ -6702,6 +7194,17 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6702
7194
|
}
|
|
6703
7195
|
}
|
|
6704
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
|
+
|
|
6705
7208
|
deleteCachePrefix(prefix: string): void {
|
|
6706
7209
|
if (prefix.length === 0) return;
|
|
6707
7210
|
try {
|
|
@@ -6796,6 +7299,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
6796
7299
|
this.#updateStmt.finalize();
|
|
6797
7300
|
this.#deleteStmt.finalize();
|
|
6798
7301
|
this.#deleteIfMatchesStmt.finalize();
|
|
7302
|
+
this.#deleteIfRevisionStmt.finalize();
|
|
6799
7303
|
this.#deleteByProviderStmt.finalize();
|
|
6800
7304
|
this.#hardDeleteStmt.finalize();
|
|
6801
7305
|
this.#getCacheStmt.finalize();
|