d3ro-voice/server/supabase/functions/realtime-token/index.ts

63 lines
2.2 KiB
TypeScript

// 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<AuthenticateResult> {
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)