@gajae-code/ai 0.18.10 → 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.
Files changed (75) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +1 -1
  3. package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
  4. package/dist/types/auth-storage.d.ts +80 -8
  5. package/dist/types/bedrock-claude-cache-policy.d.ts +1 -1
  6. package/dist/types/model-cache.d.ts +6 -4
  7. package/dist/types/model-manager.d.ts +2 -0
  8. package/dist/types/openai-completions-compat.d.ts +2 -1
  9. package/dist/types/providers/anthropic.d.ts +10 -0
  10. package/dist/types/providers/aws-credential-config.d.ts +5 -0
  11. package/dist/types/providers/google-auth.d.ts +7 -0
  12. package/dist/types/providers/google-gemini-headers.d.ts +10 -5
  13. package/dist/types/providers/kiro-api-key.d.ts +5 -0
  14. package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
  15. package/dist/types/providers/openai-completions.d.ts +1 -1
  16. package/dist/types/providers/openai-responses-shared.d.ts +2 -1
  17. package/dist/types/stream.d.ts +33 -1
  18. package/dist/types/types.d.ts +26 -3
  19. package/dist/types/usage.d.ts +3 -0
  20. package/dist/types/utils/discovery/antigravity.d.ts +11 -0
  21. package/dist/types/utils/idle-iterator.d.ts +3 -2
  22. package/dist/types/utils/oauth/types.d.ts +9 -0
  23. package/package.json +4 -4
  24. package/src/auth-broker/remote-store.ts +62 -11
  25. package/src/auth-broker/wire-schemas.ts +2 -0
  26. package/src/auth-gateway/server.ts +3 -2
  27. package/src/auth-storage.ts +647 -194
  28. package/src/bedrock-claude-cache-policy.d.ts +1 -1
  29. package/src/bedrock-claude-cache-policy.ts +2 -2
  30. package/src/model-cache.ts +45 -10
  31. package/src/model-manager.ts +18 -6
  32. package/src/model-thinking.ts +37 -7
  33. package/src/models.json +2931 -1670
  34. package/src/openai-completions-compat.d.ts +2 -1
  35. package/src/openai-completions-compat.ts +5 -0
  36. package/src/provider-models/google.ts +7 -1
  37. package/src/provider-models/openai-compat.ts +26 -0
  38. package/src/providers/amazon-bedrock.ts +20 -11
  39. package/src/providers/anthropic.d.ts +10 -0
  40. package/src/providers/anthropic.ts +139 -4
  41. package/src/providers/aws-credential-config.d.ts +5 -0
  42. package/src/providers/aws-credential-config.ts +29 -1
  43. package/src/providers/google-auth.d.ts +7 -0
  44. package/src/providers/google-auth.ts +165 -4
  45. package/src/providers/google-gemini-headers.d.ts +10 -5
  46. package/src/providers/google-gemini-headers.ts +11 -6
  47. package/src/providers/google-vertex.ts +2 -4
  48. package/src/providers/kiro-api-key.d.ts +5 -0
  49. package/src/providers/kiro-api-key.ts +379 -68
  50. package/src/providers/ollama.ts +19 -9
  51. package/src/providers/openai-anthropic-shim.ts +1 -0
  52. package/src/providers/openai-chat-server-schema.ts +1 -1
  53. package/src/providers/openai-chat-server.ts +14 -2
  54. package/src/providers/openai-codex-responses.ts +21 -14
  55. package/src/providers/openai-completions.d.ts +1 -1
  56. package/src/providers/openai-completions.ts +65 -4
  57. package/src/providers/openai-responses-server.ts +10 -1
  58. package/src/providers/openai-responses-shared.d.ts +2 -1
  59. package/src/providers/openai-responses-shared.ts +42 -1
  60. package/src/providers/openai-responses.ts +51 -10
  61. package/src/providers/pi-native-server.ts +1 -1
  62. package/src/stream.d.ts +33 -1
  63. package/src/stream.ts +107 -13
  64. package/src/types.d.ts +26 -3
  65. package/src/types.ts +55 -2
  66. package/src/usage/claude.ts +4 -2
  67. package/src/usage.ts +3 -0
  68. package/src/utils/discovery/antigravity.ts +144 -2
  69. package/src/utils/idle-iterator.d.ts +3 -2
  70. package/src/utils/idle-iterator.ts +30 -10
  71. package/src/utils/oauth/anthropic.ts +23 -22
  72. package/src/utils/oauth/api-key-validation.ts +34 -11
  73. package/src/utils/oauth/types.d.ts +9 -0
  74. package/src/utils/oauth/types.ts +9 -0
  75. package/src/utils/parse-bind.d.ts +35 -0
