n8n-nodes-github-copilot 4.1.2 → 4.2.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.
@@ -0,0 +1,212 @@
1
+ /**
2
+ * GitHub Copilot API Endpoints
3
+ * Centralized endpoint management for all nodes
4
+ */
5
+
6
+ export const GITHUB_COPILOT_API = {
7
+ // Base URLs
8
+ BASE_URL: "https://api.githubcopilot.com",
9
+ GITHUB_BASE_URL: "https://api.github.com",
10
+
11
+ // Endpoints
12
+ ENDPOINTS: {
13
+ // GitHub Copilot API
14
+ MODELS: "/models",
15
+ CHAT_COMPLETIONS: "/chat/completions",
16
+ EMBEDDINGS: "/embeddings",
17
+
18
+ // GitHub API (for billing and organization info)
19
+ ORG_BILLING: (org: string) => `/orgs/${org}/copilot/billing`,
20
+ ORG_SEATS: (org: string) => `/orgs/${org}/copilot/billing/seats`,
21
+ USER_COPILOT: "/user/copilot_access",
22
+ },
23
+
24
+ // Full URLs (convenience methods)
25
+ URLS: {
26
+ // GitHub Copilot API URLs
27
+ MODELS: "https://api.githubcopilot.com/models",
28
+ CHAT_COMPLETIONS: "https://api.githubcopilot.com/chat/completions",
29
+ EMBEDDINGS: "https://api.githubcopilot.com/embeddings",
30
+
31
+ // GitHub API URLs
32
+ ORG_BILLING: (org: string) => `https://api.github.com/orgs/${org}/copilot/billing`,
33
+ ORG_SEATS: (org: string) => `https://api.github.com/orgs/${org}/copilot/billing/seats`,
34
+ USER_COPILOT: "https://api.github.com/user/copilot_access",
35
+ },
36
+
37
+ // Headers
38
+ HEADERS: {
39
+ DEFAULT: {
40
+ "Accept": "application/json",
41
+ "Content-Type": "application/json",
42
+ },
43
+ WITH_AUTH: (token: string) => ({
44
+ "Authorization": `Bearer ${token}`,
45
+ "Accept": "application/json",
46
+ "Content-Type": "application/json",
47
+ }),
48
+ // VS Code specific headers (if needed)
49
+ VSCODE_CLIENT: {
50
+ "User-Agent": "VSCode-Copilot",
51
+ "X-GitHub-Api-Version": "2022-11-28",
52
+ },
53
+ },
54
+
55
+ // Rate Limiting & Retry
56
+ RATE_LIMITS: {
57
+ TPM_RETRY_DELAY_BASE: 1000, // Base delay for TPM quota retry (ms)
58
+ TPM_RETRY_MAX_DELAY: 10000, // Maximum delay for TPM quota retry (ms)
59
+ DEFAULT_MAX_RETRIES: 3,
60
+ EXPONENTIAL_BACKOFF_FACTOR: 2,
61
+ },
62
+
63
+ // HTTP Status Codes
64
+ STATUS_CODES: {
65
+ OK: 200,
66
+ UNAUTHORIZED: 401,
67
+ FORBIDDEN: 403,
68
+ NOT_FOUND: 404,
69
+ TOO_MANY_REQUESTS: 429,
70
+ INTERNAL_SERVER_ERROR: 500,
71
+ },
72
+
73
+ // Common Error Messages
74
+ ERRORS: {
75
+ INVALID_TOKEN: "Invalid token format. GitHub Copilot API requires tokens starting with \"gho_\"",
76
+ CREDENTIALS_REQUIRED: "GitHub Copilot API credentials are required",
77
+ TPM_QUOTA_EXCEEDED: "TPM (Transactions Per Minute) quota exceeded",
78
+ API_ERROR: (status: number, message: string) => `API Error ${status}: ${message}`,
79
+ },
80
+ } as const;
81
+
82
+ // Type definitions for better TypeScript support
83
+ export type GitHubCopilotEndpoint = keyof typeof GITHUB_COPILOT_API.ENDPOINTS;
84
+ export type GitHubCopilotUrl = keyof typeof GITHUB_COPILOT_API.URLS;
85
+ export type GitHubCopilotStatusCode = typeof GITHUB_COPILOT_API.STATUS_CODES[keyof typeof GITHUB_COPILOT_API.STATUS_CODES];
86
+
87
+ // Helper functions
88
+ export class GitHubCopilotEndpoints {
89
+ /**
90
+ * Get full URL for models endpoint
91
+ */
92
+ static getModelsUrl(): string {
93
+ return GITHUB_COPILOT_API.URLS.MODELS;
94
+ }
95
+
96
+ /**
97
+ * Get full URL for chat completions endpoint
98
+ */
99
+ static getChatCompletionsUrl(): string {
100
+ return GITHUB_COPILOT_API.URLS.CHAT_COMPLETIONS;
101
+ }
102
+
103
+ /**
104
+ * Get full URL for embeddings endpoint
105
+ */
106
+ static getEmbeddingsUrl(): string {
107
+ return GITHUB_COPILOT_API.URLS.EMBEDDINGS;
108
+ }
109
+
110
+ /**
111
+ * Get GitHub API URL for organization billing
112
+ */
113
+ static getOrgBillingUrl(org: string): string {
114
+ return GITHUB_COPILOT_API.URLS.ORG_BILLING(org);
115
+ }
116
+
117
+ /**
118
+ * Get GitHub API URL for organization seats
119
+ */
120
+ static getOrgSeatsUrl(org: string): string {
121
+ return GITHUB_COPILOT_API.URLS.ORG_SEATS(org);
122
+ }
123
+
124
+ /**
125
+ * Get GitHub API URL for user copilot access
126
+ */
127
+ static getUserCopilotUrl(): string {
128
+ return GITHUB_COPILOT_API.URLS.USER_COPILOT;
129
+ }
130
+
131
+ /**
132
+ * Get headers with authentication
133
+ * CRITICAL: Includes required headers for premium models (Claude, Gemini, GPT-5, etc.)
134
+ * Source: microsoft/vscode-copilot-chat networking.ts
135
+ */
136
+ static getAuthHeaders(token: string, includeVSCodeHeaders = false): Record<string, string> {
137
+ const headers: Record<string, string> = {
138
+ ...GITHUB_COPILOT_API.HEADERS.WITH_AUTH(token),
139
+ // CRITICAL: These headers are required for premium models
140
+ "User-Agent": "GitHubCopilotChat/0.35.0",
141
+ "Editor-Version": "vscode/1.96.0",
142
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
143
+ "X-GitHub-Api-Version": "2025-05-01",
144
+ "X-Interaction-Type": "copilot-chat",
145
+ "OpenAI-Intent": "conversation-panel",
146
+ "Copilot-Integration-Id": "vscode-chat",
147
+ };
148
+
149
+ if (includeVSCodeHeaders) {
150
+ return {
151
+ ...headers,
152
+ ...GITHUB_COPILOT_API.HEADERS.VSCODE_CLIENT,
153
+ };
154
+ }
155
+
156
+ return headers;
157
+ }
158
+
159
+ /**
160
+ * Get headers for embeddings endpoint (requires additional headers)
161
+ */
162
+ static getEmbeddingsHeaders(token: string): Record<string, string> {
163
+ // Generate unique session ID (UUID + timestamp)
164
+ const sessionId = `${this.generateUUID()}-${Date.now()}`;
165
+
166
+ return {
167
+ "Authorization": `Bearer ${token}`,
168
+ "Content-Type": "application/json",
169
+ "Accept": "application/json",
170
+ "Editor-Version": "vscode/1.95.0",
171
+ "Editor-Plugin-Version": "copilot/1.0.0",
172
+ "User-Agent": "GitHub-Copilot/1.0 (n8n-node)",
173
+ "Vscode-Sessionid": sessionId,
174
+ "X-GitHub-Api-Version": "2025-08-20",
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Generate a simple UUID v4
180
+ */
181
+ private static generateUUID(): string {
182
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
183
+ const r = (Math.random() * 16) | 0;
184
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
185
+ return v.toString(16);
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Calculate retry delay with exponential backoff
191
+ */
192
+ static getRetryDelay(attempt: number): number {
193
+ const delay = GITHUB_COPILOT_API.RATE_LIMITS.TPM_RETRY_DELAY_BASE *
194
+ Math.pow(GITHUB_COPILOT_API.RATE_LIMITS.EXPONENTIAL_BACKOFF_FACTOR, attempt - 1);
195
+
196
+ return Math.min(delay, GITHUB_COPILOT_API.RATE_LIMITS.TPM_RETRY_MAX_DELAY);
197
+ }
198
+
199
+ /**
200
+ * Check if status code indicates TPM quota exceeded
201
+ */
202
+ static isTpmQuotaError(statusCode: number): boolean {
203
+ return statusCode === GITHUB_COPILOT_API.STATUS_CODES.FORBIDDEN;
204
+ }
205
+
206
+ /**
207
+ * Validate token format
208
+ */
209
+ static validateToken(token: string): boolean {
210
+ return typeof token === "string" && token.startsWith("gho_");
211
+ }
212
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * GitHub OAuth Device Flow Handler
3
+ * Implements the complete OAuth Device Code Flow for n8n credentials
4
+ *
5
+ * This handler is called when the user clicks "Iniciar Device Flow" button
6
+ * in the GitHubCopilotDeviceFlow credentials interface
7
+ */
8
+
9
+ interface DeviceCodeResponse {
10
+ device_code: string;
11
+ user_code: string;
12
+ verification_uri: string;
13
+ verification_uri_complete?: string;
14
+ expires_in: number;
15
+ interval: number;
16
+ }
17
+
18
+ interface AccessTokenResponse {
19
+ access_token?: string;
20
+ token_type?: string;
21
+ scope?: string;
22
+ error?: string;
23
+ error_description?: string;
24
+ }
25
+
26
+ interface CopilotTokenResponse {
27
+ token: string;
28
+ expires_at: number;
29
+ refresh_in: number;
30
+ sku?: string;
31
+ chat_enabled?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Step 1: Request Device Code from GitHub
36
+ */
37
+ export async function requestDeviceCode(
38
+ clientId: string,
39
+ scopes: string,
40
+ deviceCodeUrl: string
41
+ ): Promise<DeviceCodeResponse> {
42
+ const response = await fetch(deviceCodeUrl, {
43
+ method: "POST",
44
+ headers: {
45
+ "Accept": "application/json",
46
+ "Content-Type": "application/x-www-form-urlencoded",
47
+ },
48
+ body: new URLSearchParams({
49
+ client_id: clientId,
50
+ scope: scopes,
51
+ }),
52
+ });
53
+
54
+ if (!response.ok) {
55
+ throw new Error(`Failed to request device code: ${response.status} ${response.statusText}`);
56
+ }
57
+
58
+ return (await response.json()) as DeviceCodeResponse;
59
+ }
60
+
61
+ /**
62
+ * Step 2: Poll for Access Token
63
+ * Implements exponential backoff and handles all error cases
64
+ */
65
+ export async function pollForAccessToken(
66
+ clientId: string,
67
+ deviceCode: string,
68
+ accessTokenUrl: string,
69
+ interval: number = 5,
70
+ maxAttempts: number = 180 // 15 minutes with 5s interval
71
+ ): Promise<string> {
72
+ let currentInterval = interval * 1000; // Convert to milliseconds
73
+
74
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
75
+ await sleep(currentInterval);
76
+
77
+ const response = await fetch(accessTokenUrl, {
78
+ method: "POST",
79
+ headers: {
80
+ "Accept": "application/json",
81
+ "Content-Type": "application/x-www-form-urlencoded",
82
+ },
83
+ body: new URLSearchParams({
84
+ client_id: clientId,
85
+ device_code: deviceCode,
86
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
87
+ }),
88
+ });
89
+
90
+ const data = (await response.json()) as AccessTokenResponse;
91
+
92
+ // Success case
93
+ if (data.access_token) {
94
+ return data.access_token;
95
+ }
96
+
97
+ // Error handling based on GitHub OAuth Device Flow specification
98
+ if (data.error === "authorization_pending") {
99
+ // User hasn't authorized yet, continue polling
100
+ console.log(`[Device Flow] Attempt ${attempt}/${maxAttempts}: Waiting for authorization...`);
101
+ continue;
102
+ }
103
+
104
+ if (data.error === "slow_down") {
105
+ // Rate limit hit, increase interval by 5 seconds
106
+ currentInterval += 5000;
107
+ console.log(`[Device Flow] Rate limited, increasing interval to ${currentInterval / 1000}s`);
108
+ continue;
109
+ }
110
+
111
+ if (data.error === "expired_token") {
112
+ throw new Error("Device code expired. Please start the Device Flow again.");
113
+ }
114
+
115
+ if (data.error === "access_denied") {
116
+ throw new Error("User denied authorization.");
117
+ }
118
+
119
+ // Unknown error
120
+ throw new Error(`OAuth error: ${data.error} - ${data.error_description || "Unknown error"}`);
121
+ }
122
+
123
+ throw new Error("Device Flow timeout. Authorization took too long.");
124
+ }
125
+
126
+ /**
127
+ * Step 3: Convert GitHub OAuth Token to GitHub Copilot Token (Optional)
128
+ * The GitHub OAuth token (gho_*) already works with Copilot API,
129
+ * but this conversion provides additional metadata
130
+ */
131
+ export async function convertToCopilotToken(
132
+ githubToken: string,
133
+ copilotTokenUrl: string
134
+ ): Promise<CopilotTokenResponse> {
135
+ const response = await fetch(copilotTokenUrl, {
136
+ method: "GET",
137
+ headers: {
138
+ "Authorization": `token ${githubToken}`,
139
+ "Accept": "application/vnd.github+json",
140
+ "X-GitHub-Api-Version": "2025-04-01",
141
+ "User-Agent": "GitHub-Copilot-Chat/1.0.0 VSCode/1.85.0",
142
+ "Editor-Version": "vscode/1.85.0",
143
+ "Editor-Plugin-Version": "copilot-chat/0.12.0",
144
+ },
145
+ });
146
+
147
+ if (!response.ok) {
148
+ // If conversion fails, return the GitHub token as-is
149
+ // It still works with Copilot API
150
+ console.warn(`[Device Flow] Failed to convert to Copilot token: ${response.status}`);
151
+ return {
152
+ token: githubToken,
153
+ expires_at: Date.now() + (8 * 60 * 60 * 1000), // 8 hours
154
+ refresh_in: 8 * 60 * 60, // 8 hours in seconds
155
+ };
156
+ }
157
+
158
+ const data = (await response.json()) as CopilotTokenResponse;
159
+ return data;
160
+ }
161
+
162
+ /**
163
+ * Complete Device Flow Process
164
+ * Orchestrates all steps and returns the final token
165
+ */
166
+ export async function executeDeviceFlow(
167
+ clientId: string,
168
+ scopes: string,
169
+ deviceCodeUrl: string,
170
+ accessTokenUrl: string,
171
+ copilotTokenUrl: string,
172
+ onProgress?: (status: DeviceFlowStatus) => void
173
+ ): Promise<DeviceFlowResult> {
174
+ try {
175
+ // Step 1: Request device code
176
+ onProgress?.({
177
+ step: 1,
178
+ status: "requesting_device_code",
179
+ message: "Solicitando device code do GitHub...",
180
+ });
181
+
182
+ const deviceData = await requestDeviceCode(clientId, scopes, deviceCodeUrl);
183
+
184
+ onProgress?.({
185
+ step: 2,
186
+ status: "awaiting_authorization",
187
+ message: "Aguardando sua autorização no GitHub...",
188
+ deviceData: {
189
+ userCode: deviceData.user_code,
190
+ verificationUri: deviceData.verification_uri,
191
+ verificationUriComplete: deviceData.verification_uri_complete,
192
+ expiresIn: deviceData.expires_in,
193
+ },
194
+ });
195
+
196
+ // Step 2: Poll for access token
197
+ const accessToken = await pollForAccessToken(
198
+ clientId,
199
+ deviceData.device_code,
200
+ accessTokenUrl,
201
+ deviceData.interval
202
+ );
203
+
204
+ onProgress?.({
205
+ step: 3,
206
+ status: "token_obtained",
207
+ message: "Token GitHub OAuth obtido! Convertendo para token Copilot...",
208
+ });
209
+
210
+ // Step 3: Convert to Copilot token (optional, but provides metadata)
211
+ const copilotData = await convertToCopilotToken(accessToken, copilotTokenUrl);
212
+
213
+ onProgress?.({
214
+ step: 4,
215
+ status: "complete",
216
+ message: "✅ Autenticação completa! Token salvo com sucesso.",
217
+ });
218
+
219
+ return {
220
+ success: true,
221
+ accessToken: copilotData.token || accessToken,
222
+ expiresAt: new Date(copilotData.expires_at),
223
+ metadata: {
224
+ sku: copilotData.sku,
225
+ chatEnabled: copilotData.chat_enabled,
226
+ refreshIn: copilotData.refresh_in,
227
+ },
228
+ };
229
+ } catch (error) {
230
+ onProgress?.({
231
+ step: -1,
232
+ status: "error",
233
+ message: `❌ Erro: ${error instanceof Error ? error.message : String(error)}`,
234
+ });
235
+
236
+ return {
237
+ success: false,
238
+ error: error instanceof Error ? error.message : String(error),
239
+ };
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Utility: Sleep function for polling
245
+ */
246
+ function sleep(ms: number): Promise<void> {
247
+ return new Promise(resolve => setTimeout(resolve, ms));
248
+ }
249
+
250
+ /**
251
+ * Type Definitions
252
+ */
253
+
254
+ export interface DeviceFlowStatus {
255
+ step: number;
256
+ status: "requesting_device_code" | "awaiting_authorization" | "token_obtained" | "complete" | "error";
257
+ message: string;
258
+ deviceData?: {
259
+ userCode: string;
260
+ verificationUri: string;
261
+ verificationUriComplete?: string;
262
+ expiresIn: number;
263
+ };
264
+ }
265
+
266
+ export interface DeviceFlowResult {
267
+ success: boolean;
268
+ accessToken?: string;
269
+ expiresAt?: Date;
270
+ metadata?: {
271
+ sku?: string;
272
+ chatEnabled?: boolean;
273
+ refreshIn?: number;
274
+ };
275
+ error?: string;
276
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * OAuth Token Manager for GitHub Copilot
3
+ *
4
+ * Automatically generates and caches OAuth tokens from GitHub tokens (gho_*)
5
+ * Tokens are cached in memory and auto-renewed before expiration
6
+ */
7
+
8
+ import crypto from 'crypto';
9
+ import https from 'https';
10
+
11
+ interface OAuthTokenCache {
12
+ token: string;
13
+ expiresAt: number;
14
+ generatedAt: number;
15
+ refreshIn: number;
16
+ }
17
+
18
+ interface OAuthResponse {
19
+ token: string;
20
+ expires_at: number;
21
+ refresh_in: number;
22
+ chat_enabled: boolean;
23
+ sku: string;
24
+ endpoints: {
25
+ api: string;
26
+ proxy: string;
27
+ telemetry: string;
28
+ };
29
+ }
30
+
31
+ export class OAuthTokenManager {
32
+ private static tokenCache: Map<string, OAuthTokenCache> = new Map();
33
+ private static machineIdCache: Map<string, string> = new Map();
34
+
35
+ /**
36
+ * Get valid OAuth token (generates new if expired or near expiration)
37
+ * @param githubToken - GitHub CLI token (gho_*)
38
+ * @returns Valid OAuth token
39
+ */
40
+ static async getValidOAuthToken(githubToken: string): Promise<string> {
41
+ if (!githubToken || !githubToken.startsWith('gho_')) {
42
+ throw new Error('Invalid GitHub token. Must start with gho_');
43
+ }
44
+
45
+ const cacheKey = this.getCacheKey(githubToken);
46
+ const cached = this.tokenCache.get(cacheKey);
47
+
48
+ // Check if we have a valid cached token (with 2 minute buffer before expiration)
49
+ if (cached && cached.expiresAt > Date.now() + 120000) {
50
+ const remainingMinutes = Math.round((cached.expiresAt - Date.now()) / 1000 / 60);
51
+ console.log(`✅ Using cached OAuth token (${remainingMinutes} minutes remaining)`);
52
+ return cached.token;
53
+ }
54
+
55
+ // Generate new token
56
+ console.log('🔄 Generating new OAuth token...');
57
+ const newToken = await this.generateOAuthToken(githubToken);
58
+ return newToken;
59
+ }
60
+
61
+ /**
62
+ * Generate new OAuth token from GitHub token
63
+ */
64
+ private static async generateOAuthToken(githubToken: string): Promise<string> {
65
+ const machineId = this.getMachineId(githubToken);
66
+ const sessionId = this.generateSessionId();
67
+
68
+ return new Promise((resolve, reject) => {
69
+ const options = {
70
+ hostname: 'api.github.com',
71
+ path: '/copilot_internal/v2/token',
72
+ method: 'GET',
73
+ headers: {
74
+ 'Authorization': `token ${githubToken}`,
75
+ 'Vscode-Machineid': machineId,
76
+ 'Vscode-Sessionid': sessionId,
77
+ 'Editor-Version': 'vscode/1.105.1',
78
+ 'Editor-Plugin-Version': 'copilot-chat/0.32.3',
79
+ 'X-GitHub-Api-Version': '2025-08-20',
80
+ 'Accept': 'application/json',
81
+ 'User-Agent': 'n8n-nodes-copilot/1.0.0',
82
+ },
83
+ };
84
+
85
+ const req = https.request(options, (res) => {
86
+ let data = '';
87
+ res.on('data', (chunk) => (data += chunk));
88
+ res.on('end', () => {
89
+ if (res.statusCode === 200) {
90
+ try {
91
+ const response: OAuthResponse = JSON.parse(data);
92
+ const oauthToken = response.token;
93
+ const expiresAt = response.expires_at * 1000; // Convert to ms
94
+ const refreshIn = response.refresh_in;
95
+
96
+ // Cache the token
97
+ const cacheKey = this.getCacheKey(githubToken);
98
+ this.tokenCache.set(cacheKey, {
99
+ token: oauthToken,
100
+ expiresAt: expiresAt,
101
+ generatedAt: Date.now(),
102
+ refreshIn: refreshIn,
103
+ });
104
+
105
+ const expiresInMinutes = Math.round((expiresAt - Date.now()) / 1000 / 60);
106
+ console.log(
107
+ `✅ OAuth token generated successfully (expires in ${expiresInMinutes} minutes)`,
108
+ );
109
+ resolve(oauthToken);
110
+ } catch (error) {
111
+ reject(new Error(`Failed to parse OAuth token response: ${error}`));
112
+ }
113
+ } else {
114
+ reject(
115
+ new Error(
116
+ `Failed to generate OAuth token: ${res.statusCode} ${res.statusMessage}`,
117
+ ),
118
+ );
119
+ }
120
+ });
121
+ });
122
+
123
+ req.on('error', (error) => {
124
+ reject(new Error(`Network error generating OAuth token: ${error.message}`));
125
+ });
126
+
127
+ req.setTimeout(10000, () => {
128
+ req.destroy();
129
+ reject(new Error('OAuth token generation timeout'));
130
+ });
131
+
132
+ req.end();
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Get or generate persistent machine ID for a GitHub token
138
+ */
139
+ private static getMachineId(githubToken: string): string {
140
+ const cacheKey = this.getCacheKey(githubToken);
141
+
142
+ if (!this.machineIdCache.has(cacheKey)) {
143
+ const uuid = crypto.randomUUID();
144
+ const machineId = crypto.createHash('sha256').update(uuid).digest('hex');
145
+ this.machineIdCache.set(cacheKey, machineId);
146
+ console.log('🆔 Generated new machine ID');
147
+ }
148
+
149
+ return this.machineIdCache.get(cacheKey)!;
150
+ }
151
+
152
+ /**
153
+ * Generate session ID (unique for each token generation)
154
+ */
155
+ private static generateSessionId(): string {
156
+ return `${crypto.randomUUID()}${Date.now()}`;
157
+ }
158
+
159
+ /**
160
+ * Get cache key from GitHub token (first 20 chars)
161
+ */
162
+ private static getCacheKey(githubToken: string): string {
163
+ return githubToken.substring(0, 20);
164
+ }
165
+
166
+ /**
167
+ * Clear cache for specific GitHub token
168
+ */
169
+ static clearCache(githubToken: string): void {
170
+ const cacheKey = this.getCacheKey(githubToken);
171
+ this.tokenCache.delete(cacheKey);
172
+ this.machineIdCache.delete(cacheKey);
173
+ console.log('🗑️ OAuth token cache cleared');
174
+ }
175
+
176
+ /**
177
+ * Get cache info for debugging
178
+ */
179
+ static getCacheInfo(githubToken: string): OAuthTokenCache | null {
180
+ const cacheKey = this.getCacheKey(githubToken);
181
+ return this.tokenCache.get(cacheKey) || null;
182
+ }
183
+
184
+ /**
185
+ * Check if token needs refresh
186
+ */
187
+ static needsRefresh(githubToken: string, bufferMinutes: number = 2): boolean {
188
+ const cacheKey = this.getCacheKey(githubToken);
189
+ const cached = this.tokenCache.get(cacheKey);
190
+
191
+ if (!cached) return true;
192
+
193
+ const bufferMs = bufferMinutes * 60 * 1000;
194
+ return cached.expiresAt <= Date.now() + bufferMs;
195
+ }
196
+ }