webhook.ts 1.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. import { createHmac } from "node:crypto";
  2. export interface WebhookConfig {
  3. readonly url: string;
  4. readonly secret?: string;
  5. readonly events?: ReadonlyArray<string>;
  6. }
  7. export type WebhookEvent =
  8. | "chapter-complete"
  9. | "audit-passed"
  10. | "audit-failed"
  11. | "revision-complete"
  12. | "pipeline-complete"
  13. | "pipeline-error"
  14. | "diagnostic-alert";
  15. export interface WebhookPayload {
  16. readonly event: WebhookEvent;
  17. readonly bookId: string;
  18. readonly chapterNumber?: number;
  19. readonly timestamp: string;
  20. readonly data?: Record<string, unknown>;
  21. }
  22. export async function sendWebhook(
  23. config: WebhookConfig,
  24. payload: WebhookPayload,
  25. ): Promise<void> {
  26. // Filter by subscribed events
  27. if (config.events && config.events.length > 0 && !config.events.includes(payload.event)) {
  28. return;
  29. }
  30. const body = JSON.stringify(payload);
  31. const headers: Record<string, string> = {
  32. "Content-Type": "application/json",
  33. };
  34. // HMAC-SHA256 signature if secret is configured
  35. if (config.secret) {
  36. const signature = createHmac("sha256", config.secret)
  37. .update(body)
  38. .digest("hex");
  39. headers["X-InkOS-Signature"] = `sha256=${signature}`;
  40. }
  41. const response = await fetch(config.url, {
  42. method: "POST",
  43. headers,
  44. body,
  45. });
  46. if (!response.ok) {
  47. const responseBody = await response.text();
  48. throw new Error(`Webhook POST to ${config.url} failed: ${response.status} ${responseBody}`);
  49. }
  50. }