@mastra/code-sdk 1.8.0-alpha.7 → 1.8.0-alpha.9

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 (79) hide show
  1. package/dist/agents/mastracode-gateway.d.ts +2 -0
  2. package/dist/agents/mastracode-gateway.d.ts.map +1 -1
  3. package/dist/agents/mastracode-gateway.js +29 -10
  4. package/dist/agents/mastracode-gateway.js.map +1 -1
  5. package/dist/agents/memory.d.ts +1 -1
  6. package/dist/agents/memory.d.ts.map +1 -1
  7. package/dist/agents/memory.js +32 -25
  8. package/dist/agents/memory.js.map +1 -1
  9. package/dist/agents/model.d.ts +41 -1
  10. package/dist/agents/model.d.ts.map +1 -1
  11. package/dist/agents/model.js +141 -7
  12. package/dist/agents/model.js.map +1 -1
  13. package/dist/auth/account-rotation-processor.d.ts +207 -0
  14. package/dist/auth/account-rotation-processor.d.ts.map +1 -0
  15. package/dist/auth/account-rotation-processor.js +669 -0
  16. package/dist/auth/account-rotation-processor.js.map +1 -0
  17. package/dist/auth/account-routing-context.d.ts +13 -0
  18. package/dist/auth/account-routing-context.d.ts.map +1 -0
  19. package/dist/auth/account-routing-context.js +41 -0
  20. package/dist/auth/account-routing-context.js.map +1 -0
  21. package/dist/auth/index.d.ts +1 -0
  22. package/dist/auth/index.d.ts.map +1 -1
  23. package/dist/auth/index.js +2 -1
  24. package/dist/auth/providers/anthropic.d.ts.map +1 -1
  25. package/dist/auth/providers/anthropic.js +2 -8
  26. package/dist/auth/providers/anthropic.js.map +1 -1
  27. package/dist/auth/providers/github-copilot.d.ts.map +1 -1
  28. package/dist/auth/providers/github-copilot.js +34 -12
  29. package/dist/auth/providers/github-copilot.js.map +1 -1
  30. package/dist/auth/providers/kimi-coding.d.ts.map +1 -1
  31. package/dist/auth/providers/kimi-coding.js +2 -6
  32. package/dist/auth/providers/kimi-coding.js.map +1 -1
  33. package/dist/auth/providers/openai-codex.d.ts +3 -1
  34. package/dist/auth/providers/openai-codex.d.ts.map +1 -1
  35. package/dist/auth/providers/openai-codex.js +50 -24
  36. package/dist/auth/providers/openai-codex.js.map +1 -1
  37. package/dist/auth/providers/xai.d.ts +1 -1
  38. package/dist/auth/providers/xai.d.ts.map +1 -1
  39. package/dist/auth/providers/xai.js +36 -16
  40. package/dist/auth/providers/xai.js.map +1 -1
  41. package/dist/auth/storage.d.ts +130 -8
  42. package/dist/auth/storage.d.ts.map +1 -1
  43. package/dist/auth/storage.js +602 -39
  44. package/dist/auth/storage.js.map +1 -1
  45. package/dist/auth/types.d.ts +69 -2
  46. package/dist/auth/types.d.ts.map +1 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +88 -23
  49. package/dist/index.js.map +1 -1
  50. package/dist/onboarding/custom-packs.d.ts.map +1 -1
  51. package/dist/onboarding/custom-packs.js +3 -0
  52. package/dist/onboarding/custom-packs.js.map +1 -1
  53. package/dist/onboarding/index.js +1 -1
  54. package/dist/onboarding/packs.d.ts +44 -0
  55. package/dist/onboarding/packs.d.ts.map +1 -1
  56. package/dist/onboarding/packs.js +80 -2
  57. package/dist/onboarding/packs.js.map +1 -1
  58. package/dist/onboarding/settings.d.ts +28 -0
  59. package/dist/onboarding/settings.d.ts.map +1 -1
  60. package/dist/onboarding/settings.js +81 -8
  61. package/dist/onboarding/settings.js.map +1 -1
  62. package/dist/providers/claude-max.d.ts.map +1 -1
  63. package/dist/providers/claude-max.js +4 -5
  64. package/dist/providers/claude-max.js.map +1 -1
  65. package/dist/providers/github-copilot.d.ts +1 -1
  66. package/dist/providers/github-copilot.d.ts.map +1 -1
  67. package/dist/providers/github-copilot.js +89 -37
  68. package/dist/providers/github-copilot.js.map +1 -1
  69. package/dist/providers/kimi-coding.d.ts.map +1 -1
  70. package/dist/providers/kimi-coding.js +16 -2
  71. package/dist/providers/kimi-coding.js.map +1 -1
  72. package/dist/providers/openai-codex.d.ts.map +1 -1
  73. package/dist/providers/openai-codex.js +42 -27
  74. package/dist/providers/openai-codex.js.map +1 -1
  75. package/dist/schema.d.ts +27 -0
  76. package/dist/schema.d.ts.map +1 -1
  77. package/dist/schema.js +9 -0
  78. package/dist/schema.js.map +1 -1
  79. package/package.json +8 -8
