@kuralle-agents/messaging-meta 0.8.0 → 0.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,105 @@
1
+ /**
2
+ * @module whatsapp/commerce
3
+ *
4
+ * Helpers and limits for WhatsApp commerce messages.
5
+ *
6
+ * Commerce messages let you share products from a Meta catalog and receive
7
+ * orders directly in WhatsApp. The send methods live on the
8
+ * {@link WhatsAppClient} (`sendProduct`, `sendProductList`, `sendCatalog`,
9
+ * `sendAddressRequest`); this module provides the inbound typed accessors.
10
+ *
11
+ * @see https://developers.facebook.com/documentation/business-messaging/whatsapp/catalogs/share-products
12
+ * @see https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/order
13
+ */
14
+ // ---------------------------------------------------------------------------
15
+ // Limits (per Meta Cloud API docs)
16
+ // ---------------------------------------------------------------------------
17
+ /** Maximum number of sections in a multi-product message. */
18
+ export const MAX_PRODUCT_LIST_SECTIONS = 10;
19
+ /** Maximum number of products across all sections of a multi-product message. */
20
+ export const MAX_PRODUCT_LIST_PRODUCTS = 30;
21
+ // ---------------------------------------------------------------------------
22
+ // Inbound typed accessors
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Extract the typed order from an inbound message, when the user placed an
26
+ * order from a catalog, single-, or multi-product message.
27
+ *
28
+ * The generic `InboundMessage` type has no `order` field, so the order
29
+ * travels on `message.raw` (the normalized WhatsApp webhook message). This
30
+ * accessor returns it typed.
31
+ *
32
+ * @param message - An inbound message from the WhatsApp client.
33
+ * @returns The order, or `undefined` if the message is not an order.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * client.onMessage(async (msg) => {
38
+ * const order = parseInboundOrder(msg);
39
+ * if (order) {
40
+ * console.log(`Order against catalog ${order.catalog_id}:`);
41
+ * for (const item of order.product_items) {
42
+ * console.log(` ${item.quantity}x ${item.product_retailer_id} @ ${item.item_price} ${item.currency}`);
43
+ * }
44
+ * }
45
+ * });
46
+ * ```
47
+ */
48
+ export function parseInboundOrder(message) {
49
+ const raw = message.raw;
50
+ const order = raw?.order;
51
+ if (!order || typeof order.catalog_id !== 'string' || !Array.isArray(order.product_items)) {
52
+ return undefined;
53
+ }
54
+ return order;
55
+ }
56
+ /**
57
+ * Extract the typed address submission from an inbound message, when the
58
+ * user replied to an address message (`sendAddressRequest`).
59
+ *
60
+ * Address replies arrive as `interactive.type === "nfm_reply"` messages with
61
+ * `nfm_reply.name === "address_message"`; the address fields are JSON-encoded
62
+ * in `nfm_reply.response_json`.
63
+ *
64
+ * @param message - An inbound message from the WhatsApp client.
65
+ * @returns The parsed address, or `undefined` if the message is not an
66
+ * address submission (or the payload is malformed).
67
+ */
68
+ export function parseInboundAddress(message) {
69
+ const raw = message.raw;
70
+ const nfm = raw?.interactive?.nfm_reply;
71
+ if (!nfm || nfm.name !== 'address_message' || !nfm.response_json) {
72
+ return undefined;
73
+ }
74
+ try {
75
+ const parsed = JSON.parse(nfm.response_json);
76
+ if (parsed !== null &&
77
+ typeof parsed === 'object' &&
78
+ !Array.isArray(parsed) &&
79
+ typeof parsed.values === 'object' &&
80
+ parsed.values !== null) {
81
+ return parsed;
82
+ }
83
+ return undefined;
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ /**
90
+ * Extract a product inquiry from an inbound message when the user replied
91
+ * in the context of a catalog product message (`context.referred_product`).
92
+ */
93
+ export function parseProductInquiry(message) {
94
+ const raw = message.raw;
95
+ const referred = raw?.context?.referred_product;
96
+ if (!referred?.catalog_id || !referred.product_retailer_id) {
97
+ return undefined;
98
+ }
99
+ return {
100
+ catalog_id: referred.catalog_id,
101
+ product_retailer_id: referred.product_retailer_id,
102
+ context_message_id: raw?.context?.message_id,
103
+ context_from: raw?.context?.from,
104
+ };
105
+ }
@@ -30,9 +30,10 @@
30
30
  * @packageDocumentation
31
31
  */
32
32
  export { WhatsAppClient, createWhatsAppClient } from './client.js';
33
- export type { WhatsAppClientConfig, WhatsAppThreadId, WhatsAppSendResponse, TemplateMessage, TemplateLanguage, TemplateComponent, TemplateParameter, MediaObject, ListMessage, ListSection, ListRow, ButtonMessage, ReplyButton, CTAButtonMessage, FlowInteractiveInput, LocationPayload, ContactPayload, BusinessProfile, TemplateDefinition, TemplateDefinitionComponent, TemplateInfo, FlowDefinition, FlowInfo, FlowAssets, WhatsAppMediaResponse, TextOrTemplateOptions, } from './types.js';
33
+ export type { WhatsAppClientConfig, WhatsAppThreadId, WhatsAppSendResponse, TemplateMessage, TemplateLanguage, TemplateComponent, TemplateParameter, MediaObject, ListMessage, ListSection, ListRow, ButtonMessage, ReplyButton, CTAButtonMessage, FlowInteractiveInput, ProductMessage, ProductListMessage, ProductSection, CatalogMessage, AddressMessage, WhatsAppAddressValues, WhatsAppSavedAddress, WhatsAppOrderItem, WhatsAppInboundOrder, WhatsAppInboundAddress, WhatsAppInboundProductInquiry, LocationPayload, ContactPayload, BusinessProfile, TemplateDefinition, TemplateDefinitionComponent, TemplateCreateResponse, TemplateInfo, FlowDefinition, FlowCategory, FlowInfo, FlowAssets, WhatsAppMediaResponse, TextOrTemplateOptions, } from './types.js';
34
34
  export { buildTemplateSendPayload, buildTemplatePayload, mapOutboundTemplateComponents, } from './templates.js';
35
35
  export type { TypedTemplateConfig, RawTemplateComponents, } from './templates.js';
36
36
  export { WhatsAppFormatConverter } from './format.js';
37
37
  export { splitMessage } from './split.js';
38
38
  export { generateFlowToken, buildFlowInput } from './flows.js';
39
+ export { parseInboundOrder, parseInboundAddress, parseProductInquiry, MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
@@ -39,3 +39,5 @@ export { WhatsAppFormatConverter } from './format.js';
39
39
  export { splitMessage } from './split.js';
40
40
  // ─── Flows ──────────────────────────────────────────────────────────────
41
41
  export { generateFlowToken, buildFlowInput } from './flows.js';
42
+ // ─── Commerce ───────────────────────────────────────────────────────────
43
+ export { parseInboundOrder, parseInboundAddress, parseProductInquiry, MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
@@ -27,7 +27,7 @@ export interface WhatsAppClientConfig {
27
27
  phoneNumberId: string;
28
28
  /** Custom verify token for webhook subscription validation. */
29
29
  verifyToken: string;
30
- /** Graph API version (e.g. `"v21.0"`). Default `"v21.0"`. */
30
+ /** Graph API version (e.g. `"v24.0"`). Default `"v24.0"`. */
31
31
  apiVersion?: string;
32
32
  /** Base URL for the Graph API. Default `"https://graph.facebook.com"`. */
33
33
  baseUrl?: string;
@@ -127,6 +127,8 @@ export interface TemplateParameter {
127
127
  payload?: string;
128
128
  /** Action payload (when `type` is `"action"`). */
129
129
  action?: Record<string, unknown>;
130
+ /** Parameter name for named-parameter templates. */
131
+ parameter_name?: string;
130
132
  }
131
133
  /**
132
134
  * A media object used within WhatsApp messages.
@@ -149,7 +151,7 @@ export interface MediaObject {
149
151
  /**
150
152
  * A list-style interactive message with expandable sections.
151
153
  *
152
- * Supports up to 10 sections with up to 10 rows each. The `button` text
154
+ * Supports up to 10 rows total across all sections. The `button` text
153
155
  * appears on the collapsed list control.
154
156
  */
155
157
  export interface ListMessage {
@@ -272,13 +274,187 @@ export interface FlowInteractiveInput {
272
274
  flowId: string;
273
275
  /** Call-to-action button text that opens the flow. */
274
276
  flowCta: string;
275
- /** Unique token for this flow session. */
276
- flowToken: string;
277
+ /** Unique token for this flow session (omit when not required). */
278
+ flowToken?: string;
277
279
  /** Flow action type. */
278
280
  flowAction: 'navigate' | 'data_exchange';
279
281
  /** Optional initial data for the flow. */
280
282
  flowActionPayload?: Record<string, unknown>;
281
283
  }
284
+ /**
285
+ * A single-product interactive message (`interactive.type: "product"`).
286
+ *
287
+ * Displays one product from your catalog with a product image, title,
288
+ * price, and a **View** button that opens the Product Detail Page.
289
+ */
290
+ export interface ProductMessage {
291
+ /** Optional body text (max 1024 chars). */
292
+ body?: {
293
+ text: string;
294
+ };
295
+ /** Optional footer text (max 60 chars). */
296
+ footer?: {
297
+ text: string;
298
+ };
299
+ /** Meta product catalog ID. */
300
+ catalogId: string;
301
+ /** Product SKU (labeled **Content ID** in Commerce Manager). */
302
+ productRetailerId: string;
303
+ }
304
+ /**
305
+ * A multi-product interactive message (`interactive.type: "product_list"`).
306
+ *
307
+ * Displays up to 30 products from your catalog, organized into up to
308
+ * 10 sections. Header and body are required by the Cloud API.
309
+ */
310
+ export interface ProductListMessage {
311
+ /** Required header (text only for product lists). */
312
+ header: {
313
+ type: 'text';
314
+ text: string;
315
+ };
316
+ /** Body text (required, max 1024 chars). */
317
+ body: {
318
+ text: string;
319
+ };
320
+ /** Optional footer text (max 60 chars). */
321
+ footer?: {
322
+ text: string;
323
+ };
324
+ /** Meta product catalog ID. */
325
+ catalogId: string;
326
+ /** Product sections (max 10; max 30 products across all sections). */
327
+ sections: ProductSection[];
328
+ }
329
+ /** A section within a multi-product message. */
330
+ export interface ProductSection {
331
+ /** Section title. */
332
+ title: string;
333
+ /** Product SKUs in this section (Content IDs in Commerce Manager). */
334
+ productRetailerIds: string[];
335
+ }
336
+ /**
337
+ * A catalog interactive message (`interactive.type: "catalog_message"`).
338
+ *
339
+ * Displays a product thumbnail header image, custom body text, and a
340
+ * **View catalog** button that opens your full catalog within WhatsApp.
341
+ */
342
+ export interface CatalogMessage {
343
+ /** Body text (required, max 1024 chars). */
344
+ body: {
345
+ text: string;
346
+ };
347
+ /** Optional footer text (max 60 chars). */
348
+ footer?: {
349
+ text: string;
350
+ };
351
+ /**
352
+ * Optional product SKU whose image is used as the header thumbnail.
353
+ * When omitted, the first item in the catalog is used.
354
+ */
355
+ thumbnailProductRetailerId?: string;
356
+ }
357
+ /**
358
+ * Address field values used in address messages.
359
+ *
360
+ * Field names mirror the Cloud API wire format. Currently only available
361
+ * for India-based businesses and their India customers.
362
+ */
363
+ export interface WhatsAppAddressValues {
364
+ /** Recipient name. */
365
+ name?: string;
366
+ /** Recipient phone number. */
367
+ phone_number?: string;
368
+ /** Pin code (India, max 6 chars). */
369
+ in_pin_code?: string;
370
+ /** Flat/house number. */
371
+ house_number?: string;
372
+ /** Floor number. */
373
+ floor_number?: string;
374
+ /** Tower number. */
375
+ tower_number?: string;
376
+ /** Building/apartment name. */
377
+ building_name?: string;
378
+ /** Street address. */
379
+ address?: string;
380
+ /** Landmark/area. */
381
+ landmark_area?: string;
382
+ /** City. */
383
+ city?: string;
384
+ /** State. */
385
+ state?: string;
386
+ }
387
+ /** A previously saved address offered to the user in an address message. */
388
+ export interface WhatsAppSavedAddress {
389
+ /** Identifier returned as `saved_address_id` when the user selects it. */
390
+ id: string;
391
+ /** The saved address field values. */
392
+ value: WhatsAppAddressValues;
393
+ }
394
+ /**
395
+ * An address request interactive message (`interactive.type: "address_message"`).
396
+ *
397
+ * Country-gated: only available for India-based businesses and their
398
+ * India customers. The user's submission arrives as an inbound
399
+ * `interactive.type: "nfm_reply"` message — see {@link parseInboundAddress}.
400
+ */
401
+ export interface AddressMessage {
402
+ /** Body text (required). */
403
+ body: {
404
+ text: string;
405
+ };
406
+ /** ISO country code (required by the API, e.g. `"IN"`). */
407
+ country: string;
408
+ /** Optional prefilled address field values. */
409
+ values?: WhatsAppAddressValues;
410
+ /** Optional saved addresses the user can pick instead of typing. */
411
+ savedAddresses?: WhatsAppSavedAddress[];
412
+ /**
413
+ * Optional per-field validation errors. WhatsApp blocks submission
414
+ * until the flagged fields are corrected.
415
+ */
416
+ validationErrors?: Partial<Record<keyof WhatsAppAddressValues, string>>;
417
+ }
418
+ /** A line item in an inbound WhatsApp order (wire format). */
419
+ export interface WhatsAppOrderItem {
420
+ /** Product SKU (Content ID in Commerce Manager). */
421
+ product_retailer_id: string;
422
+ /** Quantity ordered. */
423
+ quantity: number;
424
+ /** Unit price of the item. */
425
+ item_price: number;
426
+ /** Catalog currency code (e.g. `"USD"`). */
427
+ currency: string;
428
+ }
429
+ /**
430
+ * An inbound order placed by a user from a catalog, single-, or
431
+ * multi-product message (webhook message `type: "order"`).
432
+ */
433
+ export interface WhatsAppInboundOrder {
434
+ /** Meta product catalog ID the order was placed against. */
435
+ catalog_id: string;
436
+ /** Text accompanying the order. */
437
+ text?: string;
438
+ /** Ordered line items. */
439
+ product_items: WhatsAppOrderItem[];
440
+ }
441
+ /**
442
+ * A parsed inbound address submission (the `nfm_reply.response_json`
443
+ * of an address message reply).
444
+ */
445
+ export interface WhatsAppInboundAddress {
446
+ /** ID of the saved address the user selected, when applicable. */
447
+ saved_address_id?: string;
448
+ /** Address fields entered or confirmed by the user. */
449
+ values: WhatsAppAddressValues;
450
+ }
451
+ /** A product inquiry from an inbound message context.referred_product. */
452
+ export interface WhatsAppInboundProductInquiry {
453
+ catalog_id: string;
454
+ product_retailer_id: string;
455
+ context_message_id?: string;
456
+ context_from?: string;
457
+ }
282
458
  /** A location payload for sending a map pin. */
283
459
  export interface LocationPayload {
284
460
  /** Latitude in decimal degrees. */
@@ -343,6 +519,14 @@ export interface TemplateDefinition {
343
519
  components: TemplateDefinitionComponent[];
344
520
  /** Whether Meta can auto-recategorize the template. */
345
521
  allow_category_change?: boolean;
522
+ /** Named vs positional parameter placeholders. */
523
+ parameter_format?: 'named' | 'positional';
524
+ }
525
+ /** Response from creating a message template. */
526
+ export interface TemplateCreateResponse {
527
+ id: string;
528
+ status: string;
529
+ category: string;
346
530
  }
347
531
  /** A component within a template definition. */
348
532
  export interface TemplateDefinitionComponent {
@@ -387,11 +571,19 @@ export interface TemplateInfo {
387
571
  export interface FlowDefinition {
388
572
  /** Flow name. */
389
573
  name: string;
390
- /** Flow categories. */
391
- categories?: string[];
574
+ /** Flow categories (at least one required by the API). */
575
+ categories: FlowCategory[];
392
576
  /** Clone from an existing flow. */
393
577
  clone_flow_id?: string;
578
+ /** Initial flow JSON definition. */
579
+ flow_json?: string;
580
+ /** Publish immediately after creation. */
581
+ publish?: boolean;
582
+ /** Endpoint URI for data_exchange flows. */
583
+ endpoint_uri?: string;
394
584
  }
585
+ /** Valid WhatsApp Flow categories. */
586
+ export type FlowCategory = 'SIGN_UP' | 'SIGN_IN' | 'APPOINTMENT_BOOKING' | 'LEAD_GENERATION' | 'CONTACT_US' | 'CUSTOMER_SUPPORT' | 'SURVEY' | 'OTHER';
395
587
  /** Information about an existing WhatsApp Flow. */
396
588
  export interface FlowInfo {
397
589
  /** Flow ID. */
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/kuralle/kuralle-agents.git",
7
7
  "directory": "packages/kuralle-messaging-meta"
8
8
  },
9
- "version": "0.8.0",
9
+ "version": "0.9.0",
10
10
  "description": "Meta platform clients (WhatsApp, Messenger, Instagram) for Kuralle messaging",
11
11
  "type": "module",
12
12
  "main": "dist/index.js",
@@ -41,8 +41,8 @@
41
41
  "README.md"
42
42
  ],
43
43
  "dependencies": {
44
- "@kuralle-agents/http-client": "0.8.0",
45
- "@kuralle-agents/messaging": "0.8.0"
44
+ "@kuralle-agents/http-client": "0.9.0",
45
+ "@kuralle-agents/messaging": "0.9.0"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "hono": "^4.12.0"
@@ -58,9 +58,9 @@
58
58
  "redis": "^4.7.0",
59
59
  "typescript": "^5.8.2",
60
60
  "zod": "^4.0.0",
61
- "@kuralle-agents/core": "0.8.0",
62
- "@kuralle-agents/engagement": "0.8.0",
63
- "@kuralle-agents/hono-server": "0.8.0"
61
+ "@kuralle-agents/engagement": "0.9.0",
62
+ "@kuralle-agents/core": "0.9.0",
63
+ "@kuralle-agents/hono-server": "0.9.0"
64
64
  },
65
65
  "scripts": {
66
66
  "prebuild": "rm -rf dist",