/** * WhatsApp Coexistence webhooks (Embedded Signup v3+). * * When a business runs the WhatsApp Business app on a phone AND the Cloud * API on the same number, Meta delivers three extra webhook fields * alongside the ordinary `messages` field: * * - `smb_message_echoes` — messages the human sent from the phone. * - `history` — thread history synced during onboarding. * - `smb_app_state_sync` — contact / app-state changes from the phone. * * `extractInbound` doesn't model these (they carry no `contacts` + * `messages` pair, so it returns `{ kind: 'unknown' }`) and `Agent.enqueue` * ignores them. This module is the missing piece: split coexistence changes * out of an envelope, and parse the echo payload into a normalized shape. * * The load-bearing use case is **human handoff**: an echo means a human just * answered from the phone, so the agent should stay quiet for a while * instead of double-replying. Pair with `HumanHandoffWindow` * (`src/handoff/human_handoff.ts`) and wire via `mountWebhook`'s / * `mountMultiTenantWebhook`'s `onCoexistence` option. * * The `history` and `smb_app_state_sync` payloads are passed through raw: * their shapes are large, versioned by Meta, and app-specific in what's * worth persisting. Echoes get a typed shape because every consumer needs * the same three fields out of them. */ import type { InboundEnvelope } from '../types.js'; /** Webhook `field` values Meta uses for coexistence changes. */ export declare const COEXISTENCE_FIELDS: readonly ["smb_message_echoes", "history", "smb_app_state_sync"]; export type CoexistenceField = (typeof COEXISTENCE_FIELDS)[number]; /** True when `field` is one of the three coexistence webhook fields. */ export declare function isCoexistenceField(field: unknown): field is CoexistenceField; /** One coexistence change lifted out of an envelope, with its origin number. */ export interface CoexistenceChange { field: CoexistenceField; /** `metadata.phone_number_id` — the business number the change belongs to. */ phoneNumberId: string | null; /** Raw `change.value`, untouched. */ value: unknown; } /** * A message the business sent from the WhatsApp Business app (not via the * Cloud API). `to` is the customer — that's the thread the agent should * back off from. */ export interface MessageEcho { wamid: string; phoneNumberId: string | null; /** Customer the human messaged. */ to: string | null; /** Business number the echo came from. */ from: string | null; type: string; /** Body for `text` echoes, caption for media echoes, else null. */ text: string | null; timestampMs: number | null; /** Raw echo object — media ids, context, and anything Meta adds later. */ raw: unknown; } export interface PartitionedWebhook { /** True when at least one change carried a coexistence field. */ hasCoexistence: boolean; /** Every coexistence change, flattened across entries. */ coexistence: CoexistenceChange[]; /** * The same envelope with coexistence changes stripped, ready for * `Agent.enqueue`. `null` when nothing ordinary is left — don't enqueue. */ ordinary: InboundEnvelope | null; } /** * Split an envelope into its coexistence changes and the ordinary remainder. * * const { hasCoexistence, coexistence, ordinary } = partitionCoexistenceWebhook(body); * if (hasCoexistence) await handleCoexistence(coexistence); * if (ordinary) await agent.enqueue(ordinary); * * Entries whose changes were entirely coexistence are dropped from * `ordinary`; every other key on the envelope and its entries is preserved. * Non-object input yields an empty partition rather than throwing. */ export declare function partitionCoexistenceWebhook(body: unknown): PartitionedWebhook; /** True when the envelope carries at least one coexistence change. */ export declare function isCoexistenceWebhook(body: unknown): boolean; /** Every coexistence change in the envelope, flattened. */ export declare function extractCoexistenceChanges(body: unknown): CoexistenceChange[]; /** * Normalized echoes from an envelope's `smb_message_echoes` changes. * Returns `[]` when there are none — safe to call on any envelope. * * Accepts either a full envelope or the pre-partitioned change list, so * `onCoexistence` handlers can pass whichever they already hold. */ export declare function extractEchoes(body: unknown | CoexistenceChange[]): MessageEcho[]; /** Arguments handed to `mountWebhook`'s / `mountMultiTenantWebhook`'s `onCoexistence`. */ export interface CoexistenceHandlerArgs { /** Worker env, straight off the Hono context. */ env: unknown; /** Resolved tenant, or null on the single-tenant mount. */ tenantId: string | null; /** Every coexistence change in this webhook. */ changes: CoexistenceChange[]; /** Echoes already parsed out of `changes` — the common case. */ echoes: MessageEcho[]; /** The full raw envelope, for anything the parsed shapes don't cover. */ envelope: unknown; } export type CoexistenceHandler = (args: CoexistenceHandlerArgs) => void | Promise; //# sourceMappingURL=coexistence.d.ts.map