feat: WS /t2/live — proxy Gemini Live API — 124/124 tests
This commit is contained in:
parent
f08be960b0
commit
653fc3150e
8 changed files with 422 additions and 66 deletions
100
src/routes/t2live.ts
Normal file
100
src/routes/t2live.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue