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.
@@ -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
- // Check if it's an OAuth token (sk-ant-oat...) vs API key (sk-ant-api...)
379
- const isOAuthToken = token && token.startsWith('sk-ant-oat');
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
- return { valid: true, tokenType: 'api_key' };
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
- 'anthropic/claude-sonnet-4',
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)',