function getSupabaseConfig(env) { const url = env.SUPABASE_URL || ''; const publicKey = env.SUPABASE_ANON_KEY || env.SUPABASE_PUBLISHABLE_KEY || ''; const serviceKey = env.SUPABASE_SERVICE_ROLE_KEY || env.SUPABASE_SECRET_KEY || ''; if (!url || !publicKey || !serviceKey) { return null; } return { url: url.replace(/\/+$/, ''), publicKey, serviceKey }; } function getBearerToken(request) { const auth = request.headers.get('authorization') || ''; if (!auth.toLowerCase().startsWith('bearer ')) return ''; return auth.slice(7).trim(); } async function supabaseFetch(config, path, options = {}) { const isOpaqueSecretKey = config.serviceKey.startsWith('sb_secret_'); const authHeader = isOpaqueSecretKey ? {} : { authorization: `Bearer ${config.serviceKey}` }; const response = await fetch(`${config.url}${path}`, { ...options, headers: { apikey: config.serviceKey, ...authHeader, 'content-type': 'application/json', ...(options.headers || {}) } }); if (!response.ok) { const detail = await response.text(); throw new Error(`Supabase request failed ${response.status}: ${detail}`); } if (response.status === 204) return null; return response.json(); } async function getSupabaseUser(env, request) { const config = getSupabaseConfig(env); const token = getBearerToken(request); if (!config || !token) return null; const response = await fetch(`${config.url}/auth/v1/user`, { headers: { apikey: config.publicKey, authorization: `Bearer ${token}` } }); if (!response.ok) return null; return response.json(); } function normalizeEmail(value) { return String(value || '').trim().toLowerCase(); } function cleanText(value, maxLength = 220) { return String(value || '').trim().slice(0, maxLength); } function cleanNumber(value) { const numeric = Number(value); return Number.isFinite(numeric) && numeric >= 0 ? numeric : null; } function cleanStatus(value) { const status = cleanText(value || 'confirmed', 32).toLowerCase(); return ['confirmed', 'pending', 'rejected', 'refunded'].includes(status) ? status : 'confirmed'; } function safeUrl(value) { try { return new URL(String(value || '').trim()); } catch (error) { return null; } } function cleanOrigin(value) { const parsed = safeUrl(value); return parsed ? parsed.origin.slice(0, 220) : cleanText(value, 220); } function cleanUrl(value) { const raw = cleanText(value, 220); if (!raw) return ''; if (/^https?:\/\//i.test(raw)) return raw; return `https://${raw.replace(/^\/+/, '')}`; } function hostToClientId(value) { return cleanText( String(value || '') .trim() .toLowerCase() .replace(/^https?:\/\//, '') .replace(/^www\./, '') .replace(/[:/].*$/, '') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''), 120 ); } function getLedgerCreditConfig(env) { return { clientId: cleanText(env.SOULWALL_LEDGER_CLIENT_ID || env.SOULWALL_CLIENT_ID || '', 120), deploymentTitle: cleanText(env.SOULWALL_DEPLOYMENT_TITLE || '', 220), deploymentDescription: cleanText(env.SOULWALL_DEPLOYMENT_DESCRIPTION || '', 400), sourceWebsite: cleanText(env.SOULWALL_SOURCE_WEBSITE || '', 220) }; } function resolveLedgerCredit(env, request = null, payload = {}) { const config = getLedgerCreditConfig(env); const requestUrl = safeUrl(request?.url || ''); const requestOrigin = cleanOrigin( payload.origin || payload.integrationOrigin || request?.headers?.get('origin') || request?.headers?.get('referer') || requestUrl?.origin || '' ); const sourceWebsite = cleanUrl( payload.sourceWebsite || payload.website || payload.donationSourceWebsite || config.sourceWebsite || request?.headers?.get('origin') || request?.headers?.get('referer') || requestUrl?.origin || '' ); const sourceHost = safeUrl(sourceWebsite)?.hostname || requestUrl?.hostname || ''; const clientId = cleanText( payload.clientId || payload.client_id || config.clientId || hostToClientId(sourceHost) || 'soulwall-deployment', 120 ); return { clientId, integrationOrigin: requestOrigin || cleanOrigin(sourceWebsite || requestUrl?.origin || ''), sourceWebsite: sourceWebsite || null, deploymentTitle: cleanText( payload.deploymentTitle || payload.title || payload.accessTitle || config.deploymentTitle || '', 220 ) || null, deploymentDescription: cleanText( payload.deploymentDescription || payload.description || payload.accessDescription || config.deploymentDescription || '', 400 ) || null }; } function parsePartnerEmail(partnerDonationId, payload = {}) { const directEmail = normalizeEmail(payload.email || payload.user_email || payload.donor?.email || payload.donation?.email); if (directEmail.includes('@')) return directEmail; const raw = String(partnerDonationId || '').trim(); if (raw.includes('@')) { return normalizeEmail(raw.split('|')[0].split('::')[0]); } return ''; } async function getUserProfileByEmail(env, email) { const config = getSupabaseConfig(env); const normalizedEmail = normalizeEmail(email); if (!config || !normalizedEmail) return null; const rows = await supabaseFetch( config, `/rest/v1/user_profiles?email=eq.${encodeURIComponent(normalizedEmail)}&select=id,user_id,email,session_expiry&limit=1` ); return Array.isArray(rows) ? rows[0] || null : null; } async function upsertSoulwallSession(env, { email, userId = null, expiresAt, source, transactionId }) { const config = getSupabaseConfig(env); const normalizedEmail = normalizeEmail(email); if (!config || !normalizedEmail || !expiresAt) return null; const payload = { email: normalizedEmail, session_expiry: expiresAt, soulwall_source: source, soulwall_transaction_id: transactionId, updated_at: new Date().toISOString() }; if (userId) payload.user_id = userId; const rows = await supabaseFetch(config, '/rest/v1/user_profiles?on_conflict=email', { method: 'POST', headers: { prefer: 'resolution=merge-duplicates,return=representation' }, body: JSON.stringify(payload) }); return Array.isArray(rows) ? rows[0] || null : null; } async function createDonationIntent(env, { user, nonprofitSlug = '', provider = 'everyorg', externalId = '', partnerDonationId = '' }) { const config = getSupabaseConfig(env); const email = normalizeEmail(user?.email); if (!config || !user?.id || !email) return null; const uniqueIntentId = crypto.randomUUID(); const payload = { unique_intent_id: uniqueIntentId, partner_donation_id: partnerDonationId || uniqueIntentId, user_id: user.id, email, nonprofit_slug: nonprofitSlug, status: 'pending', provider, updated_at: new Date().toISOString() }; if (externalId) payload.external_id = externalId; const rows = await supabaseFetch(config, '/rest/v1/donation_intents', { method: 'POST', headers: { prefer: 'return=representation' }, body: JSON.stringify(payload) }); return Array.isArray(rows) ? rows[0] || null : null; } async function getDonationIntentById(env, uniqueIntentId) { const config = getSupabaseConfig(env); const id = String(uniqueIntentId || '').trim(); if (!config || !id) return null; const rows = await supabaseFetch( config, `/rest/v1/donation_intents?unique_intent_id=eq.${encodeURIComponent(id)}&select=*&limit=1` ); return Array.isArray(rows) ? rows[0] || null : null; } async function getDonationIntentByExternalId(env, externalId) { const config = getSupabaseConfig(env); const id = String(externalId || '').trim(); if (!config || !id) return null; const rows = await supabaseFetch( config, `/rest/v1/donation_intents?external_id=eq.${encodeURIComponent(id)}&select=*&limit=1` ); return Array.isArray(rows) ? rows[0] || null : null; } async function updateDonationIntentExternalId(env, { uniqueIntentId, provider, externalId }) { const config = getSupabaseConfig(env); const id = String(uniqueIntentId || '').trim(); const external = String(externalId || '').trim(); if (!config || !id || !external) return null; const payload = { external_id: external, updated_at: new Date().toISOString() }; if (provider) payload.provider = provider; const rows = await supabaseFetch( config, `/rest/v1/donation_intents?unique_intent_id=eq.${encodeURIComponent(id)}`, { method: 'PATCH', headers: { prefer: 'return=representation' }, body: JSON.stringify(payload) } ); return Array.isArray(rows) ? rows[0] || null : null; } async function markDonationIntentSucceeded(env, { uniqueIntentId, transactionId, accessExpiresAt, provider = '', externalId = '' }) { const config = getSupabaseConfig(env); const id = String(uniqueIntentId || '').trim(); if (!config || !id) return null; const payload = { status: 'succeeded', transaction_id: transactionId, access_expires_at: accessExpiresAt, updated_at: new Date().toISOString() }; if (provider) payload.provider = provider; if (externalId) payload.external_id = externalId; const rows = await supabaseFetch( config, `/rest/v1/donation_intents?unique_intent_id=eq.${encodeURIComponent(id)}`, { method: 'PATCH', headers: { prefer: 'return=representation' }, body: JSON.stringify(payload) } ); return Array.isArray(rows) ? rows[0] || null : null; } async function createHardshipRequest(env, { user, reason = '', note = '' }) { const config = getSupabaseConfig(env); const email = normalizeEmail(user?.email); if (!config || !user?.id || !email) return null; const rows = await supabaseFetch(config, '/rest/v1/hardship_requests', { method: 'POST', headers: { prefer: 'return=representation' }, body: JSON.stringify({ user_id: user.id, email, reason, note, status: 'pending', updated_at: new Date().toISOString() }) }); return Array.isArray(rows) ? rows[0] || null : null; } async function upsertLedgerEvent(env, { eventId = '', txHash = '', transactionId = '', clientId = '', integrationOrigin = '', request = null, sourceWebsite = '', deploymentTitle = '', deploymentDescription = '', provider = 'unknown', projectSlug = '', projectName = '', currency = 'USD', amountUsd = null, amountNative = null, network = 'unknown', status = 'confirmed', accessExpiresAt = '', confirmedAt = '', rawEvent = null } = {}) { const config = getSupabaseConfig(env); if (!config) return null; const credit = resolveLedgerCredit(env, request, { clientId, integrationOrigin, sourceWebsite, deploymentTitle, deploymentDescription }); const normalizedTxHash = cleanText(txHash, 180); const normalizedTransactionId = cleanText(transactionId, 180); const normalizedProvider = cleanText(provider, 80) || 'unknown'; const normalizedEventId = cleanText( eventId || normalizedTxHash || `${normalizedProvider}:${normalizedTransactionId || crypto.randomUUID()}`, 180 ); if (!normalizedEventId) return null; const row = { event_id: normalizedEventId, client_id: cleanText(credit.clientId, 120), integration_origin: cleanText( credit.integrationOrigin || env.SOULWALL_LEDGER_ORIGIN || env.PUBLIC_ORIGIN || '', 220 ) || null, provider: normalizedProvider, project_slug: cleanText(projectSlug, 160), project_name: cleanText(projectName, 220), tx_hash: normalizedTxHash || null, currency: cleanText(currency || 'USD', 16).toUpperCase(), amount_usd: cleanNumber(amountUsd), amount_native: cleanNumber(amountNative), network: cleanText(network || 'unknown', 64), status: cleanStatus(status), access_expires_at: cleanText(accessExpiresAt, 64) || null, confirmed_at: cleanText(confirmedAt, 64) || new Date().toISOString(), updated_at: new Date().toISOString(), raw_event: { ...(rawEvent && typeof rawEvent === 'object' ? rawEvent : {}), deploymentTitle: credit.deploymentTitle || null, deploymentDescription: credit.deploymentDescription || null, sourceWebsite: credit.sourceWebsite || null } }; const rows = await supabaseFetch(config, '/rest/v1/soulwall_ledger?on_conflict=event_id', { method: 'POST', headers: { prefer: 'resolution=merge-duplicates,return=representation' }, body: JSON.stringify(row) }); return Array.isArray(rows) ? rows[0] || null : null; } async function rewriteLedgerEvents(env, updates = []) { const config = getSupabaseConfig(env); if (!config || !Array.isArray(updates) || !updates.length) return []; const results = []; for (const update of updates) { const eventId = cleanText(update.eventId || update.event_id || update.txHash || update.tx_hash, 180); if (!eventId) continue; const row = { client_id: cleanText(update.clientId || update.client_id, 120) || null, integration_origin: cleanOrigin(update.integrationOrigin || update.integration_origin || '') || null, deployment_title: cleanText(update.deploymentTitle || update.deployment_title, 220) || null, deployment_description: cleanText(update.deploymentDescription || update.deployment_description, 400) || null, source_website: cleanUrl(update.sourceWebsite || update.source_website || '') || null, updated_at: new Date().toISOString() }; Object.keys(row).forEach((key) => { if (row[key] === null && key !== 'updated_at') delete row[key]; }); const rows = await supabaseFetch( config, `/rest/v1/soulwall_ledger?event_id=eq.${encodeURIComponent(eventId)}`, { method: 'PATCH', headers: { prefer: 'return=representation' }, body: JSON.stringify(row) } ); if (Array.isArray(rows) && rows[0]) { results.push(rows[0]); } } return results; } export { createDonationIntent, createHardshipRequest, getDonationIntentById, getDonationIntentByExternalId, getLedgerCreditConfig, getSupabaseConfig, getSupabaseUser, getUserProfileByEmail, markDonationIntentSucceeded, parsePartnerEmail, resolveLedgerCredit, rewriteLedgerEvents, supabaseFetch, updateDonationIntentExternalId, upsertLedgerEvent, upsertSoulwallSession };