chatly-sdk 1.0.0 → 2.0.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.
Files changed (63) hide show
  1. package/CONTRIBUTING.md +658 -0
  2. package/IMPROVEMENTS.md +402 -0
  3. package/LICENSE +21 -0
  4. package/README.md +1576 -162
  5. package/dist/index.d.ts +502 -11
  6. package/dist/index.js +1619 -66
  7. package/examples/01-basic-chat/README.md +61 -0
  8. package/examples/01-basic-chat/index.js +58 -0
  9. package/examples/01-basic-chat/package.json +13 -0
  10. package/examples/02-group-chat/README.md +78 -0
  11. package/examples/02-group-chat/index.js +76 -0
  12. package/examples/02-group-chat/package.json +13 -0
  13. package/examples/03-offline-messaging/README.md +73 -0
  14. package/examples/03-offline-messaging/index.js +80 -0
  15. package/examples/03-offline-messaging/package.json +13 -0
  16. package/examples/04-live-chat/README.md +80 -0
  17. package/examples/04-live-chat/index.js +114 -0
  18. package/examples/04-live-chat/package.json +13 -0
  19. package/examples/05-hybrid-messaging/README.md +71 -0
  20. package/examples/05-hybrid-messaging/index.js +106 -0
  21. package/examples/05-hybrid-messaging/package.json +13 -0
  22. package/examples/06-postgresql-integration/README.md +101 -0
  23. package/examples/06-postgresql-integration/adapters/groupStore.js +73 -0
  24. package/examples/06-postgresql-integration/adapters/messageStore.js +47 -0
  25. package/examples/06-postgresql-integration/adapters/userStore.js +40 -0
  26. package/examples/06-postgresql-integration/index.js +92 -0
  27. package/examples/06-postgresql-integration/package.json +14 -0
  28. package/examples/06-postgresql-integration/schema.sql +58 -0
  29. package/examples/08-customer-support/README.md +70 -0
  30. package/examples/08-customer-support/index.js +104 -0
  31. package/examples/08-customer-support/package.json +13 -0
  32. package/examples/README.md +105 -0
  33. package/jest.config.cjs +28 -0
  34. package/package.json +15 -6
  35. package/src/chat/ChatSession.ts +160 -3
  36. package/src/chat/GroupSession.ts +108 -1
  37. package/src/constants.ts +61 -0
  38. package/src/crypto/e2e.ts +9 -20
  39. package/src/crypto/utils.ts +3 -1
  40. package/src/index.ts +530 -63
  41. package/src/models/mediaTypes.ts +62 -0
  42. package/src/models/message.ts +4 -1
  43. package/src/storage/adapters.ts +36 -0
  44. package/src/storage/localStorage.ts +49 -0
  45. package/src/storage/s3Storage.ts +84 -0
  46. package/src/stores/adapters.ts +2 -0
  47. package/src/stores/memory/messageStore.ts +8 -0
  48. package/src/transport/adapters.ts +51 -1
  49. package/src/transport/memoryTransport.ts +75 -13
  50. package/src/transport/websocketClient.ts +269 -21
  51. package/src/transport/websocketServer.ts +26 -26
  52. package/src/utils/errors.ts +97 -0
  53. package/src/utils/logger.ts +96 -0
  54. package/src/utils/mediaUtils.ts +235 -0
  55. package/src/utils/messageQueue.ts +162 -0
  56. package/src/utils/validation.ts +99 -0
  57. package/test/crypto.test.ts +122 -35
  58. package/test/sdk.test.ts +276 -0
  59. package/test/validation.test.ts +64 -0
  60. package/tsconfig.json +11 -10
  61. package/tsconfig.test.json +11 -0
  62. package/src/ChatManager.ts +0 -103
  63. package/src/crypto/keyManager.ts +0 -28
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // src/index.ts
2
+ import { EventEmitter } from "events";
3
+
1
4
  // src/crypto/e2e.ts
2
5
  import { createECDH as createECDH2, createCipheriv, createDecipheriv, randomBytes as randomBytes2, pbkdf2Sync } from "crypto";
3
6
 
@@ -6,6 +9,8 @@ function bufferToBase64(buffer) {
6
9
  return buffer.toString("base64");
7
10
  }
8
11
  function base64ToBuffer(data) {
12
+ if (Buffer.isBuffer(data)) return data;
13
+ if (!data) return Buffer.alloc(0);
9
14
  return Buffer.from(data, "base64");
10
15
  }
11
16
 
@@ -42,6 +47,11 @@ function deriveSharedSecret(local, remotePublicKey) {
42
47
  const derivedKey = pbkdf2Sync(sharedSecret, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
43
48
  return derivedKey;
44
49
  }
50
+ function deriveLegacySharedSecret(local, remotePublicKey) {
51
+ const ecdh = createECDH2(SUPPORTED_CURVE);
52
+ ecdh.setPrivateKey(base64ToBuffer(local.privateKey));
53
+ return ecdh.computeSecret(base64ToBuffer(remotePublicKey));
54
+ }
45
55
  function encryptMessage(plaintext, secret) {
46
56
  const iv = randomBytes2(IV_LENGTH);
47
57
  const cipher = createCipheriv(ALGORITHM, secret, iv);
@@ -92,12 +102,78 @@ function generateUUID() {
92
102
  });
93
103
  }
94
104
 
105
+ // src/utils/logger.ts
106
+ var LogLevel = /* @__PURE__ */ ((LogLevel3) => {
107
+ LogLevel3[LogLevel3["DEBUG"] = 0] = "DEBUG";
108
+ LogLevel3[LogLevel3["INFO"] = 1] = "INFO";
109
+ LogLevel3[LogLevel3["WARN"] = 2] = "WARN";
110
+ LogLevel3[LogLevel3["ERROR"] = 3] = "ERROR";
111
+ LogLevel3[LogLevel3["NONE"] = 4] = "NONE";
112
+ return LogLevel3;
113
+ })(LogLevel || {});
114
+ var Logger = class {
115
+ config;
116
+ constructor(config = {}) {
117
+ this.config = {
118
+ level: config.level ?? 1 /* INFO */,
119
+ prefix: config.prefix ?? "[ChatSDK]",
120
+ timestamp: config.timestamp ?? true
121
+ };
122
+ }
123
+ shouldLog(level) {
124
+ return level >= this.config.level;
125
+ }
126
+ formatMessage(level, message, data) {
127
+ const parts = [];
128
+ if (this.config.timestamp) {
129
+ parts.push((/* @__PURE__ */ new Date()).toISOString());
130
+ }
131
+ parts.push(this.config.prefix);
132
+ parts.push(`[${level}]`);
133
+ parts.push(message);
134
+ let formatted = parts.join(" ");
135
+ if (data !== void 0) {
136
+ formatted += " " + JSON.stringify(data, null, 2);
137
+ }
138
+ return formatted;
139
+ }
140
+ debug(message, data) {
141
+ if (this.shouldLog(0 /* DEBUG */)) {
142
+ console.debug(this.formatMessage("DEBUG", message, data));
143
+ }
144
+ }
145
+ info(message, data) {
146
+ if (this.shouldLog(1 /* INFO */)) {
147
+ console.info(this.formatMessage("INFO", message, data));
148
+ }
149
+ }
150
+ warn(message, data) {
151
+ if (this.shouldLog(2 /* WARN */)) {
152
+ console.warn(this.formatMessage("WARN", message, data));
153
+ }
154
+ }
155
+ error(message, error) {
156
+ if (this.shouldLog(3 /* ERROR */)) {
157
+ const errorData = error instanceof Error ? { message: error.message, stack: error.stack } : error;
158
+ console.error(this.formatMessage("ERROR", message, errorData));
159
+ }
160
+ }
161
+ setLevel(level) {
162
+ this.config.level = level;
163
+ }
164
+ getLevel() {
165
+ return this.config.level;
166
+ }
167
+ };
168
+ var logger = new Logger();
169
+
95
170
  // src/chat/ChatSession.ts
