spectrum-ts 0.6.0 → 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";
@@ -107,21 +106,14 @@ async function disposeCloudAuth(clients) {
107
106
 
108
107
  // src/providers/imessage/local.ts
109
108
  import { createReadStream } from "fs";
110
- import { mkdtemp, rm, writeFile } from "fs/promises";
109
+ import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
111
110
  import { tmpdir } from "os";
112
111
  import { basename, join } from "path";
113
112
  import { Readable } from "stream";
114
- import {
115
- readAttachmentBytes
116
- } from "@photon-ai/imessage-kit";
117
- var toSendResult = (result) => {
118
- if (!result.message?.id) {
119
- throw new Error(
120
- "iMessage local send did not return a message id \u2014 track upstream in @photon-ai/imessage-kit"
121
- );
122
- }
123
- return { id: result.message.id, timestamp: result.sentAt };
124
- };
113
+ var synthSendResult = () => ({
114
+ id: crypto.randomUUID(),
115
+ timestamp: /* @__PURE__ */ new Date()
116
+ });
125
117
  var DEFAULT_ATTACHMENT_NAME = "attachment";
126
118
  var VCARD_MIME_TYPES = /* @__PURE__ */ new Set([
127
119
  "text/vcard",
@@ -137,17 +129,21 @@ var isVCardAttachment = (mimeType, fileName) => {
137
129
  }
138
130
  return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
139
131
  };
140
- var toSpace = (message) => ({
141
- id: message.chatId,
142
- type: message.chatKind === "group" ? "group" : "dm"
143
- });
132
+ var readLocalAttachment = async (att) => {
133
+ if (!att.localPath) {
134
+ throw new Error(
135
+ `iMessage attachment ${att.id} has no local file available on disk`
136
+ );
137
+ }
138
+ return readFile(att.localPath);
139
+ };
144
140
  var toAttachmentContent = (att) => {
145
141
  const { localPath } = att;
146
142
  return asAttachment({
147
143
  name: att.fileName ?? DEFAULT_ATTACHMENT_NAME,
148
144
  mimeType: att.mimeType,
149
145
  size: att.sizeBytes,
150
- read: () => readAttachmentBytes(att),
146
+ read: () => readLocalAttachment(att),
151
147
  stream: localPath ? async () => Readable.toWeb(
152
148
  createReadStream(localPath)
153
149
  ) : void 0
@@ -155,16 +151,23 @@ var toAttachmentContent = (att) => {
155
151
  };
156
152
  var toVCardContent = async (att) => {
157
153
  try {
158
- const buf = await readAttachmentBytes(att);
154
+ const buf = await readLocalAttachment(att);
159
155
  return asContact(fromVCard(buf.toString("utf8")));
160
156
  } catch {
161
157
  return toAttachmentContent(att);
162
158
  }
163
159
  };
164
160
  var toMessages = async (message) => {
161
+ const { chatId, chatKind } = message;
162
+ if (!chatId || chatKind === "unknown") {
163
+ return [];
164
+ }
165
+ if (message.reaction !== null || message.kind !== "text" || message.retractedAt !== null) {
166
+ return [];
167
+ }
165
168
  const base = {
166
169
  sender: { id: message.participant ?? "" },
167
- space: toSpace(message),
170
+ space: { id: chatId, type: chatKind === "group" ? "group" : "dm" },
168
171
  timestamp: message.createdAt
169
172
  };
170
173
  if (message.attachments.length > 0) {
@@ -186,19 +189,23 @@ var toMessages = async (message) => {
186
189
  };
187
190
  var messages = (client) => stream((emit, end) => {
188
191
  let lastPromise = Promise.resolve();
189
- client.startWatching({
190
- onMessage: (message) => {
191
- if (message.isFromMe) {
192
- return;
193
- }
192
+ const startPromise = client.startWatching({
193
+ onIncomingMessage: (message) => {
194
194
  lastPromise = lastPromise.then(() => toMessages(message)).then((ms) => {
195
195
  for (const m of ms) {
196
196
  emit(m);
197
197
  }
198
- }).catch((error) => end(error));
199
- }
200
- });
201
- return () => client.stopWatching();
198
+ }).catch(end);
199
+ },
200
+ onError: end
201
+ }).catch(end);
202
+ return async () => {
203
+ await startPromise.catch(() => {
204
+ });
205
+ await client.stopWatching();
206
+ await lastPromise.catch(() => {
207
+ });
208
+ };
202
209
  });
203
210
  var vcardFileName = (content) => {
204
211
  const base = content.name?.formatted ?? content.user?.id ?? "contact";
@@ -210,7 +217,7 @@ var sendTempFile = async (client, spaceId, name, data) => {
210
217
  const tmp = join(dir, safeName);
211
218
  await writeFile(tmp, data);
212
219
  try {
213
- return await client.send(spaceId, { attachments: [tmp] });
220
+ await client.send({ to: spaceId, attachments: [tmp] });
214
221
  } finally {
215
222
  await rm(dir, { recursive: true, force: true }).catch(() => {
216
223
  });
@@ -219,26 +226,23 @@ var sendTempFile = async (client, spaceId, name, data) => {
219
226
  var send = async (client, spaceId, content) => {
220
227
  switch (content.type) {
221
228
  case "text":
222
- return toSendResult(await client.send(spaceId, content.text));
229
+ await client.send({ to: spaceId, text: content.text });
230
+ return synthSendResult();
223
231
  case "attachment":
224
- return toSendResult(
225
- await sendTempFile(client, spaceId, content.name, await content.read())
226
- );
232
+ await sendTempFile(client, spaceId, content.name, await content.read());
233
+ return synthSendResult();
227
234
  case "contact": {
228
235
  const vcf = await toVCard(content);
229
- return toSendResult(
230
- await sendTempFile(
231
- client,
232
- spaceId,
233
- vcardFileName(content),
234
- Buffer.from(vcf, "utf8")
235
- )
236
+ await sendTempFile(
237
+ client,
238
+ spaceId,
239
+ vcardFileName(content),
240
+ Buffer.from(vcf, "utf8")
236
241
  );
242
+ return synthSendResult();
237
243
  }
238
244
  default:
239
- throw new Error(
240
- `Unsupported iMessage local content type: ${content.type}`
241
- );
245
+ throw UnsupportedError.content(content.type, "iMessage (local mode)");
242
246
  }
243
247
  };
244
248
 
@@ -251,7 +255,7 @@ import {
251
255
 
252
256
  // src/utils/audio.ts
253
257
  import { spawn } from "child_process";
254
- import { mkdtemp as mkdtemp2, readFile, rm as rm2, writeFile as writeFile2 } from "fs/promises";
258
+ import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
255
259
  import { tmpdir as tmpdir2 } from "os";
256
260
  import { join as join2 } from "path";
257
261
  var M4A_BRANDS = /* @__PURE__ */ new Set([
@@ -358,7 +362,7 @@ var transcodeToM4a = async (buffer) => {
358
362
  if (code !== 0) {
359
363
  throw new Error(`ffmpeg conversion failed (exit ${code}): ${stderr}`);
360
364
  }
361
- const out = await readFile(outPath);
365
+ const out = await readFile2(outPath);
362
366
  return { buffer: out, duration: parseDuration(stderr) };
363
367
  } finally {
364
368
  await rm2(dir, { recursive: true, force: true }).catch(() => {
@@ -373,7 +377,9 @@ var ensureM4a = async (buffer, mimeType) => {
373
377
  };
374
378
 
375
379
  // src/providers/imessage/remote.ts
376
- var toSendResult2 = (receipt) => ({
380
+ var PLATFORM = "iMessage";
381
+ var unsupportedContent = (type) => UnsupportedError.content(type, PLATFORM);
382
+ var toSendResult = (receipt) => ({
377
383
  id: receipt.guid,
378
384
  timestamp: /* @__PURE__ */ new Date()
379
385
  });
@@ -495,14 +501,14 @@ var send2 = async (clients, spaceId, content) => {
495
501
  const chat = chatGuid(spaceId);
496
502
  switch (content.type) {
497
503
  case "text":
498
- return toSendResult2(await remote.messages.send(chat, content.text));
504
+ return toSendResult(await remote.messages.send(chat, content.text));
499
505
  case "attachment": {
500
506
  const attachment = await remote.attachments.upload({
501
507
  data: await content.read(),
502
508
  fileName: content.name,
503
509
  mimeType: content.mimeType
504
510
  });
505
- return toSendResult2(
511
+ return toSendResult(
506
512
  await remote.messages.send(chat, "", {
507
513
  attachment: attachment.guid
508
514
  })
@@ -510,7 +516,7 @@ var send2 = async (clients, spaceId, content) => {
510
516
  }
511
517
  case "contact": {
512
518
  const attachment = await sendContactAttachment(remote, content);
513
- return toSendResult2(await remote.messages.send(chat, "", { attachment }));
519
+ return toSendResult(await remote.messages.send(chat, "", { attachment }));
514
520
  }
515
521
  case "voice": {
516
522
  const { buffer } = await ensureM4a(
@@ -522,7 +528,7 @@ var send2 = async (clients, spaceId, content) => {
522
528
  fileName: content.name ?? "voice.m4a",
523
529
  mimeType: "audio/x-m4a"
524
530
  });
525
- return toSendResult2(
531
+ return toSendResult(
526
532
  await remote.messages.send(chat, "", {
527
533
  attachment: attachment.guid,
528
534
  audioMessage: true
@@ -530,7 +536,7 @@ var send2 = async (clients, spaceId, content) => {
530
536
  );
531
537
  }
532
538
  default:
533
- throw new Error(`Unsupported iMessage content type: ${content.type}`);
539
+ throw unsupportedContent(content.type);
534
540
  }
535
541
  };
536
542
  var replyToMessage = async (clients, spaceId, msgId, content) => {
@@ -542,7 +548,7 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
542
548
  const replyTo = messageGuid(msgId);
543
549
  switch (content.type) {
544
550
  case "text":
545
- return toSendResult2(
551
+ return toSendResult(
546
552
  await remote.messages.send(chat, content.text, { replyTo })
547
553
  );
548
554
  case "attachment": {
@@ -551,7 +557,7 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
551
557
  fileName: content.name,
552
558
  mimeType: content.mimeType
553
559
  });
554
- return toSendResult2(
560
+ return toSendResult(
555
561
  await remote.messages.send(chat, "", {
556
562
  attachment: attachment.guid,
557
563
  replyTo
@@ -560,7 +566,7 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
560
566
  }
561
567
  case "contact": {
562
568
  const attachment = await sendContactAttachment(remote, content);
563
- return toSendResult2(
569
+ return toSendResult(
564
570
  await remote.messages.send(chat, "", { attachment, replyTo })
565
571
  );
566
572
  }
@@ -574,7 +580,7 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
574
580
  fileName: content.name ?? "voice.m4a",
575
581
  mimeType: "audio/x-m4a"
576
582
  });
577
- return toSendResult2(
583
+ return toSendResult(
578
584
  await remote.messages.send(chat, "", {
579
585
  attachment: attachment.guid,
580
586
  audioMessage: true,
@@ -583,12 +589,16 @@ var replyToMessage = async (clients, spaceId, msgId, content) => {
583
589
  );
584
590
  }
585
591
  default:
586
- throw new Error(`Unsupported iMessage content type: ${content.type}`);
592
+ throw unsupportedContent(content.type);
587
593
  }
588
594
  };
589
595
  var editMessage = async (clients, spaceId, msgId, content) => {
590
596
  if (content.type !== "text") {
591
- 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
+ );
592
602
  }
593
603
  const remote = clients[0];
594
604
  if (!remote) {
@@ -652,8 +662,10 @@ var imessage = definePlatform("iMessage", {
652
662
  schema: spaceSchema,
653
663
  resolve: async ({ input, client }) => {
654
664
  if (isLocal(client)) {
655
- throw new Error(
656
- "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"
657
669
  );
658
670
  }
659
671
  if (input.users.length === 0) {
@@ -735,17 +747,13 @@ var imessage = definePlatform("iMessage", {
735
747
  },
736
748
  replyToMessage: async ({ space, messageId, content, client }) => {
737
749
  if (isLocal(client)) {
738
- throw new Error(
739
- "iMessage local mode does not support replying to messages"
740
- );
750
+ throw UnsupportedError.action("reply", "iMessage (local mode)");
741
751
  }
742
752
  return await replyToMessage(client, space.id, messageId, content);
743
753
  },
744
754
  editMessage: async ({ space, messageId, content, client }) => {
745
755
  if (isLocal(client)) {
746
- throw new Error(
747
- "iMessage local mode does not support editing messages"
748
- );
756
+ throw UnsupportedError.action("edit", "iMessage (local mode)");
749
757
  }
750
758
  await editMessage(client, space.id, messageId, content);
751
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.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@photon-ai/advanced-imessage": "^0.4.3",
23
- "@photon-ai/imessage-kit": "^3.0.0-rc.2",
23
+ "@photon-ai/imessage-kit": "^3.0.0",
24
24
  "@photon-ai/whatsapp-business": "^0.1.1",
25
25
  "@repeaterjs/repeater": "^3.0.6",
26
26
  "better-grpc": "^0.3.2",
@@ -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
- };