spectrum-ts 1.4.0 → 1.7.1

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.
@@ -0,0 +1,850 @@
1
+ import {
2
+ asPollOption,
3
+ cloud,
4
+ mergeStreams,
5
+ stream
6
+ } from "./chunk-WFIUWFE4.js";
7
+ import {
8
+ UnsupportedError,
9
+ asAttachment,
10
+ asContact,
11
+ asCustom,
12
+ asReaction,
13
+ asText,
14
+ definePlatform
15
+ } from "./chunk-JWWIFSI7.js";
16
+
17
+ // src/providers/whatsapp-business/index.ts
18
+ import { createClient as createClient2 } from "@photon-ai/whatsapp-business";
19
+
20
+ // src/providers/whatsapp-business/auth.ts
21
+ import {
22
+ createClient,
23
+ TypedEventStream
24
+ } from "@photon-ai/whatsapp-business";
25
+ var RENEWAL_RATIO = 0.8;
26
+ var EXPIRY_BUFFER_MS = 3e4;
27
+ var RETRY_DELAY_MS = 3e4;
28
+ var RESUBSCRIBE_BACKOFF_MS = 500;
29
+ var cloudAuthState = /* @__PURE__ */ new WeakMap();
30
+ async function createCloudClients(projectId, projectSecret) {
31
+ let tokenData = await cloud.issueWhatsappBusinessTokens(
32
+ projectId,
33
+ projectSecret
34
+ );
35
+ let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
36
+ let disposed = false;
37
+ let renewalTimer;
38
+ const lines = /* @__PURE__ */ new Map();
39
+ const buildRawClient = (phoneNumberId) => {
40
+ const accessToken = tokenData.auth[phoneNumberId];
41
+ if (!accessToken) {
42
+ throw new Error(
43
+ `WhatsApp Business line ${phoneNumberId} missing from token response`
44
+ );
45
+ }
46
+ return createClient({ accessToken, appSecret: "", phoneNumberId });
47
+ };
48
+ const refreshTokens = async () => {
49
+ tokenData = await cloud.issueWhatsappBusinessTokens(
50
+ projectId,
51
+ projectSecret
52
+ );
53
+ tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
54
+ for (const [phoneNumberId, state] of lines) {
55
+ if (!tokenData.auth[phoneNumberId]) {
56
+ continue;
57
+ }
58
+ const old = state.current;
59
+ state.current = buildRawClient(phoneNumberId);
60
+ for (const sub of state.subscriptions) {
61
+ sub.swap();
62
+ }
63
+ await old.close().catch(() => void 0);
64
+ }
65
+ };
66
+ const clearRenewalTimer = () => {
67
+ if (renewalTimer !== void 0) {
68
+ clearTimeout(renewalTimer);
69
+ renewalTimer = void 0;
70
+ }
71
+ };
72
+ const scheduleRenewal = () => {
73
+ if (disposed) {
74
+ return;
75
+ }
76
+ clearRenewalTimer();
77
+ const ttlMs = tokenData.expiresIn * 1e3;
78
+ const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
79
+ renewalTimer = setTimeout(async () => {
80
+ try {
81
+ await refreshTokens();
82
+ scheduleRenewal();
83
+ } catch (err) {
84
+ console.warn(
85
+ `[spectrum-ts] WhatsApp Business token refresh failed; retrying in ${RETRY_DELAY_MS}ms.`,
86
+ err
87
+ );
88
+ clearRenewalTimer();
89
+ renewalTimer = setTimeout(() => scheduleRenewal(), RETRY_DELAY_MS);
90
+ renewalTimer?.unref?.();
91
+ }
92
+ }, renewInMs);
93
+ renewalTimer?.unref?.();
94
+ };
95
+ const refreshIfNeeded = async () => {
96
+ if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) {
97
+ return;
98
+ }
99
+ await refreshTokens();
100
+ scheduleRenewal();
101
+ };
102
+ scheduleRenewal();
103
+ const clients = Object.keys(tokenData.auth).map(
104
+ (phoneNumberId) => {
105
+ const state = {
106
+ current: buildRawClient(phoneNumberId),
107
+ subscriptions: /* @__PURE__ */ new Set()
108
+ };
109
+ lines.set(phoneNumberId, state);
110
+ return buildClientProxy(state, refreshIfNeeded);
111
+ }
112
+ );
113
+ cloudAuthState.set(clients, {
114
+ dispose: async () => {
115
+ disposed = true;
116
+ clearRenewalTimer();
117
+ for (const state of lines.values()) {
118
+ for (const sub of state.subscriptions) {
119
+ sub.close();
120
+ }
121
+ }
122
+ await Promise.allSettled(
123
+ Array.from(lines.values()).map((s) => s.current.close())
124
+ );
125
+ lines.clear();
126
+ }
127
+ });
128
+ return clients;
129
+ }
130
+ async function disposeCloudAuth(clients) {
131
+ const auth = cloudAuthState.get(clients);
132
+ if (!auth) {
133
+ return;
134
+ }
135
+ await auth.dispose();
136
+ cloudAuthState.delete(clients);
137
+ }
138
+ var buildClientProxy = (state, refresh) => {
139
+ const forwarder = (pick) => new Proxy({}, {
140
+ get: (_, prop) => async (...args) => {
141
+ await refresh();
142
+ const target = pick(state.current);
143
+ const fn = target[prop];
144
+ return Reflect.apply(fn, pick(state.current), args);
145
+ }
146
+ });
147
+ const events = {
148
+ fetchMissed: async (opts) => {
149
+ await refresh();
150
+ return state.current.events.fetchMissed(opts);
151
+ },
152
+ subscribe: (options) => resubscribableStream(state, options)
153
+ };
154
+ return {
155
+ events,
156
+ media: forwarder((c) => c.media),
157
+ messages: forwarder((c) => c.messages),
158
+ close: async () => {
159
+ for (const sub of state.subscriptions) {
160
+ sub.close();
161
+ }
162
+ await state.current.close();
163
+ },
164
+ [Symbol.asyncDispose]: async () => {
165
+ for (const sub of state.subscriptions) {
166
+ sub.close();
167
+ }
168
+ await state.current.close();
169
+ }
170
+ };
171
+ };
172
+ var pumpOnce = async (ctx) => {
173
+ const sub = ctx.getCurrent().events.subscribe(ctx.options);
174
+ ctx.setActive(sub);
175
+ try {
176
+ for await (const event of sub) {
177
+ await ctx.emit(event);
178
+ }
179
+ return true;
180
+ } catch {
181
+ return false;
182
+ } finally {
183
+ ctx.setActive(void 0);
184
+ }
185
+ };
186
+ var resubscribableStream = (state, options) => {
187
+ let closed = false;
188
+ let active;
189
+ const source = stream((emit, end) => {
190
+ const ctx = {
191
+ emit,
192
+ getCurrent: () => state.current,
193
+ options,
194
+ setActive: (s) => {
195
+ active = s;
196
+ }
197
+ };
198
+ const pump = (async () => {
199
+ while (!closed) {
200
+ await pumpOnce(ctx);
201
+ if (!closed) {
202
+ await new Promise((r) => setTimeout(r, RESUBSCRIBE_BACKOFF_MS));
203
+ }
204
+ }
205
+ end();
206
+ })();
207
+ return async () => {
208
+ closed = true;
209
+ active?.close().catch(() => void 0);
210
+ active = void 0;
211
+ state.subscriptions.delete(subscription);
212
+ await pump;
213
+ };
214
+ });
215
+ const subscription = {
216
+ close: () => {
217
+ closed = true;
218
+ active?.close().catch(() => void 0);
219
+ },
220
+ swap: () => {
221
+ active?.close().catch(() => void 0);
222
+ }
223
+ };
224
+ state.subscriptions.add(subscription);
225
+ return new TypedEventStream(source, async () => {
226
+ closed = true;
227
+ active?.close().catch(() => void 0);
228
+ state.subscriptions.delete(subscription);
229
+ await source.close();
230
+ });
231
+ };
232
+
233
+ // src/providers/whatsapp-business/messages.ts
234
+ import { extension as mimeExtension } from "mime-types";
235
+
236
+ // src/providers/whatsapp-business/poll.ts
237
+ import {
238
+ button,
239
+ buttons,
240
+ list
241
+ } from "@photon-ai/whatsapp-business";
242
+ var MAX_BUTTON_OPTIONS = 3;
243
+ var LIST_BUTTON_TEXT = "View options";
244
+ var LIST_SECTION_TITLE = "Options";
245
+ var pollOptionId = (index) => `opt_${index}`;
246
+ var pollToInteractive = (content) => {
247
+ if (content.options.length <= MAX_BUTTON_OPTIONS) {
248
+ return buttons(
249
+ content.title,
250
+ ...content.options.map((o, i) => button(pollOptionId(i), o.title))
251
+ );
252
+ }
253
+ return list(content.title, LIST_BUTTON_TEXT).section(
254
+ LIST_SECTION_TITLE,
255
+ content.options.map((o, i) => ({ id: pollOptionId(i), title: o.title }))
256
+ );
257
+ };
258
+
259
+ // src/providers/whatsapp-business/messages.ts
260
+ var primary = (clients) => {
261
+ const client = clients[0];
262
+ if (!client) {
263
+ throw new Error("No WhatsApp Business client available");
264
+ }
265
+ return client;
266
+ };
267
+ var toRecord = (result, spaceId, content) => ({
268
+ id: result.messageId,
269
+ content,
270
+ space: { id: spaceId },
271
+ timestamp: /* @__PURE__ */ new Date()
272
+ });
273
+ var MAX_POLL_CACHE_SIZE = 1e3;
274
+ var OPTION_ID_PREFIX = "opt_";
275
+ var pollCaches = /* @__PURE__ */ new WeakMap();
276
+ var getPollCache = (client) => {
277
+ let cache = pollCaches.get(client);
278
+ if (!cache) {
279
+ cache = /* @__PURE__ */ new Map();
280
+ pollCaches.set(client, cache);
281
+ }
282
+ return cache;
283
+ };
284
+ var cachePoll = (client, messageId, poll) => {
285
+ const cache = getPollCache(client);
286
+ if (cache.has(messageId)) {
287
+ cache.delete(messageId);
288
+ }
289
+ cache.set(messageId, poll);
290
+ if (cache.size > MAX_POLL_CACHE_SIZE) {
291
+ const first = cache.keys().next().value;
292
+ if (first !== void 0) {
293
+ cache.delete(first);
294
+ }
295
+ }
296
+ };
297
+ var optionIndexFromId = (id) => {
298
+ if (!id.startsWith(OPTION_ID_PREFIX)) {
299
+ return;
300
+ }
301
+ const index = Number(id.slice(OPTION_ID_PREFIX.length));
302
+ if (!Number.isInteger(index) || index < 0 || pollOptionId(index) !== id) {
303
+ return;
304
+ }
305
+ return index;
306
+ };
307
+ var mapWaPhoneType = (type) => {
308
+ if (!type) {
309
+ return;
310
+ }
311
+ const upper = type.toUpperCase();
312
+ if (upper === "CELL" || upper === "MOBILE" || upper === "IPHONE") {
313
+ return "mobile";
314
+ }
315
+ if (upper === "HOME") {
316
+ return "home";
317
+ }
318
+ if (upper === "WORK" || upper === "BUSINESS") {
319
+ return "work";
320
+ }
321
+ return "other";
322
+ };
323
+ var mapWaSimpleType = (type) => {
324
+ if (!type) {
325
+ return;
326
+ }
327
+ const upper = type.toUpperCase();
328
+ if (upper === "HOME") {
329
+ return "home";
330
+ }
331
+ if (upper === "WORK" || upper === "BUSINESS") {
332
+ return "work";
333
+ }
334
+ return "other";
335
+ };
336
+ var waNameToSpectrum = (name) => {
337
+ const result = { formatted: name.formattedName };
338
+ if (name.firstName) {
339
+ result.first = name.firstName;
340
+ }
341
+ if (name.lastName) {
342
+ result.last = name.lastName;
343
+ }
344
+ if (name.middleName) {
345
+ result.middle = name.middleName;
346
+ }
347
+ if (name.prefix) {
348
+ result.prefix = name.prefix;
349
+ }
350
+ if (name.suffix) {
351
+ result.suffix = name.suffix;
352
+ }
353
+ return result;
354
+ };
355
+ var waPhoneToSpectrum = (phone) => {
356
+ const entry = { value: phone.phone };
357
+ const type = mapWaPhoneType(phone.type);
358
+ if (type) {
359
+ entry.type = type;
360
+ }
361
+ return entry;
362
+ };
363
+ var waEmailToSpectrum = (email) => {
364
+ const entry = { value: email.email };
365
+ const type = mapWaSimpleType(email.type);
366
+ if (type) {
367
+ entry.type = type;
368
+ }
369
+ return entry;
370
+ };
371
+ var waAddressToSpectrum = (address) => {
372
+ const entry = {};
373
+ if (address.street) {
374
+ entry.street = address.street;
375
+ }
376
+ if (address.city) {
377
+ entry.city = address.city;
378
+ }
379
+ if (address.state) {
380
+ entry.region = address.state;
381
+ }
382
+ if (address.zip) {
383
+ entry.postalCode = address.zip;
384
+ }
385
+ if (address.country) {
386
+ entry.country = address.country;
387
+ }
388
+ const type = mapWaSimpleType(address.type);
389
+ if (type) {
390
+ entry.type = type;
391
+ }
392
+ return entry;
393
+ };
394
+ var waOrgToSpectrum = (org) => {
395
+ const entry = {};
396
+ if (org.company) {
397
+ entry.name = org.company;
398
+ }
399
+ if (org.title) {
400
+ entry.title = org.title;
401
+ }
402
+ if (org.department) {
403
+ entry.department = org.department;
404
+ }
405
+ return entry;
406
+ };
407
+ var waContactToSpectrum = (card) => {
408
+ const input = { raw: card };
409
+ input.name = waNameToSpectrum(card.name);
410
+ if (card.phones.length > 0) {
411
+ input.phones = card.phones.map(waPhoneToSpectrum);
412
+ }
413
+ if (card.emails.length > 0) {
414
+ input.emails = card.emails.map(waEmailToSpectrum);
415
+ }
416
+ if (card.addresses.length > 0) {
417
+ input.addresses = card.addresses.map(waAddressToSpectrum);
418
+ }
419
+ if (card.org) {
420
+ input.org = waOrgToSpectrum(card.org);
421
+ }
422
+ if (card.urls.length > 0) {
423
+ input.urls = card.urls.map((u) => u.url);
424
+ }
425
+ if (card.birthday) {
426
+ input.birthday = card.birthday;
427
+ }
428
+ return asContact(input);
429
+ };
430
+ var toMessages = (client, msg) => {
431
+ const base = {
432
+ sender: { id: msg.from },
433
+ space: { id: msg.from },
434
+ timestamp: msg.timestamp
435
+ };
436
+ if (msg.content.type === "contacts") {
437
+ const multi = msg.content.contacts.length > 1;
438
+ return msg.content.contacts.map((card, index) => ({
439
+ ...base,
440
+ id: multi ? `${msg.id}:${index}` : msg.id,
441
+ content: waContactToSpectrum(card)
442
+ }));
443
+ }
444
+ return [
445
+ {
446
+ ...base,
447
+ id: msg.id,
448
+ content: mapContent(client, msg)
449
+ }
450
+ ];
451
+ };
452
+ var mapContent = (client, msg) => {
453
+ const { content } = msg;
454
+ switch (content.type) {
455
+ case "text":
456
+ return asText(content.body);
457
+ case "image":
458
+ case "video":
459
+ case "audio":
460
+ case "document":
461
+ return lazyMedia(client, content.media);
462
+ case "sticker":
463
+ return asCustom({ whatsapp_type: "sticker", ...content.sticker });
464
+ case "location":
465
+ return asCustom({ whatsapp_type: "location", ...content.location });
466
+ case "reaction": {
467
+ const stubTarget = {
468
+ id: content.reaction.messageId,
469
+ content: asCustom({ whatsapp_type: "reaction-target", stub: true })
470
+ };
471
+ return asReaction({
472
+ emoji: content.reaction.emoji,
473
+ target: stubTarget
474
+ });
475
+ }
476
+ case "interactive": {
477
+ const inter = content.interactive;
478
+ if (inter.type === "button_reply" || inter.type === "list_reply") {
479
+ const poll = msg.context?.id === void 0 ? void 0 : getPollCache(client).get(msg.context.id);
480
+ const optionIndex = optionIndexFromId(inter.reply.id);
481
+ const option = optionIndex === void 0 ? void 0 : poll?.options[optionIndex];
482
+ if (poll && option) {
483
+ return asPollOption({ poll, option, selected: true });
484
+ }
485
+ }
486
+ return asCustom({ whatsapp_type: "interactive", ...inter });
487
+ }
488
+ case "button":
489
+ return asCustom({ whatsapp_type: "button", ...content.button });
490
+ case "order":
491
+ return asCustom({ whatsapp_type: "order", ...content.order });
492
+ case "system":
493
+ return asCustom({ whatsapp_type: "system", ...content.system });
494
+ default:
495
+ return asCustom({ whatsapp_type: "unknown" });
496
+ }
497
+ };
498
+ var fetchMedia = async (client, mediaId) => {
499
+ const { url } = await client.media.getUrl(mediaId);
500
+ const response = await fetch(url);
501
+ if (!response.ok) {
502
+ throw new Error(`Media download failed: ${response.status}`);
503
+ }
504
+ return response;
505
+ };
506
+ var lazyMedia = (client, media) => asAttachment({
507
+ name: media.filename ?? `media-${media.id}`,
508
+ mimeType: media.mimeType,
509
+ read: async () => Buffer.from(await (await fetchMedia(client, media.id)).arrayBuffer()),
510
+ stream: async () => {
511
+ const response = await fetchMedia(client, media.id);
512
+ if (!response.body) {
513
+ throw new Error("Media response missing body");
514
+ }
515
+ return response.body;
516
+ }
517
+ });
518
+ var mimeToMediaType = (mimeType) => {
519
+ if (mimeType.startsWith("image/")) {
520
+ return "image";
521
+ }
522
+ if (mimeType.startsWith("video/")) {
523
+ return "video";
524
+ }
525
+ if (mimeType.startsWith("audio/")) {
526
+ return "audio";
527
+ }
528
+ return "document";
529
+ };
530
+ var voiceFilename = (content) => {
531
+ if (content.name) {
532
+ return content.name;
533
+ }
534
+ const ext = mimeExtension(content.mimeType);
535
+ return ext ? `voice.${ext}` : "voice";
536
+ };
537
+ var spectrumPhoneTypeToWa = (type) => {
538
+ if (type === "mobile") {
539
+ return "CELL";
540
+ }
541
+ if (type === "home" || type === "work" || type === "other") {
542
+ return type.toUpperCase();
543
+ }
544
+ return;
545
+ };
546
+ var spectrumSimpleTypeToWa = (type) => type ? type.toUpperCase() : void 0;
547
+ var spectrumNameToWa = (name) => ({
548
+ formattedName: name?.formatted ?? ([name?.first, name?.middle, name?.last].filter((p) => Boolean(p)).join(" ") || "Unknown"),
549
+ firstName: name?.first,
550
+ lastName: name?.last,
551
+ middleName: name?.middle,
552
+ prefix: name?.prefix,
553
+ suffix: name?.suffix
554
+ });
555
+ var isWhatsAppContactCard = (value) => {
556
+ if (!value || typeof value !== "object") {
557
+ return false;
558
+ }
559
+ const raw = value;
560
+ const name = raw.name;
561
+ if (!name || typeof name !== "object" || typeof name.formattedName !== "string") {
562
+ return false;
563
+ }
564
+ return Array.isArray(raw.phones) && Array.isArray(raw.emails) && Array.isArray(raw.addresses) && Array.isArray(raw.urls);
565
+ };
566
+ var contactToWa = (contact) => {
567
+ if (isWhatsAppContactCard(contact.raw)) {
568
+ return contact.raw;
569
+ }
570
+ const card = {
571
+ name: spectrumNameToWa(contact.name),
572
+ phones: (contact.phones ?? []).map((p) => ({
573
+ phone: p.value,
574
+ type: spectrumPhoneTypeToWa(p.type)
575
+ })),
576
+ emails: (contact.emails ?? []).map((e) => ({
577
+ email: e.value,
578
+ type: spectrumSimpleTypeToWa(e.type)
579
+ })),
580
+ addresses: (contact.addresses ?? []).map((a) => ({
581
+ street: a.street,
582
+ city: a.city,
583
+ state: a.region,
584
+ zip: a.postalCode,
585
+ country: a.country,
586
+ type: spectrumSimpleTypeToWa(a.type)
587
+ })),
588
+ urls: (contact.urls ?? []).map((url) => ({ url })),
589
+ org: contact.org?.name || contact.org?.department || contact.org?.title ? {
590
+ company: contact.org.name,
591
+ department: contact.org.department,
592
+ title: contact.org.title
593
+ } : void 0,
594
+ birthday: contact.birthday
595
+ };
596
+ return card;
597
+ };
598
+ var clientStream = (client) => {
599
+ const eventStream = client.events.subscribe().filter(
600
+ (e) => e.type === "message"
601
+ );
602
+ return stream((emit, end) => {
603
+ const pump = (async () => {
604
+ try {
605
+ for await (const event of eventStream) {
606
+ for (const m of toMessages(client, event.message)) {
607
+ await emit(m);
608
+ }
609
+ }
610
+ end();
611
+ } catch (e) {
612
+ end(e);
613
+ }
614
+ })();
615
+ return async () => {
616
+ await eventStream.close();
617
+ await pump;
618
+ };
619
+ });
620
+ };
621
+ var messages = (clients) => mergeStreams(clients.map(clientStream));
622
+ var send = async (clients, spaceId, content) => {
623
+ if (content.type === "reply") {
624
+ return await replyToMessage(
625
+ clients,
626
+ spaceId,
627
+ content.target.id,
628
+ content.content
629
+ );
630
+ }
631
+ if (content.type === "reaction") {
632
+ await reactToMessage(clients, spaceId, content.target.id, content.emoji);
633
+ return;
634
+ }
635
+ if (content.type === "typing") {
636
+ return;
637
+ }
638
+ const client = primary(clients);
639
+ switch (content.type) {
640
+ case "text":
641
+ return toRecord(
642
+ await client.messages.send({ to: spaceId, text: content.text }),
643
+ spaceId,
644
+ content
645
+ );
646
+ case "attachment": {
647
+ const { mediaId } = await client.media.upload({
648
+ file: await content.read(),
649
+ mimeType: content.mimeType,
650
+ filename: content.name
651
+ });
652
+ const mediaType = mimeToMediaType(content.mimeType);
653
+ const mediaPayload = mediaType === "document" ? { id: mediaId, filename: content.name } : { id: mediaId };
654
+ return toRecord(
655
+ await client.messages.send({
656
+ to: spaceId,
657
+ [mediaType]: mediaPayload
658
+ }),
659
+ spaceId,
660
+ content
661
+ );
662
+ }
663
+ case "contact":
664
+ return toRecord(
665
+ await client.messages.send({
666
+ to: spaceId,
667
+ contacts: [contactToWa(content)]
668
+ }),
669
+ spaceId,
670
+ content
671
+ );
672
+ case "voice": {
673
+ const { mediaId } = await client.media.upload({
674
+ file: await content.read(),
675
+ mimeType: content.mimeType,
676
+ filename: voiceFilename(content)
677
+ });
678
+ return toRecord(
679
+ await client.messages.send({
680
+ to: spaceId,
681
+ audio: { id: mediaId }
682
+ }),
683
+ spaceId,
684
+ content
685
+ );
686
+ }
687
+ case "poll": {
688
+ const result = await client.messages.send({
689
+ to: spaceId,
690
+ interactive: pollToInteractive(content)
691
+ });
692
+ cachePoll(client, result.messageId, content);
693
+ return toRecord(result, spaceId, content);
694
+ }
695
+ default:
696
+ throw UnsupportedError.content(content.type);
697
+ }
698
+ };
699
+ var reactToMessage = async (clients, spaceId, messageId, reaction) => {
700
+ await primary(clients).messages.send({
701
+ to: spaceId,
702
+ reaction: { messageId, emoji: reaction }
703
+ });
704
+ };
705
+ var replyToMessage = async (clients, spaceId, messageId, content) => {
706
+ const client = primary(clients);
707
+ switch (content.type) {
708
+ case "text":
709
+ return toRecord(
710
+ await client.messages.send({
711
+ to: spaceId,
712
+ replyTo: messageId,
713
+ text: content.text
714
+ }),
715
+ spaceId,
716
+ content
717
+ );
718
+ case "attachment": {
719
+ const { mediaId } = await client.media.upload({
720
+ file: await content.read(),
721
+ mimeType: content.mimeType,
722
+ filename: content.name
723
+ });
724
+ const mediaType = mimeToMediaType(content.mimeType);
725
+ const mediaPayload = mediaType === "document" ? { id: mediaId, filename: content.name } : { id: mediaId };
726
+ return toRecord(
727
+ await client.messages.send({
728
+ to: spaceId,
729
+ replyTo: messageId,
730
+ [mediaType]: mediaPayload
731
+ }),
732
+ spaceId,
733
+ content
734
+ );
735
+ }
736
+ case "contact":
737
+ return toRecord(
738
+ await client.messages.send({
739
+ to: spaceId,
740
+ replyTo: messageId,
741
+ contacts: [contactToWa(content)]
742
+ }),
743
+ spaceId,
744
+ content
745
+ );
746
+ case "voice": {
747
+ const { mediaId } = await client.media.upload({
748
+ file: await content.read(),
749
+ mimeType: content.mimeType,
750
+ filename: voiceFilename(content)
751
+ });
752
+ return toRecord(
753
+ await client.messages.send({
754
+ to: spaceId,
755
+ replyTo: messageId,
756
+ audio: { id: mediaId }
757
+ }),
758
+ spaceId,
759
+ content
760
+ );
761
+ }
762
+ case "poll": {
763
+ const result = await client.messages.send({
764
+ to: spaceId,
765
+ replyTo: messageId,
766
+ interactive: pollToInteractive(content)
767
+ });
768
+ cachePoll(client, result.messageId, content);
769
+ return toRecord(result, spaceId, content);
770
+ }
771
+ default:
772
+ throw UnsupportedError.content(content.type);
773
+ }
774
+ };
775
+
776
+ // src/providers/whatsapp-business/types.ts
777
+ import z from "zod";
778
+ var directConfig = z.object({
779
+ accessToken: z.string().min(1),
780
+ appSecret: z.string().optional(),
781
+ phoneNumberId: z.string().min(1)
782
+ });
783
+ var cloudConfig = z.object({}).strict();
784
+ var configSchema = z.union([directConfig, cloudConfig]);
785
+ var isCloudConfig = (config) => !("accessToken" in config);
786
+ var userSchema = z.object({});
787
+ var spaceSchema = z.object({
788
+ id: z.string()
789
+ });
790
+
791
+ // src/providers/whatsapp-business/index.ts
792
+ var whatsappBusiness = definePlatform("WhatsApp Business", {
793
+ config: configSchema,
794
+ lifecycle: {
795
+ createClient: async ({
796
+ config,
797
+ projectId,
798
+ projectSecret
799
+ }) => {
800
+ if (!isCloudConfig(config)) {
801
+ return [
802
+ createClient2({
803
+ accessToken: config.accessToken,
804
+ appSecret: config.appSecret ?? "",
805
+ phoneNumberId: config.phoneNumberId
806
+ })
807
+ ];
808
+ }
809
+ if (!(projectId && projectSecret)) {
810
+ throw new Error(
811
+ "WhatsApp Business cloud mode requires projectId and projectSecret. Either pass credentials to Spectrum(), or provide direct credentials: whatsappBusiness.config({ accessToken, phoneNumberId })"
812
+ );
813
+ }
814
+ return await createCloudClients(projectId, projectSecret);
815
+ },
816
+ destroyClient: async ({ client }) => {
817
+ await disposeCloudAuth(client);
818
+ await Promise.all(client.map((c) => c.close()));
819
+ }
820
+ },
821
+ user: {
822
+ resolve: async ({ input }) => ({ id: input.userID })
823
+ },
824
+ space: {
825
+ schema: spaceSchema,
826
+ resolve: async ({ input }) => {
827
+ if (input.users.length === 0) {
828
+ throw new Error("WhatsApp space creation requires at least one user");
829
+ }
830
+ if (input.users.length > 1) {
831
+ throw UnsupportedError.action(
832
+ "createSpace",
833
+ "WhatsApp Business",
834
+ "only 1:1 conversations are supported"
835
+ );
836
+ }
837
+ const user = input.users[0];
838
+ if (!user) {
839
+ throw new Error("WhatsApp space creation requires a user");
840
+ }
841
+ return { id: user.id };
842
+ }
843
+ },
844
+ messages: ({ client }) => messages(client),
845
+ send: async ({ space, content, client }) => await send(client, space.id, content)
846
+ });
847
+
848
+ export {
849
+ whatsappBusiness
850
+ };