hedgequantx 2.6.68 → 2.6.70

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.
@@ -0,0 +1,269 @@
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
+ };
@@ -0,0 +1,233 @@
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
+ };