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