@glitchgrab/whatsapp 0.2.0 → 0.4.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/dist/index.d.mts +63 -3
- package/dist/index.d.ts +63 -3
- package/dist/index.js +31 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/{types-BKOEzRzb.d.mts → types-DdhmKV_Q.d.mts} +8 -0
- package/dist/{types-BKOEzRzb.d.ts → types-DdhmKV_Q.d.ts} +8 -0
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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-
|
|
2
|
-
export { c as WaConversationStatus, d as WaErrorCode, g as WaMessageDirection, h as WaMessageStatus, n as WaTemplateStatus, o as WhatsappError } from './types-
|
|
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-DdhmKV_Q.mjs';
|
|
2
|
+
export { c as WaConversationStatus, d as WaErrorCode, g as WaMessageDirection, h as WaMessageStatus, n as WaTemplateStatus, o as WhatsappError } from './types-DdhmKV_Q.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The server-side client.
|
|
@@ -65,6 +65,25 @@ declare class WhatsappClient {
|
|
|
65
65
|
language: string;
|
|
66
66
|
category: WaTemplateCategory;
|
|
67
67
|
components: unknown[];
|
|
68
|
+
/** What you call this template on your side. Defaults to `name`. */
|
|
69
|
+
sourceName?: string;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
template: WaTemplate;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* Rewords a template the tenant already has.
|
|
75
|
+
*
|
|
76
|
+
* The variables may not move: same `{{n}}` tokens, same order, same
|
|
77
|
+
* components, or this rejects with 400. Senders fill them positionally, so a
|
|
78
|
+
* reordered template quietly puts the wrong value in every message.
|
|
79
|
+
*
|
|
80
|
+
* Leaves the template a DRAFT — call `submitTemplate` to send it to Meta,
|
|
81
|
+
* which resets an approved template to pending until it clears again.
|
|
82
|
+
*/
|
|
83
|
+
editTemplate(params: {
|
|
84
|
+
ownerId: string;
|
|
85
|
+
templateId: string;
|
|
86
|
+
components: unknown[];
|
|
68
87
|
}): Promise<{
|
|
69
88
|
template: WaTemplate;
|
|
70
89
|
}>;
|
|
@@ -277,4 +296,45 @@ interface SignupOutcome {
|
|
|
277
296
|
}
|
|
278
297
|
declare function launchSignup(config: WaSignupLaunch): Promise<SignupOutcome>;
|
|
279
298
|
|
|
280
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Verifying a callback Glitchgrab sent you.
|
|
301
|
+
*
|
|
302
|
+
* Every callback carries `X-Glitchgrab-Signature: sha256=<hex>`, an HMAC of the
|
|
303
|
+
* raw request body under your platform's callback secret. Check it before you
|
|
304
|
+
* trust a word of the payload: the endpoint is a public URL, and an unverified
|
|
305
|
+
* one lets anyone tell you a message failed, a template was approved, or a
|
|
306
|
+
* wallet ran dry.
|
|
307
|
+
*
|
|
308
|
+
* Node only — this lives on the server entry, never the React one.
|
|
309
|
+
*/
|
|
310
|
+
/**
|
|
311
|
+
* The events a platform can receive.
|
|
312
|
+
*
|
|
313
|
+
* Duplicated from `apps/web/lib/wa/callbacks.ts` in the Glitchgrab repo, which
|
|
314
|
+
* is the side that emits them. This package ships to other repos and cannot
|
|
315
|
+
* import server code, so the two are kept in step by hand — adding an event
|
|
316
|
+
* means editing both, or this type quietly under-reports what arrives.
|
|
317
|
+
*/
|
|
318
|
+
type WaCallbackEventName = "message.status" | "message.inbound" | "template.status" | "wallet.low";
|
|
319
|
+
interface WaCallbackBody<T = Record<string, unknown>> {
|
|
320
|
+
/** The outbox row id. Stable across retries — safe to dedupe on. */
|
|
321
|
+
id: string;
|
|
322
|
+
event: WaCallbackEventName;
|
|
323
|
+
/** Your own id for the tenant, as you passed it to us. */
|
|
324
|
+
tenantId: string | null;
|
|
325
|
+
occurredAt: string;
|
|
326
|
+
data: T;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* True when the signature matches.
|
|
330
|
+
*
|
|
331
|
+
* Pass the RAW body text, not a re-serialized object: `JSON.stringify` of a
|
|
332
|
+
* parsed payload can reorder keys or change spacing, and the signature is over
|
|
333
|
+
* bytes. In Next.js that means `await request.text()`, then parse it yourself.
|
|
334
|
+
*
|
|
335
|
+
* Constant-time — a fast string compare leaks a valid signature one byte at a
|
|
336
|
+
* time to anyone willing to make enough requests.
|
|
337
|
+
*/
|
|
338
|
+
declare function verifyCallbackSignature(secret: string, rawBody: string, header: string | null | undefined): boolean;
|
|
339
|
+
|
|
340
|
+
export { type InboxHandlerOptions, type SignupOutcome, WaAgent, WaBalance, type WaCallbackBody, type WaCallbackEventName, WaConversation, WaMatchType, WaMessage, WaNumber, WaSendResult, WaSignupLaunch, WaTemplate, WaTemplateCategory, WhatsappClient, type WhatsappClientOptions, createInboxHandler, createWhatsappClient, launchSignup, verifyCallbackSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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-
|
|
2
|
-
export { c as WaConversationStatus, d as WaErrorCode, g as WaMessageDirection, h as WaMessageStatus, n as WaTemplateStatus, o as WhatsappError } from './types-
|
|
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-DdhmKV_Q.js';
|
|
2
|
+
export { c as WaConversationStatus, d as WaErrorCode, g as WaMessageDirection, h as WaMessageStatus, n as WaTemplateStatus, o as WhatsappError } from './types-DdhmKV_Q.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The server-side client.
|
|
@@ -65,6 +65,25 @@ declare class WhatsappClient {
|
|
|
65
65
|
language: string;
|
|
66
66
|
category: WaTemplateCategory;
|
|
67
67
|
components: unknown[];
|
|
68
|
+
/** What you call this template on your side. Defaults to `name`. */
|
|
69
|
+
sourceName?: string;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
template: WaTemplate;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* Rewords a template the tenant already has.
|
|
75
|
+
*
|
|
76
|
+
* The variables may not move: same `{{n}}` tokens, same order, same
|
|
77
|
+
* components, or this rejects with 400. Senders fill them positionally, so a
|
|
78
|
+
* reordered template quietly puts the wrong value in every message.
|
|
79
|
+
*
|
|
80
|
+
* Leaves the template a DRAFT — call `submitTemplate` to send it to Meta,
|
|
81
|
+
* which resets an approved template to pending until it clears again.
|
|
82
|
+
*/
|
|
83
|
+
editTemplate(params: {
|
|
84
|
+
ownerId: string;
|
|
85
|
+
templateId: string;
|
|
86
|
+
components: unknown[];
|
|
68
87
|
}): Promise<{
|
|
69
88
|
template: WaTemplate;
|
|
70
89
|
}>;
|
|
@@ -277,4 +296,45 @@ interface SignupOutcome {
|
|
|
277
296
|
}
|
|
278
297
|
declare function launchSignup(config: WaSignupLaunch): Promise<SignupOutcome>;
|
|
279
298
|
|
|
280
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Verifying a callback Glitchgrab sent you.
|
|
301
|
+
*
|
|
302
|
+
* Every callback carries `X-Glitchgrab-Signature: sha256=<hex>`, an HMAC of the
|
|
303
|
+
* raw request body under your platform's callback secret. Check it before you
|
|
304
|
+
* trust a word of the payload: the endpoint is a public URL, and an unverified
|
|
305
|
+
* one lets anyone tell you a message failed, a template was approved, or a
|
|
306
|
+
* wallet ran dry.
|
|
307
|
+
*
|
|
308
|
+
* Node only — this lives on the server entry, never the React one.
|
|
309
|
+
*/
|
|
310
|
+
/**
|
|
311
|
+
* The events a platform can receive.
|
|
312
|
+
*
|
|
313
|
+
* Duplicated from `apps/web/lib/wa/callbacks.ts` in the Glitchgrab repo, which
|
|
314
|
+
* is the side that emits them. This package ships to other repos and cannot
|
|
315
|
+
* import server code, so the two are kept in step by hand — adding an event
|
|
316
|
+
* means editing both, or this type quietly under-reports what arrives.
|
|
317
|
+
*/
|
|
318
|
+
type WaCallbackEventName = "message.status" | "message.inbound" | "template.status" | "wallet.low";
|
|
319
|
+
interface WaCallbackBody<T = Record<string, unknown>> {
|
|
320
|
+
/** The outbox row id. Stable across retries — safe to dedupe on. */
|
|
321
|
+
id: string;
|
|
322
|
+
event: WaCallbackEventName;
|
|
323
|
+
/** Your own id for the tenant, as you passed it to us. */
|
|
324
|
+
tenantId: string | null;
|
|
325
|
+
occurredAt: string;
|
|
326
|
+
data: T;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* True when the signature matches.
|
|
330
|
+
*
|
|
331
|
+
* Pass the RAW body text, not a re-serialized object: `JSON.stringify` of a
|
|
332
|
+
* parsed payload can reorder keys or change spacing, and the signature is over
|
|
333
|
+
* bytes. In Next.js that means `await request.text()`, then parse it yourself.
|
|
334
|
+
*
|
|
335
|
+
* Constant-time — a fast string compare leaks a valid signature one byte at a
|
|
336
|
+
* time to anyone willing to make enough requests.
|
|
337
|
+
*/
|
|
338
|
+
declare function verifyCallbackSignature(secret: string, rawBody: string, header: string | null | undefined): boolean;
|
|
339
|
+
|
|
340
|
+
export { type InboxHandlerOptions, type SignupOutcome, WaAgent, WaBalance, type WaCallbackBody, type WaCallbackEventName, WaConversation, WaMatchType, WaMessage, WaNumber, WaSendResult, WaSignupLaunch, WaTemplate, WaTemplateCategory, WhatsappClient, type WhatsappClientOptions, createInboxHandler, createWhatsappClient, launchSignup, verifyCallbackSignature };
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,8 @@ __export(index_exports, {
|
|
|
24
24
|
WhatsappError: () => WhatsappError,
|
|
25
25
|
createInboxHandler: () => createInboxHandler,
|
|
26
26
|
createWhatsappClient: () => createWhatsappClient,
|
|
27
|
-
launchSignup: () => launchSignup
|
|
27
|
+
launchSignup: () => launchSignup,
|
|
28
|
+
verifyCallbackSignature: () => verifyCallbackSignature
|
|
28
29
|
});
|
|
29
30
|
module.exports = __toCommonJS(index_exports);
|
|
30
31
|
|
|
@@ -115,6 +116,22 @@ var WhatsappClient = class {
|
|
|
115
116
|
saveTemplate(params) {
|
|
116
117
|
return this.request("/templates", { method: "POST", body: params });
|
|
117
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Rewords a template the tenant already has.
|
|
121
|
+
*
|
|
122
|
+
* The variables may not move: same `{{n}}` tokens, same order, same
|
|
123
|
+
* components, or this rejects with 400. Senders fill them positionally, so a
|
|
124
|
+
* reordered template quietly puts the wrong value in every message.
|
|
125
|
+
*
|
|
126
|
+
* Leaves the template a DRAFT — call `submitTemplate` to send it to Meta,
|
|
127
|
+
* which resets an approved template to pending until it clears again.
|
|
128
|
+
*/
|
|
129
|
+
editTemplate(params) {
|
|
130
|
+
return this.request(`/templates/${params.templateId}`, {
|
|
131
|
+
method: "PATCH",
|
|
132
|
+
body: { ownerId: params.ownerId, components: params.components }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
118
135
|
/** Sends a draft to Meta. The verdict arrives asynchronously — poll or wait. */
|
|
119
136
|
submitTemplate(params) {
|
|
120
137
|
return this.request(`/templates/${params.templateId}/submit`, {
|
|
@@ -319,12 +336,24 @@ function launchSignup(config) {
|
|
|
319
336
|
);
|
|
320
337
|
});
|
|
321
338
|
}
|
|
339
|
+
|
|
340
|
+
// src/callbacks.ts
|
|
341
|
+
var import_crypto = require("crypto");
|
|
342
|
+
function verifyCallbackSignature(secret, rawBody, header) {
|
|
343
|
+
if (!secret || !header) return false;
|
|
344
|
+
const provided = header.startsWith("sha256=") ? header.slice(7) : header;
|
|
345
|
+
const expected = (0, import_crypto.createHmac)("sha256", secret).update(rawBody).digest("hex");
|
|
346
|
+
const a = Buffer.from(expected);
|
|
347
|
+
const b = Buffer.from(provided);
|
|
348
|
+
return a.length === b.length && (0, import_crypto.timingSafeEqual)(a, b);
|
|
349
|
+
}
|
|
322
350
|
// Annotate the CommonJS export names for ESM import in node:
|
|
323
351
|
0 && (module.exports = {
|
|
324
352
|
WhatsappClient,
|
|
325
353
|
WhatsappError,
|
|
326
354
|
createInboxHandler,
|
|
327
355
|
createWhatsappClient,
|
|
328
|
-
launchSignup
|
|
356
|
+
launchSignup,
|
|
357
|
+
verifyCallbackSignature
|
|
329
358
|
});
|
|
330
359
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/types.ts","../src/client.ts","../src/handler.ts","../src/browser.ts"],"sourcesContent":["export { WhatsappClient, createWhatsappClient, type WhatsappClientOptions } from \"./client\";\nexport { createInboxHandler, type InboxHandlerOptions } from \"./handler\";\nexport { launchSignup, type SignupOutcome } from \"./browser\";\nexport * from \"./types\";\n","/** Shared shapes. Mirrors the `/api/v1/wa` responses. */\n\nexport type WaTemplateCategory = \"MARKETING\" | \"UTILITY\" | \"AUTHENTICATION\" | \"SERVICE\";\nexport type WaTemplateStatus =\n | \"DRAFT\"\n | \"PENDING\"\n | \"APPROVED\"\n | \"REJECTED\"\n | \"PAUSED\"\n | \"DISABLED\";\nexport type WaMessageDirection = \"INBOUND\" | \"OUTBOUND\";\nexport type WaMessageStatus = \"QUEUED\" | \"SENT\" | \"DELIVERED\" | \"READ\" | \"FAILED\";\nexport type WaConversationStatus = \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\nexport type WaMatchType = \"EXACT\" | \"CONTAINS\" | \"STARTS_WITH\" | \"REGEX\" | \"ANY\";\n\n/**\n * Every failure carries a stable `code`, so callers branch on that rather than\n * matching on a message that may be reworded.\n */\nexport type WaErrorCode =\n | \"UNAUTHORIZED\"\n | \"PLATFORM_INACTIVE\"\n | \"TENANT_NOT_FOUND\"\n | \"INSUFFICIENT_FUNDS\"\n | \"NO_PRICE_RULE\"\n | \"INVALID_AMOUNT\"\n | \"DUPLICATE_REQUEST\"\n | \"INTERNAL\";\n\nexport class WhatsappError extends Error {\n constructor(\n readonly code: WaErrorCode | string,\n message: string,\n readonly status: number,\n readonly detail?: Record<string, unknown>\n ) {\n super(message);\n this.name = \"WhatsappError\";\n }\n\n /** The owner has run out of balance. `detail.shortfallPaise` says by how much. */\n get isInsufficientFunds(): boolean {\n return this.code === \"INSUFFICIENT_FUNDS\";\n }\n}\n\nexport interface WaNumber {\n phoneNumberId: string;\n displayNumber: string;\n verifiedName: string;\n status: \"PENDING\" | \"VERIFIED\" | \"REGISTERED\" | \"DISCONNECTED\";\n qualityRating?: string | null;\n messagingLimitTier?: string | null;\n}\n\nexport interface WaMessage {\n id: string;\n direction: WaMessageDirection;\n status: WaMessageStatus;\n contactPhone?: string;\n category?: WaTemplateCategory | null;\n payload: unknown;\n error?: string | null;\n sentAt?: string | null;\n deliveredAt?: string | null;\n readAt?: string | null;\n createdAt: string;\n}\n\nexport interface WaConversation {\n id: string;\n contactPhone: string;\n contactName?: string | null;\n windowExpiresAt?: string | null;\n /** Whether free-form text is legal right now. Computed server-side. */\n windowOpen: boolean;\n lastInboundAt?: string | null;\n lastOutboundAt?: string | null;\n optedOut: boolean;\n status: WaConversationStatus;\n assignedAgentId?: string | null;\n unreadCount: number;\n updatedAt: string;\n lastMessage?: WaMessage | null;\n}\n\nexport interface WaTemplate {\n id: string;\n name: string;\n language: string;\n category: WaTemplateCategory;\n status: WaTemplateStatus;\n rejectionReason?: string | null;\n components?: unknown;\n submittedAt?: string | null;\n lastSyncedAt?: string | null;\n}\n\nexport interface WaAgent {\n id: string;\n externalAgentId: string;\n name: string;\n email?: string | null;\n role: \"AGENT\" | \"ADMIN\";\n active: boolean;\n}\n\nexport interface WaBalance {\n balancePaise: number;\n heldPaise: number;\n spendablePaise: number;\n}\n\nexport interface WaSendResult {\n messageId: string;\n metaMessageId: string;\n status: \"SENT\";\n category: WaTemplateCategory;\n tenantPricePaise: number;\n tenantBalancePaise: number;\n}\n\nexport interface WaSignupLaunch {\n appId: string;\n configId: string;\n scopes: string[];\n state: string;\n}\n","import {\n WhatsappError,\n type WaAgent,\n type WaBalance,\n type WaConversation,\n type WaMatchType,\n type WaMessage,\n type WaNumber,\n type WaSendResult,\n type WaSignupLaunch,\n type WaTemplate,\n type WaTemplateCategory,\n} from \"./types\";\n\n/**\n * The server-side client.\n *\n * **This holds your platform API key. It must never run in a browser** — the key\n * is scoped to your whole account, and every one of your customers' numbers is\n * reachable with it. Call it from a route handler or a server action, and give\n * the browser only what it needs. `createInboxSession()` exists precisely so the\n * inbox UI can work without the key ever leaving your server.\n *\n * `ownerId` is *your* user id for the business owner. We map it to a tenant on\n * our side, so you never handle a WABA id, a phone number id, or a Meta token.\n */\n\nexport interface WhatsappClientOptions {\n apiKey: string;\n /** Override for local development against a tunnel. */\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n}\n\ninterface ApiEnvelope<T> {\n success: boolean;\n data?: T;\n error?: string;\n code?: string;\n detail?: Record<string, unknown>;\n}\n\nconst DEFAULT_BASE_URL = \"https://glitchgrab.dev\";\n\nexport class WhatsappClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n\n constructor(options: WhatsappClientOptions) {\n if (!options.apiKey) throw new Error(\"@glitchgrab/whatsapp: apiKey is required\");\n\n if (typeof window !== \"undefined\") {\n // Loud on purpose. A key that reaches a bundle is a key that reaches every\n // visitor, and the failure is otherwise completely silent.\n console.error(\n \"[@glitchgrab/whatsapp] WhatsappClient is running in a browser. Your API key is now \" +\n \"public. Move this to a server route and use createInboxSession() for the UI.\"\n );\n }\n\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n }\n\n private async request<T>(\n path: string,\n init: { method?: string; body?: unknown; query?: Record<string, string | undefined> } = {}\n ): Promise<T> {\n const url = new URL(`${this.baseUrl}/api/v1/wa${path}`);\n for (const [key, value] of Object.entries(init.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n\n const res = await this.fetchImpl(url.toString(), {\n method: init.method ?? \"GET\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n ...(init.body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n let envelope: ApiEnvelope<T>;\n try {\n envelope = (await res.json()) as ApiEnvelope<T>;\n } catch {\n throw new WhatsappError(\"INTERNAL\", `Unexpected response (${res.status})`, res.status);\n }\n\n if (!res.ok || !envelope.success) {\n throw new WhatsappError(\n envelope.code ?? \"INTERNAL\",\n envelope.error ?? `Request failed (${res.status})`,\n res.status,\n envelope.detail\n );\n }\n\n return envelope.data as T;\n }\n\n // ── Onboarding ────────────────────────────────────────────────────────────\n\n /**\n * Starts Embedded Signup for one of your business owners.\n *\n * Returns config for Meta's JS SDK, not a redirect URL — a plain OAuth\n * redirect yields a token but skips WABA creation, which is the part the owner\n * actually needs. Pass the result to `launchSignup()` in the browser.\n */\n connect(params: { ownerId: string; ownerName?: string }): Promise<WaSignupLaunch> {\n return this.request(\"/signup/launch\", { method: \"POST\", body: params });\n }\n\n /** Exchanges the code Meta's popup returns. Call this from your server. */\n completeConnect(params: {\n ownerId: string;\n code: string;\n state?: string;\n }): Promise<{ wabaId: string; numbers: WaNumber[]; creditLineShared: boolean; warnings: string[] }> {\n return this.request(\"/signup/exchange\", { method: \"POST\", body: params });\n }\n\n numbers(params: { ownerId: string; refresh?: boolean }): Promise<{ numbers: WaNumber[] }> {\n return this.request(\"/numbers\", {\n query: { ownerId: params.ownerId, refresh: params.refresh ? \"true\" : undefined },\n });\n }\n\n // ── Templates ─────────────────────────────────────────────────────────────\n\n templates(params: { ownerId: string; status?: string }): Promise<{ templates: WaTemplate[] }> {\n return this.request(\"/templates\", {\n query: { ownerId: params.ownerId, status: params.status },\n });\n }\n\n saveTemplate(params: {\n ownerId: string;\n name: string;\n language: string;\n category: WaTemplateCategory;\n components: unknown[];\n }): Promise<{ template: WaTemplate }> {\n return this.request(\"/templates\", { method: \"POST\", body: params });\n }\n\n /** Sends a draft to Meta. The verdict arrives asynchronously — poll or wait. */\n submitTemplate(params: { ownerId: string; templateId: string }): Promise<{ template: WaTemplate }> {\n return this.request(`/templates/${params.templateId}/submit`, {\n method: \"POST\",\n body: { ownerId: params.ownerId },\n });\n }\n\n /** Reconciles against Meta now, rather than waiting for the hourly sweep. */\n syncTemplates(params: { ownerId: string }): Promise<{ checked: number; updated: number }> {\n return this.request(\"/templates/sync\", { method: \"POST\", body: params });\n }\n\n // ── Sending ───────────────────────────────────────────────────────────────\n\n /**\n * Sends a message from the owner's own number.\n *\n * With `template`, any time. With `body`, only inside the 24-hour window the\n * contact opened by messaging them — outside it this throws rather than\n * letting Meta accept the send and deliver nothing.\n *\n * Pass `refKey` to make a retry safe: the same key never charges twice.\n */\n send(params: {\n ownerId: string;\n to: string;\n template?: string;\n language?: string;\n components?: unknown[];\n body?: string;\n refKey?: string;\n }): Promise<WaSendResult> {\n return this.request(\"/messages/send\", { method: \"POST\", body: params });\n }\n\n messages(params: {\n ownerId: string;\n contact?: string;\n limit?: number;\n cursor?: string;\n }): Promise<{ messages: WaMessage[]; nextCursor: string | null }> {\n return this.request(\"/messages\", {\n query: {\n ownerId: params.ownerId,\n contact: params.contact,\n limit: params.limit?.toString(),\n cursor: params.cursor,\n },\n });\n }\n\n // ── Inbox ─────────────────────────────────────────────────────────────────\n\n conversations(params: {\n ownerId: string;\n status?: string;\n unread?: boolean;\n limit?: number;\n cursor?: string;\n }): Promise<{ conversations: WaConversation[]; nextCursor: string | null }> {\n return this.request(\"/conversations\", {\n query: {\n ownerId: params.ownerId,\n status: params.status,\n unread: params.unread ? \"true\" : undefined,\n limit: params.limit?.toString(),\n cursor: params.cursor,\n },\n });\n }\n\n conversation(params: {\n ownerId: string;\n conversationId: string;\n }): Promise<{ conversation: WaConversation & { messages: WaMessage[] } }> {\n return this.request(`/conversations/${params.conversationId}`, {\n query: { ownerId: params.ownerId },\n });\n }\n\n updateConversation(params: {\n ownerId: string;\n conversationId: string;\n status?: \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\n assignedAgentId?: string | null;\n optedOut?: boolean;\n }): Promise<{ updated: boolean }> {\n const { conversationId, ...body } = params;\n return this.request(`/conversations/${conversationId}`, { method: \"PATCH\", body });\n }\n\n agents(params: { ownerId: string; includeInactive?: boolean }): Promise<{ agents: WaAgent[] }> {\n return this.request(\"/agents\", {\n query: {\n ownerId: params.ownerId,\n includeInactive: params.includeInactive ? \"true\" : undefined,\n },\n });\n }\n\n saveAgent(params: {\n ownerId: string;\n agentId: string;\n name: string;\n email?: string;\n role?: \"AGENT\" | \"ADMIN\";\n active?: boolean;\n }): Promise<{ agent: WaAgent }> {\n return this.request(\"/agents\", { method: \"POST\", body: params });\n }\n\n // ── Autoreply ─────────────────────────────────────────────────────────────\n\n autoreplyRules(params: { ownerId: string }): Promise<{ rules: unknown[] }> {\n return this.request(\"/autoreply/rules\", { query: { ownerId: params.ownerId } });\n }\n\n createAutoreplyRule(params: {\n ownerId: string;\n name: string;\n matchType: WaMatchType;\n pattern?: string;\n replyText: string;\n priority?: number;\n }): Promise<{ rule: unknown }> {\n return this.request(\"/autoreply/rules\", { method: \"POST\", body: params });\n }\n\n // ── Wallet ────────────────────────────────────────────────────────────────\n\n /**\n * Adds balance for one of your owners, after you have collected the money on\n * your own rails. We hold the ledger; we never hold your customer's funds.\n */\n credit(params: {\n ownerId: string;\n amountPaise: number;\n refKey?: string;\n note?: string;\n }): Promise<WaBalance> {\n return this.request(\"/wallet/credit\", { method: \"POST\", body: params });\n }\n\n balance(params: { ownerId?: string } = {}): Promise<WaBalance> {\n return this.request(\"/wallet/balance\", { query: { ownerId: params.ownerId } });\n }\n\n // ── Inbox session ─────────────────────────────────────────────────────────\n\n /**\n * Mints a short-lived, owner-scoped session for the inbox UI.\n *\n * Call this from a server route and return the result to your page. It is\n * what lets `<WhatsappInbox>` talk to us without your API key ever reaching\n * the browser. The ticket lasts sixty seconds; the component refreshes it\n * through the same route on reconnect.\n */\n async createInboxSession(params: {\n ownerId: string;\n }): Promise<{ ownerId: string; ticket: string; expiresIn: number; baseUrl: string }> {\n const data = await this.request<{ ticket: string; expiresIn: number }>(\"/inbox/ticket\", {\n method: \"POST\",\n body: { ownerId: params.ownerId },\n });\n return { ownerId: params.ownerId, baseUrl: this.baseUrl, ...data };\n }\n}\n\nexport function createWhatsappClient(options: WhatsappClientOptions): WhatsappClient {\n return new WhatsappClient(options);\n}\n","import type { WhatsappClient } from \"./client\";\n\n/**\n * A ready-made proxy route for the inbox UI.\n *\n * The browser must never hold your platform API key, so `<WhatsappInbox>` talks\n * to *your* server, and your server talks to us. This builds that middle layer\n * so you do not have to hand-write five thin fetch wrappers.\n *\n * ```ts\n * // app/api/whatsapp/[...action]/route.ts\n * import { createWhatsappClient, createInboxHandler } from \"@glitchgrab/whatsapp\";\n *\n * const client = createWhatsappClient({ apiKey: process.env.GG_WA_KEY! });\n *\n * const handler = createInboxHandler({\n * client,\n * // The single most important line here: derive the owner from YOUR session,\n * // never from the request body. Returning a client-supplied id would let any\n * // signed-in user read any other library's WhatsApp.\n * resolveOwnerId: async () => (await auth()).user.libraryId,\n * });\n *\n * export const GET = handler;\n * export const POST = handler;\n * ```\n */\n\nexport interface InboxHandlerOptions {\n client: WhatsappClient;\n /**\n * Returns the owner id for the current request, from your own auth. Return\n * null to deny.\n */\n resolveOwnerId: (request: Request) => Promise<string | null> | string | null;\n /** Set false to make the inbox read-only for this route. Default true. */\n allowSend?: boolean;\n}\n\nconst JSON_HEADERS = { \"Content-Type\": \"application/json\" };\n\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });\n}\n\nexport function createInboxHandler(options: InboxHandlerOptions) {\n const { client, resolveOwnerId, allowSend = true } = options;\n\n return async function handler(request: Request): Promise<Response> {\n let ownerId: string | null;\n try {\n ownerId = await resolveOwnerId(request);\n } catch {\n ownerId = null;\n }\n\n if (!ownerId) return json({ error: \"Unauthorized\" }, 401);\n\n const url = new URL(request.url);\n // Everything after the mount point, so the route works wherever it is\n // mounted rather than assuming a fixed prefix.\n const segments = url.pathname.split(\"/\").filter(Boolean);\n const action = segments[segments.length - 1] ?? \"\";\n // Avoids Array.prototype.at: this package ships into other people's\n // runtimes, and ES2022 methods are not worth the compatibility floor.\n const parent = segments[segments.length - 2] ?? \"\";\n\n try {\n if (request.method === \"GET\" && action === \"conversations\") {\n return json(\n await client.conversations({\n ownerId,\n status: url.searchParams.get(\"status\") ?? undefined,\n unread: url.searchParams.get(\"unread\") === \"true\",\n limit: Number(url.searchParams.get(\"limit\")) || undefined,\n cursor: url.searchParams.get(\"cursor\") ?? undefined,\n })\n );\n }\n\n // .../conversations/<id>\n if (request.method === \"GET\" && parent === \"conversations\") {\n return json(await client.conversation({ ownerId, conversationId: action }));\n }\n\n if (request.method === \"GET\" && action === \"agents\") {\n return json(await client.agents({ ownerId }));\n }\n\n if (request.method === \"POST\" && action === \"ticket\") {\n return json(await client.createInboxSession({ ownerId }));\n }\n\n if (request.method === \"POST\" && action === \"send\") {\n if (!allowSend) return json({ error: \"Sending is disabled\" }, 403);\n const body = (await request.json()) as {\n to?: string;\n body?: string;\n template?: string;\n language?: string;\n components?: unknown[];\n };\n const { to } = body;\n if (!to) return json({ error: \"to is required\" }, 400);\n return json(await client.send({ ...body, ownerId, to }));\n }\n\n if (request.method === \"PATCH\" && parent === \"conversations\") {\n const body = (await request.json()) as {\n status?: \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\n assignedAgentId?: string | null;\n optedOut?: boolean;\n };\n return json(await client.updateConversation({ ownerId, conversationId: action, ...body }));\n }\n\n return json({ error: \"Not found\" }, 404);\n } catch (err) {\n const status = (err as { status?: number }).status ?? 500;\n return json(\n {\n error: err instanceof Error ? err.message : \"Request failed\",\n code: (err as { code?: string }).code,\n detail: (err as { detail?: unknown }).detail,\n },\n status\n );\n }\n };\n}\n","import type { WaSignupLaunch } from \"./types\";\n\n/**\n * Opens Meta's Embedded Signup popup.\n *\n * Runs in the browser, holds no credentials, and returns the `code` your server\n * exchanges via `client.completeConnect()`. Nothing is connected until that\n * exchange happens.\n *\n * Meta's JS SDK must already be on the page — Embedded Signup only works\n * through `FB.login` with a `config_id`; a plain OAuth redirect gets a token but\n * skips the WABA creation the owner actually needs.\n */\n\ninterface FacebookSdk {\n init(params: { appId: string; cookie?: boolean; xfbml?: boolean; version: string }): void;\n login(\n callback: (response: { authResponse?: { code?: string }; status?: string }) => void,\n options: {\n config_id: string;\n response_type: string;\n override_default_response_type: boolean;\n extras?: Record<string, unknown>;\n }\n ): void;\n}\n\ndeclare global {\n interface Window {\n FB?: FacebookSdk;\n }\n}\n\nexport interface SignupOutcome {\n code: string;\n state: string;\n}\n\n/** Matches the version the server pins; a mismatch changes the response shape. */\nconst GRAPH_VERSION = \"v23.0\";\n\nexport function launchSignup(config: WaSignupLaunch): Promise<SignupOutcome> {\n return new Promise((resolve, reject) => {\n if (typeof window === \"undefined\" || !window.FB) {\n reject(\n new Error(\n \"Meta's JS SDK is not loaded. Add https://connect.facebook.net/en_US/sdk.js to the page before calling launchSignup().\"\n )\n );\n return;\n }\n\n window.FB.init({ appId: config.appId, cookie: true, xfbml: false, version: GRAPH_VERSION });\n\n window.FB.login(\n (response) => {\n const code = response.authResponse?.code;\n if (!code) {\n // Closing the popup is the common case and is not an error worth\n // throwing a stack trace over — but the caller still needs to know.\n reject(new Error(\"Sign-up was cancelled before a WhatsApp account was connected\"));\n return;\n }\n resolve({ code, state: config.state });\n },\n {\n config_id: config.configId,\n response_type: \"code\",\n // Without this Meta returns an access token to the *browser* instead of\n // a code, which would mean a tenant token in client-side JavaScript.\n override_default_response_type: true,\n extras: { sessionInfoVersion: 3 },\n }\n );\n });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types.ts","../src/client.ts","../src/handler.ts","../src/browser.ts","../src/callbacks.ts"],"sourcesContent":["export { WhatsappClient, createWhatsappClient, type WhatsappClientOptions } from \"./client\";\nexport { createInboxHandler, type InboxHandlerOptions } from \"./handler\";\nexport { launchSignup, type SignupOutcome } from \"./browser\";\nexport {\n verifyCallbackSignature,\n type WaCallbackBody,\n type WaCallbackEventName,\n} from \"./callbacks\";\nexport * from \"./types\";\n","/** Shared shapes. Mirrors the `/api/v1/wa` responses. */\n\nexport type WaTemplateCategory = \"MARKETING\" | \"UTILITY\" | \"AUTHENTICATION\" | \"SERVICE\";\nexport type WaTemplateStatus =\n | \"DRAFT\"\n | \"PENDING\"\n | \"APPROVED\"\n | \"REJECTED\"\n | \"PAUSED\"\n | \"DISABLED\";\nexport type WaMessageDirection = \"INBOUND\" | \"OUTBOUND\";\nexport type WaMessageStatus = \"QUEUED\" | \"SENT\" | \"DELIVERED\" | \"READ\" | \"FAILED\";\nexport type WaConversationStatus = \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\nexport type WaMatchType = \"EXACT\" | \"CONTAINS\" | \"STARTS_WITH\" | \"REGEX\" | \"ANY\";\n\n/**\n * Every failure carries a stable `code`, so callers branch on that rather than\n * matching on a message that may be reworded.\n */\nexport type WaErrorCode =\n | \"UNAUTHORIZED\"\n | \"PLATFORM_INACTIVE\"\n | \"TENANT_NOT_FOUND\"\n | \"INSUFFICIENT_FUNDS\"\n | \"NO_PRICE_RULE\"\n | \"INVALID_AMOUNT\"\n | \"DUPLICATE_REQUEST\"\n | \"INTERNAL\";\n\nexport class WhatsappError extends Error {\n constructor(\n readonly code: WaErrorCode | string,\n message: string,\n readonly status: number,\n readonly detail?: Record<string, unknown>\n ) {\n super(message);\n this.name = \"WhatsappError\";\n }\n\n /** The owner has run out of balance. `detail.shortfallPaise` says by how much. */\n get isInsufficientFunds(): boolean {\n return this.code === \"INSUFFICIENT_FUNDS\";\n }\n}\n\nexport interface WaNumber {\n phoneNumberId: string;\n displayNumber: string;\n verifiedName: string;\n status: \"PENDING\" | \"VERIFIED\" | \"REGISTERED\" | \"DISCONNECTED\";\n qualityRating?: string | null;\n messagingLimitTier?: string | null;\n}\n\nexport interface WaMessage {\n id: string;\n direction: WaMessageDirection;\n status: WaMessageStatus;\n contactPhone?: string;\n category?: WaTemplateCategory | null;\n payload: unknown;\n error?: string | null;\n sentAt?: string | null;\n deliveredAt?: string | null;\n readAt?: string | null;\n createdAt: string;\n}\n\nexport interface WaConversation {\n id: string;\n contactPhone: string;\n contactName?: string | null;\n windowExpiresAt?: string | null;\n /** Whether free-form text is legal right now. Computed server-side. */\n windowOpen: boolean;\n lastInboundAt?: string | null;\n lastOutboundAt?: string | null;\n optedOut: boolean;\n status: WaConversationStatus;\n assignedAgentId?: string | null;\n unreadCount: number;\n updatedAt: string;\n lastMessage?: WaMessage | null;\n}\n\nexport interface WaTemplate {\n id: string;\n /** The name on the tenant's own WABA. */\n name: string;\n /**\n * What YOU call this template on your side. Equal to `name` for anything\n * pushed by a sync, and the join key when the two ever have to differ.\n */\n sourceName?: string | null;\n /** Set once the tenant reworded their copy, so a sync must not overwrite it. */\n editedAt?: string | null;\n language: string;\n category: WaTemplateCategory;\n status: WaTemplateStatus;\n rejectionReason?: string | null;\n components?: unknown;\n submittedAt?: string | null;\n lastSyncedAt?: string | null;\n}\n\nexport interface WaAgent {\n id: string;\n externalAgentId: string;\n name: string;\n email?: string | null;\n role: \"AGENT\" | \"ADMIN\";\n active: boolean;\n}\n\nexport interface WaBalance {\n balancePaise: number;\n heldPaise: number;\n spendablePaise: number;\n}\n\nexport interface WaSendResult {\n messageId: string;\n metaMessageId: string;\n status: \"SENT\";\n category: WaTemplateCategory;\n tenantPricePaise: number;\n tenantBalancePaise: number;\n}\n\nexport interface WaSignupLaunch {\n appId: string;\n configId: string;\n scopes: string[];\n state: string;\n}\n","import {\n WhatsappError,\n type WaAgent,\n type WaBalance,\n type WaConversation,\n type WaMatchType,\n type WaMessage,\n type WaNumber,\n type WaSendResult,\n type WaSignupLaunch,\n type WaTemplate,\n type WaTemplateCategory,\n} from \"./types\";\n\n/**\n * The server-side client.\n *\n * **This holds your platform API key. It must never run in a browser** — the key\n * is scoped to your whole account, and every one of your customers' numbers is\n * reachable with it. Call it from a route handler or a server action, and give\n * the browser only what it needs. `createInboxSession()` exists precisely so the\n * inbox UI can work without the key ever leaving your server.\n *\n * `ownerId` is *your* user id for the business owner. We map it to a tenant on\n * our side, so you never handle a WABA id, a phone number id, or a Meta token.\n */\n\nexport interface WhatsappClientOptions {\n apiKey: string;\n /** Override for local development against a tunnel. */\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n}\n\ninterface ApiEnvelope<T> {\n success: boolean;\n data?: T;\n error?: string;\n code?: string;\n detail?: Record<string, unknown>;\n}\n\nconst DEFAULT_BASE_URL = \"https://glitchgrab.dev\";\n\nexport class WhatsappClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n\n constructor(options: WhatsappClientOptions) {\n if (!options.apiKey) throw new Error(\"@glitchgrab/whatsapp: apiKey is required\");\n\n if (typeof window !== \"undefined\") {\n // Loud on purpose. A key that reaches a bundle is a key that reaches every\n // visitor, and the failure is otherwise completely silent.\n console.error(\n \"[@glitchgrab/whatsapp] WhatsappClient is running in a browser. Your API key is now \" +\n \"public. Move this to a server route and use createInboxSession() for the UI.\"\n );\n }\n\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n }\n\n private async request<T>(\n path: string,\n init: { method?: string; body?: unknown; query?: Record<string, string | undefined> } = {}\n ): Promise<T> {\n const url = new URL(`${this.baseUrl}/api/v1/wa${path}`);\n for (const [key, value] of Object.entries(init.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n\n const res = await this.fetchImpl(url.toString(), {\n method: init.method ?? \"GET\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n ...(init.body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n let envelope: ApiEnvelope<T>;\n try {\n envelope = (await res.json()) as ApiEnvelope<T>;\n } catch {\n throw new WhatsappError(\"INTERNAL\", `Unexpected response (${res.status})`, res.status);\n }\n\n if (!res.ok || !envelope.success) {\n throw new WhatsappError(\n envelope.code ?? \"INTERNAL\",\n envelope.error ?? `Request failed (${res.status})`,\n res.status,\n envelope.detail\n );\n }\n\n return envelope.data as T;\n }\n\n // ── Onboarding ────────────────────────────────────────────────────────────\n\n /**\n * Starts Embedded Signup for one of your business owners.\n *\n * Returns config for Meta's JS SDK, not a redirect URL — a plain OAuth\n * redirect yields a token but skips WABA creation, which is the part the owner\n * actually needs. Pass the result to `launchSignup()` in the browser.\n */\n connect(params: { ownerId: string; ownerName?: string }): Promise<WaSignupLaunch> {\n return this.request(\"/signup/launch\", { method: \"POST\", body: params });\n }\n\n /** Exchanges the code Meta's popup returns. Call this from your server. */\n completeConnect(params: {\n ownerId: string;\n code: string;\n state?: string;\n }): Promise<{ wabaId: string; numbers: WaNumber[]; creditLineShared: boolean; warnings: string[] }> {\n return this.request(\"/signup/exchange\", { method: \"POST\", body: params });\n }\n\n numbers(params: { ownerId: string; refresh?: boolean }): Promise<{ numbers: WaNumber[] }> {\n return this.request(\"/numbers\", {\n query: { ownerId: params.ownerId, refresh: params.refresh ? \"true\" : undefined },\n });\n }\n\n // ── Templates ─────────────────────────────────────────────────────────────\n\n templates(params: { ownerId: string; status?: string }): Promise<{ templates: WaTemplate[] }> {\n return this.request(\"/templates\", {\n query: { ownerId: params.ownerId, status: params.status },\n });\n }\n\n saveTemplate(params: {\n ownerId: string;\n name: string;\n language: string;\n category: WaTemplateCategory;\n components: unknown[];\n /** What you call this template on your side. Defaults to `name`. */\n sourceName?: string;\n }): Promise<{ template: WaTemplate }> {\n return this.request(\"/templates\", { method: \"POST\", body: params });\n }\n\n /**\n * Rewords a template the tenant already has.\n *\n * The variables may not move: same `{{n}}` tokens, same order, same\n * components, or this rejects with 400. Senders fill them positionally, so a\n * reordered template quietly puts the wrong value in every message.\n *\n * Leaves the template a DRAFT — call `submitTemplate` to send it to Meta,\n * which resets an approved template to pending until it clears again.\n */\n editTemplate(params: {\n ownerId: string;\n templateId: string;\n components: unknown[];\n }): Promise<{ template: WaTemplate }> {\n return this.request(`/templates/${params.templateId}`, {\n method: \"PATCH\",\n body: { ownerId: params.ownerId, components: params.components },\n });\n }\n\n /** Sends a draft to Meta. The verdict arrives asynchronously — poll or wait. */\n submitTemplate(params: { ownerId: string; templateId: string }): Promise<{ template: WaTemplate }> {\n return this.request(`/templates/${params.templateId}/submit`, {\n method: \"POST\",\n body: { ownerId: params.ownerId },\n });\n }\n\n /** Reconciles against Meta now, rather than waiting for the hourly sweep. */\n syncTemplates(params: { ownerId: string }): Promise<{ checked: number; updated: number }> {\n return this.request(\"/templates/sync\", { method: \"POST\", body: params });\n }\n\n // ── Sending ───────────────────────────────────────────────────────────────\n\n /**\n * Sends a message from the owner's own number.\n *\n * With `template`, any time. With `body`, only inside the 24-hour window the\n * contact opened by messaging them — outside it this throws rather than\n * letting Meta accept the send and deliver nothing.\n *\n * Pass `refKey` to make a retry safe: the same key never charges twice.\n */\n send(params: {\n ownerId: string;\n to: string;\n template?: string;\n language?: string;\n components?: unknown[];\n body?: string;\n refKey?: string;\n }): Promise<WaSendResult> {\n return this.request(\"/messages/send\", { method: \"POST\", body: params });\n }\n\n messages(params: {\n ownerId: string;\n contact?: string;\n limit?: number;\n cursor?: string;\n }): Promise<{ messages: WaMessage[]; nextCursor: string | null }> {\n return this.request(\"/messages\", {\n query: {\n ownerId: params.ownerId,\n contact: params.contact,\n limit: params.limit?.toString(),\n cursor: params.cursor,\n },\n });\n }\n\n // ── Inbox ─────────────────────────────────────────────────────────────────\n\n conversations(params: {\n ownerId: string;\n status?: string;\n unread?: boolean;\n limit?: number;\n cursor?: string;\n }): Promise<{ conversations: WaConversation[]; nextCursor: string | null }> {\n return this.request(\"/conversations\", {\n query: {\n ownerId: params.ownerId,\n status: params.status,\n unread: params.unread ? \"true\" : undefined,\n limit: params.limit?.toString(),\n cursor: params.cursor,\n },\n });\n }\n\n conversation(params: {\n ownerId: string;\n conversationId: string;\n }): Promise<{ conversation: WaConversation & { messages: WaMessage[] } }> {\n return this.request(`/conversations/${params.conversationId}`, {\n query: { ownerId: params.ownerId },\n });\n }\n\n updateConversation(params: {\n ownerId: string;\n conversationId: string;\n status?: \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\n assignedAgentId?: string | null;\n optedOut?: boolean;\n }): Promise<{ updated: boolean }> {\n const { conversationId, ...body } = params;\n return this.request(`/conversations/${conversationId}`, { method: \"PATCH\", body });\n }\n\n agents(params: { ownerId: string; includeInactive?: boolean }): Promise<{ agents: WaAgent[] }> {\n return this.request(\"/agents\", {\n query: {\n ownerId: params.ownerId,\n includeInactive: params.includeInactive ? \"true\" : undefined,\n },\n });\n }\n\n saveAgent(params: {\n ownerId: string;\n agentId: string;\n name: string;\n email?: string;\n role?: \"AGENT\" | \"ADMIN\";\n active?: boolean;\n }): Promise<{ agent: WaAgent }> {\n return this.request(\"/agents\", { method: \"POST\", body: params });\n }\n\n // ── Autoreply ─────────────────────────────────────────────────────────────\n\n autoreplyRules(params: { ownerId: string }): Promise<{ rules: unknown[] }> {\n return this.request(\"/autoreply/rules\", { query: { ownerId: params.ownerId } });\n }\n\n createAutoreplyRule(params: {\n ownerId: string;\n name: string;\n matchType: WaMatchType;\n pattern?: string;\n replyText: string;\n priority?: number;\n }): Promise<{ rule: unknown }> {\n return this.request(\"/autoreply/rules\", { method: \"POST\", body: params });\n }\n\n // ── Wallet ────────────────────────────────────────────────────────────────\n\n /**\n * Adds balance for one of your owners, after you have collected the money on\n * your own rails. We hold the ledger; we never hold your customer's funds.\n */\n credit(params: {\n ownerId: string;\n amountPaise: number;\n refKey?: string;\n note?: string;\n }): Promise<WaBalance> {\n return this.request(\"/wallet/credit\", { method: \"POST\", body: params });\n }\n\n balance(params: { ownerId?: string } = {}): Promise<WaBalance> {\n return this.request(\"/wallet/balance\", { query: { ownerId: params.ownerId } });\n }\n\n // ── Inbox session ─────────────────────────────────────────────────────────\n\n /**\n * Mints a short-lived, owner-scoped session for the inbox UI.\n *\n * Call this from a server route and return the result to your page. It is\n * what lets `<WhatsappInbox>` talk to us without your API key ever reaching\n * the browser. The ticket lasts sixty seconds; the component refreshes it\n * through the same route on reconnect.\n */\n async createInboxSession(params: {\n ownerId: string;\n }): Promise<{ ownerId: string; ticket: string; expiresIn: number; baseUrl: string }> {\n const data = await this.request<{ ticket: string; expiresIn: number }>(\"/inbox/ticket\", {\n method: \"POST\",\n body: { ownerId: params.ownerId },\n });\n return { ownerId: params.ownerId, baseUrl: this.baseUrl, ...data };\n }\n}\n\nexport function createWhatsappClient(options: WhatsappClientOptions): WhatsappClient {\n return new WhatsappClient(options);\n}\n","import type { WhatsappClient } from \"./client\";\n\n/**\n * A ready-made proxy route for the inbox UI.\n *\n * The browser must never hold your platform API key, so `<WhatsappInbox>` talks\n * to *your* server, and your server talks to us. This builds that middle layer\n * so you do not have to hand-write five thin fetch wrappers.\n *\n * ```ts\n * // app/api/whatsapp/[...action]/route.ts\n * import { createWhatsappClient, createInboxHandler } from \"@glitchgrab/whatsapp\";\n *\n * const client = createWhatsappClient({ apiKey: process.env.GG_WA_KEY! });\n *\n * const handler = createInboxHandler({\n * client,\n * // The single most important line here: derive the owner from YOUR session,\n * // never from the request body. Returning a client-supplied id would let any\n * // signed-in user read any other library's WhatsApp.\n * resolveOwnerId: async () => (await auth()).user.libraryId,\n * });\n *\n * export const GET = handler;\n * export const POST = handler;\n * ```\n */\n\nexport interface InboxHandlerOptions {\n client: WhatsappClient;\n /**\n * Returns the owner id for the current request, from your own auth. Return\n * null to deny.\n */\n resolveOwnerId: (request: Request) => Promise<string | null> | string | null;\n /** Set false to make the inbox read-only for this route. Default true. */\n allowSend?: boolean;\n}\n\nconst JSON_HEADERS = { \"Content-Type\": \"application/json\" };\n\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });\n}\n\nexport function createInboxHandler(options: InboxHandlerOptions) {\n const { client, resolveOwnerId, allowSend = true } = options;\n\n return async function handler(request: Request): Promise<Response> {\n let ownerId: string | null;\n try {\n ownerId = await resolveOwnerId(request);\n } catch {\n ownerId = null;\n }\n\n if (!ownerId) return json({ error: \"Unauthorized\" }, 401);\n\n const url = new URL(request.url);\n // Everything after the mount point, so the route works wherever it is\n // mounted rather than assuming a fixed prefix.\n const segments = url.pathname.split(\"/\").filter(Boolean);\n const action = segments[segments.length - 1] ?? \"\";\n // Avoids Array.prototype.at: this package ships into other people's\n // runtimes, and ES2022 methods are not worth the compatibility floor.\n const parent = segments[segments.length - 2] ?? \"\";\n\n try {\n if (request.method === \"GET\" && action === \"conversations\") {\n return json(\n await client.conversations({\n ownerId,\n status: url.searchParams.get(\"status\") ?? undefined,\n unread: url.searchParams.get(\"unread\") === \"true\",\n limit: Number(url.searchParams.get(\"limit\")) || undefined,\n cursor: url.searchParams.get(\"cursor\") ?? undefined,\n })\n );\n }\n\n // .../conversations/<id>\n if (request.method === \"GET\" && parent === \"conversations\") {\n return json(await client.conversation({ ownerId, conversationId: action }));\n }\n\n if (request.method === \"GET\" && action === \"agents\") {\n return json(await client.agents({ ownerId }));\n }\n\n if (request.method === \"POST\" && action === \"ticket\") {\n return json(await client.createInboxSession({ ownerId }));\n }\n\n if (request.method === \"POST\" && action === \"send\") {\n if (!allowSend) return json({ error: \"Sending is disabled\" }, 403);\n const body = (await request.json()) as {\n to?: string;\n body?: string;\n template?: string;\n language?: string;\n components?: unknown[];\n };\n const { to } = body;\n if (!to) return json({ error: \"to is required\" }, 400);\n return json(await client.send({ ...body, ownerId, to }));\n }\n\n if (request.method === \"PATCH\" && parent === \"conversations\") {\n const body = (await request.json()) as {\n status?: \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\n assignedAgentId?: string | null;\n optedOut?: boolean;\n };\n return json(await client.updateConversation({ ownerId, conversationId: action, ...body }));\n }\n\n return json({ error: \"Not found\" }, 404);\n } catch (err) {\n const status = (err as { status?: number }).status ?? 500;\n return json(\n {\n error: err instanceof Error ? err.message : \"Request failed\",\n code: (err as { code?: string }).code,\n detail: (err as { detail?: unknown }).detail,\n },\n status\n );\n }\n };\n}\n","import type { WaSignupLaunch } from \"./types\";\n\n/**\n * Opens Meta's Embedded Signup popup.\n *\n * Runs in the browser, holds no credentials, and returns the `code` your server\n * exchanges via `client.completeConnect()`. Nothing is connected until that\n * exchange happens.\n *\n * Meta's JS SDK must already be on the page — Embedded Signup only works\n * through `FB.login` with a `config_id`; a plain OAuth redirect gets a token but\n * skips the WABA creation the owner actually needs.\n */\n\ninterface FacebookSdk {\n init(params: { appId: string; cookie?: boolean; xfbml?: boolean; version: string }): void;\n login(\n callback: (response: { authResponse?: { code?: string }; status?: string }) => void,\n options: {\n config_id: string;\n response_type: string;\n override_default_response_type: boolean;\n extras?: Record<string, unknown>;\n }\n ): void;\n}\n\ndeclare global {\n interface Window {\n FB?: FacebookSdk;\n }\n}\n\nexport interface SignupOutcome {\n code: string;\n state: string;\n}\n\n/** Matches the version the server pins; a mismatch changes the response shape. */\nconst GRAPH_VERSION = \"v23.0\";\n\nexport function launchSignup(config: WaSignupLaunch): Promise<SignupOutcome> {\n return new Promise((resolve, reject) => {\n if (typeof window === \"undefined\" || !window.FB) {\n reject(\n new Error(\n \"Meta's JS SDK is not loaded. Add https://connect.facebook.net/en_US/sdk.js to the page before calling launchSignup().\"\n )\n );\n return;\n }\n\n window.FB.init({ appId: config.appId, cookie: true, xfbml: false, version: GRAPH_VERSION });\n\n window.FB.login(\n (response) => {\n const code = response.authResponse?.code;\n if (!code) {\n // Closing the popup is the common case and is not an error worth\n // throwing a stack trace over — but the caller still needs to know.\n reject(new Error(\"Sign-up was cancelled before a WhatsApp account was connected\"));\n return;\n }\n resolve({ code, state: config.state });\n },\n {\n config_id: config.configId,\n response_type: \"code\",\n // Without this Meta returns an access token to the *browser* instead of\n // a code, which would mean a tenant token in client-side JavaScript.\n override_default_response_type: true,\n extras: { sessionInfoVersion: 3 },\n }\n );\n });\n}\n","import { createHmac, timingSafeEqual } from \"crypto\";\n\n/**\n * Verifying a callback Glitchgrab sent you.\n *\n * Every callback carries `X-Glitchgrab-Signature: sha256=<hex>`, an HMAC of the\n * raw request body under your platform's callback secret. Check it before you\n * trust a word of the payload: the endpoint is a public URL, and an unverified\n * one lets anyone tell you a message failed, a template was approved, or a\n * wallet ran dry.\n *\n * Node only — this lives on the server entry, never the React one.\n */\n\n/**\n * The events a platform can receive.\n *\n * Duplicated from `apps/web/lib/wa/callbacks.ts` in the Glitchgrab repo, which\n * is the side that emits them. This package ships to other repos and cannot\n * import server code, so the two are kept in step by hand — adding an event\n * means editing both, or this type quietly under-reports what arrives.\n */\nexport type WaCallbackEventName =\n | \"message.status\"\n | \"message.inbound\"\n | \"template.status\"\n | \"wallet.low\";\n\nexport interface WaCallbackBody<T = Record<string, unknown>> {\n /** The outbox row id. Stable across retries — safe to dedupe on. */\n id: string;\n event: WaCallbackEventName;\n /** Your own id for the tenant, as you passed it to us. */\n tenantId: string | null;\n occurredAt: string;\n data: T;\n}\n\n/**\n * True when the signature matches.\n *\n * Pass the RAW body text, not a re-serialized object: `JSON.stringify` of a\n * parsed payload can reorder keys or change spacing, and the signature is over\n * bytes. In Next.js that means `await request.text()`, then parse it yourself.\n *\n * Constant-time — a fast string compare leaks a valid signature one byte at a\n * time to anyone willing to make enough requests.\n */\nexport function verifyCallbackSignature(\n secret: string,\n rawBody: string,\n header: string | null | undefined\n): boolean {\n if (!secret || !header) return false;\n\n const provided = header.startsWith(\"sha256=\") ? header.slice(7) : header;\n const expected = createHmac(\"sha256\", secret).update(rawBody).digest(\"hex\");\n\n const a = Buffer.from(expected);\n const b = Buffer.from(provided);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -85,6 +85,22 @@ var WhatsappClient = class {
|
|
|
85
85
|
saveTemplate(params) {
|
|
86
86
|
return this.request("/templates", { method: "POST", body: params });
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Rewords a template the tenant already has.
|
|
90
|
+
*
|
|
91
|
+
* The variables may not move: same `{{n}}` tokens, same order, same
|
|
92
|
+
* components, or this rejects with 400. Senders fill them positionally, so a
|
|
93
|
+
* reordered template quietly puts the wrong value in every message.
|
|
94
|
+
*
|
|
95
|
+
* Leaves the template a DRAFT — call `submitTemplate` to send it to Meta,
|
|
96
|
+
* which resets an approved template to pending until it clears again.
|
|
97
|
+
*/
|
|
98
|
+
editTemplate(params) {
|
|
99
|
+
return this.request(`/templates/${params.templateId}`, {
|
|
100
|
+
method: "PATCH",
|
|
101
|
+
body: { ownerId: params.ownerId, components: params.components }
|
|
102
|
+
});
|
|
103
|
+
}
|
|
88
104
|
/** Sends a draft to Meta. The verdict arrives asynchronously — poll or wait. */
|
|
89
105
|
submitTemplate(params) {
|
|
90
106
|
return this.request(`/templates/${params.templateId}/submit`, {
|
|
@@ -289,11 +305,23 @@ function launchSignup(config) {
|
|
|
289
305
|
);
|
|
290
306
|
});
|
|
291
307
|
}
|
|
308
|
+
|
|
309
|
+
// src/callbacks.ts
|
|
310
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
311
|
+
function verifyCallbackSignature(secret, rawBody, header) {
|
|
312
|
+
if (!secret || !header) return false;
|
|
313
|
+
const provided = header.startsWith("sha256=") ? header.slice(7) : header;
|
|
314
|
+
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
315
|
+
const a = Buffer.from(expected);
|
|
316
|
+
const b = Buffer.from(provided);
|
|
317
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
318
|
+
}
|
|
292
319
|
export {
|
|
293
320
|
WhatsappClient,
|
|
294
321
|
WhatsappError,
|
|
295
322
|
createInboxHandler,
|
|
296
323
|
createWhatsappClient,
|
|
297
|
-
launchSignup
|
|
324
|
+
launchSignup,
|
|
325
|
+
verifyCallbackSignature
|
|
298
326
|
};
|
|
299
327
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/client.ts","../src/handler.ts","../src/browser.ts"],"sourcesContent":["/** Shared shapes. Mirrors the `/api/v1/wa` responses. */\n\nexport type WaTemplateCategory = \"MARKETING\" | \"UTILITY\" | \"AUTHENTICATION\" | \"SERVICE\";\nexport type WaTemplateStatus =\n | \"DRAFT\"\n | \"PENDING\"\n | \"APPROVED\"\n | \"REJECTED\"\n | \"PAUSED\"\n | \"DISABLED\";\nexport type WaMessageDirection = \"INBOUND\" | \"OUTBOUND\";\nexport type WaMessageStatus = \"QUEUED\" | \"SENT\" | \"DELIVERED\" | \"READ\" | \"FAILED\";\nexport type WaConversationStatus = \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\nexport type WaMatchType = \"EXACT\" | \"CONTAINS\" | \"STARTS_WITH\" | \"REGEX\" | \"ANY\";\n\n/**\n * Every failure carries a stable `code`, so callers branch on that rather than\n * matching on a message that may be reworded.\n */\nexport type WaErrorCode =\n | \"UNAUTHORIZED\"\n | \"PLATFORM_INACTIVE\"\n | \"TENANT_NOT_FOUND\"\n | \"INSUFFICIENT_FUNDS\"\n | \"NO_PRICE_RULE\"\n | \"INVALID_AMOUNT\"\n | \"DUPLICATE_REQUEST\"\n | \"INTERNAL\";\n\nexport class WhatsappError extends Error {\n constructor(\n readonly code: WaErrorCode | string,\n message: string,\n readonly status: number,\n readonly detail?: Record<string, unknown>\n ) {\n super(message);\n this.name = \"WhatsappError\";\n }\n\n /** The owner has run out of balance. `detail.shortfallPaise` says by how much. */\n get isInsufficientFunds(): boolean {\n return this.code === \"INSUFFICIENT_FUNDS\";\n }\n}\n\nexport interface WaNumber {\n phoneNumberId: string;\n displayNumber: string;\n verifiedName: string;\n status: \"PENDING\" | \"VERIFIED\" | \"REGISTERED\" | \"DISCONNECTED\";\n qualityRating?: string | null;\n messagingLimitTier?: string | null;\n}\n\nexport interface WaMessage {\n id: string;\n direction: WaMessageDirection;\n status: WaMessageStatus;\n contactPhone?: string;\n category?: WaTemplateCategory | null;\n payload: unknown;\n error?: string | null;\n sentAt?: string | null;\n deliveredAt?: string | null;\n readAt?: string | null;\n createdAt: string;\n}\n\nexport interface WaConversation {\n id: string;\n contactPhone: string;\n contactName?: string | null;\n windowExpiresAt?: string | null;\n /** Whether free-form text is legal right now. Computed server-side. */\n windowOpen: boolean;\n lastInboundAt?: string | null;\n lastOutboundAt?: string | null;\n optedOut: boolean;\n status: WaConversationStatus;\n assignedAgentId?: string | null;\n unreadCount: number;\n updatedAt: string;\n lastMessage?: WaMessage | null;\n}\n\nexport interface WaTemplate {\n id: string;\n name: string;\n language: string;\n category: WaTemplateCategory;\n status: WaTemplateStatus;\n rejectionReason?: string | null;\n components?: unknown;\n submittedAt?: string | null;\n lastSyncedAt?: string | null;\n}\n\nexport interface WaAgent {\n id: string;\n externalAgentId: string;\n name: string;\n email?: string | null;\n role: \"AGENT\" | \"ADMIN\";\n active: boolean;\n}\n\nexport interface WaBalance {\n balancePaise: number;\n heldPaise: number;\n spendablePaise: number;\n}\n\nexport interface WaSendResult {\n messageId: string;\n metaMessageId: string;\n status: \"SENT\";\n category: WaTemplateCategory;\n tenantPricePaise: number;\n tenantBalancePaise: number;\n}\n\nexport interface WaSignupLaunch {\n appId: string;\n configId: string;\n scopes: string[];\n state: string;\n}\n","import {\n WhatsappError,\n type WaAgent,\n type WaBalance,\n type WaConversation,\n type WaMatchType,\n type WaMessage,\n type WaNumber,\n type WaSendResult,\n type WaSignupLaunch,\n type WaTemplate,\n type WaTemplateCategory,\n} from \"./types\";\n\n/**\n * The server-side client.\n *\n * **This holds your platform API key. It must never run in a browser** — the key\n * is scoped to your whole account, and every one of your customers' numbers is\n * reachable with it. Call it from a route handler or a server action, and give\n * the browser only what it needs. `createInboxSession()` exists precisely so the\n * inbox UI can work without the key ever leaving your server.\n *\n * `ownerId` is *your* user id for the business owner. We map it to a tenant on\n * our side, so you never handle a WABA id, a phone number id, or a Meta token.\n */\n\nexport interface WhatsappClientOptions {\n apiKey: string;\n /** Override for local development against a tunnel. */\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n}\n\ninterface ApiEnvelope<T> {\n success: boolean;\n data?: T;\n error?: string;\n code?: string;\n detail?: Record<string, unknown>;\n}\n\nconst DEFAULT_BASE_URL = \"https://glitchgrab.dev\";\n\nexport class WhatsappClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n\n constructor(options: WhatsappClientOptions) {\n if (!options.apiKey) throw new Error(\"@glitchgrab/whatsapp: apiKey is required\");\n\n if (typeof window !== \"undefined\") {\n // Loud on purpose. A key that reaches a bundle is a key that reaches every\n // visitor, and the failure is otherwise completely silent.\n console.error(\n \"[@glitchgrab/whatsapp] WhatsappClient is running in a browser. Your API key is now \" +\n \"public. Move this to a server route and use createInboxSession() for the UI.\"\n );\n }\n\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n }\n\n private async request<T>(\n path: string,\n init: { method?: string; body?: unknown; query?: Record<string, string | undefined> } = {}\n ): Promise<T> {\n const url = new URL(`${this.baseUrl}/api/v1/wa${path}`);\n for (const [key, value] of Object.entries(init.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n\n const res = await this.fetchImpl(url.toString(), {\n method: init.method ?? \"GET\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n ...(init.body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n let envelope: ApiEnvelope<T>;\n try {\n envelope = (await res.json()) as ApiEnvelope<T>;\n } catch {\n throw new WhatsappError(\"INTERNAL\", `Unexpected response (${res.status})`, res.status);\n }\n\n if (!res.ok || !envelope.success) {\n throw new WhatsappError(\n envelope.code ?? \"INTERNAL\",\n envelope.error ?? `Request failed (${res.status})`,\n res.status,\n envelope.detail\n );\n }\n\n return envelope.data as T;\n }\n\n // ── Onboarding ────────────────────────────────────────────────────────────\n\n /**\n * Starts Embedded Signup for one of your business owners.\n *\n * Returns config for Meta's JS SDK, not a redirect URL — a plain OAuth\n * redirect yields a token but skips WABA creation, which is the part the owner\n * actually needs. Pass the result to `launchSignup()` in the browser.\n */\n connect(params: { ownerId: string; ownerName?: string }): Promise<WaSignupLaunch> {\n return this.request(\"/signup/launch\", { method: \"POST\", body: params });\n }\n\n /** Exchanges the code Meta's popup returns. Call this from your server. */\n completeConnect(params: {\n ownerId: string;\n code: string;\n state?: string;\n }): Promise<{ wabaId: string; numbers: WaNumber[]; creditLineShared: boolean; warnings: string[] }> {\n return this.request(\"/signup/exchange\", { method: \"POST\", body: params });\n }\n\n numbers(params: { ownerId: string; refresh?: boolean }): Promise<{ numbers: WaNumber[] }> {\n return this.request(\"/numbers\", {\n query: { ownerId: params.ownerId, refresh: params.refresh ? \"true\" : undefined },\n });\n }\n\n // ── Templates ─────────────────────────────────────────────────────────────\n\n templates(params: { ownerId: string; status?: string }): Promise<{ templates: WaTemplate[] }> {\n return this.request(\"/templates\", {\n query: { ownerId: params.ownerId, status: params.status },\n });\n }\n\n saveTemplate(params: {\n ownerId: string;\n name: string;\n language: string;\n category: WaTemplateCategory;\n components: unknown[];\n }): Promise<{ template: WaTemplate }> {\n return this.request(\"/templates\", { method: \"POST\", body: params });\n }\n\n /** Sends a draft to Meta. The verdict arrives asynchronously — poll or wait. */\n submitTemplate(params: { ownerId: string; templateId: string }): Promise<{ template: WaTemplate }> {\n return this.request(`/templates/${params.templateId}/submit`, {\n method: \"POST\",\n body: { ownerId: params.ownerId },\n });\n }\n\n /** Reconciles against Meta now, rather than waiting for the hourly sweep. */\n syncTemplates(params: { ownerId: string }): Promise<{ checked: number; updated: number }> {\n return this.request(\"/templates/sync\", { method: \"POST\", body: params });\n }\n\n // ── Sending ───────────────────────────────────────────────────────────────\n\n /**\n * Sends a message from the owner's own number.\n *\n * With `template`, any time. With `body`, only inside the 24-hour window the\n * contact opened by messaging them — outside it this throws rather than\n * letting Meta accept the send and deliver nothing.\n *\n * Pass `refKey` to make a retry safe: the same key never charges twice.\n */\n send(params: {\n ownerId: string;\n to: string;\n template?: string;\n language?: string;\n components?: unknown[];\n body?: string;\n refKey?: string;\n }): Promise<WaSendResult> {\n return this.request(\"/messages/send\", { method: \"POST\", body: params });\n }\n\n messages(params: {\n ownerId: string;\n contact?: string;\n limit?: number;\n cursor?: string;\n }): Promise<{ messages: WaMessage[]; nextCursor: string | null }> {\n return this.request(\"/messages\", {\n query: {\n ownerId: params.ownerId,\n contact: params.contact,\n limit: params.limit?.toString(),\n cursor: params.cursor,\n },\n });\n }\n\n // ── Inbox ─────────────────────────────────────────────────────────────────\n\n conversations(params: {\n ownerId: string;\n status?: string;\n unread?: boolean;\n limit?: number;\n cursor?: string;\n }): Promise<{ conversations: WaConversation[]; nextCursor: string | null }> {\n return this.request(\"/conversations\", {\n query: {\n ownerId: params.ownerId,\n status: params.status,\n unread: params.unread ? \"true\" : undefined,\n limit: params.limit?.toString(),\n cursor: params.cursor,\n },\n });\n }\n\n conversation(params: {\n ownerId: string;\n conversationId: string;\n }): Promise<{ conversation: WaConversation & { messages: WaMessage[] } }> {\n return this.request(`/conversations/${params.conversationId}`, {\n query: { ownerId: params.ownerId },\n });\n }\n\n updateConversation(params: {\n ownerId: string;\n conversationId: string;\n status?: \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\n assignedAgentId?: string | null;\n optedOut?: boolean;\n }): Promise<{ updated: boolean }> {\n const { conversationId, ...body } = params;\n return this.request(`/conversations/${conversationId}`, { method: \"PATCH\", body });\n }\n\n agents(params: { ownerId: string; includeInactive?: boolean }): Promise<{ agents: WaAgent[] }> {\n return this.request(\"/agents\", {\n query: {\n ownerId: params.ownerId,\n includeInactive: params.includeInactive ? \"true\" : undefined,\n },\n });\n }\n\n saveAgent(params: {\n ownerId: string;\n agentId: string;\n name: string;\n email?: string;\n role?: \"AGENT\" | \"ADMIN\";\n active?: boolean;\n }): Promise<{ agent: WaAgent }> {\n return this.request(\"/agents\", { method: \"POST\", body: params });\n }\n\n // ── Autoreply ─────────────────────────────────────────────────────────────\n\n autoreplyRules(params: { ownerId: string }): Promise<{ rules: unknown[] }> {\n return this.request(\"/autoreply/rules\", { query: { ownerId: params.ownerId } });\n }\n\n createAutoreplyRule(params: {\n ownerId: string;\n name: string;\n matchType: WaMatchType;\n pattern?: string;\n replyText: string;\n priority?: number;\n }): Promise<{ rule: unknown }> {\n return this.request(\"/autoreply/rules\", { method: \"POST\", body: params });\n }\n\n // ── Wallet ────────────────────────────────────────────────────────────────\n\n /**\n * Adds balance for one of your owners, after you have collected the money on\n * your own rails. We hold the ledger; we never hold your customer's funds.\n */\n credit(params: {\n ownerId: string;\n amountPaise: number;\n refKey?: string;\n note?: string;\n }): Promise<WaBalance> {\n return this.request(\"/wallet/credit\", { method: \"POST\", body: params });\n }\n\n balance(params: { ownerId?: string } = {}): Promise<WaBalance> {\n return this.request(\"/wallet/balance\", { query: { ownerId: params.ownerId } });\n }\n\n // ── Inbox session ─────────────────────────────────────────────────────────\n\n /**\n * Mints a short-lived, owner-scoped session for the inbox UI.\n *\n * Call this from a server route and return the result to your page. It is\n * what lets `<WhatsappInbox>` talk to us without your API key ever reaching\n * the browser. The ticket lasts sixty seconds; the component refreshes it\n * through the same route on reconnect.\n */\n async createInboxSession(params: {\n ownerId: string;\n }): Promise<{ ownerId: string; ticket: string; expiresIn: number; baseUrl: string }> {\n const data = await this.request<{ ticket: string; expiresIn: number }>(\"/inbox/ticket\", {\n method: \"POST\",\n body: { ownerId: params.ownerId },\n });\n return { ownerId: params.ownerId, baseUrl: this.baseUrl, ...data };\n }\n}\n\nexport function createWhatsappClient(options: WhatsappClientOptions): WhatsappClient {\n return new WhatsappClient(options);\n}\n","import type { WhatsappClient } from \"./client\";\n\n/**\n * A ready-made proxy route for the inbox UI.\n *\n * The browser must never hold your platform API key, so `<WhatsappInbox>` talks\n * to *your* server, and your server talks to us. This builds that middle layer\n * so you do not have to hand-write five thin fetch wrappers.\n *\n * ```ts\n * // app/api/whatsapp/[...action]/route.ts\n * import { createWhatsappClient, createInboxHandler } from \"@glitchgrab/whatsapp\";\n *\n * const client = createWhatsappClient({ apiKey: process.env.GG_WA_KEY! });\n *\n * const handler = createInboxHandler({\n * client,\n * // The single most important line here: derive the owner from YOUR session,\n * // never from the request body. Returning a client-supplied id would let any\n * // signed-in user read any other library's WhatsApp.\n * resolveOwnerId: async () => (await auth()).user.libraryId,\n * });\n *\n * export const GET = handler;\n * export const POST = handler;\n * ```\n */\n\nexport interface InboxHandlerOptions {\n client: WhatsappClient;\n /**\n * Returns the owner id for the current request, from your own auth. Return\n * null to deny.\n */\n resolveOwnerId: (request: Request) => Promise<string | null> | string | null;\n /** Set false to make the inbox read-only for this route. Default true. */\n allowSend?: boolean;\n}\n\nconst JSON_HEADERS = { \"Content-Type\": \"application/json\" };\n\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });\n}\n\nexport function createInboxHandler(options: InboxHandlerOptions) {\n const { client, resolveOwnerId, allowSend = true } = options;\n\n return async function handler(request: Request): Promise<Response> {\n let ownerId: string | null;\n try {\n ownerId = await resolveOwnerId(request);\n } catch {\n ownerId = null;\n }\n\n if (!ownerId) return json({ error: \"Unauthorized\" }, 401);\n\n const url = new URL(request.url);\n // Everything after the mount point, so the route works wherever it is\n // mounted rather than assuming a fixed prefix.\n const segments = url.pathname.split(\"/\").filter(Boolean);\n const action = segments[segments.length - 1] ?? \"\";\n // Avoids Array.prototype.at: this package ships into other people's\n // runtimes, and ES2022 methods are not worth the compatibility floor.\n const parent = segments[segments.length - 2] ?? \"\";\n\n try {\n if (request.method === \"GET\" && action === \"conversations\") {\n return json(\n await client.conversations({\n ownerId,\n status: url.searchParams.get(\"status\") ?? undefined,\n unread: url.searchParams.get(\"unread\") === \"true\",\n limit: Number(url.searchParams.get(\"limit\")) || undefined,\n cursor: url.searchParams.get(\"cursor\") ?? undefined,\n })\n );\n }\n\n // .../conversations/<id>\n if (request.method === \"GET\" && parent === \"conversations\") {\n return json(await client.conversation({ ownerId, conversationId: action }));\n }\n\n if (request.method === \"GET\" && action === \"agents\") {\n return json(await client.agents({ ownerId }));\n }\n\n if (request.method === \"POST\" && action === \"ticket\") {\n return json(await client.createInboxSession({ ownerId }));\n }\n\n if (request.method === \"POST\" && action === \"send\") {\n if (!allowSend) return json({ error: \"Sending is disabled\" }, 403);\n const body = (await request.json()) as {\n to?: string;\n body?: string;\n template?: string;\n language?: string;\n components?: unknown[];\n };\n const { to } = body;\n if (!to) return json({ error: \"to is required\" }, 400);\n return json(await client.send({ ...body, ownerId, to }));\n }\n\n if (request.method === \"PATCH\" && parent === \"conversations\") {\n const body = (await request.json()) as {\n status?: \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\n assignedAgentId?: string | null;\n optedOut?: boolean;\n };\n return json(await client.updateConversation({ ownerId, conversationId: action, ...body }));\n }\n\n return json({ error: \"Not found\" }, 404);\n } catch (err) {\n const status = (err as { status?: number }).status ?? 500;\n return json(\n {\n error: err instanceof Error ? err.message : \"Request failed\",\n code: (err as { code?: string }).code,\n detail: (err as { detail?: unknown }).detail,\n },\n status\n );\n }\n };\n}\n","import type { WaSignupLaunch } from \"./types\";\n\n/**\n * Opens Meta's Embedded Signup popup.\n *\n * Runs in the browser, holds no credentials, and returns the `code` your server\n * exchanges via `client.completeConnect()`. Nothing is connected until that\n * exchange happens.\n *\n * Meta's JS SDK must already be on the page — Embedded Signup only works\n * through `FB.login` with a `config_id`; a plain OAuth redirect gets a token but\n * skips the WABA creation the owner actually needs.\n */\n\ninterface FacebookSdk {\n init(params: { appId: string; cookie?: boolean; xfbml?: boolean; version: string }): void;\n login(\n callback: (response: { authResponse?: { code?: string }; status?: string }) => void,\n options: {\n config_id: string;\n response_type: string;\n override_default_response_type: boolean;\n extras?: Record<string, unknown>;\n }\n ): void;\n}\n\ndeclare global {\n interface Window {\n FB?: FacebookSdk;\n }\n}\n\nexport interface SignupOutcome {\n code: string;\n state: string;\n}\n\n/** Matches the version the server pins; a mismatch changes the response shape. */\nconst GRAPH_VERSION = \"v23.0\";\n\nexport function launchSignup(config: WaSignupLaunch): Promise<SignupOutcome> {\n return new Promise((resolve, reject) => {\n if (typeof window === \"undefined\" || !window.FB) {\n reject(\n new Error(\n \"Meta's JS SDK is not loaded. Add https://connect.facebook.net/en_US/sdk.js to the page before calling launchSignup().\"\n )\n );\n return;\n }\n\n window.FB.init({ appId: config.appId, cookie: true, xfbml: false, version: GRAPH_VERSION });\n\n window.FB.login(\n (response) => {\n const code = response.authResponse?.code;\n if (!code) {\n // Closing the popup is the common case and is not an error worth\n // throwing a stack trace over — but the caller still needs to know.\n reject(new Error(\"Sign-up was cancelled before a WhatsApp account was connected\"));\n return;\n }\n resolve({ code, state: config.state });\n },\n {\n config_id: config.configId,\n response_type: \"code\",\n // Without this Meta returns an access token to the *browser* instead of\n // a code, which would mean a tenant token in client-side JavaScript.\n override_default_response_type: true,\n extras: { sessionInfoVersion: 3 },\n }\n );\n });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/client.ts","../src/handler.ts","../src/browser.ts","../src/callbacks.ts"],"sourcesContent":["/** Shared shapes. Mirrors the `/api/v1/wa` responses. */\n\nexport type WaTemplateCategory = \"MARKETING\" | \"UTILITY\" | \"AUTHENTICATION\" | \"SERVICE\";\nexport type WaTemplateStatus =\n | \"DRAFT\"\n | \"PENDING\"\n | \"APPROVED\"\n | \"REJECTED\"\n | \"PAUSED\"\n | \"DISABLED\";\nexport type WaMessageDirection = \"INBOUND\" | \"OUTBOUND\";\nexport type WaMessageStatus = \"QUEUED\" | \"SENT\" | \"DELIVERED\" | \"READ\" | \"FAILED\";\nexport type WaConversationStatus = \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\nexport type WaMatchType = \"EXACT\" | \"CONTAINS\" | \"STARTS_WITH\" | \"REGEX\" | \"ANY\";\n\n/**\n * Every failure carries a stable `code`, so callers branch on that rather than\n * matching on a message that may be reworded.\n */\nexport type WaErrorCode =\n | \"UNAUTHORIZED\"\n | \"PLATFORM_INACTIVE\"\n | \"TENANT_NOT_FOUND\"\n | \"INSUFFICIENT_FUNDS\"\n | \"NO_PRICE_RULE\"\n | \"INVALID_AMOUNT\"\n | \"DUPLICATE_REQUEST\"\n | \"INTERNAL\";\n\nexport class WhatsappError extends Error {\n constructor(\n readonly code: WaErrorCode | string,\n message: string,\n readonly status: number,\n readonly detail?: Record<string, unknown>\n ) {\n super(message);\n this.name = \"WhatsappError\";\n }\n\n /** The owner has run out of balance. `detail.shortfallPaise` says by how much. */\n get isInsufficientFunds(): boolean {\n return this.code === \"INSUFFICIENT_FUNDS\";\n }\n}\n\nexport interface WaNumber {\n phoneNumberId: string;\n displayNumber: string;\n verifiedName: string;\n status: \"PENDING\" | \"VERIFIED\" | \"REGISTERED\" | \"DISCONNECTED\";\n qualityRating?: string | null;\n messagingLimitTier?: string | null;\n}\n\nexport interface WaMessage {\n id: string;\n direction: WaMessageDirection;\n status: WaMessageStatus;\n contactPhone?: string;\n category?: WaTemplateCategory | null;\n payload: unknown;\n error?: string | null;\n sentAt?: string | null;\n deliveredAt?: string | null;\n readAt?: string | null;\n createdAt: string;\n}\n\nexport interface WaConversation {\n id: string;\n contactPhone: string;\n contactName?: string | null;\n windowExpiresAt?: string | null;\n /** Whether free-form text is legal right now. Computed server-side. */\n windowOpen: boolean;\n lastInboundAt?: string | null;\n lastOutboundAt?: string | null;\n optedOut: boolean;\n status: WaConversationStatus;\n assignedAgentId?: string | null;\n unreadCount: number;\n updatedAt: string;\n lastMessage?: WaMessage | null;\n}\n\nexport interface WaTemplate {\n id: string;\n /** The name on the tenant's own WABA. */\n name: string;\n /**\n * What YOU call this template on your side. Equal to `name` for anything\n * pushed by a sync, and the join key when the two ever have to differ.\n */\n sourceName?: string | null;\n /** Set once the tenant reworded their copy, so a sync must not overwrite it. */\n editedAt?: string | null;\n language: string;\n category: WaTemplateCategory;\n status: WaTemplateStatus;\n rejectionReason?: string | null;\n components?: unknown;\n submittedAt?: string | null;\n lastSyncedAt?: string | null;\n}\n\nexport interface WaAgent {\n id: string;\n externalAgentId: string;\n name: string;\n email?: string | null;\n role: \"AGENT\" | \"ADMIN\";\n active: boolean;\n}\n\nexport interface WaBalance {\n balancePaise: number;\n heldPaise: number;\n spendablePaise: number;\n}\n\nexport interface WaSendResult {\n messageId: string;\n metaMessageId: string;\n status: \"SENT\";\n category: WaTemplateCategory;\n tenantPricePaise: number;\n tenantBalancePaise: number;\n}\n\nexport interface WaSignupLaunch {\n appId: string;\n configId: string;\n scopes: string[];\n state: string;\n}\n","import {\n WhatsappError,\n type WaAgent,\n type WaBalance,\n type WaConversation,\n type WaMatchType,\n type WaMessage,\n type WaNumber,\n type WaSendResult,\n type WaSignupLaunch,\n type WaTemplate,\n type WaTemplateCategory,\n} from \"./types\";\n\n/**\n * The server-side client.\n *\n * **This holds your platform API key. It must never run in a browser** — the key\n * is scoped to your whole account, and every one of your customers' numbers is\n * reachable with it. Call it from a route handler or a server action, and give\n * the browser only what it needs. `createInboxSession()` exists precisely so the\n * inbox UI can work without the key ever leaving your server.\n *\n * `ownerId` is *your* user id for the business owner. We map it to a tenant on\n * our side, so you never handle a WABA id, a phone number id, or a Meta token.\n */\n\nexport interface WhatsappClientOptions {\n apiKey: string;\n /** Override for local development against a tunnel. */\n baseUrl?: string;\n fetch?: typeof globalThis.fetch;\n}\n\ninterface ApiEnvelope<T> {\n success: boolean;\n data?: T;\n error?: string;\n code?: string;\n detail?: Record<string, unknown>;\n}\n\nconst DEFAULT_BASE_URL = \"https://glitchgrab.dev\";\n\nexport class WhatsappClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof globalThis.fetch;\n\n constructor(options: WhatsappClientOptions) {\n if (!options.apiKey) throw new Error(\"@glitchgrab/whatsapp: apiKey is required\");\n\n if (typeof window !== \"undefined\") {\n // Loud on purpose. A key that reaches a bundle is a key that reaches every\n // visitor, and the failure is otherwise completely silent.\n console.error(\n \"[@glitchgrab/whatsapp] WhatsappClient is running in a browser. Your API key is now \" +\n \"public. Move this to a server route and use createInboxSession() for the UI.\"\n );\n }\n\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, \"\");\n this.fetchImpl = options.fetch ?? globalThis.fetch;\n }\n\n private async request<T>(\n path: string,\n init: { method?: string; body?: unknown; query?: Record<string, string | undefined> } = {}\n ): Promise<T> {\n const url = new URL(`${this.baseUrl}/api/v1/wa${path}`);\n for (const [key, value] of Object.entries(init.query ?? {})) {\n if (value !== undefined) url.searchParams.set(key, value);\n }\n\n const res = await this.fetchImpl(url.toString(), {\n method: init.method ?? \"GET\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n ...(init.body === undefined ? {} : { \"Content-Type\": \"application/json\" }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n let envelope: ApiEnvelope<T>;\n try {\n envelope = (await res.json()) as ApiEnvelope<T>;\n } catch {\n throw new WhatsappError(\"INTERNAL\", `Unexpected response (${res.status})`, res.status);\n }\n\n if (!res.ok || !envelope.success) {\n throw new WhatsappError(\n envelope.code ?? \"INTERNAL\",\n envelope.error ?? `Request failed (${res.status})`,\n res.status,\n envelope.detail\n );\n }\n\n return envelope.data as T;\n }\n\n // ── Onboarding ────────────────────────────────────────────────────────────\n\n /**\n * Starts Embedded Signup for one of your business owners.\n *\n * Returns config for Meta's JS SDK, not a redirect URL — a plain OAuth\n * redirect yields a token but skips WABA creation, which is the part the owner\n * actually needs. Pass the result to `launchSignup()` in the browser.\n */\n connect(params: { ownerId: string; ownerName?: string }): Promise<WaSignupLaunch> {\n return this.request(\"/signup/launch\", { method: \"POST\", body: params });\n }\n\n /** Exchanges the code Meta's popup returns. Call this from your server. */\n completeConnect(params: {\n ownerId: string;\n code: string;\n state?: string;\n }): Promise<{ wabaId: string; numbers: WaNumber[]; creditLineShared: boolean; warnings: string[] }> {\n return this.request(\"/signup/exchange\", { method: \"POST\", body: params });\n }\n\n numbers(params: { ownerId: string; refresh?: boolean }): Promise<{ numbers: WaNumber[] }> {\n return this.request(\"/numbers\", {\n query: { ownerId: params.ownerId, refresh: params.refresh ? \"true\" : undefined },\n });\n }\n\n // ── Templates ─────────────────────────────────────────────────────────────\n\n templates(params: { ownerId: string; status?: string }): Promise<{ templates: WaTemplate[] }> {\n return this.request(\"/templates\", {\n query: { ownerId: params.ownerId, status: params.status },\n });\n }\n\n saveTemplate(params: {\n ownerId: string;\n name: string;\n language: string;\n category: WaTemplateCategory;\n components: unknown[];\n /** What you call this template on your side. Defaults to `name`. */\n sourceName?: string;\n }): Promise<{ template: WaTemplate }> {\n return this.request(\"/templates\", { method: \"POST\", body: params });\n }\n\n /**\n * Rewords a template the tenant already has.\n *\n * The variables may not move: same `{{n}}` tokens, same order, same\n * components, or this rejects with 400. Senders fill them positionally, so a\n * reordered template quietly puts the wrong value in every message.\n *\n * Leaves the template a DRAFT — call `submitTemplate` to send it to Meta,\n * which resets an approved template to pending until it clears again.\n */\n editTemplate(params: {\n ownerId: string;\n templateId: string;\n components: unknown[];\n }): Promise<{ template: WaTemplate }> {\n return this.request(`/templates/${params.templateId}`, {\n method: \"PATCH\",\n body: { ownerId: params.ownerId, components: params.components },\n });\n }\n\n /** Sends a draft to Meta. The verdict arrives asynchronously — poll or wait. */\n submitTemplate(params: { ownerId: string; templateId: string }): Promise<{ template: WaTemplate }> {\n return this.request(`/templates/${params.templateId}/submit`, {\n method: \"POST\",\n body: { ownerId: params.ownerId },\n });\n }\n\n /** Reconciles against Meta now, rather than waiting for the hourly sweep. */\n syncTemplates(params: { ownerId: string }): Promise<{ checked: number; updated: number }> {\n return this.request(\"/templates/sync\", { method: \"POST\", body: params });\n }\n\n // ── Sending ───────────────────────────────────────────────────────────────\n\n /**\n * Sends a message from the owner's own number.\n *\n * With `template`, any time. With `body`, only inside the 24-hour window the\n * contact opened by messaging them — outside it this throws rather than\n * letting Meta accept the send and deliver nothing.\n *\n * Pass `refKey` to make a retry safe: the same key never charges twice.\n */\n send(params: {\n ownerId: string;\n to: string;\n template?: string;\n language?: string;\n components?: unknown[];\n body?: string;\n refKey?: string;\n }): Promise<WaSendResult> {\n return this.request(\"/messages/send\", { method: \"POST\", body: params });\n }\n\n messages(params: {\n ownerId: string;\n contact?: string;\n limit?: number;\n cursor?: string;\n }): Promise<{ messages: WaMessage[]; nextCursor: string | null }> {\n return this.request(\"/messages\", {\n query: {\n ownerId: params.ownerId,\n contact: params.contact,\n limit: params.limit?.toString(),\n cursor: params.cursor,\n },\n });\n }\n\n // ── Inbox ─────────────────────────────────────────────────────────────────\n\n conversations(params: {\n ownerId: string;\n status?: string;\n unread?: boolean;\n limit?: number;\n cursor?: string;\n }): Promise<{ conversations: WaConversation[]; nextCursor: string | null }> {\n return this.request(\"/conversations\", {\n query: {\n ownerId: params.ownerId,\n status: params.status,\n unread: params.unread ? \"true\" : undefined,\n limit: params.limit?.toString(),\n cursor: params.cursor,\n },\n });\n }\n\n conversation(params: {\n ownerId: string;\n conversationId: string;\n }): Promise<{ conversation: WaConversation & { messages: WaMessage[] } }> {\n return this.request(`/conversations/${params.conversationId}`, {\n query: { ownerId: params.ownerId },\n });\n }\n\n updateConversation(params: {\n ownerId: string;\n conversationId: string;\n status?: \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\n assignedAgentId?: string | null;\n optedOut?: boolean;\n }): Promise<{ updated: boolean }> {\n const { conversationId, ...body } = params;\n return this.request(`/conversations/${conversationId}`, { method: \"PATCH\", body });\n }\n\n agents(params: { ownerId: string; includeInactive?: boolean }): Promise<{ agents: WaAgent[] }> {\n return this.request(\"/agents\", {\n query: {\n ownerId: params.ownerId,\n includeInactive: params.includeInactive ? \"true\" : undefined,\n },\n });\n }\n\n saveAgent(params: {\n ownerId: string;\n agentId: string;\n name: string;\n email?: string;\n role?: \"AGENT\" | \"ADMIN\";\n active?: boolean;\n }): Promise<{ agent: WaAgent }> {\n return this.request(\"/agents\", { method: \"POST\", body: params });\n }\n\n // ── Autoreply ─────────────────────────────────────────────────────────────\n\n autoreplyRules(params: { ownerId: string }): Promise<{ rules: unknown[] }> {\n return this.request(\"/autoreply/rules\", { query: { ownerId: params.ownerId } });\n }\n\n createAutoreplyRule(params: {\n ownerId: string;\n name: string;\n matchType: WaMatchType;\n pattern?: string;\n replyText: string;\n priority?: number;\n }): Promise<{ rule: unknown }> {\n return this.request(\"/autoreply/rules\", { method: \"POST\", body: params });\n }\n\n // ── Wallet ────────────────────────────────────────────────────────────────\n\n /**\n * Adds balance for one of your owners, after you have collected the money on\n * your own rails. We hold the ledger; we never hold your customer's funds.\n */\n credit(params: {\n ownerId: string;\n amountPaise: number;\n refKey?: string;\n note?: string;\n }): Promise<WaBalance> {\n return this.request(\"/wallet/credit\", { method: \"POST\", body: params });\n }\n\n balance(params: { ownerId?: string } = {}): Promise<WaBalance> {\n return this.request(\"/wallet/balance\", { query: { ownerId: params.ownerId } });\n }\n\n // ── Inbox session ─────────────────────────────────────────────────────────\n\n /**\n * Mints a short-lived, owner-scoped session for the inbox UI.\n *\n * Call this from a server route and return the result to your page. It is\n * what lets `<WhatsappInbox>` talk to us without your API key ever reaching\n * the browser. The ticket lasts sixty seconds; the component refreshes it\n * through the same route on reconnect.\n */\n async createInboxSession(params: {\n ownerId: string;\n }): Promise<{ ownerId: string; ticket: string; expiresIn: number; baseUrl: string }> {\n const data = await this.request<{ ticket: string; expiresIn: number }>(\"/inbox/ticket\", {\n method: \"POST\",\n body: { ownerId: params.ownerId },\n });\n return { ownerId: params.ownerId, baseUrl: this.baseUrl, ...data };\n }\n}\n\nexport function createWhatsappClient(options: WhatsappClientOptions): WhatsappClient {\n return new WhatsappClient(options);\n}\n","import type { WhatsappClient } from \"./client\";\n\n/**\n * A ready-made proxy route for the inbox UI.\n *\n * The browser must never hold your platform API key, so `<WhatsappInbox>` talks\n * to *your* server, and your server talks to us. This builds that middle layer\n * so you do not have to hand-write five thin fetch wrappers.\n *\n * ```ts\n * // app/api/whatsapp/[...action]/route.ts\n * import { createWhatsappClient, createInboxHandler } from \"@glitchgrab/whatsapp\";\n *\n * const client = createWhatsappClient({ apiKey: process.env.GG_WA_KEY! });\n *\n * const handler = createInboxHandler({\n * client,\n * // The single most important line here: derive the owner from YOUR session,\n * // never from the request body. Returning a client-supplied id would let any\n * // signed-in user read any other library's WhatsApp.\n * resolveOwnerId: async () => (await auth()).user.libraryId,\n * });\n *\n * export const GET = handler;\n * export const POST = handler;\n * ```\n */\n\nexport interface InboxHandlerOptions {\n client: WhatsappClient;\n /**\n * Returns the owner id for the current request, from your own auth. Return\n * null to deny.\n */\n resolveOwnerId: (request: Request) => Promise<string | null> | string | null;\n /** Set false to make the inbox read-only for this route. Default true. */\n allowSend?: boolean;\n}\n\nconst JSON_HEADERS = { \"Content-Type\": \"application/json\" };\n\nfunction json(body: unknown, status = 200): Response {\n return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });\n}\n\nexport function createInboxHandler(options: InboxHandlerOptions) {\n const { client, resolveOwnerId, allowSend = true } = options;\n\n return async function handler(request: Request): Promise<Response> {\n let ownerId: string | null;\n try {\n ownerId = await resolveOwnerId(request);\n } catch {\n ownerId = null;\n }\n\n if (!ownerId) return json({ error: \"Unauthorized\" }, 401);\n\n const url = new URL(request.url);\n // Everything after the mount point, so the route works wherever it is\n // mounted rather than assuming a fixed prefix.\n const segments = url.pathname.split(\"/\").filter(Boolean);\n const action = segments[segments.length - 1] ?? \"\";\n // Avoids Array.prototype.at: this package ships into other people's\n // runtimes, and ES2022 methods are not worth the compatibility floor.\n const parent = segments[segments.length - 2] ?? \"\";\n\n try {\n if (request.method === \"GET\" && action === \"conversations\") {\n return json(\n await client.conversations({\n ownerId,\n status: url.searchParams.get(\"status\") ?? undefined,\n unread: url.searchParams.get(\"unread\") === \"true\",\n limit: Number(url.searchParams.get(\"limit\")) || undefined,\n cursor: url.searchParams.get(\"cursor\") ?? undefined,\n })\n );\n }\n\n // .../conversations/<id>\n if (request.method === \"GET\" && parent === \"conversations\") {\n return json(await client.conversation({ ownerId, conversationId: action }));\n }\n\n if (request.method === \"GET\" && action === \"agents\") {\n return json(await client.agents({ ownerId }));\n }\n\n if (request.method === \"POST\" && action === \"ticket\") {\n return json(await client.createInboxSession({ ownerId }));\n }\n\n if (request.method === \"POST\" && action === \"send\") {\n if (!allowSend) return json({ error: \"Sending is disabled\" }, 403);\n const body = (await request.json()) as {\n to?: string;\n body?: string;\n template?: string;\n language?: string;\n components?: unknown[];\n };\n const { to } = body;\n if (!to) return json({ error: \"to is required\" }, 400);\n return json(await client.send({ ...body, ownerId, to }));\n }\n\n if (request.method === \"PATCH\" && parent === \"conversations\") {\n const body = (await request.json()) as {\n status?: \"OPEN\" | \"SNOOZED\" | \"CLOSED\";\n assignedAgentId?: string | null;\n optedOut?: boolean;\n };\n return json(await client.updateConversation({ ownerId, conversationId: action, ...body }));\n }\n\n return json({ error: \"Not found\" }, 404);\n } catch (err) {\n const status = (err as { status?: number }).status ?? 500;\n return json(\n {\n error: err instanceof Error ? err.message : \"Request failed\",\n code: (err as { code?: string }).code,\n detail: (err as { detail?: unknown }).detail,\n },\n status\n );\n }\n };\n}\n","import type { WaSignupLaunch } from \"./types\";\n\n/**\n * Opens Meta's Embedded Signup popup.\n *\n * Runs in the browser, holds no credentials, and returns the `code` your server\n * exchanges via `client.completeConnect()`. Nothing is connected until that\n * exchange happens.\n *\n * Meta's JS SDK must already be on the page — Embedded Signup only works\n * through `FB.login` with a `config_id`; a plain OAuth redirect gets a token but\n * skips the WABA creation the owner actually needs.\n */\n\ninterface FacebookSdk {\n init(params: { appId: string; cookie?: boolean; xfbml?: boolean; version: string }): void;\n login(\n callback: (response: { authResponse?: { code?: string }; status?: string }) => void,\n options: {\n config_id: string;\n response_type: string;\n override_default_response_type: boolean;\n extras?: Record<string, unknown>;\n }\n ): void;\n}\n\ndeclare global {\n interface Window {\n FB?: FacebookSdk;\n }\n}\n\nexport interface SignupOutcome {\n code: string;\n state: string;\n}\n\n/** Matches the version the server pins; a mismatch changes the response shape. */\nconst GRAPH_VERSION = \"v23.0\";\n\nexport function launchSignup(config: WaSignupLaunch): Promise<SignupOutcome> {\n return new Promise((resolve, reject) => {\n if (typeof window === \"undefined\" || !window.FB) {\n reject(\n new Error(\n \"Meta's JS SDK is not loaded. Add https://connect.facebook.net/en_US/sdk.js to the page before calling launchSignup().\"\n )\n );\n return;\n }\n\n window.FB.init({ appId: config.appId, cookie: true, xfbml: false, version: GRAPH_VERSION });\n\n window.FB.login(\n (response) => {\n const code = response.authResponse?.code;\n if (!code) {\n // Closing the popup is the common case and is not an error worth\n // throwing a stack trace over — but the caller still needs to know.\n reject(new Error(\"Sign-up was cancelled before a WhatsApp account was connected\"));\n return;\n }\n resolve({ code, state: config.state });\n },\n {\n config_id: config.configId,\n response_type: \"code\",\n // Without this Meta returns an access token to the *browser* instead of\n // a code, which would mean a tenant token in client-side JavaScript.\n override_default_response_type: true,\n extras: { sessionInfoVersion: 3 },\n }\n );\n });\n}\n","import { createHmac, timingSafeEqual } from \"crypto\";\n\n/**\n * Verifying a callback Glitchgrab sent you.\n *\n * Every callback carries `X-Glitchgrab-Signature: sha256=<hex>`, an HMAC of the\n * raw request body under your platform's callback secret. Check it before you\n * trust a word of the payload: the endpoint is a public URL, and an unverified\n * one lets anyone tell you a message failed, a template was approved, or a\n * wallet ran dry.\n *\n * Node only — this lives on the server entry, never the React one.\n */\n\n/**\n * The events a platform can receive.\n *\n * Duplicated from `apps/web/lib/wa/callbacks.ts` in the Glitchgrab repo, which\n * is the side that emits them. This package ships to other repos and cannot\n * import server code, so the two are kept in step by hand — adding an event\n * means editing both, or this type quietly under-reports what arrives.\n */\nexport type WaCallbackEventName =\n | \"message.status\"\n | \"message.inbound\"\n | \"template.status\"\n | \"wallet.low\";\n\nexport interface WaCallbackBody<T = Record<string, unknown>> {\n /** The outbox row id. Stable across retries — safe to dedupe on. */\n id: string;\n event: WaCallbackEventName;\n /** Your own id for the tenant, as you passed it to us. */\n tenantId: string | null;\n occurredAt: string;\n data: T;\n}\n\n/**\n * True when the signature matches.\n *\n * Pass the RAW body text, not a re-serialized object: `JSON.stringify` of a\n * parsed payload can reorder keys or change spacing, and the signature is over\n * bytes. In Next.js that means `await request.text()`, then parse it yourself.\n *\n * Constant-time — a fast string compare leaks a valid signature one byte at a\n * time to anyone willing to make enough requests.\n */\nexport function verifyCallbackSignature(\n secret: string,\n rawBody: string,\n header: string | null | undefined\n): boolean {\n if (!secret || !header) return false;\n\n const provided = header.startsWith(\"sha256=\") ? header.slice(7) : header;\n const expected = createHmac(\"sha256\", secret).update(rawBody).digest(\"hex\");\n\n const a = Buffer.from(expected);\n const b = Buffer.from(provided);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n"],"mappings":"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","names":[]}
|
package/dist/react.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { b as WaConversation, f as WaMessage, W as WaAgent } from './types-
|
|
2
|
+
import { b as WaConversation, f as WaMessage, W as WaAgent } from './types-DdhmKV_Q.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The inbox's data layer.
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { b as WaConversation, f as WaMessage, W as WaAgent } from './types-
|
|
2
|
+
import { b as WaConversation, f as WaMessage, W as WaAgent } from './types-DdhmKV_Q.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The inbox's data layer.
|
|
@@ -57,7 +57,15 @@ interface WaConversation {
|
|
|
57
57
|
}
|
|
58
58
|
interface WaTemplate {
|
|
59
59
|
id: string;
|
|
60
|
+
/** The name on the tenant's own WABA. */
|
|
60
61
|
name: string;
|
|
62
|
+
/**
|
|
63
|
+
* What YOU call this template on your side. Equal to `name` for anything
|
|
64
|
+
* pushed by a sync, and the join key when the two ever have to differ.
|
|
65
|
+
*/
|
|
66
|
+
sourceName?: string | null;
|
|
67
|
+
/** Set once the tenant reworded their copy, so a sync must not overwrite it. */
|
|
68
|
+
editedAt?: string | null;
|
|
61
69
|
language: string;
|
|
62
70
|
category: WaTemplateCategory;
|
|
63
71
|
status: WaTemplateStatus;
|
|
@@ -57,7 +57,15 @@ interface WaConversation {
|
|
|
57
57
|
}
|
|
58
58
|
interface WaTemplate {
|
|
59
59
|
id: string;
|
|
60
|
+
/** The name on the tenant's own WABA. */
|
|
60
61
|
name: string;
|
|
62
|
+
/**
|
|
63
|
+
* What YOU call this template on your side. Equal to `name` for anything
|
|
64
|
+
* pushed by a sync, and the join key when the two ever have to differ.
|
|
65
|
+
*/
|
|
66
|
+
sourceName?: string | null;
|
|
67
|
+
/** Set once the tenant reworded their copy, so a sync must not overwrite it. */
|
|
68
|
+
editedAt?: string | null;
|
|
61
69
|
language: string;
|
|
62
70
|
category: WaTemplateCategory;
|
|
63
71
|
status: WaTemplateStatus;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glitchgrab/whatsapp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "WhatsApp Business messaging for SaaS products — connect a customer's own number, send templates, and run a shared inbox.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|