// server/supabase/functions/realtime-token/openai-provider.ts // RealtimeProviderPort의 OpenAI 어댑터. 공급자 에러 본문은 읽지 않고 버린다 // (조직 ID·마스킹된 키 힌트가 섞여 있을 수 있음) — 상태 코드만 돌려준다. import type { RealtimeSessionConfig } from './policy.ts' import type { ClientSecretResult, RealtimeProviderPort } from './handler.ts' export const OPENAI_CLIENT_SECRETS_URL = 'https://api.openai.com/v1/realtime/client_secrets' export interface OpenAiRealtimeProviderOptions { /** 호출마다 읽는다 — 시크릿 교체가 재배포 없이 반영되도록. 빈 문자열이면 미설정. */ readApiKey(): string fetchImpl?: typeof fetch } export function createOpenAiRealtimeProvider(options: OpenAiRealtimeProviderOptions): RealtimeProviderPort { const fetchImpl = options.fetchImpl ?? fetch return { isConfigured(): boolean { return options.readApiKey() !== '' }, async mintClientSecret(userId: string, session: RealtimeSessionConfig): Promise { const apiKey = options.readApiKey() if (!apiKey) return { ok: false, reason: 'network' } let response: Response try { response = await fetchImpl(OPENAI_CLIENT_SECRETS_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, 'OpenAI-Safety-Identifier': userId, }, body: JSON.stringify({ session }), }) } catch { return { ok: false, reason: 'network' } } if (!response.ok) { await response.body?.cancel().catch(() => undefined) return { ok: false, reason: 'upstream_status', status: response.status } } const data: unknown = await response.json().catch(() => null) if (!data || typeof data !== 'object' || Array.isArray(data)) { return { ok: false, reason: 'invalid_response' } } return { ok: true, data: data as Record } }, } }