@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/index.js CHANGED
@@ -1,29 +1,281 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
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, _hints) {
20
- const token = await this.resolveAccessToken();
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, _hints) {
84
- const token = await this.tokens.getValidClaudeAccessToken();
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 this.tokens.getValidOpenCodeGoApiKey();
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
- // ../core/src/outbound-api/subscriptionRegistryPort.ts
227
- var _registry = null;
228
- function setSubscriptionRegistryForOutbound(registry) {
229
- _registry = registry;
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
- // ../core/src/ports/gemini-code-assist-resolver.ts
848
- var resolver = null;
849
- function getGeminiCodeAssistResolver() {
850
- return resolver;
851
- }
852
-
853
- // ../core/src/provider-proxy/matchText.ts
854
- var MATCH_TEXT_PER_MESSAGE_CAP = 8192;
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
- const upstream = await this.hooks.fetchWithRetry(upstreamUrl, handled.headers, req.anthropicBody, currentModel);
1018
- this.profile.recordModelOutcome?.(currentModel, true);
1019
- await this.hooks.writeProxyResponse(req.res, upstream, req.isStream, req.reqId);
1020
- return;
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
- const upstream = await this.hooks.fetchWithRetry(fetchUrl, handled.headers, requestBody, currentModel);
1090
- this.profile.recordModelOutcome?.(currentModel, true);
1091
- const finalResponse = await this.hooks.executor.executeResponseChain(
1092
- requestBody,
1093
- upstream,
1094
- transformerProvider,
1095
- { providerTransformers, modelTransformers },
1096
- { endpointTransformer: this.hooks.endpointTransformer }
1097
- );
1098
- await this.hooks.writeProxyResponse(req.res, finalResponse, req.isStream, req.reqId);
1099
- return;
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 resolver2 = getGeminiCodeAssistResolver();
1190
- if (!resolver2) return void 0;
1191
- return resolver2.resolveProject(accessToken);
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,