Skip to content

Releases: DojoCodingLabs/whatsapp-adapter

sdk-v0.10.0

Choose a tag to compare

@github-actions github-actions released this 09 Sep 22:43

Version note. Earlier drafts of this section were labelled
sdk-v1.1.0 on the assumption that 1.0.0 had shipped. It has
not — the stability tag is gated on a live smoke test against a
real WABA (ROADMAP.md § Q4 2026). This
release therefore stays pre-1.0 as 0.10.0; per the policy
above, a pre-1.0 minor may carry breaking changes, and this one
does (listed first).

Two batches in one release: the Phase B additions that have been
on main since May (OptInRegistry, retry telemetry,
markAsRead, media, agent-bridge) and the September 2026 deep
audit against Meta's current Cloud API documentation
(docs/_internal/2026-09-07-sdk-deep-audit.md, 25 findings, all
addressed). Ten OpenSpec changes archived under
openspec/changes/archive/2026-09-09-*.

859 SDK tests (was 603 in 0.9.0).

⚠ Breaking

  • Meta error 131053 (media upload error) now maps to
    CapabilityError, not RateLimitError, and is never retried.

    It is a permanent content failure. Consumers matching
    instanceof RateLimitError to requeue media sends must switch
    to CapabilityError.
  • Every error leaving a WhatsAppClient method is a
    WhatsAppError subclass.
    Raw TransientHttpError,
    TypeError (fetch failure) and AbortError no longer escape
    after retry exhaustion — they surface as the new
    TransientError, NetworkError and RequestAbortedError
    (original attached as cause). Any catch that special-cased
    those raw types should now use instanceof on the typed
    classes.
  • sendReaction (and reaction payloads via sendReply) are
    window-gated.
    Meta documents only approved templates as
    window-exempt. A reaction outside the 24 h window now throws
    WindowClosedError before HTTP instead of a wasted Graph
    round-trip ending in 131047.
  • buildInteractiveList enforces ≤ 10 rows in total across
    sections
    (Meta's actual cap), not 10 per section.
  • Free-form builders enforce Meta's character limits and
    throw WhatsAppError("UNKNOWN") before HTTP for over-long
    fields (text body 4096, caption 1024, interactive body 1024 /
    footer 60 / text header 60, reply-button title 20 / id 256,
    list button 20, section title 24, row title 24 / description 72
    / id 200, cta_url display text 20). Counted in Unicode code
    points. Previously these round-tripped to Meta and came back as
    100 / 131009. Limits exported as MESSAGE_LENGTH_LIMITS.
  • Input-validation errors are WhatsAppError("UNKNOWN"), never
    bare TypeError / Error,
    across uploadMedia,
    downloadMedia, markAsRead, getTemplate, sendReply
    (real and mock).
  • Template pre-flight is stricter. A HEADER of format
    IMAGE / VIDEO / DOCUMENT / LOCATION now requires exactly
    one parameter of the matching type (previously validated clean
    with zero params and failed at Meta with 132012); named
    (parameter_format: NAMED) templates are validated by
    set-equality against {{name}} placeholders; mixed
    positional + named templates are rejected.
  • Pinned Graph API version v25.0 → v26.0. Override per
    instance with graphApiVersion if you need to stay on v25.0
    (supported by Meta until 2028-07-29). v26.0 carries no
    WhatsApp Cloud API breaking changes.

Security

  • whatsapp.path OTel span attribute no longer carries the
    bearer token.
    healthCheck() hits
    /debug_token?input_token=<token>; the query string was
    recorded verbatim on the span. whatsapp.path is now the path
    only, for every request. If you export traces to a third-party
    backend, rotate the token that was in scope.

Fixed

  • Re-engagement gate is Meta code 131047, not 131026.
    131026 ("Message Undeliverable" — recipient not on WhatsApp /
    outdated client / unaccepted ToS) maps to the new
    UndeliverableError; a template send does not recover from
    it, so the MCP recovery hint no longer tells the model to retry
    that path.
  • Meta error taxonomy realigned against the Cloud API
    error-code reference (mapMetaError):
    • 4, 80007 (app- / WABA-level throttling) → retryable
      RateLimitError (were UNKNOWN, never retried).
    • 131049, 131064, 133016 → RateLimitError but not
      retried (enforcement windows are hours / days or
      per-recipient).
    • 131008, 131009, 131051, 131052, 131053 →
      CapabilityError.
    • 131050 (recipient stopped marketing) → OptOutError with
      metaCode.
    • 368, 130497, 131031 (integrity) → new
      AccountRestrictedError.
    • 0 → AuthenticationError; 3, 10, 131005 →
      PermissionError.
    • 132xxx → TemplateError now carries metaCode.
    • Exhausted 429 / throttling → RateLimitError (metaCode
      kept); exhausted 408 / 5xx → TransientError; fetch failure →
      NetworkError; non-JSON 2xx → WhatsAppError("UNKNOWN")
      without retry (double-send risk).
    • whatsapp.error.meta_code span attribute is emitted for every
      typed error that carries a metaCode.
  • Retry loop honours the caller's AbortSignal. A pre-aborted
    or mid-flight-aborted RequestOptions.signal was classified as a
    retryable failure — three retries and ~8 s of jittered backoff
    before the cancellation surfaced. Aborted signal → immediate
    RequestAbortedError, pending backoff cut short, signal.reason
    preserved as cause. Non-caller AbortErrors (fetch-internal
    timeouts) remain retryable.
  • WindowTracker.notifyInbound(from, atMs) honours the
    customer's timestamp.
    Meta retries webhooks for up to 7 days;
    a late or replayed delivery re-opened a 24 h window from receipt
    time, so the next free-form send failed at Meta with 131047.
    The tracker now stores the inbound timestamp, sets the storage
    TTL to the remaining window, ignores deliveries older than the
    window, never lets an older replay shorten a live window, and
    clamps future-dated timestamps. Legacy true entries still read
    as open for rolling upgrades. Pass e.timestamp from your
    message handler; createAgentBridge does so automatically.
  • Template button index accepts numeric whole numbers ≥ 0
    (carousel card buttons use numeric indices); malformed and
    fractional indices are rejected pre-flight.
  • Stale claims corrected in JSDoc and docs: windowTracker option
    (reactions are gated), StatusEvent.pricingCategory (it is the
    billing category, not the pricing model), messages/types.ts
    header (Graph API v26), dedupe-TTL note in webhooks.md
    (24 h, not 1 h), several broken relative links in
    docs/compliance.md.

Added — MediaExpiredError + media downloads through the transport

OpenSpec change 2026-09-09-media-transport-parity.

  • fetchExternal(client, url, options) — authenticated GET of
    Meta's media CDN sharing the Graph transport pipeline: a
    whatsapp.media.fetch OTel span (host only — never the signed
    query string), retry on 408 / 429 / 5xx honouring Retry-After,
    fetchImpl, signal, requestId, typed errors.
  • MediaExpiredError (code: "MEDIA_EXPIRED", httpStatus,
    optional mediaId) for CDN 401 / 403 / 404 / 410 — the
    ~5-minute pre-signed URL has lapsed. Not retried; never carries
    the URL. Recovery: call downloadMedia(mediaId) again.
  • DownloadedMedia.fetchBytes(options?) and
    fetchMediaUrl(client, url, options?) accept MediaFetchOptions
    (retry policy, hooks, signal, requestId, fetchImpl);
    downloadMedia(mediaId, options?) forwards policy / hooks /
    fetchImpl to fetchBytes. A bare AbortSignal is still accepted
    for 0.9.x compatibility.
  • Exported size-gate helpers payloadByteLength,
    assertSizeAllowed, validateUploadInput;
    MockWhatsAppClient.uploadMedia runs the same pre-flight as the
    real client.
  • Contract tests for uploadMedia, downloadMedia (+ span),
    markAsRead; a media parity suite (real ⇄ mock).

Added — Typed error classes

  • AccountRestrictedError (ACCOUNT_RESTRICTED) — integrity /
    policy restrictions (368, 130497, 131031). Not retryable;
    surface to a human.
  • TransientError (TRANSIENT) — retries exhausted on 408 / 5xx.
  • NetworkError (NETWORK) — fetch itself failed after retries.
  • RequestAbortedError (ABORTED) — the caller's signal fired;
    cause is signal.reason.
  • UndeliverableError (UNDELIVERABLE) — Meta 131026.
  • MediaExpiredError (MEDIA_EXPIRED) — see above.
  • TemplateError and OptOutError gain an optional metaCode.

Added — Webhook events

OpenSpec changes 2026-09-09-webhook-user-preferences-and-flows,
2026-09-09-status-event-pricing-fields.

  • UserPreferencesEvent (kind: "user_preferences") — Meta's
    authoritative marketing stop / resume signal, one event per
    entry, typed .on("user_preferences", h), deduped on
    waId + category + value + timestamp. Wire it to
    OptInRegistry.optOut / optIn scoped to MARKETING; the
    inbound STOP-keyword pattern is a fallback only.
  • IncomingMessageKind gains "interactive_nfm_reply" (WhatsApp
    Flows completion; response_json preserved in body) and
    "request_welcome" (Click-to-WhatsApp conversation opened) —
    they no longer collapse to "unsupported".
  • StatusEvent.pricingType (regular |
    free_customer_service | free_entry_point),
    StatusEvent.pricingModel (PMP | CBP) and
    StatusEvent.billable. Financially material from Oct 1,
    2026
    , when Meta bills every free-form message and every
    in-window UTILITY template per message; only the 72 h free
    entry-point window stays free. See docs/compliance.md § 2.

Added — Template pre-flight for named / media templates

OpenSpec change 2026-09-09-fix-template-preflight-validation.

  • TemplateDefinition.parameter_format (POSITIONAL | NAMED);
    parameter_name on text / currency / date_time parameters;
    TemplateParameterLocation; TemplateMediaHeaderFormat,
    TemplateParameterFormat types.
  • listTemplatePlaceholders, `hasNamedTem...
Read more

mcp-v0.5.0

Choose a tag to compare

@github-actions github-actions released this 09 Sep 22:46

Version note. Earlier drafts of this section were labelled
mcp-v1.1.0 on the assumption that 1.0.0 had shipped. It has
not — the stability tag is gated on a live smoke test against a
real WABA (ROADMAP.md § Q4 2026). This
release stays pre-1.0 as 0.5.0 and carries the breaking change
listed first.

Depends on @dojocoding/whatsapp-sdk@0.10.0. The server now
registers 19 tools (was 16), 2 resources, 1 prompt.
201 MCP tests (was 152 in 0.4.0).

⚠ Breaking

  • whatsapp_upload_media_from_url refuses non-public
    sourceUrl values.
    The server fetches whatever URL the model
    supplies, which made the tool a server-side-request-forgery
    vector (z.string().url() accepted
    http://169.254.169.254/…). It now rejects — with
    isError: true, error.code: "source_url_rejected", and before
    any network I/O — anything that is not https://, embeds
    credentials, targets localhost / *.local, or targets a
    literal loopback / unspecified / link-local / RFC 1918 /
    100.64/10 IPv4 address (incl. IPv4-mapped IPv6) or an IPv6
    loopback / link-local / ULA. Redirects are not followed
    (redirect: "error"). Move http:// sources to https://.
  • whatsapp_send_reaction is window-gated. Meta exempts only
    approved templates; a reaction outside the 24 h window returns
    WINDOW_CLOSED with the template recovery hint.
  • Inherits the SDK's 0.10.0 breaking changes where they reach
    the tool surface: 131053 is now CAPABILITY (not
    RATE_LIMIT); over-long text / captions / button titles are
    rejected as UNKNOWN before HTTP with the field and limit
    named; template pre-flight is stricter for media headers and
    named parameters.

Added — Three tools: whatsapp_mark_as_read, whatsapp_upload_media_from_url, whatsapp_get_media_info

OpenSpec change 2026-09-09-mcp-media-ack-tools-surface (the
tools landed on main in May without a spec delta; this change
brings the spec, docs and tests up to date).

  • whatsapp_mark_as_read { messageId, typing? } →
    { success, messageId, typing }. Acks an inbound wamid (blue
    double-tick) and optionally shows a typing indicator while the
    agent composes a reply (auto-dismisses on send or after ~25 s).
    Window-independent; idempotentHint: true. Free under
    per-message pricing — the right substitute for a "one moment…"
    filler message.
  • whatsapp_upload_media_from_url { sourceUrl, mimeType, filename? } → { mediaId, mimeType, bytes }. Server-side
    fetch + upload so the model never handles bytes. Source-URL
    guard as above; non-2xx / rejected fetch →
    source_fetch_failed; oversize → SDK CAPABILITY.
  • whatsapp_get_media_info { mediaId } →
    { id, mimeType, sha256, fileSize }. Metadata only — never the
    bearer-authenticated URL or the bytes. readOnlyHint,
    idempotentHint.
  • MARK_AS_READ_TOOL, UPLOAD_MEDIA_FROM_URL_TOOL,
    GET_MEDIA_INFO_TOOL name constants exported.
  • test/contract/media-ack-tools.test.ts (+32) covers all
    three, including the SSRF guard table.

Added — Recovery hints for the SDK's new error classes

mapSdkError / recoveryHint now cover UNDELIVERABLE
(131026 — a template will not fix it; stop retrying),
ACCOUNT_RESTRICTED (surface to a human), TRANSIENT /
NETWORK (retry later), ABORTED, MEDIA_EXPIRED (call
whatsapp_get_media_info again), rate-limit codes 131049 /
131064 (per-recipient / spam-rate — do not retry soon), and
metaCode detail on TEMPLATE / OPT_OUT. See
docs/mcp/error-recovery.md.

Dependencies

  • @modelcontextprotocol/sdk ^1.29.0 → ^1.30.0. Clears the
    four high transitive advisories pnpm audit --prod reported
    (hono CORS reflection GHSA-88fw-hqm2-52qc, fast-uri ×3,
    ip-address). Remaining moderates sit in express>qs and
    @hono/node-server inside the MCP SDK's transports this package
    does not use.

Fixed

  • Re-engagement gate is 131047 (was documented as 131026).
  • docs/mcp/tools.md documents all 19 tools; every "16 tools"
    count corrected across docs and source comments.

Added — Streamable HTTP handler (createWhatsAppHttpHandler)

OpenSpec change 2026-05-12-mcp-streamable-http-transport.

New top-level export. Fetch-API native MCP Streamable HTTP
handler (spec revision 2025-06-18) runnable on Cloudflare
Workers, Vercel Functions (Node + Edge), AWS Lambda, Hono,
Next.js App Router, Bun, Deno, plain Node 18+. No Node-API-only
dependencies.

import { WhatsAppClient } from "@dojocoding/whatsapp-sdk";
import { createWhatsAppHttpHandler } from "@dojocoding/whatsapp-mcp";

const handler = createWhatsAppHttpHandler({
  client: new WhatsAppClient({ ... }),
  wabaPhoneNumberId: "...",
  staticToken: process.env.MCP_GATEWAY_TOKEN!,
});

// Next.js App Router:
export { handler as GET, handler as POST, handler as DELETE };

Returns (req: Request) => Promise<Response>. Safe to
construct at module scope; safe to reuse across concurrent
requests.

Same 16 tools / 2 resources / 1 prompt as the stdio bin and
the embedded toolset. Surface parity drift-detected at CI.

Added — Built-in bearer authentication

Two complementary modes on CreateWhatsAppHttpHandlerInput:

  • staticToken: string — shared-secret constant-time
    compare against Authorization: Bearer <token>. Right for
    closed-network deployments and defence-in-depth behind an
    outer OAuth gateway.
  • verifyToken: (token, req) => Promise<AuthInfo | null>
    — consumer-supplied verifier callback. Returns the MCP-SDK
    AuthInfo shape on success (passed verbatim to message
    handlers); null to reject with 401.

Both optional. Pass neither to delegate authentication to the
outer gateway. When both are set, verifyToken takes
precedence.

The MCP SDK's AuthInfo type is re-exported from the package
root so consumers writing a verifier don't need to import deep
paths.

Added — 401 response shape

All authentication failures (missing header, bad static token,
verifier returned null) return:

{ "jsonrpc": "2.0", "id": null, "error": { "code": -32001, "message": "Unauthorized" } }

with Content-Type: application/json. The body does NOT vary
across failure paths — clients cannot infer the cause from
the body alone, and the rejected token is never echoed.

Added — Stateless / stateful modes

  • Stateless (default): per-request server + transport.
    Right for serverless deployments where state cannot survive
    between invocations. ~1-2 ms per-request setup cost.
  • Stateful (stateless: false): shared server + transport
    at the factory level. The MCP SDK tracks sessions in-memory.
    Right for long-lived Node / Bun / Deno servers; not safe
    across multi-node deployments without an external session
    store.

Tests (+17 new)

  • test/contract/http-handler-auth.test.ts — 10 tests:
    pass-through with no auth set, static-token match,
    case-insensitive Bearer scheme prefix per RFC 6750,
    missing header → 401, wrong token → 401, verifyToken
    null → 401, verifyToken AuthInfo → passes through,
    precedence (verifyToken wins), the 401 body does NOT
    echo the rejected token.
  • test/contract/http-handler-roundtrip.test.ts — 7 tests
    driving an MCP Client against the handler via an
    in-process fetch proxy. Initialize handshake;
    tools/list returns the 16 expected tools;
    tools/call whatsapp_send_text round-trips with
    structuredContent.messageId; resources/list returns
    the 2 expected URIs; resources/read whatsapp://templates
    returns the cached body; prompts/list returns
    wa-template-send; prompts/get renders the guided
    messages.

152 in-process MCP tests (was 134 in 0.4.0).

Bundle

Library ESM 10.62 KB brotlied (was 9.93 KB; +700 B for the
HTTP handler + WebStandardStreamableHTTPServerTransport
wiring). CLI 9.26 KB brotlied (was 9.25 KB; unchanged).
Both well under their 200 KB / 300 KB budgets.

Docs

  • docs/mcp/http.md — handler reference (API, when-to-use
    matrix vs stdio + embedded, auth modes, 401 shape,
    runtime portability table).
  • docs/cookbook/mcp/streamable-http-vercel.md — end-to-end
    Vercel + JWT recipe with a jose-based verifier callback.
  • docs/mcp/README.md index gains the new page.
  • docs/mcp/transports.md § "Streamable HTTP" replaces the
    former "v2 (planned)" section.

The Streamable HTTP handler itself is non-breaking: the stdio
bin, the programmatic WhatsAppMcpServer class, and the embedded
toolset gain the new tools but keep their existing contracts.

sdk-v0.9.0

Choose a tag to compare

@github-actions github-actions released this 12 May 21:24

V1 runway — the final 0.x minor bundles three changes Site2Print
identified as Phase A blockers in the integration audit. All three
land together so consumers see one upgrade.

OpenSpec changes:

  • 2026-05-12-web-adapter-waituntil
  • 2026-05-12-webhook-ctwa-clid
  • 2026-05-12-rename-idempotency-to-request-id

Added — waitUntil on the web adapter

CreateWhatsAppHandlerOptions.waitUntil?: (p: Promise<unknown>) => void
on the Fetch-API adapter at @dojocoding/whatsapp-sdk/web. Wires the
SDK's async dispatch promise into runtime lifecycle-extension APIs
(Vercel Functions @vercel/functions.waitUntil, Cloudflare Workers
ctx.waitUntil) so handlers actually run on serverless / edge
runtimes.

Without this, the fire-and-forget dispatch promise was silently
dropped after the response on Vercel + Workers — handlers never
finished, DB writes never landed, OTel spans never flushed. Now:

// Vercel Functions:
import { waitUntil } from "@vercel/functions";
const handler = createWhatsAppHandler(receiver, { waitUntil });

// Cloudflare Workers:
const handler = createWhatsAppHandler(receiver, {
  waitUntil: ctx.waitUntil.bind(ctx),
});

When omitted, behaviour is unchanged (fire-and-forget). The adapter
chains .catch(onUnhandledHandlerError) BEFORE passing the promise
to waitUntil, so the runtime never sees an unhandled rejection.

5 new contract tests in
packages/whatsapp-sdk/test/contract/adapters/web/wait-until.test.ts.

Added — MessageEvent.referral for CTWA attribution

When Meta attaches a referral object to the first inbound message
after a Click-to-WhatsApp ad click, the SDK's parser now surfaces it
on the parsed MessageEvent:

import type { MessageEvent, WhatsAppReferral } from "@dojocoding/whatsapp-sdk";

receiver.on("message", (e: MessageEvent) => {
  if (e.referral?.ctwa_clid) {
    // forward to Meta CAPI for ad attribution
  }
});

The new WhatsAppReferral type names the documented core fields
(ctwa_clid, source_url, source_type, source_id, headline,
body, media_type, media_url, thumbnail_url,
welcome_message). The runtime type is a permissive intersection
with Record<string, unknown> — unknown future fields Meta adds
are preserved without an SDK release.

Attribution semantics:

  • Only the first message after a click carries referral.
    Subsequent messages don't; cache ctwa_clid keyed on from.
  • Empty referral: {} is preserved (distinguishes "no referral"
    from "referral present but Meta omitted details").
  • The parser does NOT throw on a non-object / null referral.

6 new tests + 2 fixtures
(message-with-ctwa-referral.json, message-with-empty-referral.json).

BREAKING (pre-1.0 minor) — idempotencyKey → requestId rename

The misleading "idempotency" naming on the request-correlation
surface is renamed:

  • RequestOptions.idempotencyKey → RequestOptions.requestId
  • Outbound header X-Dojo-Idempotency-Key → X-Request-Id
  • OTel span attribute whatsapp.idempotency_key → whatsapp.request.id

Behaviour is unchanged — the SDK still generates a UUID v4 per
logical call when requestId is omitted, still reuses it across
retry attempts, still attaches the header to every outbound
request. Only the naming changes.

Why: Meta does NOT consult any SDK-attached header for outbound
deduplication. The X-Dojo-Idempotency-Key naming created a
false-positive feeling for consumers who assumed retries were
deduplicated server-side. The v0.x → v1.0 window is the right
moment to clean this up. Real outbound dedup is post-1.0 (the
outbound-deduper capability on the v2 roadmap).

Migration: mechanical search-and-replace. See
MIGRATION.md § "SDK: 0.8.x → 1.0.0".

- await client.sendText({ to, body }, { idempotencyKey: "booking-123" });
+ await client.sendText({ to, body }, { requestId: "booking-123" });

Downstream consumers reading req.idempotencyKey in custom retry
hooks, or asserting the legacy header / span attribute in tests,
break at compile or assertion time. Renames in one shot.

Docs

  • docs/sdk/web.md § "Cloudflare Workers" and "Next.js App Router
    (Vercel)" updated with the waitUntil wiring. Long-lived vs
    serverless / edge threading model called out explicitly.
  • docs/sdk/webhooks.md § "Click-to-WhatsApp (CTWA) referral" — new
    subsection with the Meta CAPI handoff snippet + attribution caveats.
  • docs/sdk/patterns.md § 7 renamed and rewritten: "Request
    correlation with requestId" (was "Replay-safe sends with
    idempotencyKey"). Honest framing — correlation, not dedup.
  • docs/architecture.md § "Idempotency hint" replaced with
    "Request correlation" naming the new header + attribute and
    pointing at the v2 outbound-deduper for real dedup.
  • docs/compliance.md § 3.4 updated with the rename and the v2
    outbound-deduper roadmap note.
  • MIGRATION.md § "SDK: 0.8.x → 1.0.0" gains the requestId rename
    diff alongside the existing setRedactSalt deprecation entry.

Tests

  • 5 new web-adapter waitUntil contract tests.
  • 6 new CTWA / referral parser tests.
  • 1 new test asserting the legacy X-Dojo-Idempotency-Key header
    is NOT emitted (alongside the renamed-name tests).

603 SDK tests (was 591). Coverage 97.38 / 88.88 / 99.15 / 97.38 —
well above the 90/85/90/90 gate.

mcp-v0.4.0

Choose a tag to compare

@github-actions github-actions released this 12 May 19:03

OpenSpec change 2026-05-12-mcp-embedded-toolset. The Site2Print
v1 integration audit identified this as the single biggest unlock
for downstream gateway integrations — more important than the
planned Streamable HTTP transport, because it sidesteps the
transport question entirely.

Added — createWhatsAppToolset (embedded toolset)

New top-level factory exported from @dojocoding/whatsapp-mcp:

import { WhatsAppClient } from "@dojocoding/whatsapp-sdk";
import { createWhatsAppToolset } from "@dojocoding/whatsapp-mcp";

const toolset = createWhatsAppToolset({
  client: new WhatsAppClient({ ... }),
  wabaPhoneNumberId: "PNID",
});

// In your gateway's tools/list:
const tools = [...toolset.tools, ...otherUpstreamTools];

// In your gateway's tools/call:
const result = await toolset.dispatch(name, args);

Returns a WhatsAppToolset with:

  • tools: ReadonlyArray<ToolDefinition> — 16 entries in stable order.
  • resources: ReadonlyArray<ResourceDefinition> — 2 entries.
  • prompts: ReadonlyArray<PromptDefinition> — 1 entry.
  • dispatch(name, args, ctx?): Promise<CallToolResult> — schema-validates and routes.
  • readResource(uri): Promise<ReadResourceResult> — whatsapp://templates + whatsapp://window/<phone>.
  • renderPrompt(name, args?): Promise<GetPromptResult> — wa-template-send.

Same 16 tools, 2 resources, 1 prompt as WhatsAppMcpServer.
Same schemas. Same error mapping. Same recovery-hint text.
Surface parity is enforced by a new drift-detector contract
test on every PR; renaming a tool in one path without the other
fails CI immediately.

Added — MCP-spec-shaped types re-exported

The shared per-tool contract types now ship from
@dojocoding/whatsapp-mcp for downstream gateway
implementations:

  • ToolDefinition / ResourceDefinition / PromptDefinition
  • CallToolResult / ReadResourceResult / GetPromptResult
  • ResourceContent / ToolAnnotations
  • DispatchContext / ZodShape

These are MCP-spec-aligned types — pass-through compatible with
the MCP SDK's internal shapes.

Internal refactor (zero behavioural change)

Each tool / resource / prompt file under
packages/whatsapp-mcp/src/{tools,resources,prompts}/*.ts now
exports a <name>Definition constant and a handle<Name>
function alongside the existing register<Name> glue. Both
the stdio server (WhatsAppMcpServer.connect(transport)) and
the new toolset consume the same per-tool pair. The
register<Name> functions are now thin shims over a shared
registerToolOnServer(server, definition, handler) bridge.

The change is invisible at the public surface; existing
consumers (Claude Desktop spawning the stdio bin, tests using
InMemoryTransport) see identical behaviour.

Tests

  • test/contract/embedded-toolset-parity.test.ts — drift
    detector. Asserts the toolset and the stdio server expose
    identical tool names, resource URIs, prompt names, and
    JSON-Schema serialisations of every inputSchema. New
    capability addition that doesn't mirror in both paths fails
    immediately.
  • test/contract/toolset-dispatch.test.ts — 17 tests covering
    happy paths (text / template / reaction / list-templates),
    error paths (unknown_tool, invalid_args for wrong type +
    missing required, WINDOW_CLOSED from a typed SDK error),
    surface invariants (no credential fields in any
    inputSchema, stable orderings), resource reads
    (whatsapp://templates, whatsapp://window/<phone>,
    unknown URI fallback), prompt renders.

134 in-process MCP tests (was 112). E2E suite (8 skipped)
unchanged.

Docs

  • docs/mcp/embedded.md — toolset reference page (API,
    stability, when not to use, surface parity guarantee).
  • docs/cookbook/mcp/embedded-toolset.md — end-to-end recipe
    showing the toolset inside a Next.js App Router MCP gateway
    with prefix-based routing alongside another upstream (the
    Site2Print + Alegra shape).
  • docs/mcp/README.md index updated to surface the new page.
  • MIGRATION.md § "MCP server: 0.3.x → 1.0.0" gains a
    "What's new in 0.4.0" subsection.

Coverage

  • Statements 98.76 (was 98.6)
  • Branches 76.86 (was 72.0)
  • Functions 98.41 (was 100; new toolset.ts line 273
    optional-abortSignal-spread branch is unreached by tests)
  • Lines 98.76 (was 98.6)

Comfortably above the 95/65/95/95 gate.

Bundle

Library ESM 9.93 KB brotlied (was 8.54 KB; +1.4 KB for toolset

  • types + register bridge). CLI 9.25 KB brotlied (was 8.78 KB).
    Both well under their 200 KB / 300 KB budgets.

v0.7.4

Choose a tag to compare

@github-actions github-actions released this 11 May 04:34

Tooling (no SDK behaviour change)

Phase 4 of the Track F + test-coverage audit hardening pass. The
published artefact is functionally identical to 0.7.3; only the
build / CI tooling and CHANGELOG differ.

  • Bundle-size budgets (F6). size-limit is wired into CI with
    per-entry-point budgets defined in package.json:

    Entry point Budget
    @dojocoding/whatsapp (root, ESM/CJS) 100 KB
    @dojocoding/whatsapp/express 6 KB
    @dojocoding/whatsapp/web 3 KB
    @dojocoding/whatsapp/hono 3 KB
    @dojocoding/whatsapp/storage/redis 2 KB
    @dojocoding/whatsapp/storage/postgres 4 KB

    Limits are roughly 1.5× the current measured sizes, so a single PR
    cannot double a bundle by accident (e.g. an accidental
    import "lodash"). Run pnpm size locally to see the same
    budget report CI runs.

v0.7.3

Choose a tag to compare

@github-actions github-actions released this 11 May 04:25

Tests (no SDK behaviour change)

Phase 3 of the Track F + test-coverage audit hardening pass.
572 tests pass (was 524 — +48 across the seven audit items below).
The published artefact is functionally identical to 0.7.2; only the
test suite and CHANGELOG differ.

  • Property-based assertions with fast-check for three
    high-leverage modules:
    • webhooks/signature.ts — HMAC verifier invariants across
      random body / secret / header inputs.
    • webhooks/dedupe.ts — dedupe-key identity + Unicode handling.
    • client/retry.ts — fullJitterDelay math bounded by
      [floorMs, min(maxDelayMs, expCap)] for any RNG output.
  • Concurrent dedupe race test — 100 parallel handlePayload
    calls with the same wamid invoke the handler exactly once;
    validates the single-flight semantics under Meta's aggressive
    retry pattern.
  • Storage failure propagation — explicit assertions that
    WebhookDeduper, WindowTracker, and WebhookReceiver
    surface storage errors rather than swallowing them silently.
  • sendReply template-path coverage — the window-exempt path
    for template + reaction payloads through sendReply, plus the
    window-gated path for free-form payloads, all asserted against
    captured wire bodies.
  • Local pack-contents smoke — test/contract/pack-contents.test.ts
    mirrors the CI "Verify pack contents (dry-run)" assertion so
    pnpm test catches a files allowlist regression before CI.

v0.7.2

Choose a tag to compare

@github-actions github-actions released this 11 May 04:12

Changed (CI / repo hygiene only — no SDK behaviour change)

  • Dependabot now opens grouped weekly PRs for npm + GitHub Actions
    updates. @types/*, lint/format tooling, vitest/msw, and the
    @opentelemetry/* family ship as batched sweeps. Major-version
    bumps to peer-dep ecosystems (express, hono, ioredis, pg) are
    ignored — those need an explicit decision about widening the
    supported range.
  • CodeQL static-analysis workflow added (.github/workflows/codeql.yml)
    with the security-extended query suite. Runs on push, PR, and
    a weekly Monday schedule. Findings appear in the Security tab.
  • pnpm audit --prod --audit-level=moderate runs on every CI build
    (continue-on-error initially, so advisories surface in the run
    log without blocking PRs).
  • PR template + three issue templates (bug, feature, compliance
    drift) shipped. .github/CODEOWNERS pins compliance-relevant
    files and the release pipeline.

No npm-published artefact changes from 0.7.1.

v0.7.1

Choose a tag to compare

@github-actions github-actions released this 11 May 04:04

Added

  • verifySignatureOrThrow(input) — throwing variant of
    verifySignature exported from the root entry. Resolves silently
    on a valid signature; throws WebhookSignatureError on bad HMAC,
    missing header, malformed hex, or wrong byte length. Use this when
    wiring your own HTTP layer (not the SDK's Express / web / Hono
    adapters) and you want a typed error rather than a boolean.

Changed

  • CI: bumped GitHub Actions to Node 24-compatible versions —
    actions/checkout@v5, actions/setup-node@v6,
    pnpm/action-setup@v6, actions/upload-artifact@v7,
    softprops/action-gh-release@v3. Removes the deprecation banner
    on every CI run; ready for Meta's 2026-06-02 Node 20 default
    removal.

Tests

  • Added test/contract/public-surface.test.ts — a drift detector
    asserting every documented value/class/factory across the root
    entry and all five subpaths (/express, /web, /hono,
    /storage/redis, /storage/postgres) is reachable at runtime.
    If a sub-module export is added without being plumbed through, or
    a documented export is accidentally renamed/removed, this test
    fails before consumers do.
  • Added negative-path coverage for WebhookSignatureError (5 new
    tests via verifySignatureOrThrow) and MockModeError (4 new
    contract tests pinning the public shape).
  • Re-shimmed test/integration/express/middleware.test.ts to use a
    Promise-resolved-by-handler pattern instead of 5 ms setTimeout
    waits and wall-clock ack-timing windows — mirrors the
    determinism fix already applied to the web adapter test in 0.2.0.

524 tests pass (was 447).

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 11 May 01:00

Added

  • Authentication template (OTP) builder. buildAuthTemplate({ to, name, language, otp, otpButtonIndex? }) and the matching
    client.sendAuthTemplate(...) produce the documented copy-code /
    one-tap / zero-tap wire payload with the OTP code duplicated into
    both the body and URL-button parameters (the canonical footgun
    this builder exists to remove). OTP length validated against
    Meta's 15-char ceiling.
  • Voice-note builder. buildVoice({ to, id|link }) and
    client.sendVoice(...) produce audio messages with voice: true,
    triggering transcription support, auto-download, and the "played"
    delivery status.
  • Carousel-template builder. buildCarouselTemplate({ to, name, language, bodyParameters?, cards }) and
    client.sendCarouselTemplate(...) produce media-card carousel
    template sends with 1–10 cards. Each card's card_index is
    computed from iteration order; consumers can't misorder it.
  • Limited-time-offer template support. Three new
    TemplateParameter union variants:
    TemplateParameterLimitedTimeOffer,
    TemplateParameterCouponCode, and TemplateParameterPayload.
    TemplateComponent.type widens to accept "carousel" and
    "limited_time_offer". Use via the existing buildTemplate(...)
    /
    client.sendTemplate(...).
  • New CarouselCardComponent exported type. AudioMessage.audio
    gains an optional voice?: boolean field.

Every wire shape is grounded in a Meta doc URL referenced in
source-file JSDoc and pinned via byte-for-byte snapshot tests:

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 11 May 00:24

Added

  • Redis and Postgres Storage adapters at subpath exports.
    • @dojocoding/whatsapp/storage/redis exports
      createRedisStorage(client, options?). Takes an
      ioredis-compatible client; uses native SET PX / SET NX
      for TTL and atomicity. ioredis is an optional peer
      dependency on ^5.0.0.
    • @dojocoding/whatsapp/storage/postgres exports
      createPostgresStorage(client, options?) and
      POSTGRES_STORAGE_SCHEMA: string. Takes a pg-compatible
      client; runs four SQL statements (SELECT, two INSERT ... ON CONFLICT, DELETE). pg is an optional peer dependency on
      ^8.0.0.
    • Both adapters use minimal structural interfaces (RedisLike,
      PgLike) so the SDK doesn't import either library at runtime;
      consumers pass any compatible client (including test fakes).
    • See docs/storage.md.
  • Shared Storage contract test:
    test/unit/storage/contract.ts exports a parametrised suite
    that every implementation (InMemoryStorage,
    createRedisStorage, createPostgresStorage) runs against —
    drift between implementations is impossible-to-not-notice.