- Create waitlist/types.ts: WaitlistEntryDoc, Zod schemas for join/status/unsubscribe/admin - Create waitlist/repository.ts: full CRUD, dedup by emailNormalized, position assignment, stats - Create waitlist/routes.ts: 5 public endpoints + 7 admin endpoints with role guard - Add waitlist container to cosmos-init.ts (+ 13 previously missing containers) - Add dispatchWaitlistJoined webhook to webhooks.ts - Register waitlistRoutes in server.ts - Public: join, check status, count, config, unsubscribe - Admin: list, stats, get, update, delete, batch invite, CSV export
69 lines
2.2 KiB
TypeScript
69 lines
2.2 KiB
TypeScript
/**
|
|
* 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<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* 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<string, unknown>,
|
|
productId?: string
|
|
): Promise<boolean> {
|
|
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<string, unknown>): Promise<boolean> {
|
|
return dispatchWebhook(process.env.WEBHOOK_INVITATION_REDEEMED_URL, 'invitation.redeemed', data);
|
|
}
|
|
|
|
/** Dispatch a referral.status_changed webhook. */
|
|
export function dispatchReferralStatusChanged(data: Record<string, unknown>): Promise<boolean> {
|
|
return dispatchWebhook(process.env.WEBHOOK_REFERRAL_STATUS_URL, 'referral.status_changed', data);
|
|
}
|
|
|
|
/** Dispatch a waitlist.joined webhook. */
|
|
export function dispatchWaitlistJoined(data: Record<string, unknown>): Promise<boolean> {
|
|
return dispatchWebhook(process.env.WEBHOOK_WAITLIST_JOINED_URL, 'waitlist.joined', data);
|
|
}
|