spectrum-ts 0.6.1 → 0.7.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.
@@ -16,7 +16,78 @@ var resolveContents = (items) => Promise.all(
16
16
  items.map((c) => typeof c === "string" ? text(c).build() : c.build())
17
17
  );
18
18
 
19
+ // src/utils/errors.ts
20
+ var composeMessage = (opts) => {
21
+ const platform = opts.platform ?? "platform";
22
+ const subject = opts.kind === "content" ? `content type "${opts.contentType ?? "unknown"}"` : `action "${opts.action ?? "unknown"}"`;
23
+ const detail = opts.detail ? `: ${opts.detail}` : "";
24
+ return `${platform} does not support ${subject}${detail}`;
25
+ };
26
+ var UnsupportedError = class _UnsupportedError extends Error {
27
+ kind;
28
+ platform;
29
+ contentType;
30
+ action;
31
+ detail;
32
+ constructor(opts) {
33
+ super(composeMessage(opts));
34
+ this.name = "UnsupportedError";
35
+ this.kind = opts.kind;
36
+ this.platform = opts.platform;
37
+ this.contentType = opts.contentType;
38
+ this.action = opts.action;
39
+ this.detail = opts.detail;
40
+ }
41
+ static content(contentType, platform, detail) {
42
+ return new _UnsupportedError({
43
+ kind: "content",
44
+ contentType,
45
+ platform,
46
+ detail
47
+ });
48
+ }
49
+ static action(action, platform, detail) {
50
+ return new _UnsupportedError({ kind: "action", action, platform, detail });
51
+ }
52
+ withPlatform(platform) {
53
+ if (this.platform) {
54
+ return this;
55
+ }
56
+ return new _UnsupportedError({
57
+ kind: this.kind,
58
+ platform,
59
+ contentType: this.contentType,
60
+ action: this.action,
61
+ detail: this.detail
62
+ });
63
+ }
64
+ };
65
+
19
66
  // src/platform/build.ts
