commons-proxy 2.0.0

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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +757 -0
  3. package/bin/cli.js +146 -0
  4. package/package.json +97 -0
  5. package/public/Complaint Details.pdf +0 -0
  6. package/public/Cyber Crime Portal.pdf +0 -0
  7. package/public/app.js +229 -0
  8. package/public/css/src/input.css +523 -0
  9. package/public/css/style.css +1 -0
  10. package/public/favicon.png +0 -0
  11. package/public/index.html +549 -0
  12. package/public/js/components/account-manager.js +356 -0
  13. package/public/js/components/add-account-modal.js +414 -0
  14. package/public/js/components/claude-config.js +420 -0
  15. package/public/js/components/dashboard/charts.js +605 -0
  16. package/public/js/components/dashboard/filters.js +362 -0
  17. package/public/js/components/dashboard/stats.js +110 -0
  18. package/public/js/components/dashboard.js +236 -0
  19. package/public/js/components/logs-viewer.js +100 -0
  20. package/public/js/components/models.js +36 -0
  21. package/public/js/components/server-config.js +349 -0
  22. package/public/js/config/constants.js +102 -0
  23. package/public/js/data-store.js +375 -0
  24. package/public/js/settings-store.js +58 -0
  25. package/public/js/store.js +99 -0
  26. package/public/js/translations/en.js +367 -0
  27. package/public/js/translations/id.js +412 -0
  28. package/public/js/translations/pt.js +308 -0
  29. package/public/js/translations/tr.js +358 -0
  30. package/public/js/translations/zh.js +373 -0
  31. package/public/js/utils/account-actions.js +189 -0
  32. package/public/js/utils/error-handler.js +96 -0
  33. package/public/js/utils/model-config.js +42 -0
  34. package/public/js/utils/ui-logger.js +143 -0
  35. package/public/js/utils/validators.js +77 -0
  36. package/public/js/utils.js +69 -0
  37. package/public/proxy-server-64.png +0 -0
  38. package/public/views/accounts.html +361 -0
  39. package/public/views/dashboard.html +484 -0
  40. package/public/views/logs.html +97 -0
  41. package/public/views/models.html +331 -0
  42. package/public/views/settings.html +1327 -0
  43. package/src/account-manager/credentials.js +378 -0
  44. package/src/account-manager/index.js +462 -0
  45. package/src/account-manager/onboarding.js +112 -0
  46. package/src/account-manager/rate-limits.js +369 -0
  47. package/src/account-manager/storage.js +160 -0
  48. package/src/account-manager/strategies/base-strategy.js +109 -0
  49. package/src/account-manager/strategies/hybrid-strategy.js +339 -0
  50. package/src/account-manager/strategies/index.js +79 -0
  51. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  52. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  53. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  54. package/src/account-manager/strategies/trackers/index.js +9 -0
  55. package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
  56. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
  57. package/src/auth/database.js +169 -0
  58. package/src/auth/oauth.js +548 -0
  59. package/src/auth/token-extractor.js +117 -0
  60. package/src/cli/accounts.js +648 -0
  61. package/src/cloudcode/index.js +29 -0
  62. package/src/cloudcode/message-handler.js +510 -0
  63. package/src/cloudcode/model-api.js +248 -0
  64. package/src/cloudcode/rate-limit-parser.js +235 -0
  65. package/src/cloudcode/request-builder.js +93 -0
  66. package/src/cloudcode/session-manager.js +47 -0
  67. package/src/cloudcode/sse-parser.js +121 -0
  68. package/src/cloudcode/sse-streamer.js +293 -0
  69. package/src/cloudcode/streaming-handler.js +615 -0
  70. package/src/config.js +125 -0
  71. package/src/constants.js +407 -0
  72. package/src/errors.js +242 -0
  73. package/src/fallback-config.js +29 -0
  74. package/src/format/content-converter.js +193 -0
  75. package/src/format/index.js +20 -0
  76. package/src/format/request-converter.js +255 -0
  77. package/src/format/response-converter.js +120 -0
  78. package/src/format/schema-sanitizer.js +673 -0
  79. package/src/format/signature-cache.js +88 -0
  80. package/src/format/thinking-utils.js +648 -0
  81. package/src/index.js +148 -0
  82. package/src/modules/usage-stats.js +205 -0
  83. package/src/providers/anthropic-provider.js +258 -0
  84. package/src/providers/base-provider.js +157 -0
  85. package/src/providers/cloudcode.js +94 -0
  86. package/src/providers/copilot.js +399 -0
  87. package/src/providers/github-provider.js +287 -0
  88. package/src/providers/google-provider.js +192 -0
  89. package/src/providers/index.js +211 -0
  90. package/src/providers/openai-compatible.js +265 -0
  91. package/src/providers/openai-provider.js +271 -0
  92. package/src/providers/openrouter-provider.js +325 -0
  93. package/src/providers/setup.js +83 -0
  94. package/src/server.js +870 -0
  95. package/src/utils/claude-config.js +245 -0
  96. package/src/utils/helpers.js +51 -0
  97. package/src/utils/logger.js +142 -0
  98. package/src/utils/native-module-helper.js +162 -0
  99. package/src/webui/index.js +1134 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Base Provider Interface
