forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
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}