import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts' import { authErrorResponse, requireUser, type AuthError } from '../_shared/auth.ts' import { createServiceRoleClient } from '../_shared/quota.ts' import { MeetingDocumentRequestError, parseGenerateMeetingDocumentRequest, } from '../_shared/meeting-document-contract.ts' import { readProviderKey } from '../_shared/provider-key.ts' import { generateMeetingDocument, type GenerationLogger, type MeetingDocumentProvider, type MeetingDocumentStore, type ProviderOutcome, } from './generation.ts' type ServiceClient = ReturnType const PROVIDER_TIMEOUT_MS = 60_000 function json(status: number, body: Record): Response { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json', 'Cache-Control': 'no-store', }, }) } const logError: GenerationLogger = (message, context) => { if (context === undefined) console.error(message) else console.error(message, context) } /** Supabase adapter for the persistence port (service-role RPCs). */ function createSupabaseStore(client: ServiceClient): MeetingDocumentStore { return { async claim(actorId, request) { const { data, error } = await client.rpc('claim_meeting_document_generation_v1', { p_actor_id: actorId, p_idempotency_key: request.idempotencyKey, p_meeting_id: request.meetingId, p_template_id: request.templateId, p_title: request.title, p_model: request.model, }) return { data, error } }, async markFailed(actorId, idempotencyKey, code) { const { error } = await client.rpc('fail_meeting_document_generation_v1', { p_actor_id: actorId, p_idempotency_key: idempotencyKey, p_error_code: code, }) return error }, async commit(actorId, idempotencyKey, providerResult, latencyMs) { const { data, error } = await client.rpc('commit_meeting_document_generation_v1', { p_actor_id: actorId, p_idempotency_key: idempotencyKey, p_content: providerResult.content, p_latency_ms: latencyMs, p_input_tokens: providerResult.inputTokens, p_output_tokens: providerResult.outputTokens, }) return { data, error } }, async findDocument(actorId, documentId) { const { data, error } = await client .from('meeting_documents') .select('*') .eq('id', documentId) .eq('user_id', actorId) .maybeSingle() return { data, error } }, } } /** Anthropic Messages API adapter for the provider port. */ function createAnthropicProvider(apiKey: string): MeetingDocumentProvider { return { async generate(payload): Promise { let response: Response try { response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', }, body: JSON.stringify(payload), signal: AbortSignal.timeout(PROVIDER_TIMEOUT_MS), }) } catch (error) { const timedOut = error instanceof DOMException && error.name === 'TimeoutError' return timedOut ? { kind: 'timeout' } : { kind: 'network_error' } } if (!response.ok) return { kind: 'http_error', status: response.status } try { return { kind: 'ok', body: await response.json() } } catch { return { kind: 'ok', body: null } } }, } } Deno.serve(async (request: Request) => { const preflight = handleCorsPreflightRequest(request) if (preflight) return preflight if (request.method !== 'POST') return json(405, { error: 'method_not_allowed' }) try { const actor = await requireUser(request) let rawBody: unknown try { rawBody = await request.json() } catch { throw new MeetingDocumentRequestError('Invalid JSON body') } const body = parseGenerateMeetingDocumentRequest(rawBody) const providerKey = readProviderKey('ANTHROPIC_API_KEY') if (providerKey.length === 0) { return json(503, { error: 'provider_unavailable' }) } const outcome = await generateMeetingDocument(actor.id, body, { store: createSupabaseStore(createServiceRoleClient()), provider: createAnthropicProvider(providerKey), now: () => Date.now(), log: logError, }) return json(outcome.status, outcome.body) } catch (error) { if (error instanceof MeetingDocumentRequestError) { return json(error.status, { error: 'invalid_request', message: error.message }) } if (error && typeof error === 'object' && 'status' in error && 'message' in error) { return authErrorResponse(error as AuthError, corsHeaders) } console.error('generate-meeting-document failed', { kind: 'internal_error' }) return json(500, { error: 'internal_error' }) } })