(deviceAuthId: string, userCode: string, interval: number)
| 238 | } |
| 239 | |
| 240 | export async function pollForToken(deviceAuthId: string, userCode: string, interval: number): Promise<TokenResponse> { |
| 241 | const url = issuerPath("/api/accounts/deviceauth/token") |
| 242 | const startTime = Date.now() |
| 243 | const maxWait = DEVICE_AUTH_TIMEOUT_MS |
| 244 | const pollIntervalMs = Math.max(interval, 5) * 1000 |
| 245 | |
| 246 | while (true) { |
| 247 | const response = await fetch(url, { |
| 248 | method: "POST", |
| 249 | headers: { |
| 250 | "Content-Type": "application/json", |
| 251 | }, |
| 252 | body: JSON.stringify({ |
| 253 | device_auth_id: deviceAuthId, |
| 254 | user_code: userCode, |
| 255 | }), |
| 256 | }) |
| 257 | |
| 258 | if (response.ok) { |
| 259 | const payload = (await response.json()) as DeviceAuthTokenResponse |
| 260 | return exchangeAuthorizationCodeForTokens(payload.authorization_code, payload.code_verifier) |
| 261 | } |
| 262 | |
| 263 | if (response.status === 403 || response.status === 404) { |
| 264 | if (Date.now() - startTime >= maxWait) { |
| 265 | throw new Error("Device auth timed out after 15 minutes") |
| 266 | } |
| 267 | const remaining = maxWait - (Date.now() - startTime) |
| 268 | await Bun.sleep(Math.min(pollIntervalMs, remaining)) |
| 269 | continue |
| 270 | } |
| 271 | |
| 272 | const errorText = await response.text() |
| 273 | throw new Error(`Device auth failed with status ${response.status} - ${errorText}`) |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | async function exchangeAuthorizationCodeForTokens(code: string, codeVerifier: string): Promise<TokenResponse> { |
| 278 | const url = issuerPath("/oauth/token") |
no test coverage detected