feat: WS /t2/live — proxy Gemini Live API — 124/124 tests

This commit is contained in:
Hermann_Kitio 2026-04-17 03:39:21 +03:00
parent f08be960b0
commit 653fc3150e
8 changed files with 422 additions and 66 deletions

100
src/routes/t2live.ts Normal file
View file

@ -0,0 +1,100 @@
import { Hono } from 'hono'
import type { UpgradeWebSocket } from 'hono/ws'
import { EventEmitter } from 'node:events'
import { supabase } from '../lib/supabase.js'
import { checkFeatureAccess } from '../lib/access.js'
import type { Plan } from '../lib/access.js'
import {
openGeminiLiveSession,
type WebSocketLike,
} from '../lib/geminiLive.js'
/**
* Crée le router pour `WS /t2/live`.
* - Auth : JWT Supabase passé en query param `?token=<jwt>`
* - Permission : plan Premium (`oral_t2_live`) via checkFeatureAccess
* - Refus auth close 4001, refus plan close 4003
* - OK openGeminiLiveSession (proxy vers Gemini Live)
*/
export default function createT2LiveRoutes(
upgradeWebSocket: UpgradeWebSocket
) {
const app = new Hono()
app.get(
'/live',
upgradeWebSocket(async (c) => {
const token = c.req.query('token')
let denyCode: number | null = null
let denyReason = ''
if (!token) {
denyCode = 4001
denyReason = 'AUTH_REQUIRED'
} else {
try {
const {
data: { user },
error: authError,
} = await supabase.auth.getUser(token)
if (authError || !user) {
denyCode = 4001
denyReason = 'AUTH_REQUIRED'
} else {
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('plan')
.eq('id', user.id)
.single()
if (profileError || !profile) {
denyCode = 4001
denyReason = 'AUTH_REQUIRED'
} else if (
!checkFeatureAccess(profile.plan as Plan, 'oral_t2_live')
) {
denyCode = 4003
denyReason = 'PLAN_INSUFFICIENT'
}
}
} catch {
denyCode = 4001
denyReason = 'AUTH_REQUIRED'
}
}
// Adapter EventEmitter → WebSocketLike pour réutiliser openGeminiLiveSession
const adapter = new EventEmitter() as EventEmitter & WebSocketLike
adapter.send = () => {}
adapter.close = () => {}
return {
onOpen(_evt, ws) {
adapter.send = (data: unknown) =>
ws.send(data as Parameters<typeof ws.send>[0])
adapter.close = (code?: number, reason?: string) =>
ws.close(code, reason)
if (denyCode !== null) {
ws.close(denyCode, denyReason)
return
}
openGeminiLiveSession(adapter)
},
onMessage(evt) {
adapter.emit('message', evt.data)
},
onClose() {
adapter.emit('close')
},
onError() {
adapter.emit('error', new Error('CLIENT_ERROR'))
},
}
})
)
return app
}