55 lines
2 KiB
TypeScript
55 lines
2 KiB
TypeScript
// 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<ClientSecretResult> {
|
|
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<string, unknown> }
|
|
},
|
|
}
|
|
}
|