Files
braidsbymira/supabase/functions/on-inquiry-created/index.ts
belviskhoremk 35e929c5e1 fix: harden Edge Function notification path per final review
Fixes issues raised by the whole-branch review before merge:

- Critical: the previous migration hardcoded the live production
  functions URL and anon key as the committed default, so every local
  `supabase db reset` silently pointed dev/test triggers at the real
  project. Adding a new migration instead of editing the already-applied
  one (checksum/drift safety), with local dev values as the committed
  default; production values are applied out-of-band, never committed.

- Important: verify_jwt=true plus sending the anon key as a bearer
  token didn't actually stop anyone, since the anon key is public
  (ships in the frontend bundle) — confirmed empirically, a request
  with a valid anon key but no shared secret now gets a real 401.
  Added a random shared secret known only to the database and the
  Edge Functions' secret store.

- Important: notification emails interpolated raw contact fields into
  HTML with no escaping. Added escapeHtml() and tests proving injected
  markup renders as literal text, not live markup.

- Important: the URL/key/secret functions moved from public to a
  private schema with EXECUTE revoked from anon/authenticated, closing
  an unintended PostgREST RPC endpoint (verified 404 after the fix).

Also adds a test:functions npm script now that Deno is confirmed
available and the shared email builders' tests pass (12 total incl.
the new escaping cases).

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 10:15:34 +02:00

47 lines
1.5 KiB
TypeScript

import { createClient } from 'jsr:@supabase/supabase-js@2'
import { buildInquiryNotificationEmail } from '../_shared/email.ts'
const FROM_ADDRESS = 'bookings@braidsbymira.com'
const MIRA_EMAIL = 'hello@braidsbymira.com'
Deno.serve(async (req) => {
const notifySecret = Deno.env.get('NOTIFY_WEBHOOK_SECRET')
if (notifySecret && req.headers.get('X-Notify-Secret') !== notifySecret) {
return new Response(JSON.stringify({ error: 'unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
const payload = await req.json()
const inquiry = payload.record
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
)
const { data: product } = await supabaseAdmin
.from('products')
.select('name')
.eq('id', inquiry.product_id)
.single()
const { subject, html } = buildInquiryNotificationEmail({
productName: product?.name ?? inquiry.product_id,
contact: { name: inquiry.contact_name, email: inquiry.contact_email, notes: inquiry.contact_notes },
})
const resendApiKey = Deno.env.get('RESEND_API_KEY')
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${resendApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ from: FROM_ADDRESS, to: MIRA_EMAIL, subject, html }),
})
return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } })
})