@dojocoding/whatsapp-sdk 0.8.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.
package/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # `@dojocoding/whatsapp-sdk`
2
+
3
+ > **Sibling package:** [`@dojocoding/whatsapp-mcp`](../whatsapp-mcp/README.md) — use this SDK directly when you're building a server (webhook receiver, multi-tenant API, queue worker). Use the sibling MCP server when you're wiring an LLM agent (Claude Desktop, Claude Agent SDK) to send WhatsApp messages.
4
+ >
5
+ > See [`docs/when-to-use-which.md`](../../docs/when-to-use-which.md) (coming in Phase C3) for the decision tree, and [`docs/cookbook/hybrid/`](../../docs/cookbook/hybrid/) for recipes combining the two.
6
+ >
7
+ > **Renamed from `@dojocoding/whatsapp` in `0.8.0`.** See the CHANGELOG `[0.8.0]` entry for the one-line migration.
8
+
9
+ A typed TypeScript SDK for Meta's
10
+ [WhatsApp Cloud API](https://developers.facebook.com/docs/whatsapp/cloud-api).
11
+ Modular, spec-driven via
12
+ [OpenSpec](https://github.com/openspec-dev/openspec), opinionated for
13
+ agentic shapes: LLM orchestrators, multi-turn bots, slot-collection
14
+ flows, transactional notification pipelines, multi-tenant deployments,
15
+ and MCP-backed Claude tools.
16
+
17
+ > **Status:** pre-alpha — public API stable enough for production use,
18
+ > but minor breaking changes can still land between OpenSpec archives.
19
+ > Check `openspec/changes/` before pinning a SHA.
20
+
21
+ ## What this is
22
+
23
+ Eight composable capability slices that together cover the Cloud API
24
+ client + webhook receiver surface:
25
+
26
+ - **Outbound** — typed builders for every send-able WhatsApp message
27
+ (text, media, location, contacts, interactive button/list/cta_url,
28
+ template, reaction, reply); retry on 5xx + Meta rate-limit codes;
29
+ client-side idempotency keying.
30
+ - **Inbound** — verify-token handshake, raw-body HMAC-SHA256
31
+ verification, polymorphic event parsing, dedupe by `wamid`,
32
+ framework-agnostic dispatch.
33
+ - **24-hour window enforcement** — `WindowTracker` with pluggable
34
+ `Storage` so free-form sends throw `WindowClosedError` _before_ the
35
+ HTTP call.
36
+ - **Template management** — list / get approved templates,
37
+ cross-validate `{{N}}` placeholders before send.
38
+ - **Mock mode** — `MockWhatsAppClient` parity-tested with the real
39
+ client; no Meta credentials needed for CI / dev.
40
+ - **Observability** — OpenTelemetry spans on every Graph call and every
41
+ webhook handler invocation; PII-redacting salted hash for
42
+ `phone_number_id`.
43
+ - **Framework adapter** — Express middleware sub-module
44
+ (`@dojocoding/whatsapp-sdk/express`) that handles raw-body capture,
45
+ ack-within-30s, and method routing.
46
+
47
+ ## Why this exists
48
+
49
+ The Meta-published Node SDK
50
+ ([`WhatsApp/WhatsApp-Nodejs-SDK`](https://github.com/WhatsApp/WhatsApp-Nodejs-SDK))
51
+ was archived in June 2023. The most popular community alternative
52
+ ([`tawn33y/whatsapp-cloud-api`](https://github.com/tawn33y/whatsapp-cloud-api))
53
+ was archived in July 2024. This SDK fills that gap and is designed
54
+ around the patterns LLM-driven agents need: typed errors agents can
55
+ branch on by `instanceof`, mock-mode parity for deterministic tests,
56
+ OTel spans on every Graph call, and OpenSpec-grounded contracts that
57
+ survive agent-generated code review. See
58
+ [`docs/compatibility.md`](./docs/compatibility.md) for the comparison
59
+ vs the actively-maintained leader,
60
+ [`Secreto31126/whatsapp-api-js`](https://github.com/Secreto31126/whatsapp-api-js).
61
+
62
+ ## Useful for
63
+
64
+ - **Agentic front desks** — bot handles tier-1, human escalates cleanly
65
+ through a HITL inbox.
66
+ - **LLM-driven multi-turn bots** with conversation state on the side.
67
+ - **Slot-collection flows** — booking, lead qualification, surveys,
68
+ intake forms.
69
+ - **Transactional notification pipelines** — Stripe / Shopify /
70
+ calendar / internal job → utility template.
71
+ - **OTP / authentication-template senders** — strict-format outbound
72
+ with retry semantics.
73
+ - **Multi-tenant SaaS / agency / BSP platforms** — one process,
74
+ many WABAs, by construction.
75
+ - **MCP servers** exposing send / template tools to Claude or other
76
+ agents.
77
+ - **Any shape where a typed, spec-grounded Cloud API client matters**
78
+ more than a quick demo.
79
+
80
+ See [`docs/cookbook/`](./docs/cookbook/) for runnable shapes and
81
+ [`docs/patterns.md`](./docs/patterns.md) for the composable building
82
+ blocks.
83
+
84
+ ## Install
85
+
86
+ ```bash
87
+ pnpm add @dojocoding/whatsapp-sdk
88
+ # Optional: OpenTelemetry peer dependency for spans
89
+ pnpm add @opentelemetry/api
90
+ ```
91
+
92
+ Requires Node ≥ 20 LTS. Ships dual ESM + CJS via `tsup`.
93
+
94
+ ## Quickstart
95
+
96
+ Send a message:
97
+
98
+ ```ts
99
+ import { WhatsAppClient } from "@dojocoding/whatsapp-sdk";
100
+
101
+ const client = new WhatsAppClient({
102
+ phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID!,
103
+ wabaId: process.env.WHATSAPP_WABA_ID!,
104
+ token: process.env.WHATSAPP_TOKEN!,
105
+ appSecret: process.env.WHATSAPP_APP_SECRET!,
106
+ });
107
+
108
+ await client.sendText({ to: "521234567890", body: "Hi 👋" });
109
+ ```
110
+
111
+ Receive webhooks (Express):
112
+
113
+ ```ts
114
+ import express from "express";
115
+ import { WebhookReceiver } from "@dojocoding/whatsapp-sdk";
116
+ import { createWhatsAppMiddleware } from "@dojocoding/whatsapp-sdk/express";
117
+
118
+ const receiver = new WebhookReceiver({
119
+ appSecret: process.env.WHATSAPP_APP_SECRET!,
120
+ verifyToken: process.env.WHATSAPP_VERIFY_TOKEN!,
121
+ });
122
+
123
+ receiver.on("message", async (e) => console.log("msg from", e.from));
124
+
125
+ const app = express();
126
+ app.use("/webhooks/whatsapp", createWhatsAppMiddleware(receiver));
127
+ // ^ register BEFORE any global express.json()
128
+ app.listen(3000);
129
+ ```
130
+
131
+ Or use the Fetch-API handler for Cloudflare Workers, Bun, Deno, Hono,
132
+ Next.js App Router, or any WinterCG runtime:
133
+
134
+ ```ts
135
+ import { WebhookReceiver } from "@dojocoding/whatsapp-sdk";
136
+ import { createWhatsAppHandler } from "@dojocoding/whatsapp-sdk/web";
137
+
138
+ const receiver = new WebhookReceiver({ appSecret, verifyToken });
139
+ const handler = createWhatsAppHandler(receiver);
140
+ // handler: (req: Request) => Promise<Response>
141
+ ```
142
+
143
+ See [`docs/web.md`](./docs/web.md) and
144
+ [`docs/cookbook/cloudflare-workers.md`](./docs/cookbook/cloudflare-workers.md).
145
+
146
+ The full walkthrough — including window-tracker wiring, mock mode, and
147
+ OTel — lives at [`docs/quickstart.md`](./docs/quickstart.md).
148
+
149
+ ## Building real things
150
+
151
+ Beyond the quickstart, two doc trees cover usage:
152
+
153
+ - **[`docs/cookbook/`](./docs/cookbook/)** — runnable recipes for the
154
+ common shapes: inbound auto-responder, transactional notifications,
155
+ two-way support with HITL handoff, appointment booking, multi-tenant
156
+ deployment.
157
+ - **[`docs/patterns.md`](./docs/patterns.md)** — composable patterns
158
+ the recipes are built from: window-aware send, idempotent handler,
159
+ escalation, token rotation, rate-limit-aware queue, replay-safe
160
+ sends, test layering.
161
+
162
+ For AI agents (Claude Code / Claude API / similar) generating code
163
+ that uses this SDK, the operating context lives in
164
+ **[`AGENTS.md`](./AGENTS.md)** and **[`CLAUDE.md`](./CLAUDE.md)**.
165
+
166
+ ## Capabilities
167
+
168
+ | Capability | Doc | Spec |
169
+ | ------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------- |
170
+ | Cloud API client | [`docs/client.md`](./docs/client.md) | [`openspec/specs/cloud-api-client/spec.md`](./openspec/specs/cloud-api-client/spec.md) |
171
+ | Message builders | [`docs/messages.md`](./docs/messages.md) | [`openspec/specs/message-builders/spec.md`](./openspec/specs/message-builders/spec.md) |
172
+ | Webhook receiver | [`docs/webhooks.md`](./docs/webhooks.md) | [`openspec/specs/webhook-receiver/spec.md`](./openspec/specs/webhook-receiver/spec.md) |
173
+ | 24h window tracker | [`docs/window.md`](./docs/window.md) | [`openspec/specs/window-tracker/spec.md`](./openspec/specs/window-tracker/spec.md) |
174
+ | Template management | [`docs/templates.md`](./docs/templates.md) | [`openspec/specs/template-management/spec.md`](./openspec/specs/template-management/spec.md) |
175
+ | Mock mode | [`docs/mock.md`](./docs/mock.md) | [`openspec/specs/mock-mode/spec.md`](./openspec/specs/mock-mode/spec.md) |
176
+ | Observability | [`docs/observability.md`](./docs/observability.md) | [`openspec/specs/observability/spec.md`](./openspec/specs/observability/spec.md) |
177
+ | Express adapter | [`docs/express.md`](./docs/express.md) | [`openspec/specs/framework-adapters/spec.md`](./openspec/specs/framework-adapters/spec.md) |
178
+
179
+ The architecture diagram and capability map are at
180
+ [`docs/architecture.md`](./docs/architecture.md).
181
+
182
+ ## Environment
183
+
184
+ The SDK doesn't auto-load `.env` — that's your app's job. Required env
185
+ vars (used by the docs and tests):
186
+
187
+ - `WHATSAPP_PHONE_NUMBER_ID`
188
+ - `WHATSAPP_WABA_ID`
189
+ - `WHATSAPP_TOKEN`
190
+ - `WHATSAPP_APP_SECRET`
191
+ - `WHATSAPP_VERIFY_TOKEN` (your choice; shared with Meta's webhook UI)
192
+
193
+ Optional:
194
+
195
+ - `WHATSAPP_MODE=mock` — switches `pickWhatsAppClient` to the mock
196
+ - `WHATSAPP_E2E=1` — unskips the nightly real-Meta integration tests
197
+
198
+ Copy [`.env.example`](./.env.example) to start.
199
+
200
+ ## Compliance highlights
201
+
202
+ The SDK enforces these Meta rules in code:
203
+
204
+ - **Raw-body HMAC-SHA256** with timing-safe compare on every webhook.
205
+ - **30-second ack** to Meta — handlers run async on the dispatch promise.
206
+ - **Dedupe by `wamid`** to absorb Meta's up-to-7-day delivery retries.
207
+ - **24-hour customer-service window** with pre-flight client-side gate.
208
+ - **1-indexed contiguous `{{N}}` placeholders** in templates.
209
+ - **PII redaction** on observability spans (`phone_number_id` hashed).
210
+
211
+ The full list, plus rules you must enforce yourself and current
212
+ divergences from latest Meta guidance (e.g. Graph API version pin), is at
213
+ [`docs/compliance.md`](./docs/compliance.md).
214
+
215
+ ## Spec-driven development
216
+
217
+ Every meaningful change is proposed as an OpenSpec change before
218
+ implementation. Specs live under `openspec/specs/`; active proposals
219
+ under `openspec/changes/`. CI validates both.
220
+
221
+ ```bash
222
+ openspec new change <name> # scaffold proposal/design/tasks
223
+ openspec validate --change <name> # lint
224
+ # … implement against the proposal …
225
+ openspec archive <name> # merge spec deltas into specs/
226
+ ```
227
+
228
+ The "Domain rules — never violate" block in
229
+ [`openspec/config.yaml`](./openspec/config.yaml) is the canonical
230
+ constraint list. See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full
231
+ workflow.
232
+
233
+ ## What this is NOT (v1)
234
+
235
+ Voice / Calls API, Flows beyond send-only `interactive.flow`,
236
+ Click-to-WhatsApp >72 h, quality dashboards, template-authoring UI,
237
+ Embedded Signup UI. The SDK does not bundle conversation-state
238
+ storage, intent classification, or any agent-framework concept (Skills,
239
+ tools, orchestrators) — those live above the SDK in your application
240
+ layer.
241
+
242
+ This SDK targets Meta's **Cloud API** only. It is not a WhatsApp Web
243
+ client (so not Baileys, whatsapp-web.js, `openclaw/wacli`, or any
244
+ `whatsmeow`-based tool). The trust model and the public API are both
245
+ different — see
246
+ [`docs/compatibility.md`](./docs/compatibility.md#where-this-sdk-fits).
247
+
248
+ ## Repo layout
249
+
250
+ ```
251
+ src/
252
+ client/ # cloud API client (transport, retry, errors, health)
253
+ messages/ # builders + send + types
254
+ webhooks/ # handshake, signature, parser, dedupe, receiver, events
255
+ window/ # 24h tracker
256
+ templates/ # list/get + placeholder counter + validateTemplateSend
257
+ mock/ # MockWhatsAppClient + pickWhatsAppClient
258
+ observability/ # withSpan + redact salt
259
+ adapters/ # express middleware
260
+ storage/ # Storage interface + InMemoryStorage
261
+ types/ # constants, error classes
262
+ index.ts
263
+ test/
264
+ unit/ # one suite per src module
265
+ contract/ # public API surface vs spec scenarios
266
+ integration/ # framework adapters (Express + supertest)
267
+ parity/ # MockWhatsAppClient ⇆ WhatsAppClient
268
+ __fixtures__/ # captured PII-redacted Meta payloads
269
+ docs/ # consumer-facing reference (this is what you're reading)
270
+ openspec/
271
+ specs/<cap>/spec.md # stable spec per capability
272
+ changes/<name>/ # active proposals (and archive/)
273
+ config.yaml # domain rules + conventions
274
+ ```
275
+
276
+ ## Reporting
277
+
278
+ - **Issues:** [GitHub Issues](https://github.com/DojoCodingLabs/whatsapp-adapter/issues)
279
+ for bugs, missing capability, etc.
280
+ - **Security:** see [`SECURITY.md`](./SECURITY.md) — please do not file
281
+ security reports as public issues.
282
+ - **Contributing:** see [`CONTRIBUTING.md`](./CONTRIBUTING.md).
283
+
284
+ ## License
285
+
286
+ [MIT](./LICENSE) © Dojo Coding LLC.
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ var buffer = require('buffer');
4
+ var express = require('express');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var express__default = /*#__PURE__*/_interopDefault(express);
9
+
10
+ // src/adapters/express/index.ts
11
+
12
+ // src/adapters/web/index.ts
13
+ function createWhatsAppHandler(receiver, options = {}) {
14
+ const onUnhandledHandlerError = options.onUnhandledHandlerError ?? ((err) => {
15
+ console.error("[whatsapp/web] unhandled handler error:", err);
16
+ });
17
+ return async (req) => {
18
+ const method = req.method.toUpperCase();
19
+ if (method === "GET") {
20
+ const url = new URL(req.url);
21
+ const mode = url.searchParams.get("hub.mode") ?? void 0;
22
+ const verifyToken = url.searchParams.get("hub.verify_token") ?? void 0;
23
+ const challenge = url.searchParams.get("hub.challenge") ?? void 0;
24
+ const result = receiver.handleVerifyRequest({ mode, verifyToken, challenge });
25
+ if (result.status === 200) {
26
+ return new Response(result.body, {
27
+ status: 200,
28
+ headers: { "content-type": "text/plain" }
29
+ });
30
+ }
31
+ return new Response(null, { status: 403 });
32
+ }
33
+ if (method === "POST") {
34
+ const rawBody = new Uint8Array(await req.arrayBuffer());
35
+ const sigHeader = req.headers.get("x-hub-signature-256");
36
+ let parsed = void 0;
37
+ if (rawBody.length > 0) {
38
+ try {
39
+ parsed = JSON.parse(new TextDecoder("utf-8").decode(rawBody));
40
+ } catch {
41
+ parsed = void 0;
42
+ }
43
+ }
44
+ const result = await receiver.handlePayload(rawBody, sigHeader, parsed);
45
+ if (result.status === 200) {
46
+ result.dispatchPromise.catch(onUnhandledHandlerError);
47
+ return new Response(null, { status: 200 });
48
+ }
49
+ return new Response(null, { status: 401 });
50
+ }
51
+ return new Response(null, { status: 405, headers: { allow: "GET, POST" } });
52
+ };
53
+ }
54
+
55
+ // src/adapters/express/index.ts
56
+ function createWhatsAppMiddleware(receiver, options = {}) {
57
+ const router = express__default.default.Router();
58
+ const onUnhandledHandlerError = options.onUnhandledHandlerError ?? ((err) => {
59
+ console.error("[whatsapp/express] unhandled handler error:", err);
60
+ });
61
+ const handler = createWhatsAppHandler(receiver, { onUnhandledHandlerError });
62
+ router.get("/", (req, res) => {
63
+ const url = `${req.protocol}://${req.get("host") ?? "localhost"}${req.originalUrl}`;
64
+ const headers = headersFromExpress(req);
65
+ handler(new Request(url, { method: "GET", headers })).then(
66
+ (r) => writeResponseToExpress(r, res),
67
+ (err) => {
68
+ onUnhandledHandlerError(err);
69
+ res.status(500).end();
70
+ }
71
+ );
72
+ });
73
+ router.post("/", express__default.default.raw({ type: "application/json" }), (req, res) => {
74
+ const rawBody = buffer.Buffer.isBuffer(req.body) ? req.body : buffer.Buffer.alloc(0);
75
+ const url = `${req.protocol}://${req.get("host") ?? "localhost"}${req.originalUrl}`;
76
+ const headers = headersFromExpress(req);
77
+ handler(new Request(url, { method: "POST", headers, body: new Uint8Array(rawBody) })).then(
78
+ (r) => writeResponseToExpress(r, res),
79
+ (err) => {
80
+ onUnhandledHandlerError(err);
81
+ res.status(500).end();
82
+ }
83
+ );
84
+ });
85
+ router.all("/", (_req, res) => {
86
+ res.set("Allow", "GET, POST").status(405).end();
87
+ });
88
+ return router;
89
+ }
90
+ function headersFromExpress(req) {
91
+ const headers = new Headers();
92
+ for (const [k, v] of Object.entries(req.headers)) {
93
+ if (Array.isArray(v)) {
94
+ for (const item of v) headers.append(k, item);
95
+ } else if (typeof v === "string") {
96
+ headers.set(k, v);
97
+ }
98
+ }
99
+ return headers;
100
+ }
101
+ async function writeResponseToExpress(r, res) {
102
+ res.status(r.status);
103
+ r.headers.forEach((value, key) => {
104
+ res.setHeader(key, value);
105
+ });
106
+ const text = await r.text();
107
+ if (text.length === 0) {
108
+ res.end();
109
+ return;
110
+ }
111
+ res.send(text);
112
+ }
113
+
114
+ exports.createWhatsAppMiddleware = createWhatsAppMiddleware;
@@ -0,0 +1,42 @@
1
+ import { Router } from 'express';
2
+ import { W as WebhookReceiver } from '../../receiver-DWJm571Z.cjs';
3
+ import { CreateWhatsAppHandlerOptions } from '../web/index.cjs';
4
+ import '../../index-CDfzGvQJ.cjs';
5
+
6
+ /**
7
+ * Express adapter for `@dojocoding/whatsapp`.
8
+ *
9
+ * Thin shim over `@dojocoding/whatsapp/web`: every request is buffered
10
+ * into a `Uint8Array`, converted to a Fetch-API `Request`, handed to
11
+ * `createWhatsAppHandler`, and the resulting `Response` is written
12
+ * back onto Express's `res`. All behaviour (handshake, signature,
13
+ * dispatch, 30 s ack, 405 routing) lives in the web core; this file
14
+ * just translates Express's req/res calling convention.
15
+ *
16
+ * Mount with:
17
+ *
18
+ * import express from "express";
19
+ * import { WebhookReceiver } from "@dojocoding/whatsapp";
20
+ * import { createWhatsAppMiddleware } from "@dojocoding/whatsapp/express";
21
+ *
22
+ * const receiver = new WebhookReceiver({ appSecret, verifyToken });
23
+ * receiver.on("message", async (e) => { … });
24
+ *
25
+ * const app = express();
26
+ * app.use("/webhooks/whatsapp", createWhatsAppMiddleware(receiver));
27
+ * // ^ register BEFORE any global express.json() — the middleware
28
+ * // captures the raw body locally, but a global json() registered
29
+ * // earlier will consume the stream and the HMAC will fail to
30
+ * // verify (you'll see 401s).
31
+ */
32
+
33
+ type CreateWhatsAppMiddlewareOptions = CreateWhatsAppHandlerOptions;
34
+ /**
35
+ * Build an Express `Router` that wires Meta's webhook contract to a
36
+ * framework-agnostic {@link WebhookReceiver}. Delegates to the
37
+ * web-standard core (`createWhatsAppHandler`) — this is a translation
38
+ * layer, not its own implementation.
39
+ */
40
+ declare function createWhatsAppMiddleware(receiver: WebhookReceiver, options?: CreateWhatsAppMiddlewareOptions): Router;
41
+
42
+ export { type CreateWhatsAppMiddlewareOptions, createWhatsAppMiddleware };
@@ -0,0 +1,42 @@
1
+ import { Router } from 'express';
2
+ import { W as WebhookReceiver } from '../../receiver-C_yfwg6g.js';
3
+ import { CreateWhatsAppHandlerOptions } from '../web/index.js';
4
+ import '../../index-CDfzGvQJ.js';
5
+
6
+ /**
7
+ * Express adapter for `@dojocoding/whatsapp`.
8
+ *
9
+ * Thin shim over `@dojocoding/whatsapp/web`: every request is buffered
10
+ * into a `Uint8Array`, converted to a Fetch-API `Request`, handed to
11
+ * `createWhatsAppHandler`, and the resulting `Response` is written
12
+ * back onto Express's `res`. All behaviour (handshake, signature,
13
+ * dispatch, 30 s ack, 405 routing) lives in the web core; this file
14
+ * just translates Express's req/res calling convention.
15
+ *
16
+ * Mount with:
17
+ *
18
+ * import express from "express";
19
+ * import { WebhookReceiver } from "@dojocoding/whatsapp";
20
+ * import { createWhatsAppMiddleware } from "@dojocoding/whatsapp/express";
21
+ *
22
+ * const receiver = new WebhookReceiver({ appSecret, verifyToken });
23
+ * receiver.on("message", async (e) => { … });
24
+ *
25
+ * const app = express();
26
+ * app.use("/webhooks/whatsapp", createWhatsAppMiddleware(receiver));
27
+ * // ^ register BEFORE any global express.json() — the middleware
28
+ * // captures the raw body locally, but a global json() registered
29
+ * // earlier will consume the stream and the HMAC will fail to
30
+ * // verify (you'll see 401s).
31
+ */
32
+
33
+ type CreateWhatsAppMiddlewareOptions = CreateWhatsAppHandlerOptions;
34
+ /**
35
+ * Build an Express `Router` that wires Meta's webhook contract to a
36
+ * framework-agnostic {@link WebhookReceiver}. Delegates to the
37
+ * web-standard core (`createWhatsAppHandler`) — this is a translation
38
+ * layer, not its own implementation.
39
+ */
40
+ declare function createWhatsAppMiddleware(receiver: WebhookReceiver, options?: CreateWhatsAppMiddlewareOptions): Router;
41
+
42
+ export { type CreateWhatsAppMiddlewareOptions, createWhatsAppMiddleware };
@@ -0,0 +1,108 @@
1
+ import { Buffer } from 'buffer';
2
+ import express from 'express';
3
+
4
+ // src/adapters/express/index.ts
5
+
6
+ // src/adapters/web/index.ts
7
+ function createWhatsAppHandler(receiver, options = {}) {
8
+ const onUnhandledHandlerError = options.onUnhandledHandlerError ?? ((err) => {
9
+ console.error("[whatsapp/web] unhandled handler error:", err);
10
+ });
11
+ return async (req) => {
12
+ const method = req.method.toUpperCase();
13
+ if (method === "GET") {
14
+ const url = new URL(req.url);
15
+ const mode = url.searchParams.get("hub.mode") ?? void 0;
16
+ const verifyToken = url.searchParams.get("hub.verify_token") ?? void 0;
17
+ const challenge = url.searchParams.get("hub.challenge") ?? void 0;
18
+ const result = receiver.handleVerifyRequest({ mode, verifyToken, challenge });
19
+ if (result.status === 200) {
20
+ return new Response(result.body, {
21
+ status: 200,
22
+ headers: { "content-type": "text/plain" }
23
+ });
24
+ }
25
+ return new Response(null, { status: 403 });
26
+ }
27
+ if (method === "POST") {
28
+ const rawBody = new Uint8Array(await req.arrayBuffer());
29
+ const sigHeader = req.headers.get("x-hub-signature-256");
30
+ let parsed = void 0;
31
+ if (rawBody.length > 0) {
32
+ try {
33
+ parsed = JSON.parse(new TextDecoder("utf-8").decode(rawBody));
34
+ } catch {
35
+ parsed = void 0;
36
+ }
37
+ }
38
+ const result = await receiver.handlePayload(rawBody, sigHeader, parsed);
39
+ if (result.status === 200) {
40
+ result.dispatchPromise.catch(onUnhandledHandlerError);
41
+ return new Response(null, { status: 200 });
42
+ }
43
+ return new Response(null, { status: 401 });
44
+ }
45
+ return new Response(null, { status: 405, headers: { allow: "GET, POST" } });
46
+ };
47
+ }
48
+
49
+ // src/adapters/express/index.ts
50
+ function createWhatsAppMiddleware(receiver, options = {}) {
51
+ const router = express.Router();
52
+ const onUnhandledHandlerError = options.onUnhandledHandlerError ?? ((err) => {
53
+ console.error("[whatsapp/express] unhandled handler error:", err);
54
+ });
55
+ const handler = createWhatsAppHandler(receiver, { onUnhandledHandlerError });
56
+ router.get("/", (req, res) => {
57
+ const url = `${req.protocol}://${req.get("host") ?? "localhost"}${req.originalUrl}`;
58
+ const headers = headersFromExpress(req);
59
+ handler(new Request(url, { method: "GET", headers })).then(
60
+ (r) => writeResponseToExpress(r, res),
61
+ (err) => {
62
+ onUnhandledHandlerError(err);
63
+ res.status(500).end();
64
+ }
65
+ );
66
+ });
67
+ router.post("/", express.raw({ type: "application/json" }), (req, res) => {
68
+ const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
69
+ const url = `${req.protocol}://${req.get("host") ?? "localhost"}${req.originalUrl}`;
70
+ const headers = headersFromExpress(req);
71
+ handler(new Request(url, { method: "POST", headers, body: new Uint8Array(rawBody) })).then(
72
+ (r) => writeResponseToExpress(r, res),
73
+ (err) => {
74
+ onUnhandledHandlerError(err);
75
+ res.status(500).end();
76
+ }
77
+ );
78
+ });
79
+ router.all("/", (_req, res) => {
80
+ res.set("Allow", "GET, POST").status(405).end();
81
+ });
82
+ return router;
83
+ }
84
+ function headersFromExpress(req) {
85
+ const headers = new Headers();
86
+ for (const [k, v] of Object.entries(req.headers)) {
87
+ if (Array.isArray(v)) {
88
+ for (const item of v) headers.append(k, item);
89
+ } else if (typeof v === "string") {
90
+ headers.set(k, v);
91
+ }
92
+ }
93
+ return headers;
94
+ }
95
+ async function writeResponseToExpress(r, res) {
96
+ res.status(r.status);
97
+ r.headers.forEach((value, key) => {
98
+ res.setHeader(key, value);
99
+ });
100
+ const text = await r.text();
101
+ if (text.length === 0) {
102
+ res.end();
103
+ return;
104
+ }
105
+ res.send(text);
106
+ }
107
+
108
+ export { createWhatsAppMiddleware };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ // src/adapters/web/index.ts
4
+ function createWhatsAppHandler(receiver, options = {}) {
5
+ const onUnhandledHandlerError = options.onUnhandledHandlerError ?? ((err) => {
6
+ console.error("[whatsapp/web] unhandled handler error:", err);
7
+ });
8
+ return async (req) => {
9
+ const method = req.method.toUpperCase();
10
+ if (method === "GET") {
11
+ const url = new URL(req.url);
12
+ const mode = url.searchParams.get("hub.mode") ?? void 0;
13
+ const verifyToken = url.searchParams.get("hub.verify_token") ?? void 0;
14
+ const challenge = url.searchParams.get("hub.challenge") ?? void 0;
15
+ const result = receiver.handleVerifyRequest({ mode, verifyToken, challenge });
16
+ if (result.status === 200) {
17
+ return new Response(result.body, {
18
+ status: 200,
19
+ headers: { "content-type": "text/plain" }
20
+ });
21
+ }
22
+ return new Response(null, { status: 403 });
23
+ }
24
+ if (method === "POST") {
25
+ const rawBody = new Uint8Array(await req.arrayBuffer());
26
+ const sigHeader = req.headers.get("x-hub-signature-256");
27
+ let parsed = void 0;
28
+ if (rawBody.length > 0) {
29
+ try {
30
+ parsed = JSON.parse(new TextDecoder("utf-8").decode(rawBody));
31
+ } catch {
32
+ parsed = void 0;
33
+ }
34
+ }
35
+ const result = await receiver.handlePayload(rawBody, sigHeader, parsed);
36
+ if (result.status === 200) {
37
+ result.dispatchPromise.catch(onUnhandledHandlerError);
38
+ return new Response(null, { status: 200 });
39
+ }
40
+ return new Response(null, { status: 401 });
41
+ }
42
+ return new Response(null, { status: 405, headers: { allow: "GET, POST" } });
43
+ };
44
+ }
45
+
46
+ // src/adapters/hono/index.ts
47
+ function whatsappHandler(receiver, options = {}) {
48
+ const core = createWhatsAppHandler(receiver, options);
49
+ return (c) => core(c.req.raw);
50
+ }
51
+
52
+ exports.whatsappHandler = whatsappHandler;
@@ -0,0 +1,38 @@
1
+ import { Handler } from 'hono';
2
+ import { W as WebhookReceiver } from '../../receiver-DWJm571Z.cjs';
3
+ import { CreateWhatsAppHandlerOptions } from '../web/index.cjs';
4
+ import '../../index-CDfzGvQJ.cjs';
5
+
6
+ /**
7
+ * Hono adapter for `@dojocoding/whatsapp`.
8
+ *
9
+ * Thin wrapper around the web-standard `createWhatsAppHandler` core,
10
+ * adapted to Hono's `Handler` signature. The web core does all the
11
+ * work; this file just unwraps Hono's `c.req.raw` (which is a
12
+ * Fetch-API `Request`) and returns the `Response` Hono expects.
13
+ *
14
+ * Mount with:
15
+ *
16
+ * import { Hono } from "hono";
17
+ * import { WebhookReceiver } from "@dojocoding/whatsapp";
18
+ * import { whatsappHandler } from "@dojocoding/whatsapp/hono";
19
+ *
20
+ * const receiver = new WebhookReceiver({ appSecret, verifyToken });
21
+ * receiver.on("message", async (e) => { … });
22
+ *
23
+ * const app = new Hono();
24
+ * app.all("/webhooks/whatsapp", whatsappHandler(receiver));
25
+ *
26
+ * See `docs/hono.md` for a full Cloudflare Workers + Hono walkthrough.
27
+ */
28
+
29
+ /** Alias for the web-core options shape; same fields, same semantics. */
30
+ type WhatsAppHonoHandlerOptions = CreateWhatsAppHandlerOptions;
31
+ /**
32
+ * Build a Hono `Handler` that wires Meta's webhook contract to a
33
+ * framework-agnostic {@link WebhookReceiver} via the web-standard
34
+ * core. Mount with `app.all(path, whatsappHandler(receiver))`.
35
+ */
36
+ declare function whatsappHandler(receiver: WebhookReceiver, options?: WhatsAppHonoHandlerOptions): Handler;
37
+
38
+ export { type WhatsAppHonoHandlerOptions, whatsappHandler };