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.
- package/CONTRIBUTING.md +658 -0
- package/IMPROVEMENTS.md +402 -0
- package/README.md +1538 -164
- package/dist/index.d.ts +430 -9
- package/dist/index.js +1420 -63
- package/examples/01-basic-chat/README.md +61 -0
- package/examples/01-basic-chat/index.js +58 -0
- package/examples/01-basic-chat/package.json +13 -0
- package/examples/02-group-chat/README.md +78 -0
- package/examples/02-group-chat/index.js +76 -0
- package/examples/02-group-chat/package.json +13 -0
- package/examples/03-offline-messaging/README.md +73 -0
- package/examples/03-offline-messaging/index.js +80 -0
- package/examples/03-offline-messaging/package.json +13 -0
- package/examples/04-live-chat/README.md +80 -0
- package/examples/04-live-chat/index.js +114 -0
- package/examples/04-live-chat/package.json +13 -0
- package/examples/05-hybrid-messaging/README.md +71 -0
- package/examples/05-hybrid-messaging/index.js +106 -0
- package/examples/05-hybrid-messaging/package.json +13 -0
- package/examples/06-postgresql-integration/README.md +101 -0
- package/examples/06-postgresql-integration/adapters/groupStore.js +73 -0
- package/examples/06-postgresql-integration/adapters/messageStore.js +47 -0
- package/examples/06-postgresql-integration/adapters/userStore.js +40 -0
- package/examples/06-postgresql-integration/index.js +92 -0
- package/examples/06-postgresql-integration/package.json +14 -0
- package/examples/06-postgresql-integration/schema.sql +58 -0
- package/examples/08-customer-support/README.md +70 -0
- package/examples/08-customer-support/index.js +104 -0
- package/examples/08-customer-support/package.json +13 -0
- package/examples/README.md +105 -0
- package/jest.config.cjs +28 -0
- package/package.json +12 -8
- package/src/chat/ChatSession.ts +81 -0
- package/src/chat/GroupSession.ts +79 -0
- package/src/constants.ts +61 -0
- package/src/crypto/e2e.ts +0 -20
- package/src/index.ts +525 -63
- package/src/models/mediaTypes.ts +58 -0
- package/src/models/message.ts +4 -1
- package/src/transport/adapters.ts +51 -1
- package/src/transport/memoryTransport.ts +75 -13
- package/src/transport/websocketClient.ts +269 -21
- package/src/transport/websocketServer.ts +26 -26
- package/src/utils/errors.ts +97 -0
- package/src/utils/logger.ts +96 -0
- package/src/utils/mediaUtils.ts +235 -0
- package/src/utils/messageQueue.ts +162 -0
- package/src/utils/validation.ts +99 -0
- package/test/crypto.test.ts +122 -35
- package/test/sdk.test.ts +276 -0
- package/test/validation.test.ts +64 -0
- package/tsconfig.json +11 -10
- package/tsconfig.test.json +11 -0
- package/src/ChatManager.ts +0 -103
- 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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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 (
|
|
282
|
-
|
|
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.
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
1444
|
+
throw new SessionError("No current user set. Call setCurrentUser() first.");
|
|
381
1445
|
}
|
|
1446
|
+
validateMessage(plaintext);
|
|
382
1447
|
let message;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|