chatly-sdk 0.0.8 → 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/IMPROVEMENTS.md +1 -1
- package/README.md +53 -13
- package/dist/index.d.ts +74 -4
- package/dist/index.js +292 -96
- package/package.json +2 -1
- package/src/chat/ChatSession.ts +88 -12
- package/src/chat/GroupSession.ts +35 -7
- package/src/crypto/e2e.ts +9 -0
- package/src/crypto/utils.ts +3 -1
- package/src/index.ts +11 -6
- package/src/models/mediaTypes.ts +5 -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/test/crypto.test.ts +17 -17
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ function bufferToBase64(buffer) {
|
|
|
9
9
|
return buffer.toString("base64");
|
|
10
10
|
}
|
|
11
11
|
function base64ToBuffer(data) {
|
|
12
|
+
if (Buffer.isBuffer(data)) return data;
|
|
13
|
+
if (!data) return Buffer.alloc(0);
|
|
12
14
|
return Buffer.from(data, "base64");
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -45,6 +47,11 @@ function deriveSharedSecret(local, remotePublicKey) {
|
|
|
45
47
|
const derivedKey = pbkdf2Sync(sharedSecret, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
46
48
|
return derivedKey;
|
|
47
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
|
+
}
|
|
48
55
|
function encryptMessage(plaintext, secret) {
|
|
49
56
|
const iv = randomBytes2(IV_LENGTH);
|
|
50
57
|
const cipher = createCipheriv(ALGORITHM, secret, iv);
|
|
@@ -95,12 +102,78 @@ function generateUUID() {
|
|
|
95
102
|
});
|
|
96
103
|
}
|
|
97
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
|
+
|
|
98
170
|
// src/chat/ChatSession.ts
|
|
99
171
|
var ChatSession = class {
|
|
100
|
-
constructor(id, userA, userB) {
|
|
172
|
+
constructor(id, userA, userB, storageProvider) {
|
|
101
173
|
this.id = id;
|
|
102
174
|
this.userA = userA;
|
|
103
175
|
this.userB = userB;
|
|
176
|
+
this.storageProvider = storageProvider;
|
|
104
177
|
}
|
|
105
178
|
sharedSecret = null;
|
|
106
179
|
ephemeralKeyPair = null;
|
|
@@ -124,6 +197,12 @@ var ChatSession = class {
|
|
|
124
197
|
publicKey: user.publicKey,
|
|
125
198
|
privateKey: user.privateKey
|
|
126
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
|
+
});
|
|
127
206
|
this.sharedSecret = deriveSharedSecret(localKeyPair, otherUser.publicKey);
|
|
128
207
|
}
|
|
129
208
|
/**
|
|
@@ -158,14 +237,27 @@ var ChatSession = class {
|
|
|
158
237
|
throw new Error("Failed to initialize session");
|
|
159
238
|
}
|
|
160
239
|
const { ciphertext, iv } = encryptMessage(plaintext, this.sharedSecret);
|
|
161
|
-
const { ciphertext: encryptedMediaData } = encryptMessage(
|
|
162
|
-
media.data,
|
|
240
|
+
const { ciphertext: encryptedMediaData, iv: mediaIv } = encryptMessage(
|
|
241
|
+
media.data || "",
|
|
163
242
|
this.sharedSecret
|
|
164
243
|
);
|
|
165
244
|
const encryptedMedia = {
|
|
166
245
|
...media,
|
|
167
|
-
data: encryptedMediaData
|
|
246
|
+
data: encryptedMediaData,
|
|
247
|
+
iv: mediaIv
|
|
168
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
|
+
}
|
|
169
261
|
return {
|
|
170
262
|
id: generateUUID(),
|
|
171
263
|
senderId,
|
|
@@ -187,7 +279,29 @@ var ChatSession = class {
|
|
|
187
279
|
if (!this.sharedSecret) {
|
|
188
280
|
throw new Error("Failed to initialize session");
|
|
189
281
|
}
|
|
190
|
-
|
|
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);
|
|
191
305
|
}
|
|
192
306
|
/**
|
|
193
307
|
* Decrypt a media message in this session
|
|
@@ -203,11 +317,32 @@ var ChatSession = class {
|
|
|
203
317
|
throw new Error("Failed to initialize session");
|
|
204
318
|
}
|
|
205
319
|
const text = decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
message.
|
|
209
|
-
|
|
210
|
-
)
|
|
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
|
+
}
|
|
211
346
|
const decryptedMedia = {
|
|
212
347
|
...message.media,
|
|
213
348
|
data: decryptedMediaData
|
|
@@ -231,8 +366,9 @@ function deriveGroupKey(groupId) {
|
|
|
231
366
|
|
|
232
367
|
// src/chat/GroupSession.ts
|
|
233
368
|
var GroupSession = class {
|
|
234
|
-
constructor(group) {
|
|
369
|
+
constructor(group, storageProvider) {
|
|
235
370
|
this.group = group;
|
|
371
|
+
this.storageProvider = storageProvider;
|
|
236
372
|
}
|
|
237
373
|
groupKey = null;
|
|
238
374
|
/**
|
|
@@ -274,14 +410,27 @@ var GroupSession = class {
|
|
|
274
410
|
throw new Error("Failed to initialize group session");
|
|
275
411
|
}
|
|
276
412
|
const { ciphertext, iv } = encryptMessage(plaintext, this.groupKey);
|
|
277
|
-
const { ciphertext: encryptedMediaData } = encryptMessage(
|
|
278
|
-
media.data,
|
|
413
|
+
const { ciphertext: encryptedMediaData, iv: mediaIv } = encryptMessage(
|
|
414
|
+
media.data || "",
|
|
279
415
|
this.groupKey
|
|
280
416
|
);
|
|
281
417
|
const encryptedMedia = {
|
|
282
418
|
...media,
|
|
283
|
-
data: encryptedMediaData
|
|
419
|
+
data: encryptedMediaData,
|
|
420
|
+
iv: mediaIv
|
|
284
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
|
+
}
|
|
285
434
|
return {
|
|
286
435
|
id: generateUUID(),
|
|
287
436
|
senderId,
|
|
@@ -319,9 +468,17 @@ var GroupSession = class {
|
|
|
319
468
|
throw new Error("Failed to initialize group session");
|
|
320
469
|
}
|
|
321
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
|
+
}
|
|
322
478
|
const decryptedMediaData = decryptMessage(
|
|
323
|
-
|
|
324
|
-
message.iv,
|
|
479
|
+
encryptedMediaData || "",
|
|
480
|
+
message.media.iv || message.iv,
|
|
481
|
+
// Fallback to message IV for backward compatibility
|
|
325
482
|
this.groupKey
|
|
326
483
|
);
|
|
327
484
|
const decryptedMedia = {
|
|
@@ -332,71 +489,6 @@ var GroupSession = class {
|
|
|
332
489
|
}
|
|
333
490
|
};
|
|
334
491
|
|
|
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
492
|
// src/utils/errors.ts
|
|
401
493
|
var SDKError = class extends Error {
|
|
402
494
|
constructor(message, code, retryable = false, details) {
|
|
@@ -719,6 +811,9 @@ var InMemoryMessageStore = class {
|
|
|
719
811
|
this.messages.push(message);
|
|
720
812
|
return message;
|
|
721
813
|
}
|
|
814
|
+
async findById(id) {
|
|
815
|
+
return this.messages.find((msg) => msg.id === id);
|
|
816
|
+
}
|
|
722
817
|
async listByUser(userId) {
|
|
723
818
|
return this.messages.filter(
|
|
724
819
|
(msg) => msg.senderId === userId || msg.receiverId === userId
|
|
@@ -727,6 +822,9 @@ var InMemoryMessageStore = class {
|
|
|
727
822
|
async listByGroup(groupId) {
|
|
728
823
|
return this.messages.filter((msg) => msg.groupId === groupId);
|
|
729
824
|
}
|
|
825
|
+
async delete(id) {
|
|
826
|
+
this.messages = this.messages.filter((msg) => msg.id !== id);
|
|
827
|
+
}
|
|
730
828
|
};
|
|
731
829
|
|
|
732
830
|
// src/stores/memory/groupStore.ts
|
|
@@ -767,7 +865,7 @@ var InMemoryTransport = class {
|
|
|
767
865
|
if (this.stateHandler) {
|
|
768
866
|
this.stateHandler(this.connectionState);
|
|
769
867
|
}
|
|
770
|
-
await new Promise((
|
|
868
|
+
await new Promise((resolve2) => setTimeout(resolve2, 100));
|
|
771
869
|
this.connectionState = "connected" /* CONNECTED */;
|
|
772
870
|
if (this.stateHandler) {
|
|
773
871
|
this.stateHandler(this.connectionState);
|
|
@@ -837,7 +935,7 @@ var WebSocketClient = class {
|
|
|
837
935
|
}
|
|
838
936
|
this.updateState("connecting" /* CONNECTING */);
|
|
839
937
|
logger.info("Connecting to WebSocket", { url: this.url, userId: this.currentUserId });
|
|
840
|
-
return new Promise((
|
|
938
|
+
return new Promise((resolve2, reject) => {
|
|
841
939
|
try {
|
|
842
940
|
const wsUrl = this.currentUserId ? `${this.url}?userId=${this.currentUserId}` : this.url;
|
|
843
941
|
this.ws = new WebSocket(wsUrl);
|
|
@@ -855,7 +953,7 @@ var WebSocketClient = class {
|
|
|
855
953
|
this.updateState("connected" /* CONNECTED */);
|
|
856
954
|
logger.info("WebSocket connected");
|
|
857
955
|
this.startHeartbeat();
|
|
858
|
-
|
|
956
|
+
resolve2();
|
|
859
957
|
};
|
|
860
958
|
this.ws.onmessage = (event) => {
|
|
861
959
|
try {
|
|
@@ -1070,7 +1168,7 @@ var FILE_SIZE_LIMITS = {
|
|
|
1070
1168
|
|
|
1071
1169
|
// src/utils/mediaUtils.ts
|
|
1072
1170
|
async function encodeFileToBase64(file) {
|
|
1073
|
-
return new Promise((
|
|
1171
|
+
return new Promise((resolve2, reject) => {
|
|
1074
1172
|
const reader = new FileReader();
|
|
1075
1173
|
reader.onload = () => {
|
|
1076
1174
|
const result = reader.result;
|
|
@@ -1083,7 +1181,7 @@ async function encodeFileToBase64(file) {
|
|
|
1083
1181
|
reject(new Error("Failed to extract base64 data"));
|
|
1084
1182
|
return;
|
|
1085
1183
|
}
|
|
1086
|
-
|
|
1184
|
+
resolve2(base64);
|
|
1087
1185
|
};
|
|
1088
1186
|
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
1089
1187
|
reader.readAsDataURL(file);
|
|
@@ -1145,12 +1243,12 @@ async function createMediaMetadata(file, filename) {
|
|
|
1145
1243
|
return metadata;
|
|
1146
1244
|
}
|
|
1147
1245
|
function getImageDimensions(file) {
|
|
1148
|
-
return new Promise((
|
|
1246
|
+
return new Promise((resolve2, reject) => {
|
|
1149
1247
|
const img = new Image();
|
|
1150
1248
|
const url = URL.createObjectURL(file);
|
|
1151
1249
|
img.onload = () => {
|
|
1152
1250
|
URL.revokeObjectURL(url);
|
|
1153
|
-
|
|
1251
|
+
resolve2({ width: img.width, height: img.height });
|
|
1154
1252
|
};
|
|
1155
1253
|
img.onerror = () => {
|
|
1156
1254
|
URL.revokeObjectURL(url);
|
|
@@ -1160,7 +1258,7 @@ function getImageDimensions(file) {
|
|
|
1160
1258
|
});
|
|
1161
1259
|
}
|
|
1162
1260
|
async function generateThumbnail(file) {
|
|
1163
|
-
return new Promise((
|
|
1261
|
+
return new Promise((resolve2, reject) => {
|
|
1164
1262
|
const img = new Image();
|
|
1165
1263
|
const url = URL.createObjectURL(file);
|
|
1166
1264
|
img.onload = () => {
|
|
@@ -1194,7 +1292,7 @@ async function generateThumbnail(file) {
|
|
|
1194
1292
|
reject(new Error("Failed to generate thumbnail"));
|
|
1195
1293
|
return;
|
|
1196
1294
|
}
|
|
1197
|
-
|
|
1295
|
+
resolve2(thumbnail);
|
|
1198
1296
|
};
|
|
1199
1297
|
img.onerror = () => {
|
|
1200
1298
|
URL.revokeObjectURL(url);
|
|
@@ -1220,6 +1318,102 @@ function formatFileSize(bytes) {
|
|
|
1220
1318
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
1221
1319
|
}
|
|
1222
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
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1223
1417
|
// src/index.ts
|
|
1224
1418
|
var ChatSDK = class extends EventEmitter {
|
|
1225
1419
|
config;
|
|
@@ -1363,7 +1557,7 @@ var ChatSDK = class extends EventEmitter {
|
|
|
1363
1557
|
const ids = [userA.id, userB.id].sort();
|
|
1364
1558
|
const sessionId = `${ids[0]}-${ids[1]}`;
|
|
1365
1559
|
try {
|
|
1366
|
-
const session = new ChatSession(sessionId, userA, userB);
|
|
1560
|
+
const session = new ChatSession(sessionId, userA, userB, this.config.storageProvider);
|
|
1367
1561
|
await session.initialize();
|
|
1368
1562
|
logger.info("Chat session created", { sessionId, users: [userA.id, userB.id] });
|
|
1369
1563
|
this.emit(EVENTS.SESSION_CREATED, session);
|
|
@@ -1392,7 +1586,7 @@ var ChatSDK = class extends EventEmitter {
|
|
|
1392
1586
|
};
|
|
1393
1587
|
try {
|
|
1394
1588
|
await this.config.groupStore.create(group);
|
|
1395
|
-
const session = new GroupSession(group);
|
|
1589
|
+
const session = new GroupSession(group, this.config.storageProvider);
|
|
1396
1590
|
await session.initialize();
|
|
1397
1591
|
logger.info("Group created", { groupId: group.id, name: group.name, memberCount: members.length });
|
|
1398
1592
|
this.emit(EVENTS.GROUP_CREATED, session);
|
|
@@ -1417,7 +1611,7 @@ var ChatSDK = class extends EventEmitter {
|
|
|
1417
1611
|
if (!group) {
|
|
1418
1612
|
throw new SessionError(`Group not found: ${id}`, { groupId: id });
|
|
1419
1613
|
}
|
|
1420
|
-
const session = new GroupSession(group);
|
|
1614
|
+
const session = new GroupSession(group, this.config.storageProvider);
|
|
1421
1615
|
await session.initialize();
|
|
1422
1616
|
logger.debug("Group loaded", { groupId: id });
|
|
1423
1617
|
return session;
|
|
@@ -1549,7 +1743,7 @@ var ChatSDK = class extends EventEmitter {
|
|
|
1549
1743
|
}
|
|
1550
1744
|
const ids = [user.id, otherUser.id].sort();
|
|
1551
1745
|
const sessionId = `${ids[0]}-${ids[1]}`;
|
|
1552
|
-
const session = new ChatSession(sessionId, user, otherUser);
|
|
1746
|
+
const session = new ChatSession(sessionId, user, otherUser, this.config.storageProvider);
|
|
1553
1747
|
await session.initializeForUser(user);
|
|
1554
1748
|
return await session.decrypt(message, user);
|
|
1555
1749
|
}
|
|
@@ -1573,7 +1767,7 @@ var ChatSDK = class extends EventEmitter {
|
|
|
1573
1767
|
if (!group) {
|
|
1574
1768
|
throw new SessionError(`Group not found: ${message.groupId}`, { groupId: message.groupId });
|
|
1575
1769
|
}
|
|
1576
|
-
const session = new GroupSession(group);
|
|
1770
|
+
const session = new GroupSession(group, this.config.storageProvider);
|
|
1577
1771
|
await session.initialize();
|
|
1578
1772
|
return await session.decryptMedia(message);
|
|
1579
1773
|
} else {
|
|
@@ -1587,7 +1781,7 @@ var ChatSDK = class extends EventEmitter {
|
|
|
1587
1781
|
}
|
|
1588
1782
|
const ids = [user.id, otherUser.id].sort();
|
|
1589
1783
|
const sessionId = `${ids[0]}-${ids[1]}`;
|
|
1590
|
-
const session = new ChatSession(sessionId, user, otherUser);
|
|
1784
|
+
const session = new ChatSession(sessionId, user, otherUser, this.config.storageProvider);
|
|
1591
1785
|
await session.initializeForUser(user);
|
|
1592
1786
|
return await session.decryptMedia(message, user);
|
|
1593
1787
|
}
|
|
@@ -1759,6 +1953,7 @@ export {
|
|
|
1759
1953
|
InMemoryTransport,
|
|
1760
1954
|
InMemoryUserStore,
|
|
1761
1955
|
KEY_LENGTH3 as KEY_LENGTH,
|
|
1956
|
+
LocalStorageProvider,
|
|
1762
1957
|
LogLevel,
|
|
1763
1958
|
Logger,
|
|
1764
1959
|
MAX_QUEUE_SIZE,
|
|
@@ -1772,6 +1967,7 @@ export {
|
|
|
1772
1967
|
RECONNECT_BASE_DELAY,
|
|
1773
1968
|
RECONNECT_MAX_ATTEMPTS,
|
|
1774
1969
|
RECONNECT_MAX_DELAY,
|
|
1970
|
+
S3StorageProvider,
|
|
1775
1971
|
SALT_LENGTH2 as SALT_LENGTH,
|
|
1776
1972
|
SDKError,
|
|
1777
1973
|
SUPPORTED_CURVE2 as SUPPORTED_CURVE,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatly-sdk",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"description": "Production-ready end-to-end encrypted chat SDK with WhatsApp-style features",
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@aws-sdk/client-s3": "^3.958.0",
|
|
33
34
|
"buffer": "^6.0.3",
|
|
34
35
|
"events": "^3.3.0"
|
|
35
36
|
},
|