/* |-------------------------------------------------------------------------- | Agência PSI — Edge Function: get-intake-invite-info (A#31) |-------------------------------------------------------------------------- | Lookup read-only dos dados de apresentação do terapeuta/clínica a partir | do token do patient_invite. Alimenta o "hero header" da página pública | /cadastro/paciente. | | Defesa em camadas (reusa o mesmo padrão do submit-patient-intake): | 1. Rate limit por IP → check_rate_limit RPC (endpoint=invite_info_lookup) | 2. Logging → record_submission_attempt RPC | | Não usa honeypot nem captcha porque: | • Não há input humano (só o token da URL). | • Payload devolvido é intencionalmente limitado a campos não-sensíveis. | • Token é UUID (128 bits) — enumeração brute-force inviável na prática. |-------------------------------------------------------------------------- */ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const SUPABASE_URL = Deno.env.get('SUPABASE_URL') const SUPABASE_SERVICE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') const ENDPOINT_NAME = 'invite_info_lookup' const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY) const CORS_HEADERS = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type' } function jsonResponse(body, status = 200) { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...CORS_HEADERS } }) } async function hashIp(ip) { if (!ip) return null const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(`agenciapsi:${ip}`)) return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('').slice(0, 32) } function getClientIp(req) { const xff = req.headers.get('x-forwarded-for') if (xff) return xff.split(',')[0].trim() return req.headers.get('cf-connecting-ip') || req.headers.get('x-real-ip') || null } Deno.serve(async (req) => { if (req.method === 'OPTIONS') return new Response(null, { status: 204, headers: CORS_HEADERS }) if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405) let body try { body = await req.json() } catch { return jsonResponse({ error: 'invalid-json' }, 400) } const token = String(body?.token || '').trim() const ip = getClientIp(req) const ipHash = await hashIp(ip) const userAgent = String(req.headers.get('user-agent') || '').slice(0, 500) if (!token) { return jsonResponse({ error: 'missing-token' }, 400) } // ── Rate limit ── const { data: rl, error: rlErr } = await supabase.rpc('check_rate_limit', { p_ip_hash: ipHash, p_endpoint: ENDPOINT_NAME }) if (rlErr) { console.error('[get-intake-invite-info] check_rate_limit failed:', rlErr.message) } if (rl && rl.allowed === false) { await supabase.rpc('record_submission_attempt', { p_endpoint: ENDPOINT_NAME, p_ip_hash: ipHash, p_success: false, p_blocked_by: 'rate_limit', p_error_code: rl.reason || 'blocked', p_error_msg: null, p_user_agent: userAgent, p_metadata: null }) return jsonResponse({ error: 'rate-limited', retry_after_seconds: rl.retry_after_seconds || null }, 429) } // ── RPC de lookup ── const { data, error } = await supabase.rpc('get_patient_intake_invite_info', { p_token: token }) if (error) { await supabase.rpc('record_submission_attempt', { p_endpoint: ENDPOINT_NAME, p_ip_hash: ipHash, p_success: false, p_blocked_by: 'rpc', p_error_code: error.code || 'rpc_err', p_error_msg: error.message, p_user_agent: userAgent, p_metadata: null }) return jsonResponse({ error: 'rpc-failed' }, 400) } // RPC devolve { error: 'invalid-token' | 'missing-token' } para ruim e // { ok: true, info: {...} } para sucesso. if (data && data.error) { await supabase.rpc('record_submission_attempt', { p_endpoint: ENDPOINT_NAME, p_ip_hash: ipHash, p_success: false, p_blocked_by: 'validation', p_error_code: data.error, p_error_msg: null, p_user_agent: userAgent, p_metadata: null }) return jsonResponse({ error: data.error }, 404) } await supabase.rpc('record_submission_attempt', { p_endpoint: ENDPOINT_NAME, p_ip_hash: ipHash, p_success: true, p_blocked_by: null, p_error_code: null, p_error_msg: null, p_user_agent: userAgent, p_metadata: null }) return jsonResponse({ ok: true, info: data?.info || null }, 200) })