@ariaflowagents/messaging-meta 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +236 -0
  2. package/dist/graph-api/client.d.ts +125 -0
  3. package/dist/graph-api/client.d.ts.map +1 -0
  4. package/dist/graph-api/client.js +204 -0
  5. package/dist/graph-api/client.js.map +1 -0
  6. package/dist/graph-api/errors.d.ts +41 -0
  7. package/dist/graph-api/errors.d.ts.map +1 -0
  8. package/dist/graph-api/errors.js +72 -0
  9. package/dist/graph-api/errors.js.map +1 -0
  10. package/dist/graph-api/index.d.ts +15 -0
  11. package/dist/graph-api/index.d.ts.map +1 -0
  12. package/dist/graph-api/index.js +11 -0
  13. package/dist/graph-api/index.js.map +1 -0
  14. package/dist/graph-api/rate-limiter.d.ts +90 -0
  15. package/dist/graph-api/rate-limiter.d.ts.map +1 -0
  16. package/dist/graph-api/rate-limiter.js +172 -0
  17. package/dist/graph-api/rate-limiter.js.map +1 -0
  18. package/dist/graph-api/retry.d.ts +55 -0
  19. package/dist/graph-api/retry.d.ts.map +1 -0
  20. package/dist/graph-api/retry.js +103 -0
  21. package/dist/graph-api/retry.js.map +1 -0
  22. package/dist/index.d.ts +36 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +38 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/instagram/client.d.ts +365 -0
  27. package/dist/instagram/client.d.ts.map +1 -0
  28. package/dist/instagram/client.js +857 -0
  29. package/dist/instagram/client.js.map +1 -0
  30. package/dist/instagram/format.d.ts +62 -0
  31. package/dist/instagram/format.d.ts.map +1 -0
  32. package/dist/instagram/format.js +92 -0
  33. package/dist/instagram/format.js.map +1 -0
  34. package/dist/instagram/ice-breakers.d.ts +63 -0
  35. package/dist/instagram/ice-breakers.d.ts.map +1 -0
  36. package/dist/instagram/ice-breakers.js +87 -0
  37. package/dist/instagram/ice-breakers.js.map +1 -0
  38. package/dist/instagram/index.d.ts +44 -0
  39. package/dist/instagram/index.d.ts.map +1 -0
  40. package/dist/instagram/index.js +46 -0
  41. package/dist/instagram/index.js.map +1 -0
  42. package/dist/instagram/types.d.ts +188 -0
  43. package/dist/instagram/types.d.ts.map +1 -0
  44. package/dist/instagram/types.js +19 -0
  45. package/dist/instagram/types.js.map +1 -0
  46. package/dist/messenger/client.d.ts +339 -0
  47. package/dist/messenger/client.d.ts.map +1 -0
  48. package/dist/messenger/client.js +782 -0
  49. package/dist/messenger/client.js.map +1 -0
  50. package/dist/messenger/format.d.ts +69 -0
  51. package/dist/messenger/format.d.ts.map +1 -0
  52. package/dist/messenger/format.js +98 -0
  53. package/dist/messenger/format.js.map +1 -0
  54. package/dist/messenger/index.d.ts +34 -0
  55. package/dist/messenger/index.d.ts.map +1 -0
  56. package/dist/messenger/index.js +35 -0
  57. package/dist/messenger/index.js.map +1 -0
  58. package/dist/messenger/types.d.ts +181 -0
  59. package/dist/messenger/types.d.ts.map +1 -0
  60. package/dist/messenger/types.js +10 -0
  61. package/dist/messenger/types.js.map +1 -0
  62. package/dist/server.d.ts +31 -0
  63. package/dist/server.d.ts.map +1 -0
  64. package/dist/server.js +29 -0
  65. package/dist/server.js.map +1 -0
  66. package/dist/webhook/index.d.ts +10 -0
  67. package/dist/webhook/index.d.ts.map +1 -0
  68. package/dist/webhook/index.js +8 -0
  69. package/dist/webhook/index.js.map +1 -0
  70. package/dist/webhook/normalizer.d.ts +169 -0
  71. package/dist/webhook/normalizer.d.ts.map +1 -0
  72. package/dist/webhook/normalizer.js +301 -0
  73. package/dist/webhook/normalizer.js.map +1 -0
  74. package/dist/webhook/verifier.d.ts +45 -0
  75. package/dist/webhook/verifier.d.ts.map +1 -0
  76. package/dist/webhook/verifier.js +62 -0
  77. package/dist/webhook/verifier.js.map +1 -0
  78. package/dist/whatsapp/client.d.ts +481 -0
  79. package/dist/whatsapp/client.d.ts.map +1 -0
  80. package/dist/whatsapp/client.js +1043 -0
  81. package/dist/whatsapp/client.js.map +1 -0
  82. package/dist/whatsapp/flows.d.ts +74 -0
  83. package/dist/whatsapp/flows.d.ts.map +1 -0
  84. package/dist/whatsapp/flows.js +77 -0
  85. package/dist/whatsapp/flows.js.map +1 -0
  86. package/dist/whatsapp/format.d.ts +78 -0
  87. package/dist/whatsapp/format.d.ts.map +1 -0
  88. package/dist/whatsapp/format.js +195 -0
  89. package/dist/whatsapp/format.js.map +1 -0
  90. package/dist/whatsapp/index.d.ts +39 -0
  91. package/dist/whatsapp/index.d.ts.map +1 -0
  92. package/dist/whatsapp/index.js +42 -0
  93. package/dist/whatsapp/index.js.map +1 -0
  94. package/dist/whatsapp/split.d.ts +35 -0
  95. package/dist/whatsapp/split.d.ts.map +1 -0
  96. package/dist/whatsapp/split.js +76 -0
  97. package/dist/whatsapp/split.js.map +1 -0
  98. package/dist/whatsapp/templates.d.ts +129 -0
  99. package/dist/whatsapp/templates.d.ts.map +1 -0
  100. package/dist/whatsapp/templates.js +125 -0
  101. package/dist/whatsapp/templates.js.map +1 -0
  102. package/dist/whatsapp/types.d.ts +440 -0
  103. package/dist/whatsapp/types.d.ts.map +1 -0
  104. package/dist/whatsapp/types.js +11 -0
  105. package/dist/whatsapp/types.js.map +1 -0
  106. package/package.json +31 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @module webhook/normalizer
