// Follow this setup guide to integrate the Deno language server with your editor: // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. // Setup type definitions for built-in Supabase Runtime APIs import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import { serve } from "https://deno.land/std@0.177.0/http/server.ts"; const AWS_REGION = Deno.env.get('AWS_REGION'); const AWS_ACCESS_KEY = Deno.env.get('AWS_ACCESS_KEY'); const AWS_SECRET_KEY = Deno.env.get('AWS_SECRET_KEY'); serve(async (req)=>{ try { // Check if we have AWS credentials if (!AWS_REGION || !AWS_ACCESS_KEY || !AWS_SECRET_KEY) { return new Response(JSON.stringify({ error: 'AWS credentials are not configured' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } // Parse the multipart form data to get the images const formData = await req.formData(); const ktpImage = formData.get('ktp'); const selfieImage = formData.get('selfie'); if (!ktpImage || !(ktpImage instanceof File) || !selfieImage || !(selfieImage instanceof File)) { return new Response(JSON.stringify({ error: 'Both KTP and selfie images are required' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // Convert images to base64 const ktpBuffer = await ktpImage.arrayBuffer(); const selfieBuffer = await selfieImage.arrayBuffer(); const ktpBase64 = btoa(String.fromCharCode(...new Uint8Array(ktpBuffer))); const selfieBase64 = btoa(String.fromCharCode(...new Uint8Array(selfieBuffer))); // Create AWS signature for authorization const date = new Date(); const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, ''); const dateStamp = amzDate.substring(0, 8); const host = `rekognition.${AWS_REGION}.amazonaws.com`; const endpoint = `https://${host}/`; const request = { "SourceImage": { "Bytes": ktpBase64 }, "TargetImage": { "Bytes": selfieBase64 }, "SimilarityThreshold": 70 }; // AWS Signature V4 calculation const method = 'POST'; const service = 'rekognition'; const contentType = 'application/x-amz-json-1.1'; const amzTarget = 'RekognitionService.CompareFaces'; const canonicalUri = '/'; const canonicalQueryString = ''; const payloadHash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(request))).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join('')); const canonicalHeaders = `content-type:${contentType}\n` + `host:${host}\n` + `x-amz-date:${amzDate}\n` + `x-amz-target:${amzTarget}\n`; const signedHeaders = 'content-type;host;x-amz-date;x-amz-target'; const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`; const algorithm = 'AWS4-HMAC-SHA256'; const credentialScope = `${dateStamp}/${AWS_REGION}/${service}/aws4_request`; const stringToSign = `${algorithm}\n${amzDate}\n${credentialScope}\n${await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalRequest)).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join(''))}`; const getSignatureKey = async (key, dateStamp, regionName, serviceName)=>{ const kDate = await crypto.subtle.importKey("raw", new TextEncoder().encode(`AWS4${key}`), { name: "HMAC", hash: "SHA-256" }, false, [ "sign" ]); const kRegion = await crypto.subtle.sign("HMAC", kDate, new TextEncoder().encode(regionName)); const kService = await crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", kRegion, { name: "HMAC", hash: "SHA-256" }, false, [ "sign" ]), new TextEncoder().encode(serviceName)); return crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", kService, { name: "HMAC", hash: "SHA-256" }, false, [ "sign" ]), new TextEncoder().encode("aws4_request")); }; const signingKey = await getSignatureKey(AWS_SECRET_KEY, dateStamp, AWS_REGION, service); const signature = await crypto.subtle.sign("HMAC", await crypto.subtle.importKey("raw", signingKey, { name: "HMAC", hash: "SHA-256" }, false, [ "sign" ]), new TextEncoder().encode(stringToSign)).then((hash)=>Array.from(new Uint8Array(hash)).map((b)=>b.toString(16).padStart(2, '0')).join('')); const authHeader = `${algorithm} ` + `Credential=${AWS_ACCESS_KEY}/${credentialScope}, ` + `SignedHeaders=${signedHeaders}, ` + `Signature=${signature}`; // Make request to AWS Rekognition const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': contentType, 'X-Amz-Date': amzDate, 'X-Amz-Target': amzTarget, 'Authorization': authHeader }, body: JSON.stringify(request) }); const data = await response.json(); // Determine if verification passed const matched = !!(data.FaceMatches && data.FaceMatches.length > 0); let highestSimilarity = 0; if (matched && data.FaceMatches && data.FaceMatches.length > 0) { highestSimilarity = Math.max(...data.FaceMatches.map((match)=>match.Similarity || 0)); } return new Response(JSON.stringify({ success: true, matched: matched, similarity: highestSimilarity, faceMatches: data.FaceMatches || [], unmatchedFaces: data.UnmatchedFaces || [] }), { headers: { 'Content-Type': 'application/json' } }); } catch (error) { console.error("Error in verify-face function:", error); return new Response(JSON.stringify({ error: "Failed to verify faces", details: error instanceof Error ? error.message : String(error) }), { status: 500, headers: { 'Content-Type': 'application/json' } }); } }); /* To invoke locally: 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) 2. Make an HTTP request: curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/verify-face' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ --header 'Content-Type: application/json' \ --data '{"name":"Functions"}' */