@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.
- package/dist/agents/mastracode-gateway.d.ts +2 -0
- package/dist/agents/mastracode-gateway.d.ts.map +1 -1
- package/dist/agents/mastracode-gateway.js +29 -10
- package/dist/agents/mastracode-gateway.js.map +1 -1
- package/dist/agents/memory.d.ts +1 -1
- package/dist/agents/memory.d.ts.map +1 -1
- package/dist/agents/memory.js +32 -25
- package/dist/agents/memory.js.map +1 -1
- package/dist/agents/model.d.ts +41 -1
- package/dist/agents/model.d.ts.map +1 -1
- package/dist/agents/model.js +141 -7
- package/dist/agents/model.js.map +1 -1
- package/dist/auth/account-rotation-processor.d.ts +207 -0
- package/dist/auth/account-rotation-processor.d.ts.map +1 -0
- package/dist/auth/account-rotation-processor.js +669 -0
- package/dist/auth/account-rotation-processor.js.map +1 -0
- package/dist/auth/account-routing-context.d.ts +13 -0
- package/dist/auth/account-routing-context.d.ts.map +1 -0
- package/dist/auth/account-routing-context.js +41 -0
- package/dist/auth/account-routing-context.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +2 -1
- package/dist/auth/providers/anthropic.d.ts.map +1 -1
- package/dist/auth/providers/anthropic.js +2 -8
- package/dist/auth/providers/anthropic.js.map +1 -1
- package/dist/auth/providers/github-copilot.d.ts.map +1 -1
- package/dist/auth/providers/github-copilot.js +34 -12
- package/dist/auth/providers/github-copilot.js.map +1 -1
- package/dist/auth/providers/kimi-coding.d.ts.map +1 -1
- package/dist/auth/providers/kimi-coding.js +2 -6
- package/dist/auth/providers/kimi-coding.js.map +1 -1
- package/dist/auth/providers/openai-codex.d.ts +3 -1
- package/dist/auth/providers/openai-codex.d.ts.map +1 -1
- package/dist/auth/providers/openai-codex.js +50 -24
- package/dist/auth/providers/openai-codex.js.map +1 -1
- package/dist/auth/providers/xai.d.ts +1 -1
- package/dist/auth/providers/xai.d.ts.map +1 -1
- package/dist/auth/providers/xai.js +36 -16
- package/dist/auth/providers/xai.js.map +1 -1
- package/dist/auth/storage.d.ts +130 -8
- package/dist/auth/storage.d.ts.map +1 -1
- package/dist/auth/storage.js +602 -39
- package/dist/auth/storage.js.map +1 -1
- package/dist/auth/types.d.ts +69 -2
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -23
- package/dist/index.js.map +1 -1
- package/dist/onboarding/custom-packs.d.ts.map +1 -1
- package/dist/onboarding/custom-packs.js +3 -0
- package/dist/onboarding/custom-packs.js.map +1 -1
- package/dist/onboarding/index.js +1 -1
- package/dist/onboarding/packs.d.ts +44 -0
- package/dist/onboarding/packs.d.ts.map +1 -1
- package/dist/onboarding/packs.js +80 -2
- package/dist/onboarding/packs.js.map +1 -1
- package/dist/onboarding/settings.d.ts +28 -0
- package/dist/onboarding/settings.d.ts.map +1 -1
- package/dist/onboarding/settings.js +81 -8
- package/dist/onboarding/settings.js.map +1 -1
- package/dist/providers/claude-max.d.ts.map +1 -1
- package/dist/providers/claude-max.js +4 -5
- package/dist/providers/claude-max.js.map +1 -1
- package/dist/providers/github-copilot.d.ts +1 -1
- package/dist/providers/github-copilot.d.ts.map +1 -1
- package/dist/providers/github-copilot.js +89 -37
- package/dist/providers/github-copilot.js.map +1 -1
- package/dist/providers/kimi-coding.d.ts.map +1 -1
- package/dist/providers/kimi-coding.js +16 -2
- package/dist/providers/kimi-coding.js.map +1 -1
- package/dist/providers/openai-codex.d.ts.map +1 -1
- package/dist/providers/openai-codex.js +42 -27
- package/dist/providers/openai-codex.js.map +1 -1
- package/dist/schema.d.ts +27 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +9 -0
- package/dist/schema.js.map +1 -1
- package/package.json +8 -8
package/dist/auth/storage.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
};
|