forked from ericc-ch/copilot-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtoken.ts
More file actions
128 lines (108 loc) · 4.12 KB
/
token.ts
File metadata and controls
128 lines (108 loc) · 4.12 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
import consola from "consola"
import fs from "node:fs/promises"
import { PATHS } from "~/lib/paths"
import { getCopilotToken } from "~/services/github/get-copilot-token"
import { getDeviceCode } from "~/services/github/get-device-code"
import { getGitHubUser } from "~/services/github/get-user"
import { pollAccessToken } from "~/services/github/poll-access-token"
import { HTTPError } from "./error"
import { state } from "./state"
const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8")
const writeGithubToken = (token: string) =>
fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token)
/**
* Handle for the Copilot-token refresh timer. Stored module-level so the
* shutdown hook in start.ts can stop it cleanly, AND so a re-entry into
* setupCopilotToken (e.g. from tests) doesn't leak a previous interval.
*/
let copilotTokenRefreshTimer: ReturnType<typeof setInterval> | undefined
/** Cancel handle for stopCopilotTokenRefresh(). */
export function stopCopilotTokenRefresh(): void {
if (copilotTokenRefreshTimer !== undefined) {
clearInterval(copilotTokenRefreshTimer)
copilotTokenRefreshTimer = undefined
}
}
export const setupCopilotToken = async () => {
const { token, refresh_in } = await getCopilotToken()
state.copilotToken = token
// Display the Copilot token to the screen
consola.debug("GitHub Copilot Token fetched successfully!")
if (state.showToken) {
consola.info("Copilot token:", token)
}
// Defence in depth: if setupCopilotToken is called twice (test re-init,
// future hot-reauth path, etc.) we MUST drop the previous interval or
// we'd quietly stack refresh timers that all hit the GitHub token
// endpoint in parallel.
stopCopilotTokenRefresh()
const refreshInterval = (refresh_in - 60) * 1000
copilotTokenRefreshTimer = setInterval(() => {
consola.debug("Refreshing Copilot token")
// CRITICAL: do NOT `throw` from inside a setInterval async callback —
// there is no one to await the resulting rejected Promise, so it lands
// as an unhandledRejection. Bun will, in strict-mode / user-configured
// unhandledRejection handlers, crash the entire server. A refresh
// failure here is recoverable: state.copilotToken keeps its current
// value until it actually expires, at which point the next request
// surfaces a 401 → operator re-runs auth. Losing the *next* refresh
// attempt is strictly less bad than killing the process.
void (async () => {
try {
const { token: refreshed } = await getCopilotToken()
state.copilotToken = refreshed
consola.debug("Copilot token refreshed")
if (state.showToken) {
consola.info("Refreshed Copilot token:", refreshed)
}
} catch (error) {
consola.error(
"Failed to refresh Copilot token (continuing with existing token until next attempt):",
error,
)
}
})()
}, refreshInterval)
}
interface SetupGitHubTokenOptions {
force?: boolean
}
export async function setupGitHubToken(
options?: SetupGitHubTokenOptions,
): Promise<void> {
try {
const githubToken = await readGithubToken()
if (githubToken && !options?.force) {
state.githubToken = githubToken
if (state.showToken) {
consola.info("GitHub token:", githubToken)
}
await logUser()
return
}
consola.info("Not logged in, getting new access token")
const response = await getDeviceCode()
consola.debug("Device code response:", response)
consola.info(
`Please enter the code "${response.user_code}" in ${response.verification_uri}`,
)
const token = await pollAccessToken(response)
await writeGithubToken(token)
state.githubToken = token
if (state.showToken) {
consola.info("GitHub token:", token)
}
await logUser()
} catch (error) {
if (error instanceof HTTPError) {
consola.error("Failed to get GitHub token:", await error.response.json())
throw error
}
consola.error("Failed to get GitHub token:", error)
throw error
}
}
async function logUser() {
const user = await getGitHubUser()
consola.info(`Logged in as ${user.login}`)
}