@pikoloo/codex-proxy 1.0.7 → 1.2.2

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 (42) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +28 -11
  3. package/bin/cli.js +15 -15
  4. package/docs/ACCOUNT.md +104 -0
  5. package/docs/API.md +21 -29
  6. package/docs/ARCHITECTURE.md +9 -9
  7. package/docs/CLAUDE_INTEGRATION.md +3 -3
  8. package/docs/OAUTH.md +13 -13
  9. package/docs/OPENCLAW.md +1 -1
  10. package/docs/legal.md +6 -0
  11. package/images/dashboard-screenshot.png +0 -0
  12. package/images/readme-cover.png +0 -0
  13. package/images/settings-screenshot.png +0 -0
  14. package/package.json +19 -10
  15. package/public/css/style.css +802 -22
  16. package/public/index.html +236 -338
  17. package/public/js/app.js +140 -118
  18. package/src/account-manager.js +210 -292
  19. package/src/cli/account.js +236 -0
  20. package/src/direct-api.js +7 -9
  21. package/src/index.js +7 -7
  22. package/src/middleware/credentials.js +6 -47
  23. package/src/oauth.js +2 -1
  24. package/src/routes/{accounts-route.js → account-route.js} +25 -109
  25. package/src/routes/api-routes.js +18 -30
  26. package/src/routes/chat-route.js +3 -3
  27. package/src/routes/messages-route.js +37 -199
  28. package/src/routes/models-route.js +11 -21
  29. package/src/routes/settings-route.js +1 -41
  30. package/src/security.js +1 -1
  31. package/src/server-settings.js +30 -38
  32. package/src/utils/logger.js +14 -1
  33. package/docs/ACCOUNTS.md +0 -202
  34. package/images/demo-screenshot.png +0 -0
  35. package/images/f757093f-507b-4453-994e-f8275f8b07a9.png +0 -0
  36. package/src/account-rotation/index.js +0 -93
  37. package/src/account-rotation/rate-limits.js +0 -293
  38. package/src/account-rotation/strategies/base-strategy.js +0 -48
  39. package/src/account-rotation/strategies/index.js +0 -31
  40. package/src/account-rotation/strategies/round-robin-strategy.js +0 -42
  41. package/src/account-rotation/strategies/sticky-strategy.js +0 -97
  42. package/src/cli/accounts.js +0 -557
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createInterface } from 'readline/promises';
4
+ import { stdin, stdout } from 'process';
5
+ import { spawn } from 'child_process';
6
+ import net from 'net';
7
+
8
+ import {
9
+ OAUTH_CONFIG,
10
+ generatePKCE,
11
+ generateState,
12
+ getAuthorizationUrl,
13
+ extractCodeFromInput,
14
+ exchangeCodeForTokens,
15
+ extractAccountInfo
16
+ } from '../oauth.js';
17
+ import {
18
+ ACCOUNT_FILE,
19
+ getActiveAccount,
20
+ listAccounts,
21
+ refreshActiveAccount,
22
+ removeAccount,
23
+ setConfiguredAccount
24
+ } from '../account-manager.js';
25
+
26
+ const DEFAULT_PORT = 8081;
27
+
28
+ function createRL() {
29
+ return createInterface({ input: stdin, output: stdout });
30
+ }
31
+
32
+ function openBrowser(url) {
33
+ const platform = process.platform;
34
+ let command;
35
+ let args;
36
+
37
+ if (platform === 'darwin') {
38
+ command = 'open';
39
+ args = [url];
40
+ } else if (platform === 'win32') {
41
+ command = 'cmd';
42
+ args = ['/c', 'start', '', url.replace(/&/g, '^&')];
43
+ } else {
44
+ command = 'xdg-open';
45
+ args = [url];
46
+ }
47
+
48
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
49
+ child.on('error', () => {
50
+ console.log('\nCould not open browser automatically.');
51
+ console.log('Please open this URL manually:', url);
52
+ });
53
+ child.unref();
54
+ }
55
+
56
+ function isServerRunning(port) {
57
+ return new Promise((resolve) => {
58
+ const socket = new net.Socket();
59
+ socket.setTimeout(1000);
60
+ socket.on('connect', () => {
61
+ socket.destroy();
62
+ resolve(true);
63
+ });
64
+ socket.on('timeout', () => {
65
+ socket.destroy();
66
+ resolve(false);
67
+ });
68
+ socket.on('error', () => {
69
+ socket.destroy();
70
+ resolve(false);
71
+ });
72
+ socket.connect(port, 'localhost');
73
+ });
74
+ }
75
+
76
+ async function ensureServerStopped(port) {
77
+ if (await isServerRunning(port)) {
78
+ console.error(`
79
+ Error: Proxy server is currently running on port ${port}.
80
+
81
+ Please stop the server before changing the configured account from the CLI.
82
+ Use the dashboard while the server is running.
83
+ `);
84
+ process.exit(1);
85
+ }
86
+ }
87
+
88
+ function buildAccount(tokens) {
89
+ const accountInfo = extractAccountInfo(tokens.accessToken);
90
+ return {
91
+ email: accountInfo?.email || 'unknown',
92
+ accountId: accountInfo?.accountId,
93
+ planType: accountInfo?.planType || 'free',
94
+ accessToken: tokens.accessToken,
95
+ refreshToken: tokens.refreshToken,
96
+ idToken: tokens.idToken,
97
+ expiresAt: accountInfo?.expiresAt || (Date.now() + tokens.expiresIn * 1000),
98
+ addedAt: new Date().toISOString(),
99
+ lastUsed: null
100
+ };
101
+ }
102
+
103
+ async function addAccount(rl, { noBrowser }) {
104
+ console.log(noBrowser ? '\n=== Configure Account (No-Browser Mode) ===\n' : '\n=== Configure Account ===\n');
105
+
106
+ const { verifier } = generatePKCE();
107
+ const state = generateState();
108
+ const url = getAuthorizationUrl(verifier, state, OAUTH_CONFIG.callbackPort);
109
+
110
+ if (noBrowser) {
111
+ console.log('Copy this URL and open it in a browser on another device:\n');
112
+ } else {
113
+ console.log('Opening browser for ChatGPT sign-in...');
114
+ console.log('(If the browser does not open, copy this URL manually)\n');
115
+ openBrowser(url);
116
+ }
117
+
118
+ console.log(` ${url}\n`);
119
+ console.log('After signing in, paste the full callback URL or authorization code.\n');
120
+
121
+ const input = await rl.question('Callback URL or authorization code: ');
122
+
123
+ try {
124
+ const { code, state: extractedState, port } = extractCodeFromInput(input);
125
+ if (extractedState && extractedState !== state) {
126
+ throw new Error('OAuth state mismatch. Refusing to exchange the authorization code.');
127
+ }
128
+
129
+ console.log('\nExchanging authorization code for tokens...');
130
+ const tokens = await exchangeCodeForTokens(code, verifier, port || OAUTH_CONFIG.callbackPort);
131
+ const account = buildAccount(tokens);
132
+ setConfiguredAccount(account);
133
+ console.log(`\nConfigured account: ${account.email}`);
134
+ } catch (error) {
135
+ console.error(`\nAuthentication failed: ${error.message}`);
136
+ process.exitCode = 1;
137
+ }
138
+ }
139
+
140
+ function showAccount() {
141
+ const { account } = listAccounts();
142
+ if (!account) {
143
+ console.log('\nNo account configured.');
144
+ console.log(`Config file: ${ACCOUNT_FILE}`);
145
+ return;
146
+ }
147
+
148
+ console.log('\nConfigured account:');
149
+ console.log(` Email: ${account.email}`);
150
+ console.log(` Plan: ${account.planType}`);
151
+ console.log(` Token: ${account.tokenExpired ? 'expired' : 'valid'}`);
152
+ console.log(` Config file: ${ACCOUNT_FILE}`);
153
+ }
154
+
155
+ async function verifyAccount() {
156
+ const account = getActiveAccount();
157
+ if (!account) {
158
+ console.log('No account configured.');
159
+ return;
160
+ }
161
+
162
+ const result = await refreshActiveAccount();
163
+ console.log(result.success ? `OK: ${account.email}` : `Failed: ${result.message}`);
164
+ if (!result.success) process.exitCode = 1;
165
+ }
166
+
167
+ async function removeConfiguredAccount(rl) {
168
+ const account = getActiveAccount();
169
+ if (!account) {
170
+ console.log('No account configured.');
171
+ return;
172
+ }
173
+
174
+ const confirm = await rl.question(`Remove configured account ${account.email}? [y/N]: `);
175
+ if (confirm.toLowerCase() !== 'y') {
176
+ console.log('Cancelled.');
177
+ return;
178
+ }
179
+
180
+ const result = removeAccount();
181
+ console.log(result.message);
182
+ }
183
+
184
+ function showHelp() {
185
+ console.log(`
186
+ Usage:
187
+ codex-proxy account add Configure account (opens browser)
188
+ codex-proxy account add --no-browser Configure account manually
189
+ codex-proxy account show Show configured account
190
+ codex-proxy account verify Refresh and verify configured account
191
+ codex-proxy account remove Remove configured account
192
+ codex-proxy account clear Remove configured account
193
+ codex-proxy account help Show this help
194
+
195
+ Adding or importing an account replaces the existing local account.
196
+ `);
197
+ }
198
+
199
+ async function main() {
200
+ const args = process.argv.slice(2);
201
+ const command = args[0] || 'help';
202
+ const noBrowser = args.includes('--no-browser');
203
+ const port = parseInt(args.find(a => a.startsWith('--port='))?.split('=')[1]) || DEFAULT_PORT;
204
+ const rl = createRL();
205
+
206
+ try {
207
+ switch (command) {
208
+ case 'add':
209
+ await ensureServerStopped(port);
210
+ await addAccount(rl, { noBrowser });
211
+ break;
212
+ case 'show':
213
+ showAccount();
214
+ break;
215
+ case 'verify':
216
+ await verifyAccount();
217
+ break;
218
+ case 'remove':
219
+ case 'clear':
220
+ await ensureServerStopped(port);
221
+ await removeConfiguredAccount(rl);
222
+ break;
223
+ case 'help':
224
+ default:
225
+ showHelp();
226
+ break;
227
+ }
228
+ } finally {
229
+ rl.close();
230
+ }
231
+ }
232
+
233
+ main().catch((error) => {
234
+ console.error('Error:', error.message);
235
+ process.exit(1);
236
+ });
package/src/direct-api.js CHANGED
@@ -41,8 +41,7 @@ function parseResetTime(response, errorText) {
41
41
  /**
42
42
  * Send a streaming request to ChatGPT API
43
43
  */
44
- export async function* sendMessageStream(anthropicRequest, accessToken, accountId, accountRotator = null, currentEmail = null) {
45
- const modelId = anthropicRequest.model;
44
+ export async function* sendMessageStream(anthropicRequest, accessToken, accountId) {
46
45
  const request = convertAnthropicToResponsesAPI(anthropicRequest);
47
46
 
48
47
  const response = await fetch(API_URL, {
@@ -60,17 +59,11 @@ export async function* sendMessageStream(anthropicRequest, accessToken, accountI
60
59
  const errorText = await response.text();
61
60
 
62
61
  if (response.status === 401) {
63
- if (accountRotator && currentEmail) {
64
- accountRotator.markInvalid(currentEmail, 'Token expired or revoked');
65
- }
66
62
  throw new Error('AUTH_EXPIRED: Token expired or revoked. Please re-authenticate.');
67
63
  }
68
64
 
69
65
  if (response.status === 429) {
70
66
  const resetMs = parseResetTime(response, errorText);
71
- if (accountRotator && currentEmail) {
72
- accountRotator.markRateLimited(currentEmail, resetMs, modelId);
73
- }
74
67
  throw new Error(`RATE_LIMITED:${resetMs}:${errorText}`);
75
68
  }
76
69
 
@@ -117,6 +110,11 @@ export async function sendMessage(anthropicRequest, accessToken, accountId) {
117
110
  if (response.status === 401) {
118
111
  throw new Error('AUTH_EXPIRED: Token expired or revoked. Please re-authenticate.');
119
112
  }
113
+
114
+ if (response.status === 429) {
115
+ const resetMs = parseResetTime(response, errorText);
116
+ throw new Error(`RATE_LIMITED:${resetMs}:${errorText}`);
117
+ }
120
118
 
121
119
  throw new Error(`API_ERROR: ${response.status} - ${errorText}`);
122
120
  }
@@ -161,4 +159,4 @@ export default {
161
159
  sendMessageStream,
162
160
  sendMessage,
163
161
  parseResetTime
164
- };
162
+ };
package/src/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { startServer } from './server.js';
7
7
  import { logger } from './utils/logger.js';
8
- import { getStatus, ACCOUNTS_FILE } from './account-manager.js';
8
+ import { getStatus, ACCOUNT_FILE } from './account-manager.js';
9
9
 
10
10
  const PORT = Number(process.env.PORT || 8081);
11
11
  const HOST = process.env.HOST || '127.0.0.1';
@@ -14,19 +14,19 @@ startServer({ port: PORT, host: HOST });
14
14
 
15
15
  console.log(`
16
16
  ╔══════════════════════════════════════════════════════════════╗
17
- ║ Codex Claude Proxy v1.0.7
17
+ ║ Codex Claude Proxy v1.2.2
18
18
  ║ (Direct API Mode) ║
19
19
  ╠══════════════════════════════════════════════════════════════╣
20
20
  ║ Server: http://${HOST}:${PORT} ║
21
21
  ║ WebUI: http://${HOST}:${PORT} ║
22
22
  ║ Health: http://${HOST}:${PORT}/health ║
23
- Accounts: http://${HOST}:${PORT}/accounts
23
+ Account: http://${HOST}:${PORT}/account
24
24
  ║ Logs: http://${HOST}:${PORT}/api/logs/stream ║
25
25
  ╠══════════════════════════════════════════════════════════════╣
26
26
  ║ Features: ║
27
27
  ║ ✓ Native tool calling support ║
28
28
  ║ ✓ Real-time streaming ║
29
- ║ ✓ Multi-account management
29
+ ║ ✓ Single-account local mode
30
30
  ║ ✓ OpenAI & Anthropic API compatibility ║
31
31
  ╠══════════════════════════════════════════════════════════════╣
32
32
  ║ Support: ║
@@ -36,11 +36,11 @@ console.log(`
36
36
  `);
37
37
 
38
38
  const status = getStatus();
39
- logger.info(`Accounts: ${status.total} total, Active: ${status.active || 'None'}`);
39
+ logger.info(`Account configured: ${status.active || 'None'}`);
40
40
 
41
41
  if (status.total === 0) {
42
- logger.warn(`No accounts configured. Open http://${HOST}:${PORT} to add one.`);
42
+ logger.warn(`No account configured. Open http://${HOST}:${PORT} to add one.`);
43
43
  }
44
44
 
45
45
  // Expose config path in logs for convenience
46
- logger.info(`Accounts config: ${ACCOUNTS_FILE}`);
46
+ logger.info(`Account config: ${ACCOUNT_FILE}`);
@@ -1,19 +1,18 @@
1
1
  /**
2
2
  * Credentials Middleware
3
- * Resolves and validates the active account credentials,
3
+ * Resolves and validates the configured account credentials,
4
4
  * auto-refreshing tokens when they are expired or expiring soon.
5
5
  */
6
6
 
7
7
  import {
8
8
  getActiveAccount,
9
9
  refreshAccountToken,
10
- isTokenExpiredOrExpiringSoon,
11
- loadAccounts
10
+ isTokenExpiredOrExpiringSoon
12
11
  } from '../account-manager.js';
13
12
  import { logger } from '../utils/logger.js';
14
13
 
15
14
  /**
16
- * Resolves the active account credentials, refreshing the token if needed.
15
+ * Resolves the configured account credentials, refreshing the token if needed.
17
16
  * Returns null if no valid account is available.
18
17
  *
19
18
  * @returns {Promise<{accessToken: string, accountId: string, email: string}|null>}
@@ -22,7 +21,7 @@ export async function getCredentialsOrError() {
22
21
  const account = getActiveAccount();
23
22
 
24
23
  if (!account) {
25
- logger.info('No active account found');
24
+ logger.info('No configured account found');
26
25
  return null;
27
26
  }
28
27
 
@@ -61,56 +60,16 @@ export async function getCredentialsOrError() {
61
60
  };
62
61
  }
63
62
 
64
- /**
65
- * Get credentials for a specific account by email.
66
- * @param {string} email
67
- * @returns {Promise<{accessToken: string, accountId: string, email: string}|null>}
68
- */
69
- export async function getCredentialsForAccount(email) {
70
- const data = loadAccounts();
71
- const account = data.accounts.find(a => a.email === email);
72
-
73
- if (!account) {
74
- return null;
75
- }
76
-
77
- if (!account.accessToken || !account.accountId) {
78
- return null;
79
- }
80
-
81
- if (isTokenExpiredOrExpiringSoon(account)) {
82
- const result = await refreshAccountToken(account.email);
83
- if (!result.success) {
84
- return null;
85
- }
86
- const refreshedData = loadAccounts();
87
- const refreshedAccount = refreshedData.accounts.find(a => a.email === email);
88
- if (!refreshedAccount) return null;
89
-
90
- return {
91
- accessToken: refreshedAccount.accessToken,
92
- accountId: refreshedAccount.accountId,
93
- email: refreshedAccount.email
94
- };
95
- }
96
-
97
- return {
98
- accessToken: account.accessToken,
99
- accountId: account.accountId,
100
- email: account.email
101
- };
102
- }
103
-
104
63
  /**
105
64
  * Sends a 401 authentication error response.
106
65
  * @param {import('express').Response} res
107
66
  * @param {string} [message]
108
67
  */
109
- export function sendAuthError(res, message = 'No active account with valid credentials. Add an account via /accounts/add') {
68
+ export function sendAuthError(res, message = 'No configured account with valid credentials. Add an account via /account/add') {
110
69
  return res.status(401).json({
111
70
  type: 'error',
112
71
  error: { type: 'authentication_error', message }
113
72
  });
114
73
  }
115
74
 
116
- export default { getCredentialsOrError, getCredentialsForAccount, sendAuthError };
75
+ export default { getCredentialsOrError, sendAuthError };
package/src/oauth.js CHANGED
@@ -115,7 +115,7 @@ function getAuthorizationUrl(verifier, state, port) {
115
115
  id_token_add_organizations: 'true',
116
116
  codex_cli_simplified_flow: 'true',
117
117
  originator: 'codex_cli_rs',
118
- prompt: 'login', // Force login screen for multi-account support
118
+ prompt: 'login',
119
119
  max_age: '0' // Force re-authentication
120
120
  });
121
121
 
@@ -624,6 +624,7 @@ export function extractCodeFromInput(input) {
624
624
  throw new Error('No authorization code found in URL');
625
625
  }
626
626
 
627
+ const port = Number(url.port);
627
628
  return { code, state, port: Number.isInteger(port) && port > 0 ? port : null };
628
629
  } catch (e) {
629
630
  if (e.message.includes('OAuth error') || e.message.includes('No authorization code')) {
@@ -1,36 +1,19 @@
1
1
  /**
2
- * Accounts Route
3
- * Handles all /accounts/* endpoints:
4
- * GET /accounts
5
- * GET /accounts/status
6
- * GET /accounts/quota
7
- * GET /accounts/quota/all
8
- * POST /accounts/add
9
- * POST /accounts/add/manual
10
- * POST /accounts/switch
11
- * POST /accounts/import
12
- * POST /accounts/refresh
13
- * POST /accounts/refresh/all
14
- * POST /accounts/:email/refresh
15
- * POST /accounts/oauth/cleanup
16
- * DELETE /accounts/:email
2
+ * Account Route
3
+ * Handles single-account management endpoints.
17
4
  */
18
5
 
19
6
  import {
20
7
  getActiveAccount,
21
- setActiveAccount,
22
8
  removeAccount,
23
9
  listAccounts,
24
10
  refreshActiveAccount,
25
- refreshAccountToken,
26
- refreshAllAccounts,
27
11
  importFromCodex,
28
- getStatus,
12
+ updateAccountQuota,
13
+ getAccountQuota,
29
14
  loadAccounts,
30
15
  saveAccounts,
31
- updateAccountAuth,
32
- updateAccountQuota,
33
- getAccountQuota
16
+ updateAccountAuth
34
17
  } from '../account-manager.js';
35
18
 
36
19
  import {
@@ -51,17 +34,14 @@ import {
51
34
 
52
35
  import { logger } from '../utils/logger.js';
53
36
 
54
- // Tracks active OAuth callback servers keyed by port
55
37
  const activeCallbackServers = new Map();
56
38
 
57
- // ─── Route Handlers ──────────────────────────────────────────────────────────
58
-
59
- export function handleListAccounts(req, res) {
39
+ export function handleGetAccount(req, res) {
60
40
  res.json(listAccounts());
61
41
  }
62
42
 
63
43
  export function handleAccountStatus(req, res) {
64
- res.json(getStatus());
44
+ res.json(listAccounts());
65
45
  }
66
46
 
67
47
  export function handleOAuthCleanup(req, res) {
@@ -69,17 +49,15 @@ export function handleOAuthCleanup(req, res) {
69
49
  try { callback.abort(); } catch { /* ignore */ }
70
50
  }
71
51
  activeCallbackServers.clear();
72
- res.json({ success: true, message: 'OAuth servers cleaned up' });
52
+ res.json({ success: true, message: 'OAuth server cleaned up' });
73
53
  }
74
54
 
75
55
  export async function handleAddAccount(req, res) {
76
56
  const { port } = req.body || {};
77
57
  const callbackPort = port || OAUTH_CONFIG.callbackPort;
78
-
79
58
  const { verifier } = generatePKCE();
80
59
  const state = generateState();
81
60
 
82
- // Close any existing server on this port
83
61
  if (activeCallbackServers.has(callbackPort)) {
84
62
  const existing = activeCallbackServers.get(callbackPort);
85
63
  if (existing.abort) existing.abort();
@@ -100,7 +78,6 @@ export async function handleAddAccount(req, res) {
100
78
  }
101
79
 
102
80
  const oauthUrl = getAuthorizationUrl(verifier, state, actualPort);
103
-
104
81
  activeCallbackServers.set(actualPort, serverResult);
105
82
 
106
83
  serverResult.promise
@@ -110,8 +87,8 @@ export async function handleAddAccount(req, res) {
110
87
  return exchangeCodeForTokens(result.code, verifier, actualPort)
111
88
  .then(async tokens => {
112
89
  const accountInfo = _buildAccountInfo(tokens);
113
- await _upsertAccount(accountInfo);
114
- logger.info(`Added account: ${accountInfo.email}`);
90
+ await _replaceAccount(accountInfo);
91
+ logger.info(`Configured account: ${accountInfo.email}`);
115
92
  });
116
93
  }
117
94
  })
@@ -150,54 +127,30 @@ export async function handleAddAccountManual(req, res) {
150
127
  const tokens = await exchangeCodeForTokens(extractedCode, codeVerifier, callbackPort);
151
128
  const accountInfo = _buildAccountInfo(tokens);
152
129
 
153
- await _upsertAccount(accountInfo);
130
+ await _replaceAccount(accountInfo);
154
131
  const callback = activeCallbackServers.get(callbackPort);
155
132
  if (callback?.abort) callback.abort();
156
133
  activeCallbackServers.delete(callbackPort);
157
- logger.info(`Added account via manual OAuth: ${accountInfo.email}`);
158
- res.json({ success: true, message: `Account ${accountInfo.email} added successfully` });
134
+ logger.info(`Configured account via manual OAuth: ${accountInfo.email}`);
135
+ res.json({ success: true, message: `Account ${accountInfo.email} configured successfully` });
159
136
  } catch (err) {
160
137
  logger.error(`Manual OAuth failed: ${err.message}`);
161
138
  res.status(400).json({ success: false, error: err.message });
162
139
  }
163
140
  }
164
141
 
165
- export function handleSwitchAccount(req, res) {
166
- const { email } = req.body || {};
167
- if (!email) {
168
- return res.status(400).json({ success: false, message: 'Email is required' });
169
- }
170
- const result = setActiveAccount(email);
171
- if (result.success) {
172
- logger.info(`Switched to account: ${email}`);
173
- }
174
- res.json(result);
175
- }
176
-
177
142
  export async function handleRefreshAccount(req, res) {
178
- const email = decodeURIComponent(req.params.email);
179
- const result = await refreshAccountToken(email);
143
+ const result = await refreshActiveAccount();
180
144
  if (result.success) {
181
- logger.info(`Refreshed token for: ${email}`);
145
+ logger.info(result.message);
182
146
  }
183
147
  res.json(result);
184
148
  }
185
149
 
186
- export async function handleRefreshAllAccounts(req, res) {
187
- const result = await refreshAllAccounts();
188
- res.json(result);
189
- }
190
-
191
- export async function handleRefreshActiveAccount(req, res) {
192
- const result = await refreshActiveAccount();
193
- res.json(result);
194
- }
195
-
196
150
  export function handleRemoveAccount(req, res) {
197
- const email = decodeURIComponent(req.params.email);
198
- const result = removeAccount(email);
151
+ const result = removeAccount();
199
152
  if (result.success) {
200
- logger.info(`Removed account: ${email}`);
153
+ logger.info(result.message);
201
154
  }
202
155
  res.json(result);
203
156
  }
@@ -208,15 +161,13 @@ export function handleImportAccount(req, res) {
208
161
  }
209
162
 
210
163
  export async function handleGetQuota(req, res) {
211
- const { email, refresh } = req.query;
212
- const account = email
213
- ? loadAccounts().accounts.find(a => a.email === email)
214
- : getActiveAccount();
164
+ const { refresh } = req.query;
165
+ const account = getActiveAccount();
215
166
 
216
167
  if (!account) {
217
168
  return res.status(404).json({
218
169
  success: false,
219
- error: email ? `Account not found: ${email}` : 'No active account'
170
+ error: 'No account configured'
220
171
  });
221
172
  }
222
173
 
@@ -248,48 +199,17 @@ export async function handleGetQuota(req, res) {
248
199
  }
249
200
  }
250
201
 
251
- export async function handleGetAllQuotas(req, res) {
252
- const { accounts: accountList } = listAccounts();
253
- const results = [];
254
-
255
- for (const account of accountList) {
256
- try {
257
- const quota = await getAccountQuota(account.email);
258
- results.push({ email: account.email, quota: quota || null });
259
- } catch {
260
- results.push({ email: account.email, quota: null });
261
- }
262
- }
263
-
264
- res.json({ accounts: results });
265
- }
266
-
267
- // ─── Helpers ─────────────────────────────────────────────────────────────────
268
-
269
- /**
270
- * Inserts or updates an account in the persisted accounts store,
271
- * and sets it as the active account.
272
- * @param {object} accountInfo
273
- */
274
- async function _upsertAccount(accountInfo) {
202
+ async function _replaceAccount(accountInfo) {
275
203
  if (!accountInfo?.email) {
276
204
  throw new Error('OAuth response did not include account email');
277
205
  }
278
206
 
279
207
  const data = loadAccounts();
280
- const existingIndex = data.accounts.findIndex(a => a.email === accountInfo.email);
281
-
282
- if (existingIndex >= 0) {
283
- data.accounts[existingIndex] = { ...data.accounts[existingIndex], ...accountInfo };
284
- } else {
285
- data.accounts.push(accountInfo);
286
- }
287
-
208
+ data.accounts = [accountInfo];
288
209
  data.activeAccount = accountInfo.email;
289
210
  saveAccounts(data);
290
211
  updateAccountAuth(accountInfo);
291
-
292
- // Fetch initial quota immediately
212
+
293
213
  try {
294
214
  const quotaData = await fetchAccountQuota(accountInfo.accessToken, accountInfo.accountId);
295
215
  updateAccountQuota(accountInfo.email, quotaData);
@@ -316,17 +236,13 @@ function _buildAccountInfo(tokens) {
316
236
  }
317
237
 
318
238
  export default {
319
- handleListAccounts,
239
+ handleGetAccount,
320
240
  handleAccountStatus,
321
241
  handleOAuthCleanup,
322
242
  handleAddAccount,
323
243
  handleAddAccountManual,
324
- handleSwitchAccount,
325
244
  handleRefreshAccount,
326
- handleRefreshAllAccounts,
327
- handleRefreshActiveAccount,
328
245
  handleRemoveAccount,
329
246
  handleImportAccount,
330
- handleGetQuota,
331
- handleGetAllQuotas
247
+ handleGetQuota
332
248
  };