@0xmaxma/claude-gateway 1.8.10 → 1.8.12
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 +14 -1
- package/config.template.json +2 -1
- package/dist/agent/runner.d.ts +72 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +300 -6
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +1001 -4
- package/dist/api/router.js.map +1 -1
- package/dist/api/webhooks-router.d.ts +2 -0
- package/dist/api/webhooks-router.d.ts.map +1 -1
- package/dist/api/webhooks-router.js +2 -0
- package/dist/api/webhooks-router.js.map +1 -1
- package/dist/api/whatsapp-access.d.ts +123 -0
- package/dist/api/whatsapp-access.d.ts.map +1 -0
- package/dist/api/whatsapp-access.js +135 -0
- package/dist/api/whatsapp-access.js.map +1 -0
- package/dist/api/whatsapp-cloud-access.d.ts +48 -0
- package/dist/api/whatsapp-cloud-access.d.ts.map +1 -0
- package/dist/api/whatsapp-cloud-access.js +58 -0
- package/dist/api/whatsapp-cloud-access.js.map +1 -0
- package/dist/api/whatsapp-cloud-client.d.ts +143 -0
- package/dist/api/whatsapp-cloud-client.d.ts.map +1 -0
- package/dist/api/whatsapp-cloud-client.js +298 -0
- package/dist/api/whatsapp-cloud-client.js.map +1 -0
- package/dist/api/whatsapp-cloud-webhook-router.d.ts +132 -0
- package/dist/api/whatsapp-cloud-webhook-router.d.ts.map +1 -0
- package/dist/api/whatsapp-cloud-webhook-router.js +494 -0
- package/dist/api/whatsapp-cloud-webhook-router.js.map +1 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +7 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/whatsapp-accounts.d.ts +77 -0
- package/dist/config/whatsapp-accounts.d.ts.map +1 -0
- package/dist/config/whatsapp-accounts.js +218 -0
- package/dist/config/whatsapp-accounts.js.map +1 -0
- package/dist/history/db.d.ts.map +1 -1
- package/dist/history/db.js +43 -11
- package/dist/history/db.js.map +1 -1
- package/dist/history/types.d.ts +18 -1
- package/dist/history/types.d.ts.map +1 -1
- package/dist/history/types.js +1 -1
- package/dist/history/types.js.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +37 -0
- package/dist/session/process.js.map +1 -1
- package/dist/shared/image-optimize.d.ts +32 -0
- package/dist/shared/image-optimize.d.ts.map +1 -0
- package/dist/shared/image-optimize.js +197 -0
- package/dist/shared/image-optimize.js.map +1 -0
- package/dist/shared/image-sniff.d.ts +9 -0
- package/dist/shared/image-sniff.d.ts.map +1 -1
- package/dist/shared/image-sniff.js +13 -0
- package/dist/shared/image-sniff.js.map +1 -1
- package/dist/shared/text-chunk.d.ts +37 -0
- package/dist/shared/text-chunk.d.ts.map +1 -0
- package/dist/shared/text-chunk.js +105 -0
- package/dist/shared/text-chunk.js.map +1 -0
- package/dist/shared/whatsapp-ack.d.ts +13 -0
- package/dist/shared/whatsapp-ack.d.ts.map +1 -0
- package/dist/shared/whatsapp-ack.js +16 -0
- package/dist/shared/whatsapp-ack.js.map +1 -0
- package/dist/types.d.ts +158 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/whatsapp/manager.d.ts +173 -0
- package/dist/whatsapp/manager.d.ts.map +1 -0
- package/dist/whatsapp/manager.js +838 -0
- package/dist/whatsapp/manager.js.map +1 -0
- package/mcp/server.ts +4 -0
- package/mcp/tools/whatsapp/module.ts +162 -0
- package/mcp/tools/whatsapp-cloud/module.ts +536 -0
- package/package.json +9 -4
|
@@ -22,6 +22,7 @@ import { Router, type Request, type Response } from 'express';
|
|
|
22
22
|
import type { AgentRunner } from '../agent/runner';
|
|
23
23
|
import { type LineWebhookOptions } from './line-webhook-router';
|
|
24
24
|
import { type SlackWebhookOptions } from './slack-webhook-router';
|
|
25
|
+
import { type WhatsAppCloudWebhookOptions } from './whatsapp-cloud-webhook-router';
|
|
25
26
|
/**
|
|
26
27
|
* A per-app webhook handler. `verify` answers provider URL-verification probes
|
|
27
28
|
* (GET / empty POST); `handlePost` processes a signed inbound POST. Each handler
|
|
@@ -40,6 +41,7 @@ export interface WebhookAppHandler {
|
|
|
40
41
|
export interface WebhooksOptions {
|
|
41
42
|
line?: LineWebhookOptions;
|
|
42
43
|
slack?: SlackWebhookOptions;
|
|
44
|
+
whatsapp_cloud?: WhatsAppCloudWebhookOptions;
|
|
43
45
|
}
|
|
44
46
|
export declare function createWebhooksRouter(agents: Map<string, AgentRunner>, logDir: string, opts?: WebhooksOptions): Router;
|
|
45
47
|
//# sourceMappingURL=webhooks-router.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhooks-router.d.ts","sourceRoot":"","sources":["../../src/api/webhooks-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAgB,EAAE,MAAM,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"webhooks-router.d.ts","sourceRoot":"","sources":["../../src/api/webhooks-router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAgB,EAAE,MAAM,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7F,OAAO,EAAqC,KAAK,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AAItH;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1C,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,cAAc,CAAC,EAAE,2BAA2B,CAAC;CAC9C;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EAChC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,eAAoB,GACzB,MAAM,CAuCR"}
|
|
@@ -57,6 +57,7 @@ exports.createWebhooksRouter = createWebhooksRouter;
|
|
|
57
57
|
const express_1 = __importStar(require("express"));
|
|
58
58
|
const line_webhook_router_1 = require("./line-webhook-router");
|
|
59
59
|
const slack_webhook_router_1 = require("./slack-webhook-router");
|
|
60
|
+
const whatsapp_cloud_webhook_router_1 = require("./whatsapp-cloud-webhook-router");
|
|
60
61
|
const MAX_BODY_BYTES = 256 * 1024; // pre-auth body cap
|
|
61
62
|
function createWebhooksRouter(agents, logDir, opts = {}) {
|
|
62
63
|
const router = (0, express_1.Router)();
|
|
@@ -64,6 +65,7 @@ function createWebhooksRouter(agents, logDir, opts = {}) {
|
|
|
64
65
|
const handlers = {
|
|
65
66
|
line: (0, line_webhook_router_1.createLineWebhookHandler)(agents, logDir, opts.line ?? {}),
|
|
66
67
|
slack: (0, slack_webhook_router_1.createSlackWebhookHandler)(agents, logDir, opts.slack ?? {}),
|
|
68
|
+
whatsapp_cloud: (0, whatsapp_cloud_webhook_router_1.createWhatsAppCloudWebhookHandler)(agents, logDir, opts.whatsapp_cloud ?? {}),
|
|
67
69
|
};
|
|
68
70
|
const resolve = (req, res) => {
|
|
69
71
|
const app = req.params.app;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhooks-router.js","sourceRoot":"","sources":["../../src/api/webhooks-router.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"webhooks-router.js","sourceRoot":"","sources":["../../src/api/webhooks-router.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,oDA2CC;AA7FD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,mDAAuE;AAEvE,+DAA0F;AAC1F,iEAA6F;AAC7F,mFAAsH;AAEtH,MAAM,cAAc,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,oBAAoB;AAwBvD,SAAgB,oBAAoB,CAClC,MAAgC,EAChC,MAAc,EACd,OAAwB,EAAE;IAE1B,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IACxB,MAAM,OAAO,GAAG,iBAAO,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;IAEpE,MAAM,QAAQ,GAAsC;QAClD,IAAI,EAAE,IAAA,8CAAwB,EAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;QAC/D,KAAK,EAAE,IAAA,gDAAyB,EAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAClE,cAAc,EAAE,IAAA,iEAAiC,EAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC;KAC7F,CAAC;IAEF,MAAM,OAAO,GAAG,CAAC,GAAY,EAAE,GAAa,EAA4B,EAAE;QACxE,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC;QAC3B,+EAA+E;QAC/E,oFAAoF;QACpF,kFAAkF;QAClF,yCAAyC;QACzC,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAChG,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,wBAAwB,GAAG,EAAE,EAAE,CAAC,CAAC;YAC/D,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,CAAC,GAAY,EAAE,GAAa,EAAQ,EAAE;QACxD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAClC,IAAI,OAAO;YAAE,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACxC,CAAC,CAAC;IACF,MAAM,YAAY,GAAG,CAAC,GAAY,EAAE,GAAa,EAAQ,EAAE;QACzD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAClC,IAAI,OAAO;YAAE,KAAK,OAAO,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACjD,CAAC,CAAC;IAEF,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IACjC,MAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;IAC1C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;IAC5C,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;IAErD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp DM/group access control — pure, stateless sender gate.
|
|
3
|
+
*
|
|
4
|
+
* Structural port of `slack-access.ts` (same closed-by-default posture,
|
|
5
|
+
* same two-tier dmPolicy/groupPolicy shape — Slack is the right template
|
|
6
|
+
* here, not the DM-only `sms-access.ts`, because WhatsApp has real groups).
|
|
7
|
+
* The manager reads `whatsapp` config directly and calls this once per
|
|
8
|
+
* inbound message — no env plumbing, no state files.
|
|
9
|
+
*
|
|
10
|
+
* Default (policy absent) is CLOSED: only ids in the allowlist pass. Set
|
|
11
|
+
* `dmPolicy`/`groupPolicy`: 'open' to restore "reply to anyone" behavior.
|
|
12
|
+
*
|
|
13
|
+
* Allowlist entries MUST be WhatsApp JIDs (e.g. "66812345678@s.whatsapp.net"
|
|
14
|
+
* for a DM sender, "1234567890-1234567890@g.us" for a group) — never a
|
|
15
|
+
* bare phone number or a group's display name (renamable/ambiguous, same
|
|
16
|
+
* footgun Slack/LINE's own doc comments call out).
|
|
17
|
+
*/
|
|
18
|
+
export declare function isWhatsAppSenderAllowed(policy: 'open' | 'allowlist' | 'disabled' | undefined, allowlist: string[] | undefined, id: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Structural view of a Baileys inbound message — the only fields the gate
|
|
21
|
+
* needs. Kept local (not the Baileys `WAMessage` type) so this module stays
|
|
22
|
+
* pure and trivially testable.
|
|
23
|
+
*/
|
|
24
|
+
export interface WhatsAppMessageLike {
|
|
25
|
+
key?: {
|
|
26
|
+
remoteJid?: string | null;
|
|
27
|
+
/** Sender within a group (absent for 1:1 DMs, where remoteJid IS the sender). */
|
|
28
|
+
participant?: string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Phone-number JID for `participant`, present when WhatsApp's Linked ID
|
|
31
|
+
* (LID) privacy system reports `participant` as a `@lid` address instead
|
|
32
|
+
* of the classic `@s.whatsapp.net` phone JID.
|
|
33
|
+
*/
|
|
34
|
+
participantPn?: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Phone-number JID for `remoteJid` on a 1:1 chat, present when LID
|
|
37
|
+
* privacy reports `remoteJid` itself as `@lid` instead of the classic
|
|
38
|
+
* `@s.whatsapp.net` form.
|
|
39
|
+
*/
|
|
40
|
+
senderPn?: string | null;
|
|
41
|
+
fromMe?: boolean | null;
|
|
42
|
+
} | null;
|
|
43
|
+
message?: {
|
|
44
|
+
/** @mentioned JIDs, present on group messages that tag someone (including the bot). */
|
|
45
|
+
extendedTextMessage?: {
|
|
46
|
+
contextInfo?: {
|
|
47
|
+
mentionedJid?: string[] | null;
|
|
48
|
+
} | null;
|
|
49
|
+
} | null;
|
|
50
|
+
imageMessage?: {
|
|
51
|
+
contextInfo?: {
|
|
52
|
+
mentionedJid?: string[] | null;
|
|
53
|
+
} | null;
|
|
54
|
+
} | null;
|
|
55
|
+
} | null;
|
|
56
|
+
}
|
|
57
|
+
export type WhatsAppSourceKind = 'user' | 'group' | 'other';
|
|
58
|
+
export interface ResolvedWhatsAppSource {
|
|
59
|
+
/** Conversation key — the reply target: the DM JID or the group JID. */
|
|
60
|
+
conversationId: string;
|
|
61
|
+
/** The human who sent the message — same as conversationId for a DM, the participant JID for a group. */
|
|
62
|
+
senderId: string;
|
|
63
|
+
kind: WhatsAppSourceKind;
|
|
64
|
+
/** @mentioned JIDs on this message (group messages only; empty for DMs). */
|
|
65
|
+
mentionedJids: string[];
|
|
66
|
+
}
|
|
67
|
+
/** Map a raw Baileys message to {conversationId, senderId, kind, mentionedJids}. */
|
|
68
|
+
export declare function resolveWhatsAppSource(msg: WhatsAppMessageLike | undefined | null): ResolvedWhatsAppSource;
|
|
69
|
+
/** The subset of `whatsapp` config the conversation gate reads. */
|
|
70
|
+
export interface WhatsAppAccessConfig {
|
|
71
|
+
dmPolicy?: 'open' | 'allowlist' | 'disabled';
|
|
72
|
+
dmAllowlist?: string[];
|
|
73
|
+
groupPolicy?: 'open' | 'allowlist' | 'disabled';
|
|
74
|
+
groupAllowlist?: string[];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Gate an already-resolved WhatsApp source. DMs are gated on the sender's
|
|
78
|
+
* JID against `dmPolicy`/`dmAllowlist`; groups are gated on the group JID
|
|
79
|
+
* against `groupPolicy`/`groupAllowlist`, closed by default — same posture
|
|
80
|
+
* as DMs. Unknown source kinds are denied.
|
|
81
|
+
*/
|
|
82
|
+
export declare function isResolvedSourceAllowed(cfg: WhatsAppAccessConfig | undefined, resolved: ResolvedWhatsAppSource): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Convenience wrapper that resolves a raw message and gates it in one call.
|
|
85
|
+
* Equivalent to `isResolvedSourceAllowed(cfg, resolveWhatsAppSource(msg))`.
|
|
86
|
+
*/
|
|
87
|
+
export declare function isWhatsAppConversationAllowed(cfg: WhatsAppAccessConfig | undefined, msg: WhatsAppMessageLike | undefined | null): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* @mention gate for groups (mirrors `slack`/`line`'s `requireMention`,
|
|
90
|
+
* default true — only effective in groups, DMs always pass). Unlike Slack
|
|
91
|
+
* (which infers "was mentioned" from the event *type* being `app_mention`)
|
|
92
|
+
* Baileys hands every message's mentioned-JID list directly on
|
|
93
|
+
* `contextInfo.mentionedJid` — no separate text-scanning heuristic needed,
|
|
94
|
+
* just check one of the bot's own JIDs is in that list.
|
|
95
|
+
*
|
|
96
|
+
* Under WhatsApp's Linked ID (LID) privacy system a group can report the
|
|
97
|
+
* mentioned bot using its `@lid` identity even though `sock.user.id` (the
|
|
98
|
+
* phone-number JID we log in with) is the classic `@s.whatsapp.net` form —
|
|
99
|
+
* two different strings for the same bot. Pass `botLid` (`sock.user.lid`)
|
|
100
|
+
* alongside `botJid` (`sock.user.id`) so either form matches; omit it if
|
|
101
|
+
* unknown (older Baileys / non-LID accounts) and only `botJid` is checked.
|
|
102
|
+
*/
|
|
103
|
+
export declare function wasBotMentioned(mentionedJids: string[], botJid: string | undefined, botLid?: string | null): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Canonical form of a WhatsApp JID for equality checks.
|
|
106
|
+
*
|
|
107
|
+
* WhatsApp JIDs occasionally carry a ":<device>" suffix on the *user* part
|
|
108
|
+
* (multi-device, e.g. "66811112222:5@s.whatsapp.net") — strip only that
|
|
109
|
+
* segment, not everything after the first colon, or the "@server" half gets
|
|
110
|
+
* dropped too and a same-user-different-device JID never matches at all
|
|
111
|
+
* (splitting on ':' before splitting on '@' would do exactly that). The
|
|
112
|
+
* `@lid` vs `@s.whatsapp.net` distinction is preserved: those are genuinely
|
|
113
|
+
* different addresses, and callers that must accept either (wasBotMentioned)
|
|
114
|
+
* normalize both forms and compare against the set.
|
|
115
|
+
*
|
|
116
|
+
* Shared by the INBOUND mention gate (wasBotMentioned above) and the OUTBOUND
|
|
117
|
+
* `@digits` → `mentions:[jid]` matcher in WhatsAppManager, so both sides agree
|
|
118
|
+
* on what "the same participant" means.
|
|
119
|
+
*/
|
|
120
|
+
export declare function normalizeWhatsAppJid(jid: string): string;
|
|
121
|
+
/** The user half of a normalized JID — "66811112222" from "66811112222:5@s.whatsapp.net". */
|
|
122
|
+
export declare function whatsAppJidUser(jid: string): string;
|
|
123
|
+
//# sourceMappingURL=whatsapp-access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whatsapp-access.d.ts","sourceRoot":"","sources":["../../src/api/whatsapp-access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,EACrD,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAC/B,EAAE,EAAE,MAAM,GACT,OAAO,CAKT;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,EAAE;QACJ,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,iFAAiF;QACjF,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B;;;;WAIG;QACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B;;;;WAIG;QACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;KACzB,GAAG,IAAI,CAAC;IACT,OAAO,CAAC,EAAE;QACR,uFAAuF;QACvF,mBAAmB,CAAC,EAAE;YAAE,WAAW,CAAC,EAAE;gBAAE,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;aAAE,GAAG,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;QACzF,YAAY,CAAC,EAAE;YAAE,WAAW,CAAC,EAAE;gBAAE,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;aAAE,GAAG,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;KACnF,GAAG,IAAI,CAAC;CACV;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;AAE5D,MAAM,WAAW,sBAAsB;IACrC,wEAAwE;IACxE,cAAc,EAAE,MAAM,CAAC;IACvB,yGAAyG;IACzG,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,kBAAkB,CAAC;IACzB,4EAA4E;IAC5E,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,mBAAmB,GAAG,SAAS,GAAG,IAAI,GAAG,sBAAsB,CA+BzG;AAED,mEAAmE;AACnE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAC;IAChD,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,oBAAoB,GAAG,SAAS,EACrC,QAAQ,EAAE,sBAAsB,GAC/B,OAAO,CAST;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,oBAAoB,GAAG,SAAS,EACrC,GAAG,EAAE,mBAAmB,GAAG,SAAS,GAAG,IAAI,GAC1C,OAAO,CAET;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAC7B,aAAa,EAAE,MAAM,EAAE,EACvB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,OAAO,CAIT;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isWhatsAppSenderAllowed = isWhatsAppSenderAllowed;
|
|
4
|
+
exports.resolveWhatsAppSource = resolveWhatsAppSource;
|
|
5
|
+
exports.isResolvedSourceAllowed = isResolvedSourceAllowed;
|
|
6
|
+
exports.isWhatsAppConversationAllowed = isWhatsAppConversationAllowed;
|
|
7
|
+
exports.wasBotMentioned = wasBotMentioned;
|
|
8
|
+
exports.normalizeWhatsAppJid = normalizeWhatsAppJid;
|
|
9
|
+
exports.whatsAppJidUser = whatsAppJidUser;
|
|
10
|
+
/**
|
|
11
|
+
* WhatsApp DM/group access control — pure, stateless sender gate.
|
|
12
|
+
*
|
|
13
|
+
* Structural port of `slack-access.ts` (same closed-by-default posture,
|
|
14
|
+
* same two-tier dmPolicy/groupPolicy shape — Slack is the right template
|
|
15
|
+
* here, not the DM-only `sms-access.ts`, because WhatsApp has real groups).
|
|
16
|
+
* The manager reads `whatsapp` config directly and calls this once per
|
|
17
|
+
* inbound message — no env plumbing, no state files.
|
|
18
|
+
*
|
|
19
|
+
* Default (policy absent) is CLOSED: only ids in the allowlist pass. Set
|
|
20
|
+
* `dmPolicy`/`groupPolicy`: 'open' to restore "reply to anyone" behavior.
|
|
21
|
+
*
|
|
22
|
+
* Allowlist entries MUST be WhatsApp JIDs (e.g. "66812345678@s.whatsapp.net"
|
|
23
|
+
* for a DM sender, "1234567890-1234567890@g.us" for a group) — never a
|
|
24
|
+
* bare phone number or a group's display name (renamable/ambiguous, same
|
|
25
|
+
* footgun Slack/LINE's own doc comments call out).
|
|
26
|
+
*/
|
|
27
|
+
function isWhatsAppSenderAllowed(policy, allowlist, id) {
|
|
28
|
+
if (policy === 'open')
|
|
29
|
+
return true;
|
|
30
|
+
if (policy === 'disabled')
|
|
31
|
+
return false;
|
|
32
|
+
// 'allowlist' OR undefined (closed default) → only listed ids pass.
|
|
33
|
+
return !!id && (allowlist ?? []).includes(id);
|
|
34
|
+
}
|
|
35
|
+
/** Map a raw Baileys message to {conversationId, senderId, kind, mentionedJids}. */
|
|
36
|
+
function resolveWhatsAppSource(msg) {
|
|
37
|
+
const remoteJid = msg?.key?.remoteJid ?? '';
|
|
38
|
+
if (!remoteJid)
|
|
39
|
+
return { conversationId: '', senderId: '', kind: 'other', mentionedJids: [] };
|
|
40
|
+
const mentionedJids = msg?.message?.extendedTextMessage?.contextInfo?.mentionedJid ??
|
|
41
|
+
msg?.message?.imageMessage?.contextInfo?.mentionedJid ??
|
|
42
|
+
[];
|
|
43
|
+
if (remoteJid.endsWith('@g.us')) {
|
|
44
|
+
// Group: the sender is `participant`, the conversation is the group JID itself.
|
|
45
|
+
// `participant` arrives as a `@lid` address (not the classic phone JID)
|
|
46
|
+
// for some senders under WhatsApp's Linked ID privacy system — prefer
|
|
47
|
+
// `participantPn` (the real phone-number JID) when Baileys supplies it.
|
|
48
|
+
const participant = msg?.key?.participantPn || msg?.key?.participant || '';
|
|
49
|
+
if (!participant)
|
|
50
|
+
return { conversationId: '', senderId: '', kind: 'other', mentionedJids: [] };
|
|
51
|
+
return { conversationId: remoteJid, senderId: participant, kind: 'group', mentionedJids };
|
|
52
|
+
}
|
|
53
|
+
if (remoteJid.endsWith('@s.whatsapp.net') || remoteJid.endsWith('@lid')) {
|
|
54
|
+
// DM: sender and conversation are (normally) the same JID. Under
|
|
55
|
+
// WhatsApp's Linked ID (LID) privacy system, `remoteJid` itself can
|
|
56
|
+
// arrive as `<id>@lid` instead of the classic `<phone>@s.whatsapp.net`
|
|
57
|
+
// — reply on whichever JID we actually received the message on
|
|
58
|
+
// (`remoteJid`), but gate on the real phone-number JID (`senderPn`, when
|
|
59
|
+
// Baileys supplies it): allowlists are documented and configured in
|
|
60
|
+
// phone-number JID form and would never match a bare `@lid`.
|
|
61
|
+
const senderId = msg?.key?.senderPn || remoteJid;
|
|
62
|
+
return { conversationId: remoteJid, senderId, kind: 'user', mentionedJids: [] };
|
|
63
|
+
}
|
|
64
|
+
// Broadcast lists, status updates, newsletters, etc. — not a supported source.
|
|
65
|
+
return { conversationId: '', senderId: '', kind: 'other', mentionedJids: [] };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Gate an already-resolved WhatsApp source. DMs are gated on the sender's
|
|
69
|
+
* JID against `dmPolicy`/`dmAllowlist`; groups are gated on the group JID
|
|
70
|
+
* against `groupPolicy`/`groupAllowlist`, closed by default — same posture
|
|
71
|
+
* as DMs. Unknown source kinds are denied.
|
|
72
|
+
*/
|
|
73
|
+
function isResolvedSourceAllowed(cfg, resolved) {
|
|
74
|
+
const { conversationId, senderId, kind } = resolved;
|
|
75
|
+
if (kind === 'user') {
|
|
76
|
+
return isWhatsAppSenderAllowed(cfg?.dmPolicy, cfg?.dmAllowlist, senderId || conversationId);
|
|
77
|
+
}
|
|
78
|
+
if (kind === 'group') {
|
|
79
|
+
return isWhatsAppSenderAllowed(cfg?.groupPolicy, cfg?.groupAllowlist, conversationId);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Convenience wrapper that resolves a raw message and gates it in one call.
|
|
85
|
+
* Equivalent to `isResolvedSourceAllowed(cfg, resolveWhatsAppSource(msg))`.
|
|
86
|
+
*/
|
|
87
|
+
function isWhatsAppConversationAllowed(cfg, msg) {
|
|
88
|
+
return isResolvedSourceAllowed(cfg, resolveWhatsAppSource(msg));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* @mention gate for groups (mirrors `slack`/`line`'s `requireMention`,
|
|
92
|
+
* default true — only effective in groups, DMs always pass). Unlike Slack
|
|
93
|
+
* (which infers "was mentioned" from the event *type* being `app_mention`)
|
|
94
|
+
* Baileys hands every message's mentioned-JID list directly on
|
|
95
|
+
* `contextInfo.mentionedJid` — no separate text-scanning heuristic needed,
|
|
96
|
+
* just check one of the bot's own JIDs is in that list.
|
|
97
|
+
*
|
|
98
|
+
* Under WhatsApp's Linked ID (LID) privacy system a group can report the
|
|
99
|
+
* mentioned bot using its `@lid` identity even though `sock.user.id` (the
|
|
100
|
+
* phone-number JID we log in with) is the classic `@s.whatsapp.net` form —
|
|
101
|
+
* two different strings for the same bot. Pass `botLid` (`sock.user.lid`)
|
|
102
|
+
* alongside `botJid` (`sock.user.id`) so either form matches; omit it if
|
|
103
|
+
* unknown (older Baileys / non-LID accounts) and only `botJid` is checked.
|
|
104
|
+
*/
|
|
105
|
+
function wasBotMentioned(mentionedJids, botJid, botLid) {
|
|
106
|
+
if (!botJid && !botLid)
|
|
107
|
+
return false;
|
|
108
|
+
const botIdentities = [botJid, botLid].filter((v) => !!v).map(normalizeWhatsAppJid);
|
|
109
|
+
return mentionedJids.some((jid) => botIdentities.includes(normalizeWhatsAppJid(jid)));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Canonical form of a WhatsApp JID for equality checks.
|
|
113
|
+
*
|
|
114
|
+
* WhatsApp JIDs occasionally carry a ":<device>" suffix on the *user* part
|
|
115
|
+
* (multi-device, e.g. "66811112222:5@s.whatsapp.net") — strip only that
|
|
116
|
+
* segment, not everything after the first colon, or the "@server" half gets
|
|
117
|
+
* dropped too and a same-user-different-device JID never matches at all
|
|
118
|
+
* (splitting on ':' before splitting on '@' would do exactly that). The
|
|
119
|
+
* `@lid` vs `@s.whatsapp.net` distinction is preserved: those are genuinely
|
|
120
|
+
* different addresses, and callers that must accept either (wasBotMentioned)
|
|
121
|
+
* normalize both forms and compare against the set.
|
|
122
|
+
*
|
|
123
|
+
* Shared by the INBOUND mention gate (wasBotMentioned above) and the OUTBOUND
|
|
124
|
+
* `@digits` → `mentions:[jid]` matcher in WhatsAppManager, so both sides agree
|
|
125
|
+
* on what "the same participant" means.
|
|
126
|
+
*/
|
|
127
|
+
function normalizeWhatsAppJid(jid) {
|
|
128
|
+
const [userPart, server] = jid.split('@');
|
|
129
|
+
return server ? `${userPart.split(':')[0]}@${server}` : userPart.split(':')[0];
|
|
130
|
+
}
|
|
131
|
+
/** The user half of a normalized JID — "66811112222" from "66811112222:5@s.whatsapp.net". */
|
|
132
|
+
function whatsAppJidUser(jid) {
|
|
133
|
+
return normalizeWhatsAppJid(jid).split('@')[0] ?? '';
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=whatsapp-access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whatsapp-access.js","sourceRoot":"","sources":["../../src/api/whatsapp-access.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp Business Cloud API DM access control — pure, stateless sender gate.
|
|
3
|
+
*
|
|
4
|
+
* Structural port of `whatsapp-access.ts` (the Baileys sibling), simplified:
|
|
5
|
+
* the Cloud API has no group concept (a Business phone number cannot be added
|
|
6
|
+
* to a group the way a personal/linked number can — every inbound message is
|
|
7
|
+
* a 1:1 conversation with the business), so there is no group tier here at
|
|
8
|
+
* all — `kind` is `'user' | 'other'`, not `'user' | 'group' | 'other'`.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ FOOTGUN: Cloud API's inbound `from` is a BARE PHONE-NUMBER STRING (e.g.
|
|
11
|
+
* `"66812345678"` — digits only, no `+`, no `@s.whatsapp.net` suffix), NOT a
|
|
12
|
+
* JID like Baileys hands `whatsapp-access.ts`. `dmAllowlist` entries here
|
|
13
|
+
* MUST be bare digits — pasting a JID (`"66812345678@s.whatsapp.net"`) or a
|
|
14
|
+
* `+`-prefixed E.164 string will silently never match. This is the exact
|
|
15
|
+
* same class of footgun Slack/LINE's own doc comments call out for their
|
|
16
|
+
* allowlists, just with a different wrong shape (JID/`+` instead of a name).
|
|
17
|
+
*
|
|
18
|
+
* Default (policy absent) is CLOSED: only ids in the allowlist pass. Set
|
|
19
|
+
* `dmPolicy: 'open'` to restore "reply to anyone" behavior.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isWhatsAppCloudSenderAllowed(policy: 'open' | 'allowlist' | 'disabled' | undefined, allowlist: string[] | undefined, id: string): boolean;
|
|
22
|
+
export type WhatsAppCloudSourceKind = 'user' | 'other';
|
|
23
|
+
export interface ResolvedWhatsAppCloudSource {
|
|
24
|
+
/** Conversation key — the reply target: the sender's bare phone-number string. */
|
|
25
|
+
conversationId: string;
|
|
26
|
+
/** The human who sent the message — always the same as conversationId (no groups). */
|
|
27
|
+
senderId: string;
|
|
28
|
+
kind: WhatsAppCloudSourceKind;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Map a raw inbound Cloud API `from` field to {conversationId, senderId, kind}.
|
|
32
|
+
* `from` is always a bare phone-number string on a real webhook payload —
|
|
33
|
+
* empty/missing is the only "other" case, since there is no group tier to
|
|
34
|
+
* misclassify into.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveWhatsAppCloudSource(from: string | undefined | null): ResolvedWhatsAppCloudSource;
|
|
37
|
+
/** The subset of `whatsapp_cloud` config the conversation gate reads. */
|
|
38
|
+
export interface WhatsAppCloudAccessConfig {
|
|
39
|
+
dmPolicy?: 'open' | 'allowlist' | 'disabled';
|
|
40
|
+
dmAllowlist?: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Gate an already-resolved WhatsApp Cloud source. Every source is a DM
|
|
44
|
+
* (`kind === 'user'`), gated on the sender's bare phone number against
|
|
45
|
+
* `dmPolicy`/`dmAllowlist`. Unknown/empty source kinds are denied.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isResolvedWhatsAppCloudSourceAllowed(cfg: WhatsAppCloudAccessConfig | undefined, resolved: ResolvedWhatsAppCloudSource): boolean;
|
|
48
|
+
//# sourceMappingURL=whatsapp-cloud-access.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whatsapp-cloud-access.d.ts","sourceRoot":"","sources":["../../src/api/whatsapp-cloud-access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,EACrD,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAC/B,EAAE,EAAE,MAAM,GACT,OAAO,CAKT;AAED,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEvD,MAAM,WAAW,2BAA2B;IAC1C,kFAAkF;IAClF,cAAc,EAAE,MAAM,CAAC;IACvB,sFAAsF;IACtF,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,uBAAuB,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,2BAA2B,CAIvG;AAED,yEAAyE;AACzE,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,oCAAoC,CAClD,GAAG,EAAE,yBAAyB,GAAG,SAAS,EAC1C,QAAQ,EAAE,2BAA2B,GACpC,OAAO,CAMT"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isWhatsAppCloudSenderAllowed = isWhatsAppCloudSenderAllowed;
|
|
4
|
+
exports.resolveWhatsAppCloudSource = resolveWhatsAppCloudSource;
|
|
5
|
+
exports.isResolvedWhatsAppCloudSourceAllowed = isResolvedWhatsAppCloudSourceAllowed;
|
|
6
|
+
/**
|
|
7
|
+
* WhatsApp Business Cloud API DM access control — pure, stateless sender gate.
|
|
8
|
+
*
|
|
9
|
+
* Structural port of `whatsapp-access.ts` (the Baileys sibling), simplified:
|
|
10
|
+
* the Cloud API has no group concept (a Business phone number cannot be added
|
|
11
|
+
* to a group the way a personal/linked number can — every inbound message is
|
|
12
|
+
* a 1:1 conversation with the business), so there is no group tier here at
|
|
13
|
+
* all — `kind` is `'user' | 'other'`, not `'user' | 'group' | 'other'`.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ FOOTGUN: Cloud API's inbound `from` is a BARE PHONE-NUMBER STRING (e.g.
|
|
16
|
+
* `"66812345678"` — digits only, no `+`, no `@s.whatsapp.net` suffix), NOT a
|
|
17
|
+
* JID like Baileys hands `whatsapp-access.ts`. `dmAllowlist` entries here
|
|
18
|
+
* MUST be bare digits — pasting a JID (`"66812345678@s.whatsapp.net"`) or a
|
|
19
|
+
* `+`-prefixed E.164 string will silently never match. This is the exact
|
|
20
|
+
* same class of footgun Slack/LINE's own doc comments call out for their
|
|
21
|
+
* allowlists, just with a different wrong shape (JID/`+` instead of a name).
|
|
22
|
+
*
|
|
23
|
+
* Default (policy absent) is CLOSED: only ids in the allowlist pass. Set
|
|
24
|
+
* `dmPolicy: 'open'` to restore "reply to anyone" behavior.
|
|
25
|
+
*/
|
|
26
|
+
function isWhatsAppCloudSenderAllowed(policy, allowlist, id) {
|
|
27
|
+
if (policy === 'open')
|
|
28
|
+
return true;
|
|
29
|
+
if (policy === 'disabled')
|
|
30
|
+
return false;
|
|
31
|
+
// 'allowlist' OR undefined (closed default) → only listed ids pass.
|
|
32
|
+
return !!id && (allowlist ?? []).includes(id);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Map a raw inbound Cloud API `from` field to {conversationId, senderId, kind}.
|
|
36
|
+
* `from` is always a bare phone-number string on a real webhook payload —
|
|
37
|
+
* empty/missing is the only "other" case, since there is no group tier to
|
|
38
|
+
* misclassify into.
|
|
39
|
+
*/
|
|
40
|
+
function resolveWhatsAppCloudSource(from) {
|
|
41
|
+
const id = from ?? '';
|
|
42
|
+
if (!id)
|
|
43
|
+
return { conversationId: '', senderId: '', kind: 'other' };
|
|
44
|
+
return { conversationId: id, senderId: id, kind: 'user' };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Gate an already-resolved WhatsApp Cloud source. Every source is a DM
|
|
48
|
+
* (`kind === 'user'`), gated on the sender's bare phone number against
|
|
49
|
+
* `dmPolicy`/`dmAllowlist`. Unknown/empty source kinds are denied.
|
|
50
|
+
*/
|
|
51
|
+
function isResolvedWhatsAppCloudSourceAllowed(cfg, resolved) {
|
|
52
|
+
const { conversationId, senderId, kind } = resolved;
|
|
53
|
+
if (kind === 'user') {
|
|
54
|
+
return isWhatsAppCloudSenderAllowed(cfg?.dmPolicy, cfg?.dmAllowlist, senderId || conversationId);
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=whatsapp-cloud-access.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whatsapp-cloud-access.js","sourceRoot":"","sources":["../../src/api/whatsapp-cloud-access.ts"],"names":[],"mappings":";;AAoBA,oEASC;AAkBD,gEAIC;AAaD,oFASC;AAzED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAgB,4BAA4B,CAC1C,MAAqD,EACrD,SAA+B,EAC/B,EAAU;IAEV,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,MAAM,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IACxC,oEAAoE;IACpE,OAAO,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAChD,CAAC;AAYD;;;;;GAKG;AACH,SAAgB,0BAA0B,CAAC,IAA+B;IACxE,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC;IACtB,IAAI,CAAC,EAAE;QAAE,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IACpE,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC5D,CAAC;AAQD;;;;GAIG;AACH,SAAgB,oCAAoC,CAClD,GAA0C,EAC1C,QAAqC;IAErC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC;IACpD,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,4BAA4B,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,IAAI,cAAc,CAAC,CAAC;IACnG,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { WHATSAPP_ACK_EMOJI } from '../shared/whatsapp-ack';
|
|
2
|
+
/**
|
|
3
|
+
* Cloud API hard limit for a text message body. Longer replies are split
|
|
4
|
+
* across several messages rather than being rejected by Meta (error 131009)
|
|
5
|
+
* or silently truncated.
|
|
6
|
+
*/
|
|
7
|
+
export declare const WHATSAPP_CLOUD_MAX_TEXT_CHARS = 4096;
|
|
8
|
+
/**
|
|
9
|
+
* Meta's hard cap on reply buttons in an `interactive: {type: 'button'}`
|
|
10
|
+
* message. Not a soft/UX limit — the Graph API rejects a 4th button outright,
|
|
11
|
+
* so `sendInteractiveButtons` refuses locally with a readable message rather
|
|
12
|
+
* than letting the agent see an opaque Meta error code.
|
|
13
|
+
*/
|
|
14
|
+
export declare const WHATSAPP_CLOUD_MAX_BUTTONS = 3;
|
|
15
|
+
/** A single reply button: `id` is what comes back on tap, `title` is what the user sees. */
|
|
16
|
+
export interface WhatsAppCloudButton {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
}
|
|
20
|
+
/** One row of a list message. `description` is the optional grey sub-line. */
|
|
21
|
+
export interface WhatsAppCloudListRow {
|
|
22
|
+
id: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
/** A list message's section — a titled group of rows. */
|
|
27
|
+
export interface WhatsAppCloudListSection {
|
|
28
|
+
title: string;
|
|
29
|
+
rows: WhatsAppCloudListRow[];
|
|
30
|
+
}
|
|
31
|
+
export { WHATSAPP_ACK_EMOJI };
|
|
32
|
+
export interface WhatsAppCloudClientOptions {
|
|
33
|
+
accessToken: string;
|
|
34
|
+
phoneNumberId: string;
|
|
35
|
+
logDir: string;
|
|
36
|
+
/** Test-only override for the Graph API base URL. Production uses the real default. */
|
|
37
|
+
apiBase?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface WhatsAppCloudApiResponse {
|
|
40
|
+
error?: {
|
|
41
|
+
message?: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
code?: number;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
};
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
export declare class WhatsAppCloudClient {
|
|
49
|
+
private readonly accessToken;
|
|
50
|
+
private readonly phoneNumberId;
|
|
51
|
+
private readonly apiBase;
|
|
52
|
+
private readonly logger;
|
|
53
|
+
constructor(opts: WhatsAppCloudClientOptions);
|
|
54
|
+
private call;
|
|
55
|
+
/**
|
|
56
|
+
* Verify the token + phone number id — used both as the connect flow's
|
|
57
|
+
* "Save"-time check (router.ts) and reused here for any other caller that
|
|
58
|
+
* wants a live credential check without duplicating the request shape.
|
|
59
|
+
*/
|
|
60
|
+
verifyCredentials(): Promise<{
|
|
61
|
+
ok: boolean;
|
|
62
|
+
error?: string;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Send a plain text message, splitting anything over the Cloud API's
|
|
66
|
+
* 4096-char body limit into several messages (one Graph API call per chunk,
|
|
67
|
+
* sent in order).
|
|
68
|
+
*
|
|
69
|
+
* `quotedMessageId` makes the FIRST chunk a quote-reply to that inbound
|
|
70
|
+
* message (Meta's outbound `context: {message_id}`); follow-on chunks are
|
|
71
|
+
* plain so the conversation doesn't show the same quote block repeated.
|
|
72
|
+
*
|
|
73
|
+
* Returns the first errored response if any chunk failed (so existing
|
|
74
|
+
* `if (sent.error)` callers still see the failure), otherwise the last
|
|
75
|
+
* successful one.
|
|
76
|
+
*/
|
|
77
|
+
sendText(to: string, body: string, quotedMessageId?: string): Promise<WhatsAppCloudApiResponse>;
|
|
78
|
+
/**
|
|
79
|
+
* Send up to 3 tappable reply buttons under a body of text (Phase 3).
|
|
80
|
+
*
|
|
81
|
+
* A tap comes back through the webhook as `type: 'interactive'` with a
|
|
82
|
+
* `button_reply` — the inbound normalizer turns that into plain text
|
|
83
|
+
* (the button's title), so the agent reads a tap exactly as if the user had
|
|
84
|
+
* typed the label. See normalizeWhatsAppCloudMessage's interactive branch.
|
|
85
|
+
*
|
|
86
|
+
* THROWS (rather than returning an `{error}` response) when more than
|
|
87
|
+
* WHATSAPP_CLOUD_MAX_BUTTONS are passed or the list is empty: both are
|
|
88
|
+
* caller bugs that Meta would reject anyway, and a thrown, readable message
|
|
89
|
+
* reaches the agent through the MCP tool's catch as actionable text.
|
|
90
|
+
*/
|
|
91
|
+
sendInteractiveButtons(to: string, bodyText: string, buttons: WhatsAppCloudButton[]): Promise<WhatsAppCloudApiResponse>;
|
|
92
|
+
/**
|
|
93
|
+
* Send a list message — a single button that opens a picker of grouped rows
|
|
94
|
+
* (Phase 3). The way past the 3-button ceiling above.
|
|
95
|
+
*
|
|
96
|
+
* A pick comes back as `type: 'interactive'` with a `list_reply`, handled by
|
|
97
|
+
* the same normalizer branch as button taps.
|
|
98
|
+
*/
|
|
99
|
+
sendInteractiveList(to: string, bodyText: string, buttonLabel: string, sections: WhatsAppCloudListSection[]): Promise<WhatsAppCloudApiResponse>;
|
|
100
|
+
/**
|
|
101
|
+
* Send a pre-approved message template (Phase 3) — the ONLY way to open a
|
|
102
|
+
* conversation outside WhatsApp's 24h customer-service window, which is why
|
|
103
|
+
* the caller must be gated on `whatsapp_cloud.templatesEnabled` (see
|
|
104
|
+
* mcp/tools/whatsapp-cloud/module.ts; this method itself does not gate).
|
|
105
|
+
*
|
|
106
|
+
* `components` is Meta's variable-substitution array. It is passed through
|
|
107
|
+
* VERBATIM and deliberately NOT validated or typed beyond `unknown[]`: its
|
|
108
|
+
* required shape depends on the individual template's definition in Meta's
|
|
109
|
+
* Business Manager, which this gateway has no visibility into.
|
|
110
|
+
*/
|
|
111
|
+
sendTemplate(to: string, templateName: string, languageCode: string, components?: unknown[]): Promise<WhatsAppCloudApiResponse>;
|
|
112
|
+
/**
|
|
113
|
+
* Mark an inbound message as read (the blue double-tick on the sender's
|
|
114
|
+
* side). Best-effort: logged, never thrown — a read receipt is UX polish,
|
|
115
|
+
* not correctness-critical, exactly like Slack's ack reaction.
|
|
116
|
+
*/
|
|
117
|
+
markAsRead(messageId: string): Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* Ack-reaction (ported from `slack-client.ts`'s addReaction): added to the
|
|
120
|
+
* inbound message on receipt, cleared once the agent's reply lands. An
|
|
121
|
+
* EMPTY `emoji` is how the Cloud API removes an existing reaction — see
|
|
122
|
+
* removeReaction below. Best-effort, same as markAsRead.
|
|
123
|
+
*/
|
|
124
|
+
sendReaction(to: string, messageId: string, emoji?: string): Promise<void>;
|
|
125
|
+
/** Clear a reaction — the same endpoint with an empty-string emoji. */
|
|
126
|
+
removeReaction(to: string, messageId: string): Promise<void>;
|
|
127
|
+
/**
|
|
128
|
+
* Upload a local file to the Cloud API's media store, returning its
|
|
129
|
+
* `media_id` — a prerequisite step before `sendImage`/`sendDocument`
|
|
130
|
+
* (the Cloud API sends media by id, not by raw bytes or URL, for
|
|
131
|
+
* gateway-originated files).
|
|
132
|
+
*/
|
|
133
|
+
uploadMedia(filePath: string, mimeType: string): Promise<{
|
|
134
|
+
mediaId: string;
|
|
135
|
+
} | {
|
|
136
|
+
error: string;
|
|
137
|
+
}>;
|
|
138
|
+
/** Send an already-uploaded image by media id, with an optional caption. */
|
|
139
|
+
sendImage(to: string, mediaId: string, caption?: string): Promise<WhatsAppCloudApiResponse>;
|
|
140
|
+
/** Send an already-uploaded document by media id, with a filename and optional caption. */
|
|
141
|
+
sendDocument(to: string, mediaId: string, filename: string, caption?: string): Promise<WhatsAppCloudApiResponse>;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=whatsapp-cloud-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whatsapp-cloud-client.d.ts","sourceRoot":"","sources":["../../src/api/whatsapp-cloud-client.ts"],"names":[],"mappings":"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"}
|