@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 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.
@@ -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 };
@@ -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 };