/** * Webhook dispatcher — fire-and-forget POST to a configurable callback URL. * * Products register webhook URLs via env vars: * WEBHOOK_INVITATION_REDEEMED_URL — called after an invitation code is redeemed * WEBHOOK_REFERRAL_STATUS_URL — called when a referral status transitions * WEBHOOK_WAITLIST_JOINED_URL — called when someone joins a pre-launch waitlist * * Payloads are JSON; failures are logged but never block the caller. */ import { DEFAULT_PRODUCT_ID } from './product-config.js'; export interface WebhookPayload { event: string; productId: string; timestamp: string; data: Record; } /** * POST a webhook payload to the given URL. Fire-and-forget: errors are logged, * never thrown. Returns true if the POST succeeded (2xx), false otherwise. */ export async function dispatchWebhook( url: string | undefined, event: string, data: Record, productId?: string ): Promise { if (!url) return false; const payload: WebhookPayload = { event, productId: productId ?? DEFAULT_PRODUCT_ID, timestamp: new Date().toISOString(), data, }; try { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: AbortSignal.timeout(5_000), }); return res.ok; } catch (err) { // Log but never block — use stderr for structured output process.stderr.write(`[webhook] Failed to dispatch ${event} to ${url}: ${err}\n`); return false; } } /** Dispatch an invitation.redeemed webhook. */ export function dispatchInvitationRedeemed(data: Record): Promise { return dispatchWebhook(process.env.WEBHOOK_INVITATION_REDEEMED_URL, 'invitation.redeemed', data); } /** Dispatch a referral.status_changed webhook. */ export function dispatchReferralStatusChanged(data: Record): Promise { return dispatchWebhook(process.env.WEBHOOK_REFERRAL_STATUS_URL, 'referral.status_changed', data); } /** Dispatch a waitlist.joined webhook. */ export function dispatchWaitlistJoined(data: Record): Promise { return dispatchWebhook(process.env.WEBHOOK_WAITLIST_JOINED_URL, 'waitlist.joined', data); }