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,655 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/providers/blooio.ts
4
+
5
+ // src/core/errors.ts
6
+ var IMessageSDKError = class extends Error {
7
+ provider;
8
+ connectionId;
9
+ code;
10
+ statusCode;
11
+ retryable;
12
+ retryAfter;
13
+ traceId;
14
+ raw;
15
+ constructor(message, options = {}) {
16
+ super(message);
17
+ this.name = new.target.name;
18
+ this.provider = options.provider;
19
+ this.connectionId = options.connectionId;
20
+ this.code = options.code;
21
+ this.statusCode = options.statusCode;
22
+ this.retryable = options.retryable ?? false;
23
+ this.retryAfter = options.retryAfter;
24
+ this.traceId = options.traceId;
25
+ this.raw = options.raw;
26
+ }
27
+ };
28
+ var ValidationError = class extends IMessageSDKError {
29
+ };
30
+ var AuthenticationError = class extends IMessageSDKError {
31
+ };
32
+ var NotFoundError = class extends IMessageSDKError {
33
+ };
34
+ var ConflictError = class extends IMessageSDKError {
35
+ };
36
+ var RateLimitError = class extends IMessageSDKError {
37
+ };
38
+ var ProviderUnavailableError = class extends IMessageSDKError {
39
+ };
40
+ var AmbiguousDeliveryError = class extends IMessageSDKError {
41
+ };
42
+
43
+ // src/core/provider.ts
44
+ function defineProvider(provider) {
45
+ return provider;
46
+ }
47
+
48
+ // src/providers/blooio.ts
49
+ var DEFAULT_BASE_URL = "https://api.blooio.com/v2/api";
50
+ var DEFAULT_WEBHOOK_TOLERANCE_SECONDS = 300;
51
+ var BLOOIO_CAPABILITIES = {
52
+ messages: {
53
+ text: true,
54
+ attachments: true,
55
+ replies: true,
56
+ get: true,
57
+ edit: false,
58
+ delete: false
59
+ },
60
+ conversations: {
61
+ direct: true,
62
+ groups: false,
63
+ get: true,
64
+ markRead: true
65
+ },
66
+ interactions: {
67
+ reactions: true,
68
+ typingStart: true,
69
+ typingStop: true,
70
+ readReceipts: true
71
+ },
72
+ events: {
73
+ webhooks: true,
74
+ stream: false
75
+ }
76
+ };
77
+ var JsonObjectSchema = z.record(z.string(), z.unknown());
78
+ var OptionalNonEmptyStringSchema = z.string().min(1).optional().catch(void 0);
79
+ var OptionalNumberSchema = z.number().optional().catch(void 0);
80
+ var ReactionSchema = z.enum([
81
+ "love",
82
+ "like",
83
+ "dislike",
84
+ "laugh",
85
+ "emphasize",
86
+ "question"
87
+ ]);
88
+ var PlanKindSchema = z.enum([
89
+ "shared",
90
+ "dedicated",
91
+ "inbound",
92
+ "trial",
93
+ "2fa"
94
+ ]);
95
+ function dateValue(value) {
96
+ const number = OptionalNumberSchema.parse(value);
97
+ if (number !== void 0) return new Date(number);
98
+ const string = OptionalNonEmptyStringSchema.parse(value);
99
+ if (string === void 0) return void 0;
100
+ const date = new Date(string);
101
+ return Number.isNaN(date.valueOf()) ? void 0 : date;
102
+ }
103
+ function address(value) {
104
+ return {
105
+ kind: value.includes("@") ? "email" : "phone",
106
+ value
107
+ };
108
+ }
109
+ function requireAddress(value, fallback = "unknown") {
110
+ return address(OptionalNonEmptyStringSchema.parse(value) ?? fallback);
111
+ }
112
+ function mapStatus(value) {
113
+ switch (typeof value === "string" ? value.toLowerCase() : "") {
114
+ case "queued":
115
+ case "pending":
116
+ return "pending";
117
+ case "accepted":
118
+ return "accepted";
119
+ case "sent":
120
+ return "sent";
121
+ case "delivered":
122
+ return "delivered";
123
+ case "read":
124
+ return "read";
125
+ case "failed":
126
+ case "cancelled":
127
+ case "cancellation_requested":
128
+ case "error":
129
+ return "failed";
130
+ default:
131
+ return "pending";
132
+ }
133
+ }
134
+ function mapService(value) {
135
+ switch (typeof value === "string" ? value.toLowerCase() : "") {
136
+ case "imessage":
137
+ return "imessage";
138
+ case "sms":
139
+ return "sms";
140
+ case "rcs":
141
+ return "rcs";
142
+ default:
143
+ return "unknown";
144
+ }
145
+ }
146
+ function attachmentKind(contentType) {
147
+ if (contentType?.startsWith("image/")) return "image";
148
+ if (contentType?.startsWith("video/")) return "video";
149
+ return "file";
150
+ }
151
+ function mapAttachment(value) {
152
+ if (typeof value === "string") {
153
+ return { kind: "file", url: value, raw: value };
154
+ }
155
+ const parsedItem = JsonObjectSchema.safeParse(value);
156
+ const item = parsedItem.success ? parsedItem.data : {};
157
+ const contentType = OptionalNonEmptyStringSchema.parse(item["content_type"]) ?? OptionalNonEmptyStringSchema.parse(item["contentType"]) ?? OptionalNonEmptyStringSchema.parse(item["mime_type"]);
158
+ const id = OptionalNonEmptyStringSchema.parse(item["id"]);
159
+ const url = OptionalNonEmptyStringSchema.parse(item["url"]);
160
+ const filename = OptionalNonEmptyStringSchema.parse(item["name"]);
161
+ const size = OptionalNumberSchema.parse(item["size"]);
162
+ const result = {
163
+ kind: attachmentKind(contentType),
164
+ raw: value,
165
+ ...id === void 0 ? {} : { id },
166
+ ...url === void 0 ? {} : { url },
167
+ ...filename === void 0 ? {} : { filename },
168
+ ...contentType === void 0 ? {} : { contentType },
169
+ ...size === void 0 ? {} : { size }
170
+ };
171
+ return result;
172
+ }
173
+ function participantValues(value) {
174
+ if (!Array.isArray(value)) return [];
175
+ return value.flatMap((participant) => {
176
+ if (typeof participant === "string") return [participant];
177
+ const parsedParticipant = JsonObjectSchema.safeParse(participant);
178
+ if (!parsedParticipant.success) return [];
179
+ const identifier = OptionalNonEmptyStringSchema.parse(
180
+ parsedParticipant.data["identifier"]
181
+ );
182
+ return identifier === void 0 ? [] : [identifier];
183
+ });
184
+ }
185
+ function mapProviderMessage(raw, fallbackConversationId, configuredSender) {
186
+ const direction = raw["direction"] === "inbound" ? "inbound" : "outbound";
187
+ const conversationId2 = OptionalNonEmptyStringSchema.parse(raw["chat_id"]) ?? OptionalNonEmptyStringSchema.parse(raw["group_id"]) ?? OptionalNonEmptyStringSchema.parse(raw["external_id"]) ?? fallbackConversationId;
188
+ const internal = OptionalNonEmptyStringSchema.parse(raw["internal_id"]);
189
+ const parsedContact = JsonObjectSchema.safeParse(raw["contact"]);
190
+ const external = OptionalNonEmptyStringSchema.parse(raw["sender"]) ?? (parsedContact.success ? OptionalNonEmptyStringSchema.parse(parsedContact.data["identifier"]) : void 0) ?? OptionalNonEmptyStringSchema.parse(raw["external_id"]) ?? conversationId2;
191
+ const participants = participantValues(raw["participants"]);
192
+ const sender = direction === "inbound" ? address(external) : internal === void 0 ? configuredSender ?? address("unknown") : address(internal);
193
+ const recipients = direction === "inbound" ? [internal === void 0 ? configuredSender ?? address("unknown") : address(internal)] : (participants.length > 0 ? participants : [external]).map(address);
194
+ const providerStatus = OptionalNonEmptyStringSchema.parse(raw["status"]);
195
+ const parsedReply = JsonObjectSchema.safeParse(raw["reply_to"]);
196
+ const reply = parsedReply.success ? parsedReply.data : void 0;
197
+ const replyMessageId = reply === void 0 ? void 0 : OptionalNonEmptyStringSchema.parse(reply["message_id"]) ?? OptionalNonEmptyStringSchema.parse(reply["guid"]);
198
+ const sentAt = dateValue(raw["time_sent"] ?? raw["sent_at"]);
199
+ const deliveredAt = dateValue(raw["time_delivered"] ?? raw["delivered_at"]);
200
+ const readAt = dateValue(raw["read_at"]);
201
+ const partIndex = OptionalNumberSchema.parse(reply?.["part_index"]);
202
+ return {
203
+ providerMessageId: OptionalNonEmptyStringSchema.parse(raw["message_id"]) ?? OptionalNonEmptyStringSchema.parse(raw["id"]) ?? "unknown",
204
+ conversationId: conversationId2,
205
+ direction,
206
+ sender,
207
+ recipients,
208
+ text: typeof raw["text"] === "string" ? raw["text"] : "",
209
+ attachments: Array.isArray(raw["attachments"]) ? raw["attachments"].map(mapAttachment) : [],
210
+ ...replyMessageId === void 0 ? {} : {
211
+ replyTo: {
212
+ messageId: replyMessageId,
213
+ ...partIndex === void 0 ? {} : { partIndex }
214
+ }
215
+ },
216
+ service: mapService(raw["protocol"]),
217
+ status: mapStatus(providerStatus),
218
+ ...providerStatus === void 0 ? {} : { providerStatus },
219
+ createdAt: sentAt ?? dateValue(raw["received_at"]) ?? dateValue(raw["timestamp"]) ?? /* @__PURE__ */ new Date(),
220
+ ...sentAt === void 0 ? {} : { sentAt },
221
+ ...deliveredAt === void 0 ? {} : { deliveredAt },
222
+ ...readAt === void 0 ? {} : { readAt },
223
+ raw
224
+ };
225
+ }
226
+ function encodePath(value) {
227
+ return encodeURIComponent(value);
228
+ }
229
+ function conversationId(input) {
230
+ return input.participants.map((participant) => participant.value).join(",");
231
+ }
232
+ function parseSignature(header) {
233
+ let timestamp;
234
+ const signatures = [];
235
+ for (const component of header.split(",")) {
236
+ const separator = component.indexOf("=");
237
+ if (separator < 0) continue;
238
+ const key = component.slice(0, separator).trim();
239
+ const value = component.slice(separator + 1).trim();
240
+ if (key === "t") timestamp = value;
241
+ if (key === "v1") signatures.push(value);
242
+ }
243
+ return timestamp === void 0 || signatures.length === 0 ? void 0 : { timestamp, signatures };
244
+ }
245
+ function constantTimeEqual(left, right) {
246
+ if (left.length !== right.length) return false;
247
+ let result = 0;
248
+ for (let index = 0; index < left.length; index += 1) {
249
+ result |= left.charCodeAt(index) ^ right.charCodeAt(index);
250
+ }
251
+ return result === 0;
252
+ }
253
+ async function hmacSha256(secret, value) {
254
+ const encoder = new TextEncoder();
255
+ const key = await crypto.subtle.importKey(
256
+ "raw",
257
+ encoder.encode(secret),
258
+ { name: "HMAC", hash: "SHA-256" },
259
+ false,
260
+ ["sign"]
261
+ );
262
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(value));
263
+ return Array.from(
264
+ new Uint8Array(signature),
265
+ (byte) => byte.toString(16).padStart(2, "0")
266
+ ).join("");
267
+ }
268
+ function eventStatus(event, rawStatus) {
269
+ switch (event) {
270
+ case "message.received":
271
+ return "delivered";
272
+ case "message.sent":
273
+ return "sent";
274
+ case "message.delivered":
275
+ return "delivered";
276
+ case "message.read":
277
+ return "read";
278
+ case "message.failed":
279
+ return "failed";
280
+ default:
281
+ return mapStatus(rawStatus);
282
+ }
283
+ }
284
+ function mapWebhookEvent(raw) {
285
+ const event = OptionalNonEmptyStringSchema.parse(raw["event"]);
286
+ if (event === void 0) return void 0;
287
+ const timestamp = dateValue(raw["timestamp"]) ?? /* @__PURE__ */ new Date();
288
+ const messageId = OptionalNonEmptyStringSchema.parse(raw["message_id"]) ?? "unknown";
289
+ const conversation = OptionalNonEmptyStringSchema.parse(raw["group_id"]) ?? OptionalNonEmptyStringSchema.parse(raw["external_id"]) ?? "unknown";
290
+ const id = `${event}:${messageId}:${timestamp.valueOf()}`;
291
+ if (event === "message.reaction") {
292
+ const parsedReaction = ReactionSchema.safeParse(raw["reaction"]);
293
+ if (!parsedReaction.success) return void 0;
294
+ const reaction = parsedReaction.data;
295
+ const partIndex = OptionalNumberSchema.parse(raw["part_index"]);
296
+ return {
297
+ id,
298
+ type: raw["action"] === "remove" ? "reaction.removed" : "reaction.added",
299
+ timestamp,
300
+ conversationId: conversation,
301
+ messageId,
302
+ actor: requireAddress(raw["sender"] ?? raw["external_id"]),
303
+ reaction,
304
+ ...partIndex === void 0 ? {} : { partIndex },
305
+ raw
306
+ };
307
+ }
308
+ if (event === "typing.started" || event === "typing.stopped") {
309
+ const actorValue = OptionalNonEmptyStringSchema.parse(raw["sender"]);
310
+ return {
311
+ id,
312
+ type: event,
313
+ timestamp,
314
+ conversationId: conversation,
315
+ ...actorValue === void 0 ? {} : { actor: address(actorValue) },
316
+ raw
317
+ };
318
+ }
319
+ if (event !== "message.received" && event !== "message.sent" && event !== "message.delivered" && event !== "message.read" && event !== "message.failed") {
320
+ return void 0;
321
+ }
322
+ return {
323
+ id,
324
+ type: event,
325
+ timestamp,
326
+ message: mapProviderMessage(
327
+ { ...raw, direction: event === "message.received" ? "inbound" : "outbound", status: eventStatus(event, raw["status"]) },
328
+ conversation
329
+ ),
330
+ raw
331
+ };
332
+ }
333
+ function blooio(options = {}) {
334
+ const apiKey = options.apiKey ?? process.env["BLOOIO_API_KEY"];
335
+ const webhookSecret = options.webhookSecret ?? process.env["BLOOIO_WEBHOOK_SECRET"];
336
+ const configuredSender = process.env["BLOOIO_FROM_NUMBER"];
337
+ const sender = options.sender ?? (configuredSender === void 0 ? void 0 : address(configuredSender));
338
+ const config = {
339
+ ...options,
340
+ ...apiKey === void 0 ? {} : { apiKey },
341
+ ...webhookSecret === void 0 ? {} : { webhookSecret },
342
+ ...sender === void 0 ? {} : { sender }
343
+ };
344
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
345
+ const request = async (path, init = {}, requestOptions = {}) => {
346
+ if (config.apiKey === void 0 || config.apiKey.length === 0) {
347
+ throw new AuthenticationError("A Blooio API key is required.", {
348
+ provider: "blooio",
349
+ code: "missing_api_key"
350
+ });
351
+ }
352
+ let response;
353
+ try {
354
+ response = await fetch(`${baseUrl}${path}`, {
355
+ ...init,
356
+ headers: {
357
+ authorization: `Bearer ${config.apiKey}`,
358
+ accept: "application/json",
359
+ ...init.body === void 0 ? {} : { "content-type": "application/json" },
360
+ ...init.headers
361
+ }
362
+ });
363
+ } catch (cause) {
364
+ if (requestOptions.send === true) {
365
+ throw new AmbiguousDeliveryError(
366
+ "The Blooio send result is unknown; retry only with the same idempotency key.",
367
+ { provider: "blooio", code: "ambiguous_delivery", retryable: true, raw: cause }
368
+ );
369
+ }
370
+ throw new ProviderUnavailableError("Could not reach Blooio.", {
371
+ provider: "blooio",
372
+ code: "provider_unavailable",
373
+ retryable: true,
374
+ raw: cause
375
+ });
376
+ }
377
+ const rawText = await response.text();
378
+ let raw;
379
+ try {
380
+ raw = rawText.length === 0 ? void 0 : JSON.parse(rawText);
381
+ } catch {
382
+ raw = rawText;
383
+ }
384
+ if (response.ok) return raw ?? {};
385
+ if (response.status === 404 && requestOptions.notFoundNull === true) return null;
386
+ const parsedBody = JsonObjectSchema.safeParse(raw);
387
+ const body = parsedBody.success ? parsedBody.data : {};
388
+ const message = OptionalNonEmptyStringSchema.parse(body["message"]) ?? OptionalNonEmptyStringSchema.parse(body["error"]) ?? `Blooio request failed with HTTP ${response.status}.`;
389
+ const code = OptionalNonEmptyStringSchema.parse(body["code"]) ?? `http_${response.status}`;
390
+ const common = {
391
+ provider: "blooio",
392
+ code,
393
+ statusCode: response.status,
394
+ raw
395
+ };
396
+ if (response.status === 401 || response.status === 403) {
397
+ throw new AuthenticationError(message, common);
398
+ }
399
+ if (response.status === 404) throw new NotFoundError(message, common);
400
+ if (response.status === 409) throw new ConflictError(message, common);
401
+ if (response.status === 429) {
402
+ const retryAfterHeader = response.headers.get("retry-after");
403
+ const retryAfter = retryAfterHeader === null ? void 0 : Number(retryAfterHeader);
404
+ throw new RateLimitError(message, {
405
+ ...common,
406
+ retryable: true,
407
+ ...retryAfter !== void 0 && Number.isFinite(retryAfter) ? { retryAfter } : {}
408
+ });
409
+ }
410
+ if (response.status >= 500) {
411
+ throw new ProviderUnavailableError(message, {
412
+ ...common,
413
+ retryable: true
414
+ });
415
+ }
416
+ throw new IMessageSDKError(message, common);
417
+ };
418
+ const messages = {
419
+ async send(input) {
420
+ const destinations = input.to === void 0 ? [] : "value" in input.to ? [input.to] : input.to;
421
+ const chatId = input.conversationId ?? destinations.map((recipient) => recipient.value).join(",");
422
+ const attachments = input.attachments?.map((attachment) => {
423
+ if (attachment.source.type !== "url") {
424
+ throw new ValidationError(
425
+ "Blooio v2 requires attachments to use a publicly accessible URL.",
426
+ { provider: "blooio", code: "attachment_url_required" }
427
+ );
428
+ }
429
+ return attachment.filename === void 0 ? attachment.source.url : { url: attachment.source.url, name: attachment.filename };
430
+ });
431
+ const body = {
432
+ ...input.text === void 0 ? {} : { text: input.text },
433
+ ...attachments === void 0 || attachments.length === 0 ? {} : { attachments },
434
+ ...config.sender === void 0 ? {} : { from_number: config.sender.value },
435
+ ...input.replyTo === void 0 ? {} : {
436
+ reply_to: {
437
+ message_id: input.replyTo.messageId,
438
+ ...input.replyTo.partIndex === void 0 ? {} : { part_index: input.replyTo.partIndex }
439
+ }
440
+ }
441
+ };
442
+ const raw = await request(
443
+ `/chats/${encodePath(chatId)}/messages`,
444
+ {
445
+ method: "POST",
446
+ ...input.idempotencyKey === void 0 ? {} : { headers: { "idempotency-key": input.idempotencyKey } },
447
+ body: JSON.stringify(body)
448
+ },
449
+ { send: true }
450
+ );
451
+ const response = raw ?? {};
452
+ const providerMessageId = OptionalNonEmptyStringSchema.parse(
453
+ response["message_id"]
454
+ );
455
+ if (providerMessageId === void 0) {
456
+ throw new IMessageSDKError("Blooio did not return a message ID.", {
457
+ provider: "blooio",
458
+ code: "invalid_provider_response",
459
+ raw: response
460
+ });
461
+ }
462
+ const actualConversationId = OptionalNonEmptyStringSchema.parse(response["group_id"]) ?? chatId;
463
+ const providerStatus = OptionalNonEmptyStringSchema.parse(
464
+ response["status"]
465
+ );
466
+ const recipients = input.to === void 0 ? participantValues(response["participants"]).map(address) : destinations;
467
+ return {
468
+ providerMessageId,
469
+ conversationId: actualConversationId,
470
+ direction: "outbound",
471
+ sender: config.sender ?? address("unknown"),
472
+ recipients,
473
+ text: input.text ?? "",
474
+ attachments: input.attachments?.map((attachment) => {
475
+ const url = attachment.source.type === "url" ? attachment.source.url : void 0;
476
+ return {
477
+ kind: attachment.kind,
478
+ ...url === void 0 ? {} : { url },
479
+ ...attachment.filename === void 0 ? {} : { filename: attachment.filename },
480
+ ...attachment.contentType === void 0 ? {} : { contentType: attachment.contentType },
481
+ raw: attachment
482
+ };
483
+ }) ?? [],
484
+ ...input.replyTo === void 0 ? {} : { replyTo: input.replyTo },
485
+ service: "imessage",
486
+ status: mapStatus(response["status"]),
487
+ ...providerStatus === void 0 ? {} : { providerStatus },
488
+ createdAt: /* @__PURE__ */ new Date(),
489
+ raw: response
490
+ };
491
+ },
492
+ async get(message) {
493
+ const raw = await request(
494
+ `/chats/${encodePath(message.conversationId)}/messages/${encodePath(message.messageId)}`,
495
+ {},
496
+ { notFoundNull: true }
497
+ );
498
+ return raw === null ? null : mapProviderMessage(raw, message.conversationId, config.sender);
499
+ },
500
+ async getStatus(message) {
501
+ const raw = await request(
502
+ `/chats/${encodePath(message.conversationId)}/messages/${encodePath(message.messageId)}/status`,
503
+ {},
504
+ { notFoundNull: true }
505
+ );
506
+ if (raw === null) return null;
507
+ const providerStatus = OptionalNonEmptyStringSchema.parse(raw["status"]);
508
+ const sentAt = dateValue(raw["time_sent"]);
509
+ const deliveredAt = dateValue(raw["time_delivered"]);
510
+ const error = OptionalNonEmptyStringSchema.parse(raw["error"]);
511
+ return {
512
+ messageId: OptionalNonEmptyStringSchema.parse(raw["message_id"]) ?? message.messageId,
513
+ conversationId: OptionalNonEmptyStringSchema.parse(raw["chat_id"]) ?? message.conversationId,
514
+ status: mapStatus(providerStatus),
515
+ ...providerStatus === void 0 ? {} : { providerStatus },
516
+ service: mapService(raw["protocol"]),
517
+ ...sentAt === void 0 ? {} : { sentAt },
518
+ ...deliveredAt === void 0 ? {} : { deliveredAt },
519
+ ...error === void 0 ? {} : { error },
520
+ raw
521
+ };
522
+ }
523
+ };
524
+ const conversations = {
525
+ async open(input) {
526
+ const id = conversationId(input);
527
+ return {
528
+ providerConversationId: id,
529
+ participants: input.participants,
530
+ raw: { chat_id: id, resolved: false }
531
+ };
532
+ },
533
+ async get(id) {
534
+ const raw = await request(
535
+ `/chats/${encodePath(id)}`,
536
+ {},
537
+ { notFoundNull: true }
538
+ );
539
+ if (raw === null) return null;
540
+ const participantIdentifiers = participantValues(raw["participants"]);
541
+ const groupMembers = raw["is_group"] === true || id.startsWith("grp_") ? await request(
542
+ `/groups/${encodePath(id)}/members?limit=100`,
543
+ {},
544
+ { notFoundNull: true }
545
+ ) : null;
546
+ const groupMemberIdentifiers = groupMembers === null ? [] : participantValues(groupMembers["members"]);
547
+ const parsedContact = JsonObjectSchema.safeParse(raw["contact"]);
548
+ const contact = parsedContact.success ? parsedContact.data : void 0;
549
+ const contactIdentifier = contact === void 0 ? void 0 : OptionalNonEmptyStringSchema.parse(contact["identifier"]);
550
+ const participants = groupMemberIdentifiers.length > 0 ? groupMemberIdentifiers.map(address) : participantIdentifiers.length > 0 ? participantIdentifiers.map(address) : contactIdentifier === void 0 ? [address(OptionalNonEmptyStringSchema.parse(raw["id"]) ?? id)] : [address(contactIdentifier)];
551
+ const createdAt = dateValue(raw["first_message_time"]);
552
+ return {
553
+ providerConversationId: OptionalNonEmptyStringSchema.parse(raw["id"]) ?? id,
554
+ participants,
555
+ ...createdAt === void 0 ? {} : { createdAt },
556
+ raw
557
+ };
558
+ },
559
+ async markRead(id) {
560
+ await request(`/chats/${encodePath(id)}/read`, { method: "POST" });
561
+ }
562
+ };
563
+ const react = async (action, input) => {
564
+ await request(
565
+ `/chats/${encodePath(input.conversationId)}/messages/${encodePath(input.messageId)}/reactions`,
566
+ {
567
+ method: "POST",
568
+ body: JSON.stringify({ reaction: `${action}${input.reaction}` })
569
+ }
570
+ );
571
+ };
572
+ return defineProvider({
573
+ name: "blooio",
574
+ capabilities: BLOOIO_CAPABILITIES,
575
+ messages,
576
+ conversations,
577
+ reactions: {
578
+ async add(input) {
579
+ await react("+", input);
580
+ },
581
+ async remove(input) {
582
+ await react("-", input);
583
+ }
584
+ },
585
+ typing: {
586
+ async start(id) {
587
+ await request(`/chats/${encodePath(id)}/typing`, { method: "POST" });
588
+ },
589
+ async stop(id) {
590
+ await request(`/chats/${encodePath(id)}/typing`, { method: "DELETE" });
591
+ }
592
+ },
593
+ webhooks: {
594
+ async verify(webhookRequest) {
595
+ if (config.webhookSecret === void 0) return false;
596
+ const header = webhookRequest.headers.get("x-blooio-signature");
597
+ if (header === null) return false;
598
+ const parsed = parseSignature(header);
599
+ if (parsed === void 0) return false;
600
+ const timestamp = Number(parsed.timestamp);
601
+ if (!Number.isFinite(timestamp)) return false;
602
+ const age = Math.abs(Math.floor(Date.now() / 1e3) - timestamp);
603
+ if (age > DEFAULT_WEBHOOK_TOLERANCE_SECONDS) return false;
604
+ const rawBody = await webhookRequest.text();
605
+ const expected = await hmacSha256(
606
+ config.webhookSecret,
607
+ `${parsed.timestamp}.${rawBody}`
608
+ );
609
+ return parsed.signatures.some(
610
+ (signature) => constantTimeEqual(expected, signature.toLowerCase())
611
+ );
612
+ },
613
+ async parse(webhookRequest) {
614
+ const body = await webhookRequest.json();
615
+ const values = Array.isArray(body) ? body : [body];
616
+ return values.flatMap((value) => {
617
+ const parsedValue = JsonObjectSchema.safeParse(value);
618
+ if (!parsedValue.success) return [];
619
+ const event = mapWebhookEvent(parsedValue.data);
620
+ return event === void 0 ? [] : [event];
621
+ });
622
+ }
623
+ },
624
+ numbers: {
625
+ async list() {
626
+ const response = await request("/me/numbers");
627
+ const values = Array.isArray(response?.["numbers"]) ? response["numbers"] : [];
628
+ return values.flatMap((value) => {
629
+ const parsedValue = JsonObjectSchema.safeParse(value);
630
+ if (!parsedValue.success) return [];
631
+ const item = parsedValue.data;
632
+ const phoneNumber = OptionalNonEmptyStringSchema.parse(
633
+ item["phone_number"]
634
+ );
635
+ if (phoneNumber === void 0) return [];
636
+ const lastActive = dateValue(item["last_active"]);
637
+ const parsedPlanKind = PlanKindSchema.safeParse(item["plan_kind"]);
638
+ return [
639
+ {
640
+ phoneNumber,
641
+ active: item["is_active"] === true,
642
+ ...lastActive === void 0 ? {} : { lastActive },
643
+ ...parsedPlanKind.success ? { planKind: parsedPlanKind.data } : {},
644
+ raw: item
645
+ }
646
+ ];
647
+ });
648
+ }
649
+ }
650
+ });
651
+ }
652
+
653
+ export { BLOOIO_CAPABILITIES, blooio };
654
+ //# sourceMappingURL=blooio.js.map
655
+ //# sourceMappingURL=blooio.js.map