// @ts-nocheck // ─── Shared helpers for per-provider token-refresh modules ───────────────── // // Extracted from open-sse/services/tokenRefresh.ts (originally ported from // KooshaPari's PR #7338, redone here on the current tip — see #7338 for the // original provider-module-split idea). These helpers are used by more than // one provider refresh function under ./providers/, plus by // ../tokenRefresh.ts itself, so they live in one place to avoid duplication. export type RefreshLogger = { info?: (tag: string, message: string, data?: Record) => void; warn?: (tag: string, message: string, data?: Record) => void; error?: (tag: string, message: string, data?: Record) => void; debug?: (tag: string, message: string, data?: Record) => void; } | null; export function buildFormParams(entries: Record): URLSearchParams { const params = new URLSearchParams(); for (const [key, value] of Object.entries(entries)) { if (typeof value === "string" && value.length > 0) { params.set(key, value); } } return params; } /** * OAuth2 error codes that mean the refresh token is permanently dead and * retrying will never succeed → callers must emit the unrecoverable sentinel * so the HealthCheck deactivates the account instead of looping every 60s. * Deliberately EXCLUDES transient codes (server_error, temporarily_unavailable, * slow_down) so we never deactivate an account over a recoverable blip. */ const UNRECOVERABLE_OAUTH_ERROR_CODES = new Set([ "invalid_grant", "invalid_request", "refresh_token_reused", "invalid_token", "expired_token", "unauthorized_client", "access_denied", ]); /** * Matches a known unrecoverable code EMBEDDED in a human-readable message, on * word boundaries (`_` counts as a word char, so `xinvalid_grant` never hits). * Alternation is safe against overlap because every candidate is delimited on * both sides. Built once — the code set is a module constant. */ const EMBEDDED_OAUTH_ERROR_CODE_RE = new RegExp( `(?"}` by a catch branch. * The old `errorBody.error === "invalid_grant"` only matched the first shape, * so the others returned `null` → the HealthCheck refresh loop (root cause of * the 1352× claude/aa5dd5cf invalidation storm). * * Some providers do not return the code bare at all, but as the tail of a * sentence: Cline answers a dead refresh_token with * `{"error":"failed to refresh token: invalid_grant"}`. That is neither an * exact code nor a `"error":""` field pair, so it used to return `null` * and a permanently dead token was classified as a TRANSIENT failure — the * unrecoverable branch in `tokenHealthCheck` (its `credentialsChangedSinceSweep` * race guard, the "please re-authenticate" message, and the dead-token clear for * rotating providers) never ran, so the token was retried forever and every * request routed to that connection 401'd with no actionable signal. * * Returns the matched code (only if it is in UNRECOVERABLE_OAUTH_ERROR_CODES) * or null. Matching stays conservative: a known code is accepted only as a bare * code string, as the value of an `"error"`/`"error_code"` field, or as a * word-delimited token inside such a value — so a `server_error` body or a 502 * HTML page still classifies as null. The deliberate trade-off is that a * message which merely MENTIONS a dead-token code is treated as unrecoverable; * that fails safe, because the alternative is an unbounded retry loop that * burns a rotating provider's refresh tokens. */ export function extractOAuthErrorCode(raw: unknown, depth = 0): string | null { if (raw == null || depth > 6) return null; if (typeof raw === "string") { const s = raw.trim(); if (!s) return null; if (UNRECOVERABLE_OAUTH_ERROR_CODES.has(s)) return s; // The string may itself be JSON (a double-encoded body, or the raw text). if (s[0] === "{" || s[0] === "[" || s[0] === '"') { try { const nested = extractOAuthErrorCode(JSON.parse(s), depth + 1); if (nested) return nested; } catch { // not valid JSON — fall through to the field scan } } // Safety net: a known code appearing as the value of an "error"/"error_code" // field inside otherwise-unparsed text. Scoped to avoid false positives. const m = s.match(/"error(?:_code)?"\s*:\s*"([a-z_]+)"/i); if (m && UNRECOVERABLE_OAUTH_ERROR_CODES.has(m[1])) return m[1]; // Last resort: the code carried inside a message rather than returned bare // (Cline: "failed to refresh token: invalid_grant"). const embedded = s.match(EMBEDDED_OAUTH_ERROR_CODE_RE); if (embedded) return embedded[1].toLowerCase(); return null; } if (typeof raw === "object") { const o = raw as Record; return ( extractOAuthErrorCode(o.error, depth + 1) ?? extractOAuthErrorCode(o.code, depth + 1) ?? extractOAuthErrorCode(o.error_code, depth + 1) ); } return null; } /** * Read an error response body ONCE and classify it. Returns the raw text (for * logging) and the extracted unrecoverable OAuth code (or null). Reading once * avoids the double-read bug where `response.json()` consumes the stream and a * later `response.text()` returns empty. */ export async function readRefreshErrorBody( response: Response ): Promise<{ rawText: string; code: string | null }> { const rawText = await response.text().catch(() => ""); let parsed: unknown = rawText; try { parsed = JSON.parse(rawText); } catch { // keep rawText as-is } const code = extractOAuthErrorCode(parsed) ?? extractOAuthErrorCode(rawText); return { rawText, code }; } /** * Check if a refresh result indicates an unrecoverable error * (e.g. the refresh token was already consumed and cannot be reused). * Callers should stop retrying and request re-authentication. */ export function isUnrecoverableRefreshError(result) { return ( result && typeof result === "object" && (result.error === "unrecoverable_refresh_error" || result.error === "refresh_token_reused" || result.error === "invalid_request" || result.error === "invalid_grant") ); }