fad06fd
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<WsJwt> {
let response: Awaited<ReturnType<typeof Apis.auth.jwt_request>>
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<string> {
return (await requestWsJwt()).jwt
}