oc-chatgpt-multi-auth 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/LICENSE +37 -0
  2. package/README.md +507 -0
  3. package/assets/opencode-logo-ornate-dark.svg +18 -0
  4. package/assets/readme-hero.svg +31 -0
  5. package/config/README.md +110 -0
  6. package/config/minimal-opencode.json +13 -0
  7. package/config/opencode-legacy.json +572 -0
  8. package/config/opencode-modern.json +240 -0
  9. package/dist/index.d.ts +45 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +971 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/accounts.d.ts +120 -0
  14. package/dist/lib/accounts.d.ts.map +1 -0
  15. package/dist/lib/accounts.js +579 -0
  16. package/dist/lib/accounts.js.map +1 -0
  17. package/dist/lib/auth/auth.d.ts +51 -0
  18. package/dist/lib/auth/auth.d.ts.map +1 -0
  19. package/dist/lib/auth/auth.js +180 -0
  20. package/dist/lib/auth/auth.js.map +1 -0
  21. package/dist/lib/auth/browser.d.ts +17 -0
  22. package/dist/lib/auth/browser.d.ts.map +1 -0
  23. package/dist/lib/auth/browser.js +83 -0
  24. package/dist/lib/auth/browser.js.map +1 -0
  25. package/dist/lib/auth/server.d.ts +10 -0
  26. package/dist/lib/auth/server.d.ts.map +1 -0
  27. package/dist/lib/auth/server.js +85 -0
  28. package/dist/lib/auth/server.js.map +1 -0
  29. package/dist/lib/auto-update-checker.d.ts +10 -0
  30. package/dist/lib/auto-update-checker.d.ts.map +1 -0
  31. package/dist/lib/auto-update-checker.js +129 -0
  32. package/dist/lib/auto-update-checker.js.map +1 -0
  33. package/dist/lib/cli.d.ts +9 -0
  34. package/dist/lib/cli.d.ts.map +1 -0
  35. package/dist/lib/cli.js +50 -0
  36. package/dist/lib/cli.js.map +1 -0
  37. package/dist/lib/config.d.ts +17 -0
  38. package/dist/lib/config.d.ts.map +1 -0
  39. package/dist/lib/config.js +102 -0
  40. package/dist/lib/config.js.map +1 -0
  41. package/dist/lib/constants.d.ts +74 -0
  42. package/dist/lib/constants.d.ts.map +1 -0
  43. package/dist/lib/constants.js +74 -0
  44. package/dist/lib/constants.js.map +1 -0
  45. package/dist/lib/context-overflow.d.ts +27 -0
  46. package/dist/lib/context-overflow.d.ts.map +1 -0
  47. package/dist/lib/context-overflow.js +124 -0
  48. package/dist/lib/context-overflow.js.map +1 -0
  49. package/dist/lib/index.d.ts +13 -0
  50. package/dist/lib/index.d.ts.map +1 -0
  51. package/dist/lib/index.js +13 -0
  52. package/dist/lib/index.js.map +1 -0
  53. package/dist/lib/logger.d.ts +22 -0
  54. package/dist/lib/logger.d.ts.map +1 -0
  55. package/dist/lib/logger.js +175 -0
  56. package/dist/lib/logger.js.map +1 -0
  57. package/dist/lib/oauth-success.html +712 -0
  58. package/dist/lib/prompts/codex-opencode-bridge.d.ts +19 -0
  59. package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -0
  60. package/dist/lib/prompts/codex-opencode-bridge.js +152 -0
  61. package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -0
  62. package/dist/lib/prompts/codex.d.ts +32 -0
  63. package/dist/lib/prompts/codex.d.ts.map +1 -0
  64. package/dist/lib/prompts/codex.js +262 -0
  65. package/dist/lib/prompts/codex.js.map +1 -0
  66. package/dist/lib/prompts/opencode-codex.d.ts +21 -0
  67. package/dist/lib/prompts/opencode-codex.d.ts.map +1 -0
  68. package/dist/lib/prompts/opencode-codex.js +91 -0
  69. package/dist/lib/prompts/opencode-codex.js.map +1 -0
  70. package/dist/lib/recovery/constants.d.ts +12 -0
  71. package/dist/lib/recovery/constants.d.ts.map +1 -0
  72. package/dist/lib/recovery/constants.js +25 -0
  73. package/dist/lib/recovery/constants.js.map +1 -0
  74. package/dist/lib/recovery/index.d.ts +12 -0
  75. package/dist/lib/recovery/index.d.ts.map +1 -0
  76. package/dist/lib/recovery/index.js +12 -0
  77. package/dist/lib/recovery/index.js.map +1 -0
  78. package/dist/lib/recovery/storage.d.ts +24 -0
  79. package/dist/lib/recovery/storage.d.ts.map +1 -0
  80. package/dist/lib/recovery/storage.js +354 -0
  81. package/dist/lib/recovery/storage.js.map +1 -0
  82. package/dist/lib/recovery/types.d.ts +116 -0
  83. package/dist/lib/recovery/types.d.ts.map +1 -0
  84. package/dist/lib/recovery/types.js +7 -0
  85. package/dist/lib/recovery/types.js.map +1 -0
  86. package/dist/lib/recovery.d.ts +31 -0
  87. package/dist/lib/recovery.d.ts.map +1 -0
  88. package/dist/lib/recovery.js +308 -0
  89. package/dist/lib/recovery.js.map +1 -0
  90. package/dist/lib/refresh-queue.d.ts +100 -0
  91. package/dist/lib/refresh-queue.d.ts.map +1 -0
  92. package/dist/lib/refresh-queue.js +196 -0
  93. package/dist/lib/refresh-queue.js.map +1 -0
  94. package/dist/lib/request/fetch-helpers.d.ts +81 -0
  95. package/dist/lib/request/fetch-helpers.d.ts.map +1 -0
  96. package/dist/lib/request/fetch-helpers.js +325 -0
  97. package/dist/lib/request/fetch-helpers.js.map +1 -0
  98. package/dist/lib/request/helpers/input-utils.d.ts +7 -0
  99. package/dist/lib/request/helpers/input-utils.d.ts.map +1 -0
  100. package/dist/lib/request/helpers/input-utils.js +213 -0
  101. package/dist/lib/request/helpers/input-utils.js.map +1 -0
  102. package/dist/lib/request/helpers/model-map.d.ts +28 -0
  103. package/dist/lib/request/helpers/model-map.d.ts.map +1 -0
  104. package/dist/lib/request/helpers/model-map.js +109 -0
  105. package/dist/lib/request/helpers/model-map.js.map +1 -0
  106. package/dist/lib/request/rate-limit-backoff.d.ts +17 -0
  107. package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -0
  108. package/dist/lib/request/rate-limit-backoff.js +74 -0
  109. package/dist/lib/request/rate-limit-backoff.js.map +1 -0
  110. package/dist/lib/request/request-transformer.d.ts +93 -0
  111. package/dist/lib/request/request-transformer.d.ts.map +1 -0
  112. package/dist/lib/request/request-transformer.js +405 -0
  113. package/dist/lib/request/request-transformer.js.map +1 -0
  114. package/dist/lib/request/response-handler.d.ts +14 -0
  115. package/dist/lib/request/response-handler.d.ts.map +1 -0
  116. package/dist/lib/request/response-handler.js +90 -0
  117. package/dist/lib/request/response-handler.js.map +1 -0
  118. package/dist/lib/rotation.d.ts +121 -0
  119. package/dist/lib/rotation.d.ts.map +1 -0
  120. package/dist/lib/rotation.js +248 -0
  121. package/dist/lib/rotation.js.map +1 -0
  122. package/dist/lib/storage.d.ts +91 -0
  123. package/dist/lib/storage.d.ts.map +1 -0
  124. package/dist/lib/storage.js +323 -0
  125. package/dist/lib/storage.js.map +1 -0
  126. package/dist/lib/types.d.ts +185 -0
  127. package/dist/lib/types.d.ts.map +1 -0
  128. package/dist/lib/types.js +2 -0
  129. package/dist/lib/types.js.map +1 -0
  130. package/package.json +86 -0
  131. package/scripts/copy-oauth-success.js +37 -0
  132. package/scripts/install-opencode-codex-auth.js +193 -0
  133. package/scripts/test-all-models.sh +260 -0
  134. package/scripts/validate-model-map.sh +97 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Rotation Strategy Module
