(
discovery: CliLoginDiscovery,
grant: DeviceCodeGrant,
options: { readonly now?: () => number } = {},
)
| 218 | * `access_denied` / `expired_token` are terminal). |
| 219 | */ |
| 220 | export const pollForDeviceTokens = async ( |
| 221 | discovery: CliLoginDiscovery, |
| 222 | grant: DeviceCodeGrant, |
| 223 | options: { readonly now?: () => number } = {}, |
| 224 | ): Promise<DeviceTokens> => { |
| 225 | const now = options.now ?? (() => Date.now()); |
| 226 | const deadline = now() + grant.expiresInSeconds * 1000; |
| 227 | let intervalMs = Math.max(1, grant.intervalSeconds) * 1000; |
| 228 | |
| 229 | for (;;) { |
| 230 | if (now() >= deadline) { |
| 231 | throw new DeviceLoginError("Login timed out before it was approved."); |
| 232 | } |
| 233 | await sleep(intervalMs); |
| 234 | |
| 235 | const response = await post( |
| 236 | discovery.tokenEndpoint, |
| 237 | { |
| 238 | grant_type: DEVICE_CODE_GRANT_TYPE, |
| 239 | device_code: grant.deviceCode, |
| 240 | client_id: discovery.clientId, |
| 241 | }, |
| 242 | discovery.requestFormat, |
| 243 | ); |
| 244 | const body = await readJson(response); |
| 245 | |
| 246 | if (response.ok) { |
| 247 | const accessToken = asString(body.access_token); |
| 248 | if (!accessToken) { |
| 249 | throw new DeviceLoginError("Token response was missing an access token."); |
| 250 | } |
| 251 | const claims = decodeAccessTokenClaims(accessToken); |
| 252 | return { |
| 253 | accessToken, |
| 254 | refreshToken: asString(body.refresh_token), |
| 255 | expiresAt: deriveExpiresAt({ accessToken, expiresIn: asNumber(body.expires_in) }), |
| 256 | email: |
| 257 | readUserEmail(body) ?? (typeof claims?.email === "string" ? claims.email : undefined), |
| 258 | organizationId: |
| 259 | asString(body.organization_id) ?? |
| 260 | (typeof claims?.org_id === "string" ? claims.org_id : undefined), |
| 261 | }; |
| 262 | } |
| 263 | |
| 264 | const error = asString(body.error); |
| 265 | if (error === "authorization_pending") continue; |
| 266 | if (error === "slow_down") { |
| 267 | intervalMs += 5000; |
| 268 | continue; |
| 269 | } |
| 270 | if (error === "access_denied") { |
| 271 | throw new DeviceLoginError("Login was denied."); |
| 272 | } |
| 273 | if (error === "expired_token") { |
| 274 | throw new DeviceLoginError("The login request expired before it was approved."); |
| 275 | } |
| 276 | throw new DeviceLoginError( |
| 277 | `Login failed: ${asString(body.error_description) ?? error ?? `HTTP ${response.status}`}`, |
no test coverage detected