@wassist/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1134 @@
1
+ 'use strict';
2
+
3
+ const __sdkRequire = require;
4
+
5
+ // src/errors.ts
6
+ var WassistError = class extends Error {
7
+ /** HTTP status code if this error originated from an API response. */
8
+ statusCode;
9
+ /** Machine-readable error code (e.g. `not_found`) when the API provides one. */
10
+ code;
11
+ /**
12
+ * Request ID surfaced by the Wassist API in the `X-Request-Id` response
13
+ * header. Include this when filing support tickets.
14
+ */
15
+ requestId;
16
+ /** Raw response body, parsed as JSON when possible. */
17
+ raw;
18
+ constructor(opts) {
19
+ super(opts.message);
20
+ this.name = new.target.name;
21
+ this.statusCode = opts.statusCode;
22
+ this.code = opts.code;
23
+ this.requestId = opts.requestId;
24
+ this.raw = opts.raw;
25
+ if (opts.cause !== void 0) {
26
+ this.cause = opts.cause;
27
+ }
28
+ Object.setPrototypeOf(this, new.target.prototype);
29
+ }
30
+ };
31
+ var WassistAuthenticationError = class extends WassistError {
32
+ };
33
+ var WassistPermissionError = class extends WassistError {
34
+ };
35
+ var WassistInvalidRequestError = class extends WassistError {
36
+ };
37
+ var WassistNotFoundError = class extends WassistError {
38
+ };
39
+ var WassistConflictError = class extends WassistError {
40
+ };
41
+ var WassistRateLimitError = class extends WassistError {
42
+ /** Suggested wait in seconds, from the `Retry-After` response header. */
43
+ retryAfter;
44
+ constructor(opts) {
45
+ super(opts);
46
+ this.retryAfter = opts.retryAfter;
47
+ }
48
+ };
49
+ var WassistAPIError = class extends WassistError {
50
+ };
51
+ var WassistConnectionError = class extends WassistError {
52
+ };
53
+ var WassistTimeoutError = class extends WassistError {
54
+ };
55
+ var WassistSignatureVerificationError = class extends WassistError {
56
+ /** The raw `X-Wassist-Signature` header value as received. */
57
+ header;
58
+ /** The payload that was verified, as the SDK saw it. */
59
+ payload;
60
+ constructor(opts) {
61
+ super(opts);
62
+ this.header = opts.header;
63
+ this.payload = opts.payload;
64
+ }
65
+ };
66
+ function errorFromResponse(args) {
67
+ const { status, body, requestId, retryAfter } = args;
68
+ const message = extractMessage(body) ?? `HTTP ${status}`;
69
+ const code = extractCode(body);
70
+ const base = { message, statusCode: status, code, requestId, raw: body };
71
+ if (status === 401) return new WassistAuthenticationError(base);
72
+ if (status === 403) return new WassistPermissionError(base);
73
+ if (status === 404) return new WassistNotFoundError(base);
74
+ if (status === 409) return new WassistConflictError(base);
75
+ if (status === 429) return new WassistRateLimitError({ ...base, retryAfter });
76
+ if (status === 400 || status === 422) {
77
+ return new WassistInvalidRequestError(base);
78
+ }
79
+ if (status >= 500 && status < 600) return new WassistAPIError(base);
80
+ return new WassistError(base);
81
+ }
82
+ function extractMessage(body) {
83
+ if (!body || typeof body !== "object") return void 0;
84
+ const b = body;
85
+ if (typeof b.error === "string") return b.error;
86
+ if (typeof b.message === "string") return b.message;
87
+ if (typeof b.detail === "string") return b.detail;
88
+ if (b.error && typeof b.error === "object") {
89
+ const inner = b.error.message;
90
+ if (typeof inner === "string") return inner;
91
+ }
92
+ return void 0;
93
+ }
94
+ function extractCode(body) {
95
+ if (!body || typeof body !== "object") return void 0;
96
+ const b = body;
97
+ if (typeof b.code === "string") return b.code;
98
+ return void 0;
99
+ }
100
+
101
+ // src/version.ts
102
+ var SDK_VERSION = "0.1.0";
103
+
104
+ // src/http.ts
105
+ var DEFAULT_BASE_URL = "https://backend.wassist.app";
106
+ var DEFAULT_TIMEOUT_MS = 6e4;
107
+ var DEFAULT_MAX_RETRIES = 2;
108
+ var API_VERSION_PREFIX = "/api/v1";
109
+ var RETRY_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
110
+ var HttpClient = class {
111
+ apiKey;
112
+ baseUrl;
113
+ timeout;
114
+ maxRetries;
115
+ fetchImpl;
116
+ constructor(config) {
117
+ if (!config.apiKey) {
118
+ throw new WassistError({
119
+ message: "A Wassist `apiKey` is required. Create one at https://wassist.app/settings."
120
+ });
121
+ }
122
+ this.apiKey = config.apiKey;
123
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
124
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
125
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
126
+ const provided = config.fetch ?? globalThis.fetch?.bind(globalThis);
127
+ if (!provided) {
128
+ throw new WassistError({
129
+ message: "No `fetch` implementation found. Pass one via `fetch` in the Wassist constructor, or run on Node 18+ / a runtime with a global `fetch`."
130
+ });
131
+ }
132
+ this.fetchImpl = provided;
133
+ }
134
+ get(path, args) {
135
+ return this.request({ ...args, method: "GET", path });
136
+ }
137
+ post(path, body, options) {
138
+ return this.request({ method: "POST", path, body, options });
139
+ }
140
+ put(path, body, options) {
141
+ return this.request({ method: "PUT", path, body, options });
142
+ }
143
+ patch(path, body, options) {
144
+ return this.request({ method: "PATCH", path, body, options });
145
+ }
146
+ delete(path, options) {
147
+ return this.request({ method: "DELETE", path, options });
148
+ }
149
+ async request(args) {
150
+ const url = this.buildUrl(args.path, args.query);
151
+ const headers = this.buildHeaders(args);
152
+ const body = args.body === void 0 ? void 0 : JSON.stringify(args.body);
153
+ const maxRetries = args.options?.maxRetries ?? this.maxRetries;
154
+ const timeout = args.options?.timeout ?? this.timeout;
155
+ let lastError;
156
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
157
+ try {
158
+ const response = await this.doFetch({
159
+ url,
160
+ method: args.method,
161
+ headers,
162
+ body,
163
+ timeout,
164
+ signal: args.options?.signal
165
+ });
166
+ if (response.ok) {
167
+ if (response.status === 204) return void 0;
168
+ const text = await response.text();
169
+ if (!text) return void 0;
170
+ return JSON.parse(text);
171
+ }
172
+ const requestId = response.headers.get("x-request-id") ?? void 0;
173
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
174
+ const parsedBody = await safeJson(response);
175
+ const apiError = errorFromResponse({
176
+ status: response.status,
177
+ body: parsedBody,
178
+ requestId,
179
+ retryAfter
180
+ });
181
+ const isRetryable = RETRY_STATUSES.has(response.status);
182
+ if (isRetryable && attempt < maxRetries) {
183
+ await sleep(computeBackoff(attempt, apiError));
184
+ lastError = apiError;
185
+ continue;
186
+ }
187
+ throw apiError;
188
+ } catch (err) {
189
+ if (err instanceof WassistError) {
190
+ if (attempt < maxRetries && (err instanceof WassistRateLimitError || isRetryableApiError(err))) {
191
+ await sleep(computeBackoff(attempt, err));
192
+ lastError = err;
193
+ continue;
194
+ }
195
+ throw err;
196
+ }
197
+ const mapped = mapFetchError(err);
198
+ if (attempt < maxRetries && isRetryableTransport(mapped)) {
199
+ await sleep(computeBackoff(attempt));
200
+ lastError = mapped;
201
+ continue;
202
+ }
203
+ throw mapped;
204
+ }
205
+ }
206
+ throw lastError instanceof Error ? lastError : new WassistError({ message: "Wassist request failed after retries." });
207
+ }
208
+ buildUrl(path, query) {
209
+ const prefixedPath = path.startsWith("/") ? path : `/${path}`;
210
+ const fullPath = prefixedPath.startsWith(API_VERSION_PREFIX) ? prefixedPath : `${API_VERSION_PREFIX}${prefixedPath}`;
211
+ const url = new URL(this.baseUrl + fullPath);
212
+ if (query) {
213
+ for (const [key, value] of Object.entries(query)) {
214
+ if (value === void 0 || value === null) continue;
215
+ url.searchParams.append(key, String(value));
216
+ }
217
+ }
218
+ return url.toString();
219
+ }
220
+ buildHeaders(args) {
221
+ const headers = {
222
+ "X-API-Key": this.apiKey,
223
+ "Accept": "application/json",
224
+ "User-Agent": `wassist-sdk-node/${SDK_VERSION}`,
225
+ "X-Wassist-Client": `wassist-sdk-node/${SDK_VERSION}`
226
+ };
227
+ if (args.body !== void 0) headers["Content-Type"] = "application/json";
228
+ if (args.options?.idempotencyKey) {
229
+ headers["Idempotency-Key"] = args.options.idempotencyKey;
230
+ }
231
+ if (args.options?.headers) {
232
+ for (const [k, v] of Object.entries(args.options.headers)) headers[k] = v;
233
+ }
234
+ return headers;
235
+ }
236
+ async doFetch(args) {
237
+ const controller = new AbortController();
238
+ const onAbort = () => controller.abort();
239
+ if (args.signal) {
240
+ if (args.signal.aborted) controller.abort();
241
+ else args.signal.addEventListener("abort", onAbort, { once: true });
242
+ }
243
+ const timeoutId = setTimeout(() => controller.abort(), args.timeout);
244
+ try {
245
+ return await this.fetchImpl(args.url, {
246
+ method: args.method,
247
+ headers: args.headers,
248
+ body: args.body,
249
+ signal: controller.signal
250
+ });
251
+ } finally {
252
+ clearTimeout(timeoutId);
253
+ if (args.signal) args.signal.removeEventListener("abort", onAbort);
254
+ }
255
+ }
256
+ };
257
+ function safeJson(response) {
258
+ return response.text().then((text) => {
259
+ if (!text) return void 0;
260
+ try {
261
+ return JSON.parse(text);
262
+ } catch {
263
+ return text;
264
+ }
265
+ }).catch(() => void 0);
266
+ }
267
+ function parseRetryAfter(value) {
268
+ if (!value) return void 0;
269
+ const asNum = Number(value);
270
+ if (Number.isFinite(asNum) && asNum >= 0) return asNum;
271
+ const asDate = Date.parse(value);
272
+ if (Number.isNaN(asDate)) return void 0;
273
+ return Math.max(0, Math.floor((asDate - Date.now()) / 1e3));
274
+ }
275
+ function computeBackoff(attempt, err) {
276
+ if (err instanceof WassistRateLimitError && typeof err.retryAfter === "number") {
277
+ return err.retryAfter * 1e3;
278
+ }
279
+ const base = 500 * Math.pow(2, attempt);
280
+ const jitter = base * Math.random();
281
+ return Math.min(base + jitter, 8e3);
282
+ }
283
+ function sleep(ms) {
284
+ return new Promise((resolve) => setTimeout(resolve, ms));
285
+ }
286
+ function isRetryableApiError(err) {
287
+ return err.statusCode !== void 0 && RETRY_STATUSES.has(err.statusCode);
288
+ }
289
+ function isRetryableTransport(err) {
290
+ return err instanceof WassistConnectionError;
291
+ }
292
+ function mapFetchError(err) {
293
+ if (err instanceof Error) {
294
+ if (err.name === "AbortError") {
295
+ return new WassistTimeoutError({
296
+ message: "Request timed out.",
297
+ cause: err
298
+ });
299
+ }
300
+ return new WassistConnectionError({
301
+ message: `Network error: ${err.message}`,
302
+ cause: err
303
+ });
304
+ }
305
+ return new WassistConnectionError({ message: "Unknown network error." });
306
+ }
307
+
308
+ // src/pagination.ts
309
+ var AutoPaginatedList = class {
310
+ fetchPage;
311
+ params;
312
+ options;
313
+ /** @internal */
314
+ constructor(fetchPage, params = {}, options) {
315
+ this.fetchPage = fetchPage;
316
+ this.params = params;
317
+ this.options = options;
318
+ }
319
+ /**
320
+ * Async iterator over every item across every page, fetched lazily.
321
+ */
322
+ async *[Symbol.asyncIterator]() {
323
+ const limit = this.params.limit ?? 50;
324
+ let offset = this.params.offset ?? 0;
325
+ let hasMore = true;
326
+ while (hasMore) {
327
+ const page = await this.fetchPage({ limit, offset });
328
+ for (const item of page.results) yield item;
329
+ hasMore = page.next !== null && page.results.length > 0;
330
+ offset += page.results.length || limit;
331
+ }
332
+ }
333
+ /**
334
+ * Fetch the first page only and return it as a {@link Page}.
335
+ */
336
+ async firstPage() {
337
+ const limit = this.params.limit ?? 50;
338
+ const offset = this.params.offset ?? 0;
339
+ const page = await this.fetchPage({ limit, offset });
340
+ return {
341
+ data: page.results,
342
+ totalCount: page.count,
343
+ hasMore: page.next !== null,
344
+ nextOffset: offset + page.results.length
345
+ };
346
+ }
347
+ /**
348
+ * Walk every page and collect every item into a single array.
349
+ *
350
+ * Convenient for small lists; for large ones prefer `for await`.
351
+ */
352
+ async all() {
353
+ const out = [];
354
+ for await (const item of this) out.push(item);
355
+ return out;
356
+ }
357
+ };
358
+
359
+ // src/resources/agents.ts
360
+ var AgentsResource = class {
361
+ /** @internal */
362
+ constructor(http) {
363
+ this.http = http;
364
+ }
365
+ http;
366
+ /**
367
+ * List every agent on your account.
368
+ *
369
+ * Returns a lazily-fetched paginated list. Use `for await` to walk all
370
+ * pages, or `.firstPage()` for just the first.
371
+ *
372
+ * `GET /agents/`
373
+ *
374
+ * @example
375
+ * ```ts
376
+ * for await (const agent of wassist.agents.list()) {
377
+ * console.log(agent.id, agent.name);
378
+ * }
379
+ * ```
380
+ */
381
+ list(params = {}, options) {
382
+ return new AutoPaginatedList(
383
+ (p) => this.http.get("/agents/", { query: p, options }),
384
+ params,
385
+ options
386
+ );
387
+ }
388
+ /**
389
+ * Retrieve a single agent by ID.
390
+ *
391
+ * `GET /agents/{id}/`
392
+ */
393
+ get(id, options) {
394
+ return this.http.get(`/agents/${encodeURIComponent(id)}/`, { options });
395
+ }
396
+ /**
397
+ * Create a new agent with the given display name. Configure it further
398
+ * with {@link AgentsResource.update}.
399
+ *
400
+ * `POST /agents/`
401
+ */
402
+ create(input, options) {
403
+ return this.http.post("/agents/", input, options);
404
+ }
405
+ /**
406
+ * Create a Bring-Your-Own-Agent (BYOA) — Wassist forwards inbound
407
+ * messages to your webhook and posts back replies you author yourself.
408
+ *
409
+ * `POST /agents/byoa/`
410
+ */
411
+ createBYOA(input, options) {
412
+ return this.http.post("/agents/byoa/", input, options);
413
+ }
414
+ /**
415
+ * Partially update an agent. Only the fields you pass are modified.
416
+ *
417
+ * Nested sub-resource arrays (tools, memory keys, etc.) are diffed
418
+ * server-side by `id`: include an `id` to update an existing entry,
419
+ * omit it to create one.
420
+ *
421
+ * `PATCH /agents/{id}/`
422
+ */
423
+ update(id, input, options) {
424
+ return this.http.patch(`/agents/${encodeURIComponent(id)}/`, input, options);
425
+ }
426
+ /**
427
+ * Soft-delete an agent. The agent stops responding immediately;
428
+ * conversation history is retained.
429
+ *
430
+ * `DELETE /agents/{id}/`
431
+ */
432
+ delete(id, options) {
433
+ return this.http.delete(`/agents/${encodeURIComponent(id)}/`, options);
434
+ }
435
+ };
436
+
437
+ // src/resources/messages.ts
438
+ var MessagesResource = class {
439
+ /** @internal */
440
+ constructor(http) {
441
+ this.http = http;
442
+ }
443
+ http;
444
+ /**
445
+ * List messages in a conversation, newest first.
446
+ *
447
+ * `GET /conversations/{id}/messages/`
448
+ */
449
+ list(conversationId, params = {}, options) {
450
+ const path = `/conversations/${encodeURIComponent(conversationId)}/messages/`;
451
+ return new AutoPaginatedList(
452
+ (p) => this.http.get(path, { query: p, options }),
453
+ params,
454
+ options
455
+ );
456
+ }
457
+ /**
458
+ * Send a message in a conversation.
459
+ *
460
+ * The input is a discriminated union — set `type` and populate the
461
+ * matching field (`text`, `template`, `cta`, or `unified`).
462
+ *
463
+ * `POST /conversations/{id}/messages/`
464
+ *
465
+ * @example Plain text
466
+ * ```ts
467
+ * await wassist.conversations.messages.send(conv.id, {
468
+ * type: 'text',
469
+ * text: { body: 'Hello!' },
470
+ * });
471
+ * ```
472
+ *
473
+ * @example Approved template
474
+ * ```ts
475
+ * await wassist.conversations.messages.send(conv.id, {
476
+ * type: 'template',
477
+ * template: { name: 'order_update', variables: { body: ['Alex', '#1234'] } },
478
+ * });
479
+ * ```
480
+ */
481
+ send(conversationId, input, options) {
482
+ const path = `/conversations/${encodeURIComponent(conversationId)}/messages/`;
483
+ return this.http.post(path, input, options);
484
+ }
485
+ };
486
+
487
+ // src/resources/conversations.ts
488
+ var ConversationsResource = class {
489
+ /** @internal */
490
+ constructor(http) {
491
+ this.http = http;
492
+ this.messages = new MessagesResource(http);
493
+ }
494
+ http;
495
+ /** Send and list messages within a conversation. */
496
+ messages;
497
+ /**
498
+ * List conversations on your account with rich filtering.
499
+ *
500
+ * `GET /conversations/`
501
+ *
502
+ * @example
503
+ * ```ts
504
+ * const recent = await wassist.conversations
505
+ * .list({ ordering: '-last_message_time', limit: 20 })
506
+ * .firstPage();
507
+ * ```
508
+ */
509
+ list(params = {}, options) {
510
+ const { limit, offset, ...filters } = params;
511
+ return new AutoPaginatedList(
512
+ (p) => this.http.get("/conversations/", {
513
+ query: { ...filters, ...p },
514
+ options
515
+ }),
516
+ { limit, offset },
517
+ options
518
+ );
519
+ }
520
+ /**
521
+ * Retrieve a single conversation by ID.
522
+ *
523
+ * `GET /conversations/{id}/`
524
+ */
525
+ get(id, options) {
526
+ return this.http.get(`/conversations/${encodeURIComponent(id)}/`, {
527
+ options
528
+ });
529
+ }
530
+ /**
531
+ * Create a new conversation with a contact.
532
+ *
533
+ * If the WhatsApp 24-hour reply window is closed for this contact, you
534
+ * must include a `template` message to open it.
535
+ *
536
+ * `POST /conversations/`
537
+ */
538
+ create(input, options) {
539
+ return this.http.post("/conversations/", input, options);
540
+ }
541
+ /**
542
+ * Prompt the agent in this conversation with a custom instruction.
543
+ * Equivalent to "wake the agent up and have it say this" — useful for
544
+ * outbound triggers and follow-ups.
545
+ *
546
+ * `POST /conversations/{id}/prompt/`
547
+ */
548
+ prompt(id, input, options) {
549
+ return this.http.post(
550
+ `/conversations/${encodeURIComponent(id)}/prompt/`,
551
+ input,
552
+ options
553
+ );
554
+ }
555
+ /**
556
+ * Send a WhatsApp read receipt for the most recent inbound message.
557
+ *
558
+ * `POST /conversations/{id}/read/`
559
+ */
560
+ read(id, options) {
561
+ return this.http.post(
562
+ `/conversations/${encodeURIComponent(id)}/read/`,
563
+ void 0,
564
+ options
565
+ );
566
+ }
567
+ /**
568
+ * Display the WhatsApp typing indicator on the contact's device.
569
+ *
570
+ * Note: WhatsApp also marks the most recent inbound message as read as
571
+ * a side effect — this is a platform limitation.
572
+ *
573
+ * `POST /conversations/{id}/typing/`
574
+ */
575
+ typing(id, options) {
576
+ return this.http.post(
577
+ `/conversations/${encodeURIComponent(id)}/typing/`,
578
+ void 0,
579
+ options
580
+ );
581
+ }
582
+ /**
583
+ * Route this conversation to a webhook. The agent pipeline is skipped
584
+ * while the override is active.
585
+ *
586
+ * Fires a `subscription.activated` event on the target webhook.
587
+ *
588
+ * `POST /conversations/{id}/subscribe/`
589
+ */
590
+ subscribe(id, input, options) {
591
+ return this.http.post(
592
+ `/conversations/${encodeURIComponent(id)}/subscribe/`,
593
+ input,
594
+ options
595
+ );
596
+ }
597
+ /**
598
+ * Clear the per-conversation routing override and fall back to the
599
+ * phone number's default routing.
600
+ *
601
+ * Fires a `subscription.revoked` event on the previously-subscribed
602
+ * webhook (if any).
603
+ *
604
+ * `POST /conversations/{id}/unsubscribe/`
605
+ */
606
+ unsubscribe(id, options) {
607
+ return this.http.post(
608
+ `/conversations/${encodeURIComponent(id)}/unsubscribe/`,
609
+ void 0,
610
+ options
611
+ );
612
+ }
613
+ };
614
+
615
+ // src/resources/phone-numbers.ts
616
+ var PhoneNumbersResource = class {
617
+ /** @internal */
618
+ constructor(http) {
619
+ this.http = http;
620
+ }
621
+ http;
622
+ /**
623
+ * List every WhatsApp number on your account.
624
+ *
625
+ * `GET /phone-numbers/`
626
+ */
627
+ list(params = {}, options) {
628
+ return new AutoPaginatedList(
629
+ (p) => this.http.get("/phone-numbers/", {
630
+ query: p,
631
+ options
632
+ }),
633
+ params,
634
+ options
635
+ );
636
+ }
637
+ /**
638
+ * View the WhatsApp Business Profile (about, address, websites, etc.)
639
+ * attached to this number.
640
+ *
641
+ * `GET /phone-numbers/{number}/business-profile/`
642
+ */
643
+ getBusinessProfile(number, options) {
644
+ return this.http.get(
645
+ `/phone-numbers/${encodeURIComponent(number)}/business-profile/`,
646
+ { options }
647
+ );
648
+ }
649
+ /**
650
+ * Route inbound messages on this number to a webhook by default. Drops
651
+ * any agent previously connected to the number.
652
+ *
653
+ * Phone-number routing changes do **not** fire
654
+ * `subscription.activated` / `.revoked` webhook events — those are
655
+ * reserved for per-conversation
656
+ * {@link ConversationsResource.subscribe} /
657
+ * {@link ConversationsResource.unsubscribe}.
658
+ *
659
+ * `POST /phone-numbers/{number}/subscribe/`
660
+ */
661
+ subscribe(number, input, options) {
662
+ return this.http.post(
663
+ `/phone-numbers/${encodeURIComponent(number)}/subscribe/`,
664
+ input,
665
+ options
666
+ );
667
+ }
668
+ /**
669
+ * Assign an agent as the default for this number. Drops any subscribed
670
+ * webhook.
671
+ *
672
+ * The agent must already be owned by or shared with the authenticated
673
+ * user.
674
+ *
675
+ * `POST /phone-numbers/{number}/connect-agent/`
676
+ */
677
+ connectAgent(number, input, options) {
678
+ return this.http.post(
679
+ `/phone-numbers/${encodeURIComponent(number)}/connect-agent/`,
680
+ input,
681
+ options
682
+ );
683
+ }
684
+ /**
685
+ * Clear default routing on this number entirely. Drops both the agent
686
+ * and the webhook.
687
+ *
688
+ * `POST /phone-numbers/{number}/unsubscribe/`
689
+ */
690
+ unsubscribe(number, input = {}, options) {
691
+ return this.http.post(
692
+ `/phone-numbers/${encodeURIComponent(number)}/unsubscribe/`,
693
+ input,
694
+ options
695
+ );
696
+ }
697
+ };
698
+
699
+ // src/resources/whatsapp-accounts.ts
700
+ var WhatsAppAccountsResource = class {
701
+ /** @internal */
702
+ constructor(http) {
703
+ this.http = http;
704
+ }
705
+ http;
706
+ /**
707
+ * List every WhatsApp Business Account on your Wassist account.
708
+ *
709
+ * `GET /whatsapp-accounts/`
710
+ */
711
+ list(params = {}, options) {
712
+ return new AutoPaginatedList(
713
+ (p) => this.http.get("/whatsapp-accounts/", {
714
+ query: p,
715
+ options
716
+ }),
717
+ params,
718
+ options
719
+ );
720
+ }
721
+ /**
722
+ * Retrieve a single WhatsApp Business Account by ID.
723
+ *
724
+ * `GET /whatsapp-accounts/{id}/`
725
+ */
726
+ get(id, options) {
727
+ return this.http.get(
728
+ `/whatsapp-accounts/${encodeURIComponent(id)}/`,
729
+ { options }
730
+ );
731
+ }
732
+ /**
733
+ * Add a pre-verified phone number (purchased via Meta's embedded
734
+ * signup flow) to a WhatsApp Business Account.
735
+ *
736
+ * `POST /whatsapp-accounts/{id}/add-number/`
737
+ */
738
+ addNumber(id, input, options) {
739
+ return this.http.post(
740
+ `/whatsapp-accounts/${encodeURIComponent(id)}/add-number/`,
741
+ input,
742
+ options
743
+ );
744
+ }
745
+ /**
746
+ * List Twilio phone numbers available for purchase + verification.
747
+ *
748
+ * `GET /available-numbers/`
749
+ */
750
+ availableNumbers(options) {
751
+ return this.http.get("/available-numbers/", { options });
752
+ }
753
+ };
754
+
755
+ // src/resources/whatsapp-link-sessions.ts
756
+ var WhatsAppLinkSessionsResource = class {
757
+ /** @internal */
758
+ constructor(http) {
759
+ this.http = http;
760
+ }
761
+ http;
762
+ /**
763
+ * List every link session you've created.
764
+ *
765
+ * `GET /whatsapp-link-sessions/`
766
+ */
767
+ list(params = {}, options) {
768
+ return new AutoPaginatedList(
769
+ (p) => this.http.get(
770
+ "/whatsapp-link-sessions/",
771
+ { query: p, options }
772
+ ),
773
+ params,
774
+ options
775
+ );
776
+ }
777
+ /**
778
+ * Retrieve a single link session by ID.
779
+ *
780
+ * `GET /whatsapp-link-sessions/{id}/`
781
+ */
782
+ get(id, options) {
783
+ return this.http.get(
784
+ `/whatsapp-link-sessions/${encodeURIComponent(id)}/`,
785
+ { options }
786
+ );
787
+ }
788
+ /**
789
+ * Create a new link session. Direct the user to the returned `linkUrl`.
790
+ *
791
+ * `POST /whatsapp-link-sessions/`
792
+ */
793
+ create(input, options) {
794
+ return this.http.post(
795
+ "/whatsapp-link-sessions/",
796
+ input,
797
+ options
798
+ );
799
+ }
800
+ /**
801
+ * Expire a pending link session early. Useful for invalidating stale
802
+ * URLs you've handed out to customers.
803
+ *
804
+ * `POST /whatsapp-link-sessions/{id}/expire/`
805
+ */
806
+ expire(id, options) {
807
+ return this.http.post(
808
+ `/whatsapp-link-sessions/${encodeURIComponent(id)}/expire/`,
809
+ void 0,
810
+ options
811
+ );
812
+ }
813
+ };
814
+
815
+ // src/resources/whatsapp-templates.ts
816
+ var WhatsAppTemplatesResource = class {
817
+ /** @internal */
818
+ constructor(http) {
819
+ this.http = http;
820
+ }
821
+ http;
822
+ /**
823
+ * List every template you've created in Wassist.
824
+ *
825
+ * `GET /whatsapp-templates/`
826
+ */
827
+ list(params = {}, options) {
828
+ return new AutoPaginatedList(
829
+ (p) => this.http.get("/whatsapp-templates/", {
830
+ query: p,
831
+ options
832
+ }),
833
+ params,
834
+ options
835
+ );
836
+ }
837
+ /**
838
+ * Retrieve a single template by ID.
839
+ *
840
+ * `GET /whatsapp-templates/{id}/`
841
+ */
842
+ get(id, options) {
843
+ return this.http.get(
844
+ `/whatsapp-templates/${encodeURIComponent(id)}/`,
845
+ { options }
846
+ );
847
+ }
848
+ /**
849
+ * Create a new template. Published per-WABA separately via {@link publish}.
850
+ *
851
+ * `POST /whatsapp-templates/`
852
+ */
853
+ create(input, options) {
854
+ return this.http.post("/whatsapp-templates/", input, options);
855
+ }
856
+ /**
857
+ * Update an existing template's metadata and components.
858
+ *
859
+ * `PATCH /whatsapp-templates/{id}/`
860
+ */
861
+ update(id, input, options) {
862
+ return this.http.patch(
863
+ `/whatsapp-templates/${encodeURIComponent(id)}/`,
864
+ input,
865
+ options
866
+ );
867
+ }
868
+ /**
869
+ * Delete a template (and unpublish from every WABA it was published to).
870
+ *
871
+ * `DELETE /whatsapp-templates/{id}/`
872
+ */
873
+ delete(id, options) {
874
+ return this.http.delete(
875
+ `/whatsapp-templates/${encodeURIComponent(id)}/`,
876
+ options
877
+ );
878
+ }
879
+ /**
880
+ * Submit this template to one or more WABAs for approval.
881
+ *
882
+ * Returns the updated template, plus per-account `publishResults` /
883
+ * `publishErrors`.
884
+ *
885
+ * `POST /whatsapp-templates/{id}/publish/`
886
+ */
887
+ publish(id, input, options) {
888
+ return this.http.post(
889
+ `/whatsapp-templates/${encodeURIComponent(id)}/publish/`,
890
+ input,
891
+ options
892
+ );
893
+ }
894
+ /**
895
+ * Remove this template from one or more WABAs.
896
+ *
897
+ * `POST /whatsapp-templates/{id}/unpublish/`
898
+ */
899
+ unpublish(id, input, options) {
900
+ return this.http.post(
901
+ `/whatsapp-templates/${encodeURIComponent(id)}/unpublish/`,
902
+ input,
903
+ options
904
+ );
905
+ }
906
+ };
907
+
908
+ // src/webhooks.ts
909
+ var DEFAULT_TOLERANCE_SECONDS = 300;
910
+ var Webhooks = class {
911
+ /**
912
+ * Verify the signature on a webhook delivery, parse the body, and return
913
+ * the typed event.
914
+ *
915
+ * @param payload The raw request body, exactly as received — `string`,
916
+ * `Buffer`, or `Uint8Array`. **Do not parse it first;**
917
+ * even whitespace differences will break verification.
918
+ * @param header The `X-Wassist-Signature` header value.
919
+ * @param secret Your webhook signing secret. Find it in the
920
+ * [Wassist dashboard](https://wassist.app/developers/webhooks).
921
+ * @param options.tolerance Replay-protection window in seconds (default 300).
922
+ * @throws {WassistSignatureVerificationError} When the header is malformed,
923
+ * the HMAC doesn't match, or the timestamp is outside the tolerance window.
924
+ */
925
+ constructEvent(payload, header, secret, options = {}) {
926
+ const { signedPayload, parts } = this.prepare(payload, header);
927
+ const expected = this.signSync(signedPayload, secret);
928
+ this.assertValid(parts, expected, header ?? "", signedPayload, options);
929
+ return this.parseEvent(payload);
930
+ }
931
+ /**
932
+ * Async variant of {@link constructEvent} that uses Web Crypto, for
933
+ * runtimes that don't expose `node:crypto` (Cloudflare Workers, Deno,
934
+ * the browser).
935
+ */
936
+ async constructEventAsync(payload, header, secret, options = {}) {
937
+ const { signedPayload, parts } = this.prepare(payload, header);
938
+ const expected = await this.signAsync(signedPayload, secret);
939
+ this.assertValid(parts, expected, header ?? "", signedPayload, options);
940
+ return this.parseEvent(payload);
941
+ }
942
+ /**
943
+ * Compute the v1 signature for a `t.<body>` payload — exported as a
944
+ * convenience for testing.
945
+ */
946
+ signSync(signedPayload, secret) {
947
+ const nodeCrypto = loadNodeCrypto();
948
+ if (!nodeCrypto) {
949
+ throw new WassistSignatureVerificationError({
950
+ message: "node:crypto is unavailable in this runtime. Use `constructEventAsync` instead."
951
+ });
952
+ }
953
+ return nodeCrypto.createHmac("sha256", secret).update(signedPayload).digest("hex");
954
+ }
955
+ /**
956
+ * Web Crypto variant of {@link signSync}.
957
+ */
958
+ async signAsync(signedPayload, secret) {
959
+ const subtle = globalThis.crypto?.subtle;
960
+ if (!subtle) {
961
+ throw new WassistSignatureVerificationError({
962
+ message: "Web Crypto (`globalThis.crypto.subtle`) is unavailable in this runtime."
963
+ });
964
+ }
965
+ const enc = new TextEncoder();
966
+ const key = await subtle.importKey(
967
+ "raw",
968
+ enc.encode(secret),
969
+ { name: "HMAC", hash: "SHA-256" },
970
+ false,
971
+ ["sign"]
972
+ );
973
+ const sig = await subtle.sign("HMAC", key, enc.encode(signedPayload));
974
+ return toHex(new Uint8Array(sig));
975
+ }
976
+ // =============================================================================
977
+ // Internals
978
+ // =============================================================================
979
+ prepare(payload, header) {
980
+ if (!header) {
981
+ throw new WassistSignatureVerificationError({
982
+ message: "Missing X-Wassist-Signature header.",
983
+ header: header ?? void 0
984
+ });
985
+ }
986
+ const parts = parseHeader(header);
987
+ if (!parts.t || !parts.v1) {
988
+ throw new WassistSignatureVerificationError({
989
+ message: "Malformed X-Wassist-Signature header \u2014 expected `t=<unix>,v1=<hex>`.",
990
+ header
991
+ });
992
+ }
993
+ const bodyText = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
994
+ return { signedPayload: `${parts.t}.${bodyText}`, parts };
995
+ }
996
+ assertValid(parts, expected, header, signedPayload, options) {
997
+ if (!constantTimeEqualsHex(parts.v1, expected)) {
998
+ throw new WassistSignatureVerificationError({
999
+ message: "Webhook signature did not match the expected value.",
1000
+ header,
1001
+ payload: signedPayload
1002
+ });
1003
+ }
1004
+ const tolerance = options.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
1005
+ if (tolerance > 0) {
1006
+ const now = options.currentTimestamp ?? Math.floor(Date.now() / 1e3);
1007
+ const ts = Number(parts.t);
1008
+ if (!Number.isFinite(ts)) {
1009
+ throw new WassistSignatureVerificationError({
1010
+ message: `Invalid timestamp in X-Wassist-Signature: ${parts.t}`,
1011
+ header
1012
+ });
1013
+ }
1014
+ if (Math.abs(now - ts) > tolerance) {
1015
+ throw new WassistSignatureVerificationError({
1016
+ message: `Webhook timestamp is outside the ${tolerance}s tolerance window (received ${ts}, now ${now}).`,
1017
+ header
1018
+ });
1019
+ }
1020
+ }
1021
+ }
1022
+ parseEvent(payload) {
1023
+ const text = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
1024
+ try {
1025
+ return JSON.parse(text);
1026
+ } catch (err) {
1027
+ throw new WassistSignatureVerificationError({
1028
+ message: "Failed to parse webhook payload as JSON.",
1029
+ payload: text,
1030
+ cause: err
1031
+ });
1032
+ }
1033
+ }
1034
+ };
1035
+ function parseHeader(header) {
1036
+ const parts = {};
1037
+ for (const segment of header.split(",")) {
1038
+ const eq = segment.indexOf("=");
1039
+ if (eq === -1) continue;
1040
+ const key = segment.slice(0, eq).trim();
1041
+ const value = segment.slice(eq + 1).trim();
1042
+ if (key === "t" || key === "v1") parts[key] = value;
1043
+ }
1044
+ return parts;
1045
+ }
1046
+ function constantTimeEqualsHex(a, b) {
1047
+ if (a.length !== b.length) return false;
1048
+ let diff = 0;
1049
+ for (let i = 0; i < a.length; i++) {
1050
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
1051
+ }
1052
+ return diff === 0;
1053
+ }
1054
+ function toHex(bytes) {
1055
+ let out = "";
1056
+ for (let i = 0; i < bytes.length; i++) {
1057
+ const v = bytes[i] ?? 0;
1058
+ out += v.toString(16).padStart(2, "0");
1059
+ }
1060
+ return out;
1061
+ }
1062
+ var webhooks = new Webhooks();
1063
+ var cachedNodeCrypto;
1064
+ function loadNodeCrypto() {
1065
+ if (cachedNodeCrypto !== void 0) return cachedNodeCrypto ?? void 0;
1066
+ try {
1067
+ const req = typeof __sdkRequire === "function" ? __sdkRequire : globalThis.require;
1068
+ cachedNodeCrypto = req?.("node:crypto") ?? null;
1069
+ } catch {
1070
+ cachedNodeCrypto = null;
1071
+ }
1072
+ return cachedNodeCrypto ?? void 0;
1073
+ }
1074
+
1075
+ // src/client.ts
1076
+ var Wassist = class {
1077
+ /** Create and manage AI agents. */
1078
+ agents;
1079
+ /** Send messages and manage conversations (with nested `messages` resource). */
1080
+ conversations;
1081
+ /** Manage your connected WhatsApp phone numbers and their routing. */
1082
+ phoneNumbers;
1083
+ /** Manage WhatsApp Business Accounts. */
1084
+ whatsappAccounts;
1085
+ /** Manage hosted WhatsApp account-linking sessions. */
1086
+ whatsappLinkSessions;
1087
+ /** Manage WhatsApp message templates. */
1088
+ whatsappTemplates;
1089
+ /** Verify inbound webhook signatures. */
1090
+ webhooks = webhooks;
1091
+ /**
1092
+ * Webhook helpers — usable without instantiating a client.
1093
+ *
1094
+ * @example
1095
+ * ```ts
1096
+ * const event = Wassist.webhooks.constructEvent(body, header, secret);
1097
+ * ```
1098
+ */
1099
+ static webhooks = webhooks;
1100
+ constructor(config) {
1101
+ const http = new HttpClient(config);
1102
+ this.agents = new AgentsResource(http);
1103
+ this.conversations = new ConversationsResource(http);
1104
+ this.phoneNumbers = new PhoneNumbersResource(http);
1105
+ this.whatsappAccounts = new WhatsAppAccountsResource(http);
1106
+ this.whatsappLinkSessions = new WhatsAppLinkSessionsResource(http);
1107
+ this.whatsappTemplates = new WhatsAppTemplatesResource(http);
1108
+ }
1109
+ };
1110
+
1111
+ exports.AgentsResource = AgentsResource;
1112
+ exports.AutoPaginatedList = AutoPaginatedList;
1113
+ exports.ConversationsResource = ConversationsResource;
1114
+ exports.MessagesResource = MessagesResource;
1115
+ exports.PhoneNumbersResource = PhoneNumbersResource;
1116
+ exports.SDK_VERSION = SDK_VERSION;
1117
+ exports.Wassist = Wassist;
1118
+ exports.WassistAPIError = WassistAPIError;
1119
+ exports.WassistAuthenticationError = WassistAuthenticationError;
1120
+ exports.WassistConflictError = WassistConflictError;
1121
+ exports.WassistConnectionError = WassistConnectionError;
1122
+ exports.WassistError = WassistError;
1123
+ exports.WassistInvalidRequestError = WassistInvalidRequestError;
1124
+ exports.WassistNotFoundError = WassistNotFoundError;
1125
+ exports.WassistPermissionError = WassistPermissionError;
1126
+ exports.WassistRateLimitError = WassistRateLimitError;
1127
+ exports.WassistSignatureVerificationError = WassistSignatureVerificationError;
1128
+ exports.WassistTimeoutError = WassistTimeoutError;
1129
+ exports.Webhooks = Webhooks;
1130
+ exports.WhatsAppAccountsResource = WhatsAppAccountsResource;
1131
+ exports.WhatsAppLinkSessionsResource = WhatsAppLinkSessionsResource;
1132
+ exports.WhatsAppTemplatesResource = WhatsAppTemplatesResource;
1133
+ //# sourceMappingURL=index.cjs.map
1134
+ //# sourceMappingURL=index.cjs.map