import Apis from '@/shared/api' import { WS_SERVER_DEFAULTS } from './protocol' /** * Mints the single-use handshake JWT through * `GET /api/v1/auth/jwt-request?appid=chat` (lib/auth/src/handlers/jwt_request.rs). * The im gateway consumes each JWT exactly once in redis * (`SET chat:jwt_used:{jti} NX EX`, lib/chat/src/ws/jwt.rs), so every * (re)connection has to fetch a fresh one; the old JWT is dead the moment the * handshake is attempted. */ /** * Why a JWT could not be obtained. The socket client uses this to decide * between "back off and retry" and "stop, the user must act". * - `auth`: 401/403 — the session is gone, so retrying is pointless. * - `transport`: offline / timeout / 5xx / 429 — retry with backoff. * - `protocol`: a 200 without a jwt, or a status the client does not model. */ export type WsJwtFailureKind = 'auth' | 'transport' | 'protocol' export class WsJwtError extends Error { readonly kind: WsJwtFailureKind /** HTTP status mirrored into the envelope `code`, 0 for transport failures. */ readonly status: number readonly errorCode: string | null constructor(message: string, kind: WsJwtFailureKind, status: number, errorCode?: string | null) { super(message) this.name = 'WsJwtError' this.kind = kind this.status = status this.errorCode = errorCode ?? null } } export function isWsJwtError(error: unknown): error is WsJwtError { return error instanceof WsJwtError } /** `JwtRequestResp` (lib/auth/src/services/jwt.rs). */ export interface WsJwt { jwt: string expiresInSecs: number } /** Map the envelope `code` (always the HTTP status) onto a failure kind. */ function classifyStatus(status: number): WsJwtFailureKind { if (status === 401 || status === 403) return 'auth' if (status === 0 || status === 408 || status === 429 || status >= 500) return 'transport' return 'protocol' } /** Fetch a JWT and keep its TTL, which the client records for diagnostics. */ export async function requestWsJwt(): Promise { let response: Awaited> try { response = await Apis.auth.jwt_request({ params: { appid: 'chat' } }) } catch (error) { // Alova reports real transport failures here (offline, DNS, aborted fetch); // 4xx/5xx still resolve with the documented envelope. throw new WsJwtError( error instanceof Error ? error.message : 'ws jwt request failed', 'transport', 0, ) } if (response.code !== 200) { const status = typeof response.code === 'number' ? response.code : 0 throw new WsJwtError( response.error || response.message || 'ws jwt request failed', classifyStatus(status), status, response.error_code, ) } const jwt = response.data?.jwt if (!jwt) { throw new WsJwtError('ws jwt missing from response', 'protocol', response.code) } return { jwt, expiresInSecs: response.data?.expires_in_secs ?? WS_SERVER_DEFAULTS.jwtTtlSecs, } } /** JWT string only; the transport-compatible form used by `ChatWsClient`. */ export async function fetchWsJwt(): Promise { return (await requestWsJwt()).jwt }