@@ -4,7 +4,8 @@ import { githubCopilotOAuthProvider } from "./providers/github-copilot.js";
4
4
  import { kimiCodingOAuthProvider } from "./providers/kimi-coding.js";
5
5
  import { openaiCodexOAuthProvider } from "./providers/openai-codex.js";
6
6
  import { xaiOAuthProvider } from "./providers/xai.js";
7
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { createHash, randomUUID } from "crypto";
8
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
8
9
  import { dirname, join } from "path";
9
10
  //#region src/auth/storage.ts
10
11
  /**
@@ -42,6 +43,55 @@ function getOAuthProviders() {
42
43
  return Array.from(oauthProviderRegistry.values());
43
44
  }
44
45
  /**
46
+ * Mint an account instance id: `${providerId}:${randomUUID()}`.
47
+ *
48
+ * Assigned once, when the account is created, and never derived from
49
+ * credentials. Deriving it from the refresh token (the pre-A13 scheme) looked
50
+ * stable but was not: `persistRefreshedCredential` keeps an entry's key while
51
+ * writing rotated tokens onto it, so every refresh left the id describing a
52
+ * credential the account no longer had — and re-adding that same subscription
53
+ * then failed to match its own entry and registered a second one for it.
54
+ */
55
+ function mintAccountId(providerId) {
56
+ return `${providerId}:${randomUUID()}`;
57
+ }
58
+ /**
59
+ * The pre-A13 id scheme: `${providerId}:${sha256(refresh).slice(0,8)}`.
60
+ *
61
+ * No longer used to assign ids, but still needed to recognize registries
62
+ * written before A13 — both to match a re-authorized account onto its existing
63
+ * entry, and to recognize entries whose fields the A13 migration must backfill.
64
+ */
65
+ function legacyAccountIdFor(providerId, refreshToken) {
66
+ return `${providerId}:${createHash("sha256").update(refreshToken).digest("hex").slice(0, 8)}`;
67
+ }
68
+ /**
69
+ * Stable identity for the *subscription* behind an account, when the provider
70
+ * exposes one (an account id, an email — anything that survives a refresh and
71
+ * a re-authorization). Used to decide whether an added account is one we
72
+ * already have. Providers whose credentials carry no such identifier return
73
+ * undefined; for those, a re-authorization is indistinguishable from a new
74
+ * account without spending a request.
75
+ */
76
+ function providerIdentity(provider, creds) {
77
+ try {
78
+ const identity = provider?.getAccountIdentity?.(creds);
79
+ return typeof identity === "string" && identity.length > 0 ? identity : void 0;
80
+ } catch {
81
+ return;
82
+ }
83
+ }
84
+ function isOAuthAccountRecord(value) {
85
+ if (!value || typeof value !== "object") return false;
86
+ const v = value;
87
+ return v.type === "oauth-account" && typeof v.id === "string" && typeof v.label === "string" && typeof v.addedAt === "string" && typeof v.active === "boolean" && typeof v.refresh === "string" && typeof v.access === "string" && typeof v.expires === "number";
88
+ }
89
+ /** The credential fields of an account record — everything but registry identity. */
90
+ function credentialFieldsOf(record) {
91
+ const { type: _type, id: _id, label: _label, addedAt: _addedAt, active: _active, identity: _identity, ...creds } = record;
92
+ return creds;
93
+ }
94
+ /**
45
95
  * Credential storage backed by a JSON file.
46
96
  */
