@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/CHANGELOG.md +402 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/adapters/express/index.cjs +114 -0
- package/dist/adapters/express/index.d.cts +42 -0
- package/dist/adapters/express/index.d.ts +42 -0
- package/dist/adapters/express/index.js +108 -0
- package/dist/adapters/hono/index.cjs +52 -0
- package/dist/adapters/hono/index.d.cts +38 -0
- package/dist/adapters/hono/index.d.ts +38 -0
- package/dist/adapters/hono/index.js +50 -0
- package/dist/adapters/web/index.cjs +46 -0
- package/dist/adapters/web/index.d.cts +40 -0
- package/dist/adapters/web/index.d.ts +40 -0
- package/dist/adapters/web/index.js +44 -0
- package/dist/index-CDfzGvQJ.d.cts +42 -0
- package/dist/index-CDfzGvQJ.d.ts +42 -0
- package/dist/index.cjs +2242 -0
- package/dist/index.d.cts +1262 -0
- package/dist/index.d.ts +1262 -0
- package/dist/index.js +2183 -0
- package/dist/receiver-C_yfwg6g.d.ts +167 -0
- package/dist/receiver-DWJm571Z.d.cts +167 -0
- package/dist/storage/postgres.cjs +66 -0
- package/dist/storage/postgres.d.cts +38 -0
- package/dist/storage/postgres.d.ts +38 -0
- package/dist/storage/postgres.js +63 -0
- package/dist/storage/redis.cjs +32 -0
- package/dist/storage/redis.d.cts +38 -0
- package/dist/storage/redis.d.ts +38 -0
- package/dist/storage/redis.js +30 -0
- package/package.json +181 -0
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 };
|