imessage-sdk 0.1.0-beta.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.
@@ -0,0 +1,958 @@
1
+ import { createClient, IMessageError, AuthenticationError as AuthenticationError$1, NotFoundError as NotFoundError$1, RateLimitError as RateLimitError$1, ValidationError as ValidationError$1, ConnectionError } from '@photon-ai/advanced-imessage';
2
+ import { cloud, SpectrumCloudError } from '@spectrum-ts/core';
3
+ import { z } from 'zod';
4
+
5
+ // src/providers/photon.ts
6
+
7
+ // src/core/errors.ts
8
+ var IMessageSDKError = class extends Error {
9
+ provider;
10
+ connectionId;
11
+ code;
12
+ statusCode;
13
+ retryable;
14
+ retryAfter;
15
+ traceId;
16
+ raw;
17
+ constructor(message, options = {}) {
18
+ super(message);
19
+ this.name = new.target.name;
20
+ this.provider = options.provider;
21
+ this.connectionId = options.connectionId;
22
+ this.code = options.code;
23
+ this.statusCode = options.statusCode;
24
+ this.retryable = options.retryable ?? false;
25
+ this.retryAfter = options.retryAfter;
26
+ this.traceId = options.traceId;
27
+ this.raw = options.raw;
28
+ }
29
+ };
30
+ var ValidationError = class extends IMessageSDKError {
31
+ };
32
+ var AuthenticationError = class extends IMessageSDKError {
33
+ };
34
+ var NotFoundError = class extends IMessageSDKError {
35
+ };
36
+ var ConflictError = class extends IMessageSDKError {
37
+ };
38
+ var RateLimitError = class extends IMessageSDKError {
39
+ };
40
+ var ProviderUnavailableError = class extends IMessageSDKError {
41
+ };
42
+ var AmbiguousDeliveryError = class extends IMessageSDKError {
43
+ };
44
+
45
+ // src/core/provider.ts
46
+ function defineProvider(provider) {
47
+ return provider;
48
+ }
49
+
50
+ // src/providers/photon.ts
51
+ var SHARED_ADDRESS = "imessage.spectrum.photon.codes:443";
52
+ var SHARED_PHONE = "shared";
53
+ var TOKEN_EXPIRY_BUFFER_MS = 3e4;
54
+ var TOKEN_RENEWAL_RATIO = 0.8;
55
+ var TOKEN_RETRY_MS = 3e4;
56
+ var WEBHOOK_TOLERANCE_SECONDS = 300;
57
+ var PHOTON_CAPABILITIES = {
58
+ messages: {
59
+ text: true,
60
+ attachments: true,
61
+ replies: true,
62
+ get: true,
63
+ edit: false,
64
+ delete: false
65
+ },
66
+ conversations: {
67
+ direct: true,
68
+ groups: false,
69
+ get: true,
70
+ markRead: true
71
+ },
72
+ interactions: {
73
+ reactions: true,
74
+ typingStart: true,
75
+ typingStop: true,
76
+ readReceipts: true
77
+ },
78
+ events: {
79
+ webhooks: true,
80
+ stream: false
81
+ }
82
+ };
83
+ var WebhookAttachmentSchema = z.object({
84
+ type: z.literal("attachment"),
85
+ id: z.string().min(1),
86
+ name: z.string().min(1),
87
+ mimeType: z.string().min(1),
88
+ size: z.number().finite().nonnegative().optional()
89
+ });
90
+ var WebhookMessageSchema = z.object({
91
+ id: z.string().min(1),
92
+ direction: z.literal("inbound"),
93
+ timestamp: z.iso.datetime(),
94
+ sender: z.object({ id: z.string().min(1) }),
95
+ space: z.object({
96
+ id: z.string().min(1),
97
+ phone: z.string().min(1).optional()
98
+ }),
99
+ content: z.union([
100
+ z.object({ type: z.literal("text"), text: z.string() }),
101
+ WebhookAttachmentSchema,
102
+ z.object({
103
+ type: z.literal("reaction"),
104
+ emoji: z.string().min(1),
105
+ target: z.object({ id: z.string().min(1) })
106
+ }),
107
+ z.object({
108
+ type: z.literal("group"),
109
+ items: z.array(
110
+ z.object({
111
+ content: WebhookAttachmentSchema
112
+ })
113
+ )
114
+ })
115
+ ])
116
+ });
117
+ var WebhookPayloadSchema = z.object({
118
+ event: z.literal("messages"),
119
+ message: WebhookMessageSchema,
120
+ space: z.object({
121
+ id: z.string().min(1),
122
+ phone: z.string().min(1).optional()
123
+ })
124
+ });
125
+ function address(value) {
126
+ return { kind: value.includes("@") ? "email" : "phone", value };
127
+ }
128
+ function mapService(value) {
129
+ switch (value) {
130
+ case "iMessage":
131
+ return "imessage";
132
+ case "SMS":
133
+ return "sms";
134
+ case "RCS":
135
+ return "rcs";
136
+ default:
137
+ return "unknown";
138
+ }
139
+ }
140
+ function mapStatus(message) {
141
+ if (message.sendErrorCode !== 0 || message.isCorrupt) return "failed";
142
+ if (message.dateRead !== void 0) return "read";
143
+ if (message.dateDelivered !== void 0 || message.isDelivered) {
144
+ return "delivered";
145
+ }
146
+ if (message.isSent) return "sent";
147
+ return "pending";
148
+ }
149
+ function providerStatus(message) {
150
+ if (message.dateRetracted !== void 0) return "unsent";
151
+ if (message.sendErrorCode !== 0) return `error:${message.sendErrorCode}`;
152
+ return mapStatus(message);
153
+ }
154
+ function attachmentKind(mimeType) {
155
+ if (mimeType.startsWith("image/")) return "image";
156
+ if (mimeType.startsWith("video/")) return "video";
157
+ return "file";
158
+ }
159
+ function mapAttachment(attachment) {
160
+ return {
161
+ kind: attachmentKind(attachment.mimeType),
162
+ id: attachment.guid,
163
+ filename: attachment.fileName,
164
+ contentType: attachment.mimeType,
165
+ size: attachment.totalBytes,
166
+ raw: attachment
167
+ };
168
+ }
169
+ function mapConversation(chat) {
170
+ return {
171
+ providerConversationId: chat.guid,
172
+ participants: chat.participants.map(
173
+ (participant) => address(participant.address)
174
+ ),
175
+ raw: chat
176
+ };
177
+ }
178
+ function replyReference(message) {
179
+ const messageId = message.replyTargetGuid ?? message.threadOriginatorGuid;
180
+ if (messageId === void 0) return void 0;
181
+ const rawPart = message.threadOriginatorPart;
182
+ const partIndex = rawPart === void 0 ? void 0 : Number(rawPart);
183
+ return {
184
+ messageId,
185
+ ...partIndex !== void 0 && Number.isInteger(partIndex) ? { partIndex } : {}
186
+ };
187
+ }
188
+ function messageText(content) {
189
+ return content.text ?? "";
190
+ }
191
+ async function attachmentBytes(attachment) {
192
+ switch (attachment.source.type) {
193
+ case "bytes":
194
+ return attachment.source.data;
195
+ case "blob":
196
+ return new Uint8Array(await attachment.source.data.arrayBuffer());
197
+ case "url": {
198
+ let response;
199
+ try {
200
+ response = await fetch(attachment.source.url);
201
+ } catch (cause) {
202
+ throw new ProviderUnavailableError(
203
+ `Could not download Photon attachment ${attachment.source.url}.`,
204
+ {
205
+ provider: "photon",
206
+ code: "attachment_download_failed",
207
+ retryable: true,
208
+ raw: cause
209
+ }
210
+ );
211
+ }
212
+ if (!response.ok) {
213
+ throw new ProviderUnavailableError(
214
+ `Attachment download failed with HTTP ${response.status}.`,
215
+ {
216
+ provider: "photon",
217
+ code: "attachment_download_failed",
218
+ statusCode: response.status,
219
+ retryable: response.status >= 500
220
+ }
221
+ );
222
+ }
223
+ return new Uint8Array(await response.arrayBuffer());
224
+ }
225
+ }
226
+ }
227
+ function attachmentFilename(attachment) {
228
+ if (attachment.filename !== void 0 && attachment.filename.length > 0) {
229
+ return attachment.filename;
230
+ }
231
+ if (attachment.source.type === "url") {
232
+ try {
233
+ const name = new URL(attachment.source.url).pathname.split("/").at(-1);
234
+ if (name !== void 0 && name.length > 0) return name;
235
+ } catch {
236
+ }
237
+ }
238
+ const extension = attachment.kind === "image" ? "jpg" : attachment.kind === "video" ? "mp4" : "bin";
239
+ return `attachment.${extension}`;
240
+ }
241
+ function mapReaction(reaction) {
242
+ return { kind: reaction };
243
+ }
244
+ function normalizeReaction(reaction) {
245
+ switch (reaction?.kind) {
246
+ case "love":
247
+ case "like":
248
+ case "dislike":
249
+ case "laugh":
250
+ case "emphasize":
251
+ case "question":
252
+ return reaction.kind;
253
+ case "emoji":
254
+ return normalizeWebhookReaction(reaction.emoji ?? "");
255
+ default:
256
+ return void 0;
257
+ }
258
+ }
259
+ function normalizeWebhookReaction(value) {
260
+ switch (value.toLowerCase()) {
261
+ case "love":
262
+ case "\u2764\uFE0F":
263
+ case "\u2764":
264
+ return "love";
265
+ case "like":
266
+ case "\u{1F44D}":
267
+ return "like";
268
+ case "dislike":
269
+ case "\u{1F44E}":
270
+ return "dislike";
271
+ case "laugh":
272
+ case "\u{1F602}":
273
+ return "laugh";
274
+ case "emphasize":
275
+ case "\u203C\uFE0F":
276
+ case "!!":
277
+ return "emphasize";
278
+ case "question":
279
+ case "\u2753":
280
+ case "?":
281
+ return "question";
282
+ default:
283
+ return void 0;
284
+ }
285
+ }
286
+ function errorOptions(error) {
287
+ const photon2 = error instanceof IMessageError ? error : void 0;
288
+ return {
289
+ provider: "photon",
290
+ ...photon2?.code === void 0 ? {} : { code: photon2.code },
291
+ ...photon2?.retryable === void 0 ? {} : { retryable: photon2.retryable },
292
+ raw: error
293
+ };
294
+ }
295
+ function mapError(error, operation) {
296
+ if (error instanceof IMessageSDKError) throw error;
297
+ if (error instanceof IMessageError && error.code === "duplicateMessage" || error instanceof Error && /operation already processed with this client message id/i.test(
298
+ error.message
299
+ )) {
300
+ throw new ConflictError(
301
+ "Photon already processed this idempotency key; the duplicate operation was not sent again.",
302
+ {
303
+ ...errorOptions(error),
304
+ code: "duplicate_message"
305
+ }
306
+ );
307
+ }
308
+ if (error instanceof SpectrumCloudError) {
309
+ const options = {
310
+ provider: "photon",
311
+ code: error.code,
312
+ statusCode: error.status,
313
+ raw: error
314
+ };
315
+ if (error.status === 401 || error.status === 403) {
316
+ throw new AuthenticationError(error.message, options);
317
+ }
318
+ if (error.status === 429) {
319
+ throw new RateLimitError(error.message, {
320
+ ...options,
321
+ retryable: true
322
+ });
323
+ }
324
+ if (error.status >= 500) {
325
+ throw new ProviderUnavailableError(error.message, {
326
+ ...options,
327
+ retryable: true
328
+ });
329
+ }
330
+ throw new IMessageSDKError(error.message, options);
331
+ }
332
+ if (error instanceof AuthenticationError$1) {
333
+ if (error.code === "unauthorized" && /target not allowed for this project/i.test(error.message)) {
334
+ throw new AuthenticationError(
335
+ "Photon rejected this recipient because the target is not allowed for the Spectrum project. Use a permitted or opted-in project user; initiating a conversation with a new contact requires a Photon plan that supports cold outreach.",
336
+ {
337
+ ...errorOptions(error),
338
+ code: "photon_target_not_allowed"
339
+ }
340
+ );
341
+ }
342
+ throw new AuthenticationError(error.message, errorOptions(error));
343
+ }
344
+ if (error instanceof NotFoundError$1) {
345
+ throw new NotFoundError(error.message, errorOptions(error));
346
+ }
347
+ if (error instanceof RateLimitError$1) {
348
+ throw new RateLimitError(error.message, errorOptions(error));
349
+ }
350
+ if (error instanceof ValidationError$1) {
351
+ throw new ValidationError(error.message, errorOptions(error));
352
+ }
353
+ if (error instanceof ConnectionError) {
354
+ if (operation === "messages.send") {
355
+ throw new AmbiguousDeliveryError(
356
+ "The Photon send result is unknown; retry only with the same idempotency key.",
357
+ { ...errorOptions(error), code: "ambiguous_delivery", retryable: true }
358
+ );
359
+ }
360
+ throw new ProviderUnavailableError(error.message, {
361
+ ...errorOptions(error),
362
+ retryable: true
363
+ });
364
+ }
365
+ const message = error instanceof Error ? error.message : `Photon ${operation} failed.`;
366
+ if (/unknown server error occurred/i.test(message)) {
367
+ throw new ProviderUnavailableError(message, {
368
+ provider: "photon",
369
+ code: "photon_server_error",
370
+ retryable: true,
371
+ raw: error
372
+ });
373
+ }
374
+ throw new IMessageSDKError(message, {
375
+ provider: "photon",
376
+ code: "photon_error",
377
+ raw: error
378
+ });
379
+ }
380
+ async function createCloudConnection(options) {
381
+ const projectId = options.projectId;
382
+ const projectSecret = options.projectSecret;
383
+ if (projectId === void 0 || projectId.length === 0 || projectSecret === void 0 || projectSecret.length === 0) {
384
+ throw new AuthenticationError(
385
+ "Photon project credentials are required. Pass projectId and projectSecret or set PHOTON_PROJECT_ID and PHOTON_PROJECT_SECRET.",
386
+ { provider: "photon", code: "missing_project_credentials" }
387
+ );
388
+ }
389
+ let tokenData = await cloud.issueImessageTokens(
390
+ projectId,
391
+ projectSecret
392
+ );
393
+ let expiresAt = Date.now() + tokenData.expiresIn * 1e3;
394
+ let renewalTimer;
395
+ let closed = false;
396
+ const selectLine = (data) => {
397
+ if (data.type === "shared") {
398
+ return { phone: SHARED_PHONE, instanceId: "shared", type: "shared" };
399
+ }
400
+ const lines = Object.entries(data.numbers).flatMap(
401
+ ([instanceId, phone]) => phone === null ? [] : [{ phone, instanceId, type: "dedicated" }]
402
+ );
403
+ if (options.phone !== void 0) {
404
+ const selected = lines.find((line2) => line2.phone === options.phone);
405
+ if (selected === void 0) {
406
+ throw new ValidationError(
407
+ `Photon phone ${options.phone} is not available to this Spectrum project.`,
408
+ {
409
+ provider: "photon",
410
+ code: "photon_phone_not_found",
411
+ raw: { availablePhones: lines.map((line2) => line2.phone) }
412
+ }
413
+ );
414
+ }
415
+ return selected;
416
+ }
417
+ if (lines.length !== 1 || lines[0] === void 0) {
418
+ throw new ValidationError(
419
+ "Photon phone is required when the Spectrum project does not have exactly one dedicated line.",
420
+ {
421
+ provider: "photon",
422
+ code: "photon_phone_required",
423
+ raw: { availablePhones: lines.map((line2) => line2.phone) }
424
+ }
425
+ );
426
+ }
427
+ return lines[0];
428
+ };
429
+ const line = selectLine(tokenData);
430
+ function retryRefresh() {
431
+ void refresh().catch(() => {
432
+ if (!closed) {
433
+ renewalTimer = setTimeout(retryRefresh, TOKEN_RETRY_MS);
434
+ renewalTimer.unref?.();
435
+ }
436
+ });
437
+ }
438
+ const scheduleRenewal = () => {
439
+ if (closed) return;
440
+ if (renewalTimer !== void 0) clearTimeout(renewalTimer);
441
+ const delay = Math.max(tokenData.expiresIn * 1e3 * TOKEN_RENEWAL_RATIO, 5e3);
442
+ renewalTimer = setTimeout(retryRefresh, delay);
443
+ renewalTimer.unref?.();
444
+ };
445
+ let refreshPromise;
446
+ const refresh = () => {
447
+ if (refreshPromise !== void 0) return refreshPromise;
448
+ refreshPromise = cloud.issueImessageTokens(projectId, projectSecret).then((next) => {
449
+ const nextLine = selectLine(next);
450
+ if (nextLine.instanceId !== line.instanceId) {
451
+ throw new ValidationError("The selected Photon line changed during token renewal.", {
452
+ provider: "photon",
453
+ code: "photon_line_changed"
454
+ });
455
+ }
456
+ tokenData = next;
457
+ expiresAt = Date.now() + next.expiresIn * 1e3;
458
+ scheduleRenewal();
459
+ }).finally(() => {
460
+ refreshPromise = void 0;
461
+ });
462
+ return refreshPromise;
463
+ };
464
+ const getToken = async () => {
465
+ if (Date.now() >= expiresAt - TOKEN_EXPIRY_BUFFER_MS) await refresh();
466
+ if (tokenData.type === "shared") return tokenData.token;
467
+ const token = tokenData.auth[line.instanceId];
468
+ if (token === void 0) {
469
+ throw new AuthenticationError("Photon did not issue a token for the selected line.", {
470
+ provider: "photon",
471
+ code: "photon_line_token_missing"
472
+ });
473
+ }
474
+ return token;
475
+ };
476
+ const client = createClient({
477
+ address: line.type === "shared" ? SHARED_ADDRESS : `${line.instanceId}.imsg.photon.codes:443`,
478
+ token: getToken,
479
+ tls: true,
480
+ retry: options.retry ?? true,
481
+ autoIdempotency: true,
482
+ ...options.timeout === void 0 ? {} : { timeout: options.timeout }
483
+ });
484
+ scheduleRenewal();
485
+ return {
486
+ client,
487
+ line,
488
+ async close() {
489
+ if (closed) return;
490
+ closed = true;
491
+ if (renewalTimer !== void 0) clearTimeout(renewalTimer);
492
+ await client.close();
493
+ }
494
+ };
495
+ }
496
+ function constantTimeEqual(left, right) {
497
+ if (left.length !== right.length) return false;
498
+ let difference = 0;
499
+ for (let index = 0; index < left.length; index += 1) {
500
+ difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
501
+ }
502
+ return difference === 0;
503
+ }
504
+ async function hmacSha256(secret, value) {
505
+ const encoder = new TextEncoder();
506
+ const key = await crypto.subtle.importKey(
507
+ "raw",
508
+ encoder.encode(secret),
509
+ { name: "HMAC", hash: "SHA-256" },
510
+ false,
511
+ ["sign"]
512
+ );
513
+ const result = await crypto.subtle.sign("HMAC", key, encoder.encode(value));
514
+ return Array.from(
515
+ new Uint8Array(result),
516
+ (byte) => byte.toString(16).padStart(2, "0")
517
+ ).join("");
518
+ }
519
+ function webhookAttachments(content) {
520
+ const values = content.type === "attachment" ? [content] : content.type === "group" ? content.items.map((item) => item.content) : [];
521
+ return values.map((item) => ({
522
+ kind: attachmentKind(item.mimeType),
523
+ id: item.id,
524
+ filename: item.name,
525
+ contentType: item.mimeType,
526
+ ...item.size === void 0 ? {} : { size: item.size },
527
+ raw: item
528
+ }));
529
+ }
530
+ function photon(options = {}) {
531
+ const projectId = options.projectId ?? process.env["PHOTON_PROJECT_ID"];
532
+ const projectSecret = options.projectSecret ?? process.env["PHOTON_PROJECT_SECRET"];
533
+ const phone = options.phone ?? process.env["PHOTON_PHONE_NUMBER"];
534
+ const webhookSecret = options.webhookSecret ?? process.env["PHOTON_WEBHOOK_SECRET"];
535
+ const config = {
536
+ ...options,
537
+ ...projectId === void 0 ? {} : { projectId },
538
+ ...projectSecret === void 0 ? {} : { projectSecret },
539
+ ...phone === void 0 ? {} : { phone },
540
+ ...webhookSecret === void 0 ? {} : { webhookSecret }
541
+ };
542
+ let connectionPromise;
543
+ const chatCache = /* @__PURE__ */ new Map();
544
+ const activeStreams = /* @__PURE__ */ new Set();
545
+ const connection = () => {
546
+ connectionPromise ??= createCloudConnection(config);
547
+ return connectionPromise;
548
+ };
549
+ const run = async (operation, fn) => {
550
+ try {
551
+ return await fn(await connection());
552
+ } catch (error) {
553
+ return mapError(error, operation);
554
+ }
555
+ };
556
+ const getChat = async (client, id) => {
557
+ const cached = chatCache.get(id);
558
+ if (cached !== void 0) return cached;
559
+ const chat = await client.chats.get(id);
560
+ chatCache.set(id, chat);
561
+ return chat;
562
+ };
563
+ const mapMessage = async (client, message, fallbackConversationId) => {
564
+ const conversationId = fallbackConversationId ?? message.chatGuids[0] ?? "unknown";
565
+ let chat;
566
+ try {
567
+ chat = await getChat(client, conversationId);
568
+ } catch {
569
+ chat = void 0;
570
+ }
571
+ const line = (await connection()).line;
572
+ const direction = message.isFromMe ? "outbound" : "inbound";
573
+ const ownAddress = address(line.phone);
574
+ const sender = direction === "outbound" ? ownAddress : message.sender === void 0 ? address("unknown") : address(message.sender.address);
575
+ const recipients = direction === "outbound" ? chat?.participants.map(
576
+ (participant) => address(participant.address)
577
+ ) ?? [] : [ownAddress];
578
+ const replyTo = replyReference(message);
579
+ return {
580
+ providerMessageId: message.guid,
581
+ conversationId,
582
+ direction,
583
+ sender,
584
+ recipients,
585
+ text: messageText(message.content),
586
+ attachments: message.content.attachments.map(mapAttachment),
587
+ ...replyTo === void 0 ? {} : { replyTo },
588
+ service: mapService(chat?.service ?? message.sender?.service ?? "iMessage"),
589
+ status: mapStatus(message),
590
+ providerStatus: providerStatus(message),
591
+ createdAt: message.dateCreated,
592
+ ...message.isSent ? { sentAt: message.dateCreated } : {},
593
+ ...message.dateDelivered === void 0 ? {} : { deliveredAt: message.dateDelivered },
594
+ ...message.dateRead === void 0 ? {} : { readAt: message.dateRead },
595
+ raw: message
596
+ };
597
+ };
598
+ const syntheticEventMessage = async (client, event) => {
599
+ try {
600
+ return await mapMessage(client, await client.messages.get(event.messageGuid), event.chatGuid);
601
+ } catch {
602
+ const line = (await connection()).line;
603
+ const text = event.type === "message.edited" ? messageText(event.content) : "";
604
+ return {
605
+ providerMessageId: event.messageGuid,
606
+ conversationId: event.chatGuid,
607
+ direction: event.isFromMe ? "outbound" : "inbound",
608
+ sender: event.actor === void 0 ? address(line.phone) : address(event.actor.address),
609
+ recipients: [],
610
+ text,
611
+ attachments: [],
612
+ service: mapService(event.actor?.service ?? "iMessage"),
613
+ status: event.type === "message.read" ? "read" : "sent",
614
+ providerStatus: event.type,
615
+ createdAt: event.occurredAt,
616
+ ...event.type === "message.read" ? { readAt: event.readAt } : {},
617
+ raw: event
618
+ };
619
+ }
620
+ };
621
+ const mapStreamEvent = async (client, event) => {
622
+ const base = {
623
+ id: `photon:${event.sequence}`,
624
+ providerEventId: String(event.sequence),
625
+ timestamp: event.occurredAt,
626
+ raw: event
627
+ };
628
+ switch (event.type) {
629
+ case "message.received":
630
+ return {
631
+ ...base,
632
+ type: event.isFromMe ? "message.sent" : "message.received",
633
+ message: await mapMessage(client, event.message, event.chatGuid)
634
+ };
635
+ case "message.edited":
636
+ return {
637
+ ...base,
638
+ type: "message.edited",
639
+ message: await syntheticEventMessage(client, event)
640
+ };
641
+ case "message.read":
642
+ return {
643
+ ...base,
644
+ type: "message.read",
645
+ message: await syntheticEventMessage(client, event)
646
+ };
647
+ case "message.unsent":
648
+ return {
649
+ ...base,
650
+ type: "message.deleted",
651
+ conversationId: event.chatGuid,
652
+ messageId: event.messageGuid
653
+ };
654
+ case "message.reactionAdded":
655
+ case "message.reactionRemoved": {
656
+ const reaction = normalizeReaction(event.reaction);
657
+ if (reaction === void 0) return void 0;
658
+ const line = (await connection()).line;
659
+ return {
660
+ ...base,
661
+ type: event.type === "message.reactionAdded" ? "reaction.added" : "reaction.removed",
662
+ conversationId: event.chatGuid,
663
+ messageId: event.messageGuid,
664
+ actor: event.actor === void 0 ? address(line.phone) : address(event.actor.address),
665
+ reaction,
666
+ ...event.targetPartIndex === void 0 ? {} : { partIndex: event.targetPartIndex }
667
+ };
668
+ }
669
+ case "message.stickerPlaced":
670
+ return void 0;
671
+ }
672
+ };
673
+ const messages = {
674
+ async send(input) {
675
+ return run("messages.send", async ({ client }) => {
676
+ let chatId = input.conversationId;
677
+ if (chatId === void 0) {
678
+ const recipients = Array.isArray(input.to) ? input.to : [input.to];
679
+ const result = await client.chats.create(
680
+ recipients.map((recipient) => recipient.value),
681
+ input.idempotencyKey === void 0 ? void 0 : { clientMessageId: `${input.idempotencyKey}:chat` }
682
+ );
683
+ chatCache.set(result.chat.guid, result.chat);
684
+ chatId = result.chat.guid;
685
+ }
686
+ const replyTo = input.replyTo === void 0 ? void 0 : input.replyTo.partIndex === void 0 ? input.replyTo.messageId : {
687
+ guid: input.replyTo.messageId,
688
+ partIndex: input.replyTo.partIndex
689
+ };
690
+ const uploaded = await Promise.all(
691
+ (input.attachments ?? []).map(async (attachment) => {
692
+ return client.attachments.upload({
693
+ fileName: attachmentFilename(attachment),
694
+ data: await attachmentBytes(attachment)
695
+ });
696
+ })
697
+ );
698
+ const common = {
699
+ ...input.idempotencyKey === void 0 ? {} : { clientMessageId: input.idempotencyKey },
700
+ ...replyTo === void 0 ? {} : { replyTo }
701
+ };
702
+ let sent;
703
+ if (uploaded.length === 0) {
704
+ sent = await client.messages.sendText(chatId, input.text ?? "", common);
705
+ } else if (uploaded.length === 1 && input.text === void 0) {
706
+ const first = uploaded[0];
707
+ if (first === void 0) throw new ValidationError("Attachment upload failed.");
708
+ sent = await client.messages.sendAttachment(
709
+ chatId,
710
+ first.attachment.guid,
711
+ common
712
+ );
713
+ } else {
714
+ sent = await client.messages.sendMultipart(
715
+ chatId,
716
+ [
717
+ ...input.text === void 0 ? [] : [{ text: input.text }],
718
+ ...uploaded.map((result) => ({
719
+ attachmentGuid: result.attachment.guid,
720
+ attachmentName: result.attachment.fileName
721
+ }))
722
+ ],
723
+ common
724
+ );
725
+ }
726
+ return await mapMessage(client, sent, chatId);
727
+ });
728
+ },
729
+ async get(message) {
730
+ try {
731
+ return await run(
732
+ "messages.get",
733
+ async ({ client }) => mapMessage(client, await client.messages.get(message.messageId), message.conversationId)
734
+ );
735
+ } catch (error) {
736
+ if (error instanceof NotFoundError) return null;
737
+ throw error;
738
+ }
739
+ }
740
+ };
741
+ const conversations = {
742
+ async open(input) {
743
+ return run("conversations.open", async ({ client }) => {
744
+ const result = await client.chats.create(
745
+ input.participants.map((participant) => participant.value)
746
+ );
747
+ chatCache.set(result.chat.guid, result.chat);
748
+ return mapConversation(result.chat);
749
+ });
750
+ },
751
+ async get(id) {
752
+ try {
753
+ return await run(
754
+ "conversations.get",
755
+ async ({ client }) => mapConversation(await getChat(client, id))
756
+ );
757
+ } catch (error) {
758
+ if (error instanceof NotFoundError) return null;
759
+ throw error;
760
+ }
761
+ },
762
+ async markRead(id) {
763
+ await run("conversations.markRead", async ({ client }) => {
764
+ await client.chats.markRead(id);
765
+ });
766
+ }
767
+ };
768
+ const events = {
769
+ subscribe(subscribeOptions = {}) {
770
+ const iterable = {
771
+ async *[Symbol.asyncIterator]() {
772
+ const { client } = await connection();
773
+ const live = client.messages.subscribeEvents();
774
+ activeStreams.add(live);
775
+ const abort = () => {
776
+ void live.close();
777
+ };
778
+ subscribeOptions.signal?.addEventListener("abort", abort, {
779
+ once: true
780
+ });
781
+ const seen = /* @__PURE__ */ new Set();
782
+ const liveIterator = live[Symbol.asyncIterator]();
783
+ let nextLive = liveIterator.next();
784
+ try {
785
+ if (subscribeOptions.cursor !== void 0) {
786
+ const cursor = Number(subscribeOptions.cursor);
787
+ if (!Number.isSafeInteger(cursor) || cursor < 0) {
788
+ throw new ValidationError(
789
+ "Photon event cursor must be a non-negative integer sequence.",
790
+ { provider: "photon", code: "invalid_event_cursor" }
791
+ );
792
+ }
793
+ const catchUp = client.events.catchUp(cursor);
794
+ activeStreams.add(catchUp);
795
+ try {
796
+ for await (const event of catchUp) {
797
+ if (subscribeOptions.signal?.aborted === true) return;
798
+ if (event.type === "catchup.complete") break;
799
+ if (!event.type.startsWith("message.")) continue;
800
+ const messageEvent = event;
801
+ seen.add(messageEvent.sequence);
802
+ const mapped = await mapStreamEvent(client, messageEvent);
803
+ if (mapped !== void 0) yield mapped;
804
+ }
805
+ } finally {
806
+ activeStreams.delete(catchUp);
807
+ await catchUp.close();
808
+ }
809
+ }
810
+ while (true) {
811
+ const result = await nextLive;
812
+ if (result.done === true) return;
813
+ nextLive = liveIterator.next();
814
+ const event = result.value;
815
+ if (subscribeOptions.signal?.aborted === true) return;
816
+ if (seen.has(event.sequence)) continue;
817
+ const mapped = await mapStreamEvent(client, event);
818
+ if (mapped !== void 0) yield mapped;
819
+ }
820
+ } catch (error) {
821
+ mapError(error, "events.subscribe");
822
+ } finally {
823
+ subscribeOptions.signal?.removeEventListener("abort", abort);
824
+ activeStreams.delete(live);
825
+ await live.close();
826
+ }
827
+ }
828
+ };
829
+ return iterable;
830
+ }
831
+ };
832
+ return defineProvider({
833
+ name: "photon",
834
+ capabilities: PHOTON_CAPABILITIES,
835
+ messages,
836
+ conversations,
837
+ reactions: {
838
+ async add(input) {
839
+ await run("reactions.add", async ({ client }) => {
840
+ await client.messages.setReaction(
841
+ input.conversationId,
842
+ input.messageId,
843
+ mapReaction(input.reaction),
844
+ true,
845
+ input.partIndex === void 0 ? void 0 : { partIndex: input.partIndex }
846
+ );
847
+ });
848
+ },
849
+ async remove(input) {
850
+ await run("reactions.remove", async ({ client }) => {
851
+ await client.messages.setReaction(
852
+ input.conversationId,
853
+ input.messageId,
854
+ mapReaction(input.reaction),
855
+ false,
856
+ input.partIndex === void 0 ? void 0 : { partIndex: input.partIndex }
857
+ );
858
+ });
859
+ }
860
+ },
861
+ typing: {
862
+ async start(id) {
863
+ await run("typing.start", async ({ client }) => {
864
+ await client.chats.setTyping(id, true);
865
+ });
866
+ },
867
+ async stop(id) {
868
+ await run("typing.stop", async ({ client }) => {
869
+ await client.chats.setTyping(id, false);
870
+ });
871
+ }
872
+ },
873
+ webhooks: {
874
+ async verify(request) {
875
+ if (config.webhookSecret === void 0) return false;
876
+ const timestamp = request.headers.get("x-spectrum-timestamp");
877
+ const signature = request.headers.get("x-spectrum-signature");
878
+ if (timestamp === null || signature === null) return false;
879
+ const numericTimestamp = Number(timestamp);
880
+ const age = Math.abs(Math.floor(Date.now() / 1e3) - numericTimestamp);
881
+ if (!Number.isFinite(age) || age > WEBHOOK_TOLERANCE_SECONDS) return false;
882
+ const rawBody = await request.text();
883
+ const digest = await hmacSha256(
884
+ config.webhookSecret,
885
+ `v0:${timestamp}:${rawBody}`
886
+ );
887
+ return constantTimeEqual(`v0=${digest}`, signature.toLowerCase());
888
+ },
889
+ async parse(request) {
890
+ const parsed = WebhookPayloadSchema.safeParse(await request.json());
891
+ if (!parsed.success) return [];
892
+ const { message, space } = parsed.data;
893
+ if (message.content.type === "reaction") {
894
+ const reaction = normalizeWebhookReaction(message.content.emoji);
895
+ if (reaction === void 0) return [];
896
+ const providerEventId2 = request.headers.get("x-spectrum-webhook-id");
897
+ return [
898
+ {
899
+ id: message.id,
900
+ ...providerEventId2 === null ? {} : { providerEventId: providerEventId2 },
901
+ type: "reaction.added",
902
+ timestamp: new Date(message.timestamp),
903
+ conversationId: space.id,
904
+ messageId: message.content.target.id,
905
+ actor: address(message.sender.id),
906
+ reaction,
907
+ raw: parsed.data
908
+ }
909
+ ];
910
+ }
911
+ const own = address(space.phone ?? config.phone ?? SHARED_PHONE);
912
+ const providerEventId = request.headers.get("x-spectrum-webhook-id");
913
+ return [
914
+ {
915
+ id: message.id,
916
+ ...providerEventId === null ? {} : { providerEventId },
917
+ type: "message.received",
918
+ timestamp: new Date(message.timestamp),
919
+ message: {
920
+ providerMessageId: message.id,
921
+ conversationId: space.id,
922
+ direction: "inbound",
923
+ sender: address(message.sender.id),
924
+ recipients: [own],
925
+ text: message.content.type === "text" ? message.content.text : "",
926
+ attachments: webhookAttachments(message.content),
927
+ service: "imessage",
928
+ status: "delivered",
929
+ providerStatus: "received",
930
+ createdAt: new Date(message.timestamp),
931
+ raw: message
932
+ },
933
+ raw: parsed.data
934
+ }
935
+ ];
936
+ }
937
+ },
938
+ events,
939
+ connection: {
940
+ async getLine() {
941
+ return (await connection()).line;
942
+ }
943
+ },
944
+ async close() {
945
+ await Promise.all(
946
+ [...activeStreams].map(async (stream) => await stream.close())
947
+ );
948
+ activeStreams.clear();
949
+ if (connectionPromise !== void 0) {
950
+ await (await connectionPromise).close();
951
+ }
952
+ }
953
+ });
954
+ }
955
+
956
+ export { PHOTON_CAPABILITIES, photon };
957
+ //# sourceMappingURL=photon.js.map
958
+ //# sourceMappingURL=photon.js.map