(payload: Record<string, unknown>)
| 27 | } |
| 28 | |
| 29 | function tokenFromResponse(payload: Record<string, unknown>): TokenInfo { |
| 30 | // Required-field validation. Reject responses that are missing |
| 31 | // any of the three load-bearing fields rather than persisting empty |
| 32 | // strings that will fail mysteriously later. |
| 33 | const accessToken = payload['access_token']; |
| 34 | if (typeof accessToken !== 'string' || accessToken.length === 0) { |
| 35 | throw new OAuthError('OAuth response missing access_token'); |
| 36 | } |
| 37 | const refreshToken = payload['refresh_token']; |
| 38 | if (typeof refreshToken !== 'string' || refreshToken.length === 0) { |
| 39 | throw new OAuthError('OAuth response missing refresh_token'); |
| 40 | } |
| 41 | const expiresInRaw = payload['expires_in']; |
| 42 | const expiresIn = Number(expiresInRaw); |
| 43 | if (!Number.isFinite(expiresIn) || expiresIn <= 0) { |
| 44 | throw new OAuthError('OAuth response missing or invalid expires_in'); |
| 45 | } |
| 46 | return { |
| 47 | accessToken, |
| 48 | refreshToken, |
| 49 | expiresAt: Math.floor(Date.now() / 1000) + expiresIn, |
| 50 | scope: typeof payload['scope'] === 'string' ? payload['scope'] : '', |
| 51 | tokenType: typeof payload['token_type'] === 'string' ? payload['token_type'] : 'Bearer', |
| 52 | expiresIn, |
| 53 | }; |
| 54 | } |
| 55 | |
| 56 | /** HTTP client default timeout for OAuth requests. */ |
| 57 | const DEFAULT_HTTP_TIMEOUT_MS = 30_000; |
no test coverage detected