@pikoloo/codex-proxy 1.1.0 → 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.
- package/CHANGELOG.md +76 -0
- package/README.md +28 -11
- package/bin/cli.js +15 -15
- package/docs/ACCOUNT.md +104 -0
- package/docs/API.md +26 -19
- package/docs/ARCHITECTURE.md +9 -9
- package/docs/CLAUDE_INTEGRATION.md +3 -3
- package/docs/OAUTH.md +13 -13
- package/docs/OPENCLAW.md +1 -1
- package/docs/legal.md +6 -0
- package/package.json +10 -8
- package/public/css/style.css +4 -34
- package/public/index.html +105 -166
- package/public/js/app.js +23 -58
- package/src/account-manager.js +210 -292
- package/src/cli/account.js +236 -0
- package/src/direct-api.js +7 -9
- package/src/index.js +7 -7
- package/src/middleware/credentials.js +6 -47
- package/src/oauth.js +2 -1
- package/src/routes/{accounts-route.js → account-route.js} +25 -109
- package/src/routes/api-routes.js +18 -26
- package/src/routes/chat-route.js +2 -2
- package/src/routes/messages-route.js +29 -189
- package/src/routes/models-route.js +11 -21
- package/src/security.js +1 -1
- package/src/server-settings.js +1 -8
- package/docs/ACCOUNTS.md +0 -202
- package/src/account-rotation/index.js +0 -130
- package/src/account-rotation/rate-limits.js +0 -293
- 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
|
|
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,
|
|
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.
|
|
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
|
-
║
|
|
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
|
-
║ ✓
|
|
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(`
|
|
39
|
+
logger.info(`Account configured: ${status.active || 'None'}`);
|
|
40
40
|
|
|
41
41
|
if (status.total === 0) {
|
|
42
|
-
logger.warn(`No
|
|
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(`
|
|
46
|
+
logger.info(`Account config: ${ACCOUNT_FILE}`);
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Credentials Middleware
|
|
3
|
-
* Resolves and validates the
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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',
|
|
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
|
-
*
|
|
3
|
-
* Handles
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
114
|
-
logger.info(`
|
|
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
|
|
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(`
|
|
158
|
-
res.json({ success: true, message: `Account ${accountInfo.email}
|
|
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
|
|
179
|
-
const result = await refreshAccountToken(email);
|
|
143
|
+
const result = await refreshActiveAccount();
|
|
180
144
|
if (result.success) {
|
|
181
|
-
logger.info(
|
|
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
|
|
198
|
-
const result = removeAccount(email);
|
|
151
|
+
const result = removeAccount();
|
|
199
152
|
if (result.success) {
|
|
200
|
-
logger.info(
|
|
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 {
|
|
212
|
-
const account =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|