npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@molecule/api-channel-messenger

v1.0.3

Published

Facebook Messenger channel bond — implements @molecule/api-channel for sendMessage via the Send API, X-Hub-Signature-256 webhook verification, and inbound entry/messaging parsing.

Readme

@molecule/api-channel-messenger

Facebook Messenger channel provider for molecule.dev.

Implements the framework-agnostic {@link ChannelProvider} interface over the Messenger Send API and webhook envelope. Bond under the named-multi-provider 'channel' category at app startup:

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

import { setProvider } from '@molecule/api-channel'
import { provider } from '@molecule/api-channel-messenger'

setProvider('messenger', provider)

Type

provider

Installation

npm install @molecule/api-channel-messenger @molecule/api-channel @molecule/api-secrets

API

Interfaces

MessengerConfig

Configuration for the Messenger channel provider.

The page access token authorizes Send API calls; the app secret signs inbound webhooks via X-Hub-Signature-256. Both are deliberately accepted only via this config (or the CHANNEL_MESSENGER_PAGE_ACCESS_TOKEN / CHANNEL_MESSENGER_APP_SECRET env vars) and are NEVER included in error messages, log lines, or normalized payloads.

interface MessengerConfig {
  /**
   * Page access token (`EAA…`). Required for outbound `sendMessage`.
   * Defaults to the `CHANNEL_MESSENGER_PAGE_ACCESS_TOKEN` env var.
   *
   * Treat as a secret — providers redact this value in any user-facing
   * output.
   */
  pageAccessToken?: string

  /**
   * Facebook app secret used to verify `X-Hub-Signature-256` on inbound
   * webhook requests. Defaults to the `CHANNEL_MESSENGER_APP_SECRET` env
   * var.
   *
   * If unset, {@link MessengerChannelProvider.verifyWebhookSignature}
   * returns `false`.
   */
  appSecret?: string

  /**
   * Graph API base URL. Override only for tests or alternative regional
   * endpoints. Defaults to `https://graph.facebook.com`.
   */
  apiBaseUrl?: string

  /**
   * Graph API version to target. Defaults to `'v22.0'`.
   */
  apiVersion?: string

  /**
   * Per-request timeout in milliseconds. Defaults to 10000.
   */
  timeoutMs?: number

  /**
   * Optional default `messaging_type` applied to outbound sends. Defaults
   * to `'RESPONSE'` (replies to user-initiated conversations within the
   * 24-hour window). Override to `'UPDATE'` or `'MESSAGE_TAG'` when
   * sending unsolicited messages — note the Messenger Platform policy
   * restrictions.
   */
  defaultMessagingType?: MessengerMessagingType
}

ProcessEnv

Environment variables consumed by the Messenger channel provider.

interface ProcessEnv {
  /** Page access token (`EAA…`). Required for outbound sends. */
  CHANNEL_MESSENGER_PAGE_ACCESS_TOKEN: string

  /** Facebook app secret used to verify inbound webhook signatures. */
  CHANNEL_MESSENGER_APP_SECRET: string
}

MessengerActor

Subset of the Messenger Sender/Recipient shape used during inbound normalization.

interface MessengerActor {
  /** Page-scoped user identifier. */
  id: string
}

MessengerInboundAttachment

Subset of an inbound Messenger attachment object.

interface MessengerInboundAttachment {
  /** Attachment kind (`'image'`, `'video'`, `'audio'`, `'file'`, …). */
  type?: string
  /** Optional payload object — typically `{ url }`. */
  payload?: { url?: string; sticker_id?: number }
}

MessengerInboundMessage

Subset of an inbound Messenger message object.

interface MessengerInboundMessage {
  /** Provider-assigned message identifier (mid). */
  mid?: string
  /** Plain-text body. */
  text?: string
  /** Quick-reply payload, when the user clicked a quick reply. */
  quick_reply?: { payload?: string }
  /** Attachments (images, files, …) included with the message. */
  attachments?: MessengerInboundAttachment[]
  /** Whether the message was an echo of one this app sent. */
  is_echo?: boolean
}

