@gajae-code/ai 0.18.11 → 0.18.12
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 +68 -0
- package/README.md +1 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
- package/dist/types/auth-storage.d.ts +80 -8
- package/dist/types/bedrock-claude-cache-policy.d.ts +1 -1
- package/dist/types/model-cache.d.ts +6 -4
- package/dist/types/model-manager.d.ts +2 -0
- package/dist/types/openai-completions-compat.d.ts +2 -1
- package/dist/types/providers/anthropic.d.ts +10 -0
- package/dist/types/providers/aws-credential-config.d.ts +5 -0
- package/dist/types/providers/google-auth.d.ts +7 -0
- package/dist/types/providers/google-gemini-headers.d.ts +10 -5
- package/dist/types/providers/kiro-api-key.d.ts +5 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
- package/dist/types/providers/openai-completions.d.ts +1 -1
- package/dist/types/providers/openai-responses-shared.d.ts +2 -1
- package/dist/types/stream.d.ts +33 -1
- package/dist/types/types.d.ts +26 -3
- package/dist/types/usage.d.ts +3 -0
- package/dist/types/utils/discovery/antigravity.d.ts +11 -0
- package/dist/types/utils/idle-iterator.d.ts +3 -2
- package/dist/types/utils/oauth/types.d.ts +9 -0
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +62 -11
- package/src/auth-broker/wire-schemas.ts +2 -0
- package/src/auth-gateway/server.ts +3 -2
- package/src/auth-storage.ts +647 -194
- package/src/bedrock-claude-cache-policy.d.ts +1 -1
- package/src/bedrock-claude-cache-policy.ts +2 -2
- package/src/model-cache.ts +45 -10
- package/src/model-manager.ts +18 -6
- package/src/model-thinking.ts +37 -7
- package/src/models.json +2931 -1670
- package/src/openai-completions-compat.d.ts +2 -1
- package/src/openai-completions-compat.ts +5 -0
- package/src/provider-models/google.ts +7 -1
- package/src/provider-models/openai-compat.ts +26 -0
- package/src/providers/amazon-bedrock.ts +20 -11
- package/src/providers/anthropic.d.ts +10 -0
- package/src/providers/anthropic.ts +139 -4
- package/src/providers/aws-credential-config.d.ts +5 -0
- package/src/providers/aws-credential-config.ts +29 -1
- package/src/providers/google-auth.d.ts +7 -0
- package/src/providers/google-auth.ts +165 -4
- package/src/providers/google-gemini-headers.d.ts +10 -5
- package/src/providers/google-gemini-headers.ts +11 -6
- package/src/providers/google-vertex.ts +2 -4
- package/src/providers/kiro-api-key.d.ts +5 -0
- package/src/providers/kiro-api-key.ts +379 -68
- package/src/providers/ollama.ts +19 -9
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-chat-server-schema.ts +1 -1
- package/src/providers/openai-chat-server.ts +14 -2
- package/src/providers/openai-codex-responses.ts +21 -14
- package/src/providers/openai-completions.d.ts +1 -1
- package/src/providers/openai-completions.ts +65 -4
- package/src/providers/openai-responses-server.ts +10 -1
- package/src/providers/openai-responses-shared.d.ts +2 -1
- package/src/providers/openai-responses-shared.ts +42 -1
- package/src/providers/openai-responses.ts +51 -10
- package/src/providers/pi-native-server.ts +1 -1
- package/src/stream.d.ts +33 -1
- package/src/stream.ts +107 -13
- package/src/types.d.ts +26 -3
- package/src/types.ts +55 -2
- package/src/usage/claude.ts +4 -2
- package/src/usage.ts +3 -0
- package/src/utils/discovery/antigravity.ts +144 -2
- package/src/utils/idle-iterator.d.ts +3 -2
- package/src/utils/idle-iterator.ts +30 -10
- package/src/utils/oauth/anthropic.ts +23 -22
- package/src/utils/oauth/api-key-validation.ts +34 -11
- package/src/utils/oauth/types.d.ts +9 -0
- package/src/utils/oauth/types.ts +9 -0
- package/src/utils/parse-bind.d.ts +35 -0
package/src/auth-storage.ts
CHANGED
|
@@ -237,6 +237,8 @@ export interface CredentialInventoryRecord {
|
|
|
237
237
|
accountId?: string;
|
|
238
238
|
email?: string;
|
|
239
239
|
projectId?: string;
|
|
240
|
+
organizationId?: string;
|
|
241
|
+
organizationName?: string;
|
|
240
242
|
disabled: boolean;
|
|
241
243
|
disabledCause: string | null;
|
|
242
244
|
}
|
|
@@ -696,6 +698,26 @@ export interface CredentialDisabledEvent {
|
|
|
696
698
|
*/
|
|
697
699
|
export type CredentialRankingMode = "balanced" | "earliest-reset";
|
|
698
700
|
|
|
701
|
+
/**
|
|
702
|
+
* One entry of a provider's ordered, persisted soft preference list.
|
|
703
|
+
*
|
|
704
|
+
* Entries are consulted in list order when a credential is selected. Usable
|
|
705
|
+
* entries under their cap are tried first, in list order; the remaining
|
|
706
|
+
* credentials follow in the normal ranking order. An entry whose most-used
|
|
707
|
+
* known usage window has reached `maxUsedFraction` is demoted behind every
|
|
708
|
+
* other credential, so it is used only when nothing else is available. A
|
|
709
|
+
* blocked or missing entry is skipped. Unknown usage never counts as over the
|
|
710
|
+
* cap.
|
|
711
|
+
*
|
|
712
|
+
* Runtime selectors (`--credential`, `--prefer-credential`) and session
|
|
713
|
+
* stickiness take precedence over configured preferences.
|
|
714
|
+
*/
|
|
715
|
+
export interface ConfiguredCredentialPreference {
|
|
716
|
+
selector: AuthCredentialSelector;
|
|
717
|
+
/** Used fraction in (0, 1] at which this entry stops being preferred. Absent means only blocking moves past it. */
|
|
718
|
+
maxUsedFraction?: number;
|
|
719
|
+
}
|
|
720
|
+
|
|
699
721
|
/**
|
|
700
722
|
* Whether credential selection may probe provider usage endpoints.
|
|
701
723
|
*
|
|
@@ -712,6 +734,8 @@ export type AuthStorageOptions = {
|
|
|
712
734
|
usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
|
|
713
735
|
rankingStrategyResolver?: (provider: Provider) => CredentialRankingStrategy | undefined;
|
|
714
736
|
credentialRankingMode?: CredentialRankingMode;
|
|
737
|
+
/** Persisted ordered soft preferences keyed by provider id. */
|
|
738
|
+
credentialPreferences?: Readonly<Record<string, readonly ConfiguredCredentialPreference[]>>;
|
|
715
739
|
usageFetch?: typeof fetch;
|
|
716
740
|
usageRequestTimeoutMs?: number;
|
|
717
741
|
usageLogger?: UsageLogger;
|
|
@@ -1129,13 +1153,63 @@ export type AuthApiKeyOptions = {
|
|
|
1129
1153
|
/** Prefer one stored OAuth credential while preserving quota-triggered fallback. */
|
|
1130
1154
|
preferredCredentialSelector?: AuthCredentialSelector;
|
|
1131
1155
|
};
|
|
1132
|
-
export type AuthCredentialSelectorKind = "id" | "email" | "account" | "project";
|
|
1156
|
+
export type AuthCredentialSelectorKind = "id" | "email" | "account" | "project" | "org";
|
|
1133
1157
|
|
|
1134
1158
|
export interface AuthCredentialSelector {
|
|
1135
1159
|
kind: AuthCredentialSelectorKind;
|
|
1136
1160
|
value: string;
|
|
1137
1161
|
}
|
|
1138
1162
|
|
|
1163
|
+
/**
|
|
1164
|
+
* Whether an OAuth credential belongs to the organization named by an `org:`
|
|
1165
|
+
* selector value: the organization id, or its display name (case-insensitive).
|
|
1166
|
+
*/
|
|
1167
|
+
export function oauthCredentialMatchesOrganization(
|
|
1168
|
+
credential: Pick<OAuthCredentials, "organizationId" | "organizationName">,
|
|
1169
|
+
value: string,
|
|
1170
|
+
): boolean {
|
|
1171
|
+
const wanted = value.trim().toLowerCase();
|
|
1172
|
+
if (wanted.length === 0) return false;
|
|
1173
|
+
return (
|
|
1174
|
+
credential.organizationId?.trim().toLowerCase() === wanted ||
|
|
1175
|
+
credential.organizationName?.trim().toLowerCase() === wanted
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* The items an `org:` selector designates. An exact organization-id match wins
|
|
1181
|
+
* over display-name matches, because names are not unique: two organizations
|
|
1182
|
+
* can share a name, and a name can equal another organization's id. Callers
|
|
1183
|
+
* must treat more than one result as ambiguous rather than picking one.
|
|
1184
|
+
*/
|
|
1185
|
+
export function selectOrganizationMatches<T>(
|
|
1186
|
+
items: readonly T[],
|
|
1187
|
+
value: string,
|
|
1188
|
+
organizationOf: (item: T) => Pick<OAuthCredentials, "organizationId" | "organizationName"> | undefined,
|
|
1189
|
+
): T[] {
|
|
1190
|
+
const wanted = value.trim().toLowerCase();
|
|
1191
|
+
const matches = items.filter(item => {
|
|
1192
|
+
const organization = organizationOf(item);
|
|
1193
|
+
return organization !== undefined && oauthCredentialMatchesOrganization(organization, wanted);
|
|
1194
|
+
});
|
|
1195
|
+
const exact = matches.filter(item => organizationOf(item)?.organizationId?.trim().toLowerCase() === wanted);
|
|
1196
|
+
return exact.length > 0 ? exact : matches;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Human-facing identity of a stored OAuth credential. The organization name is
|
|
1201
|
+
* appended because one account (same email) can hold one credential per
|
|
1202
|
+
* organization.
|
|
1203
|
+
*/
|
|
1204
|
+
export function formatOAuthIdentityLabel(
|
|
1205
|
+
credential: Pick<OAuthCredentials, "email" | "accountId" | "projectId" | "organizationName">,
|
|
1206
|
+
): string | null {
|
|
1207
|
+
const base = credential.email ?? credential.accountId ?? credential.projectId ?? null;
|
|
1208
|
+
const organization = credential.organizationName?.trim();
|
|
1209
|
+
if (!organization) return base;
|
|
1210
|
+
return base ? `${base} (${organization})` : organization;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1139
1213
|
type OAuthResolutionResult = { apiKey: string; credential: OAuthCredential };
|
|
1140
1214
|
|
|
1141
1215
|
/**
|
|
@@ -1296,6 +1370,8 @@ function oauthNonTokenFieldsEqual(left: OAuthCredential, right: OAuthCredential)
|
|
|
1296
1370
|
left.projectId === right.projectId &&
|
|
1297
1371
|
left.enterpriseUrl === right.enterpriseUrl &&
|
|
1298
1372
|
left.profileArn === right.profileArn &&
|
|
1373
|
+
left.organizationId === right.organizationId &&
|
|
1374
|
+
left.organizationName === right.organizationName &&
|
|
1299
1375
|
left.mcpBinding?.resourceOrigin === right.mcpBinding?.resourceOrigin &&
|
|
1300
1376
|
left.mcpBinding?.tokenEndpoint === right.mcpBinding?.tokenEndpoint
|
|
1301
1377
|
);
|
|
@@ -1345,6 +1421,7 @@ function resolveMemoizedOAuthIdentityKey(provider: string, credential: OAuthCred
|
|
|
1345
1421
|
credential.projectId ?? "",
|
|
1346
1422
|
credential.enterpriseUrl ?? "",
|
|
1347
1423
|
credential.profileArn ?? "",
|
|
1424
|
+
credential.organizationId ?? "",
|
|
1348
1425
|
credential.access,
|
|
1349
1426
|
credential.refresh,
|
|
1350
1427
|
].join("\u0000");
|
|
@@ -1472,6 +1549,10 @@ export class AuthStorage {
|
|
|
1472
1549
|
#runtimeCredentialSelectors: Map<string, AuthCredentialSelector> = new Map();
|
|
1473
1550
|
/** Soft runtime credential preference per provider; quota failures may rotate away from it. */
|
|
1474
1551
|
#runtimePreferredCredentialSelectors: Map<string, AuthCredentialSelector> = new Map();
|
|
1552
|
+
/** Persisted ordered soft credential preferences per provider; usage caps and quota failures move past entries. */
|
|
1553
|
+
#configuredCredentialPreferences: Map<string, readonly ConfiguredCredentialPreference[]> = new Map();
|
|
1554
|
+
/** Providers already warned that their preference list names one stored row twice. */
|
|
1555
|
+
#warnedDuplicateCredentialPreferences = new Set<string>();
|
|
1475
1556
|
/** Credential selectors explicitly attached to a credential session scope. */
|
|
1476
1557
|
#sessionCredentialSelectors: Map<string, Map<string, AuthCredentialSelector>> = new Map();
|
|
1477
1558
|
/** Explicit AUTO masks suppress both scoped and process-global selectors for a scope/provider. */
|
|
@@ -1562,6 +1643,11 @@ export class AuthStorage {
|
|
|
1562
1643
|
: DEFAULT_USAGE_REQUEST_TIMEOUT_MS;
|
|
1563
1644
|
this.#usageFetchLeaseMs = usageTimeoutForLease + USAGE_FETCH_LEASE_GRACE_MS;
|
|
1564
1645
|
this.#credentialRankingMode = options.credentialRankingMode ?? "balanced";
|
|
1646
|
+
for (const [provider, preferences] of Object.entries(options.credentialPreferences ?? {})) {
|
|
1647
|
+
if (preferences.length > 0) {
|
|
1648
|
+
this.#configuredCredentialPreferences.set(resolveOAuthStorageProvider(provider), preferences);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1565
1651
|
this.#refreshOAuthCredentialOverride = options.refreshOAuthCredential;
|
|
1566
1652
|
this.#fetchUsageReportsOverride = options.fetchUsageReports;
|
|
1567
1653
|
this.#fetchUsageReportsForProviderOverride = options.fetchUsageReportsForProvider;
|
|
@@ -1693,10 +1779,19 @@ export class AuthStorage {
|
|
|
1693
1779
|
#hasConfigOverride(provider: string, owner?: object): boolean {
|
|
1694
1780
|
return this.#configOverrideRegistration(provider, owner) !== undefined;
|
|
1695
1781
|
}
|
|
1696
|
-
|
|
1782
|
+
/**
|
|
1783
|
+
* Return provider-specific discovery evidence. `stagedConfigApiKey` lets a read-only
|
|
1784
|
+
* config candidate derive the same fingerprint before its owner-scoped override is published.
|
|
1785
|
+
*/
|
|
1786
|
+
getProviderEvidenceGeneration(
|
|
1787
|
+
provider: string,
|
|
1788
|
+
resolvedApiKey?: string,
|
|
1789
|
+
owner?: object,
|
|
1790
|
+
stagedConfigApiKey?: { apiKey: string },
|
|
1791
|
+
): string {
|
|
1697
1792
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
1698
1793
|
provider = storageProvider;
|
|
1699
|
-
const configOverride = this.#configOverrideRegistration(storageProvider, owner);
|
|
1794
|
+
const configOverride = stagedConfigApiKey ?? this.#configOverrideRegistration(storageProvider, owner);
|
|
1700
1795
|
const ownerGeneration = owner ? this.#getOwnerProviderGeneration(owner, storageProvider) : 0;
|
|
1701
1796
|
const ownerFallbackGeneration = owner ? this.#getOwnerFallbackGeneration(owner) : this.#fallbackGeneration;
|
|
1702
1797
|
const generation = owner
|
|
@@ -2087,8 +2182,9 @@ export class AuthStorage {
|
|
|
2087
2182
|
);
|
|
2088
2183
|
}
|
|
2089
2184
|
const allRows = this.#store.listCredentialInventory?.(storageProvider) ?? [];
|
|
2090
|
-
const matchingCredentials = this.#
|
|
2091
|
-
this.#
|
|
2185
|
+
const matchingCredentials = this.#entriesDesignatedBySelector(
|
|
2186
|
+
this.#getStoredCredentials(storageProvider),
|
|
2187
|
+
selector,
|
|
2092
2188
|
);
|
|
2093
2189
|
const matchingIds = new Set(matchingCredentials.map(entry => entry.id));
|
|
2094
2190
|
const matchingRows = allRows.filter(
|
|
@@ -2138,7 +2234,7 @@ export class AuthStorage {
|
|
|
2138
2234
|
"ambiguous",
|
|
2139
2235
|
storageProvider,
|
|
2140
2236
|
selector,
|
|
2141
|
-
`Selector ${this.#formatCredentialSelector(selector)} matches multiple OAuth credentials; choose id:<row-id>`,
|
|
2237
|
+
`Selector ${this.#formatCredentialSelector(selector)} matches multiple OAuth credentials (ids: ${matchingRows.map(row => row.id).join(", ")}); choose one with id:<row-id>`,
|
|
2142
2238
|
matchingRows.map(row => row.id),
|
|
2143
2239
|
);
|
|
2144
2240
|
}
|
|
@@ -2185,10 +2281,9 @@ export class AuthStorage {
|
|
|
2185
2281
|
this.#usageCache.deletePrefix?.("reports:");
|
|
2186
2282
|
for (const [scopeId, selectors] of this.#sessionCredentialSelectors) {
|
|
2187
2283
|
const selector = selectors.get(storageProvider);
|
|
2188
|
-
const selected = selector
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
if (selected && removed.has(selected.id)) this.clearSessionCredentialSelector(storageProvider, scopeId);
|
|
2284
|
+
const selected = selector ? this.#entriesSelectedBySelector(previousEntries, selector) : [];
|
|
2285
|
+
if (selected.some(entry => removed.has(entry.id)))
|
|
2286
|
+
this.clearSessionCredentialSelector(storageProvider, scopeId);
|
|
2192
2287
|
}
|
|
2193
2288
|
for (const [sessionId, sticky] of this.#sessionLastCredential.get(storageProvider) ?? []) {
|
|
2194
2289
|
if (removed.has(previousEntries[sticky.index]?.id ?? -1))
|
|
@@ -2308,6 +2403,19 @@ export class AuthStorage {
|
|
|
2308
2403
|
return this.#getCredentialSelector(provider, undefined, sessionId) !== undefined;
|
|
2309
2404
|
}
|
|
2310
2405
|
|
|
2406
|
+
/** Whether any runtime/session scope has a credential pin, including an unavailable hard pin. */
|
|
2407
|
+
hasAnyCredentialPin(provider: string): boolean {
|
|
2408
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2409
|
+
if (this.#runtimeCredentialSelectors.has(storageProvider)) return true;
|
|
2410
|
+
for (const selectors of this.#sessionCredentialSelectors.values()) {
|
|
2411
|
+
if (selectors.has(storageProvider)) return true;
|
|
2412
|
+
}
|
|
2413
|
+
for (const unavailable of this.#sessionCredentialUnavailable.values()) {
|
|
2414
|
+
if (unavailable.has(storageProvider)) return true;
|
|
2415
|
+
}
|
|
2416
|
+
return false;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2311
2419
|
/**
|
|
2312
2420
|
* Opaque stored row id of the credential this session is currently using.
|
|
2313
2421
|
*
|
|
@@ -2327,6 +2435,82 @@ export class AuthStorage {
|
|
|
2327
2435
|
return this.#getStoredCredentials(storageProvider)[session.index]?.id;
|
|
2328
2436
|
}
|
|
2329
2437
|
|
|
2438
|
+
/** Return the stored OAuth row pinned by a credential selector without resolving tokens. */
|
|
2439
|
+
getSelectedOAuthCredentialRowId(
|
|
2440
|
+
provider: string,
|
|
2441
|
+
sessionId?: string,
|
|
2442
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
2443
|
+
): number | undefined {
|
|
2444
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2445
|
+
try {
|
|
2446
|
+
const selected = this.#resolveSelectedStoredCredential(storageProvider, options, sessionId);
|
|
2447
|
+
return selected?.credential.type === "oauth" ? selected.id : undefined;
|
|
2448
|
+
} catch {
|
|
2449
|
+
return undefined;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
/** Return the usable OAuth row selected by a soft preferred-credential selector. */
|
|
2454
|
+
getPreferredOAuthCredentialRowId(provider: string): number | undefined {
|
|
2455
|
+
const preferredSelector = this.#getPreferredCredentialSelector(provider);
|
|
2456
|
+
if (!preferredSelector) return undefined;
|
|
2457
|
+
const selected = this.#findCredentialBySelector(provider, preferredSelector);
|
|
2458
|
+
if (selected?.credential.type !== "oauth" || !this.isCredentialAvailable(provider, selected.id)) return undefined;
|
|
2459
|
+
return selected.id;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
/** Return the stored API-key row pinned by a credential selector without resolving values. */
|
|
2463
|
+
getSelectedApiKeyCredentialRowId(
|
|
2464
|
+
provider: string,
|
|
2465
|
+
sessionId?: string,
|
|
2466
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
2467
|
+
): number | undefined {
|
|
2468
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2469
|
+
try {
|
|
2470
|
+
const selected = this.#resolveSelectedStoredCredential(storageProvider, options, sessionId);
|
|
2471
|
+
return selected?.credential.type === "api_key" ? selected.id : undefined;
|
|
2472
|
+
} catch {
|
|
2473
|
+
return undefined;
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
/**
|
|
2478
|
+
* Preview the stored API-key row request dispatch would try for this session.
|
|
2479
|
+
* Requires a session id so the deterministic session order is used instead
|
|
2480
|
+
* of advancing the provider's round-robin cursor. Does not resolve key values
|
|
2481
|
+
* or mutate session selection.
|
|
2482
|
+
*/
|
|
2483
|
+
getProspectiveApiKeyCredentialRowId(
|
|
2484
|
+
provider: string,
|
|
2485
|
+
sessionId: string | undefined,
|
|
2486
|
+
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
2487
|
+
): number | undefined {
|
|
2488
|
+
if (!sessionId) return undefined;
|
|
2489
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2490
|
+
let selected: IndexedStoredCredential | undefined;
|
|
2491
|
+
try {
|
|
2492
|
+
selected = this.#resolveSelectedStoredCredential(storageProvider, options, sessionId);
|
|
2493
|
+
} catch {
|
|
2494
|
+
return undefined;
|
|
2495
|
+
}
|
|
2496
|
+
if (selected?.credential.type === "api_key") {
|
|
2497
|
+
const usable = this.#storedApiKeyUsability(storageProvider, selected.credential.key);
|
|
2498
|
+
return selected.credential.key.startsWith("!") && usable !== true ? undefined : selected.id;
|
|
2499
|
+
}
|
|
2500
|
+
const candidate = this.#selectApiKeyCredential(storageProvider, sessionId);
|
|
2501
|
+
if (!candidate) return undefined;
|
|
2502
|
+
const usable = this.#storedApiKeyUsability(storageProvider, candidate.credential.key);
|
|
2503
|
+
return candidate.credential.key.startsWith("!") && usable !== true ? undefined : candidate.id;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/** Cached usability of a stored API key; never resolves command-backed values. */
|
|
2507
|
+
getStoredApiKeyUsability(provider: string, rowId: number): boolean | undefined {
|
|
2508
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
2509
|
+
const entry = this.#getStoredCredentials(storageProvider).find(candidate => candidate.id === rowId);
|
|
2510
|
+
if (entry?.credential.type !== "api_key") return undefined;
|
|
2511
|
+
return this.#storedApiKeyUsability(storageProvider, entry.credential.key);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2330
2514
|
/** Whether an enabled stored row is currently available outside quota backoff. */
|
|
2331
2515
|
isCredentialAvailable(provider: string, rowId: number): boolean {
|
|
2332
2516
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
@@ -2782,28 +2966,30 @@ export class AuthStorage {
|
|
|
2782
2966
|
return deduped.reverse();
|
|
2783
2967
|
}
|
|
2784
2968
|
|
|
2785
|
-
#pruneDuplicateStoredCredentials(
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2969
|
+
#pruneDuplicateStoredCredentials(
|
|
2970
|
+
provider: string,
|
|
2971
|
+
entries: StoredCredential[],
|
|
2972
|
+
preferredCredentialId?: number,
|
|
2973
|
+
): StoredCredential[] {
|
|
2974
|
+
const selectedByIdentity = new Map<string, number>();
|
|
2789
2975
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
2790
2976
|
const entry = entries[index];
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
const identityKey = this.#resolveOAuthDedupeIdentityKey(provider, credential);
|
|
2797
|
-
if (!identityKey) {
|
|
2798
|
-
kept.push(entry);
|
|
2799
|
-
continue;
|
|
2800
|
-
}
|
|
2801
|
-
if (seen.has(identityKey)) {
|
|
2802
|
-
removed.push(entry);
|
|
2803
|
-
continue;
|
|
2977
|
+
if (entry.credential.type !== "oauth") continue;
|
|
2978
|
+
const identityKey = this.#resolveOAuthDedupeIdentityKey(provider, entry.credential);
|
|
2979
|
+
if (!identityKey) continue;
|
|
2980
|
+
if (!selectedByIdentity.has(identityKey) || entry.id === preferredCredentialId) {
|
|
2981
|
+
selectedByIdentity.set(identityKey, entry.id);
|
|
2804
2982
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
const selectedIds = new Set(selectedByIdentity.values());
|
|
2986
|
+
const kept: StoredCredential[] = [];
|
|
2987
|
+
const removed: StoredCredential[] = [];
|
|
2988
|
+
for (const entry of entries) {
|
|
2989
|
+
const identityKey =
|
|
2990
|
+
entry.credential.type === "oauth" ? this.#resolveOAuthDedupeIdentityKey(provider, entry.credential) : null;
|
|
2991
|
+
if (identityKey && !selectedIds.has(entry.id)) removed.push(entry);
|
|
2992
|
+
else kept.push(entry);
|
|
2807
2993
|
}
|
|
2808
2994
|
if (removed.length > 0) {
|
|
2809
2995
|
for (const entry of removed) {
|
|
@@ -2812,7 +2998,7 @@ export class AuthStorage {
|
|
|
2812
2998
|
this.#clearSelectorsForRemovedCredential(provider, new Set(removed.map(entry => entry.id)), entries);
|
|
2813
2999
|
this.#resetProviderAssignments(provider);
|
|
2814
3000
|
}
|
|
2815
|
-
return kept
|
|
3001
|
+
return kept;
|
|
2816
3002
|
}
|
|
2817
3003
|
|
|
2818
3004
|
/** Returns all credentials for a provider as an array */
|
|
@@ -2949,16 +3135,60 @@ export class AuthStorage {
|
|
|
2949
3135
|
return entry.credential.type === "oauth" && entry.credential.accountId === selector.value;
|
|
2950
3136
|
case "project":
|
|
2951
3137
|
return entry.credential.type === "oauth" && entry.credential.projectId === selector.value;
|
|
3138
|
+
case "org":
|
|
3139
|
+
return (
|
|
3140
|
+
entry.credential.type === "oauth" && oauthCredentialMatchesOrganization(entry.credential, selector.value)
|
|
3141
|
+
);
|
|
2952
3142
|
}
|
|
2953
3143
|
}
|
|
2954
3144
|
|
|
2955
3145
|
#findCredentialBySelector(provider: string, selector: AuthCredentialSelector): IndexedStoredCredential | undefined {
|
|
2956
3146
|
const stored = this.#getStoredCredentials(provider);
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
3147
|
+
const designated = this.#entriesDesignatedBySelector(stored, selector);
|
|
3148
|
+
// Organization names are not unique, so an organization selector naming several rows is rejected rather than
|
|
3149
|
+
// resolved to the first. Email/account/project selectors keep resolving to the first row: pins persisted in
|
|
3150
|
+
// config and session history use those kinds, and they are not organization-aware.
|
|
3151
|
+
if (selector.kind === "org" && designated.length > 1) {
|
|
3152
|
+
const ids = designated.map(entry => entry.id);
|
|
3153
|
+
throw new OAuthCredentialSelectorError(
|
|
3154
|
+
"ambiguous",
|
|
3155
|
+
resolveOAuthStorageProvider(provider),
|
|
3156
|
+
selector,
|
|
3157
|
+
`Selector ${this.#formatCredentialSelector(selector)} matches multiple stored credentials for ${provider} (ids: ${ids.join(", ")}); choose one with id:<row-id>`,
|
|
3158
|
+
ids,
|
|
3159
|
+
);
|
|
2960
3160
|
}
|
|
2961
|
-
|
|
3161
|
+
const [entry] = designated;
|
|
3162
|
+
return entry ? { ...entry, index: stored.indexOf(entry) } : undefined;
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
/**
|
|
3166
|
+
* Rows whose removal invalidates a selector, mirroring {@link AuthStorage.#findCredentialBySelector}: the row an
|
|
3167
|
+
* email/account/project/id selector resolves to (the first match), or every row an `org:` selector designates,
|
|
3168
|
+
* because removing one of several designated organizations would otherwise retarget the selector to another.
|
|
3169
|
+
*/
|
|
3170
|
+
#entriesSelectedBySelector<T extends StoredCredential>(
|
|
3171
|
+
entries: readonly T[],
|
|
3172
|
+
selector: AuthCredentialSelector,
|
|
3173
|
+
): T[] {
|
|
3174
|
+
const designated = this.#entriesDesignatedBySelector(entries, selector);
|
|
3175
|
+
return selector.kind === "org" ? designated : designated.slice(0, 1);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
/**
|
|
3179
|
+
* Every stored row a selector designates. An `org:` value prefers an exact organization-id match over
|
|
3180
|
+
* display-name matches; more than one designated row means the selector is ambiguous.
|
|
3181
|
+
*/
|
|
3182
|
+
#entriesDesignatedBySelector<T extends StoredCredential>(
|
|
3183
|
+
entries: readonly T[],
|
|
3184
|
+
selector: AuthCredentialSelector,
|
|
3185
|
+
): T[] {
|
|
3186
|
+
if (selector.kind === "org") {
|
|
3187
|
+
return selectOrganizationMatches(entries, selector.value, entry =>
|
|
3188
|
+
entry.credential.type === "oauth" ? entry.credential : undefined,
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
return entries.filter(entry => this.#credentialMatchesSelector(entry, selector));
|
|
2962
3192
|
}
|
|
2963
3193
|
|
|
2964
3194
|
#credentialMatchesInventorySelector(row: CredentialInventoryRecord, selector: AuthCredentialSelector): boolean {
|
|
@@ -2967,10 +3197,13 @@ export class AuthStorage {
|
|
|
2967
3197
|
case "id":
|
|
2968
3198
|
return String(row.id) === selector.value;
|
|
2969
3199
|
case "email":
|
|
2970
|
-
return row.identityLabel?.toLowerCase() === selector.value.toLowerCase();
|
|
3200
|
+
return (row.email ?? row.identityLabel)?.toLowerCase() === selector.value.toLowerCase();
|
|
2971
3201
|
case "account":
|
|
3202
|
+
return (row.accountId ?? row.identityLabel) === selector.value;
|
|
2972
3203
|
case "project":
|
|
2973
|
-
return row.identityLabel === selector.value;
|
|
3204
|
+
return (row.projectId ?? row.identityLabel) === selector.value;
|
|
3205
|
+
case "org":
|
|
3206
|
+
return oauthCredentialMatchesOrganization(row, selector.value);
|
|
2974
3207
|
}
|
|
2975
3208
|
}
|
|
2976
3209
|
#getCredentialSelector(
|
|
@@ -2995,6 +3228,138 @@ export class AuthStorage {
|
|
|
2995
3228
|
);
|
|
2996
3229
|
}
|
|
2997
3230
|
|
|
3231
|
+
/**
|
|
3232
|
+
* Providers whose configured preference list names one stored credential row through more than one entry, for
|
|
3233
|
+
* example `email:` and `account:` selectors for the same login. Startup rejects such a list.
|
|
3234
|
+
*/
|
|
3235
|
+
findDuplicateCredentialPreferences(): Array<{ provider: string; credentialId: number }> {
|
|
3236
|
+
const duplicates: Array<{ provider: string; credentialId: number }> = [];
|
|
3237
|
+
for (const [provider, preferences] of this.#configuredCredentialPreferences) {
|
|
3238
|
+
const credentialId = this.#duplicateConfiguredPreferenceRow(provider, preferences);
|
|
3239
|
+
if (credentialId !== undefined) duplicates.push({ provider, credentialId });
|
|
3240
|
+
}
|
|
3241
|
+
return duplicates;
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
/** Id of the first stored row that two entries of `preferences` both designate; undefined when every entry differs. */
|
|
3245
|
+
#duplicateConfiguredPreferenceRow(
|
|
3246
|
+
provider: string,
|
|
3247
|
+
preferences: readonly ConfiguredCredentialPreference[],
|
|
3248
|
+
): number | undefined {
|
|
3249
|
+
const designatedIds = new Set<number>();
|
|
3250
|
+
for (const { selector } of preferences) {
|
|
3251
|
+
const credentialId = this.#findCredentialBySelector(provider, selector)?.id;
|
|
3252
|
+
if (credentialId === undefined) continue;
|
|
3253
|
+
if (designatedIds.has(credentialId)) return credentialId;
|
|
3254
|
+
designatedIds.add(credentialId);
|
|
3255
|
+
}
|
|
3256
|
+
return undefined;
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
/**
|
|
3260
|
+
* Reorder `candidates` by the provider's configured preference list: usable entries under their cap first, in list
|
|
3261
|
+
* order; then the remaining candidates in their ranked order; then entries at or over their cap, in list order.
|
|
3262
|
+
* Missing and blocked entries are skipped and keep their ranked position. A list that names one stored row twice
|
|
3263
|
+
* (startup rejects it, but a later login can make two entries resolve to one row) is ignored as a whole rather than
|
|
3264
|
+
* applying one entry's position and cap and dropping the other's.
|
|
3265
|
+
*/
|
|
3266
|
+
async #applyConfiguredCredentialPreferences(
|
|
3267
|
+
provider: string,
|
|
3268
|
+
providerKey: string,
|
|
3269
|
+
candidates: Array<{
|
|
3270
|
+
selection: OAuthCredentialSelection;
|
|
3271
|
+
usage: UsageReport | null;
|
|
3272
|
+
usageChecked: boolean;
|
|
3273
|
+
}>,
|
|
3274
|
+
strategy: CredentialRankingStrategy | undefined,
|
|
3275
|
+
options?: AuthApiKeyOptions,
|
|
3276
|
+
): Promise<void> {
|
|
3277
|
+
const preferences = this.#configuredCredentialPreferences.get(resolveOAuthStorageProvider(provider));
|
|
3278
|
+
if (!preferences) return;
|
|
3279
|
+
const duplicateCredentialId = this.#duplicateConfiguredPreferenceRow(provider, preferences);
|
|
3280
|
+
if (duplicateCredentialId !== undefined) {
|
|
3281
|
+
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
3282
|
+
if (!this.#warnedDuplicateCredentialPreferences.has(storageProvider)) {
|
|
3283
|
+
this.#warnedDuplicateCredentialPreferences.add(storageProvider);
|
|
3284
|
+
logger.warn("Configured credential preferences name one account twice; using normal ranking", {
|
|
3285
|
+
provider: storageProvider,
|
|
3286
|
+
credentialId: duplicateCredentialId,
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
type Candidate = (typeof candidates)[number];
|
|
3292
|
+
const matched: Array<{ candidate: Candidate; maxUsedFraction: number | undefined }> = [];
|
|
3293
|
+
const claimed = new Set<Candidate>();
|
|
3294
|
+
for (const preference of preferences) {
|
|
3295
|
+
const position = this.#findUsableOAuthCandidate(provider, providerKey, candidates, preference.selector);
|
|
3296
|
+
if (position < 0) continue;
|
|
3297
|
+
const candidate = candidates[position]!;
|
|
3298
|
+
claimed.add(candidate);
|
|
3299
|
+
matched.push({ candidate, maxUsedFraction: preference.maxUsedFraction });
|
|
3300
|
+
}
|
|
3301
|
+
if (matched.length === 0) return;
|
|
3302
|
+
// Ranking already fetched usage for strategy-backed providers; fetch only what a cap still needs.
|
|
3303
|
+
await Promise.all(
|
|
3304
|
+
matched.map(async ({ candidate, maxUsedFraction }) => {
|
|
3305
|
+
if (maxUsedFraction === undefined || candidate.usageChecked) return;
|
|
3306
|
+
candidate.usage = await this.#getUsageReport(provider, candidate.selection.credential, {
|
|
3307
|
+
baseUrl: options?.baseUrl,
|
|
3308
|
+
timeoutMs: this.#usageRequestTimeoutMs,
|
|
3309
|
+
signal: options?.signal,
|
|
3310
|
+
});
|
|
3311
|
+
candidate.usageChecked = true;
|
|
3312
|
+
}),
|
|
3313
|
+
);
|
|
3314
|
+
const preferred: Candidate[] = [];
|
|
3315
|
+
const capped: Candidate[] = [];
|
|
3316
|
+
for (const { candidate, maxUsedFraction } of matched) {
|
|
3317
|
+
const usedFraction =
|
|
3318
|
+
maxUsedFraction === undefined ? undefined : this.#maxKnownUsedFraction(candidate.usage, strategy);
|
|
3319
|
+
if (maxUsedFraction !== undefined && usedFraction !== undefined && usedFraction >= maxUsedFraction) {
|
|
3320
|
+
capped.push(candidate);
|
|
3321
|
+
} else {
|
|
3322
|
+
preferred.push(candidate);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
const rest = candidates.filter(candidate => !claimed.has(candidate));
|
|
3326
|
+
candidates.splice(0, candidates.length, ...preferred, ...rest, ...capped);
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/**
|
|
3330
|
+
* Position in `candidates` of the OAuth row `selector` designates, or -1 when it is missing, not OAuth, or blocked.
|
|
3331
|
+
* Candidates are matched by stored row id: `#findCredentialBySelector` indexes the full provider list (API-key rows
|
|
3332
|
+
* included) while candidate selections and the OAuth backoff map use OAuth-only indices.
|
|
3333
|
+
*/
|
|
3334
|
+
#findUsableOAuthCandidate(
|
|
3335
|
+
provider: string,
|
|
3336
|
+
providerKey: string,
|
|
3337
|
+
candidates: ReadonlyArray<{ selection: OAuthCredentialSelection }>,
|
|
3338
|
+
selector: AuthCredentialSelector,
|
|
3339
|
+
): number {
|
|
3340
|
+
const designated = this.#findCredentialBySelector(provider, selector);
|
|
3341
|
+
if (designated?.credential.type !== "oauth") return -1;
|
|
3342
|
+
const position = candidates.findIndex(candidate => candidate.selection.id === designated.id);
|
|
3343
|
+
if (position < 0) return -1;
|
|
3344
|
+
return this.#isCredentialBlocked(providerKey, candidates[position]!.selection.index) ? -1 : position;
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
/** Highest reported used fraction across known usage windows; undefined when no window reports one. */
|
|
3348
|
+
#maxKnownUsedFraction(
|
|
3349
|
+
usage: UsageReport | null,
|
|
3350
|
+
strategy: CredentialRankingStrategy | undefined,
|
|
3351
|
+
): number | undefined {
|
|
3352
|
+
if (!usage) return undefined;
|
|
3353
|
+
const limits = strategy ? Object.values(strategy.findWindowLimits(usage)) : usage.limits;
|
|
3354
|
+
let maxUsed: number | undefined;
|
|
3355
|
+
for (const limit of limits) {
|
|
3356
|
+
const usedFraction = limit?.amount.usedFraction;
|
|
3357
|
+
if (typeof usedFraction !== "number" || !Number.isFinite(usedFraction)) continue;
|
|
3358
|
+
maxUsed = maxUsed === undefined ? usedFraction : Math.max(maxUsed, usedFraction);
|
|
3359
|
+
}
|
|
3360
|
+
return maxUsed;
|
|
3361
|
+
}
|
|
3362
|
+
|
|
2998
3363
|
#assertCredentialSelectorUsable(provider: string, selector: AuthCredentialSelector, owner?: object): void {
|
|
2999
3364
|
if (this.#runtimeOverrides.has(provider)) {
|
|
3000
3365
|
throw new Error(
|
|
@@ -3160,7 +3525,7 @@ export class AuthStorage {
|
|
|
3160
3525
|
const updated = [...entries];
|
|
3161
3526
|
updated[index] = { id: target.id, credential };
|
|
3162
3527
|
const configurationGeneration = this.#getProviderConfigurationGeneration(provider);
|
|
3163
|
-
this.#setStoredCredentials(provider, updated);
|
|
3528
|
+
this.#setStoredCredentials(provider, this.#pruneDuplicateStoredCredentials(provider, updated, target.id));
|
|
3164
3529
|
// Callers use the refresh generation to discount configuration bumps caused by a refresh, so it only
|
|
3165
3530
|
// advances when this refresh bumped configuration; token rotation for a known account does not.
|
|
3166
3531
|
if (
|
|
@@ -3279,8 +3644,8 @@ export class AuthStorage {
|
|
|
3279
3644
|
for (const [scopeId, selectors] of this.#sessionCredentialSelectors) {
|
|
3280
3645
|
const selector = selectors.get(storageProvider);
|
|
3281
3646
|
if (!selector) continue;
|
|
3282
|
-
const selected =
|
|
3283
|
-
if (selected
|
|
3647
|
+
const selected = this.#entriesSelectedBySelector(previousEntries, selector);
|
|
3648
|
+
if (selected.some(entry => removedIds.has(entry.id))) {
|
|
3284
3649
|
this.markSessionCredentialUnavailable(scopeId, storageProvider, selector);
|
|
3285
3650
|
}
|
|
3286
3651
|
}
|
|
@@ -3510,16 +3875,12 @@ export class AuthStorage {
|
|
|
3510
3875
|
getEffectiveCredentialType(
|
|
3511
3876
|
provider: string,
|
|
3512
3877
|
sessionId?: string,
|
|
3513
|
-
options?: Pick<AuthApiKeyOptions, "owner">,
|
|
3878
|
+
options?: Pick<AuthApiKeyOptions, "owner" | "credentialSelector">,
|
|
3514
3879
|
): AuthCredential["type"] | undefined {
|
|
3515
3880
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
3516
3881
|
let selected: ({ index: number } & StoredCredential) | undefined;
|
|
3517
3882
|
try {
|
|
3518
|
-
selected = this.#resolveSelectedStoredCredential(
|
|
3519
|
-
storageProvider,
|
|
3520
|
-
options?.owner ? { owner: options.owner } : undefined,
|
|
3521
|
-
sessionId,
|
|
3522
|
-
);
|
|
3883
|
+
selected = this.#resolveSelectedStoredCredential(storageProvider, options, sessionId);
|
|
3523
3884
|
} catch {
|
|
3524
3885
|
return undefined;
|
|
3525
3886
|
}
|
|
@@ -3541,17 +3902,14 @@ export class AuthStorage {
|
|
|
3541
3902
|
return undefined;
|
|
3542
3903
|
}
|
|
3543
3904
|
|
|
3544
|
-
/**
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3905
|
+
/** Check whether configured auth is usable without resolving credentials or refreshing OAuth. */
|
|
3906
|
+
hasUsableAuth(
|
|
3907
|
+
provider: string,
|
|
3908
|
+
options?: Pick<AuthApiKeyOptions, "owner" | "credentialSelector"> & { sessionId?: string },
|
|
3909
|
+
): boolean {
|
|
3548
3910
|
const storageProvider = resolveOAuthStorageProvider(provider);
|
|
3549
3911
|
try {
|
|
3550
|
-
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
3551
|
-
storageProvider,
|
|
3552
|
-
options?.owner ? { owner: options.owner } : undefined,
|
|
3553
|
-
undefined,
|
|
3554
|
-
);
|
|
3912
|
+
const selectedCredential = this.#resolveSelectedStoredCredential(storageProvider, options, options?.sessionId);
|
|
3555
3913
|
if (this.hasRuntimeApiKey(storageProvider)) return true;
|
|
3556
3914
|
if (this.#hasConfigOverride(storageProvider, options?.owner)) return true;
|
|
3557
3915
|
if (selectedCredential) {
|
|
@@ -4133,6 +4491,8 @@ export class AuthStorage {
|
|
|
4133
4491
|
email: credential.email,
|
|
4134
4492
|
enterpriseUrl: credential.enterpriseUrl,
|
|
4135
4493
|
profileArn: credential.profileArn,
|
|
4494
|
+
organizationId: credential.organizationId,
|
|
4495
|
+
organizationName: credential.organizationName,
|
|
4136
4496
|
mcpBinding: credential.mcpBinding,
|
|
4137
4497
|
};
|
|
4138
4498
|
}
|
|
@@ -4149,6 +4509,9 @@ export class AuthStorage {
|
|
|
4149
4509
|
if (enterpriseUrl) parts.push(`enterprise:${enterpriseUrl}`);
|
|
4150
4510
|
const profileArn = credential.profileArn?.trim();
|
|
4151
4511
|
if (profileArn) parts.push(`profile:${profileArn}`);
|
|
4512
|
+
// One account can hold a credential per organization, each with its own limits.
|
|
4513
|
+
const organizationId = credential.organizationId?.trim();
|
|
4514
|
+
if (organizationId) parts.push(`org:${organizationId}`);
|
|
4152
4515
|
// Only fall back to a secret-derived key when stable credential identity is unavailable.
|
|
4153
4516
|
// Including the token hash when accountId/email/profileArn are present causes cache misses on
|
|
4154
4517
|
// every OAuth refresh — usage data is per-account, not per-token.
|
|
@@ -4211,6 +4574,8 @@ export class AuthStorage {
|
|
|
4211
4574
|
email: credential.email,
|
|
4212
4575
|
enterpriseUrl: credential.enterpriseUrl,
|
|
4213
4576
|
profileArn: credential.profileArn,
|
|
4577
|
+
organizationId: credential.organizationId,
|
|
4578
|
+
organizationName: credential.organizationName,
|
|
4214
4579
|
mcpBinding: credential.mcpBinding,
|
|
4215
4580
|
};
|
|
4216
4581
|
}
|
|
@@ -4226,6 +4591,8 @@ export class AuthStorage {
|
|
|
4226
4591
|
email: refreshed.email ?? credential.email,
|
|
4227
4592
|
enterpriseUrl: refreshed.enterpriseUrl ?? credential.enterpriseUrl,
|
|
4228
4593
|
profileArn: refreshed.profileArn ?? credential.profileArn,
|
|
4594
|
+
organizationId: refreshed.organizationId ?? credential.organizationId,
|
|
4595
|
+
organizationName: refreshed.organizationName ?? credential.organizationName,
|
|
4229
4596
|
mcpBinding: credential.mcpBinding,
|
|
4230
4597
|
};
|
|
4231
4598
|
}
|
|
@@ -4245,7 +4612,8 @@ export class AuthStorage {
|
|
|
4245
4612
|
entry.credential.accountId === previous.accountId &&
|
|
4246
4613
|
entry.credential.email === previous.email &&
|
|
4247
4614
|
entry.credential.projectId === previous.projectId &&
|
|
4248
|
-
entry.credential.profileArn === previous.profileArn
|
|
4615
|
+
entry.credential.profileArn === previous.profileArn &&
|
|
4616
|
+
entry.credential.organizationId === previous.organizationId
|
|
4249
4617
|
);
|
|
4250
4618
|
});
|
|
4251
4619
|
return match?.id;
|
|
@@ -4266,7 +4634,8 @@ export class AuthStorage {
|
|
|
4266
4634
|
entry.credential.accountId === previous.accountId &&
|
|
4267
4635
|
entry.credential.email === previous.email &&
|
|
4268
4636
|
entry.credential.projectId === previous.projectId &&
|
|
4269
|
-
entry.credential.profileArn === previous.profileArn
|
|
4637
|
+
entry.credential.profileArn === previous.profileArn &&
|
|
4638
|
+
entry.credential.organizationId === previous.organizationId
|
|
4270
4639
|
);
|
|
4271
4640
|
});
|
|
4272
4641
|
if (index === -1) return;
|
|
@@ -4285,6 +4654,8 @@ export class AuthStorage {
|
|
|
4285
4654
|
email: next.email,
|
|
4286
4655
|
enterpriseUrl: next.enterpriseUrl,
|
|
4287
4656
|
profileArn: next.profileArn ?? existing.profileArn,
|
|
4657
|
+
organizationId: next.organizationId ?? existing.organizationId,
|
|
4658
|
+
organizationName: next.organizationName ?? existing.organizationName,
|
|
4288
4659
|
mcpBinding: next.mcpBinding ?? existing.mcpBinding,
|
|
4289
4660
|
},
|
|
4290
4661
|
persist,
|
|
@@ -4650,6 +5021,9 @@ export class AuthStorage {
|
|
|
4650
5021
|
const email = this.#getUsageReportMetadataValue(report, "email");
|
|
4651
5022
|
if (email) identifiers.push(`email:${email.toLowerCase()}`);
|
|
4652
5023
|
if (report.provider === "openai-codex" || report.provider === "anthropic") {
|
|
5024
|
+
// One Anthropic account can sign in to several organizations; each has its own limits.
|
|
5025
|
+
const orgId = report.provider === "anthropic" ? this.#getUsageReportMetadataValue(report, "orgId") : undefined;
|
|
5026
|
+
if (orgId) return identifiers.map(identifier => `${report.provider}:${identifier}|org:${orgId}`.toLowerCase());
|
|
4653
5027
|
return identifiers.map(identifier => `${report.provider}:${identifier.toLowerCase()}`);
|
|
4654
5028
|
}
|
|
4655
5029
|
const accountId = this.#getUsageReportMetadataValue(report, "accountId");
|
|
@@ -5685,20 +6059,18 @@ export class AuthStorage {
|
|
|
5685
6059
|
options?.owner,
|
|
5686
6060
|
);
|
|
5687
6061
|
}
|
|
5688
|
-
const
|
|
5689
|
-
? this.#
|
|
5690
|
-
:
|
|
5691
|
-
if (
|
|
5692
|
-
preferredSelection?.credential.type === "oauth" &&
|
|
5693
|
-
!this.#isCredentialBlocked(providerKey, preferredSelection.index)
|
|
5694
|
-
) {
|
|
5695
|
-
const preferredCandidate = candidates.findIndex(
|
|
5696
|
-
candidate => candidate.selection.index === preferredSelection.index,
|
|
5697
|
-
);
|
|
6062
|
+
const preferredCandidate = preferredSelector
|
|
6063
|
+
? this.#findUsableOAuthCandidate(provider, providerKey, candidates, preferredSelector)
|
|
6064
|
+
: -1;
|
|
6065
|
+
if (preferredCandidate >= 0) {
|
|
5698
6066
|
if (preferredCandidate > 0) {
|
|
5699
6067
|
const [preferred] = candidates.splice(preferredCandidate, 1);
|
|
5700
6068
|
candidates.unshift(preferred);
|
|
5701
6069
|
}
|
|
6070
|
+
} else if (!preferredSelector && !sessionPreferredIsAvailable) {
|
|
6071
|
+
// Configured preferences choose a credential; a session that already sticks to a usable one keeps it, so
|
|
6072
|
+
// the list (and any usage fetch its caps need) is consulted only at selection time.
|
|
6073
|
+
await this.#applyConfiguredCredentialPreferences(provider, providerKey, candidates, strategy, options);
|
|
5702
6074
|
}
|
|
5703
6075
|
}
|
|
5704
6076
|
|
|
@@ -5939,6 +6311,8 @@ export class AuthStorage {
|
|
|
5939
6311
|
projectId: claim.credential.projectId,
|
|
5940
6312
|
enterpriseUrl: claim.credential.enterpriseUrl,
|
|
5941
6313
|
profileArn: claim.credential.profileArn,
|
|
6314
|
+
organizationId: claim.credential.organizationId,
|
|
6315
|
+
organizationName: claim.credential.organizationName,
|
|
5942
6316
|
mcpBinding: claim.credential.mcpBinding,
|
|
5943
6317
|
persistedByLease: true,
|
|
5944
6318
|
};
|
|
@@ -6059,6 +6433,8 @@ export class AuthStorage {
|
|
|
6059
6433
|
projectId: effectiveRefreshed.projectId ?? credential.projectId,
|
|
6060
6434
|
enterpriseUrl: effectiveRefreshed.enterpriseUrl ?? credential.enterpriseUrl,
|
|
6061
6435
|
profileArn: effectiveRefreshed.profileArn ?? credential.profileArn,
|
|
6436
|
+
organizationId: effectiveRefreshed.organizationId ?? credential.organizationId,
|
|
6437
|
+
organizationName: effectiveRefreshed.organizationName ?? credential.organizationName,
|
|
6062
6438
|
mcpBinding: (effectiveRefreshed as RefreshedOAuthCredentials).mcpBinding ?? credential.mcpBinding,
|
|
6063
6439
|
};
|
|
6064
6440
|
if (refreshLease) {
|
|
@@ -6247,6 +6623,8 @@ export class AuthStorage {
|
|
|
6247
6623
|
projectId: result.newCredentials.projectId ?? selection.credential.projectId,
|
|
6248
6624
|
enterpriseUrl: result.newCredentials.enterpriseUrl ?? selection.credential.enterpriseUrl,
|
|
6249
6625
|
profileArn: result.newCredentials.profileArn ?? selection.credential.profileArn,
|
|
6626
|
+
organizationId: result.newCredentials.organizationId ?? selection.credential.organizationId,
|
|
6627
|
+
organizationName: result.newCredentials.organizationName ?? selection.credential.organizationName,
|
|
6250
6628
|
mcpBinding: refreshedAuthority.mcpBinding,
|
|
6251
6629
|
};
|
|
6252
6630
|
this.#replaceCredentialAt(
|
|
@@ -6526,7 +6904,7 @@ export class AuthStorage {
|
|
|
6526
6904
|
*/
|
|
6527
6905
|
async peekApiKey(
|
|
6528
6906
|
provider: string,
|
|
6529
|
-
options?:
|
|
6907
|
+
options?: AuthApiKeyOptions & { sessionId?: string },
|
|
6530
6908
|
): Promise<string | undefined> {
|
|
6531
6909
|
provider = resolveOAuthStorageProvider(provider);
|
|
6532
6910
|
const runtimeKey = this.#runtimeOverrides.get(provider);
|
|
@@ -6537,11 +6915,7 @@ export class AuthStorage {
|
|
|
6537
6915
|
if (configKey && !configOverride?.envSourced) return configKey;
|
|
6538
6916
|
if (options?.sessionId && this.hasSessionCredentialUnavailable(provider, options.sessionId)) return undefined;
|
|
6539
6917
|
|
|
6540
|
-
const selectedCredential = this.#resolveSelectedStoredCredential(
|
|
6541
|
-
provider,
|
|
6542
|
-
options?.owner ? { owner: options.owner } : undefined,
|
|
6543
|
-
options?.sessionId,
|
|
6544
|
-
);
|
|
6918
|
+
const selectedCredential = this.#resolveSelectedStoredCredential(provider, options, options?.sessionId);
|
|
6545
6919
|
if (configKey) {
|
|
6546
6920
|
// Env-sourced (`apiKeyEnv`) override: same precedence as getApiKey —
|
|
6547
6921
|
// a stored api_key credential from `auth login` wins, stored OAuth
|
|
@@ -6576,7 +6950,7 @@ export class AuthStorage {
|
|
|
6576
6950
|
// A hard selector is an identity boundary. If its selected row cannot
|
|
6577
6951
|
// provide a current token, discovery must not continue into the shared
|
|
6578
6952
|
// credential pool and silently query another account's catalog.
|
|
6579
|
-
if (this.#getCredentialSelector(provider,
|
|
6953
|
+
if (this.#getCredentialSelector(provider, options, options?.sessionId)) return undefined;
|
|
6580
6954
|
|
|
6581
6955
|
const attemptedApiKeyIndices = new Set<number>();
|
|
6582
6956
|
for (;;) {
|
|
@@ -7023,6 +7397,8 @@ export class AuthStorage {
|
|
|
7023
7397
|
projectId: refreshed.projectId ?? target.credential.projectId,
|
|
7024
7398
|
enterpriseUrl: refreshed.enterpriseUrl ?? target.credential.enterpriseUrl,
|
|
7025
7399
|
profileArn: refreshed.profileArn ?? target.credential.profileArn,
|
|
7400
|
+
organizationId: refreshed.organizationId ?? target.credential.organizationId,
|
|
7401
|
+
organizationName: refreshed.organizationName ?? target.credential.organizationName,
|
|
7026
7402
|
mcpBinding: refreshed.mcpBinding,
|
|
7027
7403
|
};
|
|
7028
7404
|
this.#replaceCredentialAt(provider, index, updated, !refreshed.persistedByLease, id);
|
|
@@ -7134,7 +7510,7 @@ export class AuthStorage {
|
|
|
7134
7510
|
const credential = chosen.credential;
|
|
7135
7511
|
const identity =
|
|
7136
7512
|
credential.type === "oauth"
|
|
7137
|
-
? (credential
|
|
7513
|
+
? (formatOAuthIdentityLabel(credential) ?? `cred ${chosen.id}`)
|
|
7138
7514
|
: `cred ${chosen.id}`;
|
|
7139
7515
|
return `${baseLabel} · ${preferredType} #${chosen.id} (${identity})`;
|
|
7140
7516
|
}
|
|
@@ -7247,7 +7623,22 @@ function resolveProviderCredentialIdentityKey(provider: string, identifiers: str
|
|
|
7247
7623
|
|
|
7248
7624
|
function resolveCredentialIdentityKey(provider: string, credential: AuthCredential): string | null {
|
|
7249
7625
|
if (credential.type === "api_key") return null;
|
|
7250
|
-
return
|
|
7626
|
+
return withOrganizationIdentity(
|
|
7627
|
+
resolveProviderCredentialIdentityKey(provider, extractOAuthCredentialIdentifiers(credential)),
|
|
7628
|
+
credential,
|
|
7629
|
+
);
|
|
7630
|
+
}
|
|
7631
|
+
|
|
7632
|
+
/**
|
|
7633
|
+
* Scope an account identity key to the credential's organization. One account
|
|
7634
|
+
* (same email) can sign in to several organizations, and each login yields an
|
|
7635
|
+
* independent credential with its own plan and limits, so they must not
|
|
7636
|
+
* replace or dedupe each other.
|
|
7637
|
+
*/
|
|
7638
|
+
function withOrganizationIdentity(identityKey: string | null, credential: OAuthCredential): string | null {
|
|
7639
|
+
const organizationId = credential.organizationId?.trim();
|
|
7640
|
+
if (!identityKey || !organizationId) return identityKey;
|
|
7641
|
+
return `${identityKey}|org:${organizationId}`;
|
|
7251
7642
|
}
|
|
7252
7643
|
|
|
7253
7644
|
function resolveRowCredentialIdentityKey(provider: string, row: AuthRow): string | null {
|
|
@@ -7342,6 +7733,7 @@ function extractOAuthTokenIdentifiers(token: string | undefined): string[] | und
|
|
|
7342
7733
|
*/
|
|
7343
7734
|
export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
7344
7735
|
#db: Database;
|
|
7736
|
+
#ownedStatements: Statement[] = [];
|
|
7345
7737
|
#listActiveStmt: Statement;
|
|
7346
7738
|
#listActiveByProviderStmt: Statement;
|
|
7347
7739
|
#listAllStmt: Statement;
|
|
@@ -7367,53 +7759,89 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7367
7759
|
this.#db = db;
|
|
7368
7760
|
this.#initializeSchema();
|
|
7369
7761
|
|
|
7370
|
-
this.#listActiveStmt = this.#
|
|
7762
|
+
this.#listActiveStmt = this.#prepareOwnedStatement(
|
|
7371
7763
|
"SELECT id, provider, credential_type, data, disabled_cause, identity_key, revision FROM auth_credentials WHERE disabled_cause IS NULL ORDER BY id ASC",
|
|
7372
7764
|
);
|
|
7373
|
-
this.#listActiveByProviderStmt = this.#
|
|
7765
|
+
this.#listActiveByProviderStmt = this.#prepareOwnedStatement(
|
|
7374
7766
|
"SELECT id, provider, credential_type, data, disabled_cause, identity_key, revision FROM auth_credentials WHERE provider = ? AND disabled_cause IS NULL ORDER BY id ASC",
|
|
7375
7767
|
);
|
|
7376
|
-
this.#listAllStmt = this.#
|
|
7768
|
+
this.#listAllStmt = this.#prepareOwnedStatement(
|
|
7377
7769
|
"SELECT id, provider, credential_type, data, disabled_cause, identity_key, revision FROM auth_credentials ORDER BY id ASC",
|
|
7378
7770
|
);
|
|
7379
|
-
this.#listAllByProviderStmt = this.#
|
|
7771
|
+
this.#listAllByProviderStmt = this.#prepareOwnedStatement(
|
|
7380
7772
|
"SELECT id, provider, credential_type, data, disabled_cause, identity_key, revision FROM auth_credentials WHERE provider = ? ORDER BY id ASC",
|
|
7381
7773
|
);
|
|
7382
|
-
this.#listDisabledByProviderStmt = this.#
|
|
7774
|
+
this.#listDisabledByProviderStmt = this.#prepareOwnedStatement(
|
|
7383
7775
|
"SELECT id, provider, credential_type, data, disabled_cause, identity_key, revision FROM auth_credentials WHERE provider = ? AND disabled_cause IS NOT NULL ORDER BY id ASC",
|
|
7384
7776
|
);
|
|
7385
|
-
this.#insertStmt = this.#
|
|
7777
|
+
this.#insertStmt = this.#prepareOwnedStatement(
|
|
7386
7778
|
`INSERT INTO auth_credentials (provider, credential_type, data, identity_key, revision, created_at, updated_at) VALUES (?, ?, ?, ?, 1, ${SQLITE_NOW_EPOCH}, ${SQLITE_NOW_EPOCH}) RETURNING id`,
|
|
7387
7779
|
);
|
|
7388
|
-
this.#updateStmt = this.#
|
|
7780
|
+
this.#updateStmt = this.#prepareOwnedStatement(
|
|
7389
7781
|
`UPDATE auth_credentials SET credential_type = ?, data = ?, identity_key = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ?`,
|
|
7390
7782
|
);
|
|
7391
|
-
this.#deleteStmt = this.#
|
|
7783
|
+
this.#deleteStmt = this.#prepareOwnedStatement(
|
|
7392
7784
|
`UPDATE auth_credentials SET disabled_cause = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ?`,
|
|
7393
7785
|
);
|
|
7394
|
-
this.#deleteIfMatchesStmt = this.#
|
|
7786
|
+
this.#deleteIfMatchesStmt = this.#prepareOwnedStatement(
|
|
7395
7787
|
`UPDATE auth_credentials SET disabled_cause = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ? AND data = ? AND disabled_cause IS NULL`,
|
|
7396
7788
|
);
|
|
7397
|
-
this.#deleteIfRevisionStmt = this.#
|
|
7789
|
+
this.#deleteIfRevisionStmt = this.#prepareOwnedStatement(
|
|
7398
7790
|
`UPDATE auth_credentials SET disabled_cause = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE id = ? AND revision = ? AND disabled_cause IS NULL`,
|
|
7399
7791
|
);
|
|
7400
|
-
this.#deleteByProviderStmt = this.#
|
|
7792
|
+
this.#deleteByProviderStmt = this.#prepareOwnedStatement(
|
|
7401
7793
|
`UPDATE auth_credentials SET disabled_cause = ?, revision = revision + 1, updated_at = ${SQLITE_NOW_EPOCH} WHERE provider = ? AND disabled_cause IS NULL`,
|
|
7402
7794
|
);
|
|
7403
|
-
this.#hardDeleteStmt = this.#
|
|
7404
|
-
this.#getCacheStmt = this.#
|
|
7795
|
+
this.#hardDeleteStmt = this.#prepareOwnedStatement("DELETE FROM auth_credentials WHERE id = ?");
|
|
7796
|
+
this.#getCacheStmt = this.#prepareOwnedStatement(
|
|
7405
7797
|
`SELECT value FROM cache WHERE key = ? AND expires_at > ${SQLITE_NOW_EPOCH}`,
|
|
7406
7798
|
);
|
|
7407
|
-
this.#getCacheIncludingExpiredStmt = this.#
|
|
7408
|
-
this.#upsertCacheStmt = this.#
|
|
7799
|
+
this.#getCacheIncludingExpiredStmt = this.#prepareOwnedStatement("SELECT value FROM cache WHERE key = ?");
|
|
7800
|
+
this.#upsertCacheStmt = this.#prepareOwnedStatement(
|
|
7409
7801
|
"INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
|
|
7410
7802
|
);
|
|
7411
|
-
this.#claimUsageFetchLeaseStmt = this.#
|
|
7803
|
+
this.#claimUsageFetchLeaseStmt = this.#prepareOwnedStatement(
|
|
7412
7804
|
"INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at WHERE cache.expires_at <= ?",
|
|
7413
7805
|
);
|
|
7414
|
-
this.#releaseUsageFetchLeaseStmt = this.#
|
|
7415
|
-
this.#deleteCachePrefixStmt = this.#
|
|
7416
|
-
this.#deleteExpiredCacheStmt = this.#
|
|
7806
|
+
this.#releaseUsageFetchLeaseStmt = this.#prepareOwnedStatement("DELETE FROM cache WHERE key = ? AND value = ?");
|
|
7807
|
+
this.#deleteCachePrefixStmt = this.#prepareOwnedStatement("DELETE FROM cache WHERE substr(key, 1, ?) = ?");
|
|
7808
|
+
this.#deleteExpiredCacheStmt = this.#prepareOwnedStatement(
|
|
7809
|
+
`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`,
|
|
7810
|
+
);
|
|
7811
|
+
}
|
|
7812
|
+
|
|
7813
|
+
#prepareOwnedStatement(sql: string): Statement {
|
|
7814
|
+
const statement = this.#db.prepare(sql);
|
|
7815
|
+
this.#ownedStatements.push(statement);
|
|
7816
|
+
return statement;
|
|
7817
|
+
}
|
|
7818
|
+
|
|
7819
|
+
#withStatement<T>(sql: string, use: (statement: Statement) => T): T {
|
|
7820
|
+
const statement = this.#db.prepare(sql);
|
|
7821
|
+
let result!: T;
|
|
7822
|
+
let operationFailed = false;
|
|
7823
|
+
let operationError: unknown;
|
|
7824
|
+
try {
|
|
7825
|
+
result = use(statement);
|
|
7826
|
+
} catch (error) {
|
|
7827
|
+
operationFailed = true;
|
|
7828
|
+
operationError = error;
|
|
7829
|
+
}
|
|
7830
|
+
|
|
7831
|
+
try {
|
|
7832
|
+
statement.finalize();
|
|
7833
|
+
} catch (finalizeError) {
|
|
7834
|
+
if (operationFailed) {
|
|
7835
|
+
throw new AggregateError(
|
|
7836
|
+
[operationError, finalizeError],
|
|
7837
|
+
"SQLite statement operation and finalization failed",
|
|
7838
|
+
{ cause: operationError },
|
|
7839
|
+
);
|
|
7840
|
+
}
|
|
7841
|
+
throw finalizeError;
|
|
7842
|
+
}
|
|
7843
|
+
if (operationFailed) throw operationError;
|
|
7844
|
+
return result;
|
|
7417
7845
|
}
|
|
7418
7846
|
|
|
7419
7847
|
static async open(dbPath: string = getAgentDbPath()): Promise<SqliteAuthCredentialStore> {
|
|
@@ -7489,33 +7917,40 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7489
7917
|
}
|
|
7490
7918
|
|
|
7491
7919
|
#authCredentialsTableExists(): boolean {
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7920
|
+
return this.#withStatement(
|
|
7921
|
+
"SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'auth_credentials'",
|
|
7922
|
+
statement => {
|
|
7923
|
+
const row = statement.get() as { present?: number } | undefined;
|
|
7924
|
+
return row?.present === 1;
|
|
7925
|
+
},
|
|
7926
|
+
);
|
|
7496
7927
|
}
|
|
7497
7928
|
|
|
7498
7929
|
#readAuthSchemaVersion(): number | null {
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7930
|
+
return this.#withStatement("SELECT version FROM auth_schema_version WHERE id = 1", statement => {
|
|
7931
|
+
const row = statement.get() as { version?: number } | undefined;
|
|
7932
|
+
return typeof row?.version === "number" ? row.version : null;
|
|
7933
|
+
});
|
|
7503
7934
|
}
|
|
7504
7935
|
|
|
7505
7936
|
#writeAuthSchemaVersion(version: number): void {
|
|
7506
|
-
this.#
|
|
7937
|
+
this.#withStatement("INSERT OR REPLACE INTO auth_schema_version(id, version) VALUES (1, ?)", statement => {
|
|
7938
|
+
statement.run(version);
|
|
7939
|
+
});
|
|
7507
7940
|
}
|
|
7508
7941
|
|
|
7509
7942
|
#inferAuthSchemaVersion(): number {
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7943
|
+
return this.#withStatement("PRAGMA table_info(auth_credentials)", statement => {
|
|
7944
|
+
const cols = statement.all() as Array<{ name?: string }>;
|
|
7945
|
+
const hasDisabledCause = cols.some(column => column.name === "disabled_cause");
|
|
7946
|
+
const hasIdentityKey = cols.some(column => column.name === "identity_key");
|
|
7947
|
+
const hasAccountId = cols.some(column => column.name === "account_id");
|
|
7948
|
+
const hasEmail = cols.some(column => column.name === "email");
|
|
7949
|
+
if (hasIdentityKey) return 3;
|
|
7950
|
+
if (hasAccountId || hasEmail) return 2;
|
|
7951
|
+
if (hasDisabledCause) return 1;
|
|
7952
|
+
return 0;
|
|
7953
|
+
});
|
|
7519
7954
|
}
|
|
7520
7955
|
|
|
7521
7956
|
#createAuthCredentialsTable(): void {
|
|
@@ -7559,7 +7994,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7559
7994
|
|
|
7560
7995
|
#migrateAuthSchemaV0ToV1(): void {
|
|
7561
7996
|
const migrate = this.#db.transaction(() => {
|
|
7562
|
-
const v0Cols = this.#
|
|
7997
|
+
const v0Cols = this.#withStatement(
|
|
7998
|
+
"PRAGMA table_info(auth_credentials)",
|
|
7999
|
+
statement => statement.all() as Array<{ name?: string }>,
|
|
8000
|
+
);
|
|
7563
8001
|
const hasDisabled = v0Cols.some(col => col.name === "disabled");
|
|
7564
8002
|
|
|
7565
8003
|
this.#db.run("ALTER TABLE auth_credentials RENAME TO auth_credentials_v0");
|
|
@@ -7635,24 +8073,27 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7635
8073
|
migrate.immediate();
|
|
7636
8074
|
}
|
|
7637
8075
|
#migrateAuthSchemaV4ToV5(): void {
|
|
7638
|
-
const columns = this.#
|
|
8076
|
+
const columns = this.#withStatement(
|
|
8077
|
+
"PRAGMA table_info(auth_credentials)",
|
|
8078
|
+
statement => statement.all() as Array<{ name?: string }>,
|
|
8079
|
+
);
|
|
7639
8080
|
if (columns.some(column => column.name === "revision")) return;
|
|
7640
8081
|
this.#db.run("ALTER TABLE auth_credentials ADD COLUMN revision INTEGER NOT NULL DEFAULT 1");
|
|
7641
8082
|
}
|
|
7642
8083
|
|
|
7643
8084
|
#backfillCredentialIdentityKeys(): void {
|
|
7644
|
-
const rows = this.#
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
.all() as AuthRow[];
|
|
8085
|
+
const rows = this.#withStatement(
|
|
8086
|
+
"SELECT id, provider, credential_type, data, disabled_cause, identity_key, revision FROM auth_credentials WHERE identity_key IS NULL ORDER BY id ASC",
|
|
8087
|
+
statement => statement.all() as AuthRow[],
|
|
8088
|
+
);
|
|
7649
8089
|
if (rows.length === 0) return;
|
|
7650
8090
|
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
8091
|
+
this.#withStatement("UPDATE auth_credentials SET identity_key = ? WHERE id = ?", updateIdentity => {
|
|
8092
|
+
for (const row of rows) {
|
|
8093
|
+
const identityKey = resolveRowCredentialIdentityKey(row.provider, row);
|
|
8094
|
+
updateIdentity.run(identityKey, row.id);
|
|
8095
|
+
}
|
|
8096
|
+
});
|
|
7656
8097
|
}
|
|
7657
8098
|
|
|
7658
8099
|
// ─── AuthCredentialStore interface ──────────────────────────────────────
|
|
@@ -7681,10 +8122,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7681
8122
|
for (const row of rows) {
|
|
7682
8123
|
const credential = deserializeCredential(row);
|
|
7683
8124
|
if (!credential) continue;
|
|
7684
|
-
const identityLabel =
|
|
7685
|
-
credential.type === "oauth"
|
|
7686
|
-
? (credential.email ?? credential.accountId ?? credential.projectId ?? null)
|
|
7687
|
-
: null;
|
|
8125
|
+
const identityLabel = credential.type === "oauth" ? formatOAuthIdentityLabel(credential) : null;
|
|
7688
8126
|
results.push({
|
|
7689
8127
|
id: row.id,
|
|
7690
8128
|
provider: row.provider,
|
|
@@ -7693,6 +8131,12 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7693
8131
|
...(credential.type === "oauth" && credential.accountId ? { accountId: credential.accountId } : {}),
|
|
7694
8132
|
...(credential.type === "oauth" && credential.email ? { email: credential.email } : {}),
|
|
7695
8133
|
...(credential.type === "oauth" && credential.projectId ? { projectId: credential.projectId } : {}),
|
|
8134
|
+
...(credential.type === "oauth" && credential.organizationId
|
|
8135
|
+
? { organizationId: credential.organizationId }
|
|
8136
|
+
: {}),
|
|
8137
|
+
...(credential.type === "oauth" && credential.organizationName
|
|
8138
|
+
? { organizationName: credential.organizationName }
|
|
8139
|
+
: {}),
|
|
7696
8140
|
disabled: row.disabled_cause !== null,
|
|
7697
8141
|
disabledCause: row.disabled_cause,
|
|
7698
8142
|
});
|
|
@@ -7715,11 +8159,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7715
8159
|
const remove = this.#db.transaction((): AuthCredentialHardRemovalResult => {
|
|
7716
8160
|
const currentIds: number[] = [];
|
|
7717
8161
|
for (const target of unique) {
|
|
7718
|
-
const row = this.#
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
8162
|
+
const row = this.#withStatement(
|
|
8163
|
+
"SELECT id, provider, credential_type, revision FROM auth_credentials WHERE id = ?",
|
|
8164
|
+
statement =>
|
|
8165
|
+
statement.get(target.id) as
|
|
8166
|
+
| { id?: number; provider?: string; credential_type?: string; revision?: number }
|
|
8167
|
+
| undefined,
|
|
8168
|
+
);
|
|
7723
8169
|
if (!row || row.provider !== provider || row.revision !== target.expectedRevision) {
|
|
7724
8170
|
currentIds.push(row?.id ?? target.id);
|
|
7725
8171
|
}
|
|
@@ -7727,12 +8173,16 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7727
8173
|
if (currentIds.length > 0) return { kind: "conflict", currentIds };
|
|
7728
8174
|
for (const target of unique) {
|
|
7729
8175
|
this.#hardDeleteStmt.run(target.id);
|
|
7730
|
-
this.#
|
|
7731
|
-
|
|
8176
|
+
this.#withStatement("DELETE FROM oauth_refresh_leases WHERE credential_id = ?", statement => {
|
|
8177
|
+
statement.run(target.id);
|
|
8178
|
+
});
|
|
8179
|
+
this.#withStatement("DELETE FROM cache WHERE key = ?", statement => {
|
|
8180
|
+
statement.run(`${HEALTH_CACHE_PREFIX}${target.id}`);
|
|
8181
|
+
});
|
|
7732
8182
|
}
|
|
7733
|
-
this.#
|
|
7734
|
-
.
|
|
7735
|
-
|
|
8183
|
+
this.#withStatement("DELETE FROM cache WHERE substr(key, 1, ?) = ?", statement => {
|
|
8184
|
+
statement.run(`usage_cache:report:${provider}:`.length, `usage_cache:report:${provider}:`);
|
|
8185
|
+
});
|
|
7736
8186
|
return { kind: "removed", ids: unique.map(target => target.id) };
|
|
7737
8187
|
});
|
|
7738
8188
|
return remove.immediate();
|
|
@@ -7749,11 +8199,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7749
8199
|
const enteredAtMs = Date.now();
|
|
7750
8200
|
const claim = this.#db.transaction((): OAuthRefreshLeaseClaim => {
|
|
7751
8201
|
const effectiveNowMs = nowMs + Math.max(0, Date.now() - enteredAtMs);
|
|
7752
|
-
const row = this.#
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
.get(credentialId) as AuthRow | undefined;
|
|
8202
|
+
const row = this.#withStatement(
|
|
8203
|
+
"SELECT id, provider, credential_type, data, disabled_cause, identity_key, revision FROM auth_credentials WHERE id = ? AND disabled_cause IS NULL",
|
|
8204
|
+
statement => statement.get(credentialId) as AuthRow | undefined,
|
|
8205
|
+
);
|
|
7757
8206
|
const credential = row ? deserializeCredential(row) : null;
|
|
7758
8207
|
if (credential?.type !== "oauth") return { kind: "missing" };
|
|
7759
8208
|
if (
|
|
@@ -7770,14 +8219,16 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7770
8219
|
) {
|
|
7771
8220
|
return { kind: "adopted", credential };
|
|
7772
8221
|
}
|
|
7773
|
-
const active = this.#
|
|
7774
|
-
|
|
7775
|
-
.get(credentialId) as { owner?: string; expires_at?: number } | undefined
|
|
8222
|
+
const active = this.#withStatement(
|
|
8223
|
+
"SELECT owner, expires_at FROM oauth_refresh_leases WHERE credential_id = ?",
|
|
8224
|
+
statement => statement.get(credentialId) as { owner?: string; expires_at?: number } | undefined,
|
|
8225
|
+
);
|
|
7776
8226
|
if (typeof active?.expires_at === "number" && active.expires_at > effectiveNowMs) {
|
|
7777
8227
|
if (active.owner === owner) {
|
|
7778
|
-
this.#
|
|
7779
|
-
|
|
7780
|
-
.run(effectiveNowMs + leaseMs, credentialId, owner)
|
|
8228
|
+
this.#withStatement(
|
|
8229
|
+
"UPDATE oauth_refresh_leases SET expires_at = ? WHERE credential_id = ? AND owner = ?",
|
|
8230
|
+
statement => statement.run(effectiveNowMs + leaseMs, credentialId, owner),
|
|
8231
|
+
);
|
|
7781
8232
|
return {
|
|
7782
8233
|
kind: "claimed",
|
|
7783
8234
|
credential,
|
|
@@ -7791,13 +8242,14 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7791
8242
|
return { kind: "busy", expiresAt: active.expires_at };
|
|
7792
8243
|
}
|
|
7793
8244
|
|
|
7794
|
-
this.#
|
|
8245
|
+
this.#withStatement("DELETE FROM oauth_refresh_leases WHERE credential_id = ?", statement => {
|
|
8246
|
+
statement.run(credentialId);
|
|
8247
|
+
});
|
|
7795
8248
|
const tokenFingerprint = crypto.createHash("sha256").update(credential.refresh).digest("hex");
|
|
7796
|
-
this.#
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
.run(credentialId, owner, tokenFingerprint, effectiveNowMs + leaseMs);
|
|
8249
|
+
this.#withStatement(
|
|
8250
|
+
"INSERT INTO oauth_refresh_leases (credential_id, owner, token_fingerprint, expires_at) VALUES (?, ?, ?, ?)",
|
|
8251
|
+
statement => statement.run(credentialId, owner, tokenFingerprint, effectiveNowMs + leaseMs),
|
|
8252
|
+
);
|
|
7801
8253
|
return { kind: "claimed", credential, lease: { credentialId, owner, tokenFingerprint } };
|
|
7802
8254
|
});
|
|
7803
8255
|
return claim.immediate();
|
|
@@ -7805,22 +8257,24 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7805
8257
|
|
|
7806
8258
|
completeOAuthRefreshLease(lease: OAuthRefreshLease, credential: OAuthCredential): boolean {
|
|
7807
8259
|
const serialized = serializeCredential(
|
|
7808
|
-
(
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
| undefined
|
|
8260
|
+
this.#withStatement(
|
|
8261
|
+
"SELECT provider FROM auth_credentials WHERE id = ?",
|
|
8262
|
+
statement => statement.get(lease.credentialId) as { provider?: string } | undefined,
|
|
7812
8263
|
)?.provider ?? "",
|
|
7813
8264
|
credential,
|
|
7814
8265
|
);
|
|
7815
8266
|
if (!serialized) return false;
|
|
7816
8267
|
const complete = this.#db.transaction(() => {
|
|
7817
|
-
const leaseRow = this.#
|
|
7818
|
-
|
|
7819
|
-
|
|
8268
|
+
const leaseRow = this.#withStatement(
|
|
8269
|
+
"SELECT owner, token_fingerprint FROM oauth_refresh_leases WHERE credential_id = ?",
|
|
8270
|
+
statement =>
|
|
8271
|
+
statement.get(lease.credentialId) as { owner?: string; token_fingerprint?: string } | undefined,
|
|
8272
|
+
);
|
|
7820
8273
|
if (leaseRow?.owner !== lease.owner || leaseRow.token_fingerprint !== lease.tokenFingerprint) return false;
|
|
7821
|
-
const row = this.#
|
|
7822
|
-
|
|
7823
|
-
.get(lease.credentialId) as { data?: string } | undefined
|
|
8274
|
+
const row = this.#withStatement(
|
|
8275
|
+
"SELECT data FROM auth_credentials WHERE id = ? AND disabled_cause IS NULL",
|
|
8276
|
+
statement => statement.get(lease.credentialId) as { data?: string } | undefined,
|
|
8277
|
+
);
|
|
7824
8278
|
if (
|
|
7825
8279
|
!row ||
|
|
7826
8280
|
crypto
|
|
@@ -7830,18 +8284,18 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
7830
8284
|
)
|
|
7831
8285
|
return false;
|
|
7832
8286
|
this.#updateStmt.run(serialized.credentialType, serialized.data, serialized.identityKey, lease.credentialId);
|
|
7833
|
-
this.#
|
|
7834
|
-
.
|
|
7835
|
-
|
|
8287
|
+
this.#withStatement("DELETE FROM oauth_refresh_leases WHERE credential_id = ? AND owner = ?", statement => {
|
|
8288
|
+
statement.run(lease.credentialId, lease.owner);
|
|
8289
|
+
});
|
|
7836
8290
|
return true;
|
|
7837
8291
|
});
|
|
7838
8292
|
return complete.immediate();
|
|
7839
8293
|
}
|
|
7840
8294
|
|
|
7841
8295
|
releaseOAuthRefreshLease(lease: OAuthRefreshLease): void {
|
|
7842
|
-
this.#
|
|
7843
|
-
.
|
|
7844
|
-
|
|
8296
|
+
this.#withStatement("DELETE FROM oauth_refresh_leases WHERE credential_id = ? AND owner = ?", statement => {
|
|
8297
|
+
statement.run(lease.credentialId, lease.owner);
|
|
8298
|
+
});
|
|
7845
8299
|
}
|
|
7846
8300
|
|
|
7847
8301
|
replaceAuthCredentialsForProvider(provider: string, credentials: AuthCredential[]): StoredAuthCredential[] {
|
|
@@ -8065,9 +8519,10 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
8065
8519
|
}
|
|
8066
8520
|
|
|
8067
8521
|
updateAuthCredential(id: number, credential: AuthCredential): void {
|
|
8068
|
-
const providerRow = this.#
|
|
8069
|
-
|
|
8070
|
-
| undefined
|
|
8522
|
+
const providerRow = this.#withStatement(
|
|
8523
|
+
"SELECT provider FROM auth_credentials WHERE id = ?",
|
|
8524
|
+
statement => statement.get(id) as { provider?: string } | undefined,
|
|
8525
|
+
);
|
|
8071
8526
|
const provider = providerRow?.provider ?? "";
|
|
8072
8527
|
const serialized = serializeCredential(provider, credential);
|
|
8073
8528
|
if (!serialized) return;
|
|
@@ -8266,25 +8721,23 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
8266
8721
|
close(): void {
|
|
8267
8722
|
if (this.#closed) return;
|
|
8268
8723
|
this.#closed = true;
|
|
8269
|
-
|
|
8270
|
-
this.#
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
this.#
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
this.#deleteExpiredCacheStmt.finalize();
|
|
8288
|
-
this.#db.close(true);
|
|
8724
|
+
const errors: unknown[] = [];
|
|
8725
|
+
for (const statement of this.#ownedStatements) {
|
|
8726
|
+
try {
|
|
8727
|
+
statement.finalize();
|
|
8728
|
+
} catch (error) {
|
|
8729
|
+
errors.push(error);
|
|
8730
|
+
}
|
|
8731
|
+
}
|
|
8732
|
+
this.#ownedStatements.length = 0;
|
|
8733
|
+
try {
|
|
8734
|
+
this.#db.close(true);
|
|
8735
|
+
} catch (error) {
|
|
8736
|
+
errors.push(error);
|
|
8737
|
+
}
|
|
8738
|
+
if (errors.length === 1) throw errors[0];
|
|
8739
|
+
if (errors.length > 1) {
|
|
8740
|
+
throw new AggregateError(errors, "Failed to close SQLite auth credential store", { cause: errors[0] });
|
|
8741
|
+
}
|
|
8289
8742
|
}
|
|
8290
8743
|
}
|