3
+ *
4
+ * Abstract base class for authentication providers.
5
+ * Each provider implements:
6
+ * - Authentication (OAuth, API keys, PAT, etc.)
7
+ * - Token/credential validation
8
+ * - Quota/rate limit fetching
9
+ * - Account information retrieval
10
+ */
11
+
12
+ import { logger } from '../utils/logger.js';
13
+
14
+ export class BaseProvider {
15
+ /**
16
+ * @param {string} id - Unique provider identifier ('google', 'anthropic', 'openai', 'github')
17
+ * @param {string} name - Display name for UI
18
+ * @param {Object} config - Provider-specific configuration
19
+ */
20
+ constructor(id, name, config = {}) {
21
+ if (new.target === BaseProvider) {
22
+ throw new Error('BaseProvider is abstract and cannot be instantiated directly');
23
+ }
24
+ this.id = id;
25
+ this.name = name;
26
+ this.config = config;
27
+ }
28
+
29
+ /**
30
+ * Validate account credentials (API key, token, etc.)
31
+ *
32
+ * @param {Object} account - Account object with credentials
33
+ * @returns {Promise<{valid: boolean, error?: string, email?: string}>}
34
+ */
35
+ async validateCredentials(account) {
36
+ throw new Error('validateCredentials() must be implemented by subclass');
37
+ }
38
+
39
+ /**
40
+ * Get access token for making API requests
41
+ *
42
+ * @param {Object} account - Account object
43
+ * @returns {Promise<string>} Access token or API key
44
+ */
45
+ async getAccessToken(account) {
46
+ throw new Error('getAccessToken() must be implemented by subclass');
47
+ }
48
+
49
+ /**
50
+ * Fetch account quota/usage information
51
+ *
52
+ * @param {Object} account - Account object
53
+ * @param {string} token - Access token
54
+ * @returns {Promise<Object>} Quota data: { models: { [modelId]: { remainingFraction, resetTime } } }
55
+ */
56
+ async getQuotas(account, token) {
57
+ throw new Error('getQuotas() must be implemented by subclass');
58
+ }
59
+
60
+ /**
61
+ * Fetch subscription/account tier information
62
+ *
63
+ * @param {Object} account - Account object
64
+ * @param {string} token - Access token
65
+ * @returns {Promise<{tier: string, projectId?: string}>} Subscription info
66
+ */
67
+ async getSubscriptionTier(account, token) {
68
+ // Default implementation - can be overridden
69
+ return { tier: 'unknown', projectId: null };
70
+ }
71
+
72
+ /**
73
+ * Refresh expired credentials (if applicable)
74
+ *
75
+ * @param {Object} account - Account object
76
+ * @returns {Promise<Object>} Updated account object with refreshed credentials
77
+ */
78
+ async refreshCredentials(account) {
79
+ // Default: no-op for API key based providers
80
+ return account;
81
+ }
82
+
83
+ /**
84
+ * Get available models for this provider
85
+ *
86
+ * @param {Object} account - Account object
87
+ * @param {string} token - Access token
88
+ * @returns {Promise<Array<{id: string, name: string, family: string}>>} Available models
89
+ */
90
+ async getAvailableModels(account, token) {
91
+ // Default: return empty array - can be overridden
92
+ return [];
93
+ }
94
+
95
+ /**
96
+ * Parse rate limit information from API response
97
+ *
98
+ * @param {Response} response - Fetch API response object
99
+ * @param {Object} [errorData] - Optional parsed error data
100
+ * @returns {Object|null} Rate limit info: { resetTime: Date, retryAfter: number }
101
+ */
102
+ parseRateLimitInfo(response, errorData = null) {
103
+ // Default implementation - can be overridden
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Handle authentication error and determine if credentials need refresh
109
+ *
110
+ * @param {Error} error - Authentication error
111
+ * @returns {boolean} True if credentials should be marked invalid
112
+ */
113
+ shouldInvalidateCredentials(error) {
114
+ // Default: invalidate on 401/403 errors
115
+ if (error.status === 401 || error.status === 403) {
116
+ return true;
117
+ }
118
+ if (error.message && error.message.toLowerCase().includes('invalid') &&
119
+ error.message.toLowerCase().includes('api')) {
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+
125
+ /**
126
+ * Log provider-specific debug information
127
+ *
128
+ * @param {string} message - Debug message
129
+ * @param {*} data - Optional data to log
130
+ */
131
+ debug(message, data = null) {
132
+ if (logger.isDebugEnabled) {
133
+ logger.debug(`[Provider:${this.name}] ${message}`, data || '');
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Log provider info
139
+ *
140
+ * @param {string} message - Info message
141
+ */
142
+ info(message) {
143
+ logger.info(`[Provider:${this.name}] ${message}`);
144
+ }
145
+
146
+ /**
147
+ * Log provider error
148
+ *
149
+ * @param {string} message - Error message
150
+ * @param {Error} [error] - Optional error object
151
+ */
152
+ error(message, error = null) {
153
+ logger.error(`[Provider:${this.name}] ${message}`, error ? error.message : '');
154
+ }
155
+ }
156
+
157
+ export default BaseProvider;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Google Cloud Code Provider
3
+ *
4
+ * Default provider for CommonsProxy - uses Google's Cloud Code API
5
+ * to access Claude and Gemini models.
6
+ */
7
+
8
+ import { ProviderType } from './index.js';
9
+ import {
10
+ CLOUDCODE_ENDPOINT_FALLBACKS,
11
+ CLOUDCODE_HEADERS,
12
+ OAUTH_CONFIG
13
+ } from '../constants.js';
14
+ import { sendMessage } from '../cloudcode/message-handler.js';
15
+ import { sendMessageStream } from '../cloudcode/streaming-handler.js';
16
+ import { fetchAvailableModels } from '../cloudcode/model-api.js';
17
+ import { logger } from '../utils/logger.js';
18
+
19
+ /**
20
+ * Google Cloud Code Provider
21
+ */
22
+ export const CloudCodeProvider = {
23
+ id: 'cloudcode',
24
+ name: 'Google Cloud Code',
25
+ type: ProviderType.CLOUDCODE,
26
+ enabled: true,
27
+
28
+ /**
29
+ * Provider configuration
30
+ */
31
+ config: {
32
+ endpoints: CLOUDCODE_ENDPOINT_FALLBACKS,
33
+ headers: CLOUDCODE_HEADERS,
34
+ oauth: OAUTH_CONFIG
35
+ },
36
+
37
+ /**
38
+ * Send a non-streaming message
39
+ * @param {Object} request - Anthropic-format request
40
+ * @param {Object} accountManager - Account manager instance
41
+ * @param {Object} options - Additional options
42
+ * @returns {Promise<Object>} Anthropic-format response
43
+ */
44
+ async sendMessage(request, accountManager, options = {}) {
45
+ const fallbackEnabled = options.fallback !== false;
46
+ return sendMessage(request, accountManager, fallbackEnabled);
47
+ },
48
+
49
+ /**
50
+ * Send a streaming message
51
+ * @param {Object} request - Anthropic-format request
52
+ * @param {Object} accountManager - Account manager instance
53
+ * @param {Object} options - Additional options
54
+ * @yields {Object} Anthropic-format SSE events
55
+ */
56
+ async *sendMessageStream(request, accountManager, options = {}) {
57
+ const fallbackEnabled = options.fallback !== false;
58
+ yield* sendMessageStream(request, accountManager, fallbackEnabled);
59
+ },
60
+
61
+ /**
62
+ * List available models
63
+ * @param {string} token - OAuth access token
64
+ * @param {string} projectId - Project ID
65
+ * @returns {Promise<Array>} Array of model info
66
+ */
67
+ async listModels(token, projectId) {
68
+ try {
69
+ return await fetchAvailableModels(token, projectId);
70
+ } catch (error) {
71
+ logger.warn(`[CloudCode] Failed to fetch models: ${error.message}`);
72
+ return [];
73
+ }
74
+ },
75
+
76
+ /**
77
+ * Get supported model families
78
+ */
79
+ getModelFamilies() {
80
+ return ['claude', 'gemini'];
81
+ },
82
+
83
+ /**
84
+ * Check if a model is supported
85
+ * @param {string} modelId - Model ID to check
86
+ * @returns {boolean}
87
+ */
88
+ supportsModel(modelId) {
89
+ const lower = modelId.toLowerCase();
90
+ return lower.includes('claude') || lower.includes('gemini');
91
+ }
92
+ };
93
+
94
+ export default CloudCodeProvider;
@@ -0,0 +1,399 @@
1
+ /**
2
+ * GitHub Copilot Provider
3
+ *
4
+ * Enables CommonsProxy to work with GitHub Copilot's API.
5
+ * Uses GitHub Device Authorization flow for authentication,
6
+ * then exchanges the GitHub token for a Copilot API token.
7
+ *
8
+ * Inspired by opencode's copilot plugin implementation.
9
+ */
10
+
11
+ import BaseProvider from './base-provider.js';
12
+
13
+ // GitHub Copilot OAuth configuration
14
+ const COPILOT_CLIENT_ID = 'Iv1.b507a08c87ecfe98';
15
+ const COPILOT_DEVICE_CODE_URL = 'https://github.com/login/device/code';
16
+ const COPILOT_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
17
+ const COPILOT_API_URL = 'https://api.githubcopilot.com';
18
+ const COPILOT_TOKEN_URL = 'https://api.github.com/copilot_internal/v2/token';
19
+
20
+ // In-memory cache for short-lived Copilot API tokens
21
+ // Maps GitHub access token -> { token, expiresAt }
22
+ const copilotTokenCache = new Map();
23
+
24
+ export class CopilotProvider extends BaseProvider {
25
+ constructor(config = {}) {
26
+ super('copilot', 'GitHub Copilot', {
27
+ clientId: COPILOT_CLIENT_ID,
28
+ deviceCodeUrl: COPILOT_DEVICE_CODE_URL,
29
+ accessTokenUrl: COPILOT_ACCESS_TOKEN_URL,
30
+ apiUrl: config.apiUrl || COPILOT_API_URL,
31
+ tokenUrl: config.tokenUrl || COPILOT_TOKEN_URL,
32
+ ...config
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Validate Copilot credentials by attempting to get a Copilot API token
38
+ *
39
+ * @param {Object} account - Account with apiKey (GitHub access token)
40
+ * @returns {Promise<{valid: boolean, error?: string, email?: string}>}
41
+ */
42
+ async validateCredentials(account) {
43
+ if (!account.apiKey) {
44
+ return { valid: false, error: 'Missing GitHub access token' };
45
+ }
46
+
47
+ try {
48
+ // Verify by fetching user info
49
+ const userResponse = await fetch('https://api.github.com/user', {
50
+ method: 'GET',
51
+ headers: {
52
+ 'Authorization': `Bearer ${account.apiKey}`,
53
+ 'Accept': 'application/vnd.github+json',
54
+ 'User-Agent': 'commons-proxy/2.0.0',
55
+ 'X-GitHub-Api-Version': '2022-11-28'
56
+ }
57
+ });
58
+
59
+ if (!userResponse.ok) {
60
+ const error = await userResponse.text();
61
+ return { valid: false, error: `GitHub token validation failed: ${error}` };
62
+ }
63
+
64
+ const userData = await userResponse.json();
65
+
66
+ // Try to get a Copilot token to verify Copilot access
67
+ try {
68
+ await this._getCopilotToken(account.apiKey);
69
+ } catch (copilotError) {
70
+ return {
71
+ valid: false,
72
+ error: `GitHub token valid but Copilot access denied: ${copilotError.message}. Ensure you have an active GitHub Copilot subscription.`
73
+ };
74
+ }
75
+
76
+ const email = userData.email || `${userData.login}@github`;
77
+ return { valid: true, email };
78
+ } catch (error) {
79
+ this.error('Credential validation failed', error);
80
+ return { valid: false, error: error.message };
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Get Copilot API token from the stored GitHub access token.
86
+ * Copilot tokens are short-lived (~30 min), so we cache and refresh.
87
+ *
88
+ * @param {Object} account - Account with apiKey (GitHub access token)
89
+ * @returns {Promise<string>} Copilot API token
90
+ */
91
+ async getAccessToken(account) {
92
+ if (!account.apiKey) {
93
+ throw new Error('Account missing GitHub access token');
94
+ }
95
+
96
+ const copilotToken = await this._getCopilotToken(account.apiKey);
97
+ return copilotToken;
98
+ }
99
+
100
+ /**
101
+ * Internal: Get or refresh Copilot API token
102
+ *
103
+ * @param {string} githubToken - GitHub OAuth access token
104
+ * @returns {Promise<string>} Copilot API token
105
+ */
106
+ async _getCopilotToken(githubToken) {
107
+ // Check cache
108
+ const cached = copilotTokenCache.get(githubToken);
109
+ if (cached && cached.expiresAt > Date.now() + 60000) {
110
+ // Return cached token if it has > 1 minute left
111
+ return cached.token;
112
+ }
113
+
114
+ const response = await fetch(this.config.tokenUrl, {
115
+ method: 'GET',
116
+ headers: {
117
+ 'Authorization': `Bearer ${githubToken}`,
118
+ 'User-Agent': 'commons-proxy/2.0.0',
119
+ 'Accept': 'application/json'
120
+ }
121
+ });
122
+
123
+ if (!response.ok) {
124
+ const text = await response.text();
125
+ throw new Error(`Failed to get Copilot token: ${response.status} ${text}`);
126
+ }
127
+
128
+ const data = await response.json();
129
+ const expiresAt = data.expires_at
130
+ ? new Date(data.expires_at * 1000).getTime()
131
+ : Date.now() + 30 * 60 * 1000; // Default 30 min
132
+
133
+ copilotTokenCache.set(githubToken, {
134
+ token: data.token,
135
+ expiresAt
136
+ });
137
+
138
+ return data.token;
139
+ }
140
+
141
+ /**
142
+ * Fetch quota information (Copilot doesn't expose usage API)
143
+ *
144
+ * @param {Object} account - Account object
145
+ * @param {string} token - Copilot API token
146
+ * @returns {Promise<Object>} Quota data
147
+ */
148
+ async getQuotas(account, token) {
149
+ // Copilot doesn't expose quota/usage API
150
+ // Return default models with full availability
151
+ const defaultModels = [
152
+ 'gpt-4o', 'gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo',
153
+ 'claude-3.5-sonnet', 'o1-preview', 'o1-mini'
154
+ ];
155
+
156
+ const models = {};
157
+ defaultModels.forEach(modelId => {
158
+ models[modelId] = {
159
+ remainingFraction: 1.0,
160
+ resetTime: null
161
+ };
162
+ });
163
+
164
+ return { models };
165
+ }
166
+
167
+ /**
168
+ * Get subscription tier
169
+ *
170
+ * @param {Object} account - Account object
171
+ * @param {string} token - Copilot API token
172
+ * @returns {Promise<{tier: string, projectId: string|null}>}
173
+ */
174
+ async getSubscriptionTier(account, token) {
175
+ try {
176
+ // Use the stored GitHub token (apiKey), not the Copilot token
177
+ const response = await fetch('https://api.github.com/user', {
178
+ method: 'GET',
179
+ headers: {
180
+ 'Authorization': `Bearer ${account.apiKey}`,
181
+ 'Accept': 'application/vnd.github+json',
182
+ 'User-Agent': 'commons-proxy/2.0.0',
183
+ 'X-GitHub-Api-Version': '2022-11-28'
184
+ }
185
+ });
186
+
187
+ if (!response.ok) {
188
+ return { tier: 'copilot', projectId: null };
189
+ }
190
+
191
+ const userData = await response.json();
192
+ return {
193
+ tier: 'copilot',
194
+ projectId: userData.login
195
+ };
196
+ } catch (error) {
197
+ this.error('Failed to fetch subscription tier', error);
198
+ return { tier: 'copilot', projectId: null };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Get available models from Copilot
204
+ *
205
+ * @param {Object} account - Account object
206
+ * @param {string} token - Copilot API token
207
+ * @returns {Promise<Array>} List of available models
208
+ */
209
+ async getAvailableModels(account, token) {
210
+ return [
211
+ { id: 'gpt-4o', name: 'GPT-4o', family: 'gpt' },
212
+ { id: 'gpt-4', name: 'GPT-4', family: 'gpt' },
213
+ { id: 'gpt-4-turbo', name: 'GPT-4 Turbo', family: 'gpt' },
214
+ { id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', family: 'gpt' },
215
+ { id: 'claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', family: 'claude' },
216
+ { id: 'o1-preview', name: 'o1 Preview', family: 'o1' },
217
+ { id: 'o1-mini', name: 'o1 Mini', family: 'o1' }
218
+ ];
219
+ }
220
+
221
+ /**
222
+ * Parse rate limit headers from Copilot API
223
+ *
224
+ * @param {Response} response - Fetch response
225
+ * @param {Object} errorData - Error data from response body
226
+ * @returns {Object|null} Rate limit info
227
+ */
228
+ parseRateLimitInfo(response, errorData = null) {
229
+ const retryAfter = response.headers.get('retry-after');
230
+ const rateLimitReset = response.headers.get('x-ratelimit-reset');
231
+
232
+ if (retryAfter) {
233
+ const retrySeconds = parseInt(retryAfter, 10);
234
+ return {
235
+ resetTime: new Date(Date.now() + retrySeconds * 1000),
236
+ retryAfter: retrySeconds
237
+ };
238
+ }
239
+
240
+ if (rateLimitReset) {
241
+ const resetTimestamp = parseInt(rateLimitReset, 10);
242
+ return {
243
+ resetTime: new Date(resetTimestamp * 1000),
244
+ retryAfter: Math.max(0, Math.floor((resetTimestamp * 1000 - Date.now()) / 1000))
245
+ };
246
+ }
247
+
248
+ return null;
249
+ }
250
+
251
+ /**
252
+ * Check if error indicates invalid credentials
253
+ *
254
+ * @param {Error} error - Error object
255
+ * @returns {boolean}
256
+ */
257
+ shouldInvalidateCredentials(error) {
258
+ if (error.message && (
259
+ error.message.includes('Bad credentials') ||
260
+ error.message.includes('Requires authentication') ||
261
+ error.message.includes('Invalid token') ||
262
+ error.message.includes('Copilot access denied')
263
+ )) {
264
+ return true;
265
+ }
266
+
267
+ return super.shouldInvalidateCredentials(error);
268
+ }
269
+
270
+ // ============================================================================
271
+ // Static Device Auth Flow Methods (used by WebUI and CLI)
272
+ // ============================================================================
273
+
274
+ /**
275
+ * Initiate device authorization flow
276
+ * @returns {Promise<Object>} Device code response with verification_uri and user_code
277
+ */
278
+ static async initiateDeviceAuth() {
279
+ const response = await fetch(COPILOT_DEVICE_CODE_URL, {
280
+ method: 'POST',
281
+ headers: {
282
+ 'Accept': 'application/json',
283
+ 'Content-Type': 'application/json',
284
+ 'User-Agent': 'commons-proxy/2.0.0'
285
+ },
286
+ body: JSON.stringify({
287
+ client_id: COPILOT_CLIENT_ID,
288
+ scope: 'read:user'
289
+ })
290
+ });
291
+
292
+ if (!response.ok) {
293
+ const text = await response.text();
294
+ throw new Error(`Failed to initiate device authorization: ${response.status} ${text}`);
295
+ }
296
+
297
+ return response.json();
298
+ }
299
+
300
+ /**
301
+ * Poll for access token after user completes device auth
302
+ * @param {string} deviceCode - Device code from initiateDeviceAuth
303
+ * @param {number} interval - Polling interval in seconds
304
+ * @param {AbortSignal} [signal] - Optional abort signal
305
+ * @returns {Promise<Object>} { accessToken, tokenType }
306
+ */
307
+ static async pollForToken(deviceCode, interval = 5, signal = null) {
308
+ const maxAttempts = 60; // 5 minutes max
309
+
310
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
311
+ if (signal?.aborted) {
312
+ throw new Error('Device auth polling aborted');
313
+ }
314
+
315
+ await new Promise(resolve => setTimeout(resolve, interval * 1000));
316
+
317
+ const response = await fetch(COPILOT_ACCESS_TOKEN_URL, {
318
+ method: 'POST',
319
+ headers: {
320
+ 'Accept': 'application/json',
321
+ 'Content-Type': 'application/json',
322
+ 'User-Agent': 'commons-proxy/2.0.0'
323
+ },
324
+ body: JSON.stringify({
325
+ client_id: COPILOT_CLIENT_ID,
326
+ device_code: deviceCode,
327
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
328
+ })
329
+ });
330
+
331
+ if (!response.ok) {
332
+ continue;
333
+ }
334
+
335
+ const data = await response.json();
336
+
337
+ if (data.access_token) {
338
+ return {
339
+ accessToken: data.access_token,
340
+ tokenType: data.token_type || 'bearer'
341
+ };
342
+ }
343
+
344
+ if (data.error === 'authorization_pending') {
345
+ continue;
346
+ }
347
+
348
+ if (data.error === 'slow_down') {
349
+ interval += 5;
350
+ continue;
351
+ }
352
+
353
+ if (data.error) {
354
+ throw new Error(`OAuth error: ${data.error_description || data.error}`);
355
+ }
356
+ }
357
+
358
+ throw new Error('Authorization timed out after 5 minutes');
359
+ }
360
+
361
+ /**
362
+ * Get GitHub user info from access token
363
+ * @param {string} accessToken - GitHub access token
364
+ * @returns {Promise<Object>} User info { login, email, name }
365
+ */
366
+ static async getUserInfo(accessToken) {
367
+ const response = await fetch('https://api.github.com/user', {
368
+ method: 'GET',
369
+ headers: {
370
+ 'Authorization': `Bearer ${accessToken}`,
371
+ 'Accept': 'application/vnd.github+json',
372
+ 'User-Agent': 'commons-proxy/2.0.0',
373
+ 'X-GitHub-Api-Version': '2022-11-28'
374
+ }
375
+ });
376
+
377
+ if (!response.ok) {
378
+ throw new Error(`Failed to get user info: ${response.status}`);
379
+ }
380
+
381
+ const data = await response.json();
382
+ return {
383
+ login: data.login,
384
+ email: data.email || `${data.login}@github`,
385
+ name: data.name || data.login
386
+ };
387
+ }
388
+ }
389
+
390
+ // Export config constants for use by other modules
391
+ export const COPILOT_CONFIG = {
392
+ clientId: COPILOT_CLIENT_ID,
393
+ deviceCodeUrl: COPILOT_DEVICE_CODE_URL,
394
+ accessTokenUrl: COPILOT_ACCESS_TOKEN_URL,
395
+ apiUrl: COPILOT_API_URL,
396
+ tokenUrl: COPILOT_TOKEN_URL
397
+ };
398
+
399
+ export default CopilotProvider;