@glitchgrab/whatsapp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -0
- package/dist/index.d.mts +280 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.js +330 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +299 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react.d.mts +52 -0
- package/dist/react.d.ts +52 -0
- package/dist/react.js +462 -0
- package/dist/react.js.map +1 -0
- package/dist/react.mjs +435 -0
- package/dist/react.mjs.map +1 -0
- package/dist/types-BKOEzRzb.d.mts +97 -0
- package/dist/types-BKOEzRzb.d.ts +97 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# @glitchgrab/whatsapp
|
|
2
|
+
|
|
3
|
+
WhatsApp Business messaging for SaaS products, without the Meta integration.
|
|
4
|
+
|
|
5
|
+
Your business owners connect **their own** WhatsApp number, so messages arrive
|
|
6
|
+
under *their* verified name — not yours. You get templates, sending, a shared
|
|
7
|
+
inbox, autoreply and per-owner billing; you never touch a WABA id, a phone number
|
|
8
|
+
id, or a Meta access token.
|
|
9
|
+
|
|
10
|
+
This is not the `glitchgrab` package (that one files GitHub issues). Different
|
|
11
|
+
product, different key.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun add @glitchgrab/whatsapp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## The one rule
|
|
20
|
+
|
|
21
|
+
**`WhatsappClient` holds your platform API key and must only run on a server.**
|
|
22
|
+
That key reaches every one of your customers' numbers. The package logs an error
|
|
23
|
+
if it detects a browser, but the fix is architectural: keep it in route handlers
|
|
24
|
+
and server actions, and let `createInboxHandler` serve the UI.
|
|
25
|
+
|
|
26
|
+
`ownerId` throughout is *your* id for the business owner — a library id, a clinic
|
|
27
|
+
id. We map it to a tenant on our side.
|
|
28
|
+
|
|
29
|
+
## Connect an owner's number
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// server
|
|
33
|
+
const client = createWhatsappClient({ apiKey: process.env.GG_WA_KEY! });
|
|
34
|
+
const config = await client.connect({ ownerId: library.id, name: library.name });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// browser — Meta's JS SDK must already be on the page
|
|
39
|
+
const { code, state } = await launchSignup(config);
|
|
40
|
+
await fetch("/api/whatsapp/complete", { method: "POST", body: JSON.stringify({ code, state }) });
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// server, in that route
|
|
45
|
+
await client.completeConnect({ ownerId: library.id, code, state });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The owner picks or creates their WhatsApp Business Account inside Meta's popup
|
|
49
|
+
and verifies their own number. Nothing is connected until `completeConnect`
|
|
50
|
+
returns — and check `warnings` on the result, which reports things that are worth
|
|
51
|
+
surfacing but are not failures (no number added yet, webhook subscription
|
|
52
|
+
refused).
|
|
53
|
+
|
|
54
|
+
## Send
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
await client.send({
|
|
58
|
+
ownerId: library.id,
|
|
59
|
+
to: student.phone,
|
|
60
|
+
template: "fee_due",
|
|
61
|
+
components: [{ type: "body", parameters: [{ type: "text", text: "₹500" }] }],
|
|
62
|
+
refKey: `fee-${invoice.id}`, // a retry with the same key never charges twice
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Free-form text (`body` instead of `template`) is legal only within 24 hours of
|
|
67
|
+
the contact's last inbound message. Outside that window this throws — Meta
|
|
68
|
+
answers 200 and delivers nothing, so failing loudly is the point.
|
|
69
|
+
|
|
70
|
+
Out of balance throws with `code: "INSUFFICIENT_FUNDS"` and a `detail.shortfallPaise`.
|
|
71
|
+
|
|
72
|
+
## Shared inbox
|
|
73
|
+
|
|
74
|
+
One route:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// app/api/whatsapp/[...action]/route.ts
|
|
78
|
+
import { createWhatsappClient, createInboxHandler } from "@glitchgrab/whatsapp";
|
|
79
|
+
|
|
80
|
+
const client = createWhatsappClient({ apiKey: process.env.GG_WA_KEY! });
|
|
81
|
+
|
|
82
|
+
const handler = createInboxHandler({
|
|
83
|
+
client,
|
|
84
|
+
// Derive the owner from YOUR session. Never from the request body — that
|
|
85
|
+
// would let any signed-in user read any other owner's WhatsApp.
|
|
86
|
+
resolveOwnerId: async () => (await auth()).user.libraryId,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export const GET = handler;
|
|
90
|
+
export const POST = handler;
|
|
91
|
+
export const PATCH = handler;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
One component:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { WhatsappInbox } from "@glitchgrab/whatsapp/react";
|
|
98
|
+
|
|
99
|
+
<WhatsappInbox api="/api/whatsapp" height={640} />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Live updates arrive over SSE. The component mints a short-lived ticket through
|
|
103
|
+
your route, so your API key never reaches the browser, and reconnects on its own
|
|
104
|
+
when the stream is closed or the ticket expires.
|
|
105
|
+
|
|
106
|
+
Restyle with CSS custom properties — `--gg-wa-accent`, `--gg-wa-bg`,
|
|
107
|
+
`--gg-wa-bubble-out`, `--gg-wa-border`, `--gg-wa-text`, `--gg-wa-muted`,
|
|
108
|
+
`--gg-wa-panel`, `--gg-wa-danger`. For a different layout, use the `useInbox`
|
|
109
|
+
hook directly and render your own.
|
|
110
|
+
|
|
111
|
+
## Billing
|
|
112
|
+
|
|
113
|
+
Prepaid, per owner. Collect payment however you already do, then:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
await client.credit({ ownerId: library.id, amountPaise: 50_000, refKey: payment.id });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
We hold the ledger; we never hold your customers' money. Balances are integer
|
|
120
|
+
paise. A send debits before it calls Meta and refunds if Meta refuses, so a
|
|
121
|
+
failed message never costs anyone anything.
|
|
122
|
+
|
|
123
|
+
## Autoreply
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
await client.createAutoreplyRule({
|
|
127
|
+
ownerId: library.id,
|
|
128
|
+
name: "Timings",
|
|
129
|
+
matchType: "CONTAINS",
|
|
130
|
+
pattern: "timing",
|
|
131
|
+
replyText: "We're open 6am–10pm, every day.",
|
|
132
|
+
priority: 10, // lower runs first; first match wins
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Rules never fire for someone who has just asked to stop.
|
|
137
|
+
|
|
138
|
+
## API
|
|
139
|
+
|
|
140
|
+
`connect` · `completeConnect` · `numbers` · `templates` · `saveTemplate` ·
|
|
141
|
+
`submitTemplate` · `syncTemplates` · `send` · `messages` · `conversations` ·
|
|
142
|
+
`conversation` · `updateConversation` · `agents` · `saveAgent` ·
|
|
143
|
+
`autoreplyRules` · `createAutoreplyRule` · `credit` · `balance` ·
|
|
144
|
+
`createInboxSession`
|
|
145
|
+
|
|
146
|
+
Every failure is a `WhatsappError` with a stable `code`. Branch on that, not on
|
|
147
|
+
the message.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { k as WaSignupLaunch, i as WaNumber, l as WaTemplate, m as WaTemplateCategory, j as WaSendResult, f as WaMessage, b as WaConversation, W as WaAgent, e as WaMatchType, a as WaBalance } from './types-BKOEzRzb.mjs';
|
|
2
|
+
export { c as WaConversationStatus, d as WaErrorCode, g as WaMessageDirection, h as WaMessageStatus, n as WaTemplateStatus, o as WhatsappError } from './types-BKOEzRzb.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The server-side client.
|
|
6
|
+
*
|
|
7
|
+
* **This holds your platform API key. It must never run in a browser** — the key
|
|
8
|
+
* is scoped to your whole account, and every one of your customers' numbers is
|
|
9
|
+
* reachable with it. Call it from a route handler or a server action, and give
|
|
10
|
+
* the browser only what it needs. `createInboxSession()` exists precisely so the
|
|
11
|
+
* inbox UI can work without the key ever leaving your server.
|
|
12
|
+
*
|
|
13
|
+
* `ownerId` is *your* user id for the business owner. We map it to a tenant on
|
|
14
|
+
* our side, so you never handle a WABA id, a phone number id, or a Meta token.
|
|
15
|
+
*/
|
|
16
|
+
interface WhatsappClientOptions {
|
|
17
|
+
apiKey: string;
|
|
18
|
+
/** Override for local development against a tunnel. */
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
fetch?: typeof globalThis.fetch;
|
|
21
|
+
}
|
|
22
|
+
declare class WhatsappClient {
|
|
23
|
+
private readonly apiKey;
|
|
24
|
+
private readonly baseUrl;
|
|
25
|
+
private readonly fetchImpl;
|
|
26
|
+
constructor(options: WhatsappClientOptions);
|
|
27
|
+
private request;
|
|
28
|
+
/**
|
|
29
|
+
* Starts Embedded Signup for one of your business owners.
|
|
30
|
+
*
|
|
31
|
+
* Returns config for Meta's JS SDK, not a redirect URL — a plain OAuth
|
|
32
|
+
* redirect yields a token but skips WABA creation, which is the part the owner
|
|
33
|
+
* actually needs. Pass the result to `launchSignup()` in the browser.
|
|
34
|
+
*/
|
|
35
|
+
connect(params: {
|
|
36
|
+
ownerId: string;
|
|
37
|
+
ownerName?: string;
|
|
38
|
+
}): Promise<WaSignupLaunch>;
|
|
39
|
+
/** Exchanges the code Meta's popup returns. Call this from your server. */
|
|
40
|
+
completeConnect(params: {
|
|
41
|
+
ownerId: string;
|
|
42
|
+
code: string;
|
|
43
|
+
state?: string;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
wabaId: string;
|
|
46
|
+
numbers: WaNumber[];
|
|
47
|
+
creditLineShared: boolean;
|
|
48
|
+
warnings: string[];
|
|
49
|
+
}>;
|
|
50
|
+
numbers(params: {
|
|
51
|
+
ownerId: string;
|
|
52
|
+
refresh?: boolean;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
numbers: WaNumber[];
|
|
55
|
+
}>;
|
|
56
|
+
templates(params: {
|
|
57
|
+
ownerId: string;
|
|
58
|
+
status?: string;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
templates: WaTemplate[];
|
|
61
|
+
}>;
|
|
62
|
+
saveTemplate(params: {
|
|
63
|
+
ownerId: string;
|
|
64
|
+
name: string;
|
|
65
|
+
language: string;
|
|
66
|
+
category: WaTemplateCategory;
|
|
67
|
+
components: unknown[];
|
|
68
|
+
}): Promise<{
|
|
69
|
+
template: WaTemplate;
|
|
70
|
+
}>;
|
|
71
|
+
/** Sends a draft to Meta. The verdict arrives asynchronously — poll or wait. */
|
|
72
|
+
submitTemplate(params: {
|
|
73
|
+
ownerId: string;
|
|
74
|
+
templateId: string;
|
|
75
|
+
}): Promise<{
|
|
76
|
+
template: WaTemplate;
|
|
77
|
+
}>;
|
|
78
|
+
/** Reconciles against Meta now, rather than waiting for the hourly sweep. */
|
|
79
|
+
syncTemplates(params: {
|
|
80
|
+
ownerId: string;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
checked: number;
|
|
83
|
+
updated: number;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Sends a message from the owner's own number.
|
|
87
|
+
*
|
|
88
|
+
* With `template`, any time. With `body`, only inside the 24-hour window the
|
|
89
|
+
* contact opened by messaging them — outside it this throws rather than
|
|
90
|
+
* letting Meta accept the send and deliver nothing.
|
|
91
|
+
*
|
|
92
|
+
* Pass `refKey` to make a retry safe: the same key never charges twice.
|
|
93
|
+
*/
|
|
94
|
+
send(params: {
|
|
95
|
+
ownerId: string;
|
|
96
|
+
to: string;
|
|
97
|
+
template?: string;
|
|
98
|
+
language?: string;
|
|
99
|
+
components?: unknown[];
|
|
100
|
+
body?: string;
|
|
101
|
+
refKey?: string;
|
|
102
|
+
}): Promise<WaSendResult>;
|
|
103
|
+
messages(params: {
|
|
104
|
+
ownerId: string;
|
|
105
|
+
contact?: string;
|
|
106
|
+
limit?: number;
|
|
107
|
+
cursor?: string;
|
|
108
|
+
}): Promise<{
|
|
109
|
+
messages: WaMessage[];
|
|
110
|
+
nextCursor: string | null;
|
|
111
|
+
}>;
|
|
112
|
+
conversations(params: {
|
|
113
|
+
ownerId: string;
|
|
114
|
+
status?: string;
|
|
115
|
+
unread?: boolean;
|
|
116
|
+
limit?: number;
|
|
117
|
+
cursor?: string;
|
|
118
|
+
}): Promise<{
|
|
119
|
+
conversations: WaConversation[];
|
|
120
|
+
nextCursor: string | null;
|
|
121
|
+
}>;
|
|
122
|
+
conversation(params: {
|
|
123
|
+
ownerId: string;
|
|
124
|
+
conversationId: string;
|
|
125
|
+
}): Promise<{
|
|
126
|
+
conversation: WaConversation & {
|
|
127
|
+
messages: WaMessage[];
|
|
128
|
+
};
|
|
129
|
+
}>;
|
|
130
|
+
updateConversation(params: {
|
|
131
|
+
ownerId: string;
|
|
132
|
+
conversationId: string;
|
|
133
|
+
status?: "OPEN" | "SNOOZED" | "CLOSED";
|
|
134
|
+
assignedAgentId?: string | null;
|
|
135
|
+
optedOut?: boolean;
|
|
136
|
+
}): Promise<{
|
|
137
|
+
updated: boolean;
|
|
138
|
+
}>;
|
|
139
|
+
agents(params: {
|
|
140
|
+
ownerId: string;
|
|
141
|
+
includeInactive?: boolean;
|
|
142
|
+
}): Promise<{
|
|
143
|
+
agents: WaAgent[];
|
|
144
|
+
}>;
|
|
145
|
+
saveAgent(params: {
|
|
146
|
+
ownerId: string;
|
|
147
|
+
agentId: string;
|
|
148
|
+
name: string;
|
|
149
|
+
email?: string;
|
|
150
|
+
role?: "AGENT" | "ADMIN";
|
|
151
|
+
active?: boolean;
|
|
152
|
+
}): Promise<{
|
|
153
|
+
agent: WaAgent;
|
|
154
|
+
}>;
|
|
155
|
+
autoreplyRules(params: {
|
|
156
|
+
ownerId: string;
|
|
157
|
+
}): Promise<{
|
|
158
|
+
rules: unknown[];
|
|
159
|
+
}>;
|
|
160
|
+
createAutoreplyRule(params: {
|
|
161
|
+
ownerId: string;
|
|
162
|
+
name: string;
|
|
163
|
+
matchType: WaMatchType;
|
|
164
|
+
pattern?: string;
|
|
165
|
+
replyText: string;
|
|
166
|
+
priority?: number;
|
|
167
|
+
}): Promise<{
|
|
168
|
+
rule: unknown;
|
|
169
|
+
}>;
|
|
170
|
+
/**
|
|
171
|
+
* Adds balance for one of your owners, after you have collected the money on
|
|
172
|
+
* your own rails. We hold the ledger; we never hold your customer's funds.
|
|
173
|
+
*/
|
|
174
|
+
credit(params: {
|
|
175
|
+
ownerId: string;
|
|
176
|
+
amountPaise: number;
|
|
177
|
+
refKey?: string;
|
|
178
|
+
note?: string;
|
|
179
|
+
}): Promise<WaBalance>;
|
|
180
|
+
balance(params?: {
|
|
181
|
+
ownerId?: string;
|
|
182
|
+
}): Promise<WaBalance>;
|
|
183
|
+
/**
|
|
184
|
+
* Mints a short-lived, owner-scoped session for the inbox UI.
|
|
185
|
+
*
|
|
186
|
+
* Call this from a server route and return the result to your page. It is
|
|
187
|
+
* what lets `<WhatsappInbox>` talk to us without your API key ever reaching
|
|
188
|
+
* the browser. The ticket lasts sixty seconds; the component refreshes it
|
|
189
|
+
* through the same route on reconnect.
|
|
190
|
+
*/
|
|
191
|
+
createInboxSession(params: {
|
|
192
|
+
ownerId: string;
|
|
193
|
+
}): Promise<{
|
|
194
|
+
ownerId: string;
|
|
195
|
+
ticket: string;
|
|
196
|
+
expiresIn: number;
|
|
197
|
+
baseUrl: string;
|
|
198
|
+
}>;
|
|
199
|
+
}
|
|
200
|
+
declare function createWhatsappClient(options: WhatsappClientOptions): WhatsappClient;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A ready-made proxy route for the inbox UI.
|
|
204
|
+
*
|
|
205
|
+
* The browser must never hold your platform API key, so `<WhatsappInbox>` talks
|
|
206
|
+
* to *your* server, and your server talks to us. This builds that middle layer
|
|
207
|
+
* so you do not have to hand-write five thin fetch wrappers.
|
|
208
|
+
*
|
|
209
|
+
* ```ts
|
|
210
|
+
* // app/api/whatsapp/[...action]/route.ts
|
|
211
|
+
* import { createWhatsappClient, createInboxHandler } from "@glitchgrab/whatsapp";
|
|
212
|
+
*
|
|
213
|
+
* const client = createWhatsappClient({ apiKey: process.env.GG_WA_KEY! });
|
|
214
|
+
*
|
|
215
|
+
* const handler = createInboxHandler({
|
|
216
|
+
* client,
|
|
217
|
+
* // The single most important line here: derive the owner from YOUR session,
|
|
218
|
+
* // never from the request body. Returning a client-supplied id would let any
|
|
219
|
+
* // signed-in user read any other library's WhatsApp.
|
|
220
|
+
* resolveOwnerId: async () => (await auth()).user.libraryId,
|
|
221
|
+
* });
|
|
222
|
+
*
|
|
223
|
+
* export const GET = handler;
|
|
224
|
+
* export const POST = handler;
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
interface InboxHandlerOptions {
|
|
228
|
+
client: WhatsappClient;
|
|
229
|
+
/**
|
|
230
|
+
* Returns the owner id for the current request, from your own auth. Return
|
|
231
|
+
* null to deny.
|
|
232
|
+
*/
|
|
233
|
+
resolveOwnerId: (request: Request) => Promise<string | null> | string | null;
|
|
234
|
+
/** Set false to make the inbox read-only for this route. Default true. */
|
|
235
|
+
allowSend?: boolean;
|
|
236
|
+
}
|
|
237
|
+
declare function createInboxHandler(options: InboxHandlerOptions): (request: Request) => Promise<Response>;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Opens Meta's Embedded Signup popup.
|
|
241
|
+
*
|
|
242
|
+
* Runs in the browser, holds no credentials, and returns the `code` your server
|
|
243
|
+
* exchanges via `client.completeConnect()`. Nothing is connected until that
|
|
244
|
+
* exchange happens.
|
|
245
|
+
*
|
|
246
|
+
* Meta's JS SDK must already be on the page — Embedded Signup only works
|
|
247
|
+
* through `FB.login` with a `config_id`; a plain OAuth redirect gets a token but
|
|
248
|
+
* skips the WABA creation the owner actually needs.
|
|
249
|
+
*/
|
|
250
|
+
interface FacebookSdk {
|
|
251
|
+
init(params: {
|
|
252
|
+
appId: string;
|
|
253
|
+
cookie?: boolean;
|
|
254
|
+
xfbml?: boolean;
|
|
255
|
+
version: string;
|
|
256
|
+
}): void;
|
|
257
|
+
login(callback: (response: {
|
|
258
|
+
authResponse?: {
|
|
259
|
+
code?: string;
|
|
260
|
+
};
|
|
261
|
+
status?: string;
|
|
262
|
+
}) => void, options: {
|
|
263
|
+
config_id: string;
|
|
264
|
+
response_type: string;
|
|
265
|
+
override_default_response_type: boolean;
|
|
266
|
+
extras?: Record<string, unknown>;
|
|
267
|
+
}): void;
|
|
268
|
+
}
|
|
269
|
+
declare global {
|
|
270
|
+
interface Window {
|
|
271
|
+
FB?: FacebookSdk;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
interface SignupOutcome {
|
|
275
|
+
code: string;
|
|
276
|
+
state: string;
|
|
277
|
+
}
|
|
278
|
+
declare function launchSignup(config: WaSignupLaunch): Promise<SignupOutcome>;
|
|
279
|
+
|
|
280
|
+
export { type InboxHandlerOptions, type SignupOutcome, WaAgent, WaBalance, WaConversation, WaMatchType, WaMessage, WaNumber, WaSendResult, WaSignupLaunch, WaTemplate, WaTemplateCategory, WhatsappClient, type WhatsappClientOptions, createInboxHandler, createWhatsappClient, launchSignup };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { k as WaSignupLaunch, i as WaNumber, l as WaTemplate, m as WaTemplateCategory, j as WaSendResult, f as WaMessage, b as WaConversation, W as WaAgent, e as WaMatchType, a as WaBalance } from './types-BKOEzRzb.js';
|
|
2
|
+
export { c as WaConversationStatus, d as WaErrorCode, g as WaMessageDirection, h as WaMessageStatus, n as WaTemplateStatus, o as WhatsappError } from './types-BKOEzRzb.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The server-side client.
|
|
6
|
+
*
|
|
7
|
+
* **This holds your platform API key. It must never run in a browser** — the key
|
|
8
|
+
* is scoped to your whole account, and every one of your customers' numbers is
|
|
9
|
+
* reachable with it. Call it from a route handler or a server action, and give
|
|
10
|
+
* the browser only what it needs. `createInboxSession()` exists precisely so the
|
|
11
|
+
* inbox UI can work without the key ever leaving your server.
|
|
12
|
+
*
|
|
13
|
+
* `ownerId` is *your* user id for the business owner. We map it to a tenant on
|
|
14
|
+
* our side, so you never handle a WABA id, a phone number id, or a Meta token.
|
|
15
|
+
*/
|
|
16
|
+
interface WhatsappClientOptions {
|
|
17
|
+
apiKey: string;
|
|
18
|
+
/** Override for local development against a tunnel. */
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
fetch?: typeof globalThis.fetch;
|
|
21
|
+
}
|
|
22
|
+
declare class WhatsappClient {
|
|
23
|
+
private readonly apiKey;
|
|
24
|
+
private readonly baseUrl;
|
|
25
|
+
private readonly fetchImpl;
|
|
26
|
+
constructor(options: WhatsappClientOptions);
|
|
27
|
+
private request;
|
|
28
|
+
/**
|
|
29
|
+
* Starts Embedded Signup for one of your business owners.
|
|
30
|
+
*
|
|
31
|
+
* Returns config for Meta's JS SDK, not a redirect URL — a plain OAuth
|
|
32
|
+
* redirect yields a token but skips WABA creation, which is the part the owner
|
|
33
|
+
* actually needs. Pass the result to `launchSignup()` in the browser.
|
|
34
|
+
*/
|
|
35
|
+
connect(params: {
|
|
36
|
+
ownerId: string;
|
|
37
|
+
ownerName?: string;
|
|
38
|
+
}): Promise<WaSignupLaunch>;
|
|
39
|
+
/** Exchanges the code Meta's popup returns. Call this from your server. */
|
|
40
|
+
completeConnect(params: {
|
|
41
|
+
ownerId: string;
|
|
42
|
+
code: string;
|
|
43
|
+
state?: string;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
wabaId: string;
|
|
46
|
+
numbers: WaNumber[];
|
|
47
|
+
creditLineShared: boolean;
|
|
48
|
+
warnings: string[];
|
|
49
|
+
}>;
|
|
50
|
+
numbers(params: {
|
|
51
|
+
ownerId: string;
|
|
52
|
+
refresh?: boolean;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
numbers: WaNumber[];
|
|
55
|
+
}>;
|
|
56
|
+
templates(params: {
|
|
57
|
+
ownerId: string;
|
|
58
|
+
status?: string;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
templates: WaTemplate[];
|
|
61
|
+
}>;
|
|
62
|
+
saveTemplate(params: {
|
|
63
|
+
ownerId: string;
|
|
64
|
+
name: string;
|
|
65
|
+
language: string;
|
|
66
|
+
category: WaTemplateCategory;
|
|
67
|
+
components: unknown[];
|
|
68
|
+
}): Promise<{
|
|
69
|
+
template: WaTemplate;
|
|
70
|
+
}>;
|
|
71
|
+
/** Sends a draft to Meta. The verdict arrives asynchronously — poll or wait. */
|
|
72
|
+
submitTemplate(params: {
|
|
73
|
+
ownerId: string;
|
|
74
|
+
templateId: string;
|
|
75
|
+
}): Promise<{
|
|
76
|
+
template: WaTemplate;
|
|
77
|
+
}>;
|
|
78
|
+
/** Reconciles against Meta now, rather than waiting for the hourly sweep. */
|
|
79
|
+
syncTemplates(params: {
|
|
80
|
+
ownerId: string;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
checked: number;
|
|
83
|
+
updated: number;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Sends a message from the owner's own number.
|
|
87
|
+
*
|
|
88
|
+
* With `template`, any time. With `body`, only inside the 24-hour window the
|
|
89
|
+
* contact opened by messaging them — outside it this throws rather than
|
|
90
|
+
* letting Meta accept the send and deliver nothing.
|
|
91
|
+
*
|
|
92
|
+
* Pass `refKey` to make a retry safe: the same key never charges twice.
|
|
93
|
+
*/
|
|
94
|
+
send(params: {
|
|
95
|
+
ownerId: string;
|
|
96
|
+
to: string;
|
|
97
|
+
template?: string;
|
|
98
|
+
language?: string;
|
|
99
|
+
components?: unknown[];
|
|
100
|
+
body?: string;
|
|
101
|
+
refKey?: string;
|
|
102
|
+
}): Promise<WaSendResult>;
|
|
103
|
+
messages(params: {
|
|
104
|
+
ownerId: string;
|
|
105
|
+
contact?: string;
|
|
106
|
+
limit?: number;
|
|
107
|
+
cursor?: string;
|
|
108
|
+
}): Promise<{
|
|
109
|
+
messages: WaMessage[];
|
|
110
|
+
nextCursor: string | null;
|
|
111
|
+
}>;
|
|
112
|
+
conversations(params: {
|
|
113
|
+
ownerId: string;
|
|
114
|
+
status?: string;
|
|
115
|
+
unread?: boolean;
|
|
116
|
+
limit?: number;
|
|
117
|
+
cursor?: string;
|
|
118
|
+
}): Promise<{
|
|
119
|
+
conversations: WaConversation[];
|
|
120
|
+
nextCursor: string | null;
|
|
121
|
+
}>;
|
|
122
|
+
conversation(params: {
|
|
123
|
+
ownerId: string;
|
|
124
|
+
conversationId: string;
|
|
125
|
+
}): Promise<{
|
|
126
|
+
conversation: WaConversation & {
|
|
127
|
+
messages: WaMessage[];
|
|
128
|
+
};
|
|
129
|
+
}>;
|
|
130
|
+
updateConversation(params: {
|
|
131
|
+
ownerId: string;
|
|
132
|
+
conversationId: string;
|
|
133
|
+
status?: "OPEN" | "SNOOZED" | "CLOSED";
|
|
134
|
+
assignedAgentId?: string | null;
|
|
135
|
+
optedOut?: boolean;
|
|
136
|
+
}): Promise<{
|
|
137
|
+
updated: boolean;
|
|
138
|
+
}>;
|
|
139
|
+
agents(params: {
|
|
140
|
+
ownerId: string;
|
|
141
|
+
includeInactive?: boolean;
|
|
142
|
+
}): Promise<{
|
|
143
|
+
agents: WaAgent[];
|
|
144
|
+
}>;
|
|
145
|
+
saveAgent(params: {
|
|
146
|
+
ownerId: string;
|
|
147
|
+
agentId: string;
|
|
148
|
+
name: string;
|
|
149
|
+
email?: string;
|
|
150
|
+
role?: "AGENT" | "ADMIN";
|
|
151
|
+
active?: boolean;
|
|
152
|
+
}): Promise<{
|
|
153
|
+
agent: WaAgent;
|
|
154
|
+
}>;
|
|
155
|
+
autoreplyRules(params: {
|
|
156
|
+
ownerId: string;
|
|
157
|
+
}): Promise<{
|
|
158
|
+
rules: unknown[];
|
|
159
|
+
}>;
|
|
160
|
+
createAutoreplyRule(params: {
|
|
161
|
+
ownerId: string;
|
|
162
|
+
name: string;
|
|
163
|
+
matchType: WaMatchType;
|
|
164
|
+
pattern?: string;
|
|
165
|
+
replyText: string;
|
|
166
|
+
priority?: number;
|
|
167
|
+
}): Promise<{
|
|
168
|
+
rule: unknown;
|
|
169
|
+
}>;
|
|
170
|
+
/**
|
|
171
|
+
* Adds balance for one of your owners, after you have collected the money on
|
|
172
|
+
* your own rails. We hold the ledger; we never hold your customer's funds.
|
|
173
|
+
*/
|
|
174
|
+
credit(params: {
|
|
175
|
+
ownerId: string;
|
|
176
|
+
amountPaise: number;
|
|
177
|
+
refKey?: string;
|
|
178
|
+
note?: string;
|
|
179
|
+
}): Promise<WaBalance>;
|
|
180
|
+
balance(params?: {
|
|
181
|
+
ownerId?: string;
|
|
182
|
+
}): Promise<WaBalance>;
|
|
183
|
+
/**
|
|
184
|
+
* Mints a short-lived, owner-scoped session for the inbox UI.
|
|
185
|
+
*
|
|
186
|
+
* Call this from a server route and return the result to your page. It is
|
|
187
|
+
* what lets `<WhatsappInbox>` talk to us without your API key ever reaching
|
|
188
|
+
* the browser. The ticket lasts sixty seconds; the component refreshes it
|
|
189
|
+
* through the same route on reconnect.
|
|
190
|
+
*/
|
|
191
|
+
createInboxSession(params: {
|
|
192
|
+
ownerId: string;
|
|
193
|
+
}): Promise<{
|
|
194
|
+
ownerId: string;
|
|
195
|
+
ticket: string;
|
|
196
|
+
expiresIn: number;
|
|
197
|
+
baseUrl: string;
|
|
198
|
+
}>;
|
|
199
|
+
}
|
|
200
|
+
declare function createWhatsappClient(options: WhatsappClientOptions): WhatsappClient;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A ready-made proxy route for the inbox UI.
|
|
204
|
+
*
|
|
205
|
+
* The browser must never hold your platform API key, so `<WhatsappInbox>` talks
|
|
206
|
+
* to *your* server, and your server talks to us. This builds that middle layer
|
|
207
|
+
* so you do not have to hand-write five thin fetch wrappers.
|
|
208
|
+
*
|
|
209
|
+
* ```ts
|
|
210
|
+
* // app/api/whatsapp/[...action]/route.ts
|
|
211
|
+
* import { createWhatsappClient, createInboxHandler } from "@glitchgrab/whatsapp";
|
|
212
|
+
*
|
|
213
|
+
* const client = createWhatsappClient({ apiKey: process.env.GG_WA_KEY! });
|
|
214
|
+
*
|
|
215
|
+
* const handler = createInboxHandler({
|
|
216
|
+
* client,
|
|
217
|
+
* // The single most important line here: derive the owner from YOUR session,
|
|
218
|
+
* // never from the request body. Returning a client-supplied id would let any
|
|
219
|
+
* // signed-in user read any other library's WhatsApp.
|
|
220
|
+
* resolveOwnerId: async () => (await auth()).user.libraryId,
|
|
221
|
+
* });
|
|
222
|
+
*
|
|
223
|
+
* export const GET = handler;
|
|
224
|
+
* export const POST = handler;
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
interface InboxHandlerOptions {
|
|
228
|
+
client: WhatsappClient;
|
|
229
|
+
/**
|
|
230
|
+
* Returns the owner id for the current request, from your own auth. Return
|
|
231
|
+
* null to deny.
|
|
232
|
+
*/
|
|
233
|
+
resolveOwnerId: (request: Request) => Promise<string | null> | string | null;
|
|
234
|
+
/** Set false to make the inbox read-only for this route. Default true. */
|
|
235
|
+
allowSend?: boolean;
|
|
236
|
+
}
|
|
237
|
+
declare function createInboxHandler(options: InboxHandlerOptions): (request: Request) => Promise<Response>;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Opens Meta's Embedded Signup popup.
|
|
241
|
+
*
|
|
242
|
+
* Runs in the browser, holds no credentials, and returns the `code` your server
|
|
243
|
+
* exchanges via `client.completeConnect()`. Nothing is connected until that
|
|
244
|
+
* exchange happens.
|
|
245
|
+
*
|
|
246
|
+
* Meta's JS SDK must already be on the page — Embedded Signup only works
|
|
247
|
+
* through `FB.login` with a `config_id`; a plain OAuth redirect gets a token but
|
|
248
|
+
* skips the WABA creation the owner actually needs.
|
|
249
|
+
*/
|
|
250
|
+
interface FacebookSdk {
|
|
251
|
+
init(params: {
|
|
252
|
+
appId: string;
|
|
253
|
+
cookie?: boolean;
|
|
254
|
+
xfbml?: boolean;
|
|
255
|
+
version: string;
|
|
256
|
+
}): void;
|
|
257
|
+
login(callback: (response: {
|
|
258
|
+
authResponse?: {
|
|
259
|
+
code?: string;
|
|
260
|
+
};
|
|
261
|
+
status?: string;
|
|
262
|
+
}) => void, options: {
|
|
263
|
+
config_id: string;
|
|
264
|
+
response_type: string;
|
|
265
|
+
override_default_response_type: boolean;
|
|
266
|
+
extras?: Record<string, unknown>;
|
|
267
|
+
}): void;
|
|
268
|
+
}
|
|
269
|
+
declare global {
|
|
270
|
+
interface Window {
|
|
271
|
+
FB?: FacebookSdk;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
interface SignupOutcome {
|
|
275
|
+
code: string;
|
|
276
|
+
state: string;
|
|
277
|
+
}
|
|
278
|
+
declare function launchSignup(config: WaSignupLaunch): Promise<SignupOutcome>;
|
|
279
|
+
|
|
280
|
+
export { type InboxHandlerOptions, type SignupOutcome, WaAgent, WaBalance, WaConversation, WaMatchType, WaMessage, WaNumber, WaSendResult, WaSignupLaunch, WaTemplate, WaTemplateCategory, WhatsappClient, type WhatsappClientOptions, createInboxHandler, createWhatsappClient, launchSignup };
|