234 lines
9.2 KiB
TypeScript
234 lines
9.2 KiB
TypeScript
import {
|
|
type ClientSecretResult,
|
|
createRealtimeTokenHandler,
|
|
type RealtimeProviderPort,
|
|
type RealtimeQuotaConsumeResult,
|
|
type RealtimeQuotaPort,
|
|
type RealtimeQuotaSnapshot,
|
|
} from './handler.ts'
|
|
import type { RealtimeSessionConfig, RealtimeTier } from './policy.ts'
|
|
import { createOpenAiRealtimeProvider } from './openai-provider.ts'
|
|
|
|
function assert(condition: boolean, message: string): asserts condition {
|
|
if (!condition) throw new Error(message)
|
|
}
|
|
|
|
function assertEquals(actual: unknown, expected: unknown, message: string): void {
|
|
const a = JSON.stringify(actual)
|
|
const e = JSON.stringify(expected)
|
|
if (a !== e) throw new Error(`${message}: expected ${e}, got ${a}`)
|
|
}
|
|
|
|
const LEAKY_BODY = JSON.stringify({
|
|
error: {
|
|
message: 'Rate limit reached for org-SECRETORG123 on requests. Incorrect API key provided: sk-proj-****abcd',
|
|
},
|
|
})
|
|
|
|
interface Harness {
|
|
handler: (req: Request) => Promise<Response>
|
|
consumeCalls: Array<{ userId: string; tier: RealtimeTier }>
|
|
mintCalls: RealtimeSessionConfig[]
|
|
logs: Array<{ message: string; meta: Record<string, unknown> }>
|
|
}
|
|
|
|
interface HarnessOptions {
|
|
tier?: RealtimeTier
|
|
checkAllowed?: boolean
|
|
consumeAllowed?: boolean
|
|
configured?: boolean
|
|
mint?: () => Promise<ClientSecretResult>
|
|
provider?: RealtimeProviderPort
|
|
consumeThrows?: Error
|
|
}
|
|
|
|
function harness(options: HarnessOptions = {}): Harness {
|
|
const consumeCalls: Harness['consumeCalls'] = []
|
|
const mintCalls: RealtimeSessionConfig[] = []
|
|
const logs: Harness['logs'] = []
|
|
const tier = options.tier ?? 'pro'
|
|
|
|
const quota: RealtimeQuotaPort = {
|
|
check(): Promise<RealtimeQuotaSnapshot> {
|
|
return Promise.resolve({
|
|
allowed: options.checkAllowed ?? true,
|
|
current: 3,
|
|
limit: 30,
|
|
period: 'daily',
|
|
tier,
|
|
overageCredits: 0,
|
|
})
|
|
},
|
|
consume(userId: string, consumeTier: RealtimeTier): Promise<RealtimeQuotaConsumeResult> {
|
|
consumeCalls.push({ userId, tier: consumeTier })
|
|
if (options.consumeThrows) return Promise.reject(options.consumeThrows)
|
|
return Promise.resolve({
|
|
allowed: options.consumeAllowed ?? true,
|
|
current: 4,
|
|
limit: 30,
|
|
overageCredits: 0,
|
|
})
|
|
},
|
|
}
|
|
|
|
const provider: RealtimeProviderPort = options.provider ?? {
|
|
isConfigured: () => options.configured ?? true,
|
|
mintClientSecret(_userId: string, session: RealtimeSessionConfig): Promise<ClientSecretResult> {
|
|
mintCalls.push(session)
|
|
return options.mint?.() ??
|
|
Promise.resolve({ ok: true, data: { value: 'ek_test', expires_at: 123 } })
|
|
},
|
|
}
|
|
|
|
const handler = createRealtimeTokenHandler({
|
|
authenticate: () => Promise.resolve({ ok: true, user: { id: 'user-1' } }),
|
|
quota,
|
|
provider,
|
|
logger: { error: (message, meta) => logs.push({ message, meta }) },
|
|
})
|
|
return { handler, consumeCalls, mintCalls, logs }
|
|
}
|
|
|
|
function post(body: unknown = {}): Request {
|
|
return new Request('http://localhost/realtime-token', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
}
|
|
|
|
Deno.test('missing provider key returns 503 without consuming the session quota', async () => {
|
|
const h = harness({ configured: false })
|
|
const res = await h.handler(post())
|
|
assertEquals(res.status, 503, 'status')
|
|
assertEquals((await res.json()).error, 'not_configured', 'error code')
|
|
assertEquals(h.consumeCalls.length, 0, 'quota must not be consumed')
|
|
assertEquals(h.mintCalls.length, 0, 'provider must not be called')
|
|
})
|
|
|
|
Deno.test('upstream 4xx/5xx returns a fixed code, keeps quota, and never leaks the provider body', async () => {
|
|
for (const status of [401, 429, 500, 503]) {
|
|
const fetchImpl: typeof fetch = () => Promise.resolve(new Response(LEAKY_BODY, { status }))
|
|
const h = harness({
|
|
provider: createOpenAiRealtimeProvider({ readApiKey: () => 'sk-test', fetchImpl }),
|
|
})
|
|
const res = await h.handler(post())
|
|
const text = await res.text()
|
|
assertEquals(res.status, 502, `status for upstream ${status}`)
|
|
assertEquals(JSON.parse(text), { error: 'provider_request_failed' }, 'fixed error body')
|
|
assert(!text.includes('org-SECRETORG123'), 'org id leaked')
|
|
assert(!text.includes('sk-proj'), 'key hint leaked')
|
|
assertEquals(h.consumeCalls.length, 0, `quota consumed on upstream ${status}`)
|
|
assertEquals(h.logs[0]?.meta, { reason: 'upstream_status', status }, 'status is logged server-side')
|
|
assert(!JSON.stringify(h.logs).includes('SECRETORG'), 'provider body must not reach logs either')
|
|
}
|
|
})
|
|
|
|
Deno.test('network failure returns 502 without consuming quota', async () => {
|
|
const fetchImpl: typeof fetch = () => Promise.reject(new TypeError('dns failure for api.openai.com'))
|
|
const h = harness({
|
|
provider: createOpenAiRealtimeProvider({ readApiKey: () => 'sk-test', fetchImpl }),
|
|
})
|
|
const res = await h.handler(post())
|
|
assertEquals(res.status, 502, 'status')
|
|
assertEquals(await res.json(), { error: 'provider_request_failed' }, 'body')
|
|
assertEquals(h.consumeCalls.length, 0, 'quota must not be consumed')
|
|
})
|
|
|
|
Deno.test('non-object upstream success body returns provider_invalid_response without consuming quota', async () => {
|
|
const fetchImpl: typeof fetch = () => Promise.resolve(new Response('not json', { status: 200 }))
|
|
const h = harness({
|
|
provider: createOpenAiRealtimeProvider({ readApiKey: () => 'sk-test', fetchImpl }),
|
|
})
|
|
const res = await h.handler(post())
|
|
assertEquals(res.status, 502, 'status')
|
|
assertEquals(await res.json(), { error: 'provider_invalid_response' }, 'body')
|
|
assertEquals(h.consumeCalls.length, 0, 'quota must not be consumed')
|
|
})
|
|
|
|
Deno.test('successful mint consumes exactly one session and returns the secret with model and tier', async () => {
|
|
const h = harness({ tier: 'pro' })
|
|
const res = await h.handler(post({ voice: 'cedar', instructions: 'be brief' }))
|
|
assertEquals(res.status, 200, 'status')
|
|
assertEquals(
|
|
await res.json(),
|
|
{ value: 'ek_test', expires_at: 123, model: 'gpt-realtime-2.1-mini', tier: 'pro' },
|
|
'body',
|
|
)
|
|
assertEquals(h.consumeCalls, [{ userId: 'user-1', tier: 'pro' }], 'consume once')
|
|
assertEquals(h.mintCalls[0], {
|
|
type: 'realtime',
|
|
model: 'gpt-realtime-2.1-mini',
|
|
instructions: 'be brief',
|
|
audio: { output: { voice: 'cedar' } },
|
|
}, 'session config')
|
|
})
|
|
|
|
Deno.test('a consume race lost after minting returns 429 and discards the secret', async () => {
|
|
const h = harness({ consumeAllowed: false })
|
|
const res = await h.handler(post())
|
|
const text = await res.text()
|
|
assertEquals(res.status, 429, 'status')
|
|
assertEquals(JSON.parse(text).error, 'quota_exceeded', 'error code')
|
|
assert(!text.includes('ek_test'), 'minted secret must not be returned when quota is denied')
|
|
})
|
|
|
|
Deno.test('tier and quota denials happen before the provider is called', async () => {
|
|
const free = harness({ tier: 'free' })
|
|
const freeRes = await free.handler(post())
|
|
assertEquals(freeRes.status, 403, 'free status')
|
|
assertEquals((await freeRes.json()).error, 'tier_not_allowed', 'free error')
|
|
|
|
const wrongModel = harness({ tier: 'pro' })
|
|
const wrongRes = await wrongModel.handler(post({ model: 'gpt-realtime-2.1' }))
|
|
assertEquals(wrongRes.status, 403, 'model status')
|
|
assertEquals(await wrongRes.json(), {
|
|
error: 'model_not_allowed',
|
|
tier: 'pro',
|
|
requested: 'gpt-realtime-2.1',
|
|
allowed: ['gpt-realtime-2.1-mini'],
|
|
}, 'model body')
|
|
|
|
const exhausted = harness({ checkAllowed: false })
|
|
const exhaustedRes = await exhausted.handler(post())
|
|
assertEquals(exhaustedRes.status, 429, 'exhausted status')
|
|
await exhaustedRes.body?.cancel()
|
|
|
|
for (const h of [free, wrongModel, exhausted]) {
|
|
assertEquals(h.mintCalls.length, 0, 'provider must not be called')
|
|
assertEquals(h.consumeCalls.length, 0, 'quota must not be consumed')
|
|
}
|
|
})
|
|
|
|
Deno.test('unexpected errors return a generic internal_error without the exception message', async () => {
|
|
const h = harness({ consumeThrows: new Error('Failed to consume quota: relation daily_usage secret detail') })
|
|
const res = await h.handler(post())
|
|
const text = await res.text()
|
|
assertEquals(res.status, 500, 'status')
|
|
assertEquals(JSON.parse(text), { error: 'internal_error' }, 'body')
|
|
assert(!text.includes('secret detail'), 'exception message leaked')
|
|
})
|
|
|
|
Deno.test('auth rejection response is returned as-is and non-POST is 405', async () => {
|
|
const handler = createRealtimeTokenHandler({
|
|
authenticate: () =>
|
|
Promise.resolve({ ok: false, response: new Response('{"error":"Invalid auth token"}', { status: 401 }) }),
|
|
quota: {
|
|
check: () => Promise.reject(new Error('must not be called')),
|
|
consume: () => Promise.reject(new Error('must not be called')),
|
|
},
|
|
provider: { isConfigured: () => true, mintClientSecret: () => Promise.reject(new Error('unused')) },
|
|
logger: { error: () => undefined },
|
|
})
|
|
const res = await handler(post())
|
|
assertEquals(res.status, 401, 'auth status')
|
|
await res.body?.cancel()
|
|
|
|
const getRes = await handler(new Request('http://localhost/realtime-token', { method: 'GET' }))
|
|
assertEquals(getRes.status, 405, 'method status')
|
|
await getRes.body?.cancel()
|
|
|
|
const preflight = await handler(new Request('http://localhost/realtime-token', { method: 'OPTIONS' }))
|
|
assertEquals(preflight.status, 200, 'preflight status')
|
|
await preflight.body?.cancel()
|
|
})
|