@glitchgrab/whatsapp 0.1.0 → 0.3.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 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-BKOEzRzb.mjs';
2
- export { c as WaConversationStatus, d as WaErrorCode, g as WaMessageDirection, h as WaMessageStatus, n as WaTemplateStatus, o as WhatsappError } from './types-BKOEzRzb.mjs';
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
  }>;
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-BKOEzRzb.js';
2
- export { c as WaConversationStatus, d as WaErrorCode, g as WaMessageDirection, h as WaMessageStatus, n as WaTemplateStatus, o as WhatsappError } from './types-BKOEzRzb.js';
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
  }>;
package/dist/index.js CHANGED
@@ -115,6 +115,22 @@ var WhatsappClient = class {
115
115
  saveTemplate(params) {
116
116
  return this.request("/templates", { method: "POST", body: params });
117
117
  }
118
+ /**
119
+ * Rewords a template the tenant already has.
120
+ *
121
+ * The variables may not move: same `{{n}}` tokens, same order, same
122
+ * components, or this rejects with 400. Senders fill them positionally, so a
123
+ * reordered template quietly puts the wrong value in every message.
124
+ *
125
+ * Leaves the template a DRAFT — call `submitTemplate` to send it to Meta,
126
+ * which resets an approved template to pending until it clears again.
127
+ */
128
+ editTemplate(params) {
129
+ return this.request(`/templates/${params.templateId}`, {
130
+ method: "PATCH",
131
+ body: { ownerId: params.ownerId, components: params.components }
132
+ });
133
+ }
118
134
  /** Sends a draft to Meta. The verdict arrives asynchronously — poll or wait. */
119
135
  submitTemplate(params) {
120
136
  return this.request(`/templates/${params.templateId}/submit`, {
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"],"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 /** 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"],"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`, {
@@ -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"],"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"],"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-BKOEzRzb.mjs';
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-BKOEzRzb.js';
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@glitchgrab/whatsapp",
3
- "version": "0.1.0",
4
- "description": "WhatsApp Business messaging for SaaS products \u2014 connect a customer's own number, send templates, and run a shared inbox.",
3
+ "version": "0.3.0",
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",
7
7
  "types": "dist/index.d.ts",
@@ -36,8 +36,16 @@
36
36
  }
37
37
  },
38
38
  "devDependencies": {
39
+ "@semantic-release/changelog": "^6",
40
+ "@semantic-release/commit-analyzer": "^13",
41
+ "@semantic-release/git": "^10",
42
+ "@semantic-release/github": "^11",
43
+ "@semantic-release/npm": "^12",
44
+ "@semantic-release/release-notes-generator": "^14",
39
45
  "@types/react": "^19",
46
+ "conventional-changelog-conventionalcommits": "^8",
40
47
  "react": "^19",
48
+ "semantic-release": "^24",
41
49
  "tsup": "^8",
42
50
  "typescript": "^5",
43
51
  "vitest": "^4.1.3"