96
171
  var ChatSession = class {
97
- constructor(id, userA, userB) {
172
+ constructor(id, userA, userB, storageProvider) {
98
173
  this.id = id;
99
174
  this.userA = userA;
100
175
  this.userB = userB;
176
+ this.storageProvider = storageProvider;
101
177
  }
102
178
  sharedSecret = null;
103
179
  ephemeralKeyPair = null;
@@ -121,6 +197,12 @@ var ChatSession = class {
121
197
  publicKey: user.publicKey,
122
198
  privateKey: user.privateKey
123
199
  };
200
+ logger.debug(`[ChatSession] Initializing for user ${user.id}`, {
201
+ hasLocalPriv: !!user.privateKey,
202
+ privType: typeof user.privateKey,
203
+ hasRemotePub: !!otherUser.publicKey,
204
+ pubType: typeof otherUser.publicKey
205
+ });
124
206
  this.sharedSecret = deriveSharedSecret(localKeyPair, otherUser.publicKey);
125
207
  }
126
208
  /**
@@ -144,6 +226,49 @@ var ChatSession = class {
144
226
  type: "text"
145
227
  };
146
228
  }
229
+ /**
230
+ * Encrypt a media message for this session
231
+ */
232
+ async encryptMedia(plaintext, media, senderId) {
233
+ if (!this.sharedSecret) {
234
+ await this.initialize();
235
+ }
236
+ if (!this.sharedSecret) {
237
+ throw new Error("Failed to initialize session");
238
+ }
239
+ const { ciphertext, iv } = encryptMessage(plaintext, this.sharedSecret);
240
+ const { ciphertext: encryptedMediaData, iv: mediaIv } = encryptMessage(
241
+ media.data || "",
242
+ this.sharedSecret
243
+ );
244
+ const encryptedMedia = {
245
+ ...media,
246
+ data: encryptedMediaData,
247
+ iv: mediaIv
248
+ };
249
+ if (this.storageProvider) {
250
+ const filename = `${this.id}/${generateUUID()}-${media.metadata.filename}`;
251
+ const uploadResult = await this.storageProvider.upload(
252
+ encryptedMediaData,
253
+ filename,
254
+ media.metadata.mimeType
255
+ );
256
+ encryptedMedia.storage = this.storageProvider.name;
257
+ encryptedMedia.storageKey = uploadResult.storageKey;
258
+ encryptedMedia.url = uploadResult.url;
259
+ encryptedMedia.data = void 0;
260
+ }
261
+ return {
262
+ id: generateUUID(),
263
+ senderId,
264
+ receiverId: senderId === this.userA.id ? this.userB.id : this.userA.id,
265
+ ciphertext,
266
+ iv,
267
+ timestamp: Date.now(),
268
+ type: "media",
269
+ media: encryptedMedia
270
+ };
271
+ }
147
272
  /**
148
273
  * Decrypt a message in this session
149
274
  */
@@ -154,7 +279,75 @@ var ChatSession = class {
154
279
  if (!this.sharedSecret) {
155
280
  throw new Error("Failed to initialize session");
156
281
  }
157
- return decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
282
+ try {
283
+ return decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
284
+ } catch (error) {
285
+ const legacySecret = this.deriveLegacySecret(user);
286
+ try {
287
+ return decryptMessage(message.ciphertext, message.iv, legacySecret);
288
+ } catch (innerError) {
289
+ throw error;
290
+ }
291
+ }
292
+ }
293
+ deriveLegacySecret(user) {
294
+ const otherUser = user.id === this.userA.id ? this.userB : this.userA;
295
+ logger.debug(`[ChatSession] Deriving legacy secret for user ${user.id}`, {
296
+ hasPriv: !!user.privateKey,
297
+ privType: typeof user.privateKey,
298
+ remotePubType: typeof otherUser.publicKey
299
+ });
300
+ const localKeyPair = {
301
+ publicKey: user.publicKey,
302
+ privateKey: user.privateKey
303
+ };
304
+ return deriveLegacySharedSecret(localKeyPair, otherUser.publicKey);
305
+ }
306
+ /**
307
+ * Decrypt a media message in this session
308
+ */
309
+ async decryptMedia(message, user) {
310
+ if (!message.media) {
311
+ throw new Error("Message does not contain media");
312
+ }
313
+ if (!this.sharedSecret || user.id !== this.userA.id && user.id !== this.userB.id) {
314
+ await this.initializeForUser(user);
315
+ }
316
+ if (!this.sharedSecret) {
317
+ throw new Error("Failed to initialize session");
318
+ }
319
+ const text = decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
320
+ let encryptedMediaData = message.media.data;
321
+ if (!encryptedMediaData && message.media.storageKey && this.storageProvider) {
322
+ encryptedMediaData = await this.storageProvider.download(message.media.storageKey);
323
+ }
324
+ if (!message.media.iv && !encryptedMediaData) {
325
+ throw new Error("Media data or IV missing");
326
+ }
327
+ let decryptedMediaData;
328
+ try {
329
+ decryptedMediaData = decryptMessage(
330
+ encryptedMediaData || "",
331
+ message.media.iv || message.iv,
332
+ this.sharedSecret
333
+ );
334
+ } catch (error) {
335
+ const legacySecret = this.deriveLegacySecret(user);
336
+ try {
337
+ decryptedMediaData = decryptMessage(
338
+ encryptedMediaData || "",
339
+ message.media.iv || message.iv,
340
+ legacySecret
341
+ );
342
+ } catch (innerError) {
343
+ throw error;
344
+ }
345
+ }
346
+ const decryptedMedia = {
347
+ ...message.media,
348
+ data: decryptedMediaData
349
+ };
350
+ return { text, media: decryptedMedia };
158
351
  }
159
352
  };
160
353
 