67
+ var ANSI_YELLOW = "\x1B[33m";
68
+ var ANSI_RESET = "\x1B[0m";
69
+ var supportsAnsiColor = () => {
70
+ if (typeof process === "undefined") {
71
+ return false;
72
+ }
73
+ if (process.env.NO_COLOR) {
74
+ return false;
75
+ }
76
+ const force = process.env.FORCE_COLOR;
77
+ if (force !== void 0) {
78
+ return force !== "" && force !== "0" && force !== "false";
79
+ }
80
+ return Boolean(process.stderr?.isTTY);
81
+ };
82
+ var warnUnsupported = (err, fallbackPlatform) => {
83
+ const platform = err.platform ?? fallbackPlatform;
84
+ const subject = err.kind === "content" ? `content type "${err.contentType ?? "unknown"}"` : `action "${err.action ?? "unknown"}"`;
85
+ const detail = err.detail ? `: ${err.detail}` : "";
86
+ const body = `[spectrum-ts] ${platform} does not support ${subject}${detail}; skipping.`;
87
+ console.warn(
88
+ supportsAnsiColor() ? `${ANSI_YELLOW}${body}${ANSI_RESET}` : body
89
+ );
90
+ };
20
91
  function buildSpace(params) {
21
92
  const { spaceRef, extras, typingCtx, definition, client, config } = params;
22
93
  let space;
@@ -24,10 +95,19 @@ function buildSpace(params) {
24
95
  const resolved = await resolveContents(content);
25
96
  const results = [];
26
97
  for (const item of resolved) {
27
- const sendResult = await definition.actions.send({
28
- ...typingCtx,
29
- content: item
30
- });
98
+ let sendResult;
99
+ try {
100
+ sendResult = await definition.actions.send({
101
+ ...typingCtx,
102
+ content: item
103
+ });
104
+ } catch (err) {
105
+ if (err instanceof UnsupportedError) {
106
+ warnUnsupported(err, definition.name);
107
+ continue;
108
+ }
109
+ throw err;
110
+ }
31
111
  if (!sendResult?.id) {
32
112
  throw new Error(
33
113
  `Platform "${definition.name}" send did not return a message id`
@@ -49,7 +129,10 @@ function buildSpace(params) {
49
129
  })
50
130
  );
51
131
  }
52
- return content.length === 1 && results[0] ? results[0] : results;
132
+ if (content.length === 1) {
133
+ return results[0];
134
+ }
135
+ return results;
53
136
  }
54
137
  space = {
55
138
  ...extras,
@@ -80,6 +163,10 @@ function buildMessage(params) {
80
163
  const { definition, client, config, spaceRef, space } = params;
81
164
  const react = async (reaction) => {
82
165
  if (!definition.actions.reactToMessage) {
166
+ warnUnsupported(
167
+ UnsupportedError.action("react", definition.name),
168
+ definition.name
169
+ );
83
170
  return;
84
171
  }
85
172
  await definition.actions.reactToMessage({
@@ -92,20 +179,31 @@ function buildMessage(params) {
92
179
  };
93
180
  async function reply(...content) {
94
181
  if (!definition.actions.replyToMessage) {
95
- throw new Error(
96
- `Platform "${definition.name}" does not support replying to messages`
182
+ warnUnsupported(
183
+ UnsupportedError.action("reply", definition.name),
184
+ definition.name
97
185
  );
186
+ return content.length === 1 ? void 0 : [];
98
187
  }
99
188
  const resolved = await resolveContents(content);
100
189
  const results = [];
101
190
  for (const item of resolved) {
102
- const sendResult = await definition.actions.replyToMessage({
103
- space: spaceRef,
104
- messageId: params.id,
105
- content: item,
106
- client,
107
- config
108
- });
191
+ let sendResult;
192
+ try {
193
+ sendResult = await definition.actions.replyToMessage({
194
+ space: spaceRef,
195
+ messageId: params.id,
196
+ content: item,
197
+ client,
198
+ config
199
+ });
200
+ } catch (err) {
201
+ if (err instanceof UnsupportedError) {
202
+ warnUnsupported(err, definition.name);
203
+ continue;
204
+ }
205
+ throw err;
206
+ }
109
207
  if (!sendResult?.id) {
110
208
  throw new Error(
111
209
  `Platform "${definition.name}" reply did not return a message id`
@@ -127,7 +225,10 @@ function buildMessage(params) {
127
225
  })
128
226
  );
129
227
  }
130
- return content.length === 1 && results[0] ? results[0] : results;
228
+ if (content.length === 1) {
229
+ return results[0];
230
+ }
231
+ return results;
131
232
  }
132
233
  const senderWithPlatform = params.sender === void 0 ? void 0 : { ...params.sender, __platform: definition.name };
133
234
  if (params.direction === "outbound") {
@@ -141,21 +242,31 @@ function buildMessage(params) {
141
242
  reply,
142
243
  edit: async (newContent) => {
143
244
  if (!definition.actions.editMessage) {
144
- throw new Error(
145
- `Platform "${definition.name}" does not support editing messages`
245
+ warnUnsupported(
246
+ UnsupportedError.action("edit", definition.name),
247
+ definition.name
146
248
  );
249
+ return;
147
250
  }
148
251
  const [resolved] = await resolveContents([newContent]);
149
252
  if (!resolved) {
150
253
  return;
151
254
  }
152
- await definition.actions.editMessage({
153
- space: spaceRef,
154
- messageId: params.id,
155
- content: resolved,
156
- client,
157
- config
158
- });
255
+ try {
256
+ await definition.actions.editMessage({
257
+ space: spaceRef,
258
+ messageId: params.id,
259
+ content: resolved,
260
+ client,
261
+ config
262
+ });
263
+ } catch (err) {
264
+ if (err instanceof UnsupportedError) {
265
+ warnUnsupported(err, definition.name);
266
+ return;
267
+ }
268
+ throw err;
269
+ }
159
270
  },
160
271
  sender: senderWithPlatform,
161
272
  space,
@@ -335,6 +446,7 @@ export {
335
446
  asText,
336
447
  text,
337
448
  resolveContents,
449
+ UnsupportedError,
338
450
  buildSpace,
339
451
  buildMessage,
340
452
  definePlatform
@@ -607,6 +607,73 @@ function mergeStreams(streams) {
607
607
  });
608
608
  }
609
609
 
610
+ // src/utils/cloud.ts
611
+ var SPECTRUM_CLOUD_URL = `https://${process.env.SPECTRUM_CLOUD_URL ?? "spectrum.photon.codes"}`;
612
+ var SpectrumCloudError = class extends Error {
613
+ status;
614
+ code;
615
+ constructor(status, code, message) {
616
+ super(message);
617
+ this.name = "SpectrumCloudError";
618
+ this.status = status;
619
+ this.code = code;
620
+ }
621
+ };
622
+ var request = async (path, init) => {
623
+ const response = await fetch(`${SPECTRUM_CLOUD_URL}${path}`, init);
624
+ if (!response.ok) {
625
+ const body = await response.text().catch(() => "");
626
+ try {
627
+ const parsed = JSON.parse(body);
628
+ throw new SpectrumCloudError(
629
+ response.status,
630
+ parsed.code,
631
+ parsed.message
632
+ );
633
+ } catch (error) {
634
+ if (error instanceof SpectrumCloudError) {
635
+ throw error;
636
+ }
637
+ throw new SpectrumCloudError(
638
+ response.status,
639
+ "UNKNOWN",
640
+ body || response.statusText
641
+ );
642
+ }
643
+ }
644
+ const json = await response.json();
645
+ if (!json.succeed) {
646
+ throw new SpectrumCloudError(
647
+ response.status,
648
+ "UNKNOWN",
649
+ "Server returned succeed=false"
650
+ );
651
+ }
652
+ return json.data;
653
+ };
654
+ var basicAuth = (projectId, projectSecret) => `Basic ${btoa(`${projectId}:${projectSecret}`)}`;
655
+ var cloud = {
656
+ getSubscription: (projectId) => request(`/projects/${projectId}/billing/subscription`),
657
+ issueImessageTokens: (projectId, projectSecret) => request(`/projects/${projectId}/imessage/tokens`, {
658
+ method: "POST",
659
+ headers: { Authorization: basicAuth(projectId, projectSecret) }
660
+ }),
661
+ getImessageInfo: (projectId) => request(`/projects/${projectId}/imessage/`),
662
+ issueWhatsappBusinessTokens: (projectId, projectSecret) => request(`/projects/${projectId}/whatsapp-business/tokens`, {
663
+ method: "POST",
664
+ headers: { Authorization: basicAuth(projectId, projectSecret) }
665
+ }),
666
+ getPlatforms: (projectId) => request(`/projects/${projectId}/platforms/`),
667
+ togglePlatform: (projectId, projectSecret, platform, enabled) => request(`/projects/${projectId}/platforms/`, {
668
+ method: "PATCH",
669
+ headers: {
670
+ Authorization: basicAuth(projectId, projectSecret),
671
+ "Content-Type": "application/json"
672
+ },
673
+ body: JSON.stringify({ platform, enabled })
674
+ })
675
+ };
676
+
610
677
  export {
611
678
  readSchema,
612
679
  streamSchema,
@@ -620,5 +687,7 @@ export {
620
687
  asCustom,
621
688
  custom,
622
689
  stream,
623
- mergeStreams
690
+ mergeStreams,
691
+ SpectrumCloudError,
692
+ cloud
624
693
  };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as ContentBuilder, U as User, a as ContentInput, b as Content, P as ProviderMessage, c as PlatformDef, d as Platform, e as PlatformProviderConfig, S as SpectrumLike, f as CustomEventStreams, g as Space, I as InboundMessage, O as OutboundMessage } from './types-DZMHfgYQ.js';
2
- export { A as AnyPlatformDef, E as EventProducer, M as Message, h as PlatformInstance, i as PlatformMessage, j as PlatformSpace, k as PlatformUser, l as SchemaMessage } from './types-DZMHfgYQ.js';
1
+ import { C as ContentBuilder, U as User, a as ContentInput, b as Content, P as ProviderMessage, c as PlatformDef, d as Platform, e as PlatformProviderConfig, S as SpectrumLike, f as CustomEventStreams, g as Space, I as InboundMessage, O as OutboundMessage } from './types-B5tTx5hc.js';
2
+ export { A as AnyPlatformDef, E as EventProducer, M as Message, h as PlatformInstance, i as PlatformMessage, j as PlatformSpace, k as PlatformUser, l as SchemaMessage } from './types-B5tTx5hc.js';
3
3
  import vCard from 'vcf';
4
4
  import z__default from 'zod';
5
5
  export { M as ManagedStream, m as mergeStreams, s as stream } from './stream-DGy4geUK.js';
@@ -163,7 +163,7 @@ declare function definePlatform<_Name extends string, _ConfigSchema extends z__d
163
163
  type SpectrumInstance<Providers extends PlatformProviderConfig[] = PlatformProviderConfig[]> = SpectrumLike<Providers> & CustomEventStreams<Providers> & {
164
164
  readonly messages: AsyncIterable<[Space, InboundMessage]>;
165
165
  stop(): Promise<void>;
166
- send(space: Space, content: ContentInput): Promise<OutboundMessage>;
166
+ send(space: Space, content: ContentInput): Promise<OutboundMessage | undefined>;
167
167
  send(space: Space, ...content: [ContentInput, ContentInput, ...ContentInput[]]): Promise<OutboundMessage[]>;
168
168
  edit(message: OutboundMessage, newContent: ContentInput): Promise<void>;
169
169
  responding<T>(space: Space, fn: () => T | Promise<T>): Promise<T>;
@@ -202,6 +202,11 @@ type PlatformsData = Record<CloudPlatform, PlatformStatus>;
202
202
  interface ImessageInfoData {
203
203
  type: "shared" | "dedicated";
204
204
  }
205
+ interface WhatsappBusinessTokenData {
206
+ auth: Record<string, string>;
207
+ expiresIn: number;
208
+ numbers: Record<string, string | null>;
209
+ }
205
210
  declare class SpectrumCloudError extends Error {
206
211
  readonly status: number;
207
212
  readonly code: string;
@@ -211,11 +216,32 @@ declare const cloud: {
211
216
  getSubscription: (projectId: string) => Promise<SubscriptionData>;
212
217
  issueImessageTokens: (projectId: string, projectSecret: string) => Promise<TokenData>;
213
218
  getImessageInfo: (projectId: string) => Promise<ImessageInfoData>;
219
+ issueWhatsappBusinessTokens: (projectId: string, projectSecret: string) => Promise<WhatsappBusinessTokenData>;
214
220
  getPlatforms: (projectId: string) => Promise<PlatformsData>;
215
221
  togglePlatform: (projectId: string, projectSecret: string, platform: CloudPlatform, enabled: boolean) => Promise<PlatformsData>;
216
222
  };
217
223
 
224
+ type UnsupportedKind = "content" | "action";
225
+ interface UnsupportedErrorOptions {
226
+ action?: string;
227
+ contentType?: string;
228
+ detail?: string;
229
+ kind: UnsupportedKind;
230
+ platform?: string;
231
+ }
232
+ declare class UnsupportedError extends Error {
233
+ readonly kind: UnsupportedKind;
234
+ readonly platform?: string;
235
+ readonly contentType?: string;
236
+ readonly action?: string;
237
+ readonly detail?: string;
238
+ constructor(opts: UnsupportedErrorOptions);
239
+ static content(contentType: string, platform?: string, detail?: string): UnsupportedError;
240
+ static action(action: string, platform?: string, detail?: string): UnsupportedError;
241
+ withPlatform(platform: string): UnsupportedError;
242
+ }
243
+
218
244
  declare const fromVCard: (vcf: string) => ContactInput;
219
245
  declare const toVCard: (contact: Contact) => Promise<string>;
220
246
 
221
- export { type CloudPlatform, type Contact, type ContactAddress, type ContactDetails, type ContactEmail, type ContactInput, type ContactName, type ContactOrg, type ContactPhone, Content, ContentBuilder, ContentInput, type DedicatedTokenData, type ImessageInfoData, Platform, PlatformDef, PlatformProviderConfig, type PlatformStatus, type PlatformsData, type SharedTokenData, Space, Spectrum, SpectrumCloudError, type SpectrumInstance, type SubscriptionData, type SubscriptionStatus, type TokenData, User, type Voice, attachment, cloud, contact, custom, definePlatform, fromVCard, resolveContents, text, toVCard, voice };
247
+ export { type CloudPlatform, type Contact, type ContactAddress, type ContactDetails, type ContactEmail, type ContactInput, type ContactName, type ContactOrg, type ContactPhone, Content, ContentBuilder, ContentInput, type DedicatedTokenData, type ImessageInfoData, Platform, PlatformDef, PlatformProviderConfig, type PlatformStatus, type PlatformsData, type SharedTokenData, Space, Spectrum, SpectrumCloudError, type SpectrumInstance, type SubscriptionData, type SubscriptionStatus, type TokenData, UnsupportedError, type UnsupportedKind, User, type Voice, attachment, cloud, contact, custom, definePlatform, fromVCard, resolveContents, text, toVCard, voice };
package/dist/index.js CHANGED
@@ -1,10 +1,8 @@
1
1
  import {
2
2
  SpectrumCloudError,
3
- cloud
4
- } from "./chunk-HXM64ENV.js";
5
- import {
6
3
  attachment,
7
4
  bufferToStream,
5
+ cloud,
8
6
  contact,
9
7
  custom,
10
8
  fromVCard,
@@ -13,14 +11,15 @@ import {
13
11
  stream,
14
12
  streamSchema,
15
13
  toVCard
16
- } from "./chunk-UZWRB3FZ.js";
14
+ } from "./chunk-GX3JCGSD.js";
17
15
  import {
16
+ UnsupportedError,
18
17
  buildMessage,
19
18
  buildSpace,
20
19
  definePlatform,
21
20
  resolveContents,
22
21
  text
23
- } from "./chunk-XZTTLPHE.js";
22
+ } from "./chunk-6URE4AYH.js";
24
23
 
25
24
  // src/content/voice.ts
26
25
  import { createReadStream } from "fs";
@@ -367,6 +366,7 @@ async function Spectrum(options) {
367
366
  export {
368
367
  Spectrum,
369
368
  SpectrumCloudError,
369
+ UnsupportedError,
370
370
  attachment,
371
371
  cloud,
372
372
  contact,
@@ -3,7 +3,7 @@ import { AdvancedIMessage } from '@photon-ai/advanced-imessage';
3
3
  import { IMessageSDK } from '@photon-ai/imessage-kit';
4
4
  import * as z from 'zod';
5
5
  import z__default from 'zod';
6
- import { l as SchemaMessage, d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-DZMHfgYQ.js';
6
+ import { l as SchemaMessage, d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-B5tTx5hc.js';
7
7
  import * as zod_v4_core from 'zod/v4/core';
8
8
  import 'hotscript';
9
9
 
@@ -1,19 +1,18 @@
1
- import {
2
- cloud
3
- } from "../../chunk-HXM64ENV.js";
4
1
  import {
5
2
  asAttachment,
6
3
  asContact,
7
4
  asCustom,
5
+ cloud,
8
6
  fromVCard,
9
7
  mergeStreams,
10
8
  stream,
11
9
  toVCard
12
- } from "../../chunk-UZWRB3FZ.js";
10
+ } from "../../chunk-GX3JCGSD.js";
13
11
  import {
12
+ UnsupportedError,
14
13
  asText,
15
14
  definePlatform
16
- } from "../../chunk-XZTTLPHE.js";
15
+ } from "../../chunk-6URE4AYH.js";
17
16
 
18
17
  // src/providers/imessage/index.ts
19
18
  import { createClient as createClient2, directChat } from "@photon-ai/advanced-imessage";
@@ -243,9 +242,7 @@ var send = async (client, spaceId, content) => {
243
242
  return synthSendResult();
244
243
  }
245
244
  default:
246
- throw new Error(
247
- `Unsupported iMessage local content type: ${content.type}`
248
- );
245
+ throw UnsupportedError.content(content.type, "iMessage (local mode)");
249
246
  }
250
247
  };
251
248
 
@@ -380,6 +377,8 @@ var ensureM4a = async (buffer, mimeType) => {
380
377
  };
381
378
 
382
379
  // src/providers/imessage/remote.ts
380
+ var PLATFORM = "iMessage";
381
+ var unsupportedContent = (type) => UnsupportedError.content(type, PLATFORM);
383
382
  var toSendResult = (receipt) => ({
384
383
  id: receipt.guid,
385
384
  timestamp: /* @__PURE__ */ new Date()
@@ -537,7 +536,7 @@ var send2 = async (clients, spaceId, content) => {
537
536
  );
538
537
  }
539
538
  default:
540
- throw new Error(`Unsupported iMessage content type: ${content.type}`);
539
+ throw unsupportedContent(content.type);
541
540
  }
542
541
  };
543
542
  var replyToMessage = async (clients, spaceId, msgId, content) => {
@@ -590,12 +589,16 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
590
589
  );
591
590
  }
592
591
  default:
593
- throw new Error(`Unsupported iMessage content type: ${content.type}`);
592
+ throw unsupportedContent(content.type);
594
593
  }
595
594
  };
596
595
  var editMessage = async (clients, spaceId, msgId, content) => {
597
596
  if (content.type !== "text") {
598
- throw new Error("iMessage only supports editing text content");
597
+ throw UnsupportedError.content(
598
+ content.type,
599
+ PLATFORM,
600
+ "only text content can be edited"
601
+ );
599
602
  }
600
603
  const remote = clients[0];
601
604
  if (!remote) {
@@ -659,8 +662,10 @@ var imessage = definePlatform("iMessage", {
659
662
  schema: spaceSchema,
660
663
  resolve: async ({ input, client }) => {
661
664
  if (isLocal(client)) {
662
- throw new Error(
663
- "Space creation is not supported in local mode. Local mode only supports replying to messages."
665
+ throw UnsupportedError.action(
666
+ "createSpace",
667
+ "iMessage (local mode)",
668
+ "local mode only supports replying to existing messages"
664
669
  );
665
670
  }
666
671
  if (input.users.length === 0) {
@@ -742,17 +747,13 @@ var imessage = definePlatform("iMessage", {
742
747
  },
743
748
  replyToMessage: async ({ space, messageId, content, client }) => {
744
749
  if (isLocal(client)) {
745
- throw new Error(
746
- "iMessage local mode does not support replying to messages"
747
- );
750
+ throw UnsupportedError.action("reply", "iMessage (local mode)");
748
751
  }
749
752
  return await replyToMessage(client, space.id, messageId, content);
750
753
  },
751
754
  editMessage: async ({ space, messageId, content, client }) => {
752
755
  if (isLocal(client)) {
753
- throw new Error(
754
- "iMessage local mode does not support editing messages"
755
- );
756
+ throw UnsupportedError.action("edit", "iMessage (local mode)");
756
757
  }
757
758
  await editMessage(client, space.id, messageId, content);
758
759
  }
@@ -1,4 +1,4 @@
1
- import { d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-DZMHfgYQ.js';
1
+ import { d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-B5tTx5hc.js';
2
2
  import * as node_readline from 'node:readline';
3
3
  import z__default from 'zod';
4
4
  import 'hotscript';
@@ -1,6 +1,7 @@
1
1
  import {
2
+ UnsupportedError,
2
3
  definePlatform
3
- } from "../../chunk-XZTTLPHE.js";
4
+ } from "../../chunk-6URE4AYH.js";
4
5
 
5
6
  // src/providers/terminal/index.ts
6
7
  import { createInterface } from "readline";
@@ -50,9 +51,7 @@ var terminal = definePlatform("terminal", {
50
51
  actions: {
51
52
  send: async ({ content }) => {
52
53
  if (content.type !== "text") {
53
- throw new Error(
54
- `Terminal provider only supports text content, got "${content.type}"`
55
- );
54
+ throw UnsupportedError.content(content.type, "terminal");
56
55
  }
57
56
  console.log(content.text);
58
57
  return { id: crypto.randomUUID(), timestamp: /* @__PURE__ */ new Date() };
@@ -1,24 +1,25 @@
1
1
  import { M as ManagedStream } from '../../stream-DGy4geUK.js';
2
+ import { WhatsAppClient } from '@photon-ai/whatsapp-business';
2
3
  import * as z from 'zod';
3
4
  import z__default from 'zod';
4
- import { l as SchemaMessage, d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-DZMHfgYQ.js';
5
+ import { l as SchemaMessage, d as Platform, c as PlatformDef, P as ProviderMessage } from '../../types-B5tTx5hc.js';
5
6
  import * as zod_v4_core from 'zod/v4/core';
6
- import { WhatsAppClient } from '@photon-ai/whatsapp-business';
7
7
  import 'hotscript';
8
8
 
9
+ type WhatsAppClients = WhatsAppClient[];
9
10
  declare const userSchema: z__default.ZodObject<{}, z__default.core.$strip>;
10
11
  declare const spaceSchema: z__default.ZodObject<{
11
12
  id: z__default.ZodString;
12
13
  }, z__default.core.$strip>;
13
14
  type WhatsAppMessage = SchemaMessage<typeof userSchema, typeof spaceSchema>;
14
15
 
15
- declare const whatsappBusiness: Platform<PlatformDef<"WhatsApp Business", z.ZodObject<{
16
+ declare const whatsappBusiness: Platform<PlatformDef<"WhatsApp Business", z.ZodUnion<readonly [z.ZodObject<{
16
17
  accessToken: z.ZodString;
17
- phoneNumberId: z.ZodString;
18
18
  appSecret: z.ZodOptional<z.ZodString>;
19
- }, zod_v4_core.$strip>, z.ZodType<object, unknown, zod_v4_core.$ZodTypeInternals<object, unknown>> | undefined, z.ZodObject<{
19
+ phoneNumberId: z.ZodString;
20
+ }, zod_v4_core.$strip>, z.ZodObject<{}, zod_v4_core.$strict>]>, z.ZodType<object, unknown, zod_v4_core.$ZodTypeInternals<object, unknown>> | undefined, z.ZodObject<{
20
21
  id: z.ZodString;
21
- }, zod_v4_core.$strip>, z.ZodType<object, unknown, zod_v4_core.$ZodTypeInternals<object, unknown>> | undefined, WhatsAppClient, {
22
+ }, zod_v4_core.$strip>, z.ZodType<object, unknown, zod_v4_core.$ZodTypeInternals<object, unknown>> | undefined, WhatsAppClients, {
22
23
  id: string;
23
24
  }, {
24
25
  id: string;
@@ -28,12 +29,12 @@ declare const whatsappBusiness: Platform<PlatformDef<"WhatsApp Business", z.ZodO
28
29
  id: string;
29
30
  }, Record<never, never>>, {
30
31
  messages: ({ client }: {
31
- client: WhatsAppClient;
32
+ client: WhatsAppClients;
32
33
  config: {
33
34
  accessToken: string;
34
35
  phoneNumberId: string;
35
36
  appSecret?: string | undefined;
36
- };
37
+ } | Record<string, never>;
37
38
  }) => ManagedStream<WhatsAppMessage>;
38
39
  }>> & Readonly<Record<never, never>>;
39
40
 
@@ -2,20 +2,242 @@ import {
2
2
  asAttachment,
3
3
  asContact,
4
4
  asCustom,
5
+ cloud,
6
+ mergeStreams,
5
7
  stream
6
- } from "../../chunk-UZWRB3FZ.js";
8
+ } from "../../chunk-GX3JCGSD.js";
7
9
  import {
10
+ UnsupportedError,
8
11
  asText,
9
12
  definePlatform
10
- } from "../../chunk-XZTTLPHE.js";
13
+ } from "../../chunk-6URE4AYH.js";
11
14
 
12
15
  // src/providers/whatsapp-business/index.ts
16
+ import { createClient as createClient2 } from "@photon-ai/whatsapp-business";
17
+
18
+ // src/providers/whatsapp-business/auth.ts
13
19
  import {
14
- createClient
20
+ createClient,
21
+ TypedEventStream
15
22
  } from "@photon-ai/whatsapp-business";
23
+ var RENEWAL_RATIO = 0.8;
24
+ var EXPIRY_BUFFER_MS = 3e4;
25
+ var RETRY_DELAY_MS = 3e4;
26
+ var RESUBSCRIBE_BACKOFF_MS = 500;
27
+ var cloudAuthState = /* @__PURE__ */ new WeakMap();
28
+ async function createCloudClients(projectId, projectSecret) {
29
+ let tokenData = await cloud.issueWhatsappBusinessTokens(
30
+ projectId,
31
+ projectSecret
32
+ );
33
+ let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
34
+ let disposed = false;
35
+ let renewalTimer;
36
+ const lines = /* @__PURE__ */ new Map();
37
+ const buildRawClient = (phoneNumberId) => {
38
+ const accessToken = tokenData.auth[phoneNumberId];
39
+ if (!accessToken) {
40
+ throw new Error(
41
+ `WhatsApp Business line ${phoneNumberId} missing from token response`
42
+ );
43
+ }
44
+ return createClient({ accessToken, appSecret: "", phoneNumberId });
45
+ };
46
+ const refreshTokens = async () => {
47
+ tokenData = await cloud.issueWhatsappBusinessTokens(
48
+ projectId,
49
+ projectSecret
50
+ );
51
+ tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
52
+ for (const [phoneNumberId, state] of lines) {
53
+ if (!tokenData.auth[phoneNumberId]) {
54
+ continue;
55
+ }
56
+ const old = state.current;
57
+ state.current = buildRawClient(phoneNumberId);
58
+ for (const sub of state.subscriptions) {
59
+ sub.swap();
60
+ }
61
+ await old.close().catch(() => void 0);
62
+ }
63
+ };
64
+ const clearRenewalTimer = () => {
65
+ if (renewalTimer !== void 0) {
66
+ clearTimeout(renewalTimer);
67
+ renewalTimer = void 0;
68
+ }
69
+ };
70
+ const scheduleRenewal = () => {
71
+ if (disposed) {
72
+ return;
73
+ }
74
+ clearRenewalTimer();
75
+ const ttlMs = tokenData.expiresIn * 1e3;
76
+ const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
77
+ renewalTimer = setTimeout(async () => {
78
+ try {
79
+ await refreshTokens();
80
+ scheduleRenewal();
81
+ } catch (err) {
82
+ console.warn(
83
+ `[spectrum-ts] WhatsApp Business token refresh failed; retrying in ${RETRY_DELAY_MS}ms.`,
84
+ err
85
+ );
86
+ clearRenewalTimer();
87
+ renewalTimer = setTimeout(() => scheduleRenewal(), RETRY_DELAY_MS);
88
+ renewalTimer?.unref?.();
89
+ }
90
+ }, renewInMs);
91
+ renewalTimer?.unref?.();
92
+ };
93
+ const refreshIfNeeded = async () => {
94
+ if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) {
95
+ return;
96
+ }
97
+ await refreshTokens();
98
+ scheduleRenewal();
99
+ };
100
+ scheduleRenewal();
101
+ const clients = Object.keys(tokenData.auth).map(
102
+ (phoneNumberId) => {
103
+ const state = {
104
+ current: buildRawClient(phoneNumberId),
105
+ subscriptions: /* @__PURE__ */ new Set()
106
+ };
107
+ lines.set(phoneNumberId, state);
108
+ return buildClientProxy(state, refreshIfNeeded);
109
+ }
110
+ );
111
+ cloudAuthState.set(clients, {
112
+ dispose: async () => {
113
+ disposed = true;
114
+ clearRenewalTimer();
115
+ for (const state of lines.values()) {
116
+ for (const sub of state.subscriptions) {
117
+ sub.close();
118
+ }
119
+ }
120
+ await Promise.allSettled(
121
+ Array.from(lines.values()).map((s) => s.current.close())
122
+ );
123
+ lines.clear();
124
+ }
125
+ });
126
+ return clients;
127
+ }
128
+ async function disposeCloudAuth(clients) {
129
+ const auth = cloudAuthState.get(clients);
130
+ if (!auth) {
131
+ return;
132
+ }
133
+ await auth.dispose();
134
+ cloudAuthState.delete(clients);
135
+ }
136
+ var buildClientProxy = (state, refresh) => {
137
+ const forwarder = (pick) => new Proxy({}, {
138
+ get: (_, prop) => {
139
+ return async (...args) => {
140
+ await refresh();
141
+ const target = pick(state.current);
142
+ const fn = target[prop];
143
+ return Reflect.apply(fn, pick(state.current), args);
144
+ };
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
+ 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
+ (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 () => {
208
+ closed = true;
209
+ active?.close().catch(() => void 0);
210
+ active = void 0;
211
+ state.subscriptions.delete(subscription);
212
+ };
213
+ });
214
+ const subscription = {
215
+ close: () => {
216
+ closed = true;
217
+ active?.close().catch(() => void 0);
218
+ },
219
+ swap: () => {
220
+ active?.close().catch(() => void 0);
221
+ }
222
+ };
223
+ state.subscriptions.add(subscription);
224
+ return new TypedEventStream(source, async () => {
225
+ closed = true;
226
+ active?.close().catch(() => void 0);
227
+ state.subscriptions.delete(subscription);
228
+ await source.close();
229
+ });
230
+ };
16
231
 
17
232
  // src/providers/whatsapp-business/messages.ts
18
233
  import { extension as mimeExtension } from "mime-types";
234
+ var primary = (clients) => {
235
+ const client = clients[0];
236
+ if (!client) {
237
+ throw new Error("No WhatsApp Business client available");
238
+ }
239
+ return client;
240
+ };
19
241
  var toSendResult = (result) => ({
20
242
  id: result.messageId
21
243
  });
@@ -291,7 +513,7 @@ var contactToWa = (contact) => {
291
513
  };
292
514
  return card;
293
515
  };
294
- var messages = (client) => {
516
+ var clientStream = (client) => {
295
517
  const eventStream = client.events.subscribe().filter(
296
518
  (e) => e.type === "message"
297
519
  );
@@ -311,7 +533,9 @@ var messages = (client) => {
311
533
  return () => eventStream.close();
312
534
  });
313
535
  };
314
- var send = async (client, spaceId, content) => {
536
+ var messages = (clients) => mergeStreams(clients.map(clientStream));
537
+ var send = async (clients, spaceId, content) => {
538
+ const client = primary(clients);
315
539
  switch (content.type) {
316
540
  case "text":
317
541
  return toSendResult(
@@ -353,16 +577,17 @@ var send = async (client, spaceId, content) => {
353
577
  );
354
578
  }
355
579
  default:
356
- throw new Error(`Unsupported WhatsApp content type: ${content.type}`);
580
+ throw UnsupportedError.content(content.type);
357
581
  }
358
582
  };
359
- var reactToMessage = async (client, spaceId, messageId, reaction) => {
360
- await client.messages.send({
583
+ var reactToMessage = async (clients, spaceId, messageId, reaction) => {
584
+ await primary(clients).messages.send({
361
585
  to: spaceId,
362
586
  reaction: { messageId, emoji: reaction }
363
587
  });
364
588
  };
365
- var replyToMessage = async (client, spaceId, messageId, content) => {
589
+ var replyToMessage = async (clients, spaceId, messageId, content) => {
590
+ const client = primary(clients);
366
591
  switch (content.type) {
367
592
  case "text":
368
593
  return toSendResult(
@@ -411,17 +636,20 @@ var replyToMessage = async (client, spaceId, messageId, content) => {
411
636
  );
412
637
  }
413
638
  default:
414
- throw new Error(`Unsupported WhatsApp content type: ${content.type}`);
639
+ throw UnsupportedError.content(content.type);
415
640
  }
416
641
  };
417
642
 
418
643
  // src/providers/whatsapp-business/types.ts
419
644
  import z from "zod";
420
- var configSchema = z.object({
645
+ var directConfig = z.object({
421
646
  accessToken: z.string().min(1),
422
- phoneNumberId: z.string().min(1),
423
- appSecret: z.string().optional()
647
+ appSecret: z.string().optional(),
648
+ phoneNumberId: z.string().min(1)
424
649
  });
650
+ var cloudConfig = z.object({}).strict();
651
+ var configSchema = z.union([directConfig, cloudConfig]);
652
+ var isCloudConfig = (config) => !("accessToken" in config);
425
653
  var userSchema = z.object({});
426
654
  var spaceSchema = z.object({
427
655
  id: z.string()
@@ -440,8 +668,10 @@ var whatsappBusiness = definePlatform("WhatsApp Business", {
440
668
  throw new Error("WhatsApp space creation requires at least one user");
441
669
  }
442
670
  if (input.users.length > 1) {
443
- throw new Error(
444
- "WhatsApp Business API only supports 1:1 conversations"
671
+ throw UnsupportedError.action(
672
+ "createSpace",
673
+ "WhatsApp Business",
674
+ "only 1:1 conversations are supported"
445
675
  );
446
676
  }
447
677
  const user = input.users[0];
@@ -452,15 +682,30 @@ var whatsappBusiness = definePlatform("WhatsApp Business", {
452
682
  }
453
683
  },
454
684
  lifecycle: {
455
- createClient: async ({ config }) => {
456
- return createClient({
457
- accessToken: config.accessToken,
458
- phoneNumberId: config.phoneNumberId,
459
- appSecret: config.appSecret ?? ""
460
- });
685
+ createClient: async ({
686
+ config,
687
+ projectId,
688
+ projectSecret
689
+ }) => {
690
+ if (!isCloudConfig(config)) {
691
+ return [
692
+ createClient2({
693
+ accessToken: config.accessToken,
694
+ appSecret: config.appSecret ?? "",
695
+ phoneNumberId: config.phoneNumberId
696
+ })
697
+ ];
698
+ }
699
+ if (!(projectId && projectSecret)) {
700
+ throw new Error(
701
+ "WhatsApp Business cloud mode requires projectId and projectSecret. Either pass credentials to Spectrum(), or provide direct credentials: whatsappBusiness.config({ accessToken, phoneNumberId })"
702
+ );
703
+ }
704
+ return await createCloudClients(projectId, projectSecret);
461
705
  },
462
706
  destroyClient: async ({ client }) => {
463
- await client.close();
707
+ await disposeCloudAuth(client);
708
+ await Promise.all(client.map((c) => c.close()));
464
709
  }
465
710
  },
466
711
  events: {
@@ -95,7 +95,7 @@ interface Space<_Def = unknown> {
95
95
  edit(message: OutboundMessage, newContent: ContentInput): Promise<void>;
96
96
  readonly id: string;
97
97
  responding<T>(fn: () => T | Promise<T>): Promise<T>;
98
- send(content: ContentInput): Promise<OutboundMessage>;
98
+ send(content: ContentInput): Promise<OutboundMessage | undefined>;
99
99
  send(...content: [ContentInput, ContentInput, ...ContentInput[]]): Promise<OutboundMessage[]>;
100
100
  startTyping(): Promise<void>;
101
101
  stopTyping(): Promise<void>;
@@ -106,7 +106,7 @@ interface BaseMessage<TPlatform extends string = string, TSender extends User =
106
106
  readonly id: string;
107
107
  platform: TPlatform;
108
108
  react(reaction: string): Promise<void>;
109
- reply(content: ContentInput): Promise<OutboundMessage<TPlatform, TSender, TSpace>>;
109
+ reply(content: ContentInput): Promise<OutboundMessage<TPlatform, TSender, TSpace> | undefined>;
110
110
  reply(...content: [ContentInput, ContentInput, ...ContentInput[]]): Promise<OutboundMessage<TPlatform, TSender, TSpace>[]>;
111
111
  space: TSpace;
112
112
  timestamp: Date;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrum-ts",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -1,67 +0,0 @@
1
- // src/utils/cloud.ts
2
- var SPECTRUM_CLOUD_URL = `https://${process.env.SPECTRUM_CLOUD_URL ?? "spectrum.photon.codes"}`;
3
- var SpectrumCloudError = class extends Error {
4
- status;
5
- code;
6
- constructor(status, code, message) {
7
- super(message);
8
- this.name = "SpectrumCloudError";
9
- this.status = status;
10
- this.code = code;
11
- }
12
- };
13
- var request = async (path, init) => {
14
- const response = await fetch(`${SPECTRUM_CLOUD_URL}${path}`, init);
15
- if (!response.ok) {
16
- const body = await response.text().catch(() => "");
17
- try {
18
- const parsed = JSON.parse(body);
19
- throw new SpectrumCloudError(
20
- response.status,
21
- parsed.code,
22
- parsed.message
23
- );
24
- } catch (error) {
25
- if (error instanceof SpectrumCloudError) {
26
- throw error;
27
- }
28
- throw new SpectrumCloudError(
29
- response.status,
30
- "UNKNOWN",
31
- body || response.statusText
32
- );
33
- }
34
- }
35
- const json = await response.json();
36
- if (!json.succeed) {
37
- throw new SpectrumCloudError(
38
- response.status,
39
- "UNKNOWN",
40
- "Server returned succeed=false"
41
- );
42
- }
43
- return json.data;
44
- };
45
- var basicAuth = (projectId, projectSecret) => `Basic ${btoa(`${projectId}:${projectSecret}`)}`;
46
- var cloud = {
47
- getSubscription: (projectId) => request(`/projects/${projectId}/billing/subscription`),
48
- issueImessageTokens: (projectId, projectSecret) => request(`/projects/${projectId}/imessage/tokens`, {
49
- method: "POST",
50
- headers: { Authorization: basicAuth(projectId, projectSecret) }
51
- }),
52
- getImessageInfo: (projectId) => request(`/projects/${projectId}/imessage/`),
53
- getPlatforms: (projectId) => request(`/projects/${projectId}/platforms/`),
54
- togglePlatform: (projectId, projectSecret, platform, enabled) => request(`/projects/${projectId}/platforms/`, {
55
- method: "PATCH",
56
- headers: {
57
- Authorization: basicAuth(projectId, projectSecret),
58
- "Content-Type": "application/json"
59
- },
60
- body: JSON.stringify({ platform, enabled })
61
- })
62
- };
63
-
64
- export {
65
- SpectrumCloudError,
66
- cloud
67
- };