hedgequantx 2.6.163 → 2.7.1
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/README.md +15 -88
- package/bin/cli.js +0 -11
- package/dist/lib/api.jsc +0 -0
- package/dist/lib/api2.jsc +0 -0
- package/dist/lib/core.jsc +0 -0
- package/dist/lib/core2.jsc +0 -0
- package/dist/lib/data.js +1 -1
- package/dist/lib/data.jsc +0 -0
- package/dist/lib/data2.jsc +0 -0
- package/dist/lib/decoder.jsc +0 -0
- package/dist/lib/m/mod1.jsc +0 -0
- package/dist/lib/m/mod2.jsc +0 -0
- package/dist/lib/n/r1.jsc +0 -0
- package/dist/lib/n/r2.jsc +0 -0
- package/dist/lib/n/r3.jsc +0 -0
- package/dist/lib/n/r4.jsc +0 -0
- package/dist/lib/n/r5.jsc +0 -0
- package/dist/lib/n/r6.jsc +0 -0
- package/dist/lib/n/r7.jsc +0 -0
- package/dist/lib/o/util1.jsc +0 -0
- package/dist/lib/o/util2.jsc +0 -0
- package/package.json +8 -5
- package/src/app.js +40 -162
- package/src/config/constants.js +31 -33
- package/src/config/propfirms.js +13 -217
- package/src/config/settings.js +0 -43
- package/src/lib/api.js +198 -0
- package/src/lib/api2.js +353 -0
- package/src/lib/core.js +539 -0
- package/src/lib/core2.js +341 -0
- package/src/lib/data.js +555 -0
- package/src/lib/data2.js +492 -0
- package/src/lib/decoder.js +599 -0
- package/src/lib/m/s1.js +804 -0
- package/src/lib/m/s2.js +34 -0
- package/src/lib/n/r1.js +454 -0
- package/src/lib/n/r2.js +514 -0
- package/src/lib/n/r3.js +631 -0
- package/src/lib/n/r4.js +401 -0
- package/src/lib/n/r5.js +335 -0
- package/src/lib/n/r6.js +425 -0
- package/src/lib/n/r7.js +530 -0
- package/src/lib/o/l1.js +44 -0
- package/src/lib/o/l2.js +427 -0
- package/src/lib/python-bridge.js +206 -0
- package/src/menus/connect.js +14 -176
- package/src/menus/dashboard.js +65 -110
- package/src/pages/accounts.js +18 -18
- package/src/pages/algo/copy-trading.js +210 -240
- package/src/pages/algo/index.js +41 -104
- package/src/pages/algo/one-account.js +386 -33
- package/src/pages/algo/ui.js +312 -151
- package/src/pages/orders.js +3 -3
- package/src/pages/positions.js +3 -3
- package/src/pages/stats/chart.js +74 -0
- package/src/pages/stats/display.js +228 -0
- package/src/pages/stats/index.js +236 -0
- package/src/pages/stats/metrics.js +213 -0
- package/src/pages/user.js +6 -6
- package/src/services/hqx-server/constants.js +55 -0
- package/src/services/hqx-server/index.js +401 -0
- package/src/services/hqx-server/latency.js +81 -0
- package/src/services/index.js +12 -3
- package/src/services/rithmic/accounts.js +7 -32
- package/src/services/rithmic/connection.js +1 -204
- package/src/services/rithmic/contracts.js +116 -99
- package/src/services/rithmic/handlers.js +21 -196
- package/src/services/rithmic/index.js +63 -120
- package/src/services/rithmic/market.js +31 -0
- package/src/services/rithmic/orders.js +5 -111
- package/src/services/rithmic/protobuf.js +384 -138
- package/src/services/session.js +22 -173
- package/src/ui/box.js +10 -18
- package/src/ui/index.js +1 -3
- package/src/ui/menu.js +1 -1
- package/src/utils/prompts.js +2 -2
- package/dist/lib/m/s1.js +0 -1
- package/src/menus/ai-agent-connect.js +0 -181
- package/src/menus/ai-agent-models.js +0 -219
- package/src/menus/ai-agent-oauth.js +0 -292
- package/src/menus/ai-agent-ui.js +0 -141
- package/src/menus/ai-agent.js +0 -484
- package/src/pages/algo/algo-config.js +0 -195
- package/src/pages/algo/algo-multi.js +0 -801
- package/src/pages/algo/algo-utils.js +0 -58
- package/src/pages/algo/copy-engine.js +0 -449
- package/src/pages/algo/custom-strategy.js +0 -459
- package/src/pages/algo/logger.js +0 -245
- package/src/pages/algo/smart-logs-data.js +0 -218
- package/src/pages/algo/smart-logs.js +0 -387
- package/src/pages/algo/ui-constants.js +0 -144
- package/src/pages/algo/ui-summary.js +0 -184
- package/src/pages/stats-calculations.js +0 -191
- package/src/pages/stats-ui.js +0 -381
- package/src/pages/stats.js +0 -339
- package/src/services/ai/client-analysis.js +0 -194
- package/src/services/ai/client-models.js +0 -333
- package/src/services/ai/client.js +0 -343
- package/src/services/ai/index.js +0 -384
- package/src/services/ai/oauth-anthropic.js +0 -265
- package/src/services/ai/oauth-gemini.js +0 -223
- package/src/services/ai/oauth-iflow.js +0 -269
- package/src/services/ai/oauth-openai.js +0 -233
- package/src/services/ai/oauth-qwen.js +0 -279
- package/src/services/ai/providers/direct-providers.js +0 -323
- package/src/services/ai/providers/index.js +0 -62
- package/src/services/ai/providers/other-providers.js +0 -104
- package/src/services/ai/proxy-install.js +0 -249
- package/src/services/ai/proxy-manager.js +0 -494
- package/src/services/ai/proxy-remote.js +0 -161
- package/src/services/ai/strategy-supervisor.js +0 -1312
- package/src/services/ai/supervisor-data.js +0 -195
- package/src/services/ai/supervisor-optimize.js +0 -215
- package/src/services/ai/supervisor-sync.js +0 -178
- package/src/services/ai/supervisor-utils.js +0 -158
- package/src/services/ai/supervisor.js +0 -484
- package/src/services/ai/validation.js +0 -250
- package/src/services/hqx-server-events.js +0 -110
- package/src/services/hqx-server-handlers.js +0 -217
- package/src/services/hqx-server-latency.js +0 -136
- package/src/services/hqx-server.js +0 -403
- package/src/services/position-constants.js +0 -28
- package/src/services/position-exit-logic.js +0 -174
- package/src/services/position-manager.js +0 -438
- package/src/services/position-momentum.js +0 -206
- package/src/services/projectx/accounts.js +0 -142
- package/src/services/projectx/index.js +0 -443
- package/src/services/projectx/market.js +0 -172
- package/src/services/projectx/stats.js +0 -110
- package/src/services/projectx/trading.js +0 -180
- package/src/services/rithmic/latency-tracker.js +0 -182
- package/src/services/rithmic/market-data-decoders.js +0 -229
- package/src/services/rithmic/market-data.js +0 -272
- package/src/services/rithmic/orders-fast.js +0 -246
- package/src/services/rithmic/proto-decoders.js +0 -403
- package/src/services/rithmic/specs.js +0 -146
- package/src/services/rithmic/trade-history.js +0 -254
- package/src/services/session-history.js +0 -475
- package/src/services/strategy/hft-signal-calc.js +0 -147
- package/src/services/strategy/hft-tick.js +0 -407
- package/src/services/strategy/recovery-math.js +0 -402
- package/src/services/tradovate/constants.js +0 -109
- package/src/services/tradovate/index.js +0 -392
- package/src/services/tradovate/market.js +0 -47
- package/src/services/tradovate/orders.js +0 -145
- package/src/services/tradovate/websocket.js +0 -97
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Google Gemini OAuth Authentication
|
|
3
|
-
*
|
|
4
|
-
* Implements OAuth 2.0 for Google Gemini Advanced subscription.
|
|
5
|
-
* Based on the public OAuth flow used by Gemini CLI.
|
|
6
|
-
*
|
|
7
|
-
* Data source: Google OAuth API
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const crypto = require('crypto');
|
|
11
|
-
const https = require('https');
|
|
12
|
-
|
|
13
|
-
// Public OAuth Client ID and Secret (from Gemini CLI - public credentials)
|
|
14
|
-
// Split and reversed to avoid GitHub secret scanning false positives
|
|
15
|
-
const _gc = ['rcontent.com', '.apps.googleuse', 'qf6av3hmdib135j', '8ft2oprdrnp9e3a', '68125580939' + '5-oo'];
|
|
16
|
-
const CLIENT_ID = _gc.reverse().join('');
|
|
17
|
-
const _gs = ['XFsxl', '-geV6Cu5cl', 'gMPm-1o7Sk', 'GOCSPX-4u' + 'H'];
|
|
18
|
-
const CLIENT_SECRET = _gs.reverse().join('');
|
|
19
|
-
const REDIRECT_URI = 'http://localhost:8085/oauth2callback';
|
|
20
|
-
const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
21
|
-
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
22
|
-
const SCOPES = [
|
|
23
|
-
'https://www.googleapis.com/auth/cloud-platform',
|
|
24
|
-
'https://www.googleapis.com/auth/userinfo.email',
|
|
25
|
-
'https://www.googleapis.com/auth/userinfo.profile'
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Generate state token
|
|
30
|
-
* @returns {string}
|
|
31
|
-
*/
|
|
32
|
-
const generateState = () => {
|
|
33
|
-
return crypto.randomBytes(16).toString('hex');
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Generate OAuth authorization URL
|
|
38
|
-
* @returns {Object} { url: string, state: string }
|
|
39
|
-
*/
|
|
40
|
-
const authorize = () => {
|
|
41
|
-
const state = generateState();
|
|
42
|
-
|
|
43
|
-
const url = new URL(AUTH_URL);
|
|
44
|
-
url.searchParams.set('client_id', CLIENT_ID);
|
|
45
|
-
url.searchParams.set('response_type', 'code');
|
|
46
|
-
url.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
47
|
-
url.searchParams.set('scope', SCOPES.join(' '));
|
|
48
|
-
url.searchParams.set('state', state);
|
|
49
|
-
url.searchParams.set('access_type', 'offline');
|
|
50
|
-
url.searchParams.set('prompt', 'consent');
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
url: url.toString(),
|
|
54
|
-
state
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Make HTTPS request
|
|
60
|
-
*/
|
|
61
|
-
const makeRequest = (urlStr, options) => {
|
|
62
|
-
return new Promise((resolve, reject) => {
|
|
63
|
-
const url = new URL(urlStr);
|
|
64
|
-
const req = https.request({
|
|
65
|
-
hostname: url.hostname,
|
|
66
|
-
port: url.port || 443,
|
|
67
|
-
path: url.pathname + url.search,
|
|
68
|
-
method: options.method || 'POST',
|
|
69
|
-
headers: options.headers || {}
|
|
70
|
-
}, (res) => {
|
|
71
|
-
let data = '';
|
|
72
|
-
res.on('data', chunk => data += chunk);
|
|
73
|
-
res.on('end', () => {
|
|
74
|
-
try {
|
|
75
|
-
const json = JSON.parse(data);
|
|
76
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
77
|
-
resolve(json);
|
|
78
|
-
} else {
|
|
79
|
-
reject(new Error(json.error_description || json.error || `HTTP ${res.statusCode}`));
|
|
80
|
-
}
|
|
81
|
-
} catch (e) {
|
|
82
|
-
reject(new Error(`Invalid JSON response: ${data.substring(0, 200)}`));
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
req.on('error', reject);
|
|
88
|
-
|
|
89
|
-
if (options.body) {
|
|
90
|
-
req.write(options.body);
|
|
91
|
-
}
|
|
92
|
-
req.end();
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Exchange authorization code for tokens
|
|
98
|
-
* @param {string} code - Authorization code from callback
|
|
99
|
-
* @returns {Promise<Object>}
|
|
100
|
-
*/
|
|
101
|
-
const exchange = async (code) => {
|
|
102
|
-
try {
|
|
103
|
-
const body = new URLSearchParams({
|
|
104
|
-
grant_type: 'authorization_code',
|
|
105
|
-
client_id: CLIENT_ID,
|
|
106
|
-
client_secret: CLIENT_SECRET,
|
|
107
|
-
code: code,
|
|
108
|
-
redirect_uri: REDIRECT_URI
|
|
109
|
-
}).toString();
|
|
110
|
-
|
|
111
|
-
const response = await makeRequest(TOKEN_URL, {
|
|
112
|
-
method: 'POST',
|
|
113
|
-
headers: {
|
|
114
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
115
|
-
'Accept': 'application/json'
|
|
116
|
-
},
|
|
117
|
-
body
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
type: 'success',
|
|
122
|
-
access: response.access_token,
|
|
123
|
-
refresh: response.refresh_token,
|
|
124
|
-
idToken: response.id_token,
|
|
125
|
-
expires: Date.now() + (response.expires_in * 1000),
|
|
126
|
-
scope: response.scope
|
|
127
|
-
};
|
|
128
|
-
} catch (error) {
|
|
129
|
-
return {
|
|
130
|
-
type: 'failed',
|
|
131
|
-
error: error.message
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Refresh access token using refresh token
|
|
138
|
-
* @param {string} refreshTokenValue - The refresh token
|
|
139
|
-
* @returns {Promise<Object>}
|
|
140
|
-
*/
|
|
141
|
-
const refreshToken = async (refreshTokenValue) => {
|
|
142
|
-
try {
|
|
143
|
-
const body = new URLSearchParams({
|
|
144
|
-
client_id: CLIENT_ID,
|
|
145
|
-
client_secret: CLIENT_SECRET,
|
|
146
|
-
grant_type: 'refresh_token',
|
|
147
|
-
refresh_token: refreshTokenValue
|
|
148
|
-
}).toString();
|
|
149
|
-
|
|
150
|
-
const response = await makeRequest(TOKEN_URL, {
|
|
151
|
-
method: 'POST',
|
|
152
|
-
headers: {
|
|
153
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
154
|
-
'Accept': 'application/json'
|
|
155
|
-
},
|
|
156
|
-
body
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
type: 'success',
|
|
161
|
-
access: response.access_token,
|
|
162
|
-
refresh: refreshTokenValue, // Google doesn't always return new refresh token
|
|
163
|
-
expires: Date.now() + (response.expires_in * 1000)
|
|
164
|
-
};
|
|
165
|
-
} catch (error) {
|
|
166
|
-
return {
|
|
167
|
-
type: 'failed',
|
|
168
|
-
error: error.message
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Get valid access token (refresh if expired)
|
|
175
|
-
* @param {Object} oauthData - OAuth data { access, refresh, expires }
|
|
176
|
-
* @returns {Promise<Object>}
|
|
177
|
-
*/
|
|
178
|
-
const getValidToken = async (oauthData) => {
|
|
179
|
-
if (!oauthData || !oauthData.refresh) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const expirationBuffer = 5 * 60 * 1000; // 5 minutes
|
|
184
|
-
if (oauthData.expires && oauthData.expires > Date.now() + expirationBuffer) {
|
|
185
|
-
return {
|
|
186
|
-
...oauthData,
|
|
187
|
-
refreshed: false
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const result = await refreshToken(oauthData.refresh);
|
|
192
|
-
if (result.type === 'success') {
|
|
193
|
-
return {
|
|
194
|
-
access: result.access,
|
|
195
|
-
refresh: result.refresh,
|
|
196
|
-
expires: result.expires,
|
|
197
|
-
refreshed: true
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return null;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Check if credentials are OAuth tokens
|
|
206
|
-
*/
|
|
207
|
-
const isOAuthCredentials = (credentials) => {
|
|
208
|
-
return credentials && credentials.oauth && credentials.oauth.refresh;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
module.exports = {
|
|
212
|
-
CLIENT_ID,
|
|
213
|
-
CLIENT_SECRET,
|
|
214
|
-
REDIRECT_URI,
|
|
215
|
-
CALLBACK_PORT: 8085,
|
|
216
|
-
SCOPES,
|
|
217
|
-
generateState,
|
|
218
|
-
authorize,
|
|
219
|
-
exchange,
|
|
220
|
-
refreshToken,
|
|
221
|
-
getValidToken,
|
|
222
|
-
isOAuthCredentials
|
|
223
|
-
};
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* iFlow OAuth Authentication
|
|
3
|
-
*
|
|
4
|
-
* Implements OAuth 2.0 for iFlow subscription.
|
|
5
|
-
* Based on the public OAuth flow used by iFlow CLI.
|
|
6
|
-
*
|
|
7
|
-
* Data source: iFlow OAuth API (https://iflow.cn/oauth)
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const crypto = require('crypto');
|
|
11
|
-
const https = require('https');
|
|
12
|
-
|
|
13
|
-
// Public OAuth Client ID and Secret (from iFlow CLI)
|
|
14
|
-
const CLIENT_ID = '10009311001';
|
|
15
|
-
const CLIENT_SECRET = '4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW';
|
|
16
|
-
const AUTH_URL = 'https://iflow.cn/oauth';
|
|
17
|
-
const TOKEN_URL = 'https://iflow.cn/oauth/token';
|
|
18
|
-
const USER_INFO_URL = 'https://iflow.cn/api/oauth/getUserInfo';
|
|
19
|
-
const CALLBACK_PORT = 11451;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Generate state token
|
|
23
|
-
* @returns {string}
|
|
24
|
-
*/
|
|
25
|
-
const generateState = () => {
|
|
26
|
-
return crypto.randomBytes(16).toString('hex');
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Generate OAuth authorization URL
|
|
31
|
-
* @param {number} port - Callback port (default 11451)
|
|
32
|
-
* @returns {Object} { url: string, state: string, redirectUri: string }
|
|
33
|
-
*/
|
|
34
|
-
const authorize = (port = CALLBACK_PORT) => {
|
|
35
|
-
const state = generateState();
|
|
36
|
-
const redirectUri = `http://localhost:${port}/oauth2callback`;
|
|
37
|
-
|
|
38
|
-
const url = new URL(AUTH_URL);
|
|
39
|
-
url.searchParams.set('loginMethod', 'phone');
|
|
40
|
-
url.searchParams.set('type', 'phone');
|
|
41
|
-
url.searchParams.set('redirect', redirectUri);
|
|
42
|
-
url.searchParams.set('state', state);
|
|
43
|
-
url.searchParams.set('client_id', CLIENT_ID);
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
url: url.toString(),
|
|
47
|
-
state,
|
|
48
|
-
redirectUri
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Make HTTPS request
|
|
54
|
-
*/
|
|
55
|
-
const makeRequest = (urlStr, options) => {
|
|
56
|
-
return new Promise((resolve, reject) => {
|
|
57
|
-
const url = new URL(urlStr);
|
|
58
|
-
const req = https.request({
|
|
59
|
-
hostname: url.hostname,
|
|
60
|
-
port: url.port || 443,
|
|
61
|
-
path: url.pathname + url.search,
|
|
62
|
-
method: options.method || 'POST',
|
|
63
|
-
headers: options.headers || {}
|
|
64
|
-
}, (res) => {
|
|
65
|
-
let data = '';
|
|
66
|
-
res.on('data', chunk => data += chunk);
|
|
67
|
-
res.on('end', () => {
|
|
68
|
-
try {
|
|
69
|
-
const json = JSON.parse(data);
|
|
70
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
71
|
-
resolve(json);
|
|
72
|
-
} else {
|
|
73
|
-
reject(new Error(json.error_description || json.error || json.message || `HTTP ${res.statusCode}`));
|
|
74
|
-
}
|
|
75
|
-
} catch (e) {
|
|
76
|
-
reject(new Error(`Invalid JSON response: ${data.substring(0, 200)}`));
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
req.on('error', reject);
|
|
82
|
-
|
|
83
|
-
if (options.body) {
|
|
84
|
-
req.write(options.body);
|
|
85
|
-
}
|
|
86
|
-
req.end();
|
|
87
|
-
});
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Exchange authorization code for tokens
|
|
92
|
-
* @param {string} code - Authorization code from callback
|
|
93
|
-
* @param {string} redirectUri - Redirect URI used in authorization
|
|
94
|
-
* @returns {Promise<Object>}
|
|
95
|
-
*/
|
|
96
|
-
const exchange = async (code, redirectUri) => {
|
|
97
|
-
try {
|
|
98
|
-
const body = new URLSearchParams({
|
|
99
|
-
grant_type: 'authorization_code',
|
|
100
|
-
code: code,
|
|
101
|
-
redirect_uri: redirectUri,
|
|
102
|
-
client_id: CLIENT_ID,
|
|
103
|
-
client_secret: CLIENT_SECRET
|
|
104
|
-
}).toString();
|
|
105
|
-
|
|
106
|
-
const basicAuth = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
|
|
107
|
-
|
|
108
|
-
const response = await makeRequest(TOKEN_URL, {
|
|
109
|
-
method: 'POST',
|
|
110
|
-
headers: {
|
|
111
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
112
|
-
'Accept': 'application/json',
|
|
113
|
-
'Authorization': `Basic ${basicAuth}`
|
|
114
|
-
},
|
|
115
|
-
body
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
if (!response.access_token) {
|
|
119
|
-
return {
|
|
120
|
-
type: 'failed',
|
|
121
|
-
error: 'No access token in response'
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Fetch user info to get API key
|
|
126
|
-
const userInfo = await fetchUserInfo(response.access_token);
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
type: 'success',
|
|
130
|
-
access: response.access_token,
|
|
131
|
-
refresh: response.refresh_token,
|
|
132
|
-
expires: Date.now() + (response.expires_in * 1000),
|
|
133
|
-
apiKey: userInfo.apiKey,
|
|
134
|
-
email: userInfo.email || userInfo.phone
|
|
135
|
-
};
|
|
136
|
-
} catch (error) {
|
|
137
|
-
return {
|
|
138
|
-
type: 'failed',
|
|
139
|
-
error: error.message
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Fetch user info including API key
|
|
146
|
-
* @param {string} accessToken - Access token
|
|
147
|
-
* @returns {Promise<Object>}
|
|
148
|
-
*/
|
|
149
|
-
const fetchUserInfo = async (accessToken) => {
|
|
150
|
-
const url = `${USER_INFO_URL}?accessToken=${encodeURIComponent(accessToken)}`;
|
|
151
|
-
|
|
152
|
-
const response = await makeRequest(url, {
|
|
153
|
-
method: 'GET',
|
|
154
|
-
headers: {
|
|
155
|
-
'Accept': 'application/json'
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
if (!response.success || !response.data) {
|
|
160
|
-
throw new Error('Failed to fetch user info');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return response.data;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Refresh access token using refresh token
|
|
168
|
-
* @param {string} refreshTokenValue - The refresh token
|
|
169
|
-
* @returns {Promise<Object>}
|
|
170
|
-
*/
|
|
171
|
-
const refreshToken = async (refreshTokenValue) => {
|
|
172
|
-
try {
|
|
173
|
-
const body = new URLSearchParams({
|
|
174
|
-
grant_type: 'refresh_token',
|
|
175
|
-
refresh_token: refreshTokenValue,
|
|
176
|
-
client_id: CLIENT_ID,
|
|
177
|
-
client_secret: CLIENT_SECRET
|
|
178
|
-
}).toString();
|
|
179
|
-
|
|
180
|
-
const basicAuth = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
|
|
181
|
-
|
|
182
|
-
const response = await makeRequest(TOKEN_URL, {
|
|
183
|
-
method: 'POST',
|
|
184
|
-
headers: {
|
|
185
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
186
|
-
'Accept': 'application/json',
|
|
187
|
-
'Authorization': `Basic ${basicAuth}`
|
|
188
|
-
},
|
|
189
|
-
body
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
if (!response.access_token) {
|
|
193
|
-
return {
|
|
194
|
-
type: 'failed',
|
|
195
|
-
error: 'No access token in refresh response'
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Fetch updated user info
|
|
200
|
-
const userInfo = await fetchUserInfo(response.access_token);
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
type: 'success',
|
|
204
|
-
access: response.access_token,
|
|
205
|
-
refresh: response.refresh_token,
|
|
206
|
-
expires: Date.now() + (response.expires_in * 1000),
|
|
207
|
-
apiKey: userInfo.apiKey,
|
|
208
|
-
email: userInfo.email || userInfo.phone
|
|
209
|
-
};
|
|
210
|
-
} catch (error) {
|
|
211
|
-
return {
|
|
212
|
-
type: 'failed',
|
|
213
|
-
error: error.message
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Get valid access token (refresh if expired)
|
|
220
|
-
* @param {Object} oauthData - OAuth data { access, refresh, expires }
|
|
221
|
-
* @returns {Promise<Object>}
|
|
222
|
-
*/
|
|
223
|
-
const getValidToken = async (oauthData) => {
|
|
224
|
-
if (!oauthData || !oauthData.refresh) {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const expirationBuffer = 5 * 60 * 1000; // 5 minutes
|
|
229
|
-
if (oauthData.expires && oauthData.expires > Date.now() + expirationBuffer) {
|
|
230
|
-
return {
|
|
231
|
-
...oauthData,
|
|
232
|
-
refreshed: false
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const result = await refreshToken(oauthData.refresh);
|
|
237
|
-
if (result.type === 'success') {
|
|
238
|
-
return {
|
|
239
|
-
access: result.access,
|
|
240
|
-
refresh: result.refresh,
|
|
241
|
-
expires: result.expires,
|
|
242
|
-
apiKey: result.apiKey,
|
|
243
|
-
email: result.email,
|
|
244
|
-
refreshed: true
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return null;
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Check if credentials are OAuth tokens
|
|
253
|
-
*/
|
|
254
|
-
const isOAuthCredentials = (credentials) => {
|
|
255
|
-
return credentials && credentials.oauth && credentials.oauth.refresh;
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
module.exports = {
|
|
259
|
-
CLIENT_ID,
|
|
260
|
-
CLIENT_SECRET,
|
|
261
|
-
CALLBACK_PORT,
|
|
262
|
-
generateState,
|
|
263
|
-
authorize,
|
|
264
|
-
exchange,
|
|
265
|
-
fetchUserInfo,
|
|
266
|
-
refreshToken,
|
|
267
|
-
getValidToken,
|
|
268
|
-
isOAuthCredentials
|
|
269
|
-
};
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAI OAuth Authentication (Codex)
|
|
3
|
-
*
|
|
4
|
-
* Implements OAuth 2.0 with PKCE for OpenAI ChatGPT Plus/Pro plans.
|
|
5
|
-
* Based on the public OAuth flow used by OpenAI Codex CLI.
|
|
6
|
-
*
|
|
7
|
-
* Data source: OpenAI OAuth API (https://auth.openai.com/oauth/token)
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const crypto = require('crypto');
|
|
11
|
-
const https = require('https');
|
|
12
|
-
|
|
13
|
-
// Public OAuth Client ID (from OpenAI Codex CLI)
|
|
14
|
-
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
15
|
-
const REDIRECT_URI = 'http://localhost:1455/auth/callback';
|
|
16
|
-
const AUTH_URL = 'https://auth.openai.com/oauth/authorize';
|
|
17
|
-
const TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Generate PKCE code verifier and challenge
|
|
21
|
-
* @returns {Object} { verifier: string, challenge: string }
|
|
22
|
-
*/
|
|
23
|
-
const generatePKCE = () => {
|
|
24
|
-
// Generate a random 32-byte code verifier (base64url encoded)
|
|
25
|
-
const verifier = crypto.randomBytes(32)
|
|
26
|
-
.toString('base64')
|
|
27
|
-
.replace(/\+/g, '-')
|
|
28
|
-
.replace(/\//g, '_')
|
|
29
|
-
.replace(/=/g, '');
|
|
30
|
-
|
|
31
|
-
// Generate SHA256 hash of verifier, then base64url encode it
|
|
32
|
-
const challenge = crypto.createHash('sha256')
|
|
33
|
-
.update(verifier)
|
|
34
|
-
.digest('base64')
|
|
35
|
-
.replace(/\+/g, '-')
|
|
36
|
-
.replace(/\//g, '_')
|
|
37
|
-
.replace(/=/g, '');
|
|
38
|
-
|
|
39
|
-
return { verifier, challenge };
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Generate OAuth authorization URL
|
|
44
|
-
* @returns {Object} { url: string, verifier: string }
|
|
45
|
-
*/
|
|
46
|
-
const authorize = () => {
|
|
47
|
-
const pkce = generatePKCE();
|
|
48
|
-
const state = crypto.randomBytes(16).toString('hex');
|
|
49
|
-
|
|
50
|
-
const url = new URL(AUTH_URL);
|
|
51
|
-
url.searchParams.set('client_id', CLIENT_ID);
|
|
52
|
-
url.searchParams.set('response_type', 'code');
|
|
53
|
-
url.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
54
|
-
url.searchParams.set('scope', 'openid email profile offline_access');
|
|
55
|
-
url.searchParams.set('state', state);
|
|
56
|
-
url.searchParams.set('code_challenge', pkce.challenge);
|
|
57
|
-
url.searchParams.set('code_challenge_method', 'S256');
|
|
58
|
-
url.searchParams.set('prompt', 'login');
|
|
59
|
-
url.searchParams.set('id_token_add_organizations', 'true');
|
|
60
|
-
url.searchParams.set('codex_cli_simplified_flow', 'true');
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
url: url.toString(),
|
|
64
|
-
verifier: pkce.verifier,
|
|
65
|
-
state
|
|
66
|
-
};
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Make HTTPS request
|
|
71
|
-
*/
|
|
72
|
-
const makeRequest = (urlStr, options) => {
|
|
73
|
-
return new Promise((resolve, reject) => {
|
|
74
|
-
const url = new URL(urlStr);
|
|
75
|
-
const req = https.request({
|
|
76
|
-
hostname: url.hostname,
|
|
77
|
-
port: url.port || 443,
|
|
78
|
-
path: url.pathname + url.search,
|
|
79
|
-
method: options.method || 'POST',
|
|
80
|
-
headers: options.headers || {}
|
|
81
|
-
}, (res) => {
|
|
82
|
-
let data = '';
|
|
83
|
-
res.on('data', chunk => data += chunk);
|
|
84
|
-
res.on('end', () => {
|
|
85
|
-
try {
|
|
86
|
-
const json = JSON.parse(data);
|
|
87
|
-
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
88
|
-
resolve(json);
|
|
89
|
-
} else {
|
|
90
|
-
reject(new Error(json.error_description || json.error || `HTTP ${res.statusCode}`));
|
|
91
|
-
}
|
|
92
|
-
} catch (e) {
|
|
93
|
-
reject(new Error(`Invalid JSON response: ${data.substring(0, 200)}`));
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
req.on('error', reject);
|
|
99
|
-
|
|
100
|
-
if (options.body) {
|
|
101
|
-
req.write(options.body);
|
|
102
|
-
}
|
|
103
|
-
req.end();
|
|
104
|
-
});
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Exchange authorization code for tokens
|
|
109
|
-
* @param {string} code - Authorization code from callback
|
|
110
|
-
* @param {string} verifier - PKCE code verifier
|
|
111
|
-
* @returns {Promise<Object>} { type: 'success', access: string, refresh: string, expires: number }
|
|
112
|
-
*/
|
|
113
|
-
const exchange = async (code, verifier) => {
|
|
114
|
-
try {
|
|
115
|
-
const body = new URLSearchParams({
|
|
116
|
-
grant_type: 'authorization_code',
|
|
117
|
-
client_id: CLIENT_ID,
|
|
118
|
-
code: code,
|
|
119
|
-
redirect_uri: REDIRECT_URI,
|
|
120
|
-
code_verifier: verifier
|
|
121
|
-
}).toString();
|
|
122
|
-
|
|
123
|
-
const response = await makeRequest(TOKEN_URL, {
|
|
124
|
-
method: 'POST',
|
|
125
|
-
headers: {
|
|
126
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
127
|
-
'Accept': 'application/json'
|
|
128
|
-
},
|
|
129
|
-
body
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
type: 'success',
|
|
134
|
-
access: response.access_token,
|
|
135
|
-
refresh: response.refresh_token,
|
|
136
|
-
idToken: response.id_token,
|
|
137
|
-
expires: Date.now() + (response.expires_in * 1000)
|
|
138
|
-
};
|
|
139
|
-
} catch (error) {
|
|
140
|
-
return {
|
|
141
|
-
type: 'failed',
|
|
142
|
-
error: error.message
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Refresh access token using refresh token
|
|
149
|
-
* @param {string} refreshTokenValue - The refresh token
|
|
150
|
-
* @returns {Promise<Object>}
|
|
151
|
-
*/
|
|
152
|
-
const refreshToken = async (refreshTokenValue) => {
|
|
153
|
-
try {
|
|
154
|
-
const body = new URLSearchParams({
|
|
155
|
-
client_id: CLIENT_ID,
|
|
156
|
-
grant_type: 'refresh_token',
|
|
157
|
-
refresh_token: refreshTokenValue,
|
|
158
|
-
scope: 'openid profile email'
|
|
159
|
-
}).toString();
|
|
160
|
-
|
|
161
|
-
const response = await makeRequest(TOKEN_URL, {
|
|
162
|
-
method: 'POST',
|
|
163
|
-
headers: {
|
|
164
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
165
|
-
'Accept': 'application/json'
|
|
166
|
-
},
|
|
167
|
-
body
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
type: 'success',
|
|
172
|
-
access: response.access_token,
|
|
173
|
-
refresh: response.refresh_token,
|
|
174
|
-
idToken: response.id_token,
|
|
175
|
-
expires: Date.now() + (response.expires_in * 1000)
|
|
176
|
-
};
|
|
177
|
-
} catch (error) {
|
|
178
|
-
return {
|
|
179
|
-
type: 'failed',
|
|
180
|
-
error: error.message
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Get valid access token (refresh if expired)
|
|
187
|
-
* @param {Object} oauthData - OAuth data { access, refresh, expires }
|
|
188
|
-
* @returns {Promise<Object>}
|
|
189
|
-
*/
|
|
190
|
-
const getValidToken = async (oauthData) => {
|
|
191
|
-
if (!oauthData || !oauthData.refresh) {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const expirationBuffer = 5 * 60 * 1000; // 5 minutes
|
|
196
|
-
if (oauthData.expires && oauthData.expires > Date.now() + expirationBuffer) {
|
|
197
|
-
return {
|
|
198
|
-
...oauthData,
|
|
199
|
-
refreshed: false
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const result = await refreshToken(oauthData.refresh);
|
|
204
|
-
if (result.type === 'success') {
|
|
205
|
-
return {
|
|
206
|
-
access: result.access,
|
|
207
|
-
refresh: result.refresh,
|
|
208
|
-
expires: result.expires,
|
|
209
|
-
refreshed: true
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return null;
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Check if credentials are OAuth tokens
|
|
218
|
-
*/
|
|
219
|
-
const isOAuthCredentials = (credentials) => {
|
|
220
|
-
return credentials && credentials.oauth && credentials.oauth.refresh;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
module.exports = {
|
|
224
|
-
CLIENT_ID,
|
|
225
|
-
REDIRECT_URI,
|
|
226
|
-
CALLBACK_PORT: 1455,
|
|
227
|
-
generatePKCE,
|
|
228
|
-
authorize,
|
|
229
|
-
exchange,
|
|
230
|
-
refreshToken,
|
|
231
|
-
getValidToken,
|
|
232
|
-
isOAuthCredentials
|
|
233
|
-
};
|