chatly-sdk 0.0.1
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/LICENSE +21 -0
- package/README.md +312 -0
- package/dist/index.d.ts +188 -0
- package/dist/index.js +443 -0
- package/examples/groupChat.ts +71 -0
- package/examples/oneToOne.ts +72 -0
- package/examples/saveLoadUser.ts +61 -0
- package/package.json +41 -0
- package/src/ChatManager.ts +103 -0
- package/src/chat/ChatSession.ts +88 -0
- package/src/chat/GroupSession.ts +60 -0
- package/src/crypto/e2e.ts +95 -0
- package/src/crypto/group.ts +24 -0
- package/src/crypto/groupKeys.ts +0 -0
- package/src/crypto/keyManager.ts +28 -0
- package/src/crypto/keys.ts +34 -0
- package/src/crypto/utils.ts +7 -0
- package/src/crypto/uuid.ts +42 -0
- package/src/features/groups.ts +0 -0
- package/src/features/imageSharing.ts +0 -0
- package/src/features/readReceipts.ts +0 -0
- package/src/index.ts +212 -0
- package/src/models/ImageMessage.ts +6 -0
- package/src/models/ReadReceipt.ts +6 -0
- package/src/models/group.ts +8 -0
- package/src/models/message.ts +13 -0
- package/src/models/user.ts +11 -0
- package/src/stores/adapters.ts +22 -0
- package/src/stores/memory/groupStore.ts +19 -0
- package/src/stores/memory/messageStore.ts +21 -0
- package/src/stores/memory/userStore.ts +24 -0
- package/src/transport/adapters.ts +7 -0
- package/src/transport/memoryTransport.ts +24 -0
- package/src/transport/websocketClient.ts +37 -0
- package/src/transport/websocketServer.ts +33 -0
- package/test/crypto.test.ts +45 -0
- package/tsconfig.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// src/crypto/e2e.ts
|
|
2
|
+
import { createECDH as createECDH2, createCipheriv, createDecipheriv, randomBytes as randomBytes2, pbkdf2Sync } from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/crypto/utils.ts
|
|
5
|
+
function bufferToBase64(buffer) {
|
|
6
|
+
return buffer.toString("base64");
|
|
7
|
+
}
|
|
8
|
+
function base64ToBuffer(data) {
|
|
9
|
+
return Buffer.from(data, "base64");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/crypto/keys.ts
|
|
13
|
+
import { createECDH } from "crypto";
|
|
14
|
+
var SUPPORTED_CURVE = "prime256v1";
|
|
15
|
+
function generateIdentityKeyPair() {
|
|
16
|
+
const ecdh = createECDH(SUPPORTED_CURVE);
|
|
17
|
+
ecdh.generateKeys();
|
|
18
|
+
return {
|
|
19
|
+
publicKey: bufferToBase64(ecdh.getPublicKey()),
|
|
20
|
+
privateKey: bufferToBase64(ecdh.getPrivateKey())
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/crypto/e2e.ts
|
|
25
|
+
import { createHash } from "crypto";
|
|
26
|
+
var ALGORITHM = "aes-256-gcm";
|
|
27
|
+
var IV_LENGTH = 12;
|
|
28
|
+
var SALT_LENGTH = 16;
|
|
29
|
+
var KEY_LENGTH = 32;
|
|
30
|
+
var TAG_LENGTH = 16;
|
|
31
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
32
|
+
function deriveSharedSecret(local, remotePublicKey) {
|
|
33
|
+
const ecdh = createECDH2(SUPPORTED_CURVE);
|
|
34
|
+
ecdh.setPrivateKey(base64ToBuffer(local.privateKey));
|
|
35
|
+
const remotePublicKeyBuffer = base64ToBuffer(remotePublicKey);
|
|
36
|
+
const sharedSecret = ecdh.computeSecret(remotePublicKeyBuffer);
|
|
37
|
+
const a = base64ToBuffer(local.publicKey);
|
|
38
|
+
const b = base64ToBuffer(remotePublicKey);
|
|
39
|
+
const [first, second] = Buffer.compare(a, b) <= 0 ? [a, b] : [b, a];
|
|
40
|
+
const hash = createHash("sha256").update(first).update(second).digest();
|
|
41
|
+
const salt = hash.slice(0, SALT_LENGTH);
|
|
42
|
+
const derivedKey = pbkdf2Sync(sharedSecret, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
43
|
+
return derivedKey;
|
|
44
|
+
}
|
|
45
|
+
function encryptMessage(plaintext, secret) {
|
|
46
|
+
const iv = randomBytes2(IV_LENGTH);
|
|
47
|
+
const cipher = createCipheriv(ALGORITHM, secret, iv);
|
|
48
|
+
let ciphertext = cipher.update(plaintext, "utf8");
|
|
49
|
+
ciphertext = Buffer.concat([ciphertext, cipher.final()]);
|
|
50
|
+
const tag = cipher.getAuthTag();
|
|
51
|
+
const encrypted = Buffer.concat([ciphertext, tag]);
|
|
52
|
+
return {
|
|
53
|
+
ciphertext: bufferToBase64(encrypted),
|
|
54
|
+
iv: bufferToBase64(iv)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function decryptMessage(ciphertext, iv, secret) {
|
|
58
|
+
const encryptedBuffer = base64ToBuffer(ciphertext);
|
|
59
|
+
const ivBuffer = base64ToBuffer(iv);
|
|
60
|
+
const tag = encryptedBuffer.slice(-TAG_LENGTH);
|
|
61
|
+
const actualCiphertext = encryptedBuffer.slice(0, -TAG_LENGTH);
|
|
62
|
+
const decipher = createDecipheriv(ALGORITHM, secret, ivBuffer);
|
|
63
|
+
decipher.setAuthTag(tag);
|
|
64
|
+
let decrypted = decipher.update(actualCiphertext);
|
|
65
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
66
|
+
return decrypted.toString("utf8");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/crypto/uuid.ts
|
|
70
|
+
function generateUUID() {
|
|
71
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
72
|
+
return crypto.randomUUID();
|
|
73
|
+
}
|
|
74
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
75
|
+
const bytes = new Uint8Array(16);
|
|
76
|
+
crypto.getRandomValues(bytes);
|
|
77
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
78
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
79
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
80
|
+
return [
|
|
81
|
+
hex.slice(0, 8),
|
|
82
|
+
hex.slice(8, 12),
|
|
83
|
+
hex.slice(12, 16),
|
|
84
|
+
hex.slice(16, 20),
|
|
85
|
+
hex.slice(20, 32)
|
|
86
|
+
].join("-");
|
|
87
|
+
}
|
|
88
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
89
|
+
const r = Math.random() * 16 | 0;
|
|
90
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
91
|
+
return v.toString(16);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/chat/ChatSession.ts
|
|
96
|
+
var ChatSession = class {
|
|
97
|
+
constructor(id, userA, userB) {
|
|
98
|
+
this.id = id;
|
|
99
|
+
this.userA = userA;
|
|
100
|
+
this.userB = userB;
|
|
101
|
+
}
|
|
102
|
+
sharedSecret = null;
|
|
103
|
+
ephemeralKeyPair = null;
|
|
104
|
+
/**
|
|
105
|
+
* Initialize the session by deriving the shared secret
|
|
106
|
+
* ECDH is commutative, so we can use either user's keys
|
|
107
|
+
*/
|
|
108
|
+
async initialize() {
|
|
109
|
+
const localKeyPair = {
|
|
110
|
+
publicKey: this.userA.publicKey,
|
|
111
|
+
privateKey: this.userA.privateKey
|
|
112
|
+
};
|
|
113
|
+
this.sharedSecret = deriveSharedSecret(localKeyPair, this.userB.publicKey);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Initialize from a specific user's perspective (useful when decrypting)
|
|
117
|
+
*/
|
|
118
|
+
async initializeForUser(user) {
|
|
119
|
+
const otherUser = user.id === this.userA.id ? this.userB : this.userA;
|
|
120
|
+
const localKeyPair = {
|
|
121
|
+
publicKey: user.publicKey,
|
|
122
|
+
privateKey: user.privateKey
|
|
123
|
+
};
|
|
124
|
+
this.sharedSecret = deriveSharedSecret(localKeyPair, otherUser.publicKey);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Encrypt a message for this session
|
|
128
|
+
*/
|
|
129
|
+
async encrypt(plaintext, senderId) {
|
|
130
|
+
if (!this.sharedSecret) {
|
|
131
|
+
await this.initialize();
|
|
132
|
+
}
|
|
133
|
+
if (!this.sharedSecret) {
|
|
134
|
+
throw new Error("Failed to initialize session");
|
|
135
|
+
}
|
|
136
|
+
const { ciphertext, iv } = encryptMessage(plaintext, this.sharedSecret);
|
|
137
|
+
return {
|
|
138
|
+
id: generateUUID(),
|
|
139
|
+
senderId,
|
|
140
|
+
receiverId: senderId === this.userA.id ? this.userB.id : this.userA.id,
|
|
141
|
+
ciphertext,
|
|
142
|
+
iv,
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
type: "text"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Decrypt a message in this session
|
|
149
|
+
*/
|
|
150
|
+
async decrypt(message, user) {
|
|
151
|
+
if (!this.sharedSecret || user.id !== this.userA.id && user.id !== this.userB.id) {
|
|
152
|
+
await this.initializeForUser(user);
|
|
153
|
+
}
|
|
154
|
+
if (!this.sharedSecret) {
|
|
155
|
+
throw new Error("Failed to initialize session");
|
|
156
|
+
}
|
|
157
|
+
return decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/crypto/group.ts
|
|
162
|
+
import { pbkdf2Sync as pbkdf2Sync2 } from "crypto";
|
|
163
|
+
var KEY_LENGTH2 = 32;
|
|
164
|
+
var PBKDF2_ITERATIONS2 = 1e5;
|
|
165
|
+
function deriveGroupKey(groupId) {
|
|
166
|
+
const salt = Buffer.from(groupId, "utf8");
|
|
167
|
+
const key = pbkdf2Sync2(groupId, salt, PBKDF2_ITERATIONS2, KEY_LENGTH2, "sha256");
|
|
168
|
+
return {
|
|
169
|
+
groupId,
|
|
170
|
+
key: bufferToBase64(key)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/chat/GroupSession.ts
|
|
175
|
+
var GroupSession = class {
|
|
176
|
+
constructor(group) {
|
|
177
|
+
this.group = group;
|
|
178
|
+
}
|
|
179
|
+
groupKey = null;
|
|
180
|
+
/**
|
|
181
|
+
* Initialize the session by deriving the group key
|
|
182
|
+
*/
|
|
183
|
+
async initialize() {
|
|
184
|
+
const groupKeyData = deriveGroupKey(this.group.id);
|
|
185
|
+
this.groupKey = base64ToBuffer(groupKeyData.key);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Encrypt a message for this group
|
|
189
|
+
*/
|
|
190
|
+
async encrypt(plaintext, senderId) {
|
|
191
|
+
if (!this.groupKey) {
|
|
192
|
+
await this.initialize();
|
|
193
|
+
}
|
|
194
|
+
if (!this.groupKey) {
|
|
195
|
+
throw new Error("Failed to initialize group session");
|
|
196
|
+
}
|
|
197
|
+
const { ciphertext, iv } = encryptMessage(plaintext, this.groupKey);
|
|
198
|
+
return {
|
|
199
|
+
id: generateUUID(),
|
|
200
|
+
senderId,
|
|
201
|
+
groupId: this.group.id,
|
|
202
|
+
ciphertext,
|
|
203
|
+
iv,
|
|
204
|
+
timestamp: Date.now(),
|
|
205
|
+
type: "text"
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Decrypt a message in this group
|
|
210
|
+
*/
|
|
211
|
+
async decrypt(message) {
|
|
212
|
+
if (!this.groupKey) {
|
|
213
|
+
await this.initialize();
|
|
214
|
+
}
|
|
215
|
+
if (!this.groupKey) {
|
|
216
|
+
throw new Error("Failed to initialize group session");
|
|
217
|
+
}
|
|
218
|
+
return decryptMessage(message.ciphertext, message.iv, this.groupKey);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/stores/memory/userStore.ts
|
|
223
|
+
var InMemoryUserStore = class {
|
|
224
|
+
users = /* @__PURE__ */ new Map();
|
|
225
|
+
async create(user) {
|
|
226
|
+
const stored = { ...user, createdAt: Date.now() };
|
|
227
|
+
this.users.set(stored.id, stored);
|
|
228
|
+
return stored;
|
|
229
|
+
}
|
|
230
|
+
async findById(id) {
|
|
231
|
+
return this.users.get(id);
|
|
232
|
+
}
|
|
233
|
+
async save(user) {
|
|
234
|
+
this.users.set(user.id, user);
|
|
235
|
+
}
|
|
236
|
+
async list() {
|
|
237
|
+
return Array.from(this.users.values());
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// src/stores/memory/messageStore.ts
|
|
242
|
+
var InMemoryMessageStore = class {
|
|
243
|
+
messages = [];
|
|
244
|
+
async create(message) {
|
|
245
|
+
this.messages.push(message);
|
|
246
|
+
return message;
|
|
247
|
+
}
|
|
248
|
+
async listByUser(userId) {
|
|
249
|
+
return this.messages.filter(
|
|
250
|
+
(msg) => msg.senderId === userId || msg.receiverId === userId
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
async listByGroup(groupId) {
|
|
254
|
+
return this.messages.filter((msg) => msg.groupId === groupId);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/stores/memory/groupStore.ts
|
|
259
|
+
var InMemoryGroupStore = class {
|
|
260
|
+
groups = /* @__PURE__ */ new Map();
|
|
261
|
+
async create(group) {
|
|
262
|
+
this.groups.set(group.id, group);
|
|
263
|
+
return group;
|
|
264
|
+
}
|
|
265
|
+
async findById(id) {
|
|
266
|
+
return this.groups.get(id);
|
|
267
|
+
}
|
|
268
|
+
async list() {
|
|
269
|
+
return Array.from(this.groups.values());
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// src/transport/memoryTransport.ts
|
|
274
|
+
var InMemoryTransport = class {
|
|
275
|
+
handler;
|
|
276
|
+
connected = false;
|
|
277
|
+
async connect(_userId) {
|
|
278
|
+
this.connected = true;
|
|
279
|
+
}
|
|
280
|
+
async send(message) {
|
|
281
|
+
if (!this.connected) {
|
|
282
|
+
throw new Error("Transport not connected");
|
|
283
|
+
}
|
|
284
|
+
this.handler?.(message);
|
|
285
|
+
}
|
|
286
|
+
onMessage(handler) {
|
|
287
|
+
this.handler = handler;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// src/index.ts
|
|
292
|
+
var ChatSDK = class {
|
|
293
|
+
config;
|
|
294
|
+
currentUser = null;
|
|
295
|
+
constructor(config) {
|
|
296
|
+
this.config = config;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Create a new user with generated identity keys
|
|
300
|
+
*/
|
|
301
|
+
async createUser(username) {
|
|
302
|
+
const keyPair = generateIdentityKeyPair();
|
|
303
|
+
const user = {
|
|
304
|
+
id: generateUUID(),
|
|
305
|
+
username,
|
|
306
|
+
identityKey: keyPair.publicKey,
|
|
307
|
+
publicKey: keyPair.publicKey,
|
|
308
|
+
privateKey: keyPair.privateKey
|
|
309
|
+
};
|
|
310
|
+
await this.config.userStore.create(user);
|
|
311
|
+
return user;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Import an existing user from stored data
|
|
315
|
+
*/
|
|
316
|
+
async importUser(userData) {
|
|
317
|
+
await this.config.userStore.save(userData);
|
|
318
|
+
return userData;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Set the current active user
|
|
322
|
+
*/
|
|
323
|
+
setCurrentUser(user) {
|
|
324
|
+
this.currentUser = user;
|
|
325
|
+
if (this.config.transport) {
|
|
326
|
+
this.config.transport.connect(user.id);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Get the current active user
|
|
331
|
+
*/
|
|
332
|
+
getCurrentUser() {
|
|
333
|
+
return this.currentUser;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Start a 1:1 chat session between two users
|
|
337
|
+
*/
|
|
338
|
+
async startSession(userA, userB) {
|
|
339
|
+
const ids = [userA.id, userB.id].sort();
|
|
340
|
+
const sessionId = `${ids[0]}-${ids[1]}`;
|
|
341
|
+
const session = new ChatSession(sessionId, userA, userB);
|
|
342
|
+
await session.initialize();
|
|
343
|
+
return session;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Create a new group with members
|
|
347
|
+
*/
|
|
348
|
+
async createGroup(name, members) {
|
|
349
|
+
if (members.length < 2) {
|
|
350
|
+
throw new Error("Group must have at least 2 members");
|
|
351
|
+
}
|
|
352
|
+
const group = {
|
|
353
|
+
id: generateUUID(),
|
|
354
|
+
name,
|
|
355
|
+
members,
|
|
356
|
+
createdAt: Date.now()
|
|
357
|
+
};
|
|
358
|
+
await this.config.groupStore.create(group);
|
|
359
|
+
const session = new GroupSession(group);
|
|
360
|
+
await session.initialize();
|
|
361
|
+
return session;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Load an existing group by ID
|
|
365
|
+
*/
|
|
366
|
+
async loadGroup(id) {
|
|
367
|
+
const group = await this.config.groupStore.findById(id);
|
|
368
|
+
if (!group) {
|
|
369
|
+
throw new Error(`Group not found: ${id}`);
|
|
370
|
+
}
|
|
371
|
+
const session = new GroupSession(group);
|
|
372
|
+
await session.initialize();
|
|
373
|
+
return session;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Send a message in a chat session (1:1 or group)
|
|
377
|
+
*/
|
|
378
|
+
async sendMessage(session, plaintext) {
|
|
379
|
+
if (!this.currentUser) {
|
|
380
|
+
throw new Error("No current user set. Call setCurrentUser() first.");
|
|
381
|
+
}
|
|
382
|
+
let message;
|
|
383
|
+
if (session instanceof ChatSession) {
|
|
384
|
+
message = await session.encrypt(plaintext, this.currentUser.id);
|
|
385
|
+
} else {
|
|
386
|
+
message = await session.encrypt(plaintext, this.currentUser.id);
|
|
387
|
+
}
|
|
388
|
+
await this.config.messageStore.create(message);
|
|
389
|
+
if (this.config.transport) {
|
|
390
|
+
await this.config.transport.send(message);
|
|
391
|
+
}
|
|
392
|
+
return message;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Decrypt a message
|
|
396
|
+
*/
|
|
397
|
+
async decryptMessage(message, user) {
|
|
398
|
+
if (message.groupId) {
|
|
399
|
+
const group = await this.config.groupStore.findById(message.groupId);
|
|
400
|
+
if (!group) {
|
|
401
|
+
throw new Error(`Group not found: ${message.groupId}`);
|
|
402
|
+
}
|
|
403
|
+
const session = new GroupSession(group);
|
|
404
|
+
await session.initialize();
|
|
405
|
+
return await session.decrypt(message);
|
|
406
|
+
} else {
|
|
407
|
+
const otherUserId = message.senderId === user.id ? message.receiverId : message.senderId;
|
|
408
|
+
if (!otherUserId) {
|
|
409
|
+
throw new Error("Invalid message: missing receiver/sender");
|
|
410
|
+
}
|
|
411
|
+
const otherUser = await this.config.userStore.findById(otherUserId);
|
|
412
|
+
if (!otherUser) {
|
|
413
|
+
throw new Error(`User not found: ${otherUserId}`);
|
|
414
|
+
}
|
|
415
|
+
const ids = [user.id, otherUser.id].sort();
|
|
416
|
+
const sessionId = `${ids[0]}-${ids[1]}`;
|
|
417
|
+
const session = new ChatSession(sessionId, user, otherUser);
|
|
418
|
+
await session.initializeForUser(user);
|
|
419
|
+
return await session.decrypt(message, user);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Get messages for a user
|
|
424
|
+
*/
|
|
425
|
+
async getMessagesForUser(userId) {
|
|
426
|
+
return await this.config.messageStore.listByUser(userId);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Get messages for a group
|
|
430
|
+
*/
|
|
431
|
+
async getMessagesForGroup(groupId) {
|
|
432
|
+
return await this.config.messageStore.listByGroup(groupId);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
export {
|
|
436
|
+
ChatSDK,
|
|
437
|
+
ChatSession,
|
|
438
|
+
GroupSession,
|
|
439
|
+
InMemoryGroupStore,
|
|
440
|
+
InMemoryMessageStore,
|
|
441
|
+
InMemoryTransport,
|
|
442
|
+
InMemoryUserStore
|
|
443
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ChatSDK } from "../src/index.js";
|
|
2
|
+
import {
|
|
3
|
+
InMemoryUserStore,
|
|
4
|
+
InMemoryMessageStore,
|
|
5
|
+
InMemoryGroupStore,
|
|
6
|
+
} from "../src/index.js";
|
|
7
|
+
import { InMemoryTransport } from "../src/index.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Example: Group Chat with multiple members
|
|
11
|
+
*/
|
|
12
|
+
async function main() {
|
|
13
|
+
// Initialize SDK
|
|
14
|
+
const sdk = new ChatSDK({
|
|
15
|
+
userStore: new InMemoryUserStore(),
|
|
16
|
+
messageStore: new InMemoryMessageStore(),
|
|
17
|
+
groupStore: new InMemoryGroupStore(),
|
|
18
|
+
transport: new InMemoryTransport(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Create users
|
|
22
|
+
console.log("Creating users...");
|
|
23
|
+
const alice = await sdk.createUser("alice");
|
|
24
|
+
const bob = await sdk.createUser("bob");
|
|
25
|
+
const charlie = await sdk.createUser("charlie");
|
|
26
|
+
|
|
27
|
+
console.log(`Alice: ${alice.id}`);
|
|
28
|
+
console.log(`Bob: ${bob.id}`);
|
|
29
|
+
console.log(`Charlie: ${charlie.id}`);
|
|
30
|
+
|
|
31
|
+
// Create a group
|
|
32
|
+
console.log("\nCreating group...");
|
|
33
|
+
const group = await sdk.createGroup("Project Team", [alice, bob, charlie]);
|
|
34
|
+
console.log(`Group created: ${group.group.id} - ${group.group.name}`);
|
|
35
|
+
console.log(`Members: ${group.group.members.map((m) => m.username).join(", ")}`);
|
|
36
|
+
|
|
37
|
+
// Alice sends a message to the group
|
|
38
|
+
console.log("\nAlice sending group message...");
|
|
39
|
+
sdk.setCurrentUser(alice);
|
|
40
|
+
const message1 = await sdk.sendMessage(group, "Hello team! Let's start the project.");
|
|
41
|
+
console.log(`Message sent: ${message1.id}`);
|
|
42
|
+
|
|
43
|
+
// Bob reads the group message
|
|
44
|
+
console.log("\nBob reading group messages...");
|
|
45
|
+
sdk.setCurrentUser(bob);
|
|
46
|
+
const bobMessages = await sdk.getMessagesForGroup(group.group.id);
|
|
47
|
+
for (const msg of bobMessages) {
|
|
48
|
+
const decrypted = await sdk.decryptMessage(msg, bob);
|
|
49
|
+
console.log(`Bob sees: "${decrypted}" from ${msg.senderId}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Charlie replies
|
|
53
|
+
console.log("\nCharlie replying...");
|
|
54
|
+
sdk.setCurrentUser(charlie);
|
|
55
|
+
const message2 = await sdk.sendMessage(group, "Sounds good! I'm ready.");
|
|
56
|
+
console.log(`Message sent: ${message2.id}`);
|
|
57
|
+
|
|
58
|
+
// Alice reads all messages
|
|
59
|
+
console.log("\nAlice reading all group messages...");
|
|
60
|
+
sdk.setCurrentUser(alice);
|
|
61
|
+
const allMessages = await sdk.getMessagesForGroup(group.group.id);
|
|
62
|
+
console.log(`Total messages: ${allMessages.length}`);
|
|
63
|
+
for (const msg of allMessages) {
|
|
64
|
+
const decrypted = await sdk.decryptMessage(msg, alice);
|
|
65
|
+
const sender = group.group.members.find((m) => m.id === msg.senderId);
|
|
66
|
+
console.log(`${sender?.username || "Unknown"}: "${decrypted}"`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
main().catch(console.error);
|
|
71
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ChatSDK } from "../src/index.js";
|
|
2
|
+
import {
|
|
3
|
+
InMemoryUserStore,
|
|
4
|
+
InMemoryMessageStore,
|
|
5
|
+
InMemoryGroupStore,
|
|
6
|
+
} from "../src/index.js";
|
|
7
|
+
import { InMemoryTransport } from "../src/index.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Example: 1:1 Chat between two users
|
|
11
|
+
*/
|
|
12
|
+
async function main() {
|
|
13
|
+
// Initialize SDK with in-memory stores
|
|
14
|
+
const sdk = new ChatSDK({
|
|
15
|
+
userStore: new InMemoryUserStore(),
|
|
16
|
+
messageStore: new InMemoryMessageStore(),
|
|
17
|
+
groupStore: new InMemoryGroupStore(),
|
|
18
|
+
transport: new InMemoryTransport(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Create two users
|
|
22
|
+
console.log("Creating users...");
|
|
23
|
+
const alice = await sdk.createUser("alice");
|
|
24
|
+
const bob = await sdk.createUser("bob");
|
|
25
|
+
|
|
26
|
+
console.log(`Alice ID: ${alice.id}`);
|
|
27
|
+
console.log(`Bob ID: ${bob.id}`);
|
|
28
|
+
|
|
29
|
+
// Set Alice as current user
|
|
30
|
+
sdk.setCurrentUser(alice);
|
|
31
|
+
|
|
32
|
+
// Start a chat session between Alice and Bob
|
|
33
|
+
console.log("\nStarting chat session...");
|
|
34
|
+
const session = await sdk.startSession(alice, bob);
|
|
35
|
+
|
|
36
|
+
// Alice sends a message to Bob
|
|
37
|
+
console.log("\nAlice sending message...");
|
|
38
|
+
const message1 = await sdk.sendMessage(session, "Hello Bob! This is encrypted.");
|
|
39
|
+
console.log(`Message sent: ${message1.id}`);
|
|
40
|
+
|
|
41
|
+
// Switch to Bob's perspective
|
|
42
|
+
sdk.setCurrentUser(bob);
|
|
43
|
+
|
|
44
|
+
// Bob receives and decrypts the message
|
|
45
|
+
console.log("\nBob receiving messages...");
|
|
46
|
+
const messages = await sdk.getMessagesForUser(bob.id);
|
|
47
|
+
console.log(`Bob has ${messages.length} message(s)`);
|
|
48
|
+
|
|
49
|
+
for (const msg of messages) {
|
|
50
|
+
const decrypted = await sdk.decryptMessage(msg, bob);
|
|
51
|
+
console.log(`Decrypted: "${decrypted}"`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Bob replies
|
|
55
|
+
console.log("\nBob replying...");
|
|
56
|
+
const replySession = await sdk.startSession(bob, alice);
|
|
57
|
+
const message2 = await sdk.sendMessage(replySession, "Hi Alice! Got your message.");
|
|
58
|
+
console.log(`Reply sent: ${message2.id}`);
|
|
59
|
+
|
|
60
|
+
// Switch back to Alice
|
|
61
|
+
sdk.setCurrentUser(alice);
|
|
62
|
+
const aliceMessages = await sdk.getMessagesForUser(alice.id);
|
|
63
|
+
console.log(`\nAlice has ${aliceMessages.length} message(s)`);
|
|
64
|
+
|
|
65
|
+
for (const msg of aliceMessages) {
|
|
66
|
+
const decrypted = await sdk.decryptMessage(msg, alice);
|
|
67
|
+
console.log(`Decrypted: "${decrypted}"`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().catch(console.error);
|
|
72
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ChatSDK } from "../src/index.js";
|
|
2
|
+
import {
|
|
3
|
+
InMemoryUserStore,
|
|
4
|
+
InMemoryMessageStore,
|
|
5
|
+
InMemoryGroupStore,
|
|
6
|
+
} from "../src/index.js";
|
|
7
|
+
import type { StoredUser } from "../src/index.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Example: Save and load user data
|
|
11
|
+
*/
|
|
12
|
+
async function main() {
|
|
13
|
+
// Initialize SDK
|
|
14
|
+
const sdk = new ChatSDK({
|
|
15
|
+
userStore: new InMemoryUserStore(),
|
|
16
|
+
messageStore: new InMemoryMessageStore(),
|
|
17
|
+
groupStore: new InMemoryGroupStore(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Create a user
|
|
21
|
+
console.log("Creating user...");
|
|
22
|
+
const user = await sdk.createUser("john_doe");
|
|
23
|
+
console.log(`User created: ${user.username} (${user.id})`);
|
|
24
|
+
console.log(`Public key: ${user.publicKey.substring(0, 20)}...`);
|
|
25
|
+
|
|
26
|
+
// Simulate saving user data (in real app, this would be to a database)
|
|
27
|
+
const userStore = new InMemoryUserStore();
|
|
28
|
+
const storedUser: StoredUser = {
|
|
29
|
+
...user,
|
|
30
|
+
createdAt: Date.now(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Save user
|
|
34
|
+
await userStore.save(storedUser);
|
|
35
|
+
console.log("\nUser saved to store");
|
|
36
|
+
|
|
37
|
+
// Simulate loading user from storage
|
|
38
|
+
console.log("\nLoading user from store...");
|
|
39
|
+
const loadedUser = await userStore.findById(user.id);
|
|
40
|
+
if (loadedUser) {
|
|
41
|
+
console.log(`User loaded: ${loadedUser.username} (${loadedUser.id})`);
|
|
42
|
+
console.log(`Keys match: ${loadedUser.publicKey === user.publicKey}`);
|
|
43
|
+
|
|
44
|
+
// Import the user into a new SDK instance
|
|
45
|
+
const sdk2 = new ChatSDK({
|
|
46
|
+
userStore: new InMemoryUserStore(),
|
|
47
|
+
messageStore: new InMemoryMessageStore(),
|
|
48
|
+
groupStore: new InMemoryGroupStore(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const importedUser = await sdk2.importUser(loadedUser as StoredUser);
|
|
52
|
+
console.log(`\nUser imported into new SDK instance: ${importedUser.username}`);
|
|
53
|
+
sdk2.setCurrentUser(importedUser);
|
|
54
|
+
console.log("User is now active in SDK2");
|
|
55
|
+
} else {
|
|
56
|
+
console.error("Failed to load user");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
main().catch(console.error);
|
|
61
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chatly-sdk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsup src/index.ts --dts --format esm",
|
|
9
|
+
"start": "ts-node src/index.ts",
|
|
10
|
+
"test": "ts-node test/crypto.test.ts"
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/bharath-arch/chatly-sdk.git"
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
"keywords": [
|
|
22
|
+
"chat",
|
|
23
|
+
"sdk",
|
|
24
|
+
"e2e",
|
|
25
|
+
"encryption",
|
|
26
|
+
"messaging",
|
|
27
|
+
"whatsapp"
|
|
28
|
+
],
|
|
29
|
+
"author": "",
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"description": "Production-ready end-to-end encrypted chat SDK with WhatsApp-style features",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"buffer": "^6.0.3"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^24.10.1",
|
|
37
|
+
"ts-node": "^10.9.2",
|
|
38
|
+
"tsup": "^8.0.0",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
}
|
|
41
|
+
}
|