47
97
  var AuthStorage = class {
@@ -65,6 +115,111 @@ var AuthStorage = class {
65
115
  } catch {
66
116
  this.data = {};
67
117
  }
118
+ this.migrate();
119
+ }
120
+ /**
121
+ * Bring auth.json up to the multi-account registry format. Runs on every
122
+ * load; saves only when something actually changed.
123
+ *
124
+ * 1. A legacy OAuth slot with no registry gets one adopted active entry
125
+ * (the slot's tokens stay put — the entry mirrors them).
126
+ * 2. When the slot's refresh token differs from the active entry's, an
127
+ * external writer (older build, another worktree) owns the slot — the
128
+ * slot wins and its tokens are adopted onto the active entry.
129
+ * 3. A registry with no active entry (hand-edited) self-heals to its first
130
+ * entry; malformed `accounts:` values are skipped, never fatal.
131
+ * 4. A registry whose legacy slot disappeared (partial write, an external
132
+ * hand-edit) self-heals by mirroring the active entry's tokens back into
133
+ * the slot, so `isLoggedIn()`/`getOAuthCredential()` keep agreeing with
134
+ * `listAccounts()`. `remove()`/`logout()` clear the registry with the
135
+ * slot, so they never leave the shape this heals.
136
+ */
137
+ migrate() {
138
+ let changed = false;
139
+ for (const key of Object.keys(this.data)) {
140
+ const slot = this.data[key];
141
+ if (!slot || slot.type !== "oauth") continue;
142
+ const providerId = key;
143
+ const entries = this.accountEntries(providerId);
144
+ if (entries.length === 0) {
145
+ this.adoptSlot(providerId, slot);
146
+ changed = true;
147
+ continue;
148
+ }
149
+ const active = entries.find((entry) => entry.active);
150
+ if (!active) {
151
+ this.data[this.accountKeyFor(entries[0].id)] = {
152
+ ...entries[0],
153
+ active: true
154
+ };
155
+ changed = true;
156
+ continue;
157
+ }
158
+ if (active.refresh !== slot.refresh) {
159
+ const matching = entries.find((entry) => entry.id !== active.id && entry.refresh === slot.refresh);
160
+ if (matching) {
161
+ const { type: _t, ...slotCreds } = slot;
162
+ this.data[this.accountKeyFor(matching.id)] = {
163
+ ...matching,
164
+ ...slotCreds,
165
+ type: "oauth-account",
166
+ active: true
167
+ };
168
+ this.data[this.accountKeyFor(active.id)] = {
169
+ ...active,
170
+ active: false
171
+ };
172
+ changed = true;
173
+ } else {
174
+ const { type: _type, ...slotCreds } = slot;
175
+ this.data[this.accountKeyFor(active.id)] = {
176
+ ...active,
177
+ ...slotCreds,
178
+ type: "oauth-account"
179
+ };
180
+ changed = true;
181
+ }
182
+ }
183
+ }
184
+ const registeredProviders = /* @__PURE__ */ new Set();
185
+ for (const key of Object.keys(this.data)) {
186
+ if (!key.startsWith("accounts:")) continue;
187
+ const providerId = key.slice(9).split(":")[0];
188
+ if (providerId) registeredProviders.add(providerId);
189
+ }
190
+ for (const providerId of registeredProviders) {
191
+ if (this.data[providerId]?.type === "oauth") continue;
192
+ const entries = this.accountEntries(providerId);
193
+ if (entries.length === 0) continue;
194
+ let active = entries.find((entry) => entry.active);
195
+ if (!active) {
196
+ active = entries[0];
197
+ this.data[this.accountKeyFor(active.id)] = {
198
+ ...active,
199
+ active: true
200
+ };
201
+ }
202
+ this.data[providerId] = {
203
+ type: "oauth",
204
+ ...credentialFieldsOf(active)
205
+ };
206
+ changed = true;
207
+ }
208
+ for (const providerId of registeredProviders) {
209
+ const provider = getOAuthProvider(providerId);
210
+ if (!provider?.getAccountIdentity) continue;
211
+ for (const entry of this.accountEntries(providerId)) {
212
+ if (entry.identity) continue;
213
+ const identity = providerIdentity(provider, entry);
214
+ if (!identity) continue;
215
+ this.data[this.accountKeyFor(entry.id)] = {
216
+ ...entry,
217
+ identity
218
+ };
219
+ changed = true;
220
+ }
221
+ }
222
+ if (changed) this.save();
68
223
  }
69
224
  /**
70
225
  * Save credentials to disk.
@@ -75,27 +230,45 @@ var AuthStorage = class {
75
230
  recursive: true,
76
231
  mode: 448
77
232
  });
78
- writeFileSync(this.authPath, JSON.stringify(this.data, null, 2), "utf-8");
79
- chmodSync(this.authPath, 384);
233
+ const tempPath = `${this.authPath}.${process.pid}.${randomUUID()}.tmp`;
234
+ try {
235
+ writeFileSync(tempPath, JSON.stringify(this.data, null, 2), {
236
+ encoding: "utf-8",
237
+ mode: 384
238
+ });
239
+ chmodSync(tempPath, 384);
240
+ renameSync(tempPath, this.authPath);
241
+ } finally {
242
+ rmSync(tempPath, { force: true });
243
+ }
80
244
  }
81
245
  /**
82
- * Get credential for a provider.
246
+ * Get credential for a provider. Registry entries (type 'oauth-account')
247
+ * live under `accounts:` keys and never surface here.
83
248
  */