3
+ *
4
+ * Implements health-based account selection with token bucket rate limiting.
5
+ * Ported from antigravity-auth rotation logic for optimal account rotation
6
+ * when rate limits are encountered.
7
+ */
8
+ export const DEFAULT_HEALTH_SCORE_CONFIG = {
9
+ successDelta: 1,
10
+ rateLimitDelta: -10,
11
+ failureDelta: -20,
12
+ maxScore: 100,
13
+ minScore: 0,
14
+ passiveRecoveryPerHour: 2,
15
+ };
16
+ /**
17
+ * Tracks health scores for accounts to prioritize healthy accounts.
18
+ * Accounts with higher health scores are preferred for selection.
19
+ */
20
+ export class HealthScoreTracker {
21
+ entries = new Map();
22
+ config;
23
+ constructor(config = {}) {
24
+ this.config = { ...DEFAULT_HEALTH_SCORE_CONFIG, ...config };
25
+ }
26
+ getKey(accountIndex, quotaKey) {
27
+ return quotaKey ? `${accountIndex}:${quotaKey}` : `${accountIndex}`;
28
+ }
29
+ applyPassiveRecovery(entry) {
30
+ const now = Date.now();
31
+ const hoursSinceUpdate = (now - entry.lastUpdated) / (1000 * 60 * 60);
32
+ const recovery = hoursSinceUpdate * this.config.passiveRecoveryPerHour;
33
+ return Math.min(entry.score + recovery, this.config.maxScore);
34
+ }
35
+ getScore(accountIndex, quotaKey) {
36
+ const key = this.getKey(accountIndex, quotaKey);
37
+ const entry = this.entries.get(key);
38
+ if (!entry)
39
+ return this.config.maxScore;
40
+ return this.applyPassiveRecovery(entry);
41
+ }
42
+ getConsecutiveFailures(accountIndex, quotaKey) {
43
+ const key = this.getKey(accountIndex, quotaKey);
44
+ const entry = this.entries.get(key);
45
+ return entry?.consecutiveFailures ?? 0;
46
+ }
47
+ recordSuccess(accountIndex, quotaKey) {
48
+ const key = this.getKey(accountIndex, quotaKey);
49
+ const entry = this.entries.get(key);
50
+ const baseScore = entry ? this.applyPassiveRecovery(entry) : this.config.maxScore;
51
+ const newScore = Math.min(baseScore + this.config.successDelta, this.config.maxScore);
52
+ this.entries.set(key, {
53
+ score: newScore,
54
+ lastUpdated: Date.now(),
55
+ consecutiveFailures: 0,
56
+ });
57
+ }
58
+ recordRateLimit(accountIndex, quotaKey) {
59
+ const key = this.getKey(accountIndex, quotaKey);
60
+ const entry = this.entries.get(key);
61
+ const baseScore = entry ? this.applyPassiveRecovery(entry) : this.config.maxScore;
62
+ const newScore = Math.max(baseScore + this.config.rateLimitDelta, this.config.minScore);
63
+ this.entries.set(key, {
64
+ score: newScore,
65
+ lastUpdated: Date.now(),
66
+ consecutiveFailures: (entry?.consecutiveFailures ?? 0) + 1,
67
+ });
68
+ }
69
+ recordFailure(accountIndex, quotaKey) {
70
+ const key = this.getKey(accountIndex, quotaKey);
71
+ const entry = this.entries.get(key);
72
+ const baseScore = entry ? this.applyPassiveRecovery(entry) : this.config.maxScore;
73
+ const newScore = Math.max(baseScore + this.config.failureDelta, this.config.minScore);
74
+ this.entries.set(key, {
75
+ score: newScore,
76
+ lastUpdated: Date.now(),
77
+ consecutiveFailures: (entry?.consecutiveFailures ?? 0) + 1,
78
+ });
79
+ }
80
+ reset(accountIndex, quotaKey) {
81
+ const key = this.getKey(accountIndex, quotaKey);
82
+ this.entries.delete(key);
83
+ }
84
+ clear() {
85
+ this.entries.clear();
86
+ }
87
+ }
88
+ export const DEFAULT_TOKEN_BUCKET_CONFIG = {
89
+ maxTokens: 50,
90
+ tokensPerMinute: 6,
91
+ };
92
+ /**
93
+ * Client-side token bucket for rate limiting requests per account.
94
+ * Prevents sending requests to accounts that are likely to be rate-limited.
95
+ */
96
+ export class TokenBucketTracker {
97
+ buckets = new Map();
98
+ config;
99
+ constructor(config = {}) {
100
+ this.config = { ...DEFAULT_TOKEN_BUCKET_CONFIG, ...config };
101
+ }
102
+ getKey(accountIndex, quotaKey) {
103
+ return quotaKey ? `${accountIndex}:${quotaKey}` : `${accountIndex}`;
104
+ }
105
+ refillTokens(entry) {
106
+ const now = Date.now();
107
+ const minutesSinceRefill = (now - entry.lastRefill) / (1000 * 60);
108
+ const tokensToAdd = minutesSinceRefill * this.config.tokensPerMinute;
109
+ return Math.min(entry.tokens + tokensToAdd, this.config.maxTokens);
110
+ }
111
+ getTokens(accountIndex, quotaKey) {
112
+ const key = this.getKey(accountIndex, quotaKey);
113
+ const entry = this.buckets.get(key);
114
+ if (!entry)
115
+ return this.config.maxTokens;
116
+ return this.refillTokens(entry);
117
+ }
118
+ /**
119
+ * Attempt to consume a token. Returns true if successful, false if bucket is empty.
120
+ */
121
+ tryConsume(accountIndex, quotaKey) {
122
+ const key = this.getKey(accountIndex, quotaKey);
123
+ const entry = this.buckets.get(key);
124
+ const currentTokens = entry ? this.refillTokens(entry) : this.config.maxTokens;
125
+ if (currentTokens < 1) {
126
+ return false;
127
+ }
128
+ this.buckets.set(key, {
129
+ tokens: currentTokens - 1,
130
+ lastRefill: Date.now(),
131
+ });
132
+ return true;
133
+ }
134
+ /**
135
+ * Drain tokens on rate limit to prevent immediate retries.
136
+ */
137
+ drain(accountIndex, quotaKey, drainAmount = 10) {
138
+ const key = this.getKey(accountIndex, quotaKey);
139
+ const entry = this.buckets.get(key);
140
+ const currentTokens = entry ? this.refillTokens(entry) : this.config.maxTokens;
141
+ this.buckets.set(key, {
142
+ tokens: Math.max(0, currentTokens - drainAmount),
143
+ lastRefill: Date.now(),
144
+ });
145
+ }
146
+ reset(accountIndex, quotaKey) {
147
+ const key = this.getKey(accountIndex, quotaKey);
148
+ this.buckets.delete(key);
149
+ }
150
+ clear() {
151
+ this.buckets.clear();
152
+ }
153
+ }
154
+ export const DEFAULT_HYBRID_SELECTION_CONFIG = {
155
+ healthWeight: 2,
156
+ tokenWeight: 5,
157
+ freshnessWeight: 0.1,
158
+ };
159
+ /**
160
+ * Selects the best account using a hybrid scoring strategy.
161
+ *
162
+ * Score = (health * healthWeight) + (tokens * tokenWeight) + (freshness * freshnessWeight)
163
+ *
164
+ * Where:
165
+ * - health: Account health score (0-100)
166
+ * - tokens: Available tokens in bucket (0-maxTokens)
167
+ * - freshness: Hours since last used (higher = more fresh for rotation)
168
+ */
169
+ export function selectHybridAccount(accounts, healthTracker, tokenTracker, quotaKey, config = {}) {
170
+ const cfg = { ...DEFAULT_HYBRID_SELECTION_CONFIG, ...config };
171
+ const available = accounts.filter((a) => a.isAvailable);
172
+ if (available.length === 0)
173
+ return null;
174
+ if (available.length === 1)
175
+ return available[0];
176
+ const now = Date.now();
177
+ let bestAccount = null;
178
+ let bestScore = -Infinity;
179
+ for (const account of available) {
180
+ const health = healthTracker.getScore(account.index, quotaKey);
181
+ const tokens = tokenTracker.getTokens(account.index, quotaKey);
182
+ const hoursSinceUsed = (now - account.lastUsed) / (1000 * 60 * 60);
183
+ const score = health * cfg.healthWeight +
184
+ tokens * cfg.tokenWeight +
185
+ hoursSinceUsed * cfg.freshnessWeight;
186
+ if (score > bestScore) {
187
+ bestScore = score;
188
+ bestAccount = account;
189
+ }
190
+ }
191
+ return bestAccount;
192
+ }
193
+ // ============================================================================
194
+ // Utility Functions
195
+ // ============================================================================
196
+ /**
197
+ * Adds random jitter to a delay value.
198
+ * @param baseMs - Base delay in milliseconds
199
+ * @param jitterFactor - Jitter factor (0-1), default 0.1 (10%)
200
+ * @returns Delay with jitter applied
201
+ */
202
+ export function addJitter(baseMs, jitterFactor = 0.1) {
203
+ const jitter = baseMs * jitterFactor * (Math.random() * 2 - 1);
204
+ return Math.max(0, Math.floor(baseMs + jitter));
205
+ }
206
+ /**
207
+ * Returns a random delay within a range.
208
+ * @param minMs - Minimum delay in milliseconds
209
+ * @param maxMs - Maximum delay in milliseconds
210
+ * @returns Random delay within range
211
+ */
212
+ export function randomDelay(minMs, maxMs) {
213
+ return Math.floor(minMs + Math.random() * (maxMs - minMs));
214
+ }
215
+ /**
216
+ * Calculates exponential backoff with jitter.
217
+ * @param attempt - Attempt number (1-based)
218
+ * @param baseMs - Base delay in milliseconds
219
+ * @param maxMs - Maximum delay in milliseconds
220
+ * @param jitterFactor - Jitter factor (0-1)
221
+ * @returns Backoff delay with jitter
222
+ */
223
+ export function exponentialBackoff(attempt, baseMs = 1000, maxMs = 60000, jitterFactor = 0.1) {
224
+ const delay = Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
225
+ return addJitter(delay, jitterFactor);
226
+ }
227
+ // ============================================================================
228
+ // Singleton Instances
229
+ // ============================================================================
230
+ let healthTrackerInstance = null;
231
+ let tokenTrackerInstance = null;
232
+ export function getHealthTracker(config) {
233
+ if (!healthTrackerInstance) {
234
+ healthTrackerInstance = new HealthScoreTracker(config);
235
+ }
236
+ return healthTrackerInstance;
237
+ }
238
+ export function getTokenTracker(config) {
239
+ if (!tokenTrackerInstance) {
240
+ tokenTrackerInstance = new TokenBucketTracker(config);
241
+ }
242
+ return tokenTrackerInstance;
243
+ }
244
+ export function resetTrackers() {
245
+ healthTrackerInstance?.clear();
246
+ tokenTrackerInstance?.clear();
247
+ }
248
+ //# sourceMappingURL=rotation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rotation.js","sourceRoot":"","sources":["../../lib/rotation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ import { type ModelFamily } from "./prompts/codex.js";
2
+ export type CooldownReason = "auth-failure" | "network-error";
3
+ export interface RateLimitStateV3 {
4
+ [key: string]: number | undefined;
5
+ }
6
+ export interface AccountMetadataV1 {
7
+ accountId?: string;
8
+ email?: string;
9
+ refreshToken: string;
10
+ addedAt: number;
11
+ lastUsed: number;
12
+ lastSwitchReason?: "rate-limit" | "initial" | "rotation";
13
+ rateLimitResetTime?: number;
14
+ coolingDownUntil?: number;
15
+ cooldownReason?: CooldownReason;
16
+ }
17
+ export interface AccountStorageV1 {
18
+ version: 1;
19
+ accounts: AccountMetadataV1[];
20
+ activeIndex: number;
21
+ }
22
+ export interface AccountMetadataV3 {
23
+ accountId?: string;
24
+ email?: string;
25
+ refreshToken: string;
26
+ addedAt: number;
27
+ lastUsed: number;
28
+ lastSwitchReason?: "rate-limit" | "initial" | "rotation";
29
+ rateLimitResetTimes?: RateLimitStateV3;
30
+ coolingDownUntil?: number;
31
+ cooldownReason?: CooldownReason;
32
+ }
33
+ export interface AccountStorageV3 {
34
+ version: 3;
35
+ accounts: AccountMetadataV3[];
36
+ activeIndex: number;
37
+ activeIndexByFamily?: Partial<Record<ModelFamily, number>>;
38
+ }
39
+ /**
40
+ * Returns the file path for the account storage JSON file.
41
+ * @returns Absolute path to ~/.opencode/openai-codex-accounts.json
42
+ */
43
+ export declare function getStoragePath(): string;
44
+ /**
45
+ * Removes duplicate accounts, keeping the most recently used entry for each unique key.
46
+ * Deduplication is based on accountId or refreshToken.
47
+ * @param accounts - Array of accounts to deduplicate
48
+ * @returns New array with duplicates removed
49
+ */
50
+ export declare function deduplicateAccounts<T extends {
51
+ accountId?: string;
52
+ refreshToken: string;
53
+ lastUsed?: number;
54
+ addedAt?: number;
55
+ }>(accounts: T[]): T[];
56
+ /**
57
+ * Removes duplicate accounts by email, keeping the most recently used entry.
58
+ * Accounts without email are always preserved.
59
+ * @param accounts - Array of accounts to deduplicate
60
+ * @returns New array with email duplicates removed
61
+ */
62
+ export declare function deduplicateAccountsByEmail<T extends {
63
+ email?: string;
64
+ lastUsed?: number;
65
+ addedAt?: number;
66
+ }>(accounts: T[]): T[];
67
+ /**
68
+ * Normalizes and validates account storage data, migrating from v1 to v3 if needed.
69
+ * Handles deduplication, index clamping, and per-family active index mapping.
70
+ * @param data - Raw storage data (unknown format)
71
+ * @returns Normalized AccountStorageV3 or null if invalid
72
+ */
73
+ export declare function normalizeAccountStorage(data: unknown): AccountStorageV3 | null;
74
+ /**
75
+ * Loads OAuth accounts from disk storage.
76
+ * Automatically migrates v1 storage to v3 format if needed.
77
+ * @returns AccountStorageV3 if file exists and is valid, null otherwise
78
+ */
79
+ export declare function loadAccounts(): Promise<AccountStorageV3 | null>;
80
+ /**
81
+ * Persists account storage to disk.
82
+ * Creates the .opencode directory if it doesn't exist.
83
+ * @param storage - Account storage data to save
84
+ */
85
+ export declare function saveAccounts(storage: AccountStorageV3): Promise<void>;
86
+ /**
87
+ * Deletes the account storage file from disk.
88
+ * Silently ignores if file doesn't exist.
89
+ */
90
+ export declare function clearAccounts(): Promise<void>;
91
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../lib/storage.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAetE,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,eAAe,CAAC;AAE9D,MAAM,WAAW,gBAAgB;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,YAAY,GAAG,SAAS,GAAG,UAAU,CAAC;IACzD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,YAAY,GAAG,SAAS,GAAG,UAAU,CAAC;IACzD,mBAAmB,CAAC,EAAE,gBAAgB,CAAC;IACvC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;CAC5D;AAgBD;;;GAGG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAuDD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAC7H,QAAQ,EAAE,CAAC,EAAE,GACZ,CAAC,EAAE,CAEL;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,SAAS;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAC1G,QAAQ,EAAE,CAAC,EAAE,GACZ,CAAC,EAAE,CAoDL;AAiED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAyF9E;AAED;;;;GAIG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA2BrE;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3E;AAED;;;GAGG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAYnD"}
@@ -0,0 +1,323 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { createLogger } from "./logger.js";
5
+ import { MODEL_FAMILIES } from "./prompts/codex.js";
6
+ const log = createLogger("storage");
7
+ let storageMutex = Promise.resolve();
8
+ function withStorageLock(fn) {
9
+ const previousMutex = storageMutex;
10
+ let releaseLock;
11
+ storageMutex = new Promise((resolve) => {
12
+ releaseLock = resolve;
13
+ });
14
+ return previousMutex.then(fn).finally(() => releaseLock());
15
+ }
16
+ function getConfigDir() {
17
+ return join(homedir(), ".opencode");
18
+ }
19
+ /**
20
+ * Returns the file path for the account storage JSON file.
21
+ * @returns Absolute path to ~/.opencode/openai-codex-accounts.json
22
+ */
23
+ export function getStoragePath() {
24
+ return join(getConfigDir(), "openai-codex-accounts.json");
25
+ }
26
+ function nowMs() {
27
+ return Date.now();
28
+ }
29
+ function selectNewestAccount(current, candidate) {
30
+ if (!current)
31
+ return candidate;
32
+ const currentLastUsed = current.lastUsed || 0;
33
+ const candidateLastUsed = candidate.lastUsed || 0;
34
+ if (candidateLastUsed > currentLastUsed)
35
+ return candidate;
36
+ if (candidateLastUsed < currentLastUsed)
37
+ return current;
38
+ const currentAddedAt = current.addedAt || 0;
39
+ const candidateAddedAt = candidate.addedAt || 0;
40
+ return candidateAddedAt >= currentAddedAt ? candidate : current;
41
+ }
42
+ function deduplicateAccountsByKey(accounts) {
43
+ const keyToIndex = new Map();
44
+ const indicesToKeep = new Set();
45
+ for (let i = 0; i < accounts.length; i += 1) {
46
+ const account = accounts[i];
47
+ if (!account)
48
+ continue;
49
+ const key = account.accountId || account.refreshToken;
50
+ if (!key)
51
+ continue;
52
+ const existingIndex = keyToIndex.get(key);
53
+ if (existingIndex === undefined) {
54
+ keyToIndex.set(key, i);
55
+ continue;
56
+ }
57
+ const existing = accounts[existingIndex];
58
+ const newest = selectNewestAccount(existing, account);
59
+ keyToIndex.set(key, newest === account ? i : existingIndex);
60
+ }
61
+ for (const idx of keyToIndex.values()) {
62
+ indicesToKeep.add(idx);
63
+ }
64
+ const result = [];
65
+ for (let i = 0; i < accounts.length; i += 1) {
66
+ if (indicesToKeep.has(i)) {
67
+ const account = accounts[i];
68
+ if (account)
69
+ result.push(account);
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+ /**
75
+ * Removes duplicate accounts, keeping the most recently used entry for each unique key.
76
+ * Deduplication is based on accountId or refreshToken.
77
+ * @param accounts - Array of accounts to deduplicate
78
+ * @returns New array with duplicates removed
79
+ */
80
+ export function deduplicateAccounts(accounts) {
81
+ return deduplicateAccountsByKey(accounts);
82
+ }
83
+ /**
84
+ * Removes duplicate accounts by email, keeping the most recently used entry.
85
+ * Accounts without email are always preserved.
86
+ * @param accounts - Array of accounts to deduplicate
87
+ * @returns New array with email duplicates removed
88
+ */
89
+ export function deduplicateAccountsByEmail(accounts) {
90
+ const emailToNewestIndex = new Map();
91
+ const indicesToKeep = new Set();
92
+ for (let i = 0; i < accounts.length; i += 1) {
93
+ const account = accounts[i];
94
+ if (!account)
95
+ continue;
96
+ const email = account.email?.trim();
97
+ if (!email) {
98
+ indicesToKeep.add(i);
99
+ continue;
100
+ }
101
+ const existingIndex = emailToNewestIndex.get(email);
102
+ if (existingIndex === undefined) {
103
+ emailToNewestIndex.set(email, i);
104
+ continue;
105
+ }
106
+ const existing = accounts[existingIndex];
107
+ if (!existing) {
108
+ emailToNewestIndex.set(email, i);
109
+ continue;
110
+ }
111
+ const existingLastUsed = existing.lastUsed || 0;
112
+ const candidateLastUsed = account.lastUsed || 0;
113
+ const existingAddedAt = existing.addedAt || 0;
114
+ const candidateAddedAt = account.addedAt || 0;
115
+ const isNewer = candidateLastUsed > existingLastUsed ||
116
+ (candidateLastUsed === existingLastUsed && candidateAddedAt > existingAddedAt);
117
+ if (isNewer) {
118
+ emailToNewestIndex.set(email, i);
119
+ }
120
+ }
121
+ for (const idx of emailToNewestIndex.values()) {
122
+ indicesToKeep.add(idx);
123
+ }
124
+ const result = [];
125
+ for (let i = 0; i < accounts.length; i += 1) {
126
+ if (indicesToKeep.has(i)) {
127
+ const account = accounts[i];
128
+ if (account)
129
+ result.push(account);
130
+ }
131
+ }
132
+ return result;
133
+ }
134
+ function isRecord(value) {
135
+ return !!value && typeof value === "object" && !Array.isArray(value);
136
+ }
137
+ function clampIndex(index, length) {
138
+ if (length <= 0)
139
+ return 0;
140
+ return Math.max(0, Math.min(index, length - 1));
141
+ }
142
+ function toAccountKey(account) {
143
+ return account.accountId || account.refreshToken;
144
+ }
145
+ function extractActiveKey(accounts, activeIndex) {
146
+ const candidate = accounts[activeIndex];
147
+ if (!isRecord(candidate))
148
+ return undefined;
149
+ const accountId = typeof candidate.accountId === "string" && candidate.accountId.trim()
150
+ ? candidate.accountId
151
+ : undefined;
152
+ const refreshToken = typeof candidate.refreshToken === "string" && candidate.refreshToken.trim()
153
+ ? candidate.refreshToken
154
+ : undefined;
155
+ return accountId || refreshToken;
156
+ }
157
+ function migrateV1ToV3(v1) {
158
+ const now = nowMs();
159
+ return {
160
+ version: 3,
161
+ accounts: v1.accounts.map((account) => {
162
+ const rateLimitResetTimes = {};
163
+ if (typeof account.rateLimitResetTime === "number" && account.rateLimitResetTime > now) {
164
+ for (const family of MODEL_FAMILIES) {
165
+ rateLimitResetTimes[family] = account.rateLimitResetTime;
166
+ }
167
+ }
168
+ return {
169
+ accountId: account.accountId,
170
+ email: account.email,
171
+ refreshToken: account.refreshToken,
172
+ addedAt: account.addedAt,
173
+ lastUsed: account.lastUsed,
174
+ lastSwitchReason: account.lastSwitchReason,
175
+ rateLimitResetTimes: Object.keys(rateLimitResetTimes).length > 0 ? rateLimitResetTimes : undefined,
176
+ coolingDownUntil: account.coolingDownUntil,
177
+ cooldownReason: account.cooldownReason,
178
+ };
179
+ }),
180
+ activeIndex: v1.activeIndex,
181
+ activeIndexByFamily: {
182
+ "gpt-5.2-codex": v1.activeIndex,
183
+ "codex-max": v1.activeIndex,
184
+ codex: v1.activeIndex,
185
+ "gpt-5.2": v1.activeIndex,
186
+ "gpt-5.1": v1.activeIndex,
187
+ },
188
+ };
189
+ }
190
+ /**
191
+ * Normalizes and validates account storage data, migrating from v1 to v3 if needed.
192
+ * Handles deduplication, index clamping, and per-family active index mapping.
193
+ * @param data - Raw storage data (unknown format)
194
+ * @returns Normalized AccountStorageV3 or null if invalid
195
+ */
196
+ export function normalizeAccountStorage(data) {
197
+ if (!isRecord(data)) {
198
+ log.warn("Invalid storage format, ignoring");
199
+ return null;
200
+ }
201
+ if (data.version !== 1 && data.version !== 3) {
202
+ log.warn("Unknown storage version, ignoring", {
203
+ version: data.version,
204
+ });
205
+ return null;
206
+ }
207
+ const rawAccounts = data.accounts;
208
+ if (!Array.isArray(rawAccounts)) {
209
+ log.warn("Invalid storage format, ignoring");
210
+ return null;
211
+ }
212
+ const activeIndexValue = typeof data.activeIndex === "number" && Number.isFinite(data.activeIndex)
213
+ ? data.activeIndex
214
+ : 0;
215
+ const rawActiveIndex = clampIndex(activeIndexValue, rawAccounts.length);
216
+ const activeKey = extractActiveKey(rawAccounts, rawActiveIndex);
217
+ const fromVersion = data.version;
218
+ const baseStorage = fromVersion === 1
219
+ ? migrateV1ToV3(data)
220
+ : data;
221
+ const validAccounts = rawAccounts.filter((account) => isRecord(account) && typeof account.refreshToken === "string" && !!account.refreshToken.trim());
222
+ const deduplicatedAccounts = deduplicateAccountsByEmail(deduplicateAccountsByKey(validAccounts));
223
+ const activeIndex = (() => {
224
+ if (deduplicatedAccounts.length === 0)
225
+ return 0;
226
+ if (activeKey) {
227
+ const mappedIndex = deduplicatedAccounts.findIndex((account) => toAccountKey(account) === activeKey);
228
+ if (mappedIndex >= 0)
229
+ return mappedIndex;
230
+ }
231
+ return clampIndex(rawActiveIndex, deduplicatedAccounts.length);
232
+ })();
233
+ const activeIndexByFamily = {};
234
+ const rawFamilyIndices = isRecord(baseStorage.activeIndexByFamily)
235
+ ? baseStorage.activeIndexByFamily
236
+ : {};
237
+ for (const family of MODEL_FAMILIES) {
238
+ const rawIndexValue = rawFamilyIndices[family];
239
+ const rawIndex = typeof rawIndexValue === "number" && Number.isFinite(rawIndexValue)
240
+ ? rawIndexValue
241
+ : rawActiveIndex;
242
+ const clampedRawIndex = clampIndex(rawIndex, rawAccounts.length);
243
+ const familyKey = extractActiveKey(rawAccounts, clampedRawIndex);
244
+ let mappedIndex = clampIndex(rawIndex, deduplicatedAccounts.length);
245
+ if (familyKey && deduplicatedAccounts.length > 0) {
246
+ const idx = deduplicatedAccounts.findIndex((account) => toAccountKey(account) === familyKey);
247
+ if (idx >= 0) {
248
+ mappedIndex = idx;
249
+ }
250
+ }
251
+ activeIndexByFamily[family] = mappedIndex;
252
+ }
253
+ return {
254
+ version: 3,
255
+ accounts: deduplicatedAccounts,
256
+ activeIndex,
257
+ activeIndexByFamily,
258
+ };
259
+ }
260
+ /**
261
+ * Loads OAuth accounts from disk storage.
262
+ * Automatically migrates v1 storage to v3 format if needed.
263
+ * @returns AccountStorageV3 if file exists and is valid, null otherwise
264
+ */
265
+ export async function loadAccounts() {
266
+ try {
267
+ const path = getStoragePath();
268
+ const content = await fs.readFile(path, "utf-8");
269
+ const data = JSON.parse(content);
270
+ const normalized = normalizeAccountStorage(data);
271
+ const storedVersion = isRecord(data) ? data.version : undefined;
272
+ if (normalized && storedVersion !== normalized.version) {
273
+ log.info("Migrating account storage to v3", { from: storedVersion, to: normalized.version });
274
+ try {
275
+ await saveAccounts(normalized);
276
+ }
277
+ catch (saveError) {
278
+ log.warn("Failed to persist migrated storage", { error: String(saveError) });
279
+ }
280
+ }
281
+ return normalized;
282
+ }
283
+ catch (error) {
284
+ const code = error.code;
285
+ if (code === "ENOENT") {
286
+ return null;
287
+ }
288
+ log.error("Failed to load account storage", { error: String(error) });
289
+ return null;
290
+ }
291
+ }
292
+ /**
293
+ * Persists account storage to disk.
294
+ * Creates the .opencode directory if it doesn't exist.
295
+ * @param storage - Account storage data to save
296
+ */
297
+ export async function saveAccounts(storage) {
298
+ return withStorageLock(async () => {
299
+ const path = getStoragePath();
300
+ await fs.mkdir(dirname(path), { recursive: true });
301
+ const content = JSON.stringify(storage, null, 2);
302
+ await fs.writeFile(path, content, "utf-8");
303
+ });
304
+ }
305
+ /**
306
+ * Deletes the account storage file from disk.
307
+ * Silently ignores if file doesn't exist.
308
+ */
309
+ export async function clearAccounts() {
310
+ return withStorageLock(async () => {
311
+ try {
312
+ const path = getStoragePath();
313
+ await fs.unlink(path);
314
+ }
315
+ catch (error) {
316
+ const code = error.code;
317
+ if (code !== "ENOENT") {
318
+ log.error("Failed to clear account storage", { error: String(error) });
319
+ }
320
+ }
321
+ });
322
+ }
323
+ //# sourceMappingURL=storage.js.map