(args: {
clientId: string;
code: string;
redirectUri: string;
verifier: string;
fetchImpl?: typeof fetch;
})
| 281 | } |
| 282 | |
| 283 | async function exchangeCodeForTokens(args: { |
| 284 | clientId: string; |
| 285 | code: string; |
| 286 | redirectUri: string; |
| 287 | verifier: string; |
| 288 | fetchImpl?: typeof fetch; |
| 289 | }): Promise<OAuthTokens> { |
| 290 | const fetchImpl = args.fetchImpl ?? fetch; |
| 291 | const body = new URLSearchParams({ |
| 292 | grant_type: "authorization_code", |
| 293 | code: args.code, |
| 294 | redirect_uri: args.redirectUri, |
| 295 | client_id: args.clientId, |
| 296 | code_verifier: args.verifier, |
| 297 | }); |
| 298 | const res = await fetchImpl(tokenEndpoint(), { |
| 299 | method: "POST", |
| 300 | headers: { |
| 301 | "content-type": "application/x-www-form-urlencoded", |
| 302 | accept: "application/json", |
| 303 | }, |
| 304 | body: body.toString(), |
| 305 | }); |
| 306 | if (res.status === 400 || res.status === 401) { |
| 307 | // The authorization code is single-use and short-lived. A 400/401 |
| 308 | // here almost always means it expired during the loopback wait or |
| 309 | // was already redeemed — surface an actionable message instead of |
| 310 | // a bare "HeyGen API error (400)". |
| 311 | const detail = (await safeText(res)) || res.statusText; |
| 312 | throw ErrRefreshFailed( |
| 313 | `authorization code rejected (${detail}); please run \`auth login\` again`, |
| 314 | ); |
| 315 | } |
| 316 | if (!res.ok) { |
| 317 | throw ErrApi(res.status, (await safeText(res)) || res.statusText); |
| 318 | } |
| 319 | return parseTokenResponse(await readJsonOrThrow(res)); |
| 320 | } |
| 321 | |
| 322 | /** |
| 323 | * Parse the RFC 6749 token response. Backend may also include |
no test coverage detected