Add ACMCC app source, Supabase backend, and project config

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 20:19:26 -04:00
parent 313b51b412
commit 183fe0a93c
1422 changed files with 259271 additions and 0 deletions
@@ -0,0 +1,162 @@
import { createClient } from 'npm:@supabase/supabase-js@2'
import { WebhookError, verifyWebhookRequest } from 'npm:@lovable.dev/webhooks-js'
// Suppression event payload sent by the Go API when Mailgun reports
// a bounce, complaint, or unsubscribe.
interface SuppressionPayload {
email: string
reason: 'bounce' | 'complaint' | 'unsubscribe'
message_id?: string
metadata?: Record<string, unknown>
is_retry: boolean
retry_count: number
}
function parseSuppressionPayload(body: string): SuppressionPayload {
const parsed = JSON.parse(body)
if (!parsed.data) {
throw new Error('Missing data field in payload')
}
const data = parsed.data as SuppressionPayload
if (!data.email || !data.reason) {
throw new Error('Missing required fields: email, reason')
}
return data
}
function jsonResponse(data: Record<string, unknown>, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' },
})
}
Deno.serve(async (req) => {
if (req.method !== 'POST') {
return jsonResponse({ error: 'Method not allowed' }, 405)
}
const apiKey = Deno.env.get('LOVABLE_API_KEY')
const supabaseUrl = Deno.env.get('SUPABASE_URL')
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
if (!apiKey || !supabaseUrl || !supabaseServiceKey) {
console.error('Missing required environment variables')
return jsonResponse({ error: 'Server configuration error' }, 500)
}
// Verify HMAC signature using the Lovable API Key (same as auth-email-hook)
let payload: SuppressionPayload
try {
const verified = await verifyWebhookRequest({
req,
secret: apiKey,
parser: parseSuppressionPayload,
})
payload = verified.payload
} catch (error) {
if (error instanceof WebhookError) {
switch (error.code) {
case 'invalid_signature':
console.error('Invalid webhook signature')
return jsonResponse({ error: 'Invalid signature' }, 401)
case 'stale_timestamp':
console.error('Stale webhook timestamp')
return jsonResponse({ error: 'Stale timestamp' }, 401)
case 'invalid_payload':
case 'invalid_json':
console.error('Invalid payload', { code: error.code })
return jsonResponse({ error: 'Invalid payload' }, 400)
default:
console.error('Webhook verification failed', {
code: error.code,
message: error.message,
})
return jsonResponse({ error: 'Verification failed' }, 401)
}
}
console.error('Unexpected error during verification', { error })
return jsonResponse({ error: 'Internal error' }, 500)
}
const supabase = createClient(supabaseUrl, supabaseServiceKey)
const normalizedEmail = payload.email.toLowerCase()
// 1. Upsert to suppressed_emails (idempotent — safe for retries)
const { error: suppressError } = await supabase
.from('suppressed_emails')
.upsert(
{
email: normalizedEmail,
reason: payload.reason,
metadata: payload.metadata ?? null,
},
{ onConflict: 'email' },
)
if (suppressError) {
console.error('Failed to upsert suppressed email', {
error: suppressError,
email_redacted: normalizedEmail[0] + '***@' + normalizedEmail.split('@')[1],
})
return jsonResponse({ error: 'Failed to write suppression' }, 500)
}
// 2. Append a new log entry for the suppression event (never update existing rows)
const sendLogStatus = mapReasonToStatus(payload.reason)
const sendLogMessage = mapReasonToMessage(payload.reason)
const { error: insertError } = await supabase
.from('email_send_log')
.insert({
message_id: payload.message_id ?? null,
template_name: 'system',
recipient_email: normalizedEmail,
status: sendLogStatus,
error_message: sendLogMessage,
metadata: payload.metadata ?? null,
})
if (insertError) {
// Non-fatal — log and continue. The suppression was already recorded.
console.warn('Failed to insert email_send_log', {
error: insertError,
})
}
console.log('Suppression processed', {
email_redacted: normalizedEmail[0] + '***@' + normalizedEmail.split('@')[1],
reason: payload.reason,
is_retry: payload.is_retry,
retry_count: payload.retry_count,
has_message_id: !!payload.message_id,
})
return jsonResponse({ success: true })
})
function mapReasonToStatus(
reason: string,
): 'bounced' | 'complained' | 'suppressed' {
switch (reason) {
case 'bounce':
return 'bounced'
case 'complaint':
return 'complained'
default:
return 'suppressed'
}
}
function mapReasonToMessage(reason: string): string {
switch (reason) {
case 'bounce':
return 'Permanent bounce — email address is invalid or rejected'
case 'complaint':
return 'Spam complaint — recipient marked email as spam'
case 'unsubscribe':
return 'Recipient unsubscribed'
default:
return 'Email suppressed'
}
}