@@ -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
- getProviderEvidenceGeneration(provider: string, resolvedApiKey?: string, owner?: object): string {
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.#getStoredCredentials(storageProvider).filter(entry =>
2091
- this.#credentialMatchesSelector(entry, selector),
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
- ? previousEntries.find(entry => this.#credentialMatchesSelector(entry, selector))
2190
- : undefined;
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(provider: string, entries: StoredCredential[]): StoredCredential[] {
2786
- const seen = new Set<string>();
2787
- const kept: StoredCredential[] = [];
2788
- const removed: StoredCredential[] = [];
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
- const credential = entry.credential;
2792
- if (credential.type !== "oauth") {
2793
- kept.push(entry);
2794
- continue;
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
- seen.add(identityKey);
2806
- kept.push(entry);
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.reverse();
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
- for (let index = 0; index < stored.length; index++) {
2958
- const entry = stored[index];
2959
- if (entry && this.#credentialMatchesSelector(entry, selector)) return { ...entry, index };
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
- return undefined;
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 = previousEntries.find(entry => this.#credentialMatchesSelector(entry, selector));
3283
- if (selected && removedIds.has(selected.id)) {
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
- * Check whether configured auth is currently usable without resolving credentials.
3546
- */
3547
- hasUsableAuth(provider: string, options?: Pick<AuthApiKeyOptions, "owner">): boolean {
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 preferredSelection = preferredSelector
5689
- ? this.#findCredentialBySelector(provider, preferredSelector)
5690
- : undefined;
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?: Pick<AuthApiKeyOptions, "owner"> & { sessionId?: string },
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, undefined, options?.sessionId)) return undefined;
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.email ?? credential.accountId ?? credential.projectId ?? `cred ${chosen.id}`)
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 resolveProviderCredentialIdentityKey(provider, extractOAuthCredentialIdentifiers(credential));
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.#db.prepare(
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.#db.prepare(
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.#db.prepare(
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.#db.prepare(
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.#db.prepare(
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.#db.prepare(
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.#db.prepare(
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.#db.prepare(
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.#db.prepare(
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.#db.prepare(
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.#db.prepare(
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.#db.prepare("DELETE FROM auth_credentials WHERE id = ?");
7404
- this.#getCacheStmt = this.#db.prepare(
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.#db.prepare("SELECT value FROM cache WHERE key = ?");
7408
- this.#upsertCacheStmt = this.#db.prepare(
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.#db.prepare(
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.#db.prepare("DELETE FROM cache WHERE key = ? AND value = ?");
7415
- this.#deleteCachePrefixStmt = this.#db.prepare("DELETE FROM cache WHERE substr(key, 1, ?) = ?");
7416
- this.#deleteExpiredCacheStmt = this.#db.prepare(`DELETE FROM cache WHERE expires_at <= ${SQLITE_NOW_EPOCH}`);
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
- const row = this.#db
7493
- .prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'auth_credentials'")
7494
- .get() as { present?: number } | undefined;
7495
- return row?.present === 1;
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
- const row = this.#db.prepare("SELECT version FROM auth_schema_version WHERE id = 1").get() as
7500
- | { version?: number }
7501
- | undefined;
7502
- return typeof row?.version === "number" ? row.version : null;
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.#db.prepare("INSERT OR REPLACE INTO auth_schema_version(id, version) VALUES (1, ?)").run(version);
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
- const cols = this.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>;
7511
- const hasDisabledCause = cols.some(column => column.name === "disabled_cause");
7512
- const hasIdentityKey = cols.some(column => column.name === "identity_key");
7513
- const hasAccountId = cols.some(column => column.name === "account_id");
7514
- const hasEmail = cols.some(column => column.name === "email");
7515
- if (hasIdentityKey) return 3;
7516
- if (hasAccountId || hasEmail) return 2;
7517
- if (hasDisabledCause) return 1;
7518
- return 0;
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.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>;
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.#db.prepare("PRAGMA table_info(auth_credentials)").all() as Array<{ name?: string }>;
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.#db
7645
- .prepare(
7646
- "SELECT id, provider, credential_type, data, disabled_cause, identity_key, revision FROM auth_credentials WHERE identity_key IS NULL ORDER BY id ASC",
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
- const updateIdentity = this.#db.prepare("UPDATE auth_credentials SET identity_key = ? WHERE id = ?");
7652
- for (const row of rows) {
7653
- const identityKey = resolveRowCredentialIdentityKey(row.provider, row);
7654
- updateIdentity.run(identityKey, row.id);
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.#db
7719
- .prepare("SELECT id, provider, credential_type, revision FROM auth_credentials WHERE id = ?")
7720
- .get(target.id) as
7721
- | { id?: number; provider?: string; credential_type?: string; revision?: number }
7722
- | undefined;
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.#db.prepare("DELETE FROM oauth_refresh_leases WHERE credential_id = ?").run(target.id);
7731
- this.#db.prepare("DELETE FROM cache WHERE key = ?").run(`${HEALTH_CACHE_PREFIX}${target.id}`);
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.#db
7734
- .prepare("DELETE FROM cache WHERE substr(key, 1, ?) = ?")
7735
- .run(`usage_cache:report:${provider}:`.length, `usage_cache:report:${provider}:`);
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.#db
7753
- .prepare(
7754
- "SELECT id, provider, credential_type, data, disabled_cause, identity_key, revision FROM auth_credentials WHERE id = ? AND disabled_cause IS NULL",
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.#db
7774
- .prepare("SELECT owner, expires_at FROM oauth_refresh_leases WHERE credential_id = ?")
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.#db
7779
- .prepare("UPDATE oauth_refresh_leases SET expires_at = ? WHERE credential_id = ? AND owner = ?")
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.#db.prepare("DELETE FROM oauth_refresh_leases WHERE credential_id = ?").run(credentialId);
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.#db
7797
- .prepare(
7798
- "INSERT INTO oauth_refresh_leases (credential_id, owner, token_fingerprint, expires_at) VALUES (?, ?, ?, ?)",
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
- this.#db.prepare("SELECT provider FROM auth_credentials WHERE id = ?").get(lease.credentialId) as
7810
- | { provider?: string }
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.#db
7818
- .prepare("SELECT owner, token_fingerprint FROM oauth_refresh_leases WHERE credential_id = ?")
7819
- .get(lease.credentialId) as { owner?: string; token_fingerprint?: string } | undefined;
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.#db
7822
- .prepare("SELECT data FROM auth_credentials WHERE id = ? AND disabled_cause IS NULL")
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.#db
7834
- .prepare("DELETE FROM oauth_refresh_leases WHERE credential_id = ? AND owner = ?")
7835
- .run(lease.credentialId, lease.owner);
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.#db
7843
- .prepare("DELETE FROM oauth_refresh_leases WHERE credential_id = ? AND owner = ?")
7844
- .run(lease.credentialId, lease.owner);
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.#db.prepare("SELECT provider FROM auth_credentials WHERE id = ?").get(id) as
8069
- | { provider?: string }
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
- this.#listActiveStmt.finalize();
8270
- this.#listActiveByProviderStmt.finalize();
8271
- this.#listAllStmt.finalize();
8272
- this.#listAllByProviderStmt.finalize();
8273
- this.#listDisabledByProviderStmt.finalize();
8274
- this.#insertStmt.finalize();
8275
- this.#updateStmt.finalize();
8276
- this.#deleteStmt.finalize();
8277
- this.#deleteIfMatchesStmt.finalize();
8278
- this.#deleteIfRevisionStmt.finalize();
8279
- this.#deleteByProviderStmt.finalize();
8280
- this.#hardDeleteStmt.finalize();
8281
- this.#getCacheStmt.finalize();
8282
- this.#getCacheIncludingExpiredStmt.finalize();
8283
- this.#upsertCacheStmt.finalize();
8284
- this.#claimUsageFetchLeaseStmt.finalize();
8285
- this.#releaseUsageFetchLeaseStmt.finalize();
8286
- this.#deleteCachePrefixStmt.finalize();
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
  }