MessengerInboundPostback

Subset of an inbound Messenger postback object — the payload returned when a user taps a button on a button_template or persistent menu.

interface MessengerInboundPostback {
  /** Opaque payload originally set on the button. */
  payload?: string
  /** Visible title shown on the button when it was tapped. */
  title?: string
}

MessengerInboundDelivery

Subset of an inbound Messenger delivery object.

interface MessengerInboundDelivery {
  /** Mids of the messages confirmed delivered. */
  mids?: string[]
  /** Watermark — all messages sent before this timestamp are delivered. */
  watermark?: number
}

MessengerInboundRead

Subset of an inbound Messenger read object.

interface MessengerInboundRead {
  /** Watermark — all messages sent before this timestamp are read. */
  watermark?: number
}

MessengerMessagingEntry

A single messaging entry inside an inbound webhook envelope.

interface MessengerMessagingEntry {
  /** Sender of the inbound event. */
  sender?: MessengerActor
  /** Recipient (typically the page receiving the event). */
  recipient?: MessengerActor
  /** Unix timestamp in milliseconds. */
  timestamp?: number
  /** Inbound user message. */
  message?: MessengerInboundMessage
  /** Inbound button-tap postback. */
  postback?: MessengerInboundPostback
  /** Delivery confirmation. */
  delivery?: MessengerInboundDelivery
  /** Read receipt. */
  read?: MessengerInboundRead
}

MessengerWebhookEntry

A single entry inside an inbound webhook envelope.

interface MessengerWebhookEntry {
  /** Page identifier the events belong to. */
  id?: string
  /** Unix timestamp in milliseconds. */
  time?: number
  /** Per-conversation events. Typically a single-element array. */
  messaging?: MessengerMessagingEntry[]
}

MessengerWebhookPayload

Top-level Messenger webhook envelope.

interface MessengerWebhookPayload {
  /** Always `'page'` for Messenger Platform webhooks. */
  object?: string
  /** Per-page event groups. */
  entry?: MessengerWebhookEntry[]
}

MessengerSendApiResponse

Successful Send API response shape used by the provider.

interface MessengerSendApiResponse {
  /** Page-scoped recipient identifier (echoed). */
  recipient_id?: string
  /** Messenger-assigned outbound message id. */
  message_id?: string
}

Types

MessengerMessagingType

Messenger messaging_type values accepted on the Send API.

type MessengerMessagingType = 'RESPONSE' | 'UPDATE' | 'MESSAGE_TAG'

Classes

MessengerChannelProvider

Concrete Messenger Platform implementation of {@link ChannelProvider}.

class MessengerChannelProvider implements ChannelProvider {
  /**
   * Stable channel name. Conventionally matches the bond name this
   * provider is registered under (`bond('channel', 'messenger', ...)`).
   */
  readonly name = 'messenger'

  private readonly pageAccessToken: string | undefined
  private readonly appSecret: string | undefined
  private readonly apiBaseUrl: string
  private readonly apiVersion: string
  private readonly timeoutMs: number
  private readonly defaultMessagingType: MessengerMessagingType

  /**
   * Constructs a Messenger channel provider.
   *
   * @param config - Optional configuration. Page access token and app
   *   secret fall back to `CHANNEL_MESSENGER_PAGE_ACCESS_TOKEN` and
   *   `CHANNEL_MESSENGER_APP_SECRET` environment variables when omitted.
   */
  constructor(config: MessengerConfig = {}) {
    this.pageAccessToken = config.pageAccessToken ?? process.env.CHANNEL_MESSENGER_PAGE_ACCESS_TOKEN
    this.appSecret = config.appSecret ?? process.env.CHANNEL_MESSENGER_APP_SECRET
    this.apiBaseUrl = (config.apiBaseUrl ?? DEFAULT_API_BASE_URL).replace(/\/+$/, '')
    this.apiVersion = config.apiVersion ?? DEFAULT_API_VERSION
    this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS
    this.defaultMessagingType = config.defaultMessagingType ?? 'RESPONSE'
  }

