@omnicross/subscriptions 0.1.0 → 0.1.3
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/chunk-IXGHVMZB.js +365 -0
- package/dist/chunk-TPW5Q25Y.cjs +365 -0
- package/dist/index.cjs +508 -662
- package/dist/index.d.cts +154 -670
- package/dist/index.d.ts +154 -670
- package/dist/index.js +408 -522
- package/dist/oauth.cjs +4 -390
- package/dist/oauth.d.cts +3 -4
- package/dist/oauth.d.ts +3 -4
- package/dist/oauth.js +5 -358
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,29 +1,281 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
claude_exports,
|
|
3
|
+
codex_exports,
|
|
4
|
+
gemini_exports
|
|
5
|
+
} from "./chunk-IXGHVMZB.js";
|
|
6
|
+
|
|
7
|
+
// src/scheduler/SubscriptionAccountSelector.ts
|
|
8
|
+
var SESSION_AFFINITY_TTL_MS = 36e5;
|
|
9
|
+
var LAST_USED_PERSIST_THROTTLE_MS = 6e4;
|
|
10
|
+
var DEFAULT_ACCOUNT_PRIORITY = 50;
|
|
11
|
+
function scopedKey(providerId, tail) {
|
|
12
|
+
return `${providerId}\0${tail}`;
|
|
13
|
+
}
|
|
14
|
+
function parseIso(iso) {
|
|
15
|
+
if (!iso) return 0;
|
|
16
|
+
const ms = Date.parse(iso);
|
|
17
|
+
return Number.isNaN(ms) ? 0 : ms;
|
|
18
|
+
}
|
|
19
|
+
var SubscriptionAccountSelector = class {
|
|
20
|
+
/** `providerId\0accountId → live lastUsedAt ms` (authoritative tie-break). */
|
|
21
|
+
lastUsedOverlay = /* @__PURE__ */ new Map();
|
|
22
|
+
/** `providerId\0sessionKey → { accountId, expiresAt }`. */
|
|
23
|
+
affinity = /* @__PURE__ */ new Map();
|
|
24
|
+
/** `providerId\0accountId → last durable-persist ms` (throttle state). */
|
|
25
|
+
lastPersist = /* @__PURE__ */ new Map();
|
|
26
|
+
/**
|
|
27
|
+
* Choose the account to serve this request, or `null` when there are ≤ 1
|
|
28
|
+
* schedulable accounts (the zero-regression signal — caller uses the active
|
|
29
|
+
* account). Updates the live `lastUsedAt` overlay for the chosen account and,
|
|
30
|
+
* when a `sessionKey` is given, records/extends the affinity mapping.
|
|
31
|
+
*/
|
|
32
|
+
select(input) {
|
|
33
|
+
const now = input.now ?? Date.now();
|
|
34
|
+
const schedulable = input.accounts.filter((a) => a.schedulable !== false);
|
|
35
|
+
if (schedulable.length <= 1) return null;
|
|
36
|
+
if (input.sessionKey) {
|
|
37
|
+
const aKey = scopedKey(input.providerId, input.sessionKey);
|
|
38
|
+
const hit = this.affinity.get(aKey);
|
|
39
|
+
if (hit && hit.expiresAt > now && schedulable.some((a) => a.id === hit.accountId)) {
|
|
40
|
+
hit.expiresAt = now + SESSION_AFFINITY_TTL_MS;
|
|
41
|
+
this.markUsed(input.providerId, hit.accountId, now);
|
|
42
|
+
return { accountId: hit.accountId, isActive: hit.accountId === input.activeAccountId };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const chosen = this.pickOrdered(input.providerId, schedulable);
|
|
46
|
+
this.markUsed(input.providerId, chosen.id, now);
|
|
47
|
+
if (input.sessionKey) {
|
|
48
|
+
this.affinity.set(scopedKey(input.providerId, input.sessionKey), {
|
|
49
|
+
accountId: chosen.id,
|
|
50
|
+
expiresAt: now + SESSION_AFFINITY_TTL_MS
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { accountId: chosen.id, isActive: chosen.id === input.activeAccountId };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Drop every session-affinity mapping bound to this account (subscription-account-
|
|
57
|
+
* health, task 4.1). Called when a selected account's by-id token turns out
|
|
58
|
+
* null/invalid or an affinity-bound account becomes unhealthy, so the next
|
|
59
|
+
* selection for those sessions picks a fresh account instead of re-sticking to
|
|
60
|
+
* the bad one. O(affinity entries) — the map is tiny (one entry per live
|
|
61
|
+
* conversation).
|
|
62
|
+
*/
|
|
63
|
+
evictAffinity(providerId, accountId) {
|
|
64
|
+
const prefix = `${providerId} `;
|
|
65
|
+
for (const [key, entry] of this.affinity) {
|
|
66
|
+
if (entry.accountId === accountId && key.startsWith(prefix)) {
|
|
67
|
+
this.affinity.delete(key);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether a best-effort `lastUsedAt` durable persist is DUE for this account
|
|
73
|
+
* (≥ `LAST_USED_PERSIST_THROTTLE_MS` since the last one). Records the persist
|
|
74
|
+
* time when it returns `true`, so the strategy calls `touchAccountLastUsed`
|
|
75
|
+
* sparingly and the request hot path does not rewrite the store every request.
|
|
76
|
+
*/
|
|
77
|
+
duePersist(providerId, accountId, now = Date.now()) {
|
|
78
|
+
const key = scopedKey(providerId, accountId);
|
|
79
|
+
const last = this.lastPersist.get(key) ?? 0;
|
|
80
|
+
if (now - last < LAST_USED_PERSIST_THROTTLE_MS) return false;
|
|
81
|
+
this.lastPersist.set(key, now);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
/** Sort by `priority` asc → effective `lastUsedAt` asc → `createdAt` asc → `[0]`. */
|
|
85
|
+
pickOrdered(providerId, accounts) {
|
|
86
|
+
const ranked = accounts.map((account) => ({
|
|
87
|
+
account,
|
|
88
|
+
priority: account.priority ?? DEFAULT_ACCOUNT_PRIORITY,
|
|
89
|
+
lastUsed: this.effectiveLastUsed(providerId, account),
|
|
90
|
+
created: parseIso(account.createdAt)
|
|
91
|
+
}));
|
|
92
|
+
ranked.sort(
|
|
93
|
+
(x, y) => x.priority - y.priority || x.lastUsed - y.lastUsed || x.created - y.created
|
|
94
|
+
);
|
|
95
|
+
return ranked[0].account;
|
|
96
|
+
}
|
|
97
|
+
/** The live tie-break value: the in-memory overlay when set, else the persisted
|
|
98
|
+
* `lastUsedAt` (0 when absent). */
|
|
99
|
+
effectiveLastUsed(providerId, account) {
|
|
100
|
+
const overlay = this.lastUsedOverlay.get(scopedKey(providerId, account.id));
|
|
101
|
+
return overlay ?? parseIso(account.lastUsedAt);
|
|
102
|
+
}
|
|
103
|
+
markUsed(providerId, accountId, now) {
|
|
104
|
+
this.lastUsedOverlay.set(scopedKey(providerId, accountId), now);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// src/SubscriptionAccountService.ts
|
|
109
|
+
import { getSharedAccountHealth } from "@omnicross/core/pipeline/SubscriptionAccountHealth";
|
|
110
|
+
|
|
111
|
+
// src/scheduler/accountModelMap.ts
|
|
112
|
+
function canonicalModelId(value) {
|
|
113
|
+
const idx = value.indexOf(",");
|
|
114
|
+
const bare = idx >= 0 ? value.slice(idx + 1) : value;
|
|
115
|
+
return bare.trim().toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
function accountSupportsModel(supportedModels, model) {
|
|
118
|
+
if (!supportedModels) return true;
|
|
119
|
+
const target = canonicalModelId(model);
|
|
120
|
+
if (Array.isArray(supportedModels)) {
|
|
121
|
+
return supportedModels.some((m) => canonicalModelId(m) === target);
|
|
122
|
+
}
|
|
123
|
+
return Object.keys(supportedModels).some((k) => canonicalModelId(k) === target);
|
|
124
|
+
}
|
|
125
|
+
function remapForAccount(supportedModels, model) {
|
|
126
|
+
if (!supportedModels || Array.isArray(supportedModels)) return model;
|
|
127
|
+
const target = canonicalModelId(model);
|
|
128
|
+
for (const [key, actual] of Object.entries(supportedModels)) {
|
|
129
|
+
if (canonicalModelId(key) === target) return actual;
|
|
130
|
+
}
|
|
131
|
+
return model;
|
|
132
|
+
}
|
|
133
|
+
function remapReportForAccount(supportedModels, model) {
|
|
134
|
+
if (!model) return void 0;
|
|
135
|
+
const remapped = remapForAccount(supportedModels, model);
|
|
136
|
+
return remapped === model ? void 0 : remapped;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/scheduler/accountSelection.ts
|
|
140
|
+
var ACCOUNTS_KEY = {
|
|
141
|
+
claude: "claudeAccounts",
|
|
142
|
+
codex: "codexAccounts",
|
|
143
|
+
gemini: "geminiAccounts",
|
|
144
|
+
opencodego: "opencodegoAccounts"
|
|
5
145
|
};
|
|
146
|
+
var ACTIVE_KEY = {
|
|
147
|
+
claude: "activeClaudeAccountId",
|
|
148
|
+
codex: "activeCodexAccountId",
|
|
149
|
+
gemini: "activeGeminiAccountId",
|
|
150
|
+
opencodego: "activeOpencodegoAccountId"
|
|
151
|
+
};
|
|
152
|
+
function gateSchedulable(accounts, providerId, health, now, resolvedModel, supportedModelsById) {
|
|
153
|
+
if (!health && !resolvedModel || accounts.length < 2) return accounts;
|
|
154
|
+
return accounts.map((a) => {
|
|
155
|
+
const healthOk = health ? health.isSchedulable(providerId, a.id, now) : true;
|
|
156
|
+
const modelOk = resolvedModel ? accountSupportsModel(supportedModelsById.get(a.id), resolvedModel) : true;
|
|
157
|
+
return { ...a, schedulable: healthOk && modelOk };
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function isPoolGated(accounts, health, resolvedModel) {
|
|
161
|
+
return (health !== void 0 || resolvedModel !== void 0) && accounts.length >= 2;
|
|
162
|
+
}
|
|
163
|
+
function readSchedulableAccounts(config, providerId) {
|
|
164
|
+
const raw = config[ACCOUNTS_KEY[providerId]] ?? [];
|
|
165
|
+
const accounts = raw.map((a) => ({
|
|
166
|
+
id: a.id,
|
|
167
|
+
priority: a.priority,
|
|
168
|
+
lastUsedAt: a.lastUsedAt,
|
|
169
|
+
createdAt: a.createdAt
|
|
170
|
+
}));
|
|
171
|
+
const supportedModelsById = new Map(
|
|
172
|
+
raw.map((a) => [a.id, a.supportedModels])
|
|
173
|
+
);
|
|
174
|
+
const activeAccountId = config[ACTIVE_KEY[providerId]];
|
|
175
|
+
return { accounts, activeAccountId, supportedModelsById };
|
|
176
|
+
}
|
|
177
|
+
function pickByIdTarget(selector, gated, providerId, activeAccountId, sessionKey, now, healthGated) {
|
|
178
|
+
const selection = selector.select({ providerId, accounts: gated, activeAccountId, sessionKey, now });
|
|
179
|
+
if (selection && !selection.isActive) return selection.accountId;
|
|
180
|
+
if (selection === null && healthGated) {
|
|
181
|
+
const schedulable = gated.filter((a) => a.schedulable !== false);
|
|
182
|
+
if (schedulable.length === 1 && schedulable[0].id !== activeAccountId) return schedulable[0].id;
|
|
183
|
+
}
|
|
184
|
+
return void 0;
|
|
185
|
+
}
|
|
186
|
+
async function resolveSelectedToken(selector, tokens, providerId, sessionKey, activeGetter, ctx) {
|
|
187
|
+
const health = ctx?.health;
|
|
188
|
+
const report = ctx?.reportSelection;
|
|
189
|
+
const now = ctx?.now;
|
|
190
|
+
const resolvedModel = ctx?.resolvedModel;
|
|
191
|
+
if (selector && tokens.getAccessTokenForAccount) {
|
|
192
|
+
const config = await tokens.getFullConfig();
|
|
193
|
+
const { accounts, activeAccountId, supportedModelsById } = readSchedulableAccounts(config, providerId);
|
|
194
|
+
const gated = gateSchedulable(accounts, providerId, health, now, resolvedModel, supportedModelsById);
|
|
195
|
+
const poolGated = isPoolGated(accounts, health, resolvedModel);
|
|
196
|
+
const remapFor = (id) => remapReportForAccount(supportedModelsById.get(id), resolvedModel);
|
|
197
|
+
const targetId = pickByIdTarget(selector, gated, providerId, activeAccountId, sessionKey, now, poolGated);
|
|
198
|
+
if (targetId !== void 0) {
|
|
199
|
+
const byId = await tokens.getAccessTokenForAccount(providerId, targetId);
|
|
200
|
+
if (byId) {
|
|
201
|
+
maybeTouchLastUsed(selector, tokens, providerId, targetId);
|
|
202
|
+
report?.(targetId, false, remapFor(targetId));
|
|
203
|
+
return byId;
|
|
204
|
+
}
|
|
205
|
+
selector.evictAffinity(providerId, targetId);
|
|
206
|
+
health?.recordUpstreamOutcome(providerId, targetId, { status: 401, now });
|
|
207
|
+
const remaining = gated.filter((a) => a.id !== targetId);
|
|
208
|
+
const retryId = pickByIdTarget(selector, remaining, providerId, activeAccountId, sessionKey, now, poolGated);
|
|
209
|
+
if (retryId !== void 0) {
|
|
210
|
+
const retryToken = await tokens.getAccessTokenForAccount(providerId, retryId);
|
|
211
|
+
if (retryToken) {
|
|
212
|
+
maybeTouchLastUsed(selector, tokens, providerId, retryId);
|
|
213
|
+
report?.(retryId, false, remapFor(retryId));
|
|
214
|
+
return retryToken;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (activeAccountId) report?.(activeAccountId, true, remapFor(activeAccountId));
|
|
219
|
+
return activeGetter();
|
|
220
|
+
}
|
|
221
|
+
return activeGetter();
|
|
222
|
+
}
|
|
223
|
+
function maybeTouchLastUsed(selector, tokens, providerId, accountId) {
|
|
224
|
+
if (!tokens.touchAccountLastUsed) return;
|
|
225
|
+
if (!selector.duePersist(providerId, accountId)) return;
|
|
226
|
+
void tokens.touchAccountLastUsed(providerId, accountId, (/* @__PURE__ */ new Date()).toISOString()).catch(() => {
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
async function refreshSelectedAccount(selector, tokens, mutex, providerId, sessionKey) {
|
|
230
|
+
if (!sessionKey || !selector || !tokens.refreshAccountToken) return null;
|
|
231
|
+
const config = await tokens.getFullConfig();
|
|
232
|
+
const { accounts, activeAccountId } = readSchedulableAccounts(config, providerId);
|
|
233
|
+
const selection = selector.select({ providerId, accounts, activeAccountId, sessionKey });
|
|
234
|
+
if (!selection || selection.isActive) return null;
|
|
235
|
+
const accountId = selection.accountId;
|
|
236
|
+
return mutex.run(`${providerId}:${accountId}`, async () => {
|
|
237
|
+
try {
|
|
238
|
+
return await tokens.refreshAccountToken(providerId, accountId) ?? false;
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.warn(`[accountSelection] ${providerId}:${accountId} by-id refresh failed:`, err);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
6
245
|
|
|
7
246
|
// src/auth/OAuthBearerAuthStrategy.ts
|
|
8
247
|
var REFRESH_LEAD_MS = 5 * 6e4;
|
|
9
248
|
var OAuthBearerAuthStrategy = class {
|
|
10
|
-
constructor(providerId, tokens, mutex) {
|
|
249
|
+
constructor(providerId, tokens, mutex, selector, health) {
|
|
11
250
|
this.tokens = tokens;
|
|
12
251
|
this.mutex = mutex;
|
|
252
|
+
this.selector = selector;
|
|
253
|
+
this.health = health;
|
|
13
254
|
this.providerId = providerId;
|
|
14
255
|
}
|
|
15
256
|
tokens;
|
|
16
257
|
mutex;
|
|
258
|
+
selector;
|
|
259
|
+
health;
|
|
17
260
|
kind = "oauth-bearer";
|
|
18
261
|
providerId;
|
|
19
|
-
async applyHeaders(headers,
|
|
20
|
-
const token = await
|
|
262
|
+
async applyHeaders(headers, hints) {
|
|
263
|
+
const token = await resolveSelectedToken(
|
|
264
|
+
this.selector,
|
|
265
|
+
this.tokens,
|
|
266
|
+
this.providerId,
|
|
267
|
+
hints?.sessionKey,
|
|
268
|
+
() => this.resolveAccessToken(),
|
|
269
|
+
{ health: this.health, reportSelection: hints?.reportSelection, resolvedModel: hints?.resolvedModel }
|
|
270
|
+
);
|
|
21
271
|
if (!token) {
|
|
22
272
|
return;
|
|
23
273
|
}
|
|
24
274
|
headers["Authorization"] = `Bearer ${token}`;
|
|
25
275
|
}
|
|
26
|
-
async onUnauthorized() {
|
|
276
|
+
async onUnauthorized(sessionKey) {
|
|
277
|
+
const byId = await refreshSelectedAccount(this.selector, this.tokens, this.mutex, this.providerId, sessionKey);
|
|
278
|
+
if (byId !== null) return byId;
|
|
27
279
|
return this.mutex.run(`${this.providerId}:refresh`, async () => {
|
|
28
280
|
try {
|
|
29
281
|
return this.providerId === "codex" ? await this.tokens.refreshCodexToken() : await this.tokens.refreshGeminiToken();
|
|
@@ -72,20 +324,33 @@ var OAuthBearerAuthStrategy = class {
|
|
|
72
324
|
|
|
73
325
|
// src/auth/PassThroughAuthStrategy.ts
|
|
74
326
|
var PassThroughAuthStrategy = class {
|
|
75
|
-
constructor(tokens, mutex) {
|
|
327
|
+
constructor(tokens, mutex, selector, health) {
|
|
76
328
|
this.tokens = tokens;
|
|
77
329
|
this.mutex = mutex;
|
|
330
|
+
this.selector = selector;
|
|
331
|
+
this.health = health;
|
|
78
332
|
}
|
|
79
333
|
tokens;
|
|
80
334
|
mutex;
|
|
335
|
+
selector;
|
|
336
|
+
health;
|
|
81
337
|
kind = "pass-through";
|
|
82
338
|
providerId = "claude";
|
|
83
|
-
async applyHeaders(headers,
|
|
84
|
-
const token = await
|
|
339
|
+
async applyHeaders(headers, hints) {
|
|
340
|
+
const token = await resolveSelectedToken(
|
|
341
|
+
this.selector,
|
|
342
|
+
this.tokens,
|
|
343
|
+
"claude",
|
|
344
|
+
hints?.sessionKey,
|
|
345
|
+
() => this.tokens.getValidClaudeAccessToken(),
|
|
346
|
+
{ health: this.health, reportSelection: hints?.reportSelection, resolvedModel: hints?.resolvedModel }
|
|
347
|
+
);
|
|
85
348
|
if (!token) return;
|
|
86
349
|
headers["Authorization"] = `Bearer ${token}`;
|
|
87
350
|
}
|
|
88
|
-
async onUnauthorized() {
|
|
351
|
+
async onUnauthorized(sessionKey) {
|
|
352
|
+
const byId = await refreshSelectedAccount(this.selector, this.tokens, this.mutex, "claude", sessionKey);
|
|
353
|
+
if (byId !== null) return byId;
|
|
89
354
|
return this.mutex.run("claude:refresh", async () => {
|
|
90
355
|
try {
|
|
91
356
|
return await this.tokens.refreshClaudeToken();
|
|
@@ -139,14 +404,25 @@ var RefreshMutex = class {
|
|
|
139
404
|
// src/auth/StaticBearerAuthStrategy.ts
|
|
140
405
|
var ANTHROPIC_SHAPE_PATH = "/v1/messages";
|
|
141
406
|
var StaticBearerAuthStrategy = class {
|
|
142
|
-
constructor(tokens) {
|
|
407
|
+
constructor(tokens, selector, health) {
|
|
143
408
|
this.tokens = tokens;
|
|
409
|
+
this.selector = selector;
|
|
410
|
+
this.health = health;
|
|
144
411
|
}
|
|
145
412
|
tokens;
|
|
413
|
+
selector;
|
|
414
|
+
health;
|
|
146
415
|
kind = "static-bearer";
|
|
147
416
|
providerId = "opencodego";
|
|
148
417
|
async applyHeaders(headers, hints) {
|
|
149
|
-
const key = await
|
|
418
|
+
const key = await resolveSelectedToken(
|
|
419
|
+
this.selector,
|
|
420
|
+
this.tokens,
|
|
421
|
+
"opencodego",
|
|
422
|
+
hints?.sessionKey,
|
|
423
|
+
() => this.tokens.getValidOpenCodeGoApiKey(),
|
|
424
|
+
{ health: this.health, reportSelection: hints?.reportSelection, resolvedModel: hints?.resolvedModel }
|
|
425
|
+
);
|
|
150
426
|
if (!key) {
|
|
151
427
|
return;
|
|
152
428
|
}
|
|
@@ -155,7 +431,7 @@ var StaticBearerAuthStrategy = class {
|
|
|
155
431
|
headers["x-api-key"] = key;
|
|
156
432
|
}
|
|
157
433
|
}
|
|
158
|
-
async onUnauthorized() {
|
|
434
|
+
async onUnauthorized(_sessionKey) {
|
|
159
435
|
return false;
|
|
160
436
|
}
|
|
161
437
|
async describeStatus() {
|
|
@@ -180,13 +456,17 @@ var DISPLAY_NAMES = {
|
|
|
180
456
|
};
|
|
181
457
|
var SubscriptionAccountService = class {
|
|
182
458
|
mutex = new RefreshMutex();
|
|
459
|
+
/** ONE account-pool scheduler (subscription-account-scheduling) shared by all
|
|
460
|
+
* four strategies so they share the affinity map + the `lastUsedAt` overlay. */
|
|
461
|
+
selector = new SubscriptionAccountSelector();
|
|
183
462
|
strategies;
|
|
184
463
|
constructor(tokens) {
|
|
464
|
+
const health = getSharedAccountHealth();
|
|
185
465
|
this.strategies = /* @__PURE__ */ new Map([
|
|
186
|
-
["claude", new PassThroughAuthStrategy(tokens, this.mutex)],
|
|
187
|
-
["codex", new OAuthBearerAuthStrategy("codex", tokens, this.mutex)],
|
|
188
|
-
["gemini", new OAuthBearerAuthStrategy("gemini", tokens, this.mutex)],
|
|
189
|
-
["opencodego", new StaticBearerAuthStrategy(tokens)]
|
|
466
|
+
["claude", new PassThroughAuthStrategy(tokens, this.mutex, this.selector, health)],
|
|
467
|
+
["codex", new OAuthBearerAuthStrategy("codex", tokens, this.mutex, this.selector, health)],
|
|
468
|
+
["gemini", new OAuthBearerAuthStrategy("gemini", tokens, this.mutex, this.selector, health)],
|
|
469
|
+
["opencodego", new StaticBearerAuthStrategy(tokens, this.selector, health)]
|
|
190
470
|
]);
|
|
191
471
|
}
|
|
192
472
|
/** Returns the strategy bound to a subscription provider, or `null` for unknown ids. */
|
|
@@ -223,27 +503,11 @@ function getSubscriptionAccountService() {
|
|
|
223
503
|
return _moduleSingleton;
|
|
224
504
|
}
|
|
225
505
|
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ../core/src/transformer/transformers/GeminiCodeAssistTransformer.ts
|
|
233
|
-
var DEFAULT_CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
234
|
-
var DEFAULT_CODE_ASSIST_API_VERSION = "v1internal";
|
|
235
|
-
function resolveCodeAssistEndpoint() {
|
|
236
|
-
return (process.env.CODE_ASSIST_ENDPOINT || DEFAULT_CODE_ASSIST_ENDPOINT).replace(/\/+$/, "");
|
|
237
|
-
}
|
|
238
|
-
function resolveCodeAssistApiVersion() {
|
|
239
|
-
return process.env.CODE_ASSIST_API_VERSION || DEFAULT_CODE_ASSIST_API_VERSION;
|
|
240
|
-
}
|
|
241
|
-
function buildCodeAssistUrl(stream) {
|
|
242
|
-
const base = resolveCodeAssistEndpoint();
|
|
243
|
-
const version = resolveCodeAssistApiVersion();
|
|
244
|
-
const method = stream ? "streamGenerateContent?alt=sse" : "generateContent";
|
|
245
|
-
return `${base}/${version}:${method}`;
|
|
246
|
-
}
|
|
506
|
+
// src/SubscriptionProviderRegistry.ts
|
|
507
|
+
import {
|
|
508
|
+
setSubscriptionRegistryForOutbound
|
|
509
|
+
} from "@omnicross/core/outbound-api/subscriptionRegistryPort";
|
|
510
|
+
import { buildCodeAssistUrl } from "@omnicross/core/transformer/transformers/GeminiCodeAssistTransformer";
|
|
247
511
|
|
|
248
512
|
// src/opencodego/CircuitBreaker.ts
|
|
249
513
|
var CircuitBreaker = class {
|
|
@@ -844,106 +1108,14 @@ function getSubscriptionProviderRegistry() {
|
|
|
844
1108
|
return _moduleSingleton2;
|
|
845
1109
|
}
|
|
846
1110
|
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
var MATCH_TEXT_RECENT_MESSAGES = 6;
|
|
856
|
-
function flattenMatchText(value) {
|
|
857
|
-
if (typeof value === "string") return value;
|
|
858
|
-
if (Array.isArray(value)) {
|
|
859
|
-
const parts = [];
|
|
860
|
-
for (const item of value) {
|
|
861
|
-
const text = flattenMatchText(item);
|
|
862
|
-
if (text) parts.push(text);
|
|
863
|
-
}
|
|
864
|
-
return parts.join("\n");
|
|
865
|
-
}
|
|
866
|
-
if (value && typeof value === "object") {
|
|
867
|
-
const obj = value;
|
|
868
|
-
if (obj.type === "tool_result" && obj.content !== void 0) {
|
|
869
|
-
return flattenMatchText(obj.content);
|
|
870
|
-
}
|
|
871
|
-
if (typeof obj.text === "string") return obj.text;
|
|
872
|
-
}
|
|
873
|
-
return "";
|
|
874
|
-
}
|
|
875
|
-
function collectMatchText(anthropicBody) {
|
|
876
|
-
const messages = Array.isArray(anthropicBody.messages) ? anthropicBody.messages : [];
|
|
877
|
-
const slices = [];
|
|
878
|
-
const sys = flattenMatchText(anthropicBody.system).trim();
|
|
879
|
-
if (sys) slices.push(sys.slice(0, MATCH_TEXT_PER_MESSAGE_CAP));
|
|
880
|
-
const recent = [];
|
|
881
|
-
for (let i = messages.length - 1; i >= 0 && recent.length < MATCH_TEXT_RECENT_MESSAGES; i--) {
|
|
882
|
-
const message = messages[i];
|
|
883
|
-
if (!message || typeof message !== "object") continue;
|
|
884
|
-
const role = message.role;
|
|
885
|
-
if (role !== "user" && role !== "system") continue;
|
|
886
|
-
const text = flattenMatchText(message.content).trim();
|
|
887
|
-
if (text) recent.push(text.slice(0, MATCH_TEXT_PER_MESSAGE_CAP));
|
|
888
|
-
}
|
|
889
|
-
for (let i = recent.length - 1; i >= 0; i--) slices.push(recent[i]);
|
|
890
|
-
return slices;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// ../core/src/serializeError.ts
|
|
894
|
-
function serializeError(err) {
|
|
895
|
-
if (err == null) return "Unknown error (null)";
|
|
896
|
-
if (err instanceof Error) {
|
|
897
|
-
let msg = err.message || err.name || "Error";
|
|
898
|
-
if (err.cause) {
|
|
899
|
-
msg += ` [cause: ${serializeError(err.cause)}]`;
|
|
900
|
-
}
|
|
901
|
-
const anyErr = err;
|
|
902
|
-
if (anyErr.status != null) msg += ` (status: ${anyErr.status})`;
|
|
903
|
-
else if (anyErr.code != null) msg += ` (code: ${anyErr.code})`;
|
|
904
|
-
return msg;
|
|
905
|
-
}
|
|
906
|
-
if (typeof err === "string") return err || "Empty error string";
|
|
907
|
-
if (typeof err !== "object") return String(err);
|
|
908
|
-
const obj = err;
|
|
909
|
-
if (typeof obj.message === "string" && obj.message) {
|
|
910
|
-
let msg = obj.message;
|
|
911
|
-
if (obj.status != null) msg += ` (status: ${obj.status})`;
|
|
912
|
-
else if (obj.code != null) msg += ` (code: ${obj.code})`;
|
|
913
|
-
if (typeof obj.type === "string") msg += ` [type: ${obj.type}]`;
|
|
914
|
-
return msg;
|
|
915
|
-
}
|
|
916
|
-
if (typeof obj.error === "string" && obj.error) {
|
|
917
|
-
return obj.error;
|
|
918
|
-
}
|
|
919
|
-
if (obj.error && typeof obj.error === "object") {
|
|
920
|
-
const inner = obj.error;
|
|
921
|
-
if (typeof inner.message === "string" && inner.message) {
|
|
922
|
-
let msg = inner.message;
|
|
923
|
-
if (typeof inner.type === "string") msg += ` [type: ${inner.type}]`;
|
|
924
|
-
return msg;
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
try {
|
|
928
|
-
const json = JSON.stringify(err, getCircularReplacer(), 2);
|
|
929
|
-
if (json && json.length > 1e3) {
|
|
930
|
-
return json.slice(0, 1e3) + "... (truncated)";
|
|
931
|
-
}
|
|
932
|
-
return json || "Unserializable error";
|
|
933
|
-
} catch {
|
|
934
|
-
return `Unserializable error: ${Object.prototype.toString.call(err)}`;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
function getCircularReplacer() {
|
|
938
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
939
|
-
return (_key, value) => {
|
|
940
|
-
if (typeof value === "object" && value !== null) {
|
|
941
|
-
if (seen.has(value)) return "[Circular]";
|
|
942
|
-
seen.add(value);
|
|
943
|
-
}
|
|
944
|
-
return value;
|
|
945
|
-
};
|
|
946
|
-
}
|
|
1111
|
+
// src/SubscriptionDispatcher.ts
|
|
1112
|
+
import { getGeminiCodeAssistResolver } from "@omnicross/core/ports/gemini-code-assist-resolver";
|
|
1113
|
+
import {
|
|
1114
|
+
getSharedAccountHealth as getSharedAccountHealth2,
|
|
1115
|
+
resolveResetSeconds
|
|
1116
|
+
} from "@omnicross/core/pipeline/SubscriptionAccountHealth";
|
|
1117
|
+
import { collectMatchText, deriveSubscriptionSessionKey } from "@omnicross/core/provider-proxy/matchText";
|
|
1118
|
+
import { serializeError } from "@omnicross/core/serializeError";
|
|
947
1119
|
|
|
948
1120
|
// src/opencodego/token-count.ts
|
|
949
1121
|
var cachedEncode = null;
|
|
@@ -970,6 +1142,7 @@ var SubscriptionDispatcher = class {
|
|
|
970
1142
|
* resolution and probe-detection.
|
|
971
1143
|
*/
|
|
972
1144
|
async dispatch(req) {
|
|
1145
|
+
const sessionKey = deriveSubscriptionSessionKey(req.anthropicBody);
|
|
973
1146
|
const ocConfig = this.profile.providerId === "opencodego" ? await this.getOpenCodeGoConfig() : void 0;
|
|
974
1147
|
let scenario = "default";
|
|
975
1148
|
let resolvedModel = req.fallbackModel;
|
|
@@ -988,40 +1161,52 @@ var SubscriptionDispatcher = class {
|
|
|
988
1161
|
provider: resolveOpenCodeGoHalf(resolvedModel, ocConfig),
|
|
989
1162
|
modelId: resolvedModel
|
|
990
1163
|
}) === "anthropic") {
|
|
991
|
-
await this.dispatchAnthropicShapeBypass(req, upstreamUrl, resolvedModel, scenario, ocConfig);
|
|
1164
|
+
await this.dispatchAnthropicShapeBypass(req, upstreamUrl, resolvedModel, scenario, ocConfig, sessionKey);
|
|
992
1165
|
return;
|
|
993
1166
|
}
|
|
994
|
-
await this.dispatchTransformerChain(req, upstreamUrl, resolvedModel, scenario, ocConfig);
|
|
1167
|
+
await this.dispatchTransformerChain(req, upstreamUrl, resolvedModel, scenario, ocConfig, sessionKey);
|
|
995
1168
|
}
|
|
996
1169
|
/** Bypass path for OpenCodeGo MiniMax models — forwards Anthropic body verbatim. */
|
|
997
|
-
async dispatchAnthropicShapeBypass(req, upstreamUrl, resolvedModel, scenario, ocConfig) {
|
|
1170
|
+
async dispatchAnthropicShapeBypass(req, upstreamUrl, resolvedModel, scenario, ocConfig, sessionKey) {
|
|
998
1171
|
const gate = this.gatePrimaryModel(resolvedModel, scenario, ocConfig);
|
|
999
1172
|
const attempted = gate.attempted;
|
|
1000
1173
|
let currentModel = gate.firstModel;
|
|
1174
|
+
let usedAccountId;
|
|
1175
|
+
const reportSelection = (accountId) => {
|
|
1176
|
+
usedAccountId = accountId;
|
|
1177
|
+
};
|
|
1001
1178
|
while (attempted.length < MAX_FALLBACK_ATTEMPTS_LOCAL) {
|
|
1002
1179
|
attempted.push(currentModel);
|
|
1003
1180
|
req.anthropicBody.model = currentModel;
|
|
1004
1181
|
const headers = { "content-type": "application/json" };
|
|
1005
|
-
await this.applyHeadersWithRetry(headers, { upstreamUrl, resolvedModel: currentModel });
|
|
1182
|
+
await this.applyHeadersWithRetry(headers, { upstreamUrl, resolvedModel: currentModel, sessionKey, reportSelection });
|
|
1006
1183
|
console.info(
|
|
1007
1184
|
`[AgentProxy:subscription] REQ#${req.reqId} | opencodego anthropic-shape -> ${upstreamUrl} model=${currentModel} attempt=${attempted.length}`
|
|
1008
1185
|
);
|
|
1009
1186
|
try {
|
|
1010
1187
|
const upstream = await this.hooks.fetchWithRetry(upstreamUrl, headers, req.anthropicBody, currentModel);
|
|
1011
1188
|
this.profile.recordModelOutcome?.(currentModel, true);
|
|
1189
|
+
this.markHealth(usedAccountId, 200);
|
|
1012
1190
|
await this.hooks.writeProxyResponse(req.res, upstream, req.isStream, req.reqId);
|
|
1013
1191
|
return;
|
|
1014
1192
|
} catch (err) {
|
|
1015
|
-
const handled = await this.maybeRetryAfterError(err, headers, req, currentModel);
|
|
1193
|
+
const handled = await this.maybeRetryAfterError(err, headers, req, currentModel, sessionKey);
|
|
1016
1194
|
if (handled.retryOnce) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1195
|
+
try {
|
|
1196
|
+
const upstream = await this.hooks.fetchWithRetry(upstreamUrl, handled.headers, req.anthropicBody, currentModel);
|
|
1197
|
+
this.profile.recordModelOutcome?.(currentModel, true);
|
|
1198
|
+
this.markHealth(usedAccountId, 200);
|
|
1199
|
+
await this.hooks.writeProxyResponse(req.res, upstream, req.isStream, req.reqId);
|
|
1200
|
+
return;
|
|
1201
|
+
} catch (retryErr) {
|
|
1202
|
+
this.markHealth(usedAccountId, errStatus(retryErr), retryErr);
|
|
1203
|
+
throw retryErr;
|
|
1204
|
+
}
|
|
1021
1205
|
}
|
|
1022
1206
|
if (caughtErrorBreakerOutcome(err) === "failure") {
|
|
1023
1207
|
this.profile.recordModelOutcome?.(currentModel, false);
|
|
1024
1208
|
}
|
|
1209
|
+
this.markHealth(usedAccountId, errStatus(err), err);
|
|
1025
1210
|
const next = this.profile.nextFallback?.(scenario, attempted, ocConfig);
|
|
1026
1211
|
if (!next || attempted.length >= MAX_FALLBACK_ATTEMPTS_LOCAL) {
|
|
1027
1212
|
throw err;
|
|
@@ -1035,7 +1220,7 @@ var SubscriptionDispatcher = class {
|
|
|
1035
1220
|
}
|
|
1036
1221
|
}
|
|
1037
1222
|
/** Standard subscription transformer chain — Codex/Gemini/OpenCodeGo OpenAI-shape. */
|
|
1038
|
-
async dispatchTransformerChain(req, upstreamUrl, resolvedModel, scenario, ocConfig) {
|
|
1223
|
+
async dispatchTransformerChain(req, upstreamUrl, resolvedModel, scenario, ocConfig, sessionKey) {
|
|
1039
1224
|
const providerNames = this.profile.resolveProviderTransformerNames?.(resolvedModel, ocConfig) ?? this.profile.providerTransformerNames;
|
|
1040
1225
|
const providerTransformers = this.resolveTransformers(providerNames);
|
|
1041
1226
|
const modelTransformers = this.resolveTransformers(this.profile.modelTransformerNames);
|
|
@@ -1052,6 +1237,10 @@ var SubscriptionDispatcher = class {
|
|
|
1052
1237
|
const gate = this.gatePrimaryModel(resolvedModel, scenario, ocConfig);
|
|
1053
1238
|
const attempted = gate.attempted;
|
|
1054
1239
|
let currentModel = gate.firstModel;
|
|
1240
|
+
let usedAccountId;
|
|
1241
|
+
const reportSelection = (accountId) => {
|
|
1242
|
+
usedAccountId = accountId;
|
|
1243
|
+
};
|
|
1055
1244
|
while (attempted.length < MAX_FALLBACK_ATTEMPTS_LOCAL) {
|
|
1056
1245
|
attempted.push(currentModel);
|
|
1057
1246
|
req.anthropicBody.model = currentModel;
|
|
@@ -1066,7 +1255,7 @@ var SubscriptionDispatcher = class {
|
|
|
1066
1255
|
...config.headers
|
|
1067
1256
|
};
|
|
1068
1257
|
stripAuthHeaders(headers);
|
|
1069
|
-
await this.applyHeadersWithRetry(headers, { upstreamUrl, resolvedModel: currentModel });
|
|
1258
|
+
await this.applyHeadersWithRetry(headers, { upstreamUrl, resolvedModel: currentModel, sessionKey, reportSelection });
|
|
1070
1259
|
const fetchUrl = usesResponsesChain(providerNames) ? upstreamUrl : resolveConfigUrl(config.url) ?? upstreamUrl;
|
|
1071
1260
|
console.info(
|
|
1072
1261
|
`[AgentProxy:subscription] REQ#${req.reqId} | provider=${this.profile.providerId} -> ${fetchUrl} model=${currentModel} attempt=${attempted.length}`
|
|
@@ -1074,6 +1263,7 @@ var SubscriptionDispatcher = class {
|
|
|
1074
1263
|
try {
|
|
1075
1264
|
const upstream = await this.hooks.fetchWithRetry(fetchUrl, headers, requestBody, currentModel);
|
|
1076
1265
|
this.profile.recordModelOutcome?.(currentModel, true);
|
|
1266
|
+
this.markHealth(usedAccountId, 200);
|
|
1077
1267
|
const finalResponse = await this.hooks.executor.executeResponseChain(
|
|
1078
1268
|
requestBody,
|
|
1079
1269
|
upstream,
|
|
@@ -1084,23 +1274,30 @@ var SubscriptionDispatcher = class {
|
|
|
1084
1274
|
await this.hooks.writeProxyResponse(req.res, finalResponse, req.isStream, req.reqId);
|
|
1085
1275
|
return;
|
|
1086
1276
|
} catch (err) {
|
|
1087
|
-
const handled = await this.maybeRetryAfterError(err, headers, req, currentModel);
|
|
1277
|
+
const handled = await this.maybeRetryAfterError(err, headers, req, currentModel, sessionKey);
|
|
1088
1278
|
if (handled.retryOnce) {
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1279
|
+
try {
|
|
1280
|
+
const upstream = await this.hooks.fetchWithRetry(fetchUrl, handled.headers, requestBody, currentModel);
|
|
1281
|
+
this.profile.recordModelOutcome?.(currentModel, true);
|
|
1282
|
+
this.markHealth(usedAccountId, 200);
|
|
1283
|
+
const finalResponse = await this.hooks.executor.executeResponseChain(
|
|
1284
|
+
requestBody,
|
|
1285
|
+
upstream,
|
|
1286
|
+
transformerProvider,
|
|
1287
|
+
{ providerTransformers, modelTransformers },
|
|
1288
|
+
{ endpointTransformer: this.hooks.endpointTransformer }
|
|
1289
|
+
);
|
|
1290
|
+
await this.hooks.writeProxyResponse(req.res, finalResponse, req.isStream, req.reqId);
|
|
1291
|
+
return;
|
|
1292
|
+
} catch (retryErr) {
|
|
1293
|
+
this.markHealth(usedAccountId, errStatus(retryErr), retryErr);
|
|
1294
|
+
throw retryErr;
|
|
1295
|
+
}
|
|
1100
1296
|
}
|
|
1101
1297
|
if (caughtErrorBreakerOutcome(err) === "failure") {
|
|
1102
1298
|
this.profile.recordModelOutcome?.(currentModel, false);
|
|
1103
1299
|
}
|
|
1300
|
+
this.markHealth(usedAccountId, errStatus(err), err);
|
|
1104
1301
|
const next = this.profile.nextFallback?.(scenario, attempted, ocConfig);
|
|
1105
1302
|
if (!next || attempted.length >= MAX_FALLBACK_ATTEMPTS_LOCAL) {
|
|
1106
1303
|
throw err;
|
|
@@ -1148,12 +1345,12 @@ var SubscriptionDispatcher = class {
|
|
|
1148
1345
|
* Returns `{ retryOnce: true, headers }` when the strategy refreshed
|
|
1149
1346
|
* successfully (caller should retry once); otherwise re-throws.
|
|
1150
1347
|
*/
|
|
1151
|
-
async maybeRetryAfterError(err, headers, req, resolvedModel) {
|
|
1348
|
+
async maybeRetryAfterError(err, headers, req, resolvedModel, sessionKey) {
|
|
1152
1349
|
const status = err?.status ?? 0;
|
|
1153
1350
|
if (status !== 401) {
|
|
1154
1351
|
return { retryOnce: false, headers };
|
|
1155
1352
|
}
|
|
1156
|
-
const refreshed = await this.profile.authStrategy.onUnauthorized();
|
|
1353
|
+
const refreshed = await this.profile.authStrategy.onUnauthorized(sessionKey);
|
|
1157
1354
|
if (!refreshed) {
|
|
1158
1355
|
console.warn(
|
|
1159
1356
|
`[AgentProxy:subscription] REQ#${req.reqId} | 401 not recoverable for provider=${this.profile.providerId}`
|
|
@@ -1162,7 +1359,7 @@ var SubscriptionDispatcher = class {
|
|
|
1162
1359
|
}
|
|
1163
1360
|
const fresh = { ...headers };
|
|
1164
1361
|
stripAuthHeaders(fresh);
|
|
1165
|
-
await this.applyHeadersWithRetry(fresh, { upstreamUrl: "", resolvedModel });
|
|
1362
|
+
await this.applyHeadersWithRetry(fresh, { upstreamUrl: "", resolvedModel, sessionKey });
|
|
1166
1363
|
return { retryOnce: true, headers: fresh };
|
|
1167
1364
|
}
|
|
1168
1365
|
async applyHeadersWithRetry(headers, hints) {
|
|
@@ -1172,6 +1369,28 @@ var SubscriptionDispatcher = class {
|
|
|
1172
1369
|
console.warn("[AgentProxy:subscription] authStrategy.applyHeaders threw:", serializeError(err));
|
|
1173
1370
|
}
|
|
1174
1371
|
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Mark the served account's health against ONE attempt's outcome
|
|
1374
|
+
* (subscription-account-health, task 3.4). No-op when no account was reported
|
|
1375
|
+
* (non-pooled / single-account) or on a session-cancel (status 0). On a caught
|
|
1376
|
+
* error `err` is passed so the 429-reset / 403-ban drivers are read STRUCTURALLY
|
|
1377
|
+
* from the error's upstream `headers` + `bodyText` (the `fetchWithRetry`
|
|
1378
|
+
* contract) — so daemon-path 429 cooldown + ban blocking function for
|
|
1379
|
+
* multi-account codex/gemini/opencodego pools; absent headers ⇒ a bare-429
|
|
1380
|
+
* (unmarked, lazy re-probe). Success (2xx) clears; 401/5xx/thrown → transient.
|
|
1381
|
+
*/
|
|
1382
|
+
markHealth(accountId, status, err) {
|
|
1383
|
+
if (accountId === void 0 || status === 0) return;
|
|
1384
|
+
const headers = err !== void 0 ? errHeaders(err) : void 0;
|
|
1385
|
+
const reset = headers ? resolveResetSeconds(this.profile.providerId, headers) : { resetHeaderSeconds: null, retryAfterSeconds: null };
|
|
1386
|
+
const bodyText = status === 403 && err !== void 0 ? errBodyText(err) : void 0;
|
|
1387
|
+
getSharedAccountHealth2().recordUpstreamOutcome(this.profile.providerId, accountId, {
|
|
1388
|
+
status,
|
|
1389
|
+
resetHeaderSeconds: reset.resetHeaderSeconds,
|
|
1390
|
+
retryAfterSeconds: reset.retryAfterSeconds,
|
|
1391
|
+
bodyText
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1175
1394
|
/**
|
|
1176
1395
|
* Resolve the Code Assist project for the gemini subscription profile. Pulls
|
|
1177
1396
|
* the Bearer the bound `AuthStrategy` would inject (so the strategy stays the
|
|
@@ -1186,9 +1405,9 @@ var SubscriptionDispatcher = class {
|
|
|
1186
1405
|
const bearer = probe.Authorization ?? probe.authorization ?? "";
|
|
1187
1406
|
const accessToken = bearer.replace(/^Bearer\s+/i, "").trim();
|
|
1188
1407
|
if (!accessToken) return void 0;
|
|
1189
|
-
const
|
|
1190
|
-
if (!
|
|
1191
|
-
return
|
|
1408
|
+
const resolver = getGeminiCodeAssistResolver();
|
|
1409
|
+
if (!resolver) return void 0;
|
|
1410
|
+
return resolver.resolveProject(accessToken);
|
|
1192
1411
|
}
|
|
1193
1412
|
resolveTransformers(names) {
|
|
1194
1413
|
if (!names || names.length === 0) return [];
|
|
@@ -1248,6 +1467,22 @@ var SubscriptionDispatcher = class {
|
|
|
1248
1467
|
}
|
|
1249
1468
|
};
|
|
1250
1469
|
var MAX_FALLBACK_ATTEMPTS_LOCAL = 3;
|
|
1470
|
+
function errStatus(err) {
|
|
1471
|
+
const status = err?.status;
|
|
1472
|
+
return typeof status === "number" ? status : null;
|
|
1473
|
+
}
|
|
1474
|
+
function errHeaders(err) {
|
|
1475
|
+
const h = err?.headers;
|
|
1476
|
+
if (!h) return void 0;
|
|
1477
|
+
if (typeof h.get === "function") return h;
|
|
1478
|
+
if (typeof h === "object") return h;
|
|
1479
|
+
return void 0;
|
|
1480
|
+
}
|
|
1481
|
+
function errBodyText(err) {
|
|
1482
|
+
const e = err;
|
|
1483
|
+
const raw = typeof e?.bodyText === "string" ? e.bodyText : typeof e?.body === "string" ? e.body : void 0;
|
|
1484
|
+
return raw?.slice(0, 2048);
|
|
1485
|
+
}
|
|
1251
1486
|
function caughtErrorBreakerOutcome(err) {
|
|
1252
1487
|
const status = err?.status;
|
|
1253
1488
|
if (typeof status !== "number") return "failure";
|
|
@@ -1272,360 +1507,11 @@ function stripAuthHeaders(headers) {
|
|
|
1272
1507
|
delete headers["x-goog-api-key"];
|
|
1273
1508
|
delete headers["X-Goog-Api-Key"];
|
|
1274
1509
|
}
|
|
1275
|
-
|
|
1276
|
-
// src/oauth/flows/claude.ts
|
|
1277
|
-
var claude_exports = {};
|
|
1278
|
-
__export(claude_exports, {
|
|
1279
|
-
exchangeCodeForTokens: () => exchangeCodeForTokens,
|
|
1280
|
-
exchangeSetupTokenCode: () => exchangeSetupTokenCode,
|
|
1281
|
-
generateAuthParams: () => generateAuthParams,
|
|
1282
|
-
generateSetupTokenParams: () => generateSetupTokenParams,
|
|
1283
|
-
refreshAccessToken: () => refreshAccessToken
|
|
1284
|
-
});
|
|
1285
|
-
import crypto from "crypto";
|
|
1286
|
-
|
|
1287
|
-
// src/oauth/fetchPort.ts
|
|
1288
|
-
function errorMessage(error, errorDescription) {
|
|
1289
|
-
if (errorDescription) return errorDescription;
|
|
1290
|
-
if (typeof error === "string") return error;
|
|
1291
|
-
if (error && typeof error === "object") {
|
|
1292
|
-
const e = error;
|
|
1293
|
-
if (typeof e.message === "string" && e.message) return e.message;
|
|
1294
|
-
if (typeof e.error_description === "string" && e.error_description) {
|
|
1295
|
-
return e.error_description;
|
|
1296
|
-
}
|
|
1297
|
-
return JSON.stringify(error);
|
|
1298
|
-
}
|
|
1299
|
-
return String(error);
|
|
1300
|
-
}
|
|
1301
|
-
async function postForm(fetchImpl, url, params, parseErrorMessage) {
|
|
1302
|
-
const response = await fetchImpl(url, {
|
|
1303
|
-
method: "POST",
|
|
1304
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1305
|
-
body: params.toString()
|
|
1306
|
-
});
|
|
1307
|
-
const responseData = await response.text();
|
|
1308
|
-
let data;
|
|
1309
|
-
try {
|
|
1310
|
-
data = JSON.parse(responseData);
|
|
1311
|
-
} catch {
|
|
1312
|
-
throw new Error(parseErrorMessage);
|
|
1313
|
-
}
|
|
1314
|
-
if (data.error) {
|
|
1315
|
-
throw new Error(errorMessage(data.error, data.error_description));
|
|
1316
|
-
}
|
|
1317
|
-
return data;
|
|
1318
|
-
}
|
|
1319
|
-
async function postJson(fetchImpl, url, body, parseErrorMessage, extraHeaders = {}) {
|
|
1320
|
-
const response = await fetchImpl(url, {
|
|
1321
|
-
method: "POST",
|
|
1322
|
-
headers: { "Content-Type": "application/json", ...extraHeaders },
|
|
1323
|
-
body: JSON.stringify(body)
|
|
1324
|
-
});
|
|
1325
|
-
const responseData = await response.text();
|
|
1326
|
-
let data;
|
|
1327
|
-
try {
|
|
1328
|
-
data = JSON.parse(responseData);
|
|
1329
|
-
} catch {
|
|
1330
|
-
throw new Error(parseErrorMessage);
|
|
1331
|
-
}
|
|
1332
|
-
if (data.error) {
|
|
1333
|
-
throw new Error(errorMessage(data.error, data.error_description));
|
|
1334
|
-
}
|
|
1335
|
-
return data;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// src/oauth/flows/claude.ts
|
|
1339
|
-
var CLAUDE_TOKEN_HEADERS = {
|
|
1340
|
-
"User-Agent": "claude-cli/1.0.56 (external, cli)",
|
|
1341
|
-
Accept: "application/json, text/plain, */*",
|
|
1342
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
1343
|
-
Referer: "https://claude.ai/",
|
|
1344
|
-
Origin: "https://claude.ai"
|
|
1345
|
-
};
|
|
1346
|
-
var CLAUDE_OAUTH_CONFIG = {
|
|
1347
|
-
clientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
|
1348
|
-
authorizationEndpoint: "https://claude.ai/oauth/authorize",
|
|
1349
|
-
// The token endpoint stays on console.anthropic.com (still the live value —
|
|
1350
|
-
// matches the official Claude Code CLI / claude-relay-service reference); only
|
|
1351
|
-
// the OAuth callback moved to platform.claude.com (2026). The redirect_uri MUST
|
|
1352
|
-
// match what the client is registered for AND match between authorize + token
|
|
1353
|
-
// exchange. Scopes mirror the live Claude Code authorize URL.
|
|
1354
|
-
tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
|
|
1355
|
-
redirectUri: "https://platform.claude.com/oauth/code/callback",
|
|
1356
|
-
scopes: [
|
|
1357
|
-
"org:create_api_key",
|
|
1358
|
-
"user:profile",
|
|
1359
|
-
"user:inference",
|
|
1360
|
-
"user:sessions:claude_code",
|
|
1361
|
-
"user:mcp_servers",
|
|
1362
|
-
"user:file_upload"
|
|
1363
|
-
]
|
|
1364
|
-
};
|
|
1365
|
-
var SETUP_TOKEN_CONFIG = {
|
|
1366
|
-
scopes: ["user:inference"]
|
|
1367
|
-
// Only inference permission, no API key creation
|
|
1368
|
-
};
|
|
1369
|
-
function generatePkce() {
|
|
1370
|
-
const codeVerifier = crypto.randomBytes(32).toString("base64url");
|
|
1371
|
-
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
1372
|
-
const state = crypto.randomBytes(16).toString("hex");
|
|
1373
|
-
return { codeVerifier, codeChallenge, state };
|
|
1374
|
-
}
|
|
1375
|
-
function generateAuthParams() {
|
|
1376
|
-
const { codeVerifier, codeChallenge, state } = generatePkce();
|
|
1377
|
-
const params = new URLSearchParams({
|
|
1378
|
-
code: "true",
|
|
1379
|
-
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
1380
|
-
response_type: "code",
|
|
1381
|
-
redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
|
|
1382
|
-
scope: CLAUDE_OAUTH_CONFIG.scopes.join(" "),
|
|
1383
|
-
code_challenge: codeChallenge,
|
|
1384
|
-
code_challenge_method: "S256",
|
|
1385
|
-
state
|
|
1386
|
-
});
|
|
1387
|
-
const authUrl = `${CLAUDE_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
|
|
1388
|
-
return { authUrl, codeVerifier, state };
|
|
1389
|
-
}
|
|
1390
|
-
function generateSetupTokenParams() {
|
|
1391
|
-
const { codeVerifier, codeChallenge, state } = generatePkce();
|
|
1392
|
-
const params = new URLSearchParams({
|
|
1393
|
-
code: "true",
|
|
1394
|
-
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
1395
|
-
response_type: "code",
|
|
1396
|
-
redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
|
|
1397
|
-
scope: SETUP_TOKEN_CONFIG.scopes.join(" "),
|
|
1398
|
-
code_challenge: codeChallenge,
|
|
1399
|
-
code_challenge_method: "S256",
|
|
1400
|
-
state
|
|
1401
|
-
});
|
|
1402
|
-
const authUrl = `${CLAUDE_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
|
|
1403
|
-
return { authUrl, codeVerifier, state };
|
|
1404
|
-
}
|
|
1405
|
-
async function exchangeCodeForTokens(request, fetchImpl) {
|
|
1406
|
-
const { authorizationCode, codeVerifier, state } = request;
|
|
1407
|
-
const code = authorizationCode.split("#")[0]?.split("&")[0] ?? authorizationCode;
|
|
1408
|
-
const data = await postJson(
|
|
1409
|
-
fetchImpl,
|
|
1410
|
-
CLAUDE_OAUTH_CONFIG.tokenEndpoint,
|
|
1411
|
-
{
|
|
1412
|
-
grant_type: "authorization_code",
|
|
1413
|
-
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
1414
|
-
code,
|
|
1415
|
-
code_verifier: codeVerifier,
|
|
1416
|
-
redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
|
|
1417
|
-
state
|
|
1418
|
-
},
|
|
1419
|
-
"Failed to parse token response",
|
|
1420
|
-
CLAUDE_TOKEN_HEADERS
|
|
1421
|
-
);
|
|
1422
|
-
return {
|
|
1423
|
-
accessToken: data.access_token,
|
|
1424
|
-
// The authorization_code grant always returns a refresh_token; the original
|
|
1425
|
-
// helper read it from an untyped `data` and declared the field `string`.
|
|
1426
|
-
refreshToken: data.refresh_token,
|
|
1427
|
-
expiresIn: data.expires_in,
|
|
1428
|
-
scopes: data.scope?.split(" ") || CLAUDE_OAUTH_CONFIG.scopes
|
|
1429
|
-
};
|
|
1430
|
-
}
|
|
1431
|
-
async function exchangeSetupTokenCode(request, fetchImpl) {
|
|
1432
|
-
const { authorizationCode, codeVerifier, state } = request;
|
|
1433
|
-
const code = authorizationCode.split("#")[0]?.split("&")[0] ?? authorizationCode;
|
|
1434
|
-
const data = await postJson(
|
|
1435
|
-
fetchImpl,
|
|
1436
|
-
CLAUDE_OAUTH_CONFIG.tokenEndpoint,
|
|
1437
|
-
{
|
|
1438
|
-
grant_type: "authorization_code",
|
|
1439
|
-
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
1440
|
-
code,
|
|
1441
|
-
code_verifier: codeVerifier,
|
|
1442
|
-
redirect_uri: CLAUDE_OAUTH_CONFIG.redirectUri,
|
|
1443
|
-
state
|
|
1444
|
-
},
|
|
1445
|
-
"Failed to parse setup token response",
|
|
1446
|
-
CLAUDE_TOKEN_HEADERS
|
|
1447
|
-
);
|
|
1448
|
-
return {
|
|
1449
|
-
accessToken: data.access_token,
|
|
1450
|
-
expiresIn: data.expires_in,
|
|
1451
|
-
scopes: data.scope?.split(" ") || SETUP_TOKEN_CONFIG.scopes
|
|
1452
|
-
};
|
|
1453
|
-
}
|
|
1454
|
-
async function refreshAccessToken(refreshToken, fetchImpl) {
|
|
1455
|
-
const data = await postJson(
|
|
1456
|
-
fetchImpl,
|
|
1457
|
-
CLAUDE_OAUTH_CONFIG.tokenEndpoint,
|
|
1458
|
-
{
|
|
1459
|
-
grant_type: "refresh_token",
|
|
1460
|
-
client_id: CLAUDE_OAUTH_CONFIG.clientId,
|
|
1461
|
-
refresh_token: refreshToken
|
|
1462
|
-
},
|
|
1463
|
-
"Failed to parse refresh response",
|
|
1464
|
-
CLAUDE_TOKEN_HEADERS
|
|
1465
|
-
);
|
|
1466
|
-
return {
|
|
1467
|
-
accessToken: data.access_token,
|
|
1468
|
-
refreshToken: data.refresh_token || refreshToken,
|
|
1469
|
-
expiresIn: data.expires_in
|
|
1470
|
-
};
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
// src/oauth/flows/codex.ts
|
|
1474
|
-
var codex_exports = {};
|
|
1475
|
-
__export(codex_exports, {
|
|
1476
|
-
exchangeCodeForTokens: () => exchangeCodeForTokens2,
|
|
1477
|
-
generateAuthParams: () => generateAuthParams2,
|
|
1478
|
-
refreshAccessToken: () => refreshAccessToken2
|
|
1479
|
-
});
|
|
1480
|
-
import crypto2 from "crypto";
|
|
1481
|
-
var CODEX_OAUTH_CONFIG = {
|
|
1482
|
-
clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
1483
|
-
authorizationEndpoint: "https://auth.openai.com/oauth/authorize",
|
|
1484
|
-
tokenEndpoint: "https://auth.openai.com/oauth/token",
|
|
1485
|
-
redirectUri: "http://localhost:1455/auth/callback",
|
|
1486
|
-
scopes: ["openid", "profile", "email", "offline_access"]
|
|
1487
|
-
};
|
|
1488
|
-
function generateAuthParams2() {
|
|
1489
|
-
const codeVerifier = crypto2.randomBytes(64).toString("hex");
|
|
1490
|
-
const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
1491
|
-
const state = crypto2.randomBytes(16).toString("hex");
|
|
1492
|
-
const params = new URLSearchParams({
|
|
1493
|
-
response_type: "code",
|
|
1494
|
-
client_id: CODEX_OAUTH_CONFIG.clientId,
|
|
1495
|
-
redirect_uri: CODEX_OAUTH_CONFIG.redirectUri,
|
|
1496
|
-
scope: CODEX_OAUTH_CONFIG.scopes.join(" "),
|
|
1497
|
-
code_challenge: codeChallenge,
|
|
1498
|
-
code_challenge_method: "S256",
|
|
1499
|
-
state
|
|
1500
|
-
});
|
|
1501
|
-
const authUrl = `${CODEX_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
|
|
1502
|
-
return { authUrl, codeVerifier, state };
|
|
1503
|
-
}
|
|
1504
|
-
async function exchangeCodeForTokens2(request, fetchImpl) {
|
|
1505
|
-
const { authorizationCode, codeVerifier } = request;
|
|
1506
|
-
const params = new URLSearchParams({
|
|
1507
|
-
grant_type: "authorization_code",
|
|
1508
|
-
client_id: CODEX_OAUTH_CONFIG.clientId,
|
|
1509
|
-
code: authorizationCode,
|
|
1510
|
-
code_verifier: codeVerifier,
|
|
1511
|
-
redirect_uri: CODEX_OAUTH_CONFIG.redirectUri
|
|
1512
|
-
});
|
|
1513
|
-
const data = await postForm(
|
|
1514
|
-
fetchImpl,
|
|
1515
|
-
CODEX_OAUTH_CONFIG.tokenEndpoint,
|
|
1516
|
-
params,
|
|
1517
|
-
"Failed to parse token response"
|
|
1518
|
-
);
|
|
1519
|
-
return {
|
|
1520
|
-
accessToken: data.access_token,
|
|
1521
|
-
// authorization_code grant returns both; the original helper read them from
|
|
1522
|
-
// an untyped `data` and declared the fields `string`.
|
|
1523
|
-
refreshToken: data.refresh_token,
|
|
1524
|
-
idToken: data.id_token,
|
|
1525
|
-
expiresIn: data.expires_in
|
|
1526
|
-
};
|
|
1527
|
-
}
|
|
1528
|
-
async function refreshAccessToken2(refreshToken, fetchImpl) {
|
|
1529
|
-
const params = new URLSearchParams({
|
|
1530
|
-
grant_type: "refresh_token",
|
|
1531
|
-
client_id: CODEX_OAUTH_CONFIG.clientId,
|
|
1532
|
-
refresh_token: refreshToken,
|
|
1533
|
-
scope: "openid profile email"
|
|
1534
|
-
});
|
|
1535
|
-
const data = await postForm(
|
|
1536
|
-
fetchImpl,
|
|
1537
|
-
CODEX_OAUTH_CONFIG.tokenEndpoint,
|
|
1538
|
-
params,
|
|
1539
|
-
"Failed to parse refresh response"
|
|
1540
|
-
);
|
|
1541
|
-
return {
|
|
1542
|
-
accessToken: data.access_token,
|
|
1543
|
-
idToken: data.id_token,
|
|
1544
|
-
refreshToken: data.refresh_token || refreshToken,
|
|
1545
|
-
expiresIn: data.expires_in || 3600
|
|
1546
|
-
};
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
// src/oauth/flows/gemini.ts
|
|
1550
|
-
var gemini_exports = {};
|
|
1551
|
-
__export(gemini_exports, {
|
|
1552
|
-
exchangeCodeForTokens: () => exchangeCodeForTokens3,
|
|
1553
|
-
generateAuthParams: () => generateAuthParams3,
|
|
1554
|
-
refreshAccessToken: () => refreshAccessToken3
|
|
1555
|
-
});
|
|
1556
|
-
import crypto3 from "crypto";
|
|
1557
|
-
var GEMINI_OAUTH_CONFIG = {
|
|
1558
|
-
clientId: "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
|
|
1559
|
-
// The Gemini CLI's *public* installed-app OAuth client secret (mirrors the
|
|
1560
|
-
// upstream CLI). Per Google's OAuth docs, native-app client secrets are not
|
|
1561
|
-
// treated as confidential — not a leaked key.
|
|
1562
|
-
clientSecret: "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",
|
|
1563
|
-
// allowlist-secret
|
|
1564
|
-
authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
1565
|
-
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
|
1566
|
-
redirectUri: "urn:ietf:wg:oauth:2.0:oob",
|
|
1567
|
-
scopes: ["https://www.googleapis.com/auth/cloud-platform"]
|
|
1568
|
-
};
|
|
1569
|
-
function generateAuthParams3() {
|
|
1570
|
-
const codeVerifier = crypto3.randomBytes(32).toString("base64url");
|
|
1571
|
-
const codeChallenge = crypto3.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
1572
|
-
const state = crypto3.randomBytes(16).toString("hex");
|
|
1573
|
-
const params = new URLSearchParams({
|
|
1574
|
-
client_id: GEMINI_OAUTH_CONFIG.clientId,
|
|
1575
|
-
redirect_uri: GEMINI_OAUTH_CONFIG.redirectUri,
|
|
1576
|
-
scope: GEMINI_OAUTH_CONFIG.scopes.join(" "),
|
|
1577
|
-
response_type: "code",
|
|
1578
|
-
code_challenge: codeChallenge,
|
|
1579
|
-
code_challenge_method: "S256",
|
|
1580
|
-
state,
|
|
1581
|
-
access_type: "offline",
|
|
1582
|
-
prompt: "consent"
|
|
1583
|
-
});
|
|
1584
|
-
const authUrl = `${GEMINI_OAUTH_CONFIG.authorizationEndpoint}?${params.toString()}`;
|
|
1585
|
-
return { authUrl, codeVerifier, state };
|
|
1586
|
-
}
|
|
1587
|
-
async function exchangeCodeForTokens3(authorizationCode, codeVerifier, fetchImpl) {
|
|
1588
|
-
const params = new URLSearchParams({
|
|
1589
|
-
grant_type: "authorization_code",
|
|
1590
|
-
client_id: GEMINI_OAUTH_CONFIG.clientId,
|
|
1591
|
-
client_secret: GEMINI_OAUTH_CONFIG.clientSecret,
|
|
1592
|
-
code: authorizationCode,
|
|
1593
|
-
code_verifier: codeVerifier,
|
|
1594
|
-
redirect_uri: GEMINI_OAUTH_CONFIG.redirectUri
|
|
1595
|
-
});
|
|
1596
|
-
const data = await postForm(
|
|
1597
|
-
fetchImpl,
|
|
1598
|
-
GEMINI_OAUTH_CONFIG.tokenEndpoint,
|
|
1599
|
-
params,
|
|
1600
|
-
"Failed to parse token response"
|
|
1601
|
-
);
|
|
1602
|
-
return {
|
|
1603
|
-
accessToken: data.access_token,
|
|
1604
|
-
// authorization_code grant returns a refresh_token; the original helper read
|
|
1605
|
-
// it from an untyped `data` and declared the field `string`.
|
|
1606
|
-
refreshToken: data.refresh_token,
|
|
1607
|
-
expiresIn: data.expires_in
|
|
1608
|
-
};
|
|
1609
|
-
}
|
|
1610
|
-
async function refreshAccessToken3(refreshToken, fetchImpl) {
|
|
1611
|
-
const params = new URLSearchParams({
|
|
1612
|
-
grant_type: "refresh_token",
|
|
1613
|
-
client_id: GEMINI_OAUTH_CONFIG.clientId,
|
|
1614
|
-
client_secret: GEMINI_OAUTH_CONFIG.clientSecret,
|
|
1615
|
-
refresh_token: refreshToken
|
|
1616
|
-
});
|
|
1617
|
-
const data = await postForm(
|
|
1618
|
-
fetchImpl,
|
|
1619
|
-
GEMINI_OAUTH_CONFIG.tokenEndpoint,
|
|
1620
|
-
params,
|
|
1621
|
-
"Failed to parse refresh response"
|
|
1622
|
-
);
|
|
1623
|
-
return {
|
|
1624
|
-
accessToken: data.access_token,
|
|
1625
|
-
expiresIn: data.expires_in
|
|
1626
|
-
};
|
|
1627
|
-
}
|
|
1628
1510
|
export {
|
|
1511
|
+
DEFAULT_ACCOUNT_PRIORITY,
|
|
1512
|
+
LAST_USED_PERSIST_THROTTLE_MS,
|
|
1513
|
+
SESSION_AFFINITY_TTL_MS,
|
|
1514
|
+
SubscriptionAccountSelector,
|
|
1629
1515
|
SubscriptionAccountService,
|
|
1630
1516
|
SubscriptionDispatcher,
|
|
1631
1517
|
SubscriptionProviderRegistry,
|