Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | 6x 2x 4x 4x 4x 4x 4x 2x 2x 2x 9x 9x 9x 7x 6x 6x 7x 5x 4x 5x 9x 6x 6x 5x 4x 3x 3x 6x 10x 10x 9x 8x 7x 7x 3x 10x | import type {
WhatsAppWebhookPayload,
WhatsAppInboundMessage,
WhatsAppStatusUpdate,
WhatsAppTemplateStatusWebhook,
} from "./types";
export type ExtractedMessage = {
message: WhatsAppInboundMessage;
contactName: string;
phoneNumberId: string;
};
/**
* Verify the X-Hub-Signature-256 header from Meta webhooks.
* Uses Node.js crypto (supported in CF Workers via nodejs_compat).
*/
export async function verifyWebhookSignature(
rawBody: string,
signature: string,
appSecret: string,
): Promise<boolean> {
if (!signature?.startsWith("sha256=")) {
return false;
}
const received = signature.slice(7);
try {
const crypto = await import("node:crypto");
const expected = crypto
.createHmac("sha256", appSecret)
.update(rawBody)
.digest("hex");
// Constant-time comparison to prevent timing attacks
if (expected.length !== received.length) return false;
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
return crypto.timingSafeEqual(a, b);
/* v8 ignore next 2 -- defensive catch; crypto dynamic import failure unreachable in tests */
} catch { return false; }
}
/**
* Extract inbound messages from a webhook payload.
*/
export function extractMessages(
payload: WhatsAppWebhookPayload,
): ExtractedMessage[] {
const results: ExtractedMessage[] = [];
for (const entry of payload.entry ?? []) {
for (const change of entry.changes ?? []) {
if (change.field !== "messages") continue;
const value = change.value;
const phoneNumberId = value.metadata?.phone_number_id ?? "";
for (const message of value.messages ?? []) {
const contact = value.contacts?.find(
(c) => c.wa_id === message.from,
);
results.push({
message,
contactName: contact?.profile?.name ?? "",
phoneNumberId,
});
}
}
}
return results;
}
/**
* Extract status updates from a webhook payload.
*/
export function extractStatuses(
payload: WhatsAppWebhookPayload,
): WhatsAppStatusUpdate[] {
const results: WhatsAppStatusUpdate[] = [];
for (const entry of payload.entry ?? []) {
for (const change of entry.changes ?? []) {
if (change.field !== "messages") continue;
for (const status of change.value.statuses ?? []) {
results.push(status);
}
}
}
return results;
}
/**
* Extract template status updates from a webhook payload.
*/
export function extractTemplateStatuses(
payload: WhatsAppWebhookPayload,
): WhatsAppTemplateStatusWebhook[] {
const results: WhatsAppTemplateStatusWebhook[] = [];
for (const entry of payload.entry ?? []) {
for (const change of entry.changes ?? []) {
if (change.field !== "message_template_status_update") continue;
const v = change.value;
if (v.event && v.message_template_id && v.message_template_name && v.message_template_language) {
results.push({
event: v.event,
message_template_id: v.message_template_id,
message_template_name: v.message_template_name,
message_template_language: v.message_template_language,
reason: v.reason,
});
}
}
}
return results;
}
|