forked from devartifex/copilot-unleashed
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub.ts
More file actions
86 lines (70 loc) · 2.95 KB
/
Copy pathgithub.ts
File metadata and controls
86 lines (70 loc) · 2.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import { config } from '../config.js';
const GITHUB_DEVICE_CODE_URL = 'https://github.com/login/device/code';
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
const GITHUB_API_URL = 'https://api.github.com';
const GITHUB_SCOPES = 'copilot read:user repo';
export interface DeviceCodeResponse {
device_code: string;
user_code: string;
verification_uri: string;
expires_in: number;
interval: number;
}
export type DevicePollStatus = 'pending' | 'slow_down' | 'authorized' | 'access_denied' | 'expired';
export async function requestDeviceCode(): Promise<DeviceCodeResponse> {
const res = await fetch(GITHUB_DEVICE_CODE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ client_id: config.github.clientId, scope: GITHUB_SCOPES }),
});
const data = (await res.json()) as Record<string, string | number>;
if (data.error) throw new Error((data.error_description as string) || (data.error as string));
return data as unknown as DeviceCodeResponse;
}
export async function pollForToken(
deviceCode: string
): Promise<{ status: DevicePollStatus; token?: string }> {
const res = await fetch(GITHUB_TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
client_id: config.github.clientId,
device_code: deviceCode,
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
}),
});
const data = (await res.json()) as Record<string, string>;
if (data.error === 'authorization_pending') return { status: 'pending' };
if (data.error === 'slow_down') return { status: 'slow_down' };
if (data.error === 'access_denied') return { status: 'access_denied' };
if (data.error === 'expired_token') return { status: 'expired' };
if (data.error) throw new Error(data.error_description || data.error);
return { status: 'authorized', token: data.access_token };
}
export type TokenValidationResult =
| { valid: true; user: { login: string; name: string } }
| { valid: false; reason: 'invalid_token' | 'api_error' };
export async function validateGitHubToken(
token: string
): Promise<TokenValidationResult> {
try {
const res = await fetch(`${GITHUB_API_URL}/user`, {
headers: { Authorization: `Bearer ${token}` },
});
if (res.status === 401) {
return { valid: false, reason: 'invalid_token' };
}
// 403 can mean rate-limiting, IP block, or scope issues — not necessarily
// a revoked token. Treat as transient so the caller doesn't nuke auth.
if (res.status === 403) {
return { valid: false, reason: 'api_error' };
}
if (!res.ok) {
return { valid: false, reason: 'api_error' };
}
const user = (await res.json()) as Record<string, string>;
return { valid: true, user: { login: user.login, name: user.name || user.login } };
} catch {
return { valid: false, reason: 'api_error' };
}
}