  /**
   * Sends an outbound message via the Messenger Send API.
   *
   * Plain text messages POST `{ message: { text } }`. Rich messages with
   * 1–3 buttons render as a `button_template`; 4+ buttons fall back to
   * `quick_replies` (which is the only Messenger affordance that scales
   * past three options on a single message). Media messages POST an
   * `attachment` whose `type` is inferred from the attachment's content
   * type or filename extension.
   *
   * @param to - Page-scoped user identifier (PSID) of the recipient.
   * @param message - Normalized {@link OutboundMessage}.
   * @returns A {@link SendResult} keyed by Messenger's `message_id`.
   * @throws {Error} If the page access token is unset or the Send API
   *   rejects the call. The token is never included in error messages.
   */
  async sendMessage(to: ChannelRecipient, message: OutboundMessage): Promise<SendResult> {
    if (!to) {
      throw new Error('Messenger recipient (PSID) is required.')
    }
    if (!this.pageAccessToken) {
      throw new Error('Messenger page access token is not configured.')
    }

    const body = buildRequestBody(to, message, this.defaultMessagingType)
    const result = await this.callSendApi(body)

    return {
      messageId: result.message_id ?? '',
      deliveredAt: new Date(),
    }
  }

  /**
   * Verifies the inbound webhook signature.
   *
   * Messenger signs each webhook request with HMAC-SHA256 of the raw body
   * keyed by the app secret, prefixed with `'sha256='` and delivered in
   * the `X-Hub-Signature-256` header. We compute the same digest and
   * compare in constant time.
   *
   * @param headers - Request headers (case-insensitive lookup).
   * @param body - Raw, unparsed request body.
   * @returns `true` when the signature is valid for the configured app
   *   secret. Returns `false` if the secret is unset, the header is
   *   missing, or the digests do not match.
   */
  verifyWebhookSignature(headers: Record<string, string>, body: string | Uint8Array): boolean {
    if (!this.appSecret) {
      return false
    }

    const provided = lookupHeader(headers, SIGNATURE_HEADER)
    if (!provided) {
      return false
    }

    const match = /^sha256=([0-9a-fA-F]+)$/.exec(provided.trim())
    if (!match) {
      return false
    }
    const providedDigest = match[1]!.toLowerCase()

    const bodyBytes = typeof body === 'string' ? Buffer.from(body, 'utf8') : Buffer.from(body)
    const computed = createHmac('sha256', this.appSecret).update(bodyBytes).digest('hex')

    return constantTimeHexEqual(computed, providedDigest)
  }

  /**
   * Normalizes a raw Messenger webhook payload into an
   * {@link InboundMessage}.
   *
   * Messenger delivers events as `entry[].messaging[]` — typically a
   * single messaging entry per webhook, but the spec allows batching.
   * This method walks the messaging array and returns the first event it
   * recognizes:
   *
   * - `message` (text or attachment) → text + attachments
   * - `message.quick_reply` → quick-reply payload as `text`
   * - `postback` → button payload as `text`
   * - `delivery` / `read` → bare receipt with `payload` preserved
   *
   * Anything else throws — callers MUST verify the signature before
   * reaching this method.
   *
   * @param payload - Raw Messenger webhook envelope (parsed JSON).
   * @returns Canonical {@link InboundMessage}. The original payload is
   *   preserved on `payload` for forensic / channel-specific handling.
   * @throws {Error} If the payload does not match a recognized variant.
   */
  parseInbound(payload: unknown): InboundMessage {
    if (!isPlainObject(payload)) {
      throw new Error('Messenger inbound payload is not an object.')
    }

    const envelope = payload as MessengerWebhookPayload
    const entry = envelope.entry?.[0]
    const messaging = entry?.messaging?.[0]
    if (!messaging) {
      throw new Error('Messenger inbound payload has no messaging entries.')
    }

    return inboundFromMessaging(messaging, payload)
  }