@@ -173,8 +366,9 @@ function deriveGroupKey(groupId) {
173
366
 
174
367
  // src/chat/GroupSession.ts
175
368
  var GroupSession = class {
176
- constructor(group) {
369
+ constructor(group, storageProvider) {
177
370
  this.group = group;
371
+ this.storageProvider = storageProvider;
178
372
  }
179
373
  groupKey = null;
180
374
  /**
@@ -205,6 +399,49 @@ var GroupSession = class {
205
399
  type: "text"
206
400
  };
207
401
  }
402
+ /**
403
+ * Encrypt a media message for this group
404
+ */
405
+ async encryptMedia(plaintext, media, senderId) {
406
+ if (!this.groupKey) {
407
+ await this.initialize();
408
+ }
409
+ if (!this.groupKey) {
410
+ throw new Error("Failed to initialize group session");
411
+ }
412
+ const { ciphertext, iv } = encryptMessage(plaintext, this.groupKey);
413
+ const { ciphertext: encryptedMediaData, iv: mediaIv } = encryptMessage(
414
+ media.data || "",
415
+ this.groupKey
416
+ );
417
+ const encryptedMedia = {
418
+ ...media,
419
+ data: encryptedMediaData,
420
+ iv: mediaIv
421
+ };
422
+ if (this.storageProvider) {
423
+ const filename = `groups/${this.group.id}/${generateUUID()}-${media.metadata.filename}`;
424
+ const uploadResult = await this.storageProvider.upload(
425
+ encryptedMediaData,
426
+ filename,
427
+ media.metadata.mimeType
428
+ );
429
+ encryptedMedia.storage = this.storageProvider.name;
430
+ encryptedMedia.storageKey = uploadResult.storageKey;
431
+ encryptedMedia.url = uploadResult.url;
432
+ encryptedMedia.data = void 0;
433
+ }
434
+ return {
435
+ id: generateUUID(),
436
+ senderId,
437
+ groupId: this.group.id,
438
+ ciphertext,
439
+ iv,
440
+ timestamp: Date.now(),
441
+ type: "media",
442
+ media: encryptedMedia
443
+ };
444
+ }
208
445
  /**
209
446
  * Decrypt a message in this group
210
447
  */
@@ -217,6 +454,335 @@ var GroupSession = class {
217
454
  }
218
455
  return decryptMessage(message.ciphertext, message.iv, this.groupKey);
219
456
  }
457
+ /**
458
+ * Decrypt a media message in this group
459
+ */
460
+ async decryptMedia(message) {
461
+ if (!message.media) {
462
+ throw new Error("Message does not contain media");
463
+ }
464
+ if (!this.groupKey) {
465
+ await this.initialize();
466
+ }
467
+ if (!this.groupKey) {
468
+ throw new Error("Failed to initialize group session");
469
+ }
470
+ const text = decryptMessage(message.ciphertext, message.iv, this.groupKey);
471
+ let encryptedMediaData = message.media.data;
472
+ if (!encryptedMediaData && message.media.storageKey && this.storageProvider) {
473
+ encryptedMediaData = await this.storageProvider.download(message.media.storageKey);
474
+ }
475
+ if (!message.media.iv && !encryptedMediaData) {
476
+ throw new Error("Media data or IV missing");
477
+ }
478
+ const decryptedMediaData = decryptMessage(
479
+ encryptedMediaData || "",
480
+ message.media.iv || message.iv,
481
+ // Fallback to message IV for backward compatibility
482
+ this.groupKey
483
+ );
484
+ const decryptedMedia = {
485
+ ...message.media,
486
+ data: decryptedMediaData
487
+ };
488
+ return { text, media: decryptedMedia };
489
+ }
490
+ };
491
+
492
+ // src/utils/errors.ts
493
+ var SDKError = class extends Error {
494
+ constructor(message, code, retryable = false, details) {
495
+ super(message);
496
+ this.code = code;
497
+ this.retryable = retryable;
498
+ this.details = details;
499
+ this.name = this.constructor.name;
500
+ Error.captureStackTrace(this, this.constructor);
501
+ }
502
+ toJSON() {
503
+ return {
504
+ name: this.name,
505
+ message: this.message,
506
+ code: this.code,
507
+ retryable: this.retryable,
508
+ details: this.details
509
+ };
510
+ }
511
+ };
512
+ var NetworkError = class extends SDKError {
513
+ constructor(message, details) {
514
+ super(message, "NETWORK_ERROR", true, details);
515
+ }
516
+ };
517
+ var EncryptionError = class extends SDKError {
518
+ constructor(message, details) {
519
+ super(message, "ENCRYPTION_ERROR", false, details);
520
+ }
521
+ };
522
+ var AuthError = class extends SDKError {
523
+ constructor(message, details) {
524
+ super(message, "AUTH_ERROR", false, details);
525
+ }
526
+ };
527
+ var ValidationError = class extends SDKError {
528
+ constructor(message, details) {
529
+ super(message, "VALIDATION_ERROR", false, details);
530
+ }
531
+ };
532
+ var StorageError = class extends SDKError {
533
+ constructor(message, retryable = true, details) {
534
+ super(message, "STORAGE_ERROR", retryable, details);
535
+ }
536
+ };
537
+ var SessionError = class extends SDKError {
538
+ constructor(message, details) {
539
+ super(message, "SESSION_ERROR", false, details);
540
+ }
541
+ };
542
+ var TransportError = class extends SDKError {
543
+ constructor(message, retryable = true, details) {
544
+ super(message, "TRANSPORT_ERROR", retryable, details);
545
+ }
546
+ };
547
+ var ConfigError = class extends SDKError {
548
+ constructor(message, details) {
549
+ super(message, "CONFIG_ERROR", false, details);
550
+ }
551
+ };
552
+
553
+ // src/utils/validation.ts
554
+ var USERNAME_REGEX = /^[a-zA-Z0-9_-]{3,20}$/;
555
+ var MAX_MESSAGE_LENGTH = 1e4;
556
+ var MIN_GROUP_MEMBERS = 2;
557
+ var MAX_GROUP_MEMBERS = 256;
558
+ var MAX_GROUP_NAME_LENGTH = 100;
559
+ function validateUsername(username) {
560
+ if (!username || typeof username !== "string") {
561
+ throw new ValidationError("Username is required", { username });
562
+ }
563
+ if (!USERNAME_REGEX.test(username)) {
564
+ throw new ValidationError(
565
+ "Username must be 3-20 characters and contain only letters, numbers, underscores, and hyphens",
566
+ { username }
567
+ );
568
+ }
569
+ }
570
+ function validateMessage(message) {
571
+ if (!message || typeof message !== "string") {
572
+ throw new ValidationError("Message content is required");
573
+ }
574
+ if (message.length === 0) {
575
+ throw new ValidationError("Message cannot be empty");
576
+ }
577
+ if (message.length > MAX_MESSAGE_LENGTH) {
578
+ throw new ValidationError(
579
+ `Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`,
580
+ { length: message.length, max: MAX_MESSAGE_LENGTH }
581
+ );
582
+ }
583
+ }
584
+ function validateGroupName(name) {
585
+ if (!name || typeof name !== "string") {
586
+ throw new ValidationError("Group name is required");
587
+ }
588
+ if (name.trim().length === 0) {
589
+ throw new ValidationError("Group name cannot be empty");
590
+ }
591
+ if (name.length > MAX_GROUP_NAME_LENGTH) {
592
+ throw new ValidationError(
593
+ `Group name exceeds maximum length of ${MAX_GROUP_NAME_LENGTH} characters`,
594
+ { length: name.length, max: MAX_GROUP_NAME_LENGTH }
595
+ );
596
+ }
597
+ }
598
+ function validateGroupMembers(memberCount) {
599
+ if (memberCount < MIN_GROUP_MEMBERS) {
600
+ throw new ValidationError(
601
+ `Group must have at least ${MIN_GROUP_MEMBERS} members`,
602
+ { count: memberCount, min: MIN_GROUP_MEMBERS }
603
+ );
604
+ }
605
+ if (memberCount > MAX_GROUP_MEMBERS) {
606
+ throw new ValidationError(
607
+ `Group cannot have more than ${MAX_GROUP_MEMBERS} members`,
608
+ { count: memberCount, max: MAX_GROUP_MEMBERS }
609
+ );
610
+ }
611
+ }
612
+ function validateUserId(userId) {
613
+ if (!userId || typeof userId !== "string") {
614
+ throw new ValidationError("User ID is required");
615
+ }
616
+ if (userId.trim().length === 0) {
617
+ throw new ValidationError("User ID cannot be empty");
618
+ }
619
+ }
620
+
621
+ // src/constants.ts
622
+ var SUPPORTED_CURVE2 = "prime256v1";
623
+ var ALGORITHM2 = "aes-256-gcm";
624
+ var IV_LENGTH2 = 12;
625
+ var SALT_LENGTH2 = 16;
626
+ var KEY_LENGTH3 = 32;
627
+ var TAG_LENGTH2 = 16;
628
+ var PBKDF2_ITERATIONS3 = 1e5;
629
+ var USERNAME_MIN_LENGTH = 3;
630
+ var USERNAME_MAX_LENGTH = 20;
631
+ var MESSAGE_MAX_LENGTH = 1e4;
632
+ var GROUP_NAME_MAX_LENGTH = 100;
633
+ var GROUP_MIN_MEMBERS = 2;
634
+ var GROUP_MAX_MEMBERS = 256;
635
+ var RECONNECT_MAX_ATTEMPTS = 5;
636
+ var RECONNECT_BASE_DELAY = 1e3;
637
+ var RECONNECT_MAX_DELAY = 3e4;
638
+ var HEARTBEAT_INTERVAL = 3e4;
639
+ var CONNECTION_TIMEOUT = 1e4;
640
+ var MAX_QUEUE_SIZE = 1e3;
641
+ var MESSAGE_RETRY_ATTEMPTS = 3;
642
+ var MESSAGE_RETRY_DELAY = 2e3;
643
+ var EVENTS = {
644
+ MESSAGE_SENT: "message:sent",
645
+ MESSAGE_RECEIVED: "message:received",
646
+ MESSAGE_FAILED: "message:failed",
647
+ CONNECTION_STATE_CHANGED: "connection:state",
648
+ SESSION_CREATED: "session:created",
649
+ GROUP_CREATED: "group:created",
650
+ ERROR: "error",
651
+ USER_CREATED: "user:created"
652
+ };
653
+ var ConnectionState = /* @__PURE__ */ ((ConnectionState2) => {
654
+ ConnectionState2["DISCONNECTED"] = "disconnected";
655
+ ConnectionState2["CONNECTING"] = "connecting";
656
+ ConnectionState2["CONNECTED"] = "connected";
657
+ ConnectionState2["RECONNECTING"] = "reconnecting";
658
+ ConnectionState2["FAILED"] = "failed";
659
+ return ConnectionState2;
660
+ })(ConnectionState || {});
661
+ var MessageStatus = /* @__PURE__ */ ((MessageStatus2) => {
662
+ MessageStatus2["PENDING"] = "pending";
663
+ MessageStatus2["SENT"] = "sent";
664
+ MessageStatus2["DELIVERED"] = "delivered";
665
+ MessageStatus2["FAILED"] = "failed";
666
+ return MessageStatus2;
667
+ })(MessageStatus || {});
668
+
669
+ // src/utils/messageQueue.ts
670
+ var MessageQueue = class {
671
+ queue = /* @__PURE__ */ new Map();
672
+ maxSize;
673
+ maxRetries;
674
+ retryDelay;
675
+ constructor(maxSize = MAX_QUEUE_SIZE, maxRetries = MESSAGE_RETRY_ATTEMPTS, retryDelay = MESSAGE_RETRY_DELAY) {
676
+ this.maxSize = maxSize;
677
+ this.maxRetries = maxRetries;
678
+ this.retryDelay = retryDelay;
679
+ }
680
+ /**
681
+ * Add a message to the queue
682
+ */
683
+ enqueue(message) {
684
+ if (this.queue.size >= this.maxSize) {
685
+ logger.warn("Message queue is full, removing oldest message");
686
+ const firstKey = this.queue.keys().next().value;
687
+ if (firstKey) {
688
+ this.queue.delete(firstKey);
689
+ }
690
+ }
691
+ this.queue.set(message.id, {
692
+ message,
693
+ status: "pending" /* PENDING */,
694
+ attempts: 0
695
+ });
696
+ logger.debug("Message enqueued", { messageId: message.id });
697
+ }
698
+ /**
699
+ * Mark a message as sent
700
+ */
701
+ markSent(messageId) {
702
+ const queued = this.queue.get(messageId);
703
+ if (queued) {
704
+ queued.status = "sent" /* SENT */;
705
+ logger.debug("Message marked as sent", { messageId });
706
+ this.queue.delete(messageId);
707
+ }
708
+ }
709
+ /**
710
+ * Mark a message as failed
711
+ */
712
+ markFailed(messageId, error) {
713
+ const queued = this.queue.get(messageId);
714
+ if (queued) {
715
+ queued.status = "failed" /* FAILED */;
716
+ queued.error = error;
717
+ queued.attempts++;
718
+ queued.lastAttempt = Date.now();
719
+ logger.warn("Message failed", {
720
+ messageId,
721
+ attempts: queued.attempts,
722
+ error: error.message
723
+ });
724
+ if (queued.attempts >= this.maxRetries) {
725
+ logger.error("Message exceeded max retries, removing from queue", {
726
+ messageId,
727
+ attempts: queued.attempts
728
+ });
729
+ this.queue.delete(messageId);
730
+ }
731
+ }
732
+ }
733
+ /**
734
+ * Get messages that need to be retried
735
+ */
736
+ getRetryableMessages() {
737
+ const now = Date.now();
738
+ const retryable = [];
739
+ for (const queued of this.queue.values()) {
740
+ if (queued.status === "failed" /* FAILED */ && queued.attempts < this.maxRetries && (!queued.lastAttempt || now - queued.lastAttempt >= this.retryDelay)) {
741
+ retryable.push(queued);
742
+ }
743
+ }
744
+ return retryable;
745
+ }
746
+ /**
747
+ * Get all pending messages
748
+ */
749
+ getPendingMessages() {
750
+ return Array.from(this.queue.values()).filter(
751
+ (q) => q.status === "pending" /* PENDING */
752
+ );
753
+ }
754
+ /**
755
+ * Get queue size
756
+ */
757
+ size() {
758
+ return this.queue.size;
759
+ }
760
+ /**
761
+ * Clear the queue
762
+ */
763
+ clear() {
764
+ this.queue.clear();
765
+ logger.debug("Message queue cleared");
766
+ }
767
+ /**
768
+ * Get message by ID
769
+ */
770
+ get(messageId) {
771
+ return this.queue.get(messageId);
772
+ }
773
+ /**
774
+ * Remove message from queue
775
+ */
776
+ remove(messageId) {
777
+ this.queue.delete(messageId);
778
+ logger.debug("Message removed from queue", { messageId });
779
+ }
780
+ /**
781
+ * Get all messages in queue
782
+ */
783
+ getAll() {
784
+ return Array.from(this.queue.values());
785
+ }
220
786
  };
221
787
 
222
788
  // src/stores/memory/userStore.ts
@@ -245,6 +811,9 @@ var InMemoryMessageStore = class {
245
811
  this.messages.push(message);
246
812
  return message;
247
813
  }
814
+ async findById(id) {
815
+ return this.messages.find((msg) => msg.id === id);
816
+ }
248
817
  async listByUser(userId) {
249
818
  return this.messages.filter(
250
819
  (msg) => msg.senderId === userId || msg.receiverId === userId
@@ -253,6 +822,9 @@ var InMemoryMessageStore = class {
253
822
  async listByGroup(groupId) {
254
823
  return this.messages.filter((msg) => msg.groupId === groupId);
255
824
  }
825
+ async delete(id) {
826
+ this.messages = this.messages.filter((msg) => msg.id !== id);
827
+ }
256
828
  };
257
829
 
258
830
  // src/stores/memory/groupStore.ts
@@ -272,33 +844,642 @@ var InMemoryGroupStore = class {
272
844
 
273
845
  // src/transport/memoryTransport.ts
274
846
  var InMemoryTransport = class {
275
- handler;
276
- connected = false;
277
- async connect(_userId) {
278
- this.connected = true;
847
+ messageHandler = null;
848
+ connectionState = "disconnected" /* DISCONNECTED */;
849
+ stateHandler = null;
850
+ errorHandler = null;
851
+ async connect(userId) {
852
+ this.connectionState = "connected" /* CONNECTED */;
853
+ if (this.stateHandler) {
854
+ this.stateHandler(this.connectionState);
855
+ }
856
+ }
857
+ async disconnect() {
858
+ this.connectionState = "disconnected" /* DISCONNECTED */;
859
+ if (this.stateHandler) {
860
+ this.stateHandler(this.connectionState);
861
+ }
862
+ }
863
+ async reconnect() {
864
+ this.connectionState = "connecting" /* CONNECTING */;
865
+ if (this.stateHandler) {
866
+ this.stateHandler(this.connectionState);
867
+ }
868
+ await new Promise((resolve2) => setTimeout(resolve2, 100));
869
+ this.connectionState = "connected" /* CONNECTED */;
870
+ if (this.stateHandler) {
871
+ this.stateHandler(this.connectionState);
872
+ }
279
873
  }
280
874
  async send(message) {
281
- if (!this.connected) {
282
- throw new Error("Transport not connected");
875
+ if (this.messageHandler) {
876
+ setTimeout(() => {
877
+ this.messageHandler(message);
878
+ }, 10);
283
879
  }
284
- this.handler?.(message);
285
880
  }
286
881
  onMessage(handler) {
287
- this.handler = handler;
882
+ this.messageHandler = handler;
883
+ }
884
+ onConnectionStateChange(handler) {
885
+ this.stateHandler = handler;
886
+ }
887
+ onError(handler) {
888
+ this.errorHandler = handler;
889
+ }
890
+ getConnectionState() {
891
+ return this.connectionState;
892
+ }
893
+ isConnected() {
894
+ return this.connectionState === "connected" /* CONNECTED */;
895
+ }
896
+ // Test helper to simulate receiving a message
897
+ simulateReceive(message) {
898
+ if (this.messageHandler) {
899
+ this.messageHandler(message);
900
+ }
901
+ }
902
+ // Test helper to simulate an error
903
+ simulateError(error) {
904
+ if (this.errorHandler) {
905
+ this.errorHandler(error);
906
+ }
907
+ }
908
+ };
909
+
910
+ // src/transport/websocketClient.ts
911
+ var WebSocketClient = class {
912
+ ws = null;
913
+ url;
914
+ messageHandler = null;
915
+ stateHandler = null;
916
+ errorHandler = null;
917
+ connectionState = "disconnected" /* DISCONNECTED */;
918
+ reconnectAttempts = 0;
919
+ reconnectTimer = null;
920
+ heartbeatTimer = null;
921
+ currentUserId = null;
922
+ shouldReconnect = true;
923
+ constructor(url) {
924
+ this.url = url;
925
+ }
926
+ async connect(userId) {
927
+ this.currentUserId = userId;
928
+ this.shouldReconnect = true;
929
+ return this.doConnect();
930
+ }
931
+ async doConnect() {
932
+ if (this.connectionState === "connecting" /* CONNECTING */) {
933
+ logger.warn("Already connecting, skipping duplicate connect attempt");
934
+ return;
935
+ }
936
+ this.updateState("connecting" /* CONNECTING */);
937
+ logger.info("Connecting to WebSocket", { url: this.url, userId: this.currentUserId });
938
+ return new Promise((resolve2, reject) => {
939
+ try {
940
+ const wsUrl = this.currentUserId ? `${this.url}?userId=${this.currentUserId}` : this.url;
941
+ this.ws = new WebSocket(wsUrl);
942
+ const connectionTimeout = setTimeout(() => {
943
+ if (this.connectionState === "connecting" /* CONNECTING */) {
944
+ this.ws?.close();
945
+ const error = new NetworkError("Connection timeout");
946
+ this.handleError(error);
947
+ reject(error);
948
+ }
949
+ }, CONNECTION_TIMEOUT);
950
+ this.ws.onopen = () => {
951
+ clearTimeout(connectionTimeout);
952
+ this.reconnectAttempts = 0;
953
+ this.updateState("connected" /* CONNECTED */);
954
+ logger.info("WebSocket connected");
955
+ this.startHeartbeat();
956
+ resolve2();
957
+ };
958
+ this.ws.onmessage = (event) => {
959
+ try {
960
+ const message = JSON.parse(event.data);
961
+ if (message.type === "pong") {
962
+ logger.debug("Received pong");
963
+ return;
964
+ }
965
+ if (this.messageHandler) {
966
+ this.messageHandler(message);
967
+ }
968
+ } catch (error) {
969
+ const parseError = new TransportError(
970
+ "Failed to parse message",
971
+ false,
972
+ { error: error instanceof Error ? error.message : String(error) }
973
+ );
974
+ logger.error("Message parse error", parseError);
975
+ this.handleError(parseError);
976
+ }
977
+ };
978
+ this.ws.onerror = (event) => {
979
+ clearTimeout(connectionTimeout);
980
+ const error = new NetworkError("WebSocket error", {
981
+ event: event.type
982
+ });
983
+ logger.error("WebSocket error", error);
984
+ this.handleError(error);
985
+ reject(error);
986
+ };
987
+ this.ws.onclose = (event) => {
988
+ clearTimeout(connectionTimeout);
989
+ this.stopHeartbeat();
990
+ logger.info("WebSocket closed", {
991
+ code: event.code,
992
+ reason: event.reason,
993
+ wasClean: event.wasClean
994
+ });
995
+ if (this.connectionState !== "disconnected" /* DISCONNECTED */) {
996
+ this.updateState("disconnected" /* DISCONNECTED */);
997
+ if (this.shouldReconnect && this.reconnectAttempts < RECONNECT_MAX_ATTEMPTS) {
998
+ this.scheduleReconnect();
999
+ } else if (this.reconnectAttempts >= RECONNECT_MAX_ATTEMPTS) {
1000
+ this.updateState("failed" /* FAILED */);
1001
+ const error = new NetworkError("Max reconnection attempts exceeded");
1002
+ this.handleError(error);
1003
+ }
1004
+ }
1005
+ };
1006
+ } catch (error) {
1007
+ const connectError = new NetworkError(
1008
+ "Failed to create WebSocket connection",
1009
+ { error: error instanceof Error ? error.message : String(error) }
1010
+ );
1011
+ logger.error("Connection error", connectError);
1012
+ this.handleError(connectError);
1013
+ reject(connectError);
1014
+ }
1015
+ });
1016
+ }
1017
+ async disconnect() {
1018
+ logger.info("Disconnecting WebSocket");
1019
+ this.shouldReconnect = false;
1020
+ this.clearReconnectTimer();
1021
+ this.stopHeartbeat();
1022
+ if (this.ws) {
1023
+ this.ws.close(1e3, "Client disconnect");
1024
+ this.ws = null;
1025
+ }
1026
+ this.updateState("disconnected" /* DISCONNECTED */);
1027
+ }
1028
+ async reconnect() {
1029
+ logger.info("Manual reconnect requested");
1030
+ this.reconnectAttempts = 0;
1031
+ this.shouldReconnect = true;
1032
+ await this.disconnect();
1033
+ if (this.currentUserId) {
1034
+ await this.doConnect();
1035
+ } else {
1036
+ throw new TransportError("Cannot reconnect: no user ID set");
1037
+ }
1038
+ }
1039
+ scheduleReconnect() {
1040
+ this.clearReconnectTimer();
1041
+ this.reconnectAttempts++;
1042
+ const delay = Math.min(
1043
+ RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1),
1044
+ RECONNECT_MAX_DELAY
1045
+ );
1046
+ const jitter = Math.random() * 1e3;
1047
+ const totalDelay = delay + jitter;
1048
+ logger.info("Scheduling reconnect", {
1049
+ attempt: this.reconnectAttempts,
1050
+ delay: totalDelay
1051
+ });
1052
+ this.updateState("reconnecting" /* RECONNECTING */);
1053
+ this.reconnectTimer = setTimeout(async () => {
1054
+ try {
1055
+ await this.doConnect();
1056
+ } catch (error) {
1057
+ logger.error("Reconnect failed", error);
1058
+ }
1059
+ }, totalDelay);
1060
+ }
1061
+ clearReconnectTimer() {
1062
+ if (this.reconnectTimer) {
1063
+ clearTimeout(this.reconnectTimer);
1064
+ this.reconnectTimer = null;
1065
+ }
1066
+ }
1067
+ startHeartbeat() {
1068
+ this.stopHeartbeat();
1069
+ this.heartbeatTimer = setInterval(() => {
1070
+ if (this.isConnected()) {
1071
+ try {
1072
+ this.ws?.send(JSON.stringify({ type: "ping" }));
1073
+ logger.debug("Sent ping");
1074
+ } catch (error) {
1075
+ logger.error("Failed to send heartbeat", error);
1076
+ }
1077
+ }
1078
+ }, HEARTBEAT_INTERVAL);
1079
+ }
1080
+ stopHeartbeat() {
1081
+ if (this.heartbeatTimer) {
1082
+ clearInterval(this.heartbeatTimer);
1083
+ this.heartbeatTimer = null;
1084
+ }
1085
+ }
1086
+ async send(message) {
1087
+ if (!this.isConnected() || !this.ws) {
1088
+ throw new NetworkError("WebSocket not connected");
1089
+ }
1090
+ try {
1091
+ this.ws.send(JSON.stringify(message));
1092
+ logger.debug("Message sent", { messageId: message.id });
1093
+ } catch (error) {
1094
+ const sendError = new NetworkError(
1095
+ "Failed to send message",
1096
+ { error: error instanceof Error ? error.message : String(error) }
1097
+ );
1098
+ logger.error("Send error", sendError);
1099
+ throw sendError;
1100
+ }
1101
+ }
1102
+ onMessage(handler) {
1103
+ this.messageHandler = handler;
1104
+ }
1105
+ onConnectionStateChange(handler) {
1106
+ this.stateHandler = handler;
1107
+ }
1108
+ onError(handler) {
1109
+ this.errorHandler = handler;
1110
+ }
1111
+ getConnectionState() {
1112
+ return this.connectionState;
1113
+ }
1114
+ isConnected() {
1115
+ return this.connectionState === "connected" /* CONNECTED */ && this.ws?.readyState === WebSocket.OPEN;
1116
+ }
1117
+ updateState(newState) {
1118
+ if (this.connectionState !== newState) {
1119
+ const oldState = this.connectionState;
1120
+ this.connectionState = newState;
1121
+ logger.info("Connection state changed", {
1122
+ from: oldState,
1123
+ to: newState
1124
+ });
1125
+ if (this.stateHandler) {
1126
+ this.stateHandler(newState);
1127
+ }
1128
+ }
1129
+ }
1130
+ handleError(error) {
1131
+ if (this.errorHandler) {
1132
+ this.errorHandler(error);
1133
+ }
1134
+ }
1135
+ };
1136
+
1137
+ // src/models/mediaTypes.ts
1138
+ var MediaType = /* @__PURE__ */ ((MediaType2) => {
1139
+ MediaType2["IMAGE"] = "image";
1140
+ MediaType2["AUDIO"] = "audio";
1141
+ MediaType2["VIDEO"] = "video";
1142
+ MediaType2["DOCUMENT"] = "document";
1143
+ return MediaType2;
1144
+ })(MediaType || {});
1145
+ var SUPPORTED_MIME_TYPES = {
1146
+ image: ["image/jpeg", "image/png", "image/gif", "image/webp"],
1147
+ audio: ["audio/mpeg", "audio/mp4", "audio/ogg", "audio/wav", "audio/webm"],
1148
+ video: ["video/mp4", "video/webm", "video/ogg"],
1149
+ document: [
1150
+ "application/pdf",
1151
+ "application/msword",
1152
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1153
+ "application/vnd.ms-excel",
1154
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1155
+ "text/plain"
1156
+ ]
1157
+ };
1158
+ var FILE_SIZE_LIMITS = {
1159
+ image: 10 * 1024 * 1024,
1160
+ // 10 MB
1161
+ audio: 16 * 1024 * 1024,
1162
+ // 16 MB
1163
+ video: 100 * 1024 * 1024,
1164
+ // 100 MB
1165
+ document: 100 * 1024 * 1024
1166
+ // 100 MB
1167
+ };
1168
+
1169
+ // src/utils/mediaUtils.ts
1170
+ async function encodeFileToBase64(file) {
1171
+ return new Promise((resolve2, reject) => {
1172
+ const reader = new FileReader();
1173
+ reader.onload = () => {
1174
+ const result = reader.result;
1175
+ if (!result) {
1176
+ reject(new Error("Failed to read file: result is null"));
1177
+ return;
1178
+ }
1179
+ const base64 = result.split(",")[1];
1180
+ if (!base64) {
1181
+ reject(new Error("Failed to extract base64 data"));
1182
+ return;
1183
+ }
1184
+ resolve2(base64);
1185
+ };
1186
+ reader.onerror = () => reject(new Error("Failed to read file"));
1187
+ reader.readAsDataURL(file);
1188
+ });
1189
+ }
1190
+ function decodeBase64ToBlob(base64, mimeType) {
1191
+ const byteCharacters = atob(base64);
1192
+ const byteNumbers = new Array(byteCharacters.length);
1193
+ for (let i = 0; i < byteCharacters.length; i++) {
1194
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
1195
+ }
1196
+ const byteArray = new Uint8Array(byteNumbers);
1197
+ return new Blob([byteArray], { type: mimeType });
1198
+ }
1199
+ function getMediaType(mimeType) {
1200
+ if (SUPPORTED_MIME_TYPES.image.includes(mimeType)) {
1201
+ return "image" /* IMAGE */;
1202
+ }
1203
+ if (SUPPORTED_MIME_TYPES.audio.includes(mimeType)) {
1204
+ return "audio" /* AUDIO */;
1205
+ }
1206
+ if (SUPPORTED_MIME_TYPES.video.includes(mimeType)) {
1207
+ return "video" /* VIDEO */;
1208
+ }
1209
+ if (SUPPORTED_MIME_TYPES.document.includes(mimeType)) {
1210
+ return "document" /* DOCUMENT */;
1211
+ }
1212
+ throw new ValidationError(`Unsupported MIME type: ${mimeType}`);
1213
+ }
1214
+ function validateMediaFile(file, filename) {
1215
+ const mimeType = file.type;
1216
+ const mediaType = getMediaType(mimeType);
1217
+ const maxSize = FILE_SIZE_LIMITS[mediaType];
1218
+ if (file.size > maxSize) {
1219
+ throw new ValidationError(
1220
+ `File size exceeds limit. Max size for ${mediaType}: ${maxSize / 1024 / 1024}MB`
1221
+ );
1222
+ }
1223
+ if (filename && filename.length > 255) {
1224
+ throw new ValidationError("Filename too long (max 255 characters)");
1225
+ }
1226
+ }
1227
+ async function createMediaMetadata(file, filename) {
1228
+ const actualFilename = filename || (file instanceof File ? file.name : "file");
1229
+ const metadata = {
1230
+ filename: actualFilename,
1231
+ mimeType: file.type,
1232
+ size: file.size
1233
+ };
1234
+ if (file.type.startsWith("image/")) {
1235
+ try {
1236
+ const dimensions = await getImageDimensions(file);
1237
+ metadata.width = dimensions.width;
1238
+ metadata.height = dimensions.height;
1239
+ metadata.thumbnail = await generateThumbnail(file);
1240
+ } catch (error) {
1241
+ }
1242
+ }
1243
+ return metadata;
1244
+ }
1245
+ function getImageDimensions(file) {
1246
+ return new Promise((resolve2, reject) => {
1247
+ const img = new Image();
1248
+ const url = URL.createObjectURL(file);
1249
+ img.onload = () => {
1250
+ URL.revokeObjectURL(url);
1251
+ resolve2({ width: img.width, height: img.height });
1252
+ };
1253
+ img.onerror = () => {
1254
+ URL.revokeObjectURL(url);
1255
+ reject(new Error("Failed to load image"));
1256
+ };
1257
+ img.src = url;
1258
+ });
1259
+ }
1260
+ async function generateThumbnail(file) {
1261
+ return new Promise((resolve2, reject) => {
1262
+ const img = new Image();
1263
+ const url = URL.createObjectURL(file);
1264
+ img.onload = () => {
1265
+ URL.revokeObjectURL(url);
1266
+ const maxSize = 200;
1267
+ let width = img.width;
1268
+ let height = img.height;
1269
+ if (width > height) {
1270
+ if (width > maxSize) {
1271
+ height = height * maxSize / width;
1272
+ width = maxSize;
1273
+ }
1274
+ } else {
1275
+ if (height > maxSize) {
1276
+ width = width * maxSize / height;
1277
+ height = maxSize;
1278
+ }
1279
+ }
1280
+ const canvas = document.createElement("canvas");
1281
+ canvas.width = width;
1282
+ canvas.height = height;
1283
+ const ctx = canvas.getContext("2d");
1284
+ if (!ctx) {
1285
+ reject(new Error("Failed to get canvas context"));
1286
+ return;
1287
+ }
1288
+ ctx.drawImage(img, 0, 0, width, height);
1289
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.7);
1290
+ const thumbnail = dataUrl.split(",")[1];
1291
+ if (!thumbnail) {
1292
+ reject(new Error("Failed to generate thumbnail"));
1293
+ return;
1294
+ }
1295
+ resolve2(thumbnail);
1296
+ };
1297
+ img.onerror = () => {
1298
+ URL.revokeObjectURL(url);
1299
+ reject(new Error("Failed to load image for thumbnail"));
1300
+ };
1301
+ img.src = url;
1302
+ });
1303
+ }
1304
+ async function createMediaAttachment(file, filename) {
1305
+ validateMediaFile(file, filename);
1306
+ const mediaType = getMediaType(file.type);
1307
+ const data = await encodeFileToBase64(file);
1308
+ const metadata = await createMediaMetadata(file, filename);
1309
+ return {
1310
+ type: mediaType,
1311
+ data,
1312
+ metadata
1313
+ };
1314
+ }
1315
+ function formatFileSize(bytes) {
1316
+ if (bytes < 1024) return `${bytes} B`;
1317
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1318
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
1319
+ }
1320
+
1321
+ // src/storage/localStorage.ts
1322
+ import { promises as fs } from "fs";
1323
+ import * as path from "path";
1324
+ var LocalStorageProvider = class {
1325
+ name = "local";
1326
+ storageDir;
1327
+ constructor(storageDir = "./storage") {
1328
+ this.storageDir = path.resolve(storageDir);
1329
+ fs.mkdir(this.storageDir, { recursive: true }).catch(() => {
1330
+ });
1331
+ }
1332
+ async upload(data, filename, mimeType) {
1333
+ const buffer = typeof data === "string" ? Buffer.from(data, "base64") : data;
1334
+ const filePath = path.join(this.storageDir, filename);
1335
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
1336
+ await fs.writeFile(filePath, buffer);
1337
+ return {
1338
+ storageKey: filename,
1339
+ url: `file://${filePath}`
1340
+ };
1341
+ }
1342
+ async download(storageKey) {
1343
+ const filePath = path.join(this.storageDir, storageKey);
1344
+ const buffer = await fs.readFile(filePath);
1345
+ return buffer.toString("base64");
1346
+ }
1347
+ async delete(storageKey) {
1348
+ const filePath = path.join(this.storageDir, storageKey);
1349
+ try {
1350
+ await fs.unlink(filePath);
1351
+ } catch (error) {
1352
+ if (error.code !== "ENOENT") {
1353
+ throw error;
1354
+ }
1355
+ }
1356
+ }
1357
+ };
1358
+
1359
+ // src/storage/s3Storage.ts
1360
+ import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
1361
+ var S3StorageProvider = class {
1362
+ name = "s3";
1363
+ client;
1364
+ bucket;
1365
+ constructor(config) {
1366
+ const s3Config = {
1367
+ region: config.region,
1368
+ forcePathStyle: config.forcePathStyle
1369
+ };
1370
+ if (config.credentials) {
1371
+ s3Config.credentials = config.credentials;
1372
+ }
1373
+ if (config.endpoint) {
1374
+ s3Config.endpoint = config.endpoint;
1375
+ }
1376
+ this.client = new S3Client(s3Config);
1377
+ this.bucket = config.bucket;
1378
+ }
1379
+ async upload(data, filename, mimeType) {
1380
+ const body = typeof data === "string" ? Buffer.from(data, "base64") : data;
1381
+ await this.client.send(
1382
+ new PutObjectCommand({
1383
+ Bucket: this.bucket,
1384
+ Key: filename,
1385
+ Body: body,
1386
+ ContentType: mimeType
1387
+ })
1388
+ );
1389
+ return {
1390
+ storageKey: filename,
1391
+ url: `https://${this.bucket}.s3.amazonaws.com/${filename}`
1392
+ };
1393
+ }
1394
+ async download(storageKey) {
1395
+ const response = await this.client.send(
1396
+ new GetObjectCommand({
1397
+ Bucket: this.bucket,
1398
+ Key: storageKey
1399
+ })
1400
+ );
1401
+ if (!response.Body) {
1402
+ throw new Error("S3 download failed: empty body");
1403
+ }
1404
+ const bytes = await response.Body.transformToByteArray();
1405
+ return Buffer.from(bytes).toString("base64");
1406
+ }
1407
+ async delete(storageKey) {
1408
+ await this.client.send(
1409
+ new DeleteObjectCommand({
1410
+ Bucket: this.bucket,
1411
+ Key: storageKey
1412
+ })
1413
+ );
288
1414
  }
289
1415
  };
290
1416
 
291
1417
  // src/index.ts
292
- var ChatSDK = class {
1418
+ var ChatSDK = class extends EventEmitter {
293
1419
  config;
294
1420
  currentUser = null;
1421
+ messageQueue;
295
1422
  constructor(config) {
1423
+ super();
296
1424
  this.config = config;
1425
+ this.messageQueue = new MessageQueue();
1426
+ if (config.logLevel !== void 0) {
1427
+ logger.setLevel(config.logLevel);
1428
+ }
1429
+ if (this.config.transport) {
1430
+ this.setupTransportHandlers();
1431
+ }
1432
+ logger.info("ChatSDK initialized");
1433
+ }
1434
+ setupTransportHandlers() {
1435
+ if (!this.config.transport) return;
1436
+ this.config.transport.onMessage((message) => {
1437
+ logger.debug("Message received via transport", { messageId: message.id });
1438
+ this.emit(EVENTS.MESSAGE_RECEIVED, message);
1439
+ this.config.messageStore.create(message).catch((error) => {
1440
+ logger.error("Failed to store received message", error);
1441
+ });
1442
+ });
1443
+ if (this.config.transport.onConnectionStateChange) {
1444
+ this.config.transport.onConnectionStateChange((state) => {
1445
+ logger.info("Connection state changed", { state });
1446
+ this.emit(EVENTS.CONNECTION_STATE_CHANGED, state);
1447
+ if (state === "connected" /* CONNECTED */) {
1448
+ this.processMessageQueue();
1449
+ }
1450
+ });
1451
+ }
1452
+ if (this.config.transport.onError) {
1453
+ this.config.transport.onError((error) => {
1454
+ logger.error("Transport error", error);
1455
+ this.emit(EVENTS.ERROR, error);
1456
+ });
1457
+ }
1458
+ }
1459
+ async processMessageQueue() {
1460
+ const pending = this.messageQueue.getPendingMessages();
1461
+ const retryable = this.messageQueue.getRetryableMessages();
1462
+ const toSend = [...pending, ...retryable];
1463
+ logger.info("Processing message queue", { count: toSend.length });
1464
+ for (const queued of toSend) {
1465
+ try {
1466
+ await this.config.transport.send(queued.message);
1467
+ this.messageQueue.markSent(queued.message.id);
1468
+ this.emit(EVENTS.MESSAGE_SENT, queued.message);
1469
+ } catch (error) {
1470
+ this.messageQueue.markFailed(
1471
+ queued.message.id,
1472
+ error instanceof Error ? error : new Error(String(error))
1473
+ );
1474
+ this.emit(EVENTS.MESSAGE_FAILED, queued.message, error);
1475
+ }
1476
+ }
297
1477
  }
298
1478
  /**
299
1479
  * Create a new user with generated identity keys
300
1480
  */
301
1481
  async createUser(username) {
1482
+ validateUsername(username);
302
1483
  const keyPair = generateIdentityKeyPair();
303
1484
  const user = {
304
1485
  id: generateUUID(),
@@ -307,23 +1488,60 @@ var ChatSDK = class {
307
1488
  publicKey: keyPair.publicKey,
308
1489
  privateKey: keyPair.privateKey
309
1490
  };
310
- await this.config.userStore.create(user);
311
- return user;
1491
+ try {
1492
+ await this.config.userStore.create(user);
1493
+ logger.info("User created", { userId: user.id, username: user.username });
1494
+ this.emit(EVENTS.USER_CREATED, user);
1495
+ return user;
1496
+ } catch (error) {
1497
+ const storageError = new StorageError(
1498
+ "Failed to create user",
1499
+ true,
1500
+ { username, error: error instanceof Error ? error.message : String(error) }
1501
+ );
1502
+ logger.error("User creation failed", storageError);
1503
+ this.emit(EVENTS.ERROR, storageError);
1504
+ throw storageError;
1505
+ }
312
1506
  }
313
1507
  /**
314
1508
  * Import an existing user from stored data
315
1509
  */
316
1510
  async importUser(userData) {
317
- await this.config.userStore.save(userData);
318
- return userData;
1511
+ try {
1512
+ await this.config.userStore.save(userData);
1513
+ logger.info("User imported", { userId: userData.id });
1514
+ return userData;
1515
+ } catch (error) {
1516
+ const storageError = new StorageError(
1517
+ "Failed to import user",
1518
+ true,
1519
+ { userId: userData.id, error: error instanceof Error ? error.message : String(error) }
1520
+ );
1521
+ logger.error("User import failed", storageError);
1522
+ this.emit(EVENTS.ERROR, storageError);
1523
+ throw storageError;
1524
+ }
319
1525
  }
320
1526
  /**
321
1527
  * Set the current active user
322
1528
  */
323
- setCurrentUser(user) {
1529
+ async setCurrentUser(user) {
324
1530
  this.currentUser = user;
1531
+ logger.info("Current user set", { userId: user.id, username: user.username });
325
1532
  if (this.config.transport) {
326
- this.config.transport.connect(user.id);
1533
+ try {
1534
+ await this.config.transport.connect(user.id);
1535
+ } catch (error) {
1536
+ const transportError = new TransportError(
1537
+ "Failed to connect transport",
1538
+ true,
1539
+ { userId: user.id, error: error instanceof Error ? error.message : String(error) }
1540
+ );
1541
+ logger.error("Transport connection failed", transportError);
1542
+ this.emit(EVENTS.ERROR, transportError);
1543
+ throw transportError;
1544
+ }
327
1545
  }
328
1546
  }
329
1547
  /**
@@ -338,106 +1556,441 @@ var ChatSDK = class {
338
1556
  async startSession(userA, userB) {
339
1557
  const ids = [userA.id, userB.id].sort();
340
1558
  const sessionId = `${ids[0]}-${ids[1]}`;
341
- const session = new ChatSession(sessionId, userA, userB);
342
- await session.initialize();
343
- return session;
1559
+ try {
1560
+ const session = new ChatSession(sessionId, userA, userB, this.config.storageProvider);
1561
+ await session.initialize();
1562
+ logger.info("Chat session created", { sessionId, users: [userA.id, userB.id] });
1563
+ this.emit(EVENTS.SESSION_CREATED, session);
1564
+ return session;
1565
+ } catch (error) {
1566
+ const sessionError = new SessionError(
1567
+ "Failed to create chat session",
1568
+ { sessionId, error: error instanceof Error ? error.message : String(error) }
1569
+ );
1570
+ logger.error("Session creation failed", sessionError);
1571
+ this.emit(EVENTS.ERROR, sessionError);
1572
+ throw sessionError;
1573
+ }
344
1574
  }
345
1575
  /**
346
1576
  * Create a new group with members
347
1577
  */
348
1578
  async createGroup(name, members) {
349
- if (members.length < 2) {
350
- throw new Error("Group must have at least 2 members");
351
- }
1579
+ validateGroupName(name);
1580
+ validateGroupMembers(members.length);
352
1581
  const group = {
353
1582
  id: generateUUID(),
354
1583
  name,
355
1584
  members,
356
1585
  createdAt: Date.now()
357
1586
  };
358
- await this.config.groupStore.create(group);
359
- const session = new GroupSession(group);
360
- await session.initialize();
361
- return session;
1587
+ try {
1588
+ await this.config.groupStore.create(group);
1589
+ const session = new GroupSession(group, this.config.storageProvider);
1590
+ await session.initialize();
1591
+ logger.info("Group created", { groupId: group.id, name: group.name, memberCount: members.length });
1592
+ this.emit(EVENTS.GROUP_CREATED, session);
1593
+ return session;
1594
+ } catch (error) {
1595
+ const storageError = new StorageError(
1596
+ "Failed to create group",
1597
+ true,
1598
+ { groupName: name, error: error instanceof Error ? error.message : String(error) }
1599
+ );
1600
+ logger.error("Group creation failed", storageError);
1601
+ this.emit(EVENTS.ERROR, storageError);
1602
+ throw storageError;
1603
+ }
362
1604
  }
363
1605
  /**
364
1606
  * Load an existing group by ID
365
1607
  */
366
1608
  async loadGroup(id) {
367
- const group = await this.config.groupStore.findById(id);
368
- if (!group) {
369
- throw new Error(`Group not found: ${id}`);
1609
+ try {
1610
+ const group = await this.config.groupStore.findById(id);
1611
+ if (!group) {
1612
+ throw new SessionError(`Group not found: ${id}`, { groupId: id });
1613
+ }
1614
+ const session = new GroupSession(group, this.config.storageProvider);
1615
+ await session.initialize();
1616
+ logger.debug("Group loaded", { groupId: id });
1617
+ return session;
1618
+ } catch (error) {
1619
+ if (error instanceof SessionError) {
1620
+ this.emit(EVENTS.ERROR, error);
1621
+ throw error;
1622
+ }
1623
+ const storageError = new StorageError(
1624
+ "Failed to load group",
1625
+ true,
1626
+ { groupId: id, error: error instanceof Error ? error.message : String(error) }
1627
+ );
1628
+ logger.error("Group load failed", storageError);
1629
+ this.emit(EVENTS.ERROR, storageError);
1630
+ throw storageError;
370
1631
  }
371
- const session = new GroupSession(group);
372
- await session.initialize();
373
- return session;
374
1632
  }
375
1633
  /**
376
1634
  * Send a message in a chat session (1:1 or group)
377
1635
  */
378
1636
  async sendMessage(session, plaintext) {
379
1637
  if (!this.currentUser) {
380
- throw new Error("No current user set. Call setCurrentUser() first.");
1638
+ throw new SessionError("No current user set. Call setCurrentUser() first.");
381
1639
  }
1640
+ validateMessage(plaintext);
382
1641
  let message;
383
- if (session instanceof ChatSession) {
384
- message = await session.encrypt(plaintext, this.currentUser.id);
385
- } else {
386
- message = await session.encrypt(plaintext, this.currentUser.id);
1642
+ try {
1643
+ if (session instanceof ChatSession) {
1644
+ message = await session.encrypt(plaintext, this.currentUser.id);
1645
+ } else {
1646
+ message = await session.encrypt(plaintext, this.currentUser.id);
1647
+ }
1648
+ await this.config.messageStore.create(message);
1649
+ logger.debug("Message stored", { messageId: message.id });
1650
+ if (this.config.transport) {
1651
+ if (this.config.transport.isConnected()) {
1652
+ try {
1653
+ await this.config.transport.send(message);
1654
+ logger.debug("Message sent via transport", { messageId: message.id });
1655
+ this.emit(EVENTS.MESSAGE_SENT, message);
1656
+ } catch (error) {
1657
+ this.messageQueue.enqueue(message);
1658
+ this.messageQueue.markFailed(
1659
+ message.id,
1660
+ error instanceof Error ? error : new Error(String(error))
1661
+ );
1662
+ logger.warn("Message send failed, queued for retry", { messageId: message.id });
1663
+ this.emit(EVENTS.MESSAGE_FAILED, message, error);
1664
+ }
1665
+ } else {
1666
+ this.messageQueue.enqueue(message);
1667
+ logger.info("Message queued (offline)", { messageId: message.id });
1668
+ }
1669
+ }
1670
+ return message;
1671
+ } catch (error) {
1672
+ const sendError = error instanceof Error ? error : new Error(String(error));
1673
+ logger.error("Failed to send message", sendError);
1674
+ this.emit(EVENTS.ERROR, sendError);
1675
+ throw sendError;
387
1676
  }
388
- await this.config.messageStore.create(message);
389
- if (this.config.transport) {
390
- await this.config.transport.send(message);
1677
+ }
1678
+ /**
1679
+ * Send a media message in a chat session (1:1 or group)
1680
+ */
1681
+ async sendMediaMessage(session, caption, media) {
1682
+ if (!this.currentUser) {
1683
+ throw new SessionError("No current user set. Call setCurrentUser() first.");
1684
+ }
1685
+ let message;
1686
+ try {
1687
+ if (session instanceof ChatSession) {
1688
+ message = await session.encryptMedia(caption, media, this.currentUser.id);
1689
+ } else {
1690
+ message = await session.encryptMedia(caption, media, this.currentUser.id);
1691
+ }
1692
+ await this.config.messageStore.create(message);
1693
+ logger.debug("Media message stored", { messageId: message.id, mediaType: media.type });
1694
+ if (this.config.transport) {
1695
+ if (this.config.transport.isConnected()) {
1696
+ try {
1697
+ await this.config.transport.send(message);
1698
+ logger.debug("Media message sent via transport", { messageId: message.id });
1699
+ this.emit(EVENTS.MESSAGE_SENT, message);
1700
+ } catch (error) {
1701
+ this.messageQueue.enqueue(message);
1702
+ this.messageQueue.markFailed(
1703
+ message.id,
1704
+ error instanceof Error ? error : new Error(String(error))
1705
+ );
1706
+ logger.warn("Media message send failed, queued for retry", { messageId: message.id });
1707
+ this.emit(EVENTS.MESSAGE_FAILED, message, error);
1708
+ }
1709
+ } else {
1710
+ this.messageQueue.enqueue(message);
1711
+ logger.info("Media message queued (offline)", { messageId: message.id });
1712
+ }
1713
+ }
1714
+ return message;
1715
+ } catch (error) {
1716
+ const sendError = error instanceof Error ? error : new Error(String(error));
1717
+ logger.error("Failed to send media message", sendError);
1718
+ this.emit(EVENTS.ERROR, sendError);
1719
+ throw sendError;
391
1720
  }
392
- return message;
393
1721
  }
394
1722
  /**
395
1723
  * Decrypt a message
396
1724
  */
397
1725
  async decryptMessage(message, user) {
398
- if (message.groupId) {
399
- const group = await this.config.groupStore.findById(message.groupId);
400
- if (!group) {
401
- throw new Error(`Group not found: ${message.groupId}`);
402
- }
403
- const session = new GroupSession(group);
404
- await session.initialize();
405
- return await session.decrypt(message);
406
- } else {
407
- const otherUserId = message.senderId === user.id ? message.receiverId : message.senderId;
408
- if (!otherUserId) {
409
- throw new Error("Invalid message: missing receiver/sender");
1726
+ try {
1727
+ if (message.groupId) {
1728
+ const group = await this.config.groupStore.findById(message.groupId);
1729
+ if (!group) {
1730
+ throw new SessionError(`Group not found: ${message.groupId}`, { groupId: message.groupId });
1731
+ }
1732
+ const session = new GroupSession(group);
1733
+ await session.initialize();
1734
+ return await session.decrypt(message);
1735
+ } else {
1736
+ const otherUserId = message.senderId === user.id ? message.receiverId : message.senderId;
1737
+ if (!otherUserId) {
1738
+ throw new SessionError("Invalid message: missing receiver/sender");
1739
+ }
1740
+ const otherUser = await this.config.userStore.findById(otherUserId);
1741
+ if (!otherUser) {
1742
+ throw new SessionError(`User not found: ${otherUserId}`, { userId: otherUserId });
1743
+ }
1744
+ const ids = [user.id, otherUser.id].sort();
1745
+ const sessionId = `${ids[0]}-${ids[1]}`;
1746
+ const session = new ChatSession(sessionId, user, otherUser, this.config.storageProvider);
1747
+ await session.initializeForUser(user);
1748
+ return await session.decrypt(message, user);
410
1749
  }
411
- const otherUser = await this.config.userStore.findById(otherUserId);
412
- if (!otherUser) {
413
- throw new Error(`User not found: ${otherUserId}`);
1750
+ } catch (error) {
1751
+ const decryptError = error instanceof Error ? error : new Error(String(error));
1752
+ logger.error("Failed to decrypt message", decryptError);
1753
+ this.emit(EVENTS.ERROR, decryptError);
1754
+ throw decryptError;
1755
+ }
1756
+ }
1757
+ /**
1758
+ * Decrypt a media message
1759
+ */
1760
+ async decryptMediaMessage(message, user) {
1761
+ if (!message.media) {
1762
+ throw new SessionError("Message does not contain media");
1763
+ }
1764
+ try {
1765
+ if (message.groupId) {
1766
+ const group = await this.config.groupStore.findById(message.groupId);
1767
+ if (!group) {
1768
+ throw new SessionError(`Group not found: ${message.groupId}`, { groupId: message.groupId });
1769
+ }
1770
+ const session = new GroupSession(group, this.config.storageProvider);
1771
+ await session.initialize();
1772
+ return await session.decryptMedia(message);
1773
+ } else {
1774
+ const otherUserId = message.senderId === user.id ? message.receiverId : message.senderId;
1775
+ if (!otherUserId) {
1776
+ throw new SessionError("Invalid message: missing receiver/sender");
1777
+ }
1778
+ const otherUser = await this.config.userStore.findById(otherUserId);
1779
+ if (!otherUser) {
1780
+ throw new SessionError(`User not found: ${otherUserId}`, { userId: otherUserId });
1781
+ }
1782
+ const ids = [user.id, otherUser.id].sort();
1783
+ const sessionId = `${ids[0]}-${ids[1]}`;
1784
+ const session = new ChatSession(sessionId, user, otherUser, this.config.storageProvider);
1785
+ await session.initializeForUser(user);
1786
+ return await session.decryptMedia(message, user);
414
1787
  }
415
- const ids = [user.id, otherUser.id].sort();
416
- const sessionId = `${ids[0]}-${ids[1]}`;
417
- const session = new ChatSession(sessionId, user, otherUser);
418
- await session.initializeForUser(user);
419
- return await session.decrypt(message, user);
1788
+ } catch (error) {
1789
+ const decryptError = error instanceof Error ? error : new Error(String(error));
1790
+ logger.error("Failed to decrypt media message", decryptError);
1791
+ this.emit(EVENTS.ERROR, decryptError);
1792
+ throw decryptError;
420
1793
  }
421
1794
  }
422
1795
  /**
423
1796
  * Get messages for a user
424
1797
  */
425
1798
  async getMessagesForUser(userId) {
426
- return await this.config.messageStore.listByUser(userId);
1799
+ try {
1800
+ return await this.config.messageStore.listByUser(userId);
1801
+ } catch (error) {
1802
+ const storageError = new StorageError(
1803
+ "Failed to get messages for user",
1804
+ true,
1805
+ { userId, error: error instanceof Error ? error.message : String(error) }
1806
+ );
1807
+ logger.error("Get messages failed", storageError);
1808
+ this.emit(EVENTS.ERROR, storageError);
1809
+ throw storageError;
1810
+ }
427
1811
  }
428
1812
  /**
429
1813
  * Get messages for a group
430
1814
  */
431
1815
  async getMessagesForGroup(groupId) {
432
- return await this.config.messageStore.listByGroup(groupId);
1816
+ try {
1817
+ return await this.config.messageStore.listByGroup(groupId);
1818
+ } catch (error) {
1819
+ const storageError = new StorageError(
1820
+ "Failed to get messages for group",
1821
+ true,
1822
+ { groupId, error: error instanceof Error ? error.message : String(error) }
1823
+ );
1824
+ logger.error("Get messages failed", storageError);
1825
+ this.emit(EVENTS.ERROR, storageError);
1826
+ throw storageError;
1827
+ }
1828
+ }
1829
+ // ========== Public Accessor Methods ==========
1830
+ /**
1831
+ * Get the transport adapter
1832
+ */
1833
+ getTransport() {
1834
+ return this.config.transport;
1835
+ }
1836
+ /**
1837
+ * Get all users
1838
+ */
1839
+ async listUsers() {
1840
+ try {
1841
+ return await this.config.userStore.list();
1842
+ } catch (error) {
1843
+ const storageError = new StorageError(
1844
+ "Failed to list users",
1845
+ true,
1846
+ { error: error instanceof Error ? error.message : String(error) }
1847
+ );
1848
+ logger.error("List users failed", storageError);
1849
+ this.emit(EVENTS.ERROR, storageError);
1850
+ throw storageError;
1851
+ }
1852
+ }
1853
+ /**
1854
+ * Get user by ID
1855
+ */
1856
+ async getUserById(userId) {
1857
+ try {
1858
+ return await this.config.userStore.findById(userId);
1859
+ } catch (error) {
1860
+ const storageError = new StorageError(
1861
+ "Failed to get user",
1862
+ true,
1863
+ { userId, error: error instanceof Error ? error.message : String(error) }
1864
+ );
1865
+ logger.error("Get user failed", storageError);
1866
+ this.emit(EVENTS.ERROR, storageError);
1867
+ throw storageError;
1868
+ }
1869
+ }
1870
+ /**
1871
+ * Get all groups
1872
+ */
1873
+ async listGroups() {
1874
+ try {
1875
+ return await this.config.groupStore.list();
1876
+ } catch (error) {
1877
+ const storageError = new StorageError(
1878
+ "Failed to list groups",
1879
+ true,
1880
+ { error: error instanceof Error ? error.message : String(error) }
1881
+ );
1882
+ logger.error("List groups failed", storageError);
1883
+ this.emit(EVENTS.ERROR, storageError);
1884
+ throw storageError;
1885
+ }
1886
+ }
1887
+ /**
1888
+ * Get connection state
1889
+ */
1890
+ getConnectionState() {
1891
+ if (!this.config.transport) {
1892
+ return "disconnected" /* DISCONNECTED */;
1893
+ }
1894
+ return this.config.transport.getConnectionState();
1895
+ }
1896
+ /**
1897
+ * Check if connected
1898
+ */
1899
+ isConnected() {
1900
+ if (!this.config.transport) {
1901
+ return false;
1902
+ }
1903
+ return this.config.transport.isConnected();
1904
+ }
1905
+ /**
1906
+ * Disconnect transport
1907
+ */
1908
+ async disconnect() {
1909
+ if (this.config.transport) {
1910
+ await this.config.transport.disconnect();
1911
+ logger.info("Transport disconnected");
1912
+ }
1913
+ }
1914
+ /**
1915
+ * Reconnect transport
1916
+ */
1917
+ async reconnect() {
1918
+ if (this.config.transport) {
1919
+ await this.config.transport.reconnect();
1920
+ logger.info("Transport reconnected");
1921
+ }
1922
+ }
1923
+ /**
1924
+ * Get message queue status
1925
+ */
1926
+ getQueueStatus() {
1927
+ return {
1928
+ size: this.messageQueue.size(),
1929
+ pending: this.messageQueue.getPendingMessages().length,
1930
+ retryable: this.messageQueue.getRetryableMessages().length
1931
+ };
433
1932
  }
434
1933
  };
435
1934
  export {
1935
+ ALGORITHM2 as ALGORITHM,
1936
+ AuthError,
1937
+ CONNECTION_TIMEOUT,
436
1938
  ChatSDK,
437
1939
  ChatSession,
1940
+ ConfigError,
1941
+ ConnectionState,
1942
+ EVENTS,
1943
+ EncryptionError,
1944
+ FILE_SIZE_LIMITS,
1945
+ GROUP_MAX_MEMBERS,
1946
+ GROUP_MIN_MEMBERS,
1947
+ GROUP_NAME_MAX_LENGTH,
438
1948
  GroupSession,
1949
+ HEARTBEAT_INTERVAL,
1950
+ IV_LENGTH2 as IV_LENGTH,
439
1951
  InMemoryGroupStore,
440
1952
  InMemoryMessageStore,
441
1953
  InMemoryTransport,
442
- InMemoryUserStore
1954
+ InMemoryUserStore,
1955
+ KEY_LENGTH3 as KEY_LENGTH,
1956
+ LocalStorageProvider,
1957
+ LogLevel,
1958
+ Logger,
1959
+ MAX_QUEUE_SIZE,
1960
+ MESSAGE_MAX_LENGTH,
1961
+ MESSAGE_RETRY_ATTEMPTS,
1962
+ MESSAGE_RETRY_DELAY,
1963
+ MediaType,
1964
+ MessageStatus,
1965
+ NetworkError,
1966
+ PBKDF2_ITERATIONS3 as PBKDF2_ITERATIONS,
1967
+ RECONNECT_BASE_DELAY,
1968
+ RECONNECT_MAX_ATTEMPTS,
1969
+ RECONNECT_MAX_DELAY,
1970
+ S3StorageProvider,
1971
+ SALT_LENGTH2 as SALT_LENGTH,
1972
+ SDKError,
1973
+ SUPPORTED_CURVE2 as SUPPORTED_CURVE,
1974
+ SUPPORTED_MIME_TYPES,
1975
+ SessionError,
1976
+ StorageError,
1977
+ TAG_LENGTH2 as TAG_LENGTH,
1978
+ TransportError,
1979
+ USERNAME_MAX_LENGTH,
1980
+ USERNAME_MIN_LENGTH,
1981
+ ValidationError,
1982
+ WebSocketClient,
1983
+ createMediaAttachment,
1984
+ createMediaMetadata,
1985
+ decodeBase64ToBlob,
1986
+ encodeFileToBase64,
1987
+ formatFileSize,
1988
+ getMediaType,
1989
+ logger,
1990
+ validateGroupMembers,
1991
+ validateGroupName,
1992
+ validateMediaFile,
1993
+ validateMessage,
1994
+ validateUserId,
1995
+ validateUsername
443
1996
  };