chatly-sdk 0.0.5 → 0.0.6

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