| 148 | | { readonly kind: 'denied'; readonly description: string }; |
| 149 | |
| 150 | export async function pollDeviceToken( |
| 151 | config: OAuthFlowConfig, |
| 152 | deviceCode: string, |
| 153 | options: { readonly deviceHeaders?: DeviceHeaders | undefined }, |
| 154 | ): Promise<DevicePollResult> { |
| 155 | const url = `${config.oauthHost.replace(/\/$/, '')}/api/oauth/token`; |
| 156 | const { status, data } = await postForm( |
| 157 | url, |
| 158 | { |
| 159 | client_id: config.clientId, |
| 160 | device_code: deviceCode, |
| 161 | grant_type: 'urn:ietf:params:oauth:grant-type:device_code', |
| 162 | }, |
| 163 | options.deviceHeaders, |
| 164 | ); |
| 165 | |
| 166 | if (status === 200 && typeof data['access_token'] === 'string') { |
| 167 | return { kind: 'success', token: tokenFromResponse(data) }; |
| 168 | } |
| 169 | |
| 170 | if (status >= 500) { |
| 171 | throw new OAuthError( |
| 172 | `Device token polling server error (HTTP ${status}): ${pickErrorDetail(data)}`, |
| 173 | ); |
| 174 | } |
| 175 | |
| 176 | const errorCode = typeof data['error'] === 'string' ? data['error'] : 'unknown_error'; |
| 177 | const detail = extractApiErrorMessage(data); |
| 178 | const description = |
| 179 | typeof data['error_description'] === 'string' ? data['error_description'] : (detail ?? ''); |
| 180 | switch (errorCode) { |
| 181 | case 'authorization_pending': |
| 182 | case 'slow_down': |
| 183 | return { kind: 'pending', errorCode, description }; |
| 184 | case 'expired_token': |
| 185 | return { kind: 'expired' }; |
| 186 | case 'access_denied': |
| 187 | return { kind: 'denied', description }; |
| 188 | default: |
| 189 | throw new OAuthError( |
| 190 | `Device token polling failed (HTTP ${status}): ${detail ?? `${errorCode} ${description}`}`, |
| 191 | ); |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | // ── refreshAccessToken ──────────────────────────────────────────────── |
| 196 | |