meta-cloud-api 3.8.0 → 3.9.0

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.
@@ -0,0 +1,377 @@
1
+ import { FlowActionEnum, FlowDataExchangeResponse, ImageMessage, InteractiveButtonReplyMessage, InteractiveListReplyMessage, LocationMessage, MessageWebhookValue, MetaErrorData, NonMessageWebhookField, ReactionMessage, StatusWebhook, StatusWebhookValue, TemplateResponse, TextMessage, WebhookFieldType, WebhookFieldValueMap, WebhookPayload, WebhookValue, WhatsAppMessage } from "../index.mjs";
2
+
3
+ //#region src/testing/flows.d.ts
4
+ /** Default URL for Flow endpoint requests built by {@link createFlowRequest}. */
5
+ declare const TEST_FLOW_URL = "https://localhost/flow";
6
+ /** The decrypted request body Meta sends to a Flow endpoint. */
7
+ interface FlowRequestInit {
8
+ /** Defaults to `'3.0'`. */
9
+ version?: '3.0';
10
+ /** Defaults to `'data_exchange'`. */
11
+ action?: 'ping' | `${FlowActionEnum}`;
12
+ screen?: string;
13
+ /** Defaults to `'test-flow-token'` (not added to `ping` requests). */
14
+ flow_token?: string;
15
+ data?: Record<string, unknown>;
16
+ }
17
+ interface FlowRequestOptions {
18
+ /**
19
+ * Your Flow endpoint's RSA public key: an SPKI PEM (`-----BEGIN PUBLIC KEY-----`,
20
+ * as returned by `generateEncryption()` and {@link generateFlowKeyPair}) or a Web Crypto key.
21
+ */
22
+ publicKey: string | CryptoKey;
23
+ /**
24
+ * Key for the `X-Hub-Signature-256` header: the App Secret the processor is
25
+ * configured with. Leave it out to send an unsigned request.
26
+ */
27
+ appSecret?: string;
28
+ /** Request URL. Defaults to {@link TEST_FLOW_URL}. */
29
+ url?: string;
30
+ }
31
+ /** The AES key and IV a Flow request was encrypted with; needed to read the reply. */
32
+ interface FlowSessionKeys {
33
+ aesKey: Uint8Array;
34
+ initialVector: Uint8Array;
35
+ }
36
+ interface FlowTestRequest extends FlowSessionKeys {
37
+ /** Ready to pass to `processor.processFlow(request)` or a Flow route handler. */
38
+ request: Request;
39
+ /** The encrypted JSON body (`encrypted_aes_key`, `encrypted_flow_data`, `initial_vector`). */
40
+ body: string;
41
+ /** The plaintext that was encrypted. */
42
+ payload: FlowRequestInit;
43
+ /** Shorthand for `decryptFlowResponse(response, this)`. */
44
+ decryptResponse<T = FlowDataExchangeResponse>(response: FlowResponseLike): Promise<T>;
45
+ }
46
+ /** A Flow endpoint reply: the raw body, `processor.processFlow()`'s result, or a fetch `Response`. */
47
+ type FlowResponseLike = string | {
48
+ body: string;
49
+ } | Response;
50
+ interface FlowKeyPair {
51
+ publicKey: CryptoKey;
52
+ privateKey: CryptoKey;
53
+ /** SPKI PEM; what you upload to Meta. */
54
+ publicPem: string;
55
+ /** Unencrypted PKCS#8 PEM; pass as `privatePem` (no passphrase needed). */
56
+ privatePem: string;
57
+ }
58
+ /**
59
+ * Generate a throwaway 2048-bit RSA key pair for Flow tests with Web Crypto.
60
+ * Works on every runtime, unlike `generateEncryption()` which needs `node:crypto`.
61
+ */
62
+ declare function generateFlowKeyPair(): Promise<FlowKeyPair>;
63
+ /**
64
+ * Encrypt a Flow endpoint request the way Meta does: a fresh AES-128-GCM key
65
+ * encrypts the JSON, and RSA-OAEP (SHA-256) with your public key encrypts that
66
+ * AES key. Use the returned keys (or `decryptResponse`) to read the encrypted reply.
67
+ */
68
+ declare function createFlowRequest(init: FlowRequestInit, options: FlowRequestOptions): Promise<FlowTestRequest>;
69
+ /**
70
+ * Read a Flow endpoint reply. Encrypted replies (ping, data_exchange) are
71
+ * decrypted with the request's AES key and the bit-flipped IV, as Meta does;
72
+ * plain JSON replies (error notification acknowledgements) are parsed as is.
73
+ */
74
+ declare function decryptFlowResponse<T = FlowDataExchangeResponse>(response: FlowResponseLike, keys: FlowSessionKeys): Promise<T>;
75
+ //#endregion
76
+ //#region src/testing/mockCloudApi.d.ts
77
+ type MockHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
78
+ /** A request the mock intercepted. */
79
+ interface RecordedRequest {
80
+ method: string;
81
+ /** Full request URL. */
82
+ url: string;
83
+ host: string;
84
+ /** Path without the API version, e.g. `/1234567890/messages`. */
85
+ path: string;
86
+ /** API version from the URL, e.g. `v23.0`, when present. */
87
+ apiVersion: string | undefined;
88
+ /** Query string parameters (the last value wins for repeated keys). */
89
+ query: Record<string, string>;
90
+ /** Request headers with lower-cased names. */
91
+ headers: Record<string, string>;
92
+ /** How the body was encoded. */
93
+ bodyType: 'none' | 'json' | 'form' | 'multipart' | 'text';
94
+ /**
95
+ * Parsed body: the JSON value, an object of URL-encoded or multipart fields
96
+ * (multipart files stay `File`/`Blob`), the raw text, or `undefined`.
97
+ */
98
+ body: unknown;
99
+ }
100
+ /** What a route handler receives: the recorded request plus the `:params` it matched. */
101
+ interface MockRouteRequest extends RecordedRequest {
102
+ params: Record<string, string>;
103
+ }
104
+ /**
105
+ * A route handler returns a `Response`, any JSON-serializable value (sent with
106
+ * status 200), or `undefined` to fall through to the next matching route and
107
+ * finally to the built-in default.
108
+ */
109
+ type MockRouteHandler = (request: MockRouteRequest) => unknown;
110
+ interface MockRouteOptions {
111
+ /** Answer only this many requests, then step aside. Defaults to unlimited. */
112
+ times?: number;
113
+ }
114
+ /** An error in Meta's shape. Only `code` is required. */
115
+ type MetaErrorInit = Partial<MetaErrorData> & Pick<MetaErrorData, 'code'> & {
116
+ error_user_title?: string;
117
+ error_user_msg?: string;
118
+ };
119
+ /** A message payload the SDK sent to `POST /{PHONE_NUMBER_ID}/messages`. */
120
+ interface SentMessage {
121
+ messaging_product: 'whatsapp';
122
+ to: string;
123
+ type: string;
124
+ recipient_type?: string;
125
+ context?: {
126
+ message_id: string;
127
+ };
128
+ [field: string]: unknown;
129
+ }
130
+ /** A file uploaded to the mock with `media.uploadMedia()`. */
131
+ interface MockMedia {
132
+ id: string;
133
+ mimeType: string;
134
+ fileSize: number;
135
+ sha256: string;
136
+ bytes: Uint8Array;
137
+ }
138
+ interface MockCloudApiOptions {
139
+ /** Templates returned by `GET /{WABA_ID}/message_templates`. Defaults to one approved `hello_world`. */
140
+ templates?: TemplateResponse[];
141
+ /**
142
+ * fetch used for requests that are not for the Graph API (and the one
143
+ * {@link MockCloudApi.restore} puts back). Defaults to `globalThis.fetch`.
144
+ */
145
+ fetch?: typeof fetch;
146
+ /**
147
+ * What to do with a Graph API request no route or default answers:
148
+ * `'error'` (default) replies with a Meta error so the SDK throws,
149
+ * `'success'` replies `{ "success": true }`.
150
+ */
151
+ unhandled?: 'error' | 'success';
152
+ }
153
+ /**
154
+ * Build a Graph API error response in Meta's shape
155
+ * (`{ "error": { message, type, code, error_subcode?, error_data?, fbtrace_id } }`).
156
+ * The status defaults to 401 for auth errors, 429 for rate limits and 400 otherwise.
157
+ */
158
+ declare function createMetaErrorResponse(error: MetaErrorInit, status?: number): Response;
159
+ /**
160
+ * An in-memory Graph API. Install it on `globalThis.fetch` (the SDK uses the
161
+ * global fetch) or hand {@link MockCloudApi.fetch} to code that accepts one.
162
+ * Every Graph API request is recorded; non-Graph requests pass through.
163
+ */
164
+ declare class MockCloudApi {
165
+ /** Every Graph API request, oldest first. */
166
+ readonly requests: RecordedRequest[];
167
+ /** Files uploaded through `POST /{PHONE_NUMBER_ID}/media`, by media ID. */
168
+ readonly media: Map<string, MockMedia>;
169
+ /** Templates served by `GET /{WABA_ID}/message_templates`. */
170
+ templates: TemplateResponse[];
171
+ private routes;
172
+ private readonly passthrough;
173
+ private readonly unhandled;
174
+ private previousFetch;
175
+ constructor(options?: MockCloudApiOptions);
176
+ /** A fetch that answers Graph API requests from this mock. */
177
+ readonly fetch: typeof fetch;
178
+ /** Replace `globalThis.fetch` with {@link MockCloudApi.fetch}. Returns the mock for chaining. */
179
+ install(): this;
180
+ /** Put back the `globalThis.fetch` that {@link MockCloudApi.install} replaced. */
181
+ restore(): void;
182
+ /**
183
+ * Answer matching requests with `handler`. `path` is matched without the API
184
+ * version: `'/:phoneNumberId/messages'` (`:name` captures a segment, `*` matches
185
+ * any one) or a RegExp (named groups become params). `method` may be `'*'`.
186
+ * Later routes win. Returns a function that removes the route.
187
+ */
188
+ on(method: MockHttpMethod | '*', path: string | RegExp, handler: MockRouteHandler, options?: MockRouteOptions): () => void;
189
+ /**
190
+ * Reply to matching requests with a Meta error, so the SDK throws the
191
+ * matching `WhatsAppError` subclass (e.g. code 190 → `WhatsAppAuthorizationError`).
192
+ */
193
+ fail(method: MockHttpMethod | '*', path: string | RegExp, error: MetaErrorInit, options?: MockRouteOptions & {
194
+ status?: number;
195
+ }): () => void;
196
+ /** Requests matching `method` and `path` (same matching rules as {@link MockCloudApi.on}). */
197
+ requestsTo(method: MockHttpMethod | '*', path: string | RegExp): RecordedRequest[];
198
+ /**
199
+ * Message payloads sent to `POST /{PHONE_NUMBER_ID}/messages` (and
200
+ * `/marketing_messages`), oldest first. Read receipts and typing indicators
201
+ * are left out. Pass a type such as `'text'` to filter.
202
+ */
203
+ sentMessages(type?: string): SentMessage[];
204
+ /** The most recent {@link MockCloudApi.sentMessages} entry. */
205
+ lastSentMessage(type?: string): SentMessage | undefined;
206
+ /** Forget recorded requests and uploaded media; keep routes. */
207
+ clearRequests(): void;
208
+ /** Forget recorded requests, uploaded media and custom routes. */
209
+ reset(): void;
210
+ private respond;
211
+ private defaultResponse;
212
+ }
213
+ /**
214
+ * Create an in-memory Graph API for tests. Call `.install()` to route the SDK's
215
+ * requests to it and `.restore()` afterwards.
216
+ *
217
+ * ```ts
218
+ * const mock = createMockCloudApi().install();
219
+ * await new WhatsApp({ accessToken: 'test', phoneNumberId: 1 }).messages.text({ to: '15551234567', body: 'hi' });
220
+ * mock.sentMessages(); // [{ messaging_product: 'whatsapp', to: '15551234567', type: 'text', text: { body: 'hi' }, ... }]
221
+ * mock.restore();
222
+ * ```
223
+ */
224
+ declare function createMockCloudApi(options?: MockCloudApiOptions): MockCloudApi;
225
+ //#endregion
226
+ //#region src/testing/requests.d.ts
227
+ /** Default URL for requests built by the testing helpers. */
228
+ declare const TEST_WEBHOOK_URL = "https://localhost/webhook";
229
+ interface SignedWebhookRequestOptions {
230
+ /** App Secret used for the `X-Hub-Signature-256` HMAC (the same one the processor is configured with). */
231
+ appSecret: string;
232
+ /** Request URL. Defaults to {@link TEST_WEBHOOK_URL}. */
233
+ url?: string;
234
+ /** Extra headers. They can override the defaults, e.g. to send a broken signature. */
235
+ headers?: Record<string, string>;
236
+ }
237
+ /**
238
+ * Build the `POST` Meta sends to your webhook, signed with `appSecret` the way
239
+ * Meta signs it. Pass the result to `processor.processWebhook(request)` or to
240
+ * your framework route handler. Strings are sent unchanged; anything else is
241
+ * serialized with `JSON.stringify`.
242
+ */
243
+ declare function createSignedWebhookRequest(payload: WebhookPayload | string, options: SignedWebhookRequestOptions): Promise<Request>;
244
+ //#endregion
245
+ //#region src/testing/webhooks.d.ts
246
+ /** Phone number ID used when a factory is not given one. */
247
+ declare const TEST_PHONE_NUMBER_ID = "100000000000001";
248
+ /** Display phone number used when a factory is not given one. */
249
+ declare const TEST_DISPLAY_PHONE_NUMBER = "15550100000";
250
+ /** WhatsApp Business Account ID used when a factory is not given one. */
251
+ declare const TEST_WABA_ID = "100000000000002";
252
+ /** The `change` object for a webhook field, as Meta nests it under `entry[].changes[]`. */
253
+ type WebhookChangeFor<F extends WebhookFieldType> = F extends NonMessageWebhookField ? {
254
+ field: F;
255
+ value: WebhookFieldValueMap[F];
256
+ } : {
257
+ field: 'messages';
258
+ value: WebhookValue;
259
+ };
260
+ /** Any `change` object the SDK understands. */
261
+ type WebhookChange = WebhookChangeFor<WebhookFieldType>;
262
+ /**
263
+ * A webhook payload whose changes are narrowed to `C`. It is assignable to the
264
+ * SDK's `WebhookPayload`, so it can be passed anywhere one is expected.
265
+ */
266
+ type TestWebhookPayload<C extends WebhookChange = WebhookChange> = {
267
+ object: 'whatsapp_business_account';
268
+ entry: Array<{
269
+ id: string;
270
+ changes: Array<C>;
271
+ }>;
272
+ };
273
+ /** `messages` field value carrying messages of type `M`. */
274
+ type MessageWebhookValueOf<M extends WhatsAppMessage> = Omit<MessageWebhookValue, 'messages'> & {
275
+ messages: Array<M>;
276
+ };
277
+ /** Webhook payload delivering one inbound message of type `M`. */
278
+ type MessageWebhookPayload<M extends WhatsAppMessage = WhatsAppMessage> = TestWebhookPayload<{
279
+ field: 'messages';
280
+ value: MessageWebhookValueOf<M>;
281
+ }>;
282
+ /** Webhook payload delivering message status updates. */
283
+ type StatusWebhookPayload = TestWebhookPayload<{
284
+ field: 'messages';
285
+ value: StatusWebhookValue;
286
+ }>;
287
+ /** Options shared by every factory. */
288
+ interface WebhookTargetOptions {
289
+ /** Your business phone number ID. Defaults to {@link TEST_PHONE_NUMBER_ID}. */
290
+ phoneNumberId?: string | number;
291
+ /** Your business display phone number. Defaults to {@link TEST_DISPLAY_PHONE_NUMBER}. */
292
+ displayPhoneNumber?: string;
293
+ /** WhatsApp Business Account ID (`entry[].id`). Defaults to {@link TEST_WABA_ID}. */
294
+ wabaId?: string;
295
+ }
296
+ /** Options for factories that deliver a message sent by a customer. */
297
+ interface InboundMessageOptions extends WebhookTargetOptions {
298
+ /** Customer phone number (WhatsApp ID) that sent the message. */
299
+ from: string;
300
+ /** Customer profile name. Defaults to `'Test User'`. */
301
+ profileName?: string;
302
+ /** Message ID. Defaults to a random `wamid.` ID. */
303
+ id?: string;
304
+ /** Unix seconds (string or number) or a Date. Defaults to now. */
305
+ timestamp?: string | number | Date;
306
+ }
307
+ /**
308
+ * Wrap a field value in the `object` / `entry` / `changes` envelope Meta posts to
309
+ * your webhook. Works for every field the SDK types, for example
310
+ * `createWebhookPayload('account_update', { event: 'VERIFIED_ACCOUNT' })`.
311
+ */
312
+ declare function createWebhookPayload<F extends WebhookFieldType>(field: F, value: WebhookChangeFor<F>['value'], options?: Pick<WebhookTargetOptions, 'wabaId'>): TestWebhookPayload<WebhookChangeFor<F>>;
313
+ /**
314
+ * Deliver any inbound message object. The typed factories below build the
315
+ * message for you; use this one for types they do not cover (video, document, ...).
316
+ */
317
+ declare function createMessageWebhook<M extends WhatsAppMessage>(message: M, options?: WebhookTargetOptions & {
318
+ profileName?: string;
319
+ }): MessageWebhookPayload<M>;
320
+ /** A customer sent a text message. Dispatched to `processor.onText`. */
321
+ declare function createTextMessageWebhook(options: InboundMessageOptions & {
322
+ text: string;
323
+ }): MessageWebhookPayload<TextMessage>;
324
+ /** A customer sent an image. Dispatched to `processor.onImage`. */
325
+ declare function createImageMessageWebhook(options: InboundMessageOptions & {
326
+ /** Media ID to pass to `media.getMediaById`. Defaults to a random numeric ID. */mediaId?: string;
327
+ mimeType?: string;
328
+ sha256?: string;
329
+ caption?: string; /** Defaults to a lookaside URL built from the media ID. */
330
+ url?: string;
331
+ }): MessageWebhookPayload<ImageMessage>;
332
+ /** A customer tapped a reply button. Dispatched to `processor.onInteractive`. */
333
+ declare function createButtonReplyWebhook(options: InboundMessageOptions & {
334
+ /** The `id` you gave the button. */buttonId: string;
335
+ title: string; /** ID of your interactive message. Defaults to a random `wamid.` ID. */
336
+ contextMessageId?: string;
337
+ }): MessageWebhookPayload<InteractiveButtonReplyMessage>;
338
+ /** A customer picked a list row. Dispatched to `processor.onInteractive`. */
339
+ declare function createListReplyWebhook(options: InboundMessageOptions & {
340
+ /** The `id` you gave the row. */rowId: string;
341
+ title: string;
342
+ description?: string; /** ID of your list message. Defaults to a random `wamid.` ID. */
343
+ contextMessageId?: string;
344
+ }): MessageWebhookPayload<InteractiveListReplyMessage>;
345
+ /**
346
+ * A customer reacted to a message. Leave `emoji` out to simulate removing a
347
+ * reaction. Dispatched to `processor.onReaction`.
348
+ */
349
+ declare function createReactionWebhook(options: InboundMessageOptions & {
350
+ /** ID of the message that was reacted to. */messageId: string;
351
+ emoji?: string;
352
+ }): MessageWebhookPayload<ReactionMessage>;
353
+ /** A customer shared a location. Dispatched to `processor.onLocation`. */
354
+ declare function createLocationWebhook(options: InboundMessageOptions & {
355
+ latitude: number;
356
+ longitude: number;
357
+ name?: string;
358
+ address?: string;
359
+ url?: string;
360
+ }): MessageWebhookPayload<LocationMessage>;
361
+ /**
362
+ * WhatsApp reports a status for a message you sent. Dispatched to
363
+ * `processor.onStatus`. A `failed` status gets error 131026 unless `errors` is given.
364
+ */
365
+ declare function createStatusWebhook(options: WebhookTargetOptions & {
366
+ status: StatusWebhook['status']; /** Customer phone number the message was sent to. */
367
+ recipientId: string; /** ID of the message you sent (from `messages.text()` etc.). Defaults to a random `wamid.` ID. */
368
+ messageId?: string;
369
+ timestamp?: string | number | Date;
370
+ errors?: StatusWebhook['errors'];
371
+ pricing?: StatusWebhook['pricing'];
372
+ conversation?: StatusWebhook['conversation'];
373
+ bizOpaqueCallbackData?: string;
374
+ }): StatusWebhookPayload;
375
+ //#endregion
376
+ export { type FlowKeyPair, type FlowRequestInit, type FlowRequestOptions, type FlowResponseLike, type FlowSessionKeys, type FlowTestRequest, type InboundMessageOptions, type MessageWebhookPayload, type MessageWebhookValueOf, type MetaErrorInit, MockCloudApi, type MockCloudApiOptions, type MockHttpMethod, type MockMedia, type MockRouteHandler, type MockRouteOptions, type MockRouteRequest, type RecordedRequest, type SentMessage, type SignedWebhookRequestOptions, type StatusWebhookPayload, TEST_DISPLAY_PHONE_NUMBER, TEST_FLOW_URL, TEST_PHONE_NUMBER_ID, TEST_WABA_ID, TEST_WEBHOOK_URL, type TestWebhookPayload, type WebhookChange, type WebhookChangeFor, type WebhookTargetOptions, createButtonReplyWebhook, createFlowRequest, createImageMessageWebhook, createListReplyWebhook, createLocationWebhook, createMessageWebhook, createMetaErrorResponse, createMockCloudApi, createReactionWebhook, createSignedWebhookRequest, createStatusWebhook, createTextMessageWebhook, createWebhookPayload, decryptFlowResponse, generateFlowKeyPair };
377
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/testing/flows.ts","../../src/testing/mockCloudApi.ts","../../src/testing/requests.ts","../../src/testing/webhooks.ts"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import{CategoryEnum as e,LanguagesEnum as t,MessageTypesEnum as n,generateXHub256SigAsync as r}from"../index.mjs";const i=new TextEncoder,a=new TextDecoder;function o(e){return i.encode(e)}function s(e){return a.decode(e)}function c(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}function l(e){let t=``,n=32768;for(let r=0;r<e.length;r+=n)t+=String.fromCharCode(...e.subarray(r,r+n));return btoa(t)}function u(e){let t=``;for(let n of e)t+=n.toString(16).padStart(2,`0`);return t}const d=`https://localhost/flow`;function f(){let e=globalThis.crypto?.subtle;if(!e)throw Error(`Web Crypto (globalThis.crypto.subtle) is not available in this runtime.`);return e}function p(e,t){return`-----BEGIN ${e}-----\n${(l(new Uint8Array(t)).match(/.{1,64}/g)??[]).join(`
2
+ `)}\n-----END ${e}-----\n`}async function m(){let e=await f().generateKey({name:`RSA-OAEP`,modulusLength:2048,publicExponent:new Uint8Array([1,0,1]),hash:`SHA-256`},!0,[`encrypt`,`decrypt`]);return{publicKey:e.publicKey,privateKey:e.privateKey,publicPem:p(`PUBLIC KEY`,await f().exportKey(`spki`,e.publicKey)),privatePem:p(`PRIVATE KEY`,await f().exportKey(`pkcs8`,e.privateKey))}}async function h(e){if(typeof e!=`string`)return e;let t=e.includes(`\\n`)?e.replace(/\\n/g,`
3
+ `):e,n=/-----BEGIN PUBLIC KEY-----([\s\S]*?)-----END PUBLIC KEY-----/.exec(t);if(!n)throw Error(`createFlowRequest: publicKey must be an SPKI PEM ("-----BEGIN PUBLIC KEY-----"). Convert a PKCS#1 key with: openssl rsa -RSAPublicKey_in -in key.pem -pubout`);return f().importKey(`spki`,c((n[1]??``).replace(/\s+/g,``)),{name:`RSA-OAEP`,hash:`SHA-256`},!1,[`encrypt`])}function g(e){let t=e.action??`data_exchange`,n={version:`3.0`,...e,action:t};return t!==`ping`&&n.flow_token===void 0&&(n.flow_token=`test-flow-token`),n}async function _(e,t){let n=g(e),i=f(),a=globalThis.crypto.getRandomValues(new Uint8Array(16)),s=globalThis.crypto.getRandomValues(new Uint8Array(16)),c=await i.importKey(`raw`,a,{name:`AES-GCM`},!1,[`encrypt`]),u=JSON.stringify({encrypted_aes_key:l(new Uint8Array(await i.encrypt({name:`RSA-OAEP`},await h(t.publicKey),a))),encrypted_flow_data:l(new Uint8Array(await i.encrypt({name:`AES-GCM`,iv:s},c,o(JSON.stringify(n))))),initial_vector:l(s)}),d={"content-type":`application/json`};t.appSecret!==void 0&&(d[`x-hub-signature-256`]=`sha256=${await r(u,t.appSecret)}`);let p={aesKey:a,initialVector:s};return{request:new Request(t.url??`https://localhost/flow`,{method:`POST`,body:u,headers:d}),body:u,payload:n,aesKey:a,initialVector:s,decryptResponse:e=>y(e,p)}}async function v(e){return typeof e==`string`?e:e instanceof Response?e.text():e.body}async function y(e,t){let n=(await v(e)).trim();if(n.startsWith(`{`))return JSON.parse(n);let r=t.initialVector.map(e=>~e&255),i=f(),a=await i.importKey(`raw`,new Uint8Array(t.aesKey),{name:`AES-GCM`},!1,[`decrypt`]),o;try{o=await i.decrypt({name:`AES-GCM`,iv:r},a,c(n))}catch{throw Error(`decryptFlowResponse: could not decrypt the Flow response: ${n.slice(0,200)}`)}return JSON.parse(s(new Uint8Array(o)))}function b(e){return globalThis.crypto.getRandomValues(new Uint8Array(e))}function x(){return`wamid.HBgL${l(b(24))}`}function S(e){let t=String(1+(b(1)[0]??0)%9);for(let n of b(e-1))t+=String(n%10);return t}const C=`lookaside.fbsbx.com`,w=[{id:`100000000000010`,name:`hello_world`,status:`APPROVED`,language:t.English_US,category:e.Utility,components:[]}];function T(e){return[0,3,10,190].includes(e)?401:[4,80007,130429,131048,131056].includes(e)?429:400}function E(e,t=T(e.code)){return D({error:{message:e.message??`(#${e.code}) Mock error`,type:e.type??`OAuthException`,...e,fbtrace_id:e.fbtrace_id??`mock-${S(8)}`}},t)}function D(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"content-type":`application/json`}})}function O(e,t){if(e instanceof RegExp){let n=new RegExp(e.source,e.flags).exec(t);return n?{...n.groups}:void 0}let n=e.replace(/^\/+|\/+$/g,``).split(`/`),r=t.replace(/^\/+|\/+$/g,``).split(`/`);if(n.length!==r.length)return;let i={};for(let e=0;e<n.length;e++){let t=n[e]??``,a=r[e]??``;if(t.startsWith(`:`))i[t.slice(1)]=decodeURIComponent(a);else if(t!==`*`&&t!==a)return}return i}function k(e){let t={};return e.forEach((e,n)=>{let r=t[n];r===void 0?t[n]=e:t[n]=Array.isArray(r)?[...r,e]:[r,e]}),t}function A(e){let t={};return e.forEach((e,n)=>{let r=t[n];r===void 0?t[n]=e:t[n]=Array.isArray(r)?[...r,e]:[r,e]}),t}async function j(e,t){let n=t?.body;if(n instanceof FormData)return{bodyType:`multipart`,body:k(n)};if(n instanceof URLSearchParams)return{bodyType:`form`,body:A(n)};if(e.body===null)return{bodyType:`none`,body:void 0};let r=e.headers.get(`content-type`)??``;if(r.includes(`multipart/form-data`))return{bodyType:`multipart`,body:k(await e.formData())};let i=await e.text();if(r.includes(`application/x-www-form-urlencoded`))return{bodyType:`form`,body:A(new URLSearchParams(i))};if(i===``)return{bodyType:`none`,body:void 0};try{return{bodyType:`json`,body:JSON.parse(i)}}catch{return{bodyType:`text`,body:i}}}function M(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function N(e){return e.method===`POST`&&/^\/[^/]+\/(messages|marketing_messages)$/.test(e.path)&&M(e.body)&&typeof e.body.to==`string`&&e.body.status===void 0}var P=class{requests=[];media=new Map;templates;routes=[];passthrough;unhandled;previousFetch;constructor(e={}){let t=e.fetch??globalThis.fetch;this.passthrough=(e,n)=>t(e,n),this.templates=structuredClone(e.templates??w),this.unhandled=e.unhandled??`error`}fetch=async(e,t)=>{let n=new URL(typeof e==`string`?e:e instanceof URL?e.href:e.url);if(n.host!==`graph.facebook.com`&&n.host!==C)return this.passthrough(e,t);let r=new Request(e,t),i=/^\/(v\d+(?:\.\d+)?)(\/.*)?$/.exec(n.pathname),a={};r.headers.forEach((e,t)=>{a[t]=e});let o={method:r.method.toUpperCase(),url:r.url,host:n.host,path:i?i[2]||`/`:n.pathname,apiVersion:i?.[1],query:Object.fromEntries(n.searchParams),headers:a,...await j(r,t)};return this.requests.push(o),this.respond(o)};install(){return this.previousFetch===void 0&&(this.previousFetch=globalThis.fetch,globalThis.fetch=this.fetch),this}restore(){this.previousFetch!==void 0&&(globalThis.fetch=this.previousFetch,this.previousFetch=void 0)}on(e,t,n,r={}){let i={method:e,pattern:t,handler:n,remaining:r.times??1/0};return this.routes.unshift(i),()=>{this.routes=this.routes.filter(e=>e!==i)}}fail(e,t,n,r={}){return this.on(e,t,()=>E(n,r.status),r)}requestsTo(e,t){return this.requests.filter(n=>(e===`*`||n.method===e)&&O(t,n.path)!==void 0)}sentMessages(e){return this.requests.filter(N).map(e=>e.body).filter(t=>e===void 0||t.type===e)}lastSentMessage(e){return this.sentMessages(e).at(-1)}clearRequests(){this.requests.length=0,this.media.clear()}reset(){this.clearRequests(),this.routes=[]}async respond(e){for(let t of[...this.routes]){if(t.remaining<=0||t.method!==`*`&&t.method!==e.method)continue;let n=O(t.pattern,e.path);if(!n)continue;t.remaining--;let r;try{r=await t.handler({...e,params:n})}catch(e){throw t.remaining++,e}if(r===void 0){t.remaining++;continue}return r instanceof Response?r:D(r)}return this.defaultResponse(e)}async defaultResponse(e){let{method:t,path:n,host:r}=e,i=n.replace(/^\/+|\/+$/g,``).split(`/`);if(r===C){let t=this.media.get(e.query.mid??``);return new Response(t?new Uint8Array(t.bytes):`mock media`,{headers:{"content-type":t?.mimeType??`application/octet-stream`}})}if(t===`POST`&&i.length===2&&/^(messages|marketing_messages)$/.test(i[1]??``)){let t=M(e.body)?e.body:{};if(t.status===`read`||t.status===`typing`)return D({success:!0});let n=typeof t.to==`string`?t.to:``;return D({messaging_product:`whatsapp`,contacts:[{input:n,wa_id:n.replace(/\D/g,``)||n}],messages:[{id:x(),...t.type===`template`&&{message_status:`accepted`}}]})}if(t===`POST`&&i.length===2&&i[1]===`media`){let t=M(e.body)?e.body:{},n=t.file,r=S(16),i=n instanceof Blob?new Uint8Array(await n.arrayBuffer()):new Uint8Array,a=new Uint8Array(await globalThis.crypto.subtle.digest(`SHA-256`,i));return this.media.set(r,{id:r,mimeType:typeof t.type==`string`&&t.type||n instanceof Blob&&n.type||``,fileSize:i.byteLength,sha256:u(a),bytes:i}),D({id:r})}if(i.length===2&&i[1]===`message_templates`){if(t===`GET`){let{name:t,status:n,language:r,category:i}=e.query,a=this.templates.filter(e=>(!t||e.name===t)&&(!n||e.status===n)&&(!r||e.language===r)&&(!i||e.category===i)),o=Number(e.query.limit)||a.length;return D({data:a.slice(0,o),paging:{cursors:{before:`MAZDZD`,after:`MjQZD`},next:``}})}if(t===`POST`){let t=M(e.body)?e.body:{};return D({id:S(15),status:`PENDING`,category:t.category??`UTILITY`})}if(t===`DELETE`)return D({success:!0})}if(i.length===1&&i[0]){let e=i[0];if(t===`DELETE`)return this.media.delete(e),D({success:!0});if(t===`GET`){let t=this.media.get(e);return D({messaging_product:`whatsapp`,id:e,url:`https://${C}/whatsapp_business/attachments/?mid=${e}`,mime_type:t?.mimeType||`image/jpeg`,sha256:t?.sha256??`0`.repeat(64),file_size:t?.fileSize??0})}}return this.unhandled===`success`?D({success:!0}):E({code:100,type:`GraphMethodException`,message:`meta-cloud-api/testing: no mock route for ${t} ${n}. Add one with mock.on('${t}', '${n}', handler).`},400)}};function F(e){return new P(e)}const I=`https://localhost/webhook`;async function L(e,t){let n=typeof e==`string`?e:JSON.stringify(e),i=await r(n,t.appSecret),a=new Headers({"content-type":`application/json`,"x-hub-signature-256":`sha256=${i}`});for(let[e,n]of Object.entries(t.headers??{}))a.set(e,n);return new Request(t.url??`https://localhost/webhook`,{method:`POST`,body:n,headers:a})}const R=`100000000000001`,z=`15550100000`,B=`100000000000002`;function V(e){return e===void 0?String(Math.floor(Date.now()/1e3)):e instanceof Date?String(Math.floor(e.getTime()/1e3)):String(e)}function H(e){return{display_phone_number:e.displayPhoneNumber??`15550100000`,phone_number_id:String(e.phoneNumberId??`100000000000001`)}}function U(e){return{from:e.from,id:e.id??x(),timestamp:V(e.timestamp)}}function W(e){return{from:e.displayPhoneNumber??`15550100000`,id:e.contextMessageId??x()}}function G(e,t,n={}){return K({field:e,value:t},n)}function K(e,t){return{object:`whatsapp_business_account`,entry:[{id:t.wabaId??`100000000000002`,changes:[e]}]}}function q(e,t={}){return K({field:`messages`,value:{messaging_product:`whatsapp`,metadata:H(t),contacts:[{profile:{name:t.profileName??`Test User`},wa_id:e.from}],messages:[e]}},t)}function J(e){return q({...U(e),type:n.Text,text:{body:e.text}},e)}function Y(e){let t=e.mediaId??S(16),r={id:t,mime_type:e.mimeType??`image/jpeg`,sha256:e.sha256??`test-sha256`,url:e.url??`https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=${t}`};return e.caption!==void 0&&(r.caption=e.caption),q({...U(e),type:n.Image,image:r},e)}function X(e){return q({...U(e),type:n.Interactive,context:W(e),interactive:{type:`button_reply`,button_reply:{id:e.buttonId,title:e.title}}},e)}function Z(e){let t={id:e.rowId,title:e.title};return e.description!==void 0&&(t.description=e.description),q({...U(e),type:n.Interactive,context:W(e),interactive:{type:`list_reply`,list_reply:t}},e)}function Q(e){let t={message_id:e.messageId};return e.emoji!==void 0&&(t.emoji=e.emoji),q({...U(e),type:n.Reaction,reaction:t},e)}function $(e){let t={latitude:e.latitude,longitude:e.longitude};return e.name!==void 0&&(t.name=e.name),e.address!==void 0&&(t.address=e.address),e.url!==void 0&&(t.url=e.url),q({...U(e),type:n.Location,location:t},e)}const ee={code:131026,title:`Message undeliverable`,message:`Message undeliverable`,error_data:{details:`Message Undeliverable.`}};function te(e){let t={id:e.messageId??x(),status:e.status,timestamp:V(e.timestamp),recipient_id:e.recipientId};return e.conversation&&(t.conversation=e.conversation),e.pricing&&(t.pricing=e.pricing),e.bizOpaqueCallbackData!==void 0&&(t.biz_opaque_callback_data=e.bizOpaqueCallbackData),e.errors?t.errors=e.errors:e.status===`failed`&&(t.errors=[structuredClone(ee)]),K({field:`messages`,value:{messaging_product:`whatsapp`,metadata:H(e),statuses:[t]}},e)}export{P as MockCloudApi,z as TEST_DISPLAY_PHONE_NUMBER,d as TEST_FLOW_URL,R as TEST_PHONE_NUMBER_ID,B as TEST_WABA_ID,I as TEST_WEBHOOK_URL,X as createButtonReplyWebhook,_ as createFlowRequest,Y as createImageMessageWebhook,Z as createListReplyWebhook,$ as createLocationWebhook,q as createMessageWebhook,E as createMetaErrorResponse,F as createMockCloudApi,Q as createReactionWebhook,L as createSignedWebhookRequest,te as createStatusWebhook,J as createTextMessageWebhook,G as createWebhookPayload,y as decryptFlowResponse,m as generateFlowKeyPair};
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/utils/runtime.ts","../../src/testing/flows.ts","../../src/testing/random.ts","../../src/testing/mockCloudApi.ts","../../src/testing/requests.ts","../../src/testing/webhooks.ts"],"sourcesContent":["import { nodeCrypto } from './runtimeCrypto';\n\n/**\n * Runtime helpers that keep the SDK free of static `node:*` imports, so the\n * universal bundle loads on Bun, Deno, Cloudflare Workers and Vercel Edge.\n * Node conditional entries inject crypto for versions lacking getBuiltinModule.\n */\n\ntype NodeCrypto = typeof import('node:crypto');\n\ntype ProcessLike = {\n env?: Record<string, string | undefined>;\n version?: string;\n versions?: Record<string, string | undefined>;\n getBuiltinModule?: (id: string) => unknown;\n};\n\nfunction getProcess(): ProcessLike | undefined {\n const proc = (globalThis as { process?: ProcessLike }).process;\n return typeof proc === 'object' && proc !== null ? proc : undefined;\n}\n\n/**\n * Read an environment variable. Returns undefined on runtimes without\n * `process.env` (e.g. Cloudflare Workers without `nodejs_compat`).\n */\nexport function readEnv(name: string): string | undefined {\n return getProcess()?.env?.[name];\n}\n\n/** True when the `DEBUG` environment variable is `'true'`. */\nexport function isDebugEnv(): boolean {\n return readEnv('DEBUG') === 'true';\n}\n\n/**\n * Load `node:crypto` synchronously without a static import, via\n * `process.getBuiltinModule` (Node.js >= 20.16, Bun, Deno, workerd with\n * `nodejs_compat`), or the Node entry module. Returns undefined\n * when it is not available.\n */\nexport function getNodeCrypto(): NodeCrypto | undefined {\n const getBuiltinModule = getProcess()?.getBuiltinModule;\n if (typeof getBuiltinModule !== 'function') return getProcess() ? nodeCrypto : undefined;\n try {\n return getBuiltinModule('node:crypto') as NodeCrypto;\n } catch {\n return undefined;\n }\n}\n\n/** Like {@link getNodeCrypto} but throws a descriptive error when missing. */\nexport function requireNodeCrypto(feature: string): NodeCrypto {\n const crypto = getNodeCrypto();\n if (!crypto) {\n throw new Error(\n `${feature} needs the node:crypto module, which is not available in this runtime. ` +\n 'Use the async Web Crypto variant instead, or run on Node.js >= 20.12, Bun or Deno.',\n );\n }\n return crypto;\n}\n\n/** Web Crypto `SubtleCrypto`, available on every supported runtime. */\nexport function getSubtle(): SubtleCrypto {\n const subtle = globalThis.crypto?.subtle;\n if (!subtle) {\n throw new Error('Web Crypto (globalThis.crypto.subtle) is not available in this runtime.');\n }\n return subtle;\n}\n\n/** Human-readable runtime name for the User-Agent header. */\nexport function describeRuntime(): string {\n const g = globalThis as {\n Bun?: { version?: string };\n Deno?: { version?: { deno?: string } };\n EdgeRuntime?: string;\n navigator?: { userAgent?: string };\n };\n if (g.Bun?.version) return `Bun ${g.Bun.version}`;\n if (g.Deno?.version?.deno) return `Deno ${g.Deno.version.deno}`;\n if (typeof g.EdgeRuntime === 'string') return `Edge Runtime ${g.EdgeRuntime}`;\n if (g.navigator?.userAgent === 'Cloudflare-Workers') return 'Cloudflare Workers';\n const version = getProcess()?.version;\n if (version) return `Node.js ${version}`;\n return 'unknown runtime';\n}\n\nconst textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\n\nexport function utf8Encode(value: string): Uint8Array<ArrayBuffer> {\n return textEncoder.encode(value) as Uint8Array<ArrayBuffer>;\n}\n\nexport function utf8Decode(value: Uint8Array): string {\n return textDecoder.decode(value);\n}\n\nexport function base64ToBytes(value: string): Uint8Array<ArrayBuffer> {\n const binary = atob(value);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes;\n}\n\nexport function bytesToBase64(bytes: Uint8Array): string {\n let binary = '';\n const chunk = 0x8000;\n for (let i = 0; i < bytes.length; i += chunk) {\n binary += String.fromCharCode(...bytes.subarray(i, i + chunk));\n }\n return btoa(binary);\n}\n\nexport function bytesToHex(bytes: Uint8Array): string {\n let hex = '';\n for (const byte of bytes) {\n hex += byte.toString(16).padStart(2, '0');\n }\n return hex;\n}\n\n/** Constant-time string comparison (length is not secret). */\nexport function timingSafeEqualString(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return diff === 0;\n}\n\n/** HMAC-SHA256 hex digest using Web Crypto. */\nexport async function hmacSha256Hex(key: string, data: string): Promise<string> {\n const subtle = getSubtle();\n const cryptoKey = await subtle.importKey('raw', utf8Encode(key), { name: 'HMAC', hash: 'SHA-256' }, false, [\n 'sign',\n ]);\n const signature = await subtle.sign('HMAC', cryptoKey, utf8Encode(data));\n return bytesToHex(new Uint8Array(signature));\n}\n","// Docs: https://developers.facebook.com/documentation/business-messaging/whatsapp/flows/guides/implementingyourflowendpoint\nimport { base64ToBytes, bytesToBase64, utf8Decode, utf8Encode } from '../utils/runtime';\nimport type { FlowActionEnum, FlowDataExchangeResponse } from './sdk';\nimport { generateXHub256SigAsync } from './sdk';\n\n/** Default URL for Flow endpoint requests built by {@link createFlowRequest}. */\nexport const TEST_FLOW_URL = 'https://localhost/flow';\n\n/** The decrypted request body Meta sends to a Flow endpoint. */\nexport interface FlowRequestInit {\n /** Defaults to `'3.0'`. */\n version?: '3.0';\n /** Defaults to `'data_exchange'`. */\n action?: 'ping' | `${FlowActionEnum}`;\n screen?: string;\n /** Defaults to `'test-flow-token'` (not added to `ping` requests). */\n flow_token?: string;\n data?: Record<string, unknown>;\n}\n\nexport interface FlowRequestOptions {\n /**\n * Your Flow endpoint's RSA public key: an SPKI PEM (`-----BEGIN PUBLIC KEY-----`,\n * as returned by `generateEncryption()` and {@link generateFlowKeyPair}) or a Web Crypto key.\n */\n publicKey: string | CryptoKey;\n /**\n * Key for the `X-Hub-Signature-256` header: the App Secret the processor is\n * configured with. Leave it out to send an unsigned request.\n */\n appSecret?: string;\n /** Request URL. Defaults to {@link TEST_FLOW_URL}. */\n url?: string;\n}\n\n/** The AES key and IV a Flow request was encrypted with; needed to read the reply. */\nexport interface FlowSessionKeys {\n aesKey: Uint8Array;\n initialVector: Uint8Array;\n}\n\nexport interface FlowTestRequest extends FlowSessionKeys {\n /** Ready to pass to `processor.processFlow(request)` or a Flow route handler. */\n request: Request;\n /** The encrypted JSON body (`encrypted_aes_key`, `encrypted_flow_data`, `initial_vector`). */\n body: string;\n /** The plaintext that was encrypted. */\n payload: FlowRequestInit;\n /** Shorthand for `decryptFlowResponse(response, this)`. */\n decryptResponse<T = FlowDataExchangeResponse>(response: FlowResponseLike): Promise<T>;\n}\n\n/** A Flow endpoint reply: the raw body, `processor.processFlow()`'s result, or a fetch `Response`. */\nexport type FlowResponseLike = string | { body: string } | Response;\n\nexport interface FlowKeyPair {\n publicKey: CryptoKey;\n privateKey: CryptoKey;\n /** SPKI PEM; what you upload to Meta. */\n publicPem: string;\n /** Unencrypted PKCS#8 PEM; pass as `privatePem` (no passphrase needed). */\n privatePem: string;\n}\n\nfunction subtle(): SubtleCrypto {\n const value = globalThis.crypto?.subtle;\n if (!value) throw new Error('Web Crypto (globalThis.crypto.subtle) is not available in this runtime.');\n return value;\n}\n\nfunction toPem(label: string, der: ArrayBuffer): string {\n const lines = bytesToBase64(new Uint8Array(der)).match(/.{1,64}/g) ?? [];\n return `-----BEGIN ${label}-----\\n${lines.join('\\n')}\\n-----END ${label}-----\\n`;\n}\n\n/**\n * Generate a throwaway 2048-bit RSA key pair for Flow tests with Web Crypto.\n * Works on every runtime, unlike `generateEncryption()` which needs `node:crypto`.\n */\nexport async function generateFlowKeyPair(): Promise<FlowKeyPair> {\n const pair = await subtle().generateKey(\n { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },\n true,\n ['encrypt', 'decrypt'],\n );\n return {\n publicKey: pair.publicKey,\n privateKey: pair.privateKey,\n publicPem: toPem('PUBLIC KEY', await subtle().exportKey('spki', pair.publicKey)),\n privatePem: toPem('PRIVATE KEY', await subtle().exportKey('pkcs8', pair.privateKey)),\n };\n}\n\nasync function importPublicKey(key: string | CryptoKey): Promise<CryptoKey> {\n if (typeof key !== 'string') return key;\n const pem = key.includes('\\\\n') ? key.replace(/\\\\n/g, '\\n') : key;\n const match = /-----BEGIN PUBLIC KEY-----([\\s\\S]*?)-----END PUBLIC KEY-----/.exec(pem);\n if (!match) {\n throw new Error(\n 'createFlowRequest: publicKey must be an SPKI PEM (\"-----BEGIN PUBLIC KEY-----\"). ' +\n 'Convert a PKCS#1 key with: openssl rsa -RSAPublicKey_in -in key.pem -pubout',\n );\n }\n return subtle().importKey(\n 'spki',\n base64ToBytes((match[1] ?? '').replace(/\\s+/g, '')),\n { name: 'RSA-OAEP', hash: 'SHA-256' },\n false,\n ['encrypt'],\n );\n}\n\nfunction withDefaults(init: FlowRequestInit): FlowRequestInit {\n const action = init.action ?? 'data_exchange';\n const payload: FlowRequestInit = { version: '3.0', ...init, action };\n if (action !== 'ping' && payload.flow_token === undefined) payload.flow_token = 'test-flow-token';\n return payload;\n}\n\n/**\n * Encrypt a Flow endpoint request the way Meta does: a fresh AES-128-GCM key\n * encrypts the JSON, and RSA-OAEP (SHA-256) with your public key encrypts that\n * AES key. Use the returned keys (or `decryptResponse`) to read the encrypted reply.\n */\nexport async function createFlowRequest(init: FlowRequestInit, options: FlowRequestOptions): Promise<FlowTestRequest> {\n const payload = withDefaults(init);\n const crypto = subtle();\n const aesKey = globalThis.crypto.getRandomValues(new Uint8Array(16));\n const initialVector = globalThis.crypto.getRandomValues(new Uint8Array(16));\n const key = await crypto.importKey('raw', aesKey, { name: 'AES-GCM' }, false, ['encrypt']);\n\n const body = JSON.stringify({\n encrypted_aes_key: bytesToBase64(\n new Uint8Array(\n await crypto.encrypt({ name: 'RSA-OAEP' }, await importPublicKey(options.publicKey), aesKey),\n ),\n ),\n encrypted_flow_data: bytesToBase64(\n new Uint8Array(\n await crypto.encrypt({ name: 'AES-GCM', iv: initialVector }, key, utf8Encode(JSON.stringify(payload))),\n ),\n ),\n initial_vector: bytesToBase64(initialVector),\n });\n\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n if (options.appSecret !== undefined) {\n headers['x-hub-signature-256'] = `sha256=${await generateXHub256SigAsync(body, options.appSecret)}`;\n }\n\n const keys: FlowSessionKeys = { aesKey, initialVector };\n return {\n request: new Request(options.url ?? TEST_FLOW_URL, { method: 'POST', body, headers }),\n body,\n payload,\n aesKey,\n initialVector,\n decryptResponse: (response) => decryptFlowResponse(response, keys),\n };\n}\n\nasync function responseText(response: FlowResponseLike): Promise<string> {\n if (typeof response === 'string') return response;\n if (response instanceof Response) return response.text();\n return response.body;\n}\n\n/**\n * Read a Flow endpoint reply. Encrypted replies (ping, data_exchange) are\n * decrypted with the request's AES key and the bit-flipped IV, as Meta does;\n * plain JSON replies (error notification acknowledgements) are parsed as is.\n */\nexport async function decryptFlowResponse<T = FlowDataExchangeResponse>(\n response: FlowResponseLike,\n keys: FlowSessionKeys,\n): Promise<T> {\n const text = (await responseText(response)).trim();\n if (text.startsWith('{')) return JSON.parse(text) as T;\n\n const flippedIv = keys.initialVector.map((byte) => ~byte & 0xff);\n const crypto = subtle();\n const key = await crypto.importKey('raw', new Uint8Array(keys.aesKey), { name: 'AES-GCM' }, false, ['decrypt']);\n let plain: ArrayBuffer;\n try {\n plain = await crypto.decrypt({ name: 'AES-GCM', iv: flippedIv }, key, base64ToBytes(text));\n } catch {\n throw new Error(`decryptFlowResponse: could not decrypt the Flow response: ${text.slice(0, 200)}`);\n }\n return JSON.parse(utf8Decode(new Uint8Array(plain))) as T;\n}\n","import { bytesToBase64 } from '../utils/runtime';\n\nfunction randomBytes(length: number): Uint8Array {\n return globalThis.crypto.getRandomValues(new Uint8Array(length));\n}\n\n/** A random message ID shaped like Meta's (`wamid.` followed by base64). */\nexport function randomWamid(): string {\n return `wamid.HBgL${bytesToBase64(randomBytes(24))}`;\n}\n\n/** A random numeric ID such as a media ID (no leading zero). */\nexport function randomDigits(length: number): string {\n let digits = String(1 + ((randomBytes(1)[0] ?? 0) % 9));\n for (const byte of randomBytes(length - 1)) digits += String(byte % 10);\n return digits;\n}\n","// Docs: https://developers.facebook.com/documentation/business-messaging/whatsapp/overview/\nimport { bytesToHex } from '../utils/runtime';\nimport { randomDigits, randomWamid } from './random';\nimport {\n CategoryEnum,\n LanguagesEnum,\n type MediaResponse,\n type MessagesResponse,\n type MetaErrorData,\n type ResponsePagination,\n type TemplateResponse,\n type UploadMediaResponse,\n} from './sdk';\n\nconst GRAPH_HOST = 'graph.facebook.com';\nconst MEDIA_HOST = 'lookaside.fbsbx.com';\n\nexport type MockHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\n/** A request the mock intercepted. */\nexport interface RecordedRequest {\n method: string;\n /** Full request URL. */\n url: string;\n host: string;\n /** Path without the API version, e.g. `/1234567890/messages`. */\n path: string;\n /** API version from the URL, e.g. `v23.0`, when present. */\n apiVersion: string | undefined;\n /** Query string parameters (the last value wins for repeated keys). */\n query: Record<string, string>;\n /** Request headers with lower-cased names. */\n headers: Record<string, string>;\n /** How the body was encoded. */\n bodyType: 'none' | 'json' | 'form' | 'multipart' | 'text';\n /**\n * Parsed body: the JSON value, an object of URL-encoded or multipart fields\n * (multipart files stay `File`/`Blob`), the raw text, or `undefined`.\n */\n body: unknown;\n}\n\n/** What a route handler receives: the recorded request plus the `:params` it matched. */\nexport interface MockRouteRequest extends RecordedRequest {\n params: Record<string, string>;\n}\n\n/**\n * A route handler returns a `Response`, any JSON-serializable value (sent with\n * status 200), or `undefined` to fall through to the next matching route and\n * finally to the built-in default.\n */\nexport type MockRouteHandler = (request: MockRouteRequest) => unknown;\n\nexport interface MockRouteOptions {\n /** Answer only this many requests, then step aside. Defaults to unlimited. */\n times?: number;\n}\n\n/** An error in Meta's shape. Only `code` is required. */\nexport type MetaErrorInit = Partial<MetaErrorData> &\n Pick<MetaErrorData, 'code'> & {\n error_user_title?: string;\n error_user_msg?: string;\n };\n\n/** A message payload the SDK sent to `POST /{PHONE_NUMBER_ID}/messages`. */\nexport interface SentMessage {\n messaging_product: 'whatsapp';\n to: string;\n type: string;\n recipient_type?: string;\n context?: { message_id: string };\n [field: string]: unknown;\n}\n\n/** A file uploaded to the mock with `media.uploadMedia()`. */\nexport interface MockMedia {\n id: string;\n mimeType: string;\n fileSize: number;\n sha256: string;\n bytes: Uint8Array;\n}\n\nexport interface MockCloudApiOptions {\n /** Templates returned by `GET /{WABA_ID}/message_templates`. Defaults to one approved `hello_world`. */\n templates?: TemplateResponse[];\n /**\n * fetch used for requests that are not for the Graph API (and the one\n * {@link MockCloudApi.restore} puts back). Defaults to `globalThis.fetch`.\n */\n fetch?: typeof fetch;\n /**\n * What to do with a Graph API request no route or default answers:\n * `'error'` (default) replies with a Meta error so the SDK throws,\n * `'success'` replies `{ \"success\": true }`.\n */\n unhandled?: 'error' | 'success';\n}\n\nconst DEFAULT_TEMPLATES: TemplateResponse[] = [\n {\n id: '100000000000010',\n name: 'hello_world',\n status: 'APPROVED',\n language: LanguagesEnum.English_US,\n category: CategoryEnum.Utility,\n components: [],\n },\n];\n\n/** Status Meta uses for an error code when none is given. */\nfunction defaultStatusFor(code: number): number {\n if ([0, 3, 10, 190].includes(code)) return 401;\n if ([4, 80007, 130429, 131048, 131056].includes(code)) return 429;\n return 400;\n}\n\n/**\n * Build a Graph API error response in Meta's shape\n * (`{ \"error\": { message, type, code, error_subcode?, error_data?, fbtrace_id } }`).\n * The status defaults to 401 for auth errors, 429 for rate limits and 400 otherwise.\n */\nexport function createMetaErrorResponse(error: MetaErrorInit, status: number = defaultStatusFor(error.code)): Response {\n const body = {\n error: {\n message: error.message ?? `(#${error.code}) Mock error`,\n type: error.type ?? 'OAuthException',\n ...error,\n fbtrace_id: error.fbtrace_id ?? `mock-${randomDigits(8)}`,\n },\n };\n return jsonResponse(body, status);\n}\n\nfunction jsonResponse(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n}\n\ninterface Route {\n method: string;\n pattern: string | RegExp;\n handler: MockRouteHandler;\n remaining: number;\n}\n\nfunction matchPath(pattern: string | RegExp, path: string): Record<string, string> | undefined {\n if (pattern instanceof RegExp) {\n const match = new RegExp(pattern.source, pattern.flags).exec(path);\n return match ? { ...match.groups } : undefined;\n }\n const want = pattern.replace(/^\\/+|\\/+$/g, '').split('/');\n const got = path.replace(/^\\/+|\\/+$/g, '').split('/');\n if (want.length !== got.length) return undefined;\n const params: Record<string, string> = {};\n for (let i = 0; i < want.length; i++) {\n const w = want[i] ?? '';\n const g = got[i] ?? '';\n if (w.startsWith(':')) params[w.slice(1)] = decodeURIComponent(g);\n else if (w !== '*' && w !== g) return undefined;\n }\n return params;\n}\n\nfunction formToObject(form: FormData): Record<string, FormDataEntryValue | FormDataEntryValue[]> {\n const out: Record<string, FormDataEntryValue | FormDataEntryValue[]> = {};\n form.forEach((value, key) => {\n const existing = out[key];\n if (existing === undefined) out[key] = value;\n else out[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];\n });\n return out;\n}\n\nfunction paramsToObject(params: URLSearchParams): Record<string, string | string[]> {\n const out: Record<string, string | string[]> = {};\n params.forEach((value, key) => {\n const existing = out[key];\n if (existing === undefined) out[key] = value;\n else out[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];\n });\n return out;\n}\n\nasync function readBody(\n request: Request,\n init: RequestInit | undefined,\n): Promise<Pick<RecordedRequest, 'body' | 'bodyType'>> {\n // The SDK hands fetch a FormData for uploads; read it directly.\n const raw = init?.body;\n if (raw instanceof FormData) return { bodyType: 'multipart', body: formToObject(raw) };\n if (raw instanceof URLSearchParams) return { bodyType: 'form', body: paramsToObject(raw) };\n if (request.body === null) return { bodyType: 'none', body: undefined };\n\n const contentType = request.headers.get('content-type') ?? '';\n if (contentType.includes('multipart/form-data')) {\n return { bodyType: 'multipart', body: formToObject(await request.formData()) };\n }\n const text = await request.text();\n if (contentType.includes('application/x-www-form-urlencoded')) {\n return { bodyType: 'form', body: paramsToObject(new URLSearchParams(text)) };\n }\n if (text === '') return { bodyType: 'none', body: undefined };\n try {\n return { bodyType: 'json', body: JSON.parse(text) };\n } catch {\n return { bodyType: 'text', body: text };\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isSentMessage(request: RecordedRequest): boolean {\n return (\n request.method === 'POST' &&\n /^\\/[^/]+\\/(messages|marketing_messages)$/.test(request.path) &&\n isRecord(request.body) &&\n typeof request.body.to === 'string' &&\n request.body.status === undefined\n );\n}\n\n/**\n * An in-memory Graph API. Install it on `globalThis.fetch` (the SDK uses the\n * global fetch) or hand {@link MockCloudApi.fetch} to code that accepts one.\n * Every Graph API request is recorded; non-Graph requests pass through.\n */\nexport class MockCloudApi {\n /** Every Graph API request, oldest first. */\n readonly requests: RecordedRequest[] = [];\n /** Files uploaded through `POST /{PHONE_NUMBER_ID}/media`, by media ID. */\n readonly media = new Map<string, MockMedia>();\n /** Templates served by `GET /{WABA_ID}/message_templates`. */\n templates: TemplateResponse[];\n\n private routes: Route[] = [];\n private readonly passthrough: typeof fetch;\n private readonly unhandled: 'error' | 'success';\n private previousFetch: typeof fetch | undefined;\n\n constructor(options: MockCloudApiOptions = {}) {\n const base = options.fetch ?? globalThis.fetch;\n this.passthrough = (input, init) => base(input, init);\n this.templates = structuredClone(options.templates ?? DEFAULT_TEMPLATES);\n this.unhandled = options.unhandled ?? 'error';\n }\n\n /** A fetch that answers Graph API requests from this mock. */\n readonly fetch: typeof fetch = async (input, init) => {\n const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);\n if (url.host !== GRAPH_HOST && url.host !== MEDIA_HOST) return this.passthrough(input, init);\n const request = new Request(input, init);\n\n const versionMatch = /^\\/(v\\d+(?:\\.\\d+)?)(\\/.*)?$/.exec(url.pathname);\n const headers: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headers[key] = value;\n });\n const recorded: RecordedRequest = {\n method: request.method.toUpperCase(),\n url: request.url,\n host: url.host,\n path: versionMatch ? versionMatch[2] || '/' : url.pathname,\n apiVersion: versionMatch?.[1],\n query: Object.fromEntries(url.searchParams),\n headers,\n ...(await readBody(request, init)),\n };\n this.requests.push(recorded);\n return this.respond(recorded);\n };\n\n /** Replace `globalThis.fetch` with {@link MockCloudApi.fetch}. Returns the mock for chaining. */\n install(): this {\n if (this.previousFetch === undefined) {\n this.previousFetch = globalThis.fetch;\n globalThis.fetch = this.fetch;\n }\n return this;\n }\n\n /** Put back the `globalThis.fetch` that {@link MockCloudApi.install} replaced. */\n restore(): void {\n if (this.previousFetch !== undefined) {\n globalThis.fetch = this.previousFetch;\n this.previousFetch = undefined;\n }\n }\n\n /**\n * Answer matching requests with `handler`. `path` is matched without the API\n * version: `'/:phoneNumberId/messages'` (`:name` captures a segment, `*` matches\n * any one) or a RegExp (named groups become params). `method` may be `'*'`.\n * Later routes win. Returns a function that removes the route.\n */\n on(\n method: MockHttpMethod | '*',\n path: string | RegExp,\n handler: MockRouteHandler,\n options: MockRouteOptions = {},\n ): () => void {\n const route: Route = { method, pattern: path, handler, remaining: options.times ?? Number.POSITIVE_INFINITY };\n this.routes.unshift(route);\n return () => {\n this.routes = this.routes.filter((r) => r !== route);\n };\n }\n\n /**\n * Reply to matching requests with a Meta error, so the SDK throws the\n * matching `WhatsAppError` subclass (e.g. code 190 → `WhatsAppAuthorizationError`).\n */\n fail(\n method: MockHttpMethod | '*',\n path: string | RegExp,\n error: MetaErrorInit,\n options: MockRouteOptions & { status?: number } = {},\n ): () => void {\n return this.on(method, path, () => createMetaErrorResponse(error, options.status), options);\n }\n\n /** Requests matching `method` and `path` (same matching rules as {@link MockCloudApi.on}). */\n requestsTo(method: MockHttpMethod | '*', path: string | RegExp): RecordedRequest[] {\n return this.requests.filter(\n (r) => (method === '*' || r.method === method) && matchPath(path, r.path) !== undefined,\n );\n }\n\n /**\n * Message payloads sent to `POST /{PHONE_NUMBER_ID}/messages` (and\n * `/marketing_messages`), oldest first. Read receipts and typing indicators\n * are left out. Pass a type such as `'text'` to filter.\n */\n sentMessages(type?: string): SentMessage[] {\n return this.requests\n .filter(isSentMessage)\n .map((r) => r.body as SentMessage)\n .filter((message) => type === undefined || message.type === type);\n }\n\n /** The most recent {@link MockCloudApi.sentMessages} entry. */\n lastSentMessage(type?: string): SentMessage | undefined {\n return this.sentMessages(type).at(-1);\n }\n\n /** Forget recorded requests and uploaded media; keep routes. */\n clearRequests(): void {\n this.requests.length = 0;\n this.media.clear();\n }\n\n /** Forget recorded requests, uploaded media and custom routes. */\n reset(): void {\n this.clearRequests();\n this.routes = [];\n }\n\n private async respond(request: RecordedRequest): Promise<Response> {\n for (const route of [...this.routes]) {\n if (route.remaining <= 0) continue;\n if (route.method !== '*' && route.method !== request.method) continue;\n const params = matchPath(route.pattern, request.path);\n if (!params) continue;\n // Reserve before awaiting so concurrent requests respect `times`.\n route.remaining--;\n let reply: unknown;\n try {\n reply = await route.handler({ ...request, params });\n } catch (error) {\n route.remaining++;\n throw error;\n }\n if (reply === undefined) {\n route.remaining++;\n continue;\n }\n return reply instanceof Response ? reply : jsonResponse(reply);\n }\n return this.defaultResponse(request);\n }\n\n private async defaultResponse(request: RecordedRequest): Promise<Response> {\n const { method, path, host } = request;\n const segments = path.replace(/^\\/+|\\/+$/g, '').split('/');\n\n if (host === MEDIA_HOST) {\n const media = this.media.get(request.query.mid ?? '');\n return new Response(media ? new Uint8Array(media.bytes) : 'mock media', {\n headers: { 'content-type': media?.mimeType ?? 'application/octet-stream' },\n });\n }\n\n if (method === 'POST' && segments.length === 2 && /^(messages|marketing_messages)$/.test(segments[1] ?? '')) {\n const body = isRecord(request.body) ? request.body : {};\n if (body.status === 'read' || body.status === 'typing') return jsonResponse({ success: true });\n const to = typeof body.to === 'string' ? body.to : '';\n const response: MessagesResponse = {\n messaging_product: 'whatsapp',\n contacts: [{ input: to, wa_id: to.replace(/\\D/g, '') || to }],\n messages: [{ id: randomWamid(), ...(body.type === 'template' && { message_status: 'accepted' }) }],\n };\n return jsonResponse(response);\n }\n\n if (method === 'POST' && segments.length === 2 && segments[1] === 'media') {\n const body = isRecord(request.body) ? request.body : {};\n const file = body.file;\n const id = randomDigits(16);\n const bytes = file instanceof Blob ? new Uint8Array(await file.arrayBuffer()) : new Uint8Array();\n const digest = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', bytes));\n this.media.set(id, {\n id,\n mimeType: (typeof body.type === 'string' && body.type) || (file instanceof Blob && file.type) || '',\n fileSize: bytes.byteLength,\n sha256: bytesToHex(digest),\n bytes,\n });\n const response: UploadMediaResponse = { id };\n return jsonResponse(response);\n }\n\n if (segments.length === 2 && segments[1] === 'message_templates') {\n if (method === 'GET') {\n const { name, status, language, category } = request.query;\n const data = this.templates.filter(\n (t) =>\n (!name || t.name === name) &&\n (!status || t.status === status) &&\n (!language || t.language === language) &&\n (!category || t.category === category),\n );\n const limit = Number(request.query.limit) || data.length;\n const response: ResponsePagination<TemplateResponse> = {\n data: data.slice(0, limit),\n paging: { cursors: { before: 'MAZDZD', after: 'MjQZD' }, next: '' },\n };\n return jsonResponse(response);\n }\n if (method === 'POST') {\n const body = isRecord(request.body) ? request.body : {};\n return jsonResponse({ id: randomDigits(15), status: 'PENDING', category: body.category ?? 'UTILITY' });\n }\n if (method === 'DELETE') return jsonResponse({ success: true });\n }\n\n if (segments.length === 1 && segments[0]) {\n const id = segments[0];\n if (method === 'DELETE') {\n this.media.delete(id);\n return jsonResponse({ success: true });\n }\n if (method === 'GET') {\n const media = this.media.get(id);\n const response: MediaResponse = {\n messaging_product: 'whatsapp',\n id,\n url: `https://${MEDIA_HOST}/whatsapp_business/attachments/?mid=${id}`,\n mime_type: media?.mimeType || 'image/jpeg',\n sha256: media?.sha256 ?? '0'.repeat(64),\n file_size: media?.fileSize ?? 0,\n };\n return jsonResponse(response);\n }\n }\n\n if (this.unhandled === 'success') return jsonResponse({ success: true });\n return createMetaErrorResponse(\n {\n code: 100,\n type: 'GraphMethodException',\n message: `meta-cloud-api/testing: no mock route for ${method} ${path}. Add one with mock.on('${method}', '${path}', handler).`,\n },\n 400,\n );\n }\n}\n\n/**\n * Create an in-memory Graph API for tests. Call `.install()` to route the SDK's\n * requests to it and `.restore()` afterwards.\n *\n * ```ts\n * const mock = createMockCloudApi().install();\n * await new WhatsApp({ accessToken: 'test', phoneNumberId: 1 }).messages.text({ to: '15551234567', body: 'hi' });\n * mock.sentMessages(); // [{ messaging_product: 'whatsapp', to: '15551234567', type: 'text', text: { body: 'hi' }, ... }]\n * mock.restore();\n * ```\n */\nexport function createMockCloudApi(options?: MockCloudApiOptions): MockCloudApi {\n return new MockCloudApi(options);\n}\n","// Docs: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/overview\nimport { generateXHub256SigAsync, type WebhookPayload } from './sdk';\n\n/** Default URL for requests built by the testing helpers. */\nexport const TEST_WEBHOOK_URL = 'https://localhost/webhook';\n\nexport interface SignedWebhookRequestOptions {\n /** App Secret used for the `X-Hub-Signature-256` HMAC (the same one the processor is configured with). */\n appSecret: string;\n /** Request URL. Defaults to {@link TEST_WEBHOOK_URL}. */\n url?: string;\n /** Extra headers. They can override the defaults, e.g. to send a broken signature. */\n headers?: Record<string, string>;\n}\n\n/**\n * Build the `POST` Meta sends to your webhook, signed with `appSecret` the way\n * Meta signs it. Pass the result to `processor.processWebhook(request)` or to\n * your framework route handler. Strings are sent unchanged; anything else is\n * serialized with `JSON.stringify`.\n */\nexport async function createSignedWebhookRequest(\n payload: WebhookPayload | string,\n options: SignedWebhookRequestOptions,\n): Promise<Request> {\n const body = typeof payload === 'string' ? payload : JSON.stringify(payload);\n const signature = await generateXHub256SigAsync(body, options.appSecret);\n const headers = new Headers({\n 'content-type': 'application/json',\n 'x-hub-signature-256': `sha256=${signature}`,\n });\n for (const [name, value] of Object.entries(options.headers ?? {})) headers.set(name, value);\n return new Request(options.url ?? TEST_WEBHOOK_URL, { method: 'POST', body, headers });\n}\n","// Docs: https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages\nimport { randomDigits, randomWamid } from './random';\nimport {\n type ImageMessage,\n type InteractiveButtonReplyMessage,\n type InteractiveListReplyMessage,\n type LocationMessage,\n MessageTypesEnum,\n type MessageWebhookValue,\n type NonMessageWebhookField,\n type ReactionMessage,\n type StatusWebhook,\n type StatusWebhookValue,\n type TextMessage,\n type WebhookFieldType,\n type WebhookFieldValueMap,\n type WebhookValue,\n type WhatsAppMessage,\n} from './sdk';\n\n/** Phone number ID used when a factory is not given one. */\nexport const TEST_PHONE_NUMBER_ID = '100000000000001';\n/** Display phone number used when a factory is not given one. */\nexport const TEST_DISPLAY_PHONE_NUMBER = '15550100000';\n/** WhatsApp Business Account ID used when a factory is not given one. */\nexport const TEST_WABA_ID = '100000000000002';\n\n/** The `change` object for a webhook field, as Meta nests it under `entry[].changes[]`. */\nexport type WebhookChangeFor<F extends WebhookFieldType> = F extends NonMessageWebhookField\n ? { field: F; value: WebhookFieldValueMap[F] }\n : { field: 'messages'; value: WebhookValue };\n\n/** Any `change` object the SDK understands. */\nexport type WebhookChange = WebhookChangeFor<WebhookFieldType>;\n\n/**\n * A webhook payload whose changes are narrowed to `C`. It is assignable to the\n * SDK's `WebhookPayload`, so it can be passed anywhere one is expected.\n */\nexport type TestWebhookPayload<C extends WebhookChange = WebhookChange> = {\n object: 'whatsapp_business_account';\n entry: Array<{ id: string; changes: Array<C> }>;\n};\n\n/** `messages` field value carrying messages of type `M`. */\nexport type MessageWebhookValueOf<M extends WhatsAppMessage> = Omit<MessageWebhookValue, 'messages'> & {\n messages: Array<M>;\n};\n\n/** Webhook payload delivering one inbound message of type `M`. */\nexport type MessageWebhookPayload<M extends WhatsAppMessage = WhatsAppMessage> = TestWebhookPayload<{\n field: 'messages';\n value: MessageWebhookValueOf<M>;\n}>;\n\n/** Webhook payload delivering message status updates. */\nexport type StatusWebhookPayload = TestWebhookPayload<{ field: 'messages'; value: StatusWebhookValue }>;\n\n/** Options shared by every factory. */\nexport interface WebhookTargetOptions {\n /** Your business phone number ID. Defaults to {@link TEST_PHONE_NUMBER_ID}. */\n phoneNumberId?: string | number;\n /** Your business display phone number. Defaults to {@link TEST_DISPLAY_PHONE_NUMBER}. */\n displayPhoneNumber?: string;\n /** WhatsApp Business Account ID (`entry[].id`). Defaults to {@link TEST_WABA_ID}. */\n wabaId?: string;\n}\n\n/** Options for factories that deliver a message sent by a customer. */\nexport interface InboundMessageOptions extends WebhookTargetOptions {\n /** Customer phone number (WhatsApp ID) that sent the message. */\n from: string;\n /** Customer profile name. Defaults to `'Test User'`. */\n profileName?: string;\n /** Message ID. Defaults to a random `wamid.` ID. */\n id?: string;\n /** Unix seconds (string or number) or a Date. Defaults to now. */\n timestamp?: string | number | Date;\n}\n\nfunction toTimestamp(value: string | number | Date | undefined): string {\n if (value === undefined) return String(Math.floor(Date.now() / 1000));\n if (value instanceof Date) return String(Math.floor(value.getTime() / 1000));\n return String(value);\n}\n\nfunction metadata(options: WebhookTargetOptions): MessageWebhookValue['metadata'] {\n return {\n display_phone_number: options.displayPhoneNumber ?? TEST_DISPLAY_PHONE_NUMBER,\n phone_number_id: String(options.phoneNumberId ?? TEST_PHONE_NUMBER_ID),\n };\n}\n\ntype BaseFields = { from: string; id: string; timestamp: string };\n\nfunction baseFields(options: InboundMessageOptions): BaseFields {\n return { from: options.from, id: options.id ?? randomWamid(), timestamp: toTimestamp(options.timestamp) };\n}\n\n/** The `context` Meta attaches to button and list replies: the business message being answered. */\nfunction replyContext(options: InboundMessageOptions & { contextMessageId?: string }) {\n return {\n from: options.displayPhoneNumber ?? TEST_DISPLAY_PHONE_NUMBER,\n id: options.contextMessageId ?? randomWamid(),\n };\n}\n\n/**\n * Wrap a field value in the `object` / `entry` / `changes` envelope Meta posts to\n * your webhook. Works for every field the SDK types, for example\n * `createWebhookPayload('account_update', { event: 'VERIFIED_ACCOUNT' })`.\n */\nexport function createWebhookPayload<F extends WebhookFieldType>(\n field: F,\n value: WebhookChangeFor<F>['value'],\n options: Pick<WebhookTargetOptions, 'wabaId'> = {},\n): TestWebhookPayload<WebhookChangeFor<F>> {\n // TypeScript cannot correlate `field` and `value` through the conditional type.\n return envelope({ field, value } as WebhookChangeFor<F>, options);\n}\n\nfunction envelope<C extends WebhookChange>(\n change: C,\n options: Pick<WebhookTargetOptions, 'wabaId'>,\n): TestWebhookPayload<C> {\n return {\n object: 'whatsapp_business_account',\n entry: [{ id: options.wabaId ?? TEST_WABA_ID, changes: [change] }],\n };\n}\n\n/**\n * Deliver any inbound message object. The typed factories below build the\n * message for you; use this one for types they do not cover (video, document, ...).\n */\nexport function createMessageWebhook<M extends WhatsAppMessage>(\n message: M,\n options: WebhookTargetOptions & { profileName?: string } = {},\n): MessageWebhookPayload<M> {\n const value: MessageWebhookValueOf<M> = {\n messaging_product: 'whatsapp',\n metadata: metadata(options),\n contacts: [{ profile: { name: options.profileName ?? 'Test User' }, wa_id: message.from }],\n messages: [message],\n };\n return envelope({ field: 'messages', value }, options);\n}\n\n/** A customer sent a text message. Dispatched to `processor.onText`. */\nexport function createTextMessageWebhook(\n options: InboundMessageOptions & { text: string },\n): MessageWebhookPayload<TextMessage> {\n const message: TextMessage = { ...baseFields(options), type: MessageTypesEnum.Text, text: { body: options.text } };\n return createMessageWebhook(message, options);\n}\n\n/** A customer sent an image. Dispatched to `processor.onImage`. */\nexport function createImageMessageWebhook(\n options: InboundMessageOptions & {\n /** Media ID to pass to `media.getMediaById`. Defaults to a random numeric ID. */\n mediaId?: string;\n mimeType?: string;\n sha256?: string;\n caption?: string;\n /** Defaults to a lookaside URL built from the media ID. */\n url?: string;\n },\n): MessageWebhookPayload<ImageMessage> {\n const mediaId = options.mediaId ?? randomDigits(16);\n const image: ImageMessage['image'] = {\n id: mediaId,\n mime_type: options.mimeType ?? 'image/jpeg',\n sha256: options.sha256 ?? 'test-sha256',\n url: options.url ?? `https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=${mediaId}`,\n };\n if (options.caption !== undefined) image.caption = options.caption;\n const message: ImageMessage = { ...baseFields(options), type: MessageTypesEnum.Image, image };\n return createMessageWebhook(message, options);\n}\n\n/** A customer tapped a reply button. Dispatched to `processor.onInteractive`. */\nexport function createButtonReplyWebhook(\n options: InboundMessageOptions & {\n /** The `id` you gave the button. */\n buttonId: string;\n title: string;\n /** ID of your interactive message. Defaults to a random `wamid.` ID. */\n contextMessageId?: string;\n },\n): MessageWebhookPayload<InteractiveButtonReplyMessage> {\n const message: InteractiveButtonReplyMessage = {\n ...baseFields(options),\n type: MessageTypesEnum.Interactive,\n context: replyContext(options),\n interactive: { type: 'button_reply', button_reply: { id: options.buttonId, title: options.title } },\n };\n return createMessageWebhook(message, options);\n}\n\n/** A customer picked a list row. Dispatched to `processor.onInteractive`. */\nexport function createListReplyWebhook(\n options: InboundMessageOptions & {\n /** The `id` you gave the row. */\n rowId: string;\n title: string;\n description?: string;\n /** ID of your list message. Defaults to a random `wamid.` ID. */\n contextMessageId?: string;\n },\n): MessageWebhookPayload<InteractiveListReplyMessage> {\n const listReply: InteractiveListReplyMessage['interactive']['list_reply'] = {\n id: options.rowId,\n title: options.title,\n };\n if (options.description !== undefined) listReply.description = options.description;\n const message: InteractiveListReplyMessage = {\n ...baseFields(options),\n type: MessageTypesEnum.Interactive,\n context: replyContext(options),\n interactive: { type: 'list_reply', list_reply: listReply },\n };\n return createMessageWebhook(message, options);\n}\n\n/**\n * A customer reacted to a message. Leave `emoji` out to simulate removing a\n * reaction. Dispatched to `processor.onReaction`.\n */\nexport function createReactionWebhook(\n options: InboundMessageOptions & {\n /** ID of the message that was reacted to. */\n messageId: string;\n emoji?: string;\n },\n): MessageWebhookPayload<ReactionMessage> {\n const reaction: ReactionMessage['reaction'] = { message_id: options.messageId };\n if (options.emoji !== undefined) reaction.emoji = options.emoji;\n const message: ReactionMessage = { ...baseFields(options), type: MessageTypesEnum.Reaction, reaction };\n return createMessageWebhook(message, options);\n}\n\n/** A customer shared a location. Dispatched to `processor.onLocation`. */\nexport function createLocationWebhook(\n options: InboundMessageOptions & {\n latitude: number;\n longitude: number;\n name?: string;\n address?: string;\n url?: string;\n },\n): MessageWebhookPayload<LocationMessage> {\n const location: LocationMessage['location'] = { latitude: options.latitude, longitude: options.longitude };\n if (options.name !== undefined) location.name = options.name;\n if (options.address !== undefined) location.address = options.address;\n if (options.url !== undefined) location.url = options.url;\n const message: LocationMessage = { ...baseFields(options), type: MessageTypesEnum.Location, location };\n return createMessageWebhook(message, options);\n}\n\n/** Default error Meta reports on a `failed` status when none is given. */\nconst DEFAULT_FAILED_ERROR = {\n code: 131026,\n title: 'Message undeliverable',\n message: 'Message undeliverable',\n error_data: { details: 'Message Undeliverable.' },\n};\n\n/**\n * WhatsApp reports a status for a message you sent. Dispatched to\n * `processor.onStatus`. A `failed` status gets error 131026 unless `errors` is given.\n */\nexport function createStatusWebhook(\n options: WebhookTargetOptions & {\n status: StatusWebhook['status'];\n /** Customer phone number the message was sent to. */\n recipientId: string;\n /** ID of the message you sent (from `messages.text()` etc.). Defaults to a random `wamid.` ID. */\n messageId?: string;\n timestamp?: string | number | Date;\n errors?: StatusWebhook['errors'];\n pricing?: StatusWebhook['pricing'];\n conversation?: StatusWebhook['conversation'];\n bizOpaqueCallbackData?: string;\n },\n): StatusWebhookPayload {\n const status: StatusWebhook = {\n id: options.messageId ?? randomWamid(),\n status: options.status,\n timestamp: toTimestamp(options.timestamp),\n recipient_id: options.recipientId,\n };\n if (options.conversation) status.conversation = options.conversation;\n if (options.pricing) status.pricing = options.pricing;\n if (options.bizOpaqueCallbackData !== undefined) status.biz_opaque_callback_data = options.bizOpaqueCallbackData;\n if (options.errors) status.errors = options.errors;\n else if (options.status === 'failed') status.errors = [structuredClone(DEFAULT_FAILED_ERROR)];\n\n const value: StatusWebhookValue = {\n messaging_product: 'whatsapp',\n metadata: metadata(options),\n statuses: [status],\n };\n return envelope({ field: 'messages', value }, options);\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import{CategoryEnum as e,LanguagesEnum as t,MessageTypesEnum as n,generateXHub256SigAsync as r}from"../../node/index.mjs";const i=new TextEncoder,a=new TextDecoder;function o(e){return i.encode(e)}function s(e){return a.decode(e)}function c(e){let t=atob(e),n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);return n}function l(e){let t=``,n=32768;for(let r=0;r<e.length;r+=n)t+=String.fromCharCode(...e.subarray(r,r+n));return btoa(t)}function u(e){let t=``;for(let n of e)t+=n.toString(16).padStart(2,`0`);return t}const d=`https://localhost/flow`;function f(){let e=globalThis.crypto?.subtle;if(!e)throw Error(`Web Crypto (globalThis.crypto.subtle) is not available in this runtime.`);return e}function p(e,t){return`-----BEGIN ${e}-----\n${(l(new Uint8Array(t)).match(/.{1,64}/g)??[]).join(`
2
+ `)}\n-----END ${e}-----\n`}async function m(){let e=await f().generateKey({name:`RSA-OAEP`,modulusLength:2048,publicExponent:new Uint8Array([1,0,1]),hash:`SHA-256`},!0,[`encrypt`,`decrypt`]);return{publicKey:e.publicKey,privateKey:e.privateKey,publicPem:p(`PUBLIC KEY`,await f().exportKey(`spki`,e.publicKey)),privatePem:p(`PRIVATE KEY`,await f().exportKey(`pkcs8`,e.privateKey))}}async function h(e){if(typeof e!=`string`)return e;let t=e.includes(`\\n`)?e.replace(/\\n/g,`
3
+ `):e,n=/-----BEGIN PUBLIC KEY-----([\s\S]*?)-----END PUBLIC KEY-----/.exec(t);if(!n)throw Error(`createFlowRequest: publicKey must be an SPKI PEM ("-----BEGIN PUBLIC KEY-----"). Convert a PKCS#1 key with: openssl rsa -RSAPublicKey_in -in key.pem -pubout`);return f().importKey(`spki`,c((n[1]??``).replace(/\s+/g,``)),{name:`RSA-OAEP`,hash:`SHA-256`},!1,[`encrypt`])}function g(e){let t=e.action??`data_exchange`,n={version:`3.0`,...e,action:t};return t!==`ping`&&n.flow_token===void 0&&(n.flow_token=`test-flow-token`),n}async function _(e,t){let n=g(e),i=f(),a=globalThis.crypto.getRandomValues(new Uint8Array(16)),s=globalThis.crypto.getRandomValues(new Uint8Array(16)),c=await i.importKey(`raw`,a,{name:`AES-GCM`},!1,[`encrypt`]),u=JSON.stringify({encrypted_aes_key:l(new Uint8Array(await i.encrypt({name:`RSA-OAEP`},await h(t.publicKey),a))),encrypted_flow_data:l(new Uint8Array(await i.encrypt({name:`AES-GCM`,iv:s},c,o(JSON.stringify(n))))),initial_vector:l(s)}),d={"content-type":`application/json`};t.appSecret!==void 0&&(d[`x-hub-signature-256`]=`sha256=${await r(u,t.appSecret)}`);let p={aesKey:a,initialVector:s};return{request:new Request(t.url??`https://localhost/flow`,{method:`POST`,body:u,headers:d}),body:u,payload:n,aesKey:a,initialVector:s,decryptResponse:e=>y(e,p)}}async function v(e){return typeof e==`string`?e:e instanceof Response?e.text():e.body}async function y(e,t){let n=(await v(e)).trim();if(n.startsWith(`{`))return JSON.parse(n);let r=t.initialVector.map(e=>~e&255),i=f(),a=await i.importKey(`raw`,new Uint8Array(t.aesKey),{name:`AES-GCM`},!1,[`decrypt`]),o;try{o=await i.decrypt({name:`AES-GCM`,iv:r},a,c(n))}catch{throw Error(`decryptFlowResponse: could not decrypt the Flow response: ${n.slice(0,200)}`)}return JSON.parse(s(new Uint8Array(o)))}function b(e){return globalThis.crypto.getRandomValues(new Uint8Array(e))}function x(){return`wamid.HBgL${l(b(24))}`}function S(e){let t=String(1+(b(1)[0]??0)%9);for(let n of b(e-1))t+=String(n%10);return t}const C=`lookaside.fbsbx.com`,w=[{id:`100000000000010`,name:`hello_world`,status:`APPROVED`,language:t.English_US,category:e.Utility,components:[]}];function T(e){return[0,3,10,190].includes(e)?401:[4,80007,130429,131048,131056].includes(e)?429:400}function E(e,t=T(e.code)){return D({error:{message:e.message??`(#${e.code}) Mock error`,type:e.type??`OAuthException`,...e,fbtrace_id:e.fbtrace_id??`mock-${S(8)}`}},t)}function D(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"content-type":`application/json`}})}function O(e,t){if(e instanceof RegExp){let n=new RegExp(e.source,e.flags).exec(t);return n?{...n.groups}:void 0}let n=e.replace(/^\/+|\/+$/g,``).split(`/`),r=t.replace(/^\/+|\/+$/g,``).split(`/`);if(n.length!==r.length)return;let i={};for(let e=0;e<n.length;e++){let t=n[e]??``,a=r[e]??``;if(t.startsWith(`:`))i[t.slice(1)]=decodeURIComponent(a);else if(t!==`*`&&t!==a)return}return i}function k(e){let t={};return e.forEach((e,n)=>{let r=t[n];r===void 0?t[n]=e:t[n]=Array.isArray(r)?[...r,e]:[r,e]}),t}function A(e){let t={};return e.forEach((e,n)=>{let r=t[n];r===void 0?t[n]=e:t[n]=Array.isArray(r)?[...r,e]:[r,e]}),t}async function j(e,t){let n=t?.body;if(n instanceof FormData)return{bodyType:`multipart`,body:k(n)};if(n instanceof URLSearchParams)return{bodyType:`form`,body:A(n)};if(e.body===null)return{bodyType:`none`,body:void 0};let r=e.headers.get(`content-type`)??``;if(r.includes(`multipart/form-data`))return{bodyType:`multipart`,body:k(await e.formData())};let i=await e.text();if(r.includes(`application/x-www-form-urlencoded`))return{bodyType:`form`,body:A(new URLSearchParams(i))};if(i===``)return{bodyType:`none`,body:void 0};try{return{bodyType:`json`,body:JSON.parse(i)}}catch{return{bodyType:`text`,body:i}}}function M(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function N(e){return e.method===`POST`&&/^\/[^/]+\/(messages|marketing_messages)$/.test(e.path)&&M(e.body)&&typeof e.body.to==`string`&&e.body.status===void 0}var P=class{requests=[];media=new Map;templates;routes=[];passthrough;unhandled;previousFetch;constructor(e={}){let t=e.fetch??globalThis.fetch;this.passthrough=(e,n)=>t(e,n),this.templates=structuredClone(e.templates??w),this.unhandled=e.unhandled??`error`}fetch=async(e,t)=>{let n=new URL(typeof e==`string`?e:e instanceof URL?e.href:e.url);if(n.host!==`graph.facebook.com`&&n.host!==C)return this.passthrough(e,t);let r=new Request(e,t),i=/^\/(v\d+(?:\.\d+)?)(\/.*)?$/.exec(n.pathname),a={};r.headers.forEach((e,t)=>{a[t]=e});let o={method:r.method.toUpperCase(),url:r.url,host:n.host,path:i?i[2]||`/`:n.pathname,apiVersion:i?.[1],query:Object.fromEntries(n.searchParams),headers:a,...await j(r,t)};return this.requests.push(o),this.respond(o)};install(){return this.previousFetch===void 0&&(this.previousFetch=globalThis.fetch,globalThis.fetch=this.fetch),this}restore(){this.previousFetch!==void 0&&(globalThis.fetch=this.previousFetch,this.previousFetch=void 0)}on(e,t,n,r={}){let i={method:e,pattern:t,handler:n,remaining:r.times??1/0};return this.routes.unshift(i),()=>{this.routes=this.routes.filter(e=>e!==i)}}fail(e,t,n,r={}){return this.on(e,t,()=>E(n,r.status),r)}requestsTo(e,t){return this.requests.filter(n=>(e===`*`||n.method===e)&&O(t,n.path)!==void 0)}sentMessages(e){return this.requests.filter(N).map(e=>e.body).filter(t=>e===void 0||t.type===e)}lastSentMessage(e){return this.sentMessages(e).at(-1)}clearRequests(){this.requests.length=0,this.media.clear()}reset(){this.clearRequests(),this.routes=[]}async respond(e){for(let t of[...this.routes]){if(t.remaining<=0||t.method!==`*`&&t.method!==e.method)continue;let n=O(t.pattern,e.path);if(!n)continue;t.remaining--;let r;try{r=await t.handler({...e,params:n})}catch(e){throw t.remaining++,e}if(r===void 0){t.remaining++;continue}return r instanceof Response?r:D(r)}return this.defaultResponse(e)}async defaultResponse(e){let{method:t,path:n,host:r}=e,i=n.replace(/^\/+|\/+$/g,``).split(`/`);if(r===C){let t=this.media.get(e.query.mid??``);return new Response(t?new Uint8Array(t.bytes):`mock media`,{headers:{"content-type":t?.mimeType??`application/octet-stream`}})}if(t===`POST`&&i.length===2&&/^(messages|marketing_messages)$/.test(i[1]??``)){let t=M(e.body)?e.body:{};if(t.status===`read`||t.status===`typing`)return D({success:!0});let n=typeof t.to==`string`?t.to:``;return D({messaging_product:`whatsapp`,contacts:[{input:n,wa_id:n.replace(/\D/g,``)||n}],messages:[{id:x(),...t.type===`template`&&{message_status:`accepted`}}]})}if(t===`POST`&&i.length===2&&i[1]===`media`){let t=M(e.body)?e.body:{},n=t.file,r=S(16),i=n instanceof Blob?new Uint8Array(await n.arrayBuffer()):new Uint8Array,a=new Uint8Array(await globalThis.crypto.subtle.digest(`SHA-256`,i));return this.media.set(r,{id:r,mimeType:typeof t.type==`string`&&t.type||n instanceof Blob&&n.type||``,fileSize:i.byteLength,sha256:u(a),bytes:i}),D({id:r})}if(i.length===2&&i[1]===`message_templates`){if(t===`GET`){let{name:t,status:n,language:r,category:i}=e.query,a=this.templates.filter(e=>(!t||e.name===t)&&(!n||e.status===n)&&(!r||e.language===r)&&(!i||e.category===i)),o=Number(e.query.limit)||a.length;return D({data:a.slice(0,o),paging:{cursors:{before:`MAZDZD`,after:`MjQZD`},next:``}})}if(t===`POST`){let t=M(e.body)?e.body:{};return D({id:S(15),status:`PENDING`,category:t.category??`UTILITY`})}if(t===`DELETE`)return D({success:!0})}if(i.length===1&&i[0]){let e=i[0];if(t===`DELETE`)return this.media.delete(e),D({success:!0});if(t===`GET`){let t=this.media.get(e);return D({messaging_product:`whatsapp`,id:e,url:`https://${C}/whatsapp_business/attachments/?mid=${e}`,mime_type:t?.mimeType||`image/jpeg`,sha256:t?.sha256??`0`.repeat(64),file_size:t?.fileSize??0})}}return this.unhandled===`success`?D({success:!0}):E({code:100,type:`GraphMethodException`,message:`meta-cloud-api/testing: no mock route for ${t} ${n}. Add one with mock.on('${t}', '${n}', handler).`},400)}};function F(e){return new P(e)}const I=`https://localhost/webhook`;async function L(e,t){let n=typeof e==`string`?e:JSON.stringify(e),i=await r(n,t.appSecret),a=new Headers({"content-type":`application/json`,"x-hub-signature-256":`sha256=${i}`});for(let[e,n]of Object.entries(t.headers??{}))a.set(e,n);return new Request(t.url??`https://localhost/webhook`,{method:`POST`,body:n,headers:a})}const R=`100000000000001`,z=`15550100000`,B=`100000000000002`;function V(e){return e===void 0?String(Math.floor(Date.now()/1e3)):e instanceof Date?String(Math.floor(e.getTime()/1e3)):String(e)}function H(e){return{display_phone_number:e.displayPhoneNumber??`15550100000`,phone_number_id:String(e.phoneNumberId??`100000000000001`)}}function U(e){return{from:e.from,id:e.id??x(),timestamp:V(e.timestamp)}}function W(e){return{from:e.displayPhoneNumber??`15550100000`,id:e.contextMessageId??x()}}function G(e,t,n={}){return K({field:e,value:t},n)}function K(e,t){return{object:`whatsapp_business_account`,entry:[{id:t.wabaId??`100000000000002`,changes:[e]}]}}function q(e,t={}){return K({field:`messages`,value:{messaging_product:`whatsapp`,metadata:H(t),contacts:[{profile:{name:t.profileName??`Test User`},wa_id:e.from}],messages:[e]}},t)}function J(e){return q({...U(e),type:n.Text,text:{body:e.text}},e)}function Y(e){let t=e.mediaId??S(16),r={id:t,mime_type:e.mimeType??`image/jpeg`,sha256:e.sha256??`test-sha256`,url:e.url??`https://lookaside.fbsbx.com/whatsapp_business/attachments/?mid=${t}`};return e.caption!==void 0&&(r.caption=e.caption),q({...U(e),type:n.Image,image:r},e)}function X(e){return q({...U(e),type:n.Interactive,context:W(e),interactive:{type:`button_reply`,button_reply:{id:e.buttonId,title:e.title}}},e)}function Z(e){let t={id:e.rowId,title:e.title};return e.description!==void 0&&(t.description=e.description),q({...U(e),type:n.Interactive,context:W(e),interactive:{type:`list_reply`,list_reply:t}},e)}function Q(e){let t={message_id:e.messageId};return e.emoji!==void 0&&(t.emoji=e.emoji),q({...U(e),type:n.Reaction,reaction:t},e)}function $(e){let t={latitude:e.latitude,longitude:e.longitude};return e.name!==void 0&&(t.name=e.name),e.address!==void 0&&(t.address=e.address),e.url!==void 0&&(t.url=e.url),q({...U(e),type:n.Location,location:t},e)}const ee={code:131026,title:`Message undeliverable`,message:`Message undeliverable`,error_data:{details:`Message Undeliverable.`}};function te(e){let t={id:e.messageId??x(),status:e.status,timestamp:V(e.timestamp),recipient_id:e.recipientId};return e.conversation&&(t.conversation=e.conversation),e.pricing&&(t.pricing=e.pricing),e.bizOpaqueCallbackData!==void 0&&(t.biz_opaque_callback_data=e.bizOpaqueCallbackData),e.errors?t.errors=e.errors:e.status===`failed`&&(t.errors=[structuredClone(ee)]),K({field:`messages`,value:{messaging_product:`whatsapp`,metadata:H(e),statuses:[t]}},e)}export{P as MockCloudApi,z as TEST_DISPLAY_PHONE_NUMBER,d as TEST_FLOW_URL,R as TEST_PHONE_NUMBER_ID,B as TEST_WABA_ID,I as TEST_WEBHOOK_URL,X as createButtonReplyWebhook,_ as createFlowRequest,Y as createImageMessageWebhook,Z as createListReplyWebhook,$ as createLocationWebhook,q as createMessageWebhook,E as createMetaErrorResponse,F as createMockCloudApi,Q as createReactionWebhook,L as createSignedWebhookRequest,te as createStatusWebhook,J as createTextMessageWebhook,G as createWebhookPayload,y as decryptFlowResponse,m as generateFlowKeyPair};