84
249
  get(provider) {
85
- return this.data[provider] ?? void 0;
250
+ const cred = this.data[provider];
251
+ return cred !== void 0 && cred.type !== "oauth-account" ? cred : void 0;
86
252
  }
87
253
  /**
88
254
  * Set credential for a provider.
89
255
  */
90
256
  set(provider, credential) {
257
+ this.reload();
91
258
  this.data[provider] = credential;
92
259
  this.save();
93
260
  }
94
261
  /**
95
- * Remove credential for a provider.
262
+ * Remove credential for a provider, including its account registry.
263
+ * Clearing only the slot would leave the registry behind, and the next
264
+ * load's migration would heal the slot back from it — resurrecting the
265
+ * provider the caller just signed out.
96
266
  */
97
267
  remove(provider) {
268
+ this.reload();
98
269
  delete this.data[provider];
270
+ const prefix = this.accountPrefixFor(provider);
271
+ for (const key of Object.keys(this.data)) if (key.startsWith(prefix)) delete this.data[key];
99
272
  this.save();
100
273
  }
101
274
  /**
@@ -155,53 +328,443 @@ var AuthStorage = class {
155
328
  }
156
329
  }
157
330
  /**
158
- * Login to an OAuth provider.
331
+ * Login to an OAuth provider. Resolves to the registered account record.
332
+ * Pass `activate: false` to add the account without making it active (the
333
+ * first account for a provider is always activated — a provider with
334
+ * accounts must have an active one).
159
335
  */
