hedgequantx 2.5.23 → 2.5.25
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 +87 -14
- package/package.json +1 -1
- package/src/menus/ai-agent.js +142 -176
- package/src/services/ai/client.js +77 -8
- package/src/services/ai/index.js +10 -40
- package/src/services/ai/oauth-anthropic.js +265 -0
- package/src/services/ai/providers/index.js +12 -11
- package/src/services/ai/token-scanner.js +0 -1414
package/src/services/ai/index.js
CHANGED
|
@@ -374,58 +374,28 @@ const validateConnection = async (providerId, optionId, credentials) => {
|
|
|
374
374
|
const validateAnthropic = async (credentials) => {
|
|
375
375
|
try {
|
|
376
376
|
const token = credentials.apiKey || credentials.sessionKey || credentials.accessToken;
|
|
377
|
+
if (!token) return { valid: false, error: 'No API key provided' };
|
|
377
378
|
|
|
378
|
-
//
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
if (isOAuthToken) {
|
|
382
|
-
// OAuth tokens (from Claude Max/Pro subscription) cannot be validated via public API
|
|
383
|
-
// They use a different authentication flow through claude.ai
|
|
384
|
-
// Trust them if they have the correct format (sk-ant-oatXX-...)
|
|
385
|
-
if (token.length > 50 && /^sk-ant-oat\d{2}-[a-zA-Z0-9_-]+$/.test(token)) {
|
|
386
|
-
return {
|
|
387
|
-
valid: true,
|
|
388
|
-
tokenType: 'oauth',
|
|
389
|
-
subscriptionType: credentials.subscriptionType || 'max',
|
|
390
|
-
trusted: credentials.fromKeychain || false
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return { valid: false, error: 'Invalid OAuth token format' };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Standard API key validation (sk-ant-api...)
|
|
398
|
-
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
399
|
-
method: 'POST',
|
|
379
|
+
// Validate by fetching models from API - this proves the token works
|
|
380
|
+
const response = await fetch('https://api.anthropic.com/v1/models', {
|
|
381
|
+
method: 'GET',
|
|
400
382
|
headers: {
|
|
401
|
-
'Content-Type': 'application/json',
|
|
402
383
|
'x-api-key': token,
|
|
403
384
|
'anthropic-version': '2023-06-01'
|
|
404
|
-
}
|
|
405
|
-
body: JSON.stringify({
|
|
406
|
-
model: 'claude-sonnet-4-5-20250929',
|
|
407
|
-
max_tokens: 10,
|
|
408
|
-
messages: [{ role: 'user', content: 'Hi' }]
|
|
409
|
-
})
|
|
385
|
+
}
|
|
410
386
|
});
|
|
411
387
|
|
|
412
388
|
if (response.ok) {
|
|
413
|
-
|
|
389
|
+
const data = await response.json();
|
|
390
|
+
if (data.data && Array.isArray(data.data) && data.data.length > 0) {
|
|
391
|
+
return { valid: true, tokenType: 'api_key' };
|
|
392
|
+
}
|
|
393
|
+
return { valid: false, error: 'API returned no models' };
|
|
414
394
|
}
|
|
415
395
|
|
|
416
396
|
const error = await response.json();
|
|
417
397
|
return { valid: false, error: error.error?.message || 'Invalid API key' };
|
|
418
398
|
} catch (e) {
|
|
419
|
-
// Network error - if it's an OAuth token, still accept it (can't validate anyway)
|
|
420
|
-
const token = credentials.apiKey || credentials.sessionKey || credentials.accessToken;
|
|
421
|
-
if (token && token.startsWith('sk-ant-oat') && token.length > 50) {
|
|
422
|
-
return {
|
|
423
|
-
valid: true,
|
|
424
|
-
tokenType: 'oauth',
|
|
425
|
-
subscriptionType: credentials.subscriptionType || 'max',
|
|
426
|
-
warning: 'Could not validate online (network error), but token format is valid'
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
399
|
return { valid: false, error: e.message };
|
|
430
400
|
}
|
|
431
401
|
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic OAuth Authentication
|
|
3
|
+
*
|
|
4
|
+
* Implements OAuth 2.0 with PKCE for Anthropic Claude Pro/Max plans.
|
|
5
|
+
* Based on the public OAuth flow used by OpenCode.
|
|
6
|
+
*
|
|
7
|
+
* Data source: Anthropic OAuth API (https://console.anthropic.com/v1/oauth/token)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const https = require('https');
|
|
12
|
+
|
|
13
|
+
// Public OAuth Client ID (same as OpenCode - registered with Anthropic)
|
|
14
|
+
const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
15
|
+
const REDIRECT_URI = 'https://console.anthropic.com/oauth/code/callback';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate PKCE code verifier and challenge
|
|
19
|
+
* @returns {Object} { verifier: string, challenge: string }
|
|
20
|
+
*/
|
|
21
|
+
const generatePKCE = () => {
|
|
22
|
+
// Generate a random 32-byte code verifier (base64url encoded)
|
|
23
|
+
const verifier = crypto.randomBytes(32)
|
|
24
|
+
.toString('base64')
|
|
25
|
+
.replace(/\+/g, '-')
|
|
26
|
+
.replace(/\//g, '_')
|
|
27
|
+
.replace(/=/g, '');
|
|
28
|
+
|
|
29
|
+
// Generate SHA256 hash of verifier, then base64url encode it
|
|
30
|
+
const challenge = crypto.createHash('sha256')
|
|
31
|
+
.update(verifier)
|
|
32
|
+
.digest('base64')
|
|
33
|
+
.replace(/\+/g, '-')
|
|
34
|
+
.replace(/\//g, '_')
|
|
35
|
+
.replace(/=/g, '');
|
|
36
|
+
|
|
37
|
+
return { verifier, challenge };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate OAuth authorization URL
|
|
42
|
+
* @param {'max' | 'console'} mode - 'max' for Claude Pro/Max, 'console' for API key creation
|
|
43
|
+
* @returns {Object} { url: string, verifier: string }
|
|
44
|
+
*/
|
|
45
|
+
const authorize = (mode = 'max') => {
|
|
46
|
+
const pkce = generatePKCE();
|
|
47
|
+
|
|
48
|
+
const baseUrl = mode === 'max'
|
|
49
|
+
? 'https://claude.ai/oauth/authorize'
|
|
50
|
+
: 'https://console.anthropic.com/oauth/authorize';
|
|
51
|
+
|
|
52
|
+
const url = new URL(baseUrl);
|
|
53
|
+
url.searchParams.set('code', 'true');
|
|
54
|
+
url.searchParams.set('client_id', CLIENT_ID);
|
|
55
|
+
url.searchParams.set('response_type', 'code');
|
|
56
|
+
url.searchParams.set('redirect_uri', REDIRECT_URI);
|
|
57
|
+
url.searchParams.set('scope', 'org:create_api_key user:profile user:inference');
|
|
58
|
+
url.searchParams.set('code_challenge', pkce.challenge);
|
|
59
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
60
|
+
url.searchParams.set('state', pkce.verifier);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
url: url.toString(),
|
|
64
|
+
verifier: pkce.verifier
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Make HTTPS request
|
|
70
|
+
* @param {string} url - Full URL
|
|
71
|
+
* @param {Object} options - Request options
|
|
72
|
+
* @returns {Promise<Object>} Response JSON
|
|
73
|
+
*/
|
|
74
|
+
const makeRequest = (url, options) => {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const req = https.request(url, {
|
|
77
|
+
method: options.method || 'POST',
|
|
78
|
+
headers: options.headers || {}
|
|
79
|
+
}, (res) => {
|
|
80
|
+
let data = '';
|
|
81
|
+
res.on('data', chunk => data += chunk);
|
|
82
|
+
res.on('end', () => {
|
|
83
|
+
try {
|
|
84
|
+
const json = JSON.parse(data);
|
|
85
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
86
|
+
resolve(json);
|
|
87
|
+
} else {
|
|
88
|
+
reject(new Error(json.error?.message || `HTTP ${res.statusCode}: ${data}`));
|
|
89
|
+
}
|
|
90
|
+
} catch (e) {
|
|
91
|
+
reject(new Error(`Invalid JSON response: ${data.substring(0, 200)}`));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
req.on('error', reject);
|
|
97
|
+
|
|
98
|
+
if (options.body) {
|
|
99
|
+
req.write(JSON.stringify(options.body));
|
|
100
|
+
}
|
|
101
|
+
req.end();
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Exchange authorization code for tokens
|
|
107
|
+
* @param {string} code - Authorization code from callback (format: code#state)
|
|
108
|
+
* @param {string} verifier - PKCE code verifier
|
|
109
|
+
* @returns {Promise<Object>} { type: 'success', access: string, refresh: string, expires: number } or { type: 'failed' }
|
|
110
|
+
*
|
|
111
|
+
* Data source: https://console.anthropic.com/v1/oauth/token (POST)
|
|
112
|
+
*/
|
|
113
|
+
const exchange = async (code, verifier) => {
|
|
114
|
+
try {
|
|
115
|
+
// Code format from callback: "authorization_code#state"
|
|
116
|
+
const splits = code.split('#');
|
|
117
|
+
const authCode = splits[0];
|
|
118
|
+
const state = splits[1] || '';
|
|
119
|
+
|
|
120
|
+
const response = await makeRequest('https://console.anthropic.com/v1/oauth/token', {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json'
|
|
124
|
+
},
|
|
125
|
+
body: {
|
|
126
|
+
code: authCode,
|
|
127
|
+
state: state,
|
|
128
|
+
grant_type: 'authorization_code',
|
|
129
|
+
client_id: CLIENT_ID,
|
|
130
|
+
redirect_uri: REDIRECT_URI,
|
|
131
|
+
code_verifier: verifier
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
type: 'success',
|
|
137
|
+
access: response.access_token,
|
|
138
|
+
refresh: response.refresh_token,
|
|
139
|
+
expires: Date.now() + (response.expires_in * 1000)
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
type: 'failed',
|
|
144
|
+
error: error.message
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Refresh access token using refresh token
|
|
151
|
+
* @param {string} refreshToken - The refresh token
|
|
152
|
+
* @returns {Promise<Object>} { type: 'success', access: string, refresh: string, expires: number } or { type: 'failed' }
|
|
153
|
+
*
|
|
154
|
+
* Data source: https://console.anthropic.com/v1/oauth/token (POST)
|
|
155
|
+
*/
|
|
156
|
+
const refreshToken = async (refreshToken) => {
|
|
157
|
+
try {
|
|
158
|
+
const response = await makeRequest('https://console.anthropic.com/v1/oauth/token', {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Content-Type': 'application/json'
|
|
162
|
+
},
|
|
163
|
+
body: {
|
|
164
|
+
grant_type: 'refresh_token',
|
|
165
|
+
refresh_token: refreshToken,
|
|
166
|
+
client_id: CLIENT_ID
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
type: 'success',
|
|
172
|
+
access: response.access_token,
|
|
173
|
+
refresh: response.refresh_token,
|
|
174
|
+
expires: Date.now() + (response.expires_in * 1000)
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return {
|
|
178
|
+
type: 'failed',
|
|
179
|
+
error: error.message
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create an API key using OAuth token (for console mode)
|
|
186
|
+
* @param {string} accessToken - The access token
|
|
187
|
+
* @returns {Promise<Object>} { type: 'success', key: string } or { type: 'failed' }
|
|
188
|
+
*
|
|
189
|
+
* Data source: https://api.anthropic.com/api/oauth/claude_cli/create_api_key (POST)
|
|
190
|
+
*/
|
|
191
|
+
const createApiKey = async (accessToken) => {
|
|
192
|
+
try {
|
|
193
|
+
const response = await makeRequest('https://api.anthropic.com/api/oauth/claude_cli/create_api_key', {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
'Authorization': `Bearer ${accessToken}`
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
type: 'success',
|
|
203
|
+
key: response.raw_key
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return {
|
|
207
|
+
type: 'failed',
|
|
208
|
+
error: error.message
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get valid access token (refresh if expired)
|
|
215
|
+
* @param {Object} oauthData - OAuth data { access, refresh, expires }
|
|
216
|
+
* @returns {Promise<Object>} { access: string, refresh: string, expires: number, refreshed: boolean }
|
|
217
|
+
*/
|
|
218
|
+
const getValidToken = async (oauthData) => {
|
|
219
|
+
if (!oauthData || !oauthData.refresh) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Check if token is expired or will expire in the next 5 minutes
|
|
224
|
+
const expirationBuffer = 5 * 60 * 1000; // 5 minutes
|
|
225
|
+
if (oauthData.expires && oauthData.expires > Date.now() + expirationBuffer) {
|
|
226
|
+
return {
|
|
227
|
+
...oauthData,
|
|
228
|
+
refreshed: false
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Token expired or about to expire, refresh it
|
|
233
|
+
const result = await refreshToken(oauthData.refresh);
|
|
234
|
+
if (result.type === 'success') {
|
|
235
|
+
return {
|
|
236
|
+
access: result.access,
|
|
237
|
+
refresh: result.refresh,
|
|
238
|
+
expires: result.expires,
|
|
239
|
+
refreshed: true
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if credentials are OAuth tokens
|
|
248
|
+
* @param {Object} credentials - Agent credentials
|
|
249
|
+
* @returns {boolean}
|
|
250
|
+
*/
|
|
251
|
+
const isOAuthCredentials = (credentials) => {
|
|
252
|
+
return credentials && credentials.oauth && credentials.oauth.refresh;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
CLIENT_ID,
|
|
257
|
+
REDIRECT_URI,
|
|
258
|
+
generatePKCE,
|
|
259
|
+
authorize,
|
|
260
|
+
exchange,
|
|
261
|
+
refreshToken,
|
|
262
|
+
createApiKey,
|
|
263
|
+
getValidToken,
|
|
264
|
+
isOAuthCredentials
|
|
265
|
+
};
|
|
@@ -10,17 +10,8 @@ const PROVIDERS = {
|
|
|
10
10
|
name: 'OPENROUTER (RECOMMENDED)',
|
|
11
11
|
description: '1 API key for 100+ models',
|
|
12
12
|
category: 'unified',
|
|
13
|
-
models: [
|
|
14
|
-
|
|
15
|
-
'anthropic/claude-3-opus',
|
|
16
|
-
'openai/gpt-4o',
|
|
17
|
-
'openai/gpt-4-turbo',
|
|
18
|
-
'google/gemini-pro-1.5',
|
|
19
|
-
'meta-llama/llama-3-70b',
|
|
20
|
-
'mistralai/mistral-large',
|
|
21
|
-
'deepseek/deepseek-chat'
|
|
22
|
-
],
|
|
23
|
-
defaultModel: 'anthropic/claude-sonnet-4',
|
|
13
|
+
models: [], // Fetched from API at runtime
|
|
14
|
+
defaultModel: null, // Will use first model from API
|
|
24
15
|
options: [
|
|
25
16
|
{
|
|
26
17
|
id: 'api_key',
|
|
@@ -46,6 +37,16 @@ const PROVIDERS = {
|
|
|
46
37
|
models: [], // Fetched from API at runtime
|
|
47
38
|
defaultModel: null, // Will use first model from API
|
|
48
39
|
options: [
|
|
40
|
+
{
|
|
41
|
+
id: 'oauth_max',
|
|
42
|
+
label: 'CLAUDE PRO/MAX (OAUTH)',
|
|
43
|
+
description: [
|
|
44
|
+
'Login with your Claude subscription',
|
|
45
|
+
'Unlimited usage with your plan'
|
|
46
|
+
],
|
|
47
|
+
fields: ['oauth'],
|
|
48
|
+
authType: 'oauth'
|
|
49
|
+
},
|
|
49
50
|
{
|
|
50
51
|
id: 'api_key',
|
|
51
52
|
label: 'API KEY (PAY-PER-USE)',
|