  /**
   * Returns the static capability matrix for the Messenger Send API.
   *
   * Reactions are technically supported on Messenger but the Bot Send API
   * surface is limited (bots cannot react to arbitrary messages); the
   * normalized contract has no `reactions` flag, so the capability is not
   * advertised. Threads are reported as `false` because Messenger has no
   * first-class thread concept on the Send API — conversations are
   * 1:1 / per-page.
   *
   * @returns The {@link ChannelFeatures} flags for Messenger.
   */
  listSupportedFeatures(): ChannelFeatures {
    return FEATURES
  }

  /**
   * Performs a single Send API call. Centralizes redaction so the page
   * access token never ends up in thrown error messages.
   *
   * @param body - JSON-serializable Send API request payload.
   * @returns The successful Send API response body.
   * @throws {Error} On non-2xx responses, error envelopes, or network
   *   failures. Errors quote a redacted method URL only.
   */
  private async callSendApi(body: unknown): Promise<MessengerSendApiResponse> {
    const url = `${this.apiBaseUrl}/${this.apiVersion}/me/messages?access_token=${this.pageAccessToken}`
    const safeUrl = `${this.apiBaseUrl}/${this.apiVersion}/me/messages?access_token=<redacted>`

    const controller = new AbortController()
    const timer = setTimeout(() => controller.abort(), this.timeoutMs)

    let response: Response
    try {
      response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
        signal: controller.signal,
      })
    } catch (error) {
      const original = error instanceof Error ? error.message : String(error)
      throw new Error(`Messenger Send API request failed (${safeUrl}): ${redact(original)}`, {
        cause: error,
      })
    } finally {
      clearTimeout(timer)
    }

    let parsed: {
      error?: { message?: string; code?: number }
      message_id?: string
      recipient_id?: string
    }
    try {
      parsed = (await response.json()) as typeof parsed
    } catch (error) {
      throw new Error(
        `Messenger Send API returned non-JSON response (${safeUrl}, HTTP ${response.status}).`,
        { cause: error },
      )
    }

    if (!response.ok || parsed.error) {
      const description = parsed.error?.message ?? `HTTP ${response.status}`
      throw new Error(`Messenger Send API call failed: ${redact(description)}`)
    }

    return parsed
  }
}

Functions

createProvider(config)

Convenience factory for the named-multi-provider bond pattern.

function createProvider(config?: MessengerConfig): MessengerChannelProvider
  • config — Optional Messenger config.

Returns: A new {@link MessengerChannelProvider} instance.

Constants

provider

Lazily-instantiated singleton instance for app-startup wiring. Reads configuration from environment variables on first use.

const provider: ChannelProvider

channelMessengerSecretDefinitions

Secret definitions required by the Messenger channel bond.

const channelMessengerSecretDefinitions: SecretDefinition[]

Core Interface

Implements @molecule/api-channel interface.

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-channel ^1.0.1
  • @molecule/api-secrets ^1.0.1

Environment Variables

  • CHANNEL_MESSENGER_PAGE_ACCESS_TOKEN (required) — Messenger page access token
  • CHANNEL_MESSENGER_APP_SECRET (required) — Meta app secret

Runtime Dependencies

  • @molecule/api-channel

  • @molecule/api-secrets

  • Webhook subscription needs a GET echo the bond does not provide. When you register the webhook URL in the Meta console, Meta first sends GET ?hub.mode=subscribe&hub.verify_token=<your token>&hub.challenge=<n>. Your route must check the verify token you chose in the console and respond 200 with the raw hub.challenge value. Only POST deliveries go through verifyWebhookSignature() / parseInbound().

  • 24-hour messaging window: outside 24h since the user's last message, the Send API rejects standard sends — Meta requires an approved message tag for out-of-window messages. Expect and surface that API error.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:

  • [ ] Each channel-notifying flow the app defines (a Slack/Discord alert on a new order, a status-change message) actually produces a message. The sandbox CAPTURES channel messages instead of sending — read them with the read_activity tool (filter type 'channel'); never mock the flow or modify production code to expose the message.
  • [ ] The captured message targets the configured channel/provider name and carries the app's real content (readable text, no undefined placeholders, no secrets).
  • [ ] A failed send (unbonded or misconfigured provider) is visible in logs/UI — never silently swallowed.