-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub-oauth.ts
More file actions
129 lines (114 loc) · 3.61 KB
/
Copy pathgithub-oauth.ts
File metadata and controls
129 lines (114 loc) · 3.61 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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import type { GitHubTokenResponse, GitHubUser } from './types.js'
export const GITHUB_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize'
export const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'
export const GITHUB_API_BASE = 'https://api.github.com'
export function buildAuthorizeUrl(
clientId: string,
redirectUri: string,
state: string,
scopes = 'read:user user:email',
): string {
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
scope: scopes,
state,
})
return `${GITHUB_AUTHORIZE_URL}?${params}`
}
export async function exchangeCodeForToken(
code: string,
clientId: string,
clientSecret: string,
redirectUri: string,
): Promise<GitHubTokenResponse> {
const res = await fetch(GITHUB_TOKEN_URL, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
code,
redirect_uri: redirectUri,
}),
})
if (!res.ok) {
throw new Error(`GitHub token exchange failed: HTTP ${res.status}`)
}
const data = await res.json() as GitHubTokenResponse
if (data.error) {
throw new Error(`GitHub OAuth error: ${data.error} — ${data.error_description ?? ''}`)
}
return data
}
export async function refreshGitHubToken(
refreshToken: string,
clientId: string,
clientSecret: string,
): Promise<GitHubTokenResponse> {
const res = await fetch(GITHUB_TOKEN_URL, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
})
if (!res.ok) {
throw new Error(`GitHub token refresh failed: HTTP ${res.status}`)
}
const data = await res.json() as GitHubTokenResponse
if (data.error) {
throw new Error(`GitHub token refresh error: ${data.error} — ${data.error_description ?? ''}`)
}
return data
}
export async function fetchGitHubUser(accessToken: string): Promise<GitHubUser> {
const res = await fetch(`${GITHUB_API_BASE}/user`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
})
if (res.status === 401) {
throw new Error('GitHub token is invalid or has been revoked')
}
if (!res.ok) {
throw new Error(`Failed to fetch GitHub user: HTTP ${res.status}`)
}
return res.json() as Promise<GitHubUser>
}
/**
* Deletes the token via the GitHub Applications API so it can no longer be used.
* Uses HTTP Basic auth with the OAuth app credentials (not a Bearer token).
*/
export async function revokeGitHubToken(
accessToken: string,
clientId: string,
clientSecret: string,
): Promise<void> {
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
const res = await fetch(`${GITHUB_API_BASE}/applications/${clientId}/token`, {
method: 'DELETE',
headers: {
Authorization: `Basic ${credentials}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'Content-Type': 'application/json',
},
body: JSON.stringify({ access_token: accessToken }),
})
// 404 means the token was already invalid — acceptable outcome
if (!res.ok && res.status !== 404) {
throw new Error(`Failed to revoke GitHub token: HTTP ${res.status}`)
}
}