160
- async login(providerId, callbacks) {
336
+ async login(providerId, callbacks, opts) {
161
337
  const provider = getOAuthProvider(providerId);
162
338
  if (!provider) throw new Error(`Unknown OAuth provider: ${providerId}`);
163
339
  const credentials = await provider.login(callbacks);
164
- this.set(providerId, {
165
- type: "oauth",
166
- ...credentials
167
- });
340
+ return this.addAccount(providerId, credentials, opts);
168
341
  }
169
342
  /**
170
- * Logout from a provider.
343
+ * Logout from a provider: remove the legacy slot and every registered account.
171
344
  */
172
345
  logout(provider) {
173
346
  this.remove(provider);
174
347
  }
348
+ accountPrefixFor(providerId) {
349
+ return `accounts:${providerId}:`;
350
+ }
351
+ accountKeyFor(id) {
352
+ return `accounts:${id}`;
353
+ }
354
+ /** Registry entries for a provider in JSON insertion order (malformed values skipped). */
355
+ accountEntries(providerId) {
356
+ const prefix = this.accountPrefixFor(providerId);
357
+ const out = [];
358
+ for (const [key, value] of Object.entries(this.data)) if (key.startsWith(prefix) && isOAuthAccountRecord(value)) out.push(value);
359
+ return out;
360
+ }
361
+ /** Adopt a legacy slot credential as the registry's first active entry. */
362
+ adoptSlot(providerId, slot) {
363
+ const id = mintAccountId(providerId);
364
+ const provider = getOAuthProvider(providerId);
365
+ const identity = providerIdentity(provider, slot);
366
+ const record = {
367
+ ...slot,
368
+ type: "oauth-account",
369
+ id,
370
+ ...identity ? { identity } : {},
371
+ label: `${provider?.name ?? providerId} account 1`,
372
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
373
+ active: true
374
+ };
375
+ this.data[this.accountKeyFor(id)] = record;
376
+ return record;
377
+ }
378
+ /**
379
+ * Registered OAuth accounts for a provider, in insertion order. Copies —
380
+ * callers cannot corrupt unsaved storage state by mutating a record.
381
+ */
382
+ listAccounts(providerId) {
383
+ return this.accountEntries(providerId).map((entry) => ({ ...entry }));
384
+ }
385
+ /**
386
+ * The provider's active registry entry, if a registry exists. A copy.
387
+ */
388
+ getActiveAccount(providerId) {
389
+ const active = this.accountEntries(providerId).find((entry) => entry.active);
390
+ return active ? { ...active } : void 0;
391
+ }
392
+ /**
393
+ * Register OAuth credentials as an account for a provider, making it active
394
+ * unless `activate: false` is passed.
395
+ *
396
+ * With `replaceAccountId` (re-authentication of a picked account), the
397
+ * target entry's tokens are replaced in place — id re-keyed to the new
398
+ * refresh-token hash, label/position/addedAt preserved — because providers
399
+ * rotate refresh tokens per authorization, so the picked account's old id
400
+ * never matches the new token hash. Re-authentication keeps the target's
401
+ * active state: an inactive account stays inactive and the currently active
402
+ * account keeps the legacy slot. Otherwise, when the new credentials
403
+ * hash to an existing entry's id (same refresh token), that entry is
404
+ * updated in place; a genuinely new account is appended and activated.
405
+ *
406
+ * `activate: false` only applies to the plain add path: the new/updated entry
407
+ * keeps the active state it had, and the previously active account stays
408
+ * active. The first account of a provider is activated regardless.
409
+ */
410
+ async addAccount(providerId, creds, opts) {
411
+ this.reload();
412
+ const entries = this.accountEntries(providerId);
413
+ if (opts?.replaceAccountId) {
414
+ const target = entries.find((entry) => entry.id === opts.replaceAccountId);
415
+ if (!target) throw new Error(`No account ${opts.replaceAccountId} for provider ${providerId}`);
416
+ const identity = providerIdentity(getOAuthProvider(providerId), creds) ?? target.identity;
417
+ const legacyId = legacyAccountIdFor(providerId, creds.refresh);
418
+ const collided = entries.find((entry) => entry.id !== target.id && (identity !== void 0 && entry.identity === identity || entry.id === legacyId || entry.refresh === creds.refresh));
419
+ const replacement = {
420
+ ...target,
421
+ ...collided ? credentialFieldsOf(collided) : {},
422
+ ...creds,
423
+ type: "oauth-account",
424
+ ...identity ? { identity } : {},
425
+ label: opts.label ?? target.label
426
+ };
427
+ const rebuilt = {};
428
+ for (const [key, value] of Object.entries(this.data)) if (key === this.accountKeyFor(target.id)) rebuilt[key] = collided?.active ? {
429
+ ...replacement,
430
+ active: true
431
+ } : replacement;
432
+ else if (collided && key === this.accountKeyFor(collided.id)) continue;
433
+ else rebuilt[key] = value;
434
+ this.data = rebuilt;
435
+ if (target.active === true || collided?.active === true || entries.some((entry) => entry.active && entry.id !== target.id) === false) {
436
+ const activated = this.activateInMemory(providerId, target.id);
437
+ if (!activated) throw new Error(`Failed to activate account ${target.id} for provider ${providerId}`);
438
+ this.save();
439
+ return activated;
440
+ }
441
+ const replacementEntry = this.accountEntries(providerId).find((entry) => entry.id === target.id);
442
+ if (!replacementEntry) throw new Error(`Failed to store account ${target.id} for provider ${providerId}`);
443
+ this.save();
444
+ return replacementEntry;
445
+ }
446
+ const provider = getOAuthProvider(providerId);
447
+ const identity = providerIdentity(provider, creds);
448
+ const label = opts?.label ?? await provider?.getAccountLabel?.(creds) ?? null;
449
+ this.reload();
450
+ const freshEntries = this.accountEntries(providerId);
451
+ const legacyId = legacyAccountIdFor(providerId, creds.refresh);
452
+ const existing = (identity ? freshEntries.find((entry) => entry.identity === identity) : void 0) ?? freshEntries.find((entry) => entry.id === legacyId) ?? freshEntries.find((entry) => entry.refresh === creds.refresh);
453
+ const id = existing?.id ?? mintAccountId(providerId);
454
+ if (existing) this.data[this.accountKeyFor(id)] = {
455
+ ...existing,
456
+ ...creds,
457
+ type: "oauth-account",
458
+ id,
459
+ ...identity ? { identity } : {},
460
+ active: existing.active
461
+ };
462
+ else {
463
+ const resolvedLabel = label ?? `${provider?.name ?? providerId} account ${freshEntries.length + 1}`;
464
+ this.data[this.accountKeyFor(id)] = {
465
+ type: "oauth-account",
466
+ id,
467
+ ...identity ? { identity } : {},
468
+ label: resolvedLabel,
469
+ addedAt: (/* @__PURE__ */ new Date()).toISOString(),
470
+ active: false,
471
+ ...creds
472
+ };
473
+ }
474
+ if (opts?.activate === false && freshEntries.length > 0 && !existing?.active) {
475
+ this.save();
476
+ return { ...this.data[this.accountKeyFor(id)] };
477
+ }
478
+ const activated = this.activateInMemory(providerId, id);
479
+ if (!activated) throw new Error(`Failed to activate account ${id} for provider ${providerId}`);
480
+ this.save();
481
+ return activated;
482
+ }
175
483
  /**
176
- * Get API key for a provider, auto-refreshing OAuth tokens if needed.
484
+ * Activate an account: move the active tokens out of the legacy slot back
485
+ * into the previously active entry, and the target's tokens into the slot
486
+ * (never copy — one home per token set). With no `instanceId`, rotate to
487
+ * the next entry in insertion order, wrapping once to the front; returns
488
+ * undefined when there is no other entry to rotate to.
177
489
  */
178
- async getApiKey(providerId) {
490
+ activateAccount(providerId, instanceId) {
491
+ this.reload();
492
+ const activated = this.activateInMemory(providerId, instanceId);
493
+ if (activated) this.save();
494
+ return activated;
495
+ }
496
+ /**
497
+ * Activate an account in memory only — the caller owns reload/save. Move the
498
+ * active tokens out of the legacy slot back into the previously active
499
+ * entry, and the target's tokens into the slot (never copy — one home per
500
+ * token set). With no `instanceId`, rotate to the next entry in insertion
501
+ * order, wrapping once to the front; returns undefined when there is no
502
+ * other entry to rotate to.
503
+ */
504
+ activateInMemory(providerId, instanceId) {
505
+ const entries = this.accountEntries(providerId);
506
+ if (entries.length === 0) return void 0;
507
+ let target;
508
+ if (instanceId !== void 0) {
509
+ target = entries.find((entry) => entry.id === instanceId);
510
+ if (!target) return void 0;
511
+ } else {
512
+ if (entries.length <= 1) return void 0;
513
+ const currentIdx = entries.findIndex((entry) => entry.active);
514
+ target = (currentIdx >= 0 ? [...entries.slice(currentIdx + 1), ...entries.slice(0, currentIdx)] : [...entries])[0];
515
+ }
516
+ if (!target) return void 0;
517
+ const slot = this.data[providerId];
518
+ const current = entries.find((entry) => entry.active && entry.id !== target.id);
519
+ if (current && slot?.type === "oauth") {
520
+ const { type: _type, ...slotCreds } = slot;
521
+ this.data[this.accountKeyFor(current.id)] = {
522
+ ...current,
523
+ ...slotCreds,
524
+ type: "oauth-account",
525
+ active: false
526
+ };
527
+ }
528
+ this.data[providerId] = {
529
+ ...credentialFieldsOf(target),
530
+ type: "oauth"
531
+ };
532
+ this.data[this.accountKeyFor(target.id)] = {
533
+ ...target,
534
+ active: true
535
+ };
536
+ for (const entry of entries) {
537
+ if (entry.id === target.id || entry.id === current?.id) continue;
538
+ if (entry.active) this.data[this.accountKeyFor(entry.id)] = {
539
+ ...entry,
540
+ active: false
541
+ };
542
+ }
543
+ return {
544
+ ...target,
545
+ active: true
546
+ };
547
+ }
548
+ /**
549
+ * Remove an account from the registry. If it was active, the next entry in
550
+ * insertion order is activated; when it was the last one, the legacy slot
551
+ * is removed too (full sign-out for that provider).
552
+ */
553
+ removeAccount(providerId, instanceId) {
554
+ this.reload();
555
+ const entries = this.accountEntries(providerId);
556
+ const target = entries.find((entry) => entry.id === instanceId);
557
+ if (!target) return;
558
+ const wasActive = target.active;
559
+ delete this.data[this.accountKeyFor(instanceId)];
560
+ if (wasActive) {
561
+ const next = entries[entries.indexOf(target) + 1] ?? entries.find((entry) => entry.id !== instanceId);
562
+ if (next) {
563
+ this.data[providerId] = {
564
+ type: "oauth",
565
+ ...credentialFieldsOf(next)
566
+ };
567
+ this.data[this.accountKeyFor(next.id)] = {
568
+ ...next,
569
+ active: true
570
+ };
571
+ for (const entry of entries) {
572
+ if (entry.id === next.id || entry.id === instanceId) continue;
573
+ if (entry.active) this.data[this.accountKeyFor(entry.id)] = {
574
+ ...entry,
575
+ active: false
576
+ };
577
+ }
578
+ } else delete this.data[providerId];
579
+ }
580
+ this.save();
581
+ }
582
+ /**
583
+ * Rename an account's label.
584
+ */
585
+ renameAccount(providerId, instanceId, label) {
586
+ this.reload();
587
+ const key = this.accountKeyFor(instanceId);
588
+ const entry = this.data[key];
589
+ if (!isOAuthAccountRecord(entry)) return;
590
+ this.data[key] = {
591
+ ...entry,
592
+ label
593
+ };
594
+ this.save();
595
+ }
596
+ /**
597
+ * Persist refreshed credentials for the account that initiated the refresh.
598
+ * The legacy slot is updated only if that account is still active when the
599
+ * refresh completes; otherwise a delayed refresh could overwrite a newer
600
+ * activation.
601
+ */
602
+ persistRefreshedCredential(providerId, instanceId, creds) {
603
+ this.reload();
604
+ if (instanceId) {
605
+ const key = this.accountKeyFor(instanceId);
606
+ const entry = this.data[key];
607
+ if (!isOAuthAccountRecord(entry)) return;
608
+ this.data[key] = {
609
+ ...entry,
610
+ ...creds,
611
+ type: "oauth-account"
612
+ };
613
+ if (entry.active) {
614
+ const slot = this.data[providerId];
615
+ this.data[providerId] = slot?.type === "oauth" ? {
616
+ ...slot,
617
+ ...creds,
618
+ type: "oauth"
619
+ } : {
620
+ type: "oauth",
621
+ ...creds
622
+ };
623
+ }
624
+ } else {
625
+ const slot = this.data[providerId];
626
+ this.data[providerId] = slot?.type === "oauth" ? {
627
+ ...slot,
628
+ ...creds,
629
+ type: "oauth"
630
+ } : {
631
+ type: "oauth",
632
+ ...creds
633
+ };
634
+ }
635
+ this.save();
636
+ }
637
+ /**
638
+ * Refresh one account instance through the per-instance dedupe map. Used
639
+ * for sibling refreshes during the rotation walk, so a concurrent
640
+ * `getApiKey` that reloads storage mid-walk (after the candidate was
641
+ * activated but before its refresh resolves) joins the same refresh
642
+ * instead of double-spending a single-use refresh token.
643
+ */
644
+ async refreshInstance(providerId, instanceId, creds) {
645
+ const provider = getOAuthProvider(providerId);
646
+ if (!provider) return void 0;
647
+ const refreshKey = `${providerId}:${instanceId}`;
648
+ const pending = this.refreshPromises.get(refreshKey);
649
+ if (pending) return pending;
650
+ const refresh = (async () => {
651
+ try {
652
+ const fresh = await provider.refreshToken(creds);
653
+ this.persistRefreshedCredential(providerId, instanceId, fresh);
654
+ return fresh;
655
+ } catch {
656
+ return;
657
+ }
658
+ })();
659
+ this.refreshPromises.set(refreshKey, refresh);
660
+ try {
661
+ return await refresh;
662
+ } finally {
663
+ this.refreshPromises.delete(refreshKey);
664
+ }
665
+ }
666
+ /**
667
+ * Get a ready-to-use OAuth credential snapshot, auto-refreshing if needed.
668
+ * A failed refresh leaves the active account unchanged and returns undefined;
669
+ * the account-rotation processor owns switching so it can persist a visible
670
+ * account-switch notice with the request that triggered the change.
671
+ */
672
+ async getOAuthCredential(providerId, accountInstanceId) {
673
+ this.reload();
674
+ const activeEntry = this.getActiveAccount(providerId);
675
+ const selectedEntry = accountInstanceId ? this.accountEntries(providerId).find((entry) => entry.id === accountInstanceId) : activeEntry;
676
+ const slot = this.data[providerId];
677
+ let credential;
678
+ if (selectedEntry) {
679
+ const { type: _type, id: _id, label: _label, addedAt: _addedAt, active: _active, ...accountCredential } = selectedEntry;
680
+ credential = accountCredential;
681
+ if (selectedEntry.active && slot?.type === "oauth" && slot.refresh === selectedEntry.refresh) {
682
+ const { type: _slotType, ...slotCredential } = slot;
683
+ credential = {
684
+ ...credential,
685
+ ...slotCredential
686
+ };
687
+ }
688
+ } else if (!accountInstanceId && slot?.type === "oauth") credential = slot;
689
+ if (!credential) return void 0;
690
+ const provider = getOAuthProvider(providerId);
691
+ if (!provider) return void 0;
692
+ const selectedInstanceId = selectedEntry?.id;
693
+ const toSnapshot = (credentials) => ({
694
+ type: "oauth",
695
+ ...credentials,
696
+ accountInstanceId: selectedInstanceId
697
+ });
698
+ if (Date.now() < credential.expires) return toSnapshot(credential);
699
+ if (selectedInstanceId) {
700
+ const refreshed = await this.refreshInstance(providerId, selectedInstanceId, credential);
701
+ return refreshed ? toSnapshot(refreshed) : void 0;
702
+ }
703
+ const pendingRefresh = this.refreshPromises.get(providerId);
704
+ const refresh = pendingRefresh ?? (async () => {
705
+ try {
706
+ const fresh = await provider.refreshToken(credential);
707
+ this.persistRefreshedCredential(providerId, void 0, fresh);
708
+ return fresh;
709
+ } catch {
710
+ return;
711
+ }
712
+ })();
713
+ if (!pendingRefresh) this.refreshPromises.set(providerId, refresh);
714
+ try {
715
+ const refreshed = await refresh;
716
+ return refreshed ? toSnapshot(refreshed) : void 0;
717
+ } finally {
718
+ if (!pendingRefresh) this.refreshPromises.delete(providerId);
719
+ }
720
+ }
721
+ /** Get API key for a provider, refreshing OAuth tokens if needed. */
722
+ async getApiKey(providerId, accountInstanceId) {
723
+ this.reload();
179
724
  const cred = this.data[providerId];
180
- if (cred?.type === "api_key") return cred.key;
181
- if (cred?.type === "oauth") {
182
- const provider = getOAuthProvider(providerId);
183
- if (!provider) return;
184
- if (Date.now() >= cred.expires) {
185
- const pendingRefresh = this.refreshPromises.get(providerId);
186
- if (pendingRefresh) return pendingRefresh;
187
- const refresh = (async () => {
188
- try {
189
- const newCreds = await provider.refreshToken(cred);
190
- this.set(providerId, {
191
- type: "oauth",
192
- ...newCreds
193
- });
194
- return provider.getApiKey(newCreds);
195
- } catch {
196
- return;
197
- } finally {
198
- this.refreshPromises.delete(providerId);
199
- }
200
- })();
201
- this.refreshPromises.set(providerId, refresh);
202
- return refresh;
725
+ if (accountInstanceId === void 0 && cred?.type === "api_key") return cred.key;
726
+ const oauth = await this.getOAuthCredential(providerId, accountInstanceId);
727
+ const provider = oauth ? getOAuthProvider(providerId) : void 0;
728
+ return oauth && provider ? provider.getApiKey(oauth) : void 0;
729
+ }
730
+ /**
731
+ * Force one refresh of the active OAuth account's tokens, regardless of
732
+ * expiry. The account-rotation error processor calls this when a provider
733
+ * rejects a not-yet-expired token (401/403) — server-side clock skew and
734
+ * refresh-token races surface that way. Returns the fresh access token, or
735
+ * undefined when the refresh fails or there is no OAuth credential.
736
+ * Shares the per-instance refresh dedupe with `getApiKey`.
737
+ */
738
+ async forceRefreshActiveAccount(providerId, accountInstanceId) {
739
+ this.reload();
740
+ const activeEntry = this.getActiveAccount(providerId);
741
+ const selectedEntry = accountInstanceId ? this.accountEntries(providerId).find((entry) => entry.id === accountInstanceId) : activeEntry;
742
+ const slot = this.get(providerId);
743
+ let cred;
744
+ if (selectedEntry) {
745
+ const { type: _type, id: _id, label: _label, addedAt: _addedAt, active: _active, ...accountCredential } = selectedEntry;
746
+ cred = accountCredential;
747
+ } else if (!accountInstanceId && slot?.type === "oauth") cred = slot;
748
+ if (!cred) return void 0;
749
+ const provider = getOAuthProvider(providerId);
750
+ if (!provider) return void 0;
751
+ const refreshKey = selectedEntry ? `${providerId}:${selectedEntry.id}` : providerId;
752
+ const pending = this.refreshPromises.get(refreshKey);
753
+ const refresh = pending ?? (async () => {
754
+ try {
755
+ const fresh = await provider.refreshToken(cred);
756
+ this.persistRefreshedCredential(providerId, selectedEntry?.id, fresh);
757
+ return fresh;
758
+ } catch {
759
+ return;
203
760
  }
204
- return provider.getApiKey(cred);
761
+ })();
762
+ if (!pending) this.refreshPromises.set(refreshKey, refresh);
763
+ try {
764
+ const creds = await refresh;
765
+ return creds ? provider.getApiKey(creds) : void 0;
766
+ } finally {
767
+ if (!pending) this.refreshPromises.delete(refreshKey);
205
768
  }
206
769
  }
207
770
  };