( refreshToken: string, region: Region = 'global', )
| 25 | * network errors up to MAX_RETRIES times; gives up immediately on 4xx. |
| 26 | */ |
| 27 | export async function refreshAccessToken( |
| 28 | refreshToken: string, |
| 29 | region: Region = 'global', |
| 30 | ): Promise<{ access_token: string; refresh_token: string; expires_at: string; resource_url?: string }> { |
| 31 | const tokenUrl = `${OAUTH_HOSTS[region]}/oauth2/token`; |
| 32 | let lastErr: Error | null = null; |
| 33 | |
| 34 | for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { |
| 35 | if (attempt > 0) await new Promise(r => setTimeout(r, RETRY_DELAY_MS * attempt)); |
| 36 | |
| 37 | let res: Response; |
| 38 | try { |
| 39 | res = await fetch(tokenUrl, { |
| 40 | method: 'POST', |
| 41 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, |
| 42 | body: new URLSearchParams({ |
| 43 | grant_type: 'refresh_token', |
| 44 | refresh_token: refreshToken, |
| 45 | client_id: CLIENT_ID, |
| 46 | }), |
| 47 | signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), |
| 48 | }); |
| 49 | } catch (err) { |
| 50 | const isTimeout = err instanceof Error |
| 51 | && (err.name === 'AbortError' || err.name === 'TimeoutError' || err.message.includes('timed out')); |
| 52 | lastErr = new Error( |
| 53 | isTimeout |
| 54 | ? 'Token refresh timed out — auth server did not respond within 10 s.' |
| 55 | : `Token refresh failed: ${err instanceof Error ? err.message : String(err)}`, |
| 56 | ); |
| 57 | continue; |
| 58 | } |
| 59 | |
| 60 | if (!res.ok) { |
| 61 | if (res.status >= 400 && res.status < 500) { |
| 62 | throw new CLIError( |
| 63 | 'OAuth session expired and could not be refreshed.', |
| 64 | ExitCode.AUTH, |
| 65 | 'Re-authenticate: mmx auth login', |
| 66 | ); |
| 67 | } |
| 68 | lastErr = new Error(`Token refresh failed: HTTP ${res.status}`); |
| 69 | continue; |
| 70 | } |
| 71 | |
| 72 | const body = (await res.json()) as RefreshResponse; |
| 73 | if (body.status !== 'success' || !body.access_token) { |
| 74 | throw new CLIError( |
| 75 | `OAuth refresh failed: ${body.status}.`, |
| 76 | ExitCode.AUTH, |
| 77 | 'Re-authenticate: mmx auth login', |
| 78 | ); |
| 79 | } |
| 80 | return { |
| 81 | access_token: body.access_token, |
| 82 | refresh_token: body.refresh_token ?? refreshToken, |
| 83 | expires_at: new Date(body.expired_in ?? Date.now()).toISOString(), |
| 84 | resource_url: body.resource_url, |
no test coverage detected