(port: number, email: string, password: string)
| 113 | * ready / a worker recycling) even right after the org route answers OK, so we |
| 114 | * retry on 5xx and network errors. Real auth failures (4xx) fail immediately. */ |
| 115 | export async function apiLogin(port: number, email: string, password: string): Promise<string> { |
| 116 | const maxAttempts = 6 |
| 117 | let lastErr = '' |
| 118 | for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
| 119 | let res: Response |
| 120 | try { |
| 121 | res = await fetch(`http://localhost:${port}/api/v1/auth/login`, { |
| 122 | method: 'POST', |
| 123 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, |
| 124 | body: new URLSearchParams({ username: email, password }).toString(), |
| 125 | signal: AbortSignal.timeout(10_000), |
| 126 | }) |
| 127 | } catch (err) { |
| 128 | lastErr = `network error: ${(err as Error)?.message ?? err}` // proxy/upstream not reachable yet |
| 129 | if (attempt < maxAttempts) { await new Promise((r) => setTimeout(r, 2000)); continue } |
| 130 | throw new Error(`Login failed after ${maxAttempts} attempts (${lastErr})`) |
| 131 | } |
| 132 | if (res.ok) { |
| 133 | // API returns { tokens: { access_token } } (nested) or { access_token } (flat) |
| 134 | const data = (await res.json()) as { tokens?: { access_token: string }; access_token?: string } |
| 135 | const token = data.tokens?.access_token ?? data.access_token |
| 136 | if (!token) throw new Error('Login response missing access_token') |
| 137 | return token |
| 138 | } |
| 139 | // 5xx = stack still settling → retry; 4xx = a real auth/route failure → stop now. |
| 140 | if (res.status >= 500 && attempt < maxAttempts) { |
| 141 | lastErr = `${res.status}` |
| 142 | await new Promise((r) => setTimeout(r, 2000)) |
| 143 | continue |
| 144 | } |
| 145 | throw new Error(`Login failed: ${res.status}`) |
| 146 | } |
| 147 | throw new Error(`Login failed after ${maxAttempts} attempts (last: ${lastErr})`) |
| 148 | } |
| 149 | |
| 150 | /** GET a JSON endpoint with an optional bearer token. |
| 151 | * Retries on 5xx and network errors: on a CI box the freshly-installed/just- |
no test coverage detected