Files
moeka-project/server/apps/api/src/routes/chat-ws/v2/auth.ts
T
2026-08-26 19:49:58 +08:00

95 lines
2.8 KiB
TypeScript

import type { WSContext } from 'hono/ws'
import { parseAuthenticateRequest } from '@proj-airi/server-sdk-shared/v2'
import { WS_CLOSE_INTERNAL_ERROR, WS_CLOSE_TRY_AGAIN_LATER, WS_CLOSE_UNAUTHORIZED } from '../../../libs/ws-auth'
const CHAT_AUTH_TIMEOUT_MS = 15_000
export interface ChatWsAuthResolver {
(token: string): Promise<null | string>
}
export interface ChatWsV2Authentication {
/** Handles the only authentication request accepted for this socket. */
authenticate: (request: unknown) => Promise<{ userId: string }>
/** Stops authentication when the websocket disconnects. */
disconnect: () => void
}
interface CreateChatWsV2AuthenticationOptions {
onAuthenticated: (userId: string) => void
resolveUserId: ChatWsAuthResolver
socket?: Pick<WSContext, 'close'>
}
/**
* Owns authentication lifetime for one version-two websocket connection.
*
* The session timer begins when the socket opens. A close or timeout marks the
* session inactive before the token resolver completes, preventing a late
* resolver result from registering a disconnected peer.
*/
export function createChatWsV2Authentication(options: CreateChatWsV2AuthenticationOptions): ChatWsV2Authentication {
let authenticationStarted = false
let connectionActive = true
let authenticationComplete = false
let authTimer: ReturnType<typeof setTimeout>
function stopAuthentication(code: number, reason: string): void {
connectionActive = false
clearTimeout(authTimer)
options.socket?.close(code, reason)
}
authTimer = setTimeout(() => {
if (!authenticationComplete)
stopAuthentication(WS_CLOSE_TRY_AGAIN_LATER, 'authentication timeout')
}, CHAT_AUTH_TIMEOUT_MS)
return {
async authenticate(request) {
if (authenticationStarted)
throw new Error('WebSocket authentication already attempted')
authenticationStarted = true
let parsedRequest
try {
parsedRequest = parseAuthenticateRequest(request)
}
catch {
stopAuthentication(WS_CLOSE_UNAUTHORIZED, 'unauthorized')
throw new Error('WebSocket authentication failed')
}
let userId: null | string
try {
userId = await options.resolveUserId(parsedRequest.token)
}
catch (error) {
if (connectionActive)
stopAuthentication(WS_CLOSE_INTERNAL_ERROR, 'authentication unavailable')
throw error
}
if (!connectionActive)
throw new Error('WebSocket closed during authentication')
if (!userId) {
stopAuthentication(WS_CLOSE_UNAUTHORIZED, 'unauthorized')
throw new Error('WebSocket authentication failed')
}
authenticationComplete = true
clearTimeout(authTimer)
options.onAuthenticated(userId)
return { userId }
},
disconnect() {
connectionActive = false
clearTimeout(authTimer)
},
}
}