/** * Check Transaction Status Edge Function * * Allows clients to poll the status of a moderation transaction * using its idempotency key. * * Part of Sacred Pipeline Phase 3: Enhanced Error Handling */ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; interface StatusRequest { idempotencyKey: string; } interface StatusResponse { status: 'pending' | 'processing' | 'completed' | 'failed' | 'expired' | 'not_found'; createdAt?: string; updatedAt?: string; expiresAt?: string; attempts?: number; lastError?: string; completedAt?: string; action?: string; submissionId?: string; } const handler = async (req: Request): Promise => { if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } const tracking = startRequest(); try { // Verify authentication const authHeader = req.headers.get('Authorization'); if (!authHeader) { edgeLogger.warn('Missing authorization header', { requestId: tracking.requestId }); return new Response( JSON.stringify({ error: 'Unauthorized', status: 'not_found' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!, { global: { headers: { Authorization: authHeader } } } ); // Verify user const { data: { user }, error: authError } = await supabase.auth.getUser(); if (authError || !user) { edgeLogger.warn('Invalid auth token', { requestId: tracking.requestId, error: authError }); return new Response( JSON.stringify({ error: 'Unauthorized', status: 'not_found' }), { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Parse request const { idempotencyKey }: StatusRequest = await req.json(); if (!idempotencyKey) { return new Response( JSON.stringify({ error: 'Missing idempotencyKey', status: 'not_found' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } edgeLogger.info('Checking transaction status', { requestId: tracking.requestId, userId: user.id, idempotencyKey, }); // Query idempotency_keys table const { data: keyRecord, error: queryError } = await supabase .from('idempotency_keys') .select('*') .eq('key', idempotencyKey) .single(); if (queryError || !keyRecord) { edgeLogger.info('Idempotency key not found', { requestId: tracking.requestId, idempotencyKey, error: queryError, }); return new Response( JSON.stringify({ status: 'not_found', error: 'Transaction not found. It may have expired or never existed.' } as StatusResponse), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Verify user owns this key if (keyRecord.user_id !== user.id) { edgeLogger.warn('User does not own idempotency key', { requestId: tracking.requestId, userId: user.id, keyUserId: keyRecord.user_id, }); return new Response( JSON.stringify({ error: 'Unauthorized', status: 'not_found' }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } // Build response const response: StatusResponse = { status: keyRecord.status, createdAt: keyRecord.created_at, updatedAt: keyRecord.updated_at, expiresAt: keyRecord.expires_at, attempts: keyRecord.attempts, action: keyRecord.action, submissionId: keyRecord.submission_id, }; // Include error if failed if (keyRecord.status === 'failed' && keyRecord.last_error) { response.lastError = keyRecord.last_error; } // Include completed timestamp if completed if (keyRecord.status === 'completed' && keyRecord.completed_at) { response.completedAt = keyRecord.completed_at; } const duration = endRequest(tracking); edgeLogger.info('Transaction status retrieved', { requestId: tracking.requestId, duration, status: response.status, }); return new Response( JSON.stringify(response), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } catch (error) { const duration = endRequest(tracking); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; edgeLogger.error('Error checking transaction status', { requestId: tracking.requestId, duration, error: errorMessage, }); return new Response( JSON.stringify({ error: 'Internal server error', status: 'not_found' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } }; Deno.serve(handler);