// server/supabase/functions/realtime-token/index.ts // OpenAI Realtime API ephemeral token 발급. // 렌더러가 이 토큰으로 OpenAI와 직접 WebRTC 연결한다 (서버 키 비노출). // 요청: application/json { model?, voice?, instructions? } // 응답: OpenAI client_secrets 응답 그대로 + { model, tier } // // 이 파일은 조립 루트(composition root)다. 유스케이스는 handler.ts, // 정책은 policy.ts, OpenAI 호출은 openai-provider.ts에 있다. import { corsHeaders } from '../_shared/cors.ts' import { authErrorResponse, requireUser, type AuthError } from '../_shared/auth.ts' import { checkQuota, consumeQuota, createServiceRoleClient, getQuotaPolicy, } from '../_shared/quota.ts' import { readProviderKey } from '../_shared/provider-key.ts' import { type AuthenticateResult, createRealtimeTokenHandler, type RealtimeQuotaPort, } from './handler.ts' import { createOpenAiRealtimeProvider } from './openai-provider.ts' function isAuthError(err: unknown): err is AuthError { return !!err && typeof err === 'object' && 'status' in err && 'message' in err } async function authenticate(req: Request): Promise { try { const user = await requireUser(req) return { ok: true, user: { id: user.id } } } catch (err) { if (isAuthError(err)) return { ok: false, response: authErrorResponse(err, corsHeaders) } throw err } } /** 서비스 롤 클라이언트는 요청 처리 중에 만든다 — 환경 누락이 부팅 실패가 아닌 500이 되도록. */ function createSupabaseRealtimeQuota(): RealtimeQuotaPort { return { check: (userId) => checkQuota(userId, 'realtime_session', createServiceRoleClient()), consume: (userId, tier) => consumeQuota( userId, 'realtime_session', createServiceRoleClient(), getQuotaPolicy(tier, 'realtime_session').limit, ), } } const handler = createRealtimeTokenHandler({ authenticate, quota: createSupabaseRealtimeQuota(), provider: createOpenAiRealtimeProvider({ readApiKey: () => readProviderKey('OPENAI_API_KEY') }), logger: { error: (message, meta) => console.error(message, meta), }, }) Deno.serve(handler)