n8n-nodes-github-copilot 4.3.0 → 4.4.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.
- package/dist/nodes/GitHubCopilotChatAPI/GitHubCopilotChatAPI.node.d.ts +1 -0
- package/dist/nodes/GitHubCopilotChatAPI/GitHubCopilotChatAPI.node.js +47 -15
- package/dist/nodes/GitHubCopilotChatAPI/nodeProperties.js +37 -0
- package/dist/nodes/GitHubCopilotChatModel/GitHubCopilotChatModel.node.d.ts +1 -0
- package/dist/nodes/GitHubCopilotChatModel/GitHubCopilotChatModel.node.js +174 -10
- package/dist/nodes/GitHubCopilotOpenAI/GitHubCopilotOpenAI.node.d.ts +1 -0
- package/dist/nodes/GitHubCopilotOpenAI/GitHubCopilotOpenAI.node.js +65 -4
- package/dist/nodes/GitHubCopilotOpenAI/nodeProperties.js +64 -27
- package/dist/package.json +3 -4
- package/dist/shared/models/DynamicModelLoader.d.ts +1 -0
- package/dist/shared/models/DynamicModelLoader.js +12 -0
- package/dist/shared/models/GitHubCopilotModels.d.ts +14 -8
- package/dist/shared/models/GitHubCopilotModels.js +255 -74
- package/dist/shared/utils/DynamicModelsManager.d.ts +11 -0
- package/dist/shared/utils/DynamicModelsManager.js +50 -0
- package/dist/shared/utils/GitHubCopilotApiUtils.d.ts +1 -0
- package/dist/shared/utils/GitHubCopilotApiUtils.js +85 -6
- package/package.json +3 -4
- package/shared/icons/copilot.svg +0 -34
- package/shared/index.ts +0 -27
- package/shared/models/DynamicModelLoader.ts +0 -124
- package/shared/models/GitHubCopilotModels.ts +0 -420
- package/shared/models/ModelVersionRequirements.ts +0 -165
- package/shared/properties/ModelProperties.ts +0 -52
- package/shared/properties/ModelSelectionProperty.ts +0 -68
- package/shared/utils/DynamicModelsManager.ts +0 -355
- package/shared/utils/EmbeddingsApiUtils.ts +0 -135
- package/shared/utils/FileChunkingApiUtils.ts +0 -176
- package/shared/utils/FileOptimizationUtils.ts +0 -210
- package/shared/utils/GitHubCopilotApiUtils.ts +0 -407
- package/shared/utils/GitHubCopilotEndpoints.ts +0 -212
- package/shared/utils/GitHubDeviceFlowHandler.ts +0 -276
- package/shared/utils/OAuthTokenManager.ts +0 -196
- package/shared/utils/provider-injection.ts +0 -277
- package/shared/utils/version-detection.ts +0 -145
|
@@ -1,212 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,276 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
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
|
-
}
|