@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.
@@ -0,0 +1,38 @@
1
+ import { Handler } from 'hono';
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
+ * 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 };
@@ -0,0 +1,50 @@
1
+ // src/adapters/web/index.ts
2
+ function createWhatsAppHandler(receiver, options = {}) {
3
+ const onUnhandledHandlerError = options.onUnhandledHandlerError ?? ((err) => {
4
+ console.error("[whatsapp/web] unhandled handler error:", err);
5
+ });
6
+ return async (req) => {
7
+ const method = req.method.toUpperCase();
8
+ if (method === "GET") {
9
+ const url = new URL(req.url);
10
+ const mode = url.searchParams.get("hub.mode") ?? void 0;
11
+ const verifyToken = url.searchParams.get("hub.verify_token") ?? void 0;
12
+ const challenge = url.searchParams.get("hub.challenge") ?? void 0;
13
+ const result = receiver.handleVerifyRequest({ mode, verifyToken, challenge });
14
+ if (result.status === 200) {
15
+ return new Response(result.body, {
16
+ status: 200,
17
+ headers: { "content-type": "text/plain" }
18
+ });
19
+ }
20
+ return new Response(null, { status: 403 });
21
+ }
22
+ if (method === "POST") {
23
+ const rawBody = new Uint8Array(await req.arrayBuffer());
24
+ const sigHeader = req.headers.get("x-hub-signature-256");
25
+ let parsed = void 0;
26
+ if (rawBody.length > 0) {
27
+ try {
28
+ parsed = JSON.parse(new TextDecoder("utf-8").decode(rawBody));
29
+ } catch {
30
+ parsed = void 0;
31
+ }
32
+ }
33
+ const result = await receiver.handlePayload(rawBody, sigHeader, parsed);
34
+ if (result.status === 200) {
35
+ result.dispatchPromise.catch(onUnhandledHandlerError);
36
+ return new Response(null, { status: 200 });
37
+ }
38
+ return new Response(null, { status: 401 });
39
+ }
40
+ return new Response(null, { status: 405, headers: { allow: "GET, POST" } });
41
+ };
42
+ }
43
+
44
+ // src/adapters/hono/index.ts
45
+ function whatsappHandler(receiver, options = {}) {
46
+ const core = createWhatsAppHandler(receiver, options);
47
+ return (c) => core(c.req.raw);
48
+ }
49
+
50
+ export { whatsappHandler };
@@ -0,0 +1,46 @@
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
+ exports.createWhatsAppHandler = createWhatsAppHandler;
@@ -0,0 +1,40 @@
1
+ import { W as WebhookReceiver } from '../../receiver-DWJm571Z.cjs';
2
+ import '../../index-CDfzGvQJ.cjs';
3
+
4
+ /**
5
+ * Web-standard (Fetch-API) adapter for `@dojocoding/whatsapp`.
6
+ *
7
+ * Returns a function with shape `(req: Request) => Promise<Response>` —
8
+ * mountable as a Cloudflare Workers `fetch` handler, a Hono / Next.js
9
+ * App Router route handler, a Bun `Bun.serve` handler, or anywhere
10
+ * else that speaks the Fetch API.
11
+ *
12
+ * The Express adapter (`@dojocoding/whatsapp/express`) is a thin shim
13
+ * over this same core; see `docs/web.md` for runtime examples.
14
+ *
15
+ * Behaviour:
16
+ * - GET → verify-token handshake via `receiver.handleVerifyRequest`.
17
+ * 200 text/plain with the challenge body on success, 403 on
18
+ * mismatch.
19
+ * - POST → raw bytes via `req.arrayBuffer()` (read exactly once),
20
+ * passed verbatim to `receiver.handlePayload`. Acks 200
21
+ * BEFORE awaiting handlers (Meta's 30 s rule), then runs
22
+ * handlers asynchronously.
23
+ * - Other verbs → 405 Method Not Allowed.
24
+ */
25
+
26
+ interface CreateWhatsAppHandlerOptions {
27
+ /**
28
+ * Invoked when a handler thrown error escapes the dispatch promise.
29
+ * Defaults to `console.error`.
30
+ */
31
+ onUnhandledHandlerError?: (err: unknown) => void;
32
+ }
33
+ type WhatsAppHandler = (req: Request) => Promise<Response>;
34
+ /**
35
+ * Build a Fetch-API handler wiring Meta's webhook contract to a
36
+ * framework-agnostic {@link WebhookReceiver}.
37
+ */
38
+ declare function createWhatsAppHandler(receiver: WebhookReceiver, options?: CreateWhatsAppHandlerOptions): WhatsAppHandler;
39
+
40
+ export { type CreateWhatsAppHandlerOptions, type WhatsAppHandler, createWhatsAppHandler };
@@ -0,0 +1,40 @@
1
+ import { W as WebhookReceiver } from '../../receiver-C_yfwg6g.js';
2
+ import '../../index-CDfzGvQJ.js';
3
+
4
+ /**
5
+ * Web-standard (Fetch-API) adapter for `@dojocoding/whatsapp`.
6
+ *
7
+ * Returns a function with shape `(req: Request) => Promise<Response>` —
8
+ * mountable as a Cloudflare Workers `fetch` handler, a Hono / Next.js
9
+ * App Router route handler, a Bun `Bun.serve` handler, or anywhere
10
+ * else that speaks the Fetch API.
11
+ *
12
+ * The Express adapter (`@dojocoding/whatsapp/express`) is a thin shim
13
+ * over this same core; see `docs/web.md` for runtime examples.
14
+ *
15
+ * Behaviour:
16
+ * - GET → verify-token handshake via `receiver.handleVerifyRequest`.
17
+ * 200 text/plain with the challenge body on success, 403 on
18
+ * mismatch.
19
+ * - POST → raw bytes via `req.arrayBuffer()` (read exactly once),
20
+ * passed verbatim to `receiver.handlePayload`. Acks 200
21
+ * BEFORE awaiting handlers (Meta's 30 s rule), then runs
22
+ * handlers asynchronously.
23
+ * - Other verbs → 405 Method Not Allowed.
24
+ */
25
+
26
+ interface CreateWhatsAppHandlerOptions {
27
+ /**
28
+ * Invoked when a handler thrown error escapes the dispatch promise.
29
+ * Defaults to `console.error`.
30
+ */
31
+ onUnhandledHandlerError?: (err: unknown) => void;
32
+ }
33
+ type WhatsAppHandler = (req: Request) => Promise<Response>;
34
+ /**
35
+ * Build a Fetch-API handler wiring Meta's webhook contract to a
36
+ * framework-agnostic {@link WebhookReceiver}.
37
+ */
38
+ declare function createWhatsAppHandler(receiver: WebhookReceiver, options?: CreateWhatsAppHandlerOptions): WhatsAppHandler;
39
+
40
+ export { type CreateWhatsAppHandlerOptions, type WhatsAppHandler, createWhatsAppHandler };
@@ -0,0 +1,44 @@
1
+ // src/adapters/web/index.ts
2
+ function createWhatsAppHandler(receiver, options = {}) {
3
+ const onUnhandledHandlerError = options.onUnhandledHandlerError ?? ((err) => {
4
+ console.error("[whatsapp/web] unhandled handler error:", err);
5
+ });
6
+ return async (req) => {
7
+ const method = req.method.toUpperCase();
8
+ if (method === "GET") {
9
+ const url = new URL(req.url);
10
+ const mode = url.searchParams.get("hub.mode") ?? void 0;
11
+ const verifyToken = url.searchParams.get("hub.verify_token") ?? void 0;
12
+ const challenge = url.searchParams.get("hub.challenge") ?? void 0;
13
+ const result = receiver.handleVerifyRequest({ mode, verifyToken, challenge });
14
+ if (result.status === 200) {
15
+ return new Response(result.body, {
16
+ status: 200,
17
+ headers: { "content-type": "text/plain" }
18
+ });
19
+ }
20
+ return new Response(null, { status: 403 });
21
+ }
22
+ if (method === "POST") {
23
+ const rawBody = new Uint8Array(await req.arrayBuffer());
24
+ const sigHeader = req.headers.get("x-hub-signature-256");
25
+ let parsed = void 0;
26
+ if (rawBody.length > 0) {
27
+ try {
28
+ parsed = JSON.parse(new TextDecoder("utf-8").decode(rawBody));
29
+ } catch {
30
+ parsed = void 0;
31
+ }
32
+ }
33
+ const result = await receiver.handlePayload(rawBody, sigHeader, parsed);
34
+ if (result.status === 200) {
35
+ result.dispatchPromise.catch(onUnhandledHandlerError);
36
+ return new Response(null, { status: 200 });
37
+ }
38
+ return new Response(null, { status: 401 });
39
+ }
40
+ return new Response(null, { status: 405, headers: { allow: "GET, POST" } });
41
+ };
42
+ }
43
+
44
+ export { createWhatsAppHandler };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pluggable async key-value storage with TTL semantics. Implementations
3
+ * MUST honour `ttlMs`: entries past their `expiresAt` SHALL NOT be
4
+ * returned by `get`. Implementations SHOULD NOT spawn background timers;
5
+ * lazy eviction (on access) is preferred so consumers do not leak timers
6
+ * that prevent process exit.
7
+ */
8
+ interface Storage {
9
+ get<T>(key: string): Promise<T | undefined>;
10
+ set<T>(key: string, value: T, ttlMs: number): Promise<void>;
11
+ /**
12
+ * Atomically write `value` for `key` only if no live entry exists.
13
+ * Returns `true` when the value was written, `false` when an unexpired
14
+ * entry was already present. Implementations MUST honour `ttlMs` the
15
+ * same way as `set`.
16
+ */
17
+ setIfAbsent<T>(key: string, value: T, ttlMs: number): Promise<boolean>;
18
+ delete(key: string): Promise<void>;
19
+ }
20
+ /**
21
+ * In-memory implementation of {@link Storage}. Backed by a `Map`. TTL is
22
+ * enforced lazily — expired entries are evicted only on access. No
23
+ * background timers are spawned.
24
+ *
25
+ * Suitable for development, tests, and single-process deployments. For
26
+ * multi-process or cross-instance dedupe, implement `Storage` against
27
+ * Redis / Postgres / your shared cache.
28
+ */
29
+ declare class InMemoryStorage implements Storage {
30
+ #private;
31
+ constructor(options?: {
32
+ now?: () => number;
33
+ });
34
+ get<T>(key: string): Promise<T | undefined>;
35
+ set<T>(key: string, value: T, ttlMs: number): Promise<void>;
36
+ setIfAbsent<T>(key: string, value: T, ttlMs: number): Promise<boolean>;
37
+ delete(key: string): Promise<void>;
38
+ /** Test-only — returns the current map size (including expired entries). */
39
+ _rawSize(): number;
40
+ }
41
+
42
+ export { InMemoryStorage as I, type Storage as S };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pluggable async key-value storage with TTL semantics. Implementations
3
+ * MUST honour `ttlMs`: entries past their `expiresAt` SHALL NOT be
4
+ * returned by `get`. Implementations SHOULD NOT spawn background timers;
5
+ * lazy eviction (on access) is preferred so consumers do not leak timers
6
+ * that prevent process exit.
7
+ */
8
+ interface Storage {
9
+ get<T>(key: string): Promise<T | undefined>;
10
+ set<T>(key: string, value: T, ttlMs: number): Promise<void>;
11
+ /**
12
+ * Atomically write `value` for `key` only if no live entry exists.
13
+ * Returns `true` when the value was written, `false` when an unexpired
14
+ * entry was already present. Implementations MUST honour `ttlMs` the
15
+ * same way as `set`.
16
+ */
17
+ setIfAbsent<T>(key: string, value: T, ttlMs: number): Promise<boolean>;
18
+ delete(key: string): Promise<void>;
19
+ }
20
+ /**
21
+ * In-memory implementation of {@link Storage}. Backed by a `Map`. TTL is
22
+ * enforced lazily — expired entries are evicted only on access. No
23
+ * background timers are spawned.
24
+ *
25
+ * Suitable for development, tests, and single-process deployments. For
26
+ * multi-process or cross-instance dedupe, implement `Storage` against
27
+ * Redis / Postgres / your shared cache.
28
+ */
29
+ declare class InMemoryStorage implements Storage {
30
+ #private;
31
+ constructor(options?: {
32
+ now?: () => number;
33
+ });
34
+ get<T>(key: string): Promise<T | undefined>;
35
+ set<T>(key: string, value: T, ttlMs: number): Promise<void>;
36
+ setIfAbsent<T>(key: string, value: T, ttlMs: number): Promise<boolean>;
37
+ delete(key: string): Promise<void>;
38
+ /** Test-only — returns the current map size (including expired entries). */
39
+ _rawSize(): number;
40
+ }
41
+
42
+ export { InMemoryStorage as I, type Storage as S };