| 99 | // ── requestDeviceAuthorization ──────────────────────────────────────── |
| 100 | |
| 101 | export async function requestDeviceAuthorization( |
| 102 | config: OAuthFlowConfig, |
| 103 | options: { readonly deviceHeaders?: DeviceHeaders | undefined }, |
| 104 | ): Promise<DeviceAuthorization> { |
| 105 | const url = `${config.oauthHost.replace(/\/$/, '')}/api/oauth/device_authorization`; |
| 106 | const { status, data } = await postForm( |
| 107 | url, |
| 108 | { client_id: config.clientId }, |
| 109 | options.deviceHeaders, |
| 110 | ); |
| 111 | |
| 112 | if (status !== 200) { |
| 113 | throw new OAuthError( |
| 114 | `Device authorization failed (HTTP ${status}): ${pickErrorDetail(data)}`, |
| 115 | ); |
| 116 | } |
| 117 | |
| 118 | // Required-field validation for the device authorization response. |
| 119 | const userCode = data['user_code']; |
| 120 | const deviceCode = data['device_code']; |
| 121 | const verificationUriComplete = data['verification_uri_complete']; |
| 122 | if (typeof userCode !== 'string' || userCode.length === 0) { |
| 123 | throw new OAuthError('Device authorization response missing user_code'); |
| 124 | } |
| 125 | if (typeof deviceCode !== 'string' || deviceCode.length === 0) { |
| 126 | throw new OAuthError('Device authorization response missing device_code'); |
| 127 | } |
| 128 | if (typeof verificationUriComplete !== 'string' || verificationUriComplete.length === 0) { |
| 129 | throw new OAuthError('Device authorization response missing verification_uri_complete'); |
| 130 | } |
| 131 | |
| 132 | return { |
| 133 | userCode, |
| 134 | deviceCode, |
| 135 | verificationUri: typeof data['verification_uri'] === 'string' ? data['verification_uri'] : '', |
| 136 | verificationUriComplete, |
| 137 | expiresIn: data['expires_in'] !== undefined ? Number(data['expires_in']) : null, |
| 138 | interval: Number(data['interval'] ?? 5), |
| 139 | }; |
| 140 | } |
| 141 | |
| 142 | // ── pollDeviceToken ─────────────────────────────────────────────────── |
| 143 | |