import type { DeviceAuthorizationRequest, DeviceAuthorizationResponse, DeviceTokenRequest, DeviceTokenError, OAuthTokenResponse, OAuthUserInfo, } from "./types.ts"; export class DeviceFlowClient { constructor( private authBaseUrl: string, private clientId: string, ) {} async requestDeviceAuthorization(scope?: string): Promise { const url = `${this.authBaseUrl}/oauth/device`; const request: DeviceAuthorizationRequest = { client_id: this.clientId, scope, }; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams(request as unknown as Record), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Device authorization request failed: ${response.status} - ${errorText}`); } return await response.json() as DeviceAuthorizationResponse; } async pollForToken( deviceCode: string, interval: number = 5, expiresIn: number, ): Promise { const url = `${this.authBaseUrl}/oauth/token`; const pollInterval = interval * 1000; // Convert to milliseconds const startTime = Date.now(); const timeout = expiresIn * 1000; // Convert to milliseconds const request: DeviceTokenRequest = { grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: deviceCode, client_id: this.clientId, }; while (true) { if (Date.now() - startTime > timeout) { throw new Error(`Device code expired after ${expiresIn} seconds`); } const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams(request as unknown as Record), }); if (response.ok) { return await response.json() as OAuthTokenResponse; } const errorResponse: DeviceTokenError = await response.json(); switch (errorResponse.error) { case "authorization_pending": // Continue polling break; case "slow_down": // Increase polling interval await new Promise(resolve => setTimeout(resolve, pollInterval * 2)); continue; case "expired_token": throw new Error("Device code has expired"); case "access_denied": throw new Error("User denied the authorization request"); case "server_error": { // Handle AIP-specific authorization pending errors const description = errorResponse.error_description || ""; if (description.includes("Authorization pending") || description.includes("Device code not yet authorized")) { // Continue polling break; } else { throw new Error(`Server error: ${description}`); } } default: throw new Error( `Token request failed: ${errorResponse.error} - ${errorResponse.error_description || ""}` ); } await new Promise(resolve => setTimeout(resolve, pollInterval)); } } async getUserInfo(accessToken: string): Promise { const url = `${this.authBaseUrl}/oauth/userinfo`; const response = await fetch(url, { headers: { "Authorization": `Bearer ${accessToken}`, }, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Failed to get user info: ${response.status} - ${errorText}`); } return await response.json(); } }