Highly ambitious ATProtocol AppView service and sdks
at fix-postgres 124 lines 3.7 kB view raw
1import type { 2 DeviceAuthorizationRequest, 3 DeviceAuthorizationResponse, 4 DeviceTokenRequest, 5 DeviceTokenError, 6 OAuthTokenResponse, 7 OAuthUserInfo, 8} from "./types.ts"; 9 10export class DeviceFlowClient { 11 constructor( 12 private authBaseUrl: string, 13 private clientId: string, 14 ) {} 15 16 async requestDeviceAuthorization(scope?: string): Promise<DeviceAuthorizationResponse> { 17 const url = `${this.authBaseUrl}/oauth/device`; 18 19 const request: DeviceAuthorizationRequest = { 20 client_id: this.clientId, 21 scope, 22 }; 23 24 const response = await fetch(url, { 25 method: "POST", 26 headers: { 27 "Content-Type": "application/x-www-form-urlencoded", 28 }, 29 body: new URLSearchParams(request as unknown as Record<string, string>), 30 }); 31 32 if (!response.ok) { 33 const errorText = await response.text(); 34 throw new Error(`Device authorization request failed: ${response.status} - ${errorText}`); 35 } 36 37 return await response.json() as DeviceAuthorizationResponse; 38 } 39 40 async pollForToken( 41 deviceCode: string, 42 interval: number = 5, 43 expiresIn: number, 44 ): Promise<OAuthTokenResponse> { 45 const url = `${this.authBaseUrl}/oauth/token`; 46 const pollInterval = interval * 1000; // Convert to milliseconds 47 const startTime = Date.now(); 48 const timeout = expiresIn * 1000; // Convert to milliseconds 49 50 const request: DeviceTokenRequest = { 51 grant_type: "urn:ietf:params:oauth:grant-type:device_code", 52 device_code: deviceCode, 53 client_id: this.clientId, 54 }; 55 56 while (true) { 57 if (Date.now() - startTime > timeout) { 58 throw new Error(`Device code expired after ${expiresIn} seconds`); 59 } 60 61 const response = await fetch(url, { 62 method: "POST", 63 headers: { 64 "Content-Type": "application/x-www-form-urlencoded", 65 }, 66 body: new URLSearchParams(request as unknown as Record<string, string>), 67 }); 68 69 if (response.ok) { 70 return await response.json() as OAuthTokenResponse; 71 } 72 73 const errorResponse: DeviceTokenError = await response.json(); 74 75 switch (errorResponse.error) { 76 case "authorization_pending": 77 // Continue polling 78 break; 79 case "slow_down": 80 // Increase polling interval 81 await new Promise(resolve => setTimeout(resolve, pollInterval * 2)); 82 continue; 83 case "expired_token": 84 throw new Error("Device code has expired"); 85 case "access_denied": 86 throw new Error("User denied the authorization request"); 87 case "server_error": { 88 // Handle AIP-specific authorization pending errors 89 const description = errorResponse.error_description || ""; 90 if (description.includes("Authorization pending") || 91 description.includes("Device code not yet authorized")) { 92 // Continue polling 93 break; 94 } else { 95 throw new Error(`Server error: ${description}`); 96 } 97 } 98 default: 99 throw new Error( 100 `Token request failed: ${errorResponse.error} - ${errorResponse.error_description || ""}` 101 ); 102 } 103 104 await new Promise(resolve => setTimeout(resolve, pollInterval)); 105 } 106 } 107 108 async getUserInfo(accessToken: string): Promise<OAuthUserInfo> { 109 const url = `${this.authBaseUrl}/oauth/userinfo`; 110 111 const response = await fetch(url, { 112 headers: { 113 "Authorization": `Bearer ${accessToken}`, 114 }, 115 }); 116 117 if (!response.ok) { 118 const errorText = await response.text(); 119 throw new Error(`Failed to get user info: ${response.status} - ${errorText}`); 120 } 121 122 return await response.json(); 123 } 124}