3
+ *
4
+ * Normalize raw Meta webhook payloads into a flat, platform-agnostic shape.
5
+ *
6
+ * Meta delivers webhooks with a deeply nested structure that varies between
7
+ * WhatsApp Business, Messenger, and Instagram. This module flattens the
8
+ * nesting into three simple arrays — `messages`, `statuses`, and `reactions`
9
+ * — so downstream consumers can process events uniformly regardless of the
10
+ * originating platform.
11
+ *
12
+ * Inspired by the normalization approach used by Kapso and other Meta SDK
13
+ * wrappers.
14
+ */
15
+ /** Aggregated result of normalizing a single webhook delivery. */
16
+ export interface NormalizedWebhookEvents {
17
+ /** Inbound messages from users. */
18
+ messages: NormalizedMessage[];
19
+ /** Delivery / read / failure status updates. */
20
+ statuses: NormalizedStatus[];
21
+ /** Emoji reactions on existing messages. */
22
+ reactions: NormalizedReaction[];
23
+ }
24
+ /** A single inbound message extracted from the webhook payload. */
25
+ export interface NormalizedMessage {
26
+ /** Platform message ID. */
27
+ id: string;
28
+ /** Sender identifier (phone number for WhatsApp, PSID for Messenger/Instagram). */
29
+ from: string;
30
+ /** Unix timestamp (seconds) as a string. */
31
+ timestamp: string;
32
+ /** Message type (e.g. `"text"`, `"image"`, `"interactive"`, `"postback"`). */
33
+ type: string;
34
+ /** Phone number ID or page ID that received the message. */
35
+ phoneNumberId: string;
36
+ /** Display name of the contact, when available. */
37
+ contactName?: string;
38
+ text?: {
39
+ body: string;
40
+ };
41
+ image?: {
42
+ id: string;
43
+ caption?: string;
44
+ mime_type?: string;
45
+ };
46
+ video?: {
47
+ id: string;
48
+ caption?: string;
49
+ mime_type?: string;
50
+ };
51
+ audio?: {
52
+ id: string;
53
+ mime_type?: string;
54
+ };
55
+ document?: {
56
+ id: string;
57
+ filename?: string;
58
+ caption?: string;
59
+ mime_type?: string;
60
+ };
61
+ sticker?: {
62
+ id: string;
63
+ mime_type?: string;
64
+ };
65
+ location?: {
66
+ latitude: number;
67
+ longitude: number;
68
+ name?: string;
69
+ address?: string;
70
+ };
71
+ contacts?: unknown[];
72
+ interactive?: {
73
+ type: string;
74
+ button_reply?: {
75
+ id: string;
76
+ title: string;
77
+ };
78
+ list_reply?: {
79
+ id: string;
80
+ title: string;
81
+ description?: string;
82
+ };
83
+ };
84
+ button?: {
85
+ text: string;
86
+ payload: string;
87
+ };
88
+ /** Quoted/replied-to message context. */
89
+ context?: {
90
+ message_id: string;
91
+ from?: string;
92
+ };
93
+ /** Reaction (only set for WhatsApp reaction messages before they're split out). */
94
+ reaction?: {
95
+ message_id: string;
96
+ emoji: string;
97
+ };
98
+ /** Click-to-WhatsApp ad referral data. */
99
+ referral?: unknown;
100
+ }
101
+ /** A delivery status update. */
102
+ export interface NormalizedStatus {
103
+ /** Message ID this status refers to. */
104
+ id: string;
105
+ /** Recipient identifier. */
106
+ recipientId: string;
107
+ /** Status value. */
108
+ status: 'sent' | 'delivered' | 'read' | 'failed';
109
+ /** Unix timestamp (seconds) as a string. */
110
+ timestamp: string;
111
+ /** Phone number ID or page ID that sent the original message. */
112
+ phoneNumberId: string;
113
+ /** Conversation metadata (WhatsApp-specific). */
114
+ conversation?: {
115
+ id: string;
116
+ expiration_timestamp?: string;
117
+ origin?: {
118
+ type: string;
119
+ };
120
+ };
121
+ /** Pricing information (WhatsApp-specific). */
122
+ pricing?: {
123
+ billable: boolean;
124
+ pricing_model: string;
125
+ category: string;
126
+ };
127
+ /** Error details when `status === "failed"`. */
128
+ errors?: Array<{
129
+ code: number;
130
+ title?: string;
131
+ message?: string;
132
+ }>;
133
+ }
134
+ /** An emoji reaction on an existing message. */
135
+ export interface NormalizedReaction {
136
+ /** ID of the message that was reacted to. */
137
+ messageId: string;
138
+ /** Sender of the reaction. */
139
+ from: string;
140
+ /** Emoji used in the reaction (empty string means reaction was removed). */
141
+ emoji: string;
142
+ /** Phone number ID or page ID that received the reaction. */
143
+ phoneNumberId: string;
144
+ /** Unix timestamp (seconds) as a string. */
145
+ timestamp: string;
146
+ }
147
+ /**
148
+ * Normalize a raw Meta webhook payload into flat arrays of messages,
149
+ * statuses, and reactions.
150
+ *
151
+ * Supports:
152
+ * - **WhatsApp Business** (`object === "whatsapp_business_account"`)
153
+ * - **Messenger / Instagram** (`object === "page"` or `object === "instagram"`)
154
+ *
155
+ * Unknown `object` types return empty arrays without throwing.
156
+ *
157
+ * @param payload - Parsed JSON body from the webhook POST request.
158
+ * @returns Normalized events grouped by type.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * const events = normalizeWebhook(JSON.parse(rawBody));
163
+ * for (const msg of events.messages) {
164
+ * console.log(`${msg.from}: ${msg.text?.body}`);
165
+ * }
166
+ * ```
167
+ */
168
+ export declare function normalizeWebhook(payload: unknown): NormalizedWebhookEvents;
169
+ //# sourceMappingURL=normalizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizer.d.ts","sourceRoot":"","sources":["../../src/webhook/normalizer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,301 @@
1
+ /**
2
+ * @module webhook/normalizer
3
+ *
4
+ * Normalize raw Meta webhook payloads into a flat, platform-agnostic shape.
5
+ *
6
+ * Meta delivers webhooks with a deeply nested structure that varies between
7
+ * WhatsApp Business, Messenger, and Instagram. This module flattens the
8
+ * nesting into three simple arrays — `messages`, `statuses`, and `reactions`
9
+ * — so downstream consumers can process events uniformly regardless of the
10
+ * originating platform.
11
+ *
12
+ * Inspired by the normalization approach used by Kapso and other Meta SDK
13
+ * wrappers.
14
+ */
15
+ // ---------------------------------------------------------------------------
16
+ // Public API
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * Normalize a raw Meta webhook payload into flat arrays of messages,
20
+ * statuses, and reactions.
21
+ *
22
+ * Supports:
23
+ * - **WhatsApp Business** (`object === "whatsapp_business_account"`)
24
+ * - **Messenger / Instagram** (`object === "page"` or `object === "instagram"`)
25
+ *
26
+ * Unknown `object` types return empty arrays without throwing.
27
+ *
28
+ * @param payload - Parsed JSON body from the webhook POST request.
29
+ * @returns Normalized events grouped by type.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const events = normalizeWebhook(JSON.parse(rawBody));
34
+ * for (const msg of events.messages) {
35
+ * console.log(`${msg.from}: ${msg.text?.body}`);
36
+ * }
37
+ * ```
38
+ */
39
+ export function normalizeWebhook(payload) {
40
+ const result = {
41
+ messages: [],
42
+ statuses: [],
43
+ reactions: [],
44
+ };
45
+ if (!payload || typeof payload !== 'object') {
46
+ return result;
47
+ }
48
+ const p = payload;
49
+ if (p.object === 'whatsapp_business_account') {
50
+ return normalizeWhatsAppWebhook(p);
51
+ }
52
+ if (p.object === 'page' || p.object === 'instagram') {
53
+ return normalizePageWebhook(p);
54
+ }
55
+ return result;
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // WhatsApp Business normalization
59
+ // ---------------------------------------------------------------------------
60
+ /**
61
+ * Normalize a WhatsApp Business Account webhook payload.
62
+ *
63
+ * Structure: `entry[].changes[]` where `field === "messages"`.
64
+ * Each change's `value` contains `messages[]`, `statuses[]`, and `contacts[]`.
65
+ */
66
+ function normalizeWhatsAppWebhook(payload) {
67
+ const result = {
68
+ messages: [],
69
+ statuses: [],
70
+ reactions: [],
71
+ };
72
+ const entries = payload.entry ?? [];
73
+ for (const entry of entries) {
74
+ const changes = entry.changes ?? [];
75
+ for (const change of changes) {
76
+ if (change.field !== 'messages')
77
+ continue;
78
+ const value = change.value;
79
+ if (!value)
80
+ continue;
81
+ const phoneNumberId = value.metadata?.phone_number_id ?? '';
82
+ // Build a contact-name lookup from the contacts array.
83
+ const contactNames = new Map();
84
+ for (const contact of value.contacts ?? []) {
85
+ if (contact.wa_id && contact.profile?.name) {
86
+ contactNames.set(contact.wa_id, contact.profile.name);
87
+ }
88
+ }
89
+ // --- Messages ---
90
+ for (const msg of value.messages ?? []) {
91
+ // Reactions are delivered as messages with type "reaction" — split them out.
92
+ if (msg.type === 'reaction' && msg.reaction) {
93
+ result.reactions.push({
94
+ messageId: msg.reaction.message_id ?? '',
95
+ from: msg.from ?? '',
96
+ emoji: msg.reaction.emoji ?? '',
97
+ phoneNumberId,
98
+ timestamp: msg.timestamp ?? '',
99
+ });
100
+ continue;
101
+ }
102
+ const normalized = {
103
+ id: msg.id ?? '',
104
+ from: msg.from ?? '',
105
+ timestamp: msg.timestamp ?? '',
106
+ type: msg.type ?? 'unknown',
107
+ phoneNumberId,
108
+ contactName: contactNames.get(msg.from ?? ''),
109
+ };
110
+ // Attach type-specific payload.
111
+ if (msg.text)
112
+ normalized.text = { body: msg.text.body ?? '' };
113
+ if (msg.image)
114
+ normalized.image = msg.image;
115
+ if (msg.video)
116
+ normalized.video = msg.video;
117
+ if (msg.audio)
118
+ normalized.audio = msg.audio;
119
+ if (msg.document)
120
+ normalized.document = msg.document;
121
+ if (msg.sticker)
122
+ normalized.sticker = msg.sticker;
123
+ if (msg.location)
124
+ normalized.location = msg.location;
125
+ if (msg.contacts)
126
+ normalized.contacts = msg.contacts;
127
+ if (msg.interactive)
128
+ normalized.interactive = msg.interactive;
129
+ if (msg.button)
130
+ normalized.button = msg.button;
131
+ if (msg.context)
132
+ normalized.context = msg.context;
133
+ if (msg.referral)
134
+ normalized.referral = msg.referral;
135
+ result.messages.push(normalized);
136
+ }
137
+ // --- Statuses ---
138
+ for (const status of value.statuses ?? []) {
139
+ const normalizedStatus = {
140
+ id: status.id ?? '',
141
+ recipientId: status.recipient_id ?? '',
142
+ status: normalizeStatusValue(status.status),
143
+ timestamp: status.timestamp ?? '',
144
+ phoneNumberId,
145
+ };
146
+ if (status.conversation) {
147
+ normalizedStatus.conversation = status.conversation;
148
+ }
149
+ if (status.pricing) {
150
+ normalizedStatus.pricing = status.pricing;
151
+ }
152
+ if (status.errors) {
153
+ normalizedStatus.errors = status.errors;
154
+ }
155
+ result.statuses.push(normalizedStatus);
156
+ }
157
+ }
158
+ }
159
+ return result;
160
+ }
161
+ // ---------------------------------------------------------------------------
162
+ // Page / Instagram normalization
163
+ // ---------------------------------------------------------------------------
164
+ /**
165
+ * Normalize a Messenger or Instagram webhook payload.
166
+ *
167
+ * Structure: `entry[].messaging[]` where each event has `sender.id`,
168
+ * `recipient.id`, and a timestamp.
169
+ */
170
+ function normalizePageWebhook(payload) {
171
+ const result = {
172
+ messages: [],
173
+ statuses: [],
174
+ reactions: [],
175
+ };
176
+ const entries = payload.entry ?? [];
177
+ for (const entry of entries) {
178
+ const pageId = entry.id ?? '';
179
+ const events = entry.messaging ?? [];
180
+ for (const event of events) {
181
+ const senderId = event.sender?.id ?? '';
182
+ const timestamp = String(event.timestamp ?? '');
183
+ // --- Inbound message ---
184
+ if (event.message && !event.message.is_echo) {
185
+ const msg = event.message;
186
+ const normalized = {
187
+ id: msg.mid ?? '',
188
+ from: senderId,
189
+ timestamp,
190
+ type: resolvePageMessageType(msg),
191
+ phoneNumberId: pageId,
192
+ };
193
+ if (msg.text) {
194
+ normalized.text = { body: msg.text };
195
+ }
196
+ // Attachments (images, video, audio, files).
197
+ if (msg.attachments && msg.attachments.length > 0) {
198
+ const first = msg.attachments[0];
199
+ const attachType = first.type ?? 'unknown';
200
+ if (attachType === 'image' && first.payload) {
201
+ normalized.image = { id: '', ...first.payload };
202
+ }
203
+ else if (attachType === 'video' && first.payload) {
204
+ normalized.video = { id: '', ...first.payload };
205
+ }
206
+ else if (attachType === 'audio' && first.payload) {
207
+ normalized.audio = { id: '', ...first.payload };
208
+ }
209
+ else if (attachType === 'file' && first.payload) {
210
+ normalized.document = { id: '', ...first.payload };
211
+ }
212
+ }
213
+ // Reply context.
214
+ if (msg.reply_to?.mid) {
215
+ normalized.context = { message_id: msg.reply_to.mid };
216
+ }
217
+ result.messages.push(normalized);
218
+ continue;
219
+ }
220
+ // --- Postback (button tap / get-started) ---
221
+ if (event.postback) {
222
+ const pb = event.postback;
223
+ result.messages.push({
224
+ id: pb.mid ?? `postback_${timestamp}`,
225
+ from: senderId,
226
+ timestamp,
227
+ type: 'postback',
228
+ phoneNumberId: pageId,
229
+ button: {
230
+ text: pb.title ?? '',
231
+ payload: pb.payload ?? '',
232
+ },
233
+ });
234
+ continue;
235
+ }
236
+ // --- Reaction ---
237
+ if (event.reaction) {
238
+ const r = event.reaction;
239
+ result.reactions.push({
240
+ messageId: r.mid ?? '',
241
+ from: senderId,
242
+ emoji: r.emoji ?? r.reaction ?? '',
243
+ phoneNumberId: pageId,
244
+ timestamp,
245
+ });
246
+ continue;
247
+ }
248
+ // --- Delivery receipt ---
249
+ if (event.delivery) {
250
+ for (const mid of event.delivery.mids ?? []) {
251
+ result.statuses.push({
252
+ id: mid,
253
+ recipientId: senderId,
254
+ status: 'delivered',
255
+ timestamp,
256
+ phoneNumberId: pageId,
257
+ });
258
+ }
259
+ continue;
260
+ }
261
+ // --- Read receipt ---
262
+ if (event.read) {
263
+ result.statuses.push({
264
+ id: `read_${timestamp}`,
265
+ recipientId: senderId,
266
+ status: 'read',
267
+ timestamp,
268
+ phoneNumberId: pageId,
269
+ });
270
+ continue;
271
+ }
272
+ }
273
+ }
274
+ return result;
275
+ }
276
+ // ---------------------------------------------------------------------------
277
+ // Helpers
278
+ // ---------------------------------------------------------------------------
279
+ /** Resolve the message type for a Messenger / Instagram message. */
280
+ function resolvePageMessageType(msg) {
281
+ if (msg.attachments && msg.attachments.length > 0) {
282
+ return msg.attachments[0]?.type ?? 'attachment';
283
+ }
284
+ if (msg.text) {
285
+ return 'text';
286
+ }
287
+ return 'unknown';
288
+ }
289
+ /** Normalise a status string to the union type, defaulting to `"sent"`. */
290
+ function normalizeStatusValue(raw) {
291
+ switch (raw) {
292
+ case 'sent':
293
+ case 'delivered':
294
+ case 'read':
295
+ case 'failed':
296
+ return raw;
297
+ default:
298
+ return 'sent';
299
+ }
300
+ }
301
+ //# sourceMappingURL=normalizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalizer.js","sourceRoot":"","sources":["../../src/webhook/normalizer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @module webhook/verifier
3
+ *
4
+ * HMAC-SHA256 signature verification for Meta webhook payloads.
5
+ *
6
+ * Meta signs every webhook delivery with a `X-Hub-Signature-256` header
7
+ * containing `sha256=<hex>`. This module validates that signature using
8
+ * the app secret, ensuring the payload was not tampered with in transit.
9
+ *
10
+ * @see https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verification-requests
11
+ */
12
+ /** Options for {@link verifySignature}. */
13
+ export interface VerifySignatureOptions {
14
+ /** The Meta app secret used as the HMAC key. */
15
+ appSecret: string;
16
+ /** Raw request body exactly as received (before any JSON parsing). */
17
+ rawBody: Buffer | string;
18
+ /** Value of the `X-Hub-Signature-256` header (e.g. `"sha256=abcdef..."`). */
19
+ signatureHeader: string;
20
+ }
21
+ /**
22
+ * Verify the HMAC-SHA256 signature of a Meta webhook payload.
23
+ *
24
+ * Uses timing-safe comparison to prevent timing attacks. Returns `false`
25
+ * (rather than throwing) when the signature is missing, malformed, or invalid
26
+ * so that callers can respond with an appropriate HTTP status.
27
+ *
28
+ * @param options - Verification parameters.
29
+ * @returns `true` if the signature is valid, `false` otherwise.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const valid = verifySignature({
34
+ * appSecret: process.env.META_APP_SECRET!,
35
+ * rawBody: request.rawBody,
36
+ * signatureHeader: request.headers['x-hub-signature-256'] ?? '',
37
+ * });
38
+ *
39
+ * if (!valid) {
40
+ * return new Response('Invalid signature', { status: 403 });
41
+ * }
42
+ * ```
43
+ */
44
+ export declare function verifySignature(options: VerifySignatureOptions): boolean;
45
+ //# sourceMappingURL=verifier.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/webhook/verifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,2CAA2C;AAC3C,MAAM,WAAW,sBAAsB;IACrC,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;CACzB;AAMD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CA6BxE"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @module webhook/verifier
3
+ *
4
+ * HMAC-SHA256 signature verification for Meta webhook payloads.
5
+ *
6
+ * Meta signs every webhook delivery with a `X-Hub-Signature-256` header
7
+ * containing `sha256=<hex>`. This module validates that signature using
8
+ * the app secret, ensuring the payload was not tampered with in transit.
9
+ *
10
+ * @see https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verification-requests
11
+ */
12
+ import { createHmac, timingSafeEqual } from 'node:crypto';
13
+ // ---------------------------------------------------------------------------
14
+ // Verification
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Verify the HMAC-SHA256 signature of a Meta webhook payload.
18
+ *
19
+ * Uses timing-safe comparison to prevent timing attacks. Returns `false`
20
+ * (rather than throwing) when the signature is missing, malformed, or invalid
21
+ * so that callers can respond with an appropriate HTTP status.
22
+ *
23
+ * @param options - Verification parameters.
24
+ * @returns `true` if the signature is valid, `false` otherwise.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const valid = verifySignature({
29
+ * appSecret: process.env.META_APP_SECRET!,
30
+ * rawBody: request.rawBody,
31
+ * signatureHeader: request.headers['x-hub-signature-256'] ?? '',
32
+ * });
33
+ *
34
+ * if (!valid) {
35
+ * return new Response('Invalid signature', { status: 403 });
36
+ * }
37
+ * ```
38
+ */
39
+ export function verifySignature(options) {
40
+ const { appSecret, rawBody, signatureHeader } = options;
41
+ // The header must start with the `sha256=` prefix.
42
+ if (!signatureHeader || !signatureHeader.startsWith('sha256=')) {
43
+ return false;
44
+ }
45
+ const expectedSignature = signatureHeader.slice(7); // Strip `sha256=` prefix.
46
+ // Validate that the expected signature is plausible hex (64 hex chars for SHA-256).
47
+ if (!/^[0-9a-f]{64}$/i.test(expectedSignature)) {
48
+ return false;
49
+ }
50
+ const actualSignature = createHmac('sha256', appSecret)
51
+ .update(typeof rawBody === 'string' ? rawBody : rawBody)
52
+ .digest('hex');
53
+ // Use timing-safe comparison to prevent timing side-channel attacks.
54
+ try {
55
+ return timingSafeEqual(Buffer.from(expectedSignature, 'hex'), Buffer.from(actualSignature, 'hex'));
56
+ }
57
+ catch {
58
+ // `timingSafeEqual` throws if buffers have different lengths.
59
+ return false;
60
+ }
61
+ }
62
+ //# sourceMappingURL=verifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verifier.js","sourceRoot":"","sources":["../../src/webhook/verifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAgB1D,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,eAAe,CAAC,OAA+B;IAC7D,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC;IAExD,mDAAmD;IACnD,IAAI,CAAC,eAAe,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,iBAAiB,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,0BAA0B;IAE9E,oFAAoF;IACpF,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,iBAAiB,CAAC,EAAE,CAAC;QAC/C,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,eAAe,GAAG,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC;SACpD,MAAM,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;SACvD,MAAM,CAAC,KAAK,CAAC,CAAC;IAEjB,qEAAqE;IACrE,IAAI,CAAC;QACH,OAAO,eAAe,CACpB,MAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE,KAAK,CAAC,EACrC,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,KAAK,CAAC,CACpC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;QAC9D,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}