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/test/sdk.test.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { ChatSDK } from '../src/index';
|
|
2
|
+
import { InMemoryUserStore } from '../src/stores/memory/userStore';
|
|
3
|
+
import { InMemoryMessageStore } from '../src/stores/memory/messageStore';
|
|
4
|
+
import { InMemoryGroupStore } from '../src/stores/memory/groupStore';
|
|
5
|
+
import { EVENTS } from '../src/constants';
|
|
6
|
+
import { ValidationError, SessionError } from '../src/utils/errors';
|
|
7
|
+
|
|
8
|
+
describe('ChatSDK', () => {
|
|
9
|
+
let sdk: ChatSDK;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
sdk = new ChatSDK({
|
|
13
|
+
userStore: new InMemoryUserStore(),
|
|
14
|
+
messageStore: new InMemoryMessageStore(),
|
|
15
|
+
groupStore: new InMemoryGroupStore(),
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('User Management', () => {
|
|
20
|
+
it('should create a user with valid username', async () => {
|
|
21
|
+
const user = await sdk.createUser('alice');
|
|
22
|
+
|
|
23
|
+
expect(user).toHaveProperty('id');
|
|
24
|
+
expect(user).toHaveProperty('username', 'alice');
|
|
25
|
+
expect(user).toHaveProperty('publicKey');
|
|
26
|
+
expect(user).toHaveProperty('privateKey');
|
|
27
|
+
expect(user).toHaveProperty('identityKey');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should reject invalid usernames', async () => {
|
|
31
|
+
await expect(sdk.createUser('')).rejects.toThrow(ValidationError);
|
|
32
|
+
await expect(sdk.createUser('ab')).rejects.toThrow(ValidationError); // Too short
|
|
33
|
+
await expect(sdk.createUser('a'.repeat(21))).rejects.toThrow(ValidationError); // Too long
|
|
34
|
+
await expect(sdk.createUser('user@name')).rejects.toThrow(ValidationError); // Invalid chars
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should emit user:created event', async () => {
|
|
38
|
+
const handler = jest.fn();
|
|
39
|
+
sdk.on(EVENTS.USER_CREATED, handler);
|
|
40
|
+
|
|
41
|
+
const user = await sdk.createUser('bob');
|
|
42
|
+
|
|
43
|
+
expect(handler).toHaveBeenCalledWith(user);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should list all users', async () => {
|
|
47
|
+
await sdk.createUser('alice');
|
|
48
|
+
await sdk.createUser('bob');
|
|
49
|
+
|
|
50
|
+
const users = await sdk.listUsers();
|
|
51
|
+
|
|
52
|
+
expect(users).toHaveLength(2);
|
|
53
|
+
expect(users.map(u => u.username)).toContain('alice');
|
|
54
|
+
expect(users.map(u => u.username)).toContain('bob');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should get user by ID', async () => {
|
|
58
|
+
const alice = await sdk.createUser('alice');
|
|
59
|
+
|
|
60
|
+
const retrieved = await sdk.getUserById(alice.id);
|
|
61
|
+
|
|
62
|
+
expect(retrieved).toBeDefined();
|
|
63
|
+
expect(retrieved?.id).toBe(alice.id);
|
|
64
|
+
expect(retrieved?.username).toBe('alice');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('Session Management', () => {
|
|
69
|
+
it('should create a 1:1 chat session', async () => {
|
|
70
|
+
const alice = await sdk.createUser('alice');
|
|
71
|
+
const bob = await sdk.createUser('bob');
|
|
72
|
+
|
|
73
|
+
const session = await sdk.startSession(alice, bob);
|
|
74
|
+
|
|
75
|
+
expect(session).toBeDefined();
|
|
76
|
+
expect(session.id).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should emit session:created event', async () => {
|
|
80
|
+
const alice = await sdk.createUser('alice');
|
|
81
|
+
const bob = await sdk.createUser('bob');
|
|
82
|
+
const handler = jest.fn();
|
|
83
|
+
sdk.on(EVENTS.SESSION_CREATED, handler);
|
|
84
|
+
|
|
85
|
+
const session = await sdk.startSession(alice, bob);
|
|
86
|
+
|
|
87
|
+
expect(handler).toHaveBeenCalledWith(session);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should create consistent session IDs regardless of user order', async () => {
|
|
91
|
+
const alice = await sdk.createUser('alice');
|
|
92
|
+
const bob = await sdk.createUser('bob');
|
|
93
|
+
|
|
94
|
+
const session1 = await sdk.startSession(alice, bob);
|
|
95
|
+
const session2 = await sdk.startSession(bob, alice);
|
|
96
|
+
|
|
97
|
+
expect(session1.id).toBe(session2.id);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('Group Management', () => {
|
|
102
|
+
it('should create a group with valid members', async () => {
|
|
103
|
+
const alice = await sdk.createUser('alice');
|
|
104
|
+
const bob = await sdk.createUser('bob');
|
|
105
|
+
|
|
106
|
+
const group = await sdk.createGroup('Team Chat', [alice, bob]);
|
|
107
|
+
|
|
108
|
+
expect(group).toBeDefined();
|
|
109
|
+
expect(group.group.name).toBe('Team Chat');
|
|
110
|
+
expect(group.group.members).toHaveLength(2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should reject groups with less than 2 members', async () => {
|
|
114
|
+
const alice = await sdk.createUser('alice');
|
|
115
|
+
|
|
116
|
+
await expect(
|
|
117
|
+
sdk.createGroup('Solo Chat', [alice])
|
|
118
|
+
).rejects.toThrow(ValidationError);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should reject invalid group names', async () => {
|
|
122
|
+
const alice = await sdk.createUser('alice');
|
|
123
|
+
const bob = await sdk.createUser('bob');
|
|
124
|
+
|
|
125
|
+
await expect(
|
|
126
|
+
sdk.createGroup('', [alice, bob])
|
|
127
|
+
).rejects.toThrow(ValidationError);
|
|
128
|
+
|
|
129
|
+
await expect(
|
|
130
|
+
sdk.createGroup(' ', [alice, bob])
|
|
131
|
+
).rejects.toThrow(ValidationError);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should emit group:created event', async () => {
|
|
135
|
+
const alice = await sdk.createUser('alice');
|
|
136
|
+
const bob = await sdk.createUser('bob');
|
|
137
|
+
const handler = jest.fn();
|
|
138
|
+
sdk.on(EVENTS.GROUP_CREATED, handler);
|
|
139
|
+
|
|
140
|
+
const group = await sdk.createGroup('Team', [alice, bob]);
|
|
141
|
+
|
|
142
|
+
expect(handler).toHaveBeenCalledWith(group);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should list all groups', async () => {
|
|
146
|
+
const alice = await sdk.createUser('alice');
|
|
147
|
+
const bob = await sdk.createUser('bob');
|
|
148
|
+
|
|
149
|
+
await sdk.createGroup('Group 1', [alice, bob]);
|
|
150
|
+
await sdk.createGroup('Group 2', [alice, bob]);
|
|
151
|
+
|
|
152
|
+
const groups = await sdk.listGroups();
|
|
153
|
+
|
|
154
|
+
expect(groups).toHaveLength(2);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should load an existing group', async () => {
|
|
158
|
+
const alice = await sdk.createUser('alice');
|
|
159
|
+
const bob = await sdk.createUser('bob');
|
|
160
|
+
const created = await sdk.createGroup('Team', [alice, bob]);
|
|
161
|
+
|
|
162
|
+
const loaded = await sdk.loadGroup(created.group.id);
|
|
163
|
+
|
|
164
|
+
expect(loaded.group.id).toBe(created.group.id);
|
|
165
|
+
expect(loaded.group.name).toBe('Team');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Messaging', () => {
|
|
170
|
+
it('should send and decrypt a 1:1 message', async () => {
|
|
171
|
+
const alice = await sdk.createUser('alice');
|
|
172
|
+
const bob = await sdk.createUser('bob');
|
|
173
|
+
const session = await sdk.startSession(alice, bob);
|
|
174
|
+
|
|
175
|
+
sdk.setCurrentUser(alice);
|
|
176
|
+
const message = await sdk.sendMessage(session, 'Hello Bob!');
|
|
177
|
+
|
|
178
|
+
expect(message).toBeDefined();
|
|
179
|
+
expect(message.senderId).toBe(alice.id);
|
|
180
|
+
expect(message.receiverId).toBe(bob.id);
|
|
181
|
+
|
|
182
|
+
const decrypted = await sdk.decryptMessage(message, bob);
|
|
183
|
+
expect(decrypted).toBe('Hello Bob!');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should reject messages without current user', async () => {
|
|
187
|
+
const alice = await sdk.createUser('alice');
|
|
188
|
+
const bob = await sdk.createUser('bob');
|
|
189
|
+
const session = await sdk.startSession(alice, bob);
|
|
190
|
+
|
|
191
|
+
await expect(
|
|
192
|
+
sdk.sendMessage(session, 'Hello')
|
|
193
|
+
).rejects.toThrow(SessionError);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should reject invalid messages', async () => {
|
|
197
|
+
const alice = await sdk.createUser('alice');
|
|
198
|
+
const bob = await sdk.createUser('bob');
|
|
199
|
+
const session = await sdk.startSession(alice, bob);
|
|
200
|
+
|
|
201
|
+
sdk.setCurrentUser(alice);
|
|
202
|
+
|
|
203
|
+
await expect(
|
|
204
|
+
sdk.sendMessage(session, '')
|
|
205
|
+
).rejects.toThrow(ValidationError);
|
|
206
|
+
|
|
207
|
+
await expect(
|
|
208
|
+
sdk.sendMessage(session, 'A'.repeat(10001))
|
|
209
|
+
).rejects.toThrow(ValidationError);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should send and decrypt a group message', async () => {
|
|
213
|
+
const alice = await sdk.createUser('alice');
|
|
214
|
+
const bob = await sdk.createUser('bob');
|
|
215
|
+
const charlie = await sdk.createUser('charlie');
|
|
216
|
+
const group = await sdk.createGroup('Team', [alice, bob, charlie]);
|
|
217
|
+
|
|
218
|
+
sdk.setCurrentUser(alice);
|
|
219
|
+
const message = await sdk.sendMessage(group, 'Hello team!');
|
|
220
|
+
|
|
221
|
+
expect(message.groupId).toBe(group.group.id);
|
|
222
|
+
|
|
223
|
+
const decryptedByBob = await sdk.decryptMessage(message, bob);
|
|
224
|
+
const decryptedByCharlie = await sdk.decryptMessage(message, charlie);
|
|
225
|
+
|
|
226
|
+
expect(decryptedByBob).toBe('Hello team!');
|
|
227
|
+
expect(decryptedByCharlie).toBe('Hello team!');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should get messages for a user', async () => {
|
|
231
|
+
const alice = await sdk.createUser('alice');
|
|
232
|
+
const bob = await sdk.createUser('bob');
|
|
233
|
+
const session = await sdk.startSession(alice, bob);
|
|
234
|
+
|
|
235
|
+
sdk.setCurrentUser(alice);
|
|
236
|
+
await sdk.sendMessage(session, 'Message 1');
|
|
237
|
+
await sdk.sendMessage(session, 'Message 2');
|
|
238
|
+
|
|
239
|
+
const messages = await sdk.getMessagesForUser(bob.id);
|
|
240
|
+
|
|
241
|
+
expect(messages).toHaveLength(2);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should get messages for a group', async () => {
|
|
245
|
+
const alice = await sdk.createUser('alice');
|
|
246
|
+
const bob = await sdk.createUser('bob');
|
|
247
|
+
const group = await sdk.createGroup('Team', [alice, bob]);
|
|
248
|
+
|
|
249
|
+
sdk.setCurrentUser(alice);
|
|
250
|
+
await sdk.sendMessage(group, 'Message 1');
|
|
251
|
+
await sdk.sendMessage(group, 'Message 2');
|
|
252
|
+
|
|
253
|
+
const messages = await sdk.getMessagesForGroup(group.group.id);
|
|
254
|
+
|
|
255
|
+
expect(messages).toHaveLength(2);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('Connection State', () => {
|
|
260
|
+
it('should return disconnected when no transport', () => {
|
|
261
|
+
expect(sdk.isConnected()).toBe(false);
|
|
262
|
+
expect(sdk.getConnectionState()).toBe('disconnected');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('Queue Status', () => {
|
|
267
|
+
it('should return queue status', () => {
|
|
268
|
+
const status = sdk.getQueueStatus();
|
|
269
|
+
|
|
270
|
+
expect(status).toHaveProperty('size');
|
|
271
|
+
expect(status).toHaveProperty('pending');
|
|
272
|
+
expect(status).toHaveProperty('retryable');
|
|
273
|
+
expect(status.size).toBe(0);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { validateUsername, validateMessage, validateGroupName, validateGroupMembers } from '../src/utils/validation';
|
|
2
|
+
import { ValidationError } from '../src/utils/errors';
|
|
3
|
+
|
|
4
|
+
describe('Validation Utilities', () => {
|
|
5
|
+
describe('validateUsername', () => {
|
|
6
|
+
it('should accept valid usernames', () => {
|
|
7
|
+
expect(() => validateUsername('alice')).not.toThrow();
|
|
8
|
+
expect(() => validateUsername('bob123')).not.toThrow();
|
|
9
|
+
expect(() => validateUsername('user_name')).not.toThrow();
|
|
10
|
+
expect(() => validateUsername('user-name')).not.toThrow();
|
|
11
|
+
expect(() => validateUsername('ABC')).not.toThrow();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should reject invalid usernames', () => {
|
|
15
|
+
expect(() => validateUsername('')).toThrow(ValidationError);
|
|
16
|
+
expect(() => validateUsername('ab')).toThrow(ValidationError); // Too short
|
|
17
|
+
expect(() => validateUsername('a'.repeat(21))).toThrow(ValidationError); // Too long
|
|
18
|
+
expect(() => validateUsername('user@name')).toThrow(ValidationError); // Invalid char
|
|
19
|
+
expect(() => validateUsername('user name')).toThrow(ValidationError); // Space
|
|
20
|
+
expect(() => validateUsername('user.name')).toThrow(ValidationError); // Dot
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('validateMessage', () => {
|
|
25
|
+
it('should accept valid messages', () => {
|
|
26
|
+
expect(() => validateMessage('Hello')).not.toThrow();
|
|
27
|
+
expect(() => validateMessage('A'.repeat(10000))).not.toThrow();
|
|
28
|
+
expect(() => validateMessage('Message with emojis 👋🌍')).not.toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should reject invalid messages', () => {
|
|
32
|
+
expect(() => validateMessage('')).toThrow(ValidationError);
|
|
33
|
+
expect(() => validateMessage('A'.repeat(10001))).toThrow(ValidationError); // Too long
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('validateGroupName', () => {
|
|
38
|
+
it('should accept valid group names', () => {
|
|
39
|
+
expect(() => validateGroupName('Team Chat')).not.toThrow();
|
|
40
|
+
expect(() => validateGroupName('Project 2024')).not.toThrow();
|
|
41
|
+
expect(() => validateGroupName('A'.repeat(100))).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should reject invalid group names', () => {
|
|
45
|
+
expect(() => validateGroupName('')).toThrow(ValidationError);
|
|
46
|
+
expect(() => validateGroupName(' ')).toThrow(ValidationError);
|
|
47
|
+
expect(() => validateGroupName('A'.repeat(101))).toThrow(ValidationError); // Too long
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('validateGroupMembers', () => {
|
|
52
|
+
it('should accept valid member counts', () => {
|
|
53
|
+
expect(() => validateGroupMembers(2)).not.toThrow();
|
|
54
|
+
expect(() => validateGroupMembers(10)).not.toThrow();
|
|
55
|
+
expect(() => validateGroupMembers(256)).not.toThrow();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should reject invalid member counts', () => {
|
|
59
|
+
expect(() => validateGroupMembers(0)).toThrow(ValidationError);
|
|
60
|
+
expect(() => validateGroupMembers(1)).toThrow(ValidationError); // Too few
|
|
61
|
+
expect(() => validateGroupMembers(257)).toThrow(ValidationError); // Too many
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -4,26 +4,24 @@
|
|
|
4
4
|
// File Layout
|
|
5
5
|
// "rootDir": "./src",
|
|
6
6
|
// "outDir": "./dist",
|
|
7
|
-
|
|
8
7
|
// Environment Settings
|
|
9
8
|
// See also https://aka.ms/tsconfig/module
|
|
10
9
|
"module": "nodenext",
|
|
11
10
|
"target": "esnext",
|
|
12
|
-
|
|
13
11
|
// For nodejs:
|
|
14
12
|
// "lib": ["esnext"],
|
|
15
|
-
"types": [
|
|
13
|
+
"types": [
|
|
14
|
+
"node",
|
|
15
|
+
"jest"
|
|
16
|
+
],
|
|
16
17
|
// and npm install -D @types/node
|
|
17
|
-
|
|
18
18
|
// Other Outputs
|
|
19
19
|
"sourceMap": true,
|
|
20
20
|
"declaration": true,
|
|
21
21
|
"declarationMap": true,
|
|
22
|
-
|
|
23
22
|
// Stricter Typechecking Options
|
|
24
23
|
"noUncheckedIndexedAccess": true,
|
|
25
24
|
"exactOptionalPropertyTypes": true,
|
|
26
|
-
|
|
27
25
|
// Style Options
|
|
28
26
|
// "noImplicitReturns": true,
|
|
29
27
|
// "noImplicitOverride": true,
|
|
@@ -31,7 +29,6 @@
|
|
|
31
29
|
// "noUnusedParameters": true,
|
|
32
30
|
// "noFallthroughCasesInSwitch": true,
|
|
33
31
|
// "noPropertyAccessFromIndexSignature": true,
|
|
34
|
-
|
|
35
32
|
// Recommended Options
|
|
36
33
|
"strict": true,
|
|
37
34
|
"jsx": "react-jsx",
|
|
@@ -39,6 +36,10 @@
|
|
|
39
36
|
"isolatedModules": true,
|
|
40
37
|
"noUncheckedSideEffectImports": true,
|
|
41
38
|
"moduleDetection": "force",
|
|
42
|
-
"skipLibCheck": true
|
|
43
|
-
}
|
|
44
|
-
|
|
39
|
+
"skipLibCheck": true
|
|
40
|
+
},
|
|
41
|
+
"exclude": [
|
|
42
|
+
"dist",
|
|
43
|
+
"node_modules"
|
|
44
|
+
]
|
|
45
|
+
}
|
package/src/ChatManager.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { KeyManager, KeyPair } from './crypto/keyManager';
|
|
3
|
-
import { encrypt } from './crypto/encrypt';
|
|
4
|
-
import { decrypt } from './crypto/decrypt';
|
|
5
|
-
import { ChatClient } from './transport/websocketClient';
|
|
6
|
-
import { Message } from './models/Message';
|
|
7
|
-
|
|
8
|
-
export class ChatManager {
|
|
9
|
-
private keyManager: KeyManager;
|
|
10
|
-
private chatClient: ChatClient;
|
|
11
|
-
private contacts: { [id: string]: Uint8Array } = {}; // Maps user ID to public key
|
|
12
|
-
|
|
13
|
-
constructor(websocketUrl: string) {
|
|
14
|
-
this.keyManager = new KeyManager();
|
|
15
|
-
this.chatClient = new ChatClient(websocketUrl);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async initialize(): Promise<void> {
|
|
19
|
-
await this.keyManager.generateKeys();
|
|
20
|
-
this.chatClient.connect();
|
|
21
|
-
this.chatClient.onMessage(this.handleIncomingMessage.bind(this));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
addContact(userId: string, publicKey: Uint8Array): void {
|
|
25
|
-
this.contacts[userId] = publicKey;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async sendMessage(recipientId: string, content: string): Promise<void> {
|
|
29
|
-
const recipientPublicKey = this.contacts[recipientId];
|
|
30
|
-
if (!recipientPublicKey) {
|
|
31
|
-
throw new Error(`Contact not found: ${recipientId}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const senderPrivateKey = this.keyManager.getPrivateKey();
|
|
35
|
-
if (!senderPrivateKey) {
|
|
36
|
-
throw new Error('Sender keys not generated.');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const message: Message = {
|
|
40
|
-
id: Date.now().toString(),
|
|
41
|
-
senderId: this.getOwnUserId(),
|
|
42
|
-
recipientId,
|
|
43
|
-
content,
|
|
44
|
-
timestamp: Date.now(),
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const encryptedMessage = await encrypt(
|
|
48
|
-
JSON.stringify(message),
|
|
49
|
-
recipientPublicKey,
|
|
50
|
-
senderPrivateKey
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
this.chatClient.sendMessage(Buffer.from(encryptedMessage));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private async handleIncomingMessage(encryptedMessage: Buffer): Promise<void> {
|
|
57
|
-
// In a real application, you would need a way to identify the sender
|
|
58
|
-
// and get their public key. For this example, we'll assume the sender
|
|
59
|
-
// is the only other contact.
|
|
60
|
-
const senderId = Object.keys(this.contacts)[0];
|
|
61
|
-
if (!senderId) {
|
|
62
|
-
console.error('Received message but no contacts are known.');
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
const senderPublicKey = this.contacts[senderId];
|
|
66
|
-
|
|
67
|
-
if (!senderPublicKey) {
|
|
68
|
-
console.error('Received message from unknown sender.');
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const recipientPrivateKey = this.keyManager.getPrivateKey();
|
|
73
|
-
if (!recipientPrivateKey) {
|
|
74
|
-
throw new Error('Recipient keys not generated.');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const decryptedMessageJson = await decrypt(
|
|
79
|
-
encryptedMessage,
|
|
80
|
-
senderPublicKey,
|
|
81
|
-
recipientPrivateKey
|
|
82
|
-
);
|
|
83
|
-
const message: Message = JSON.parse(decryptedMessageJson);
|
|
84
|
-
console.log('Decrypted message:', message);
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.error('Failed to decrypt message:', error);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
getOwnUserId(): string {
|
|
91
|
-
// In a real app, this would be a unique user ID.
|
|
92
|
-
// For this example, we'll use a hash of the public key.
|
|
93
|
-
const publicKey = this.keyManager.getPublicKey();
|
|
94
|
-
if (!publicKey) {
|
|
95
|
-
throw new Error('Keys not generated.');
|
|
96
|
-
}
|
|
97
|
-
return Buffer.from(publicKey).toString('hex');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
getOwnPublicKey(): Uint8Array | null {
|
|
101
|
-
return this.keyManager.getPublicKey();
|
|
102
|
-
}
|
|
103
|
-
}
|
package/src/crypto/keyManager.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import sodium from 'libsodium-wrappers';
|
|
3
|
-
|
|
4
|
-
export interface KeyPair {
|
|
5
|
-
publicKey: Uint8Array;
|
|
6
|
-
privateKey: Uint8Array;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class KeyManager {
|
|
10
|
-
private keyPair: KeyPair | null = null;
|
|
11
|
-
|
|
12
|
-
async generateKeys(): Promise<void> {
|
|
13
|
-
await sodium.ready;
|
|
14
|
-
const keys = sodium.crypto_box_keypair();
|
|
15
|
-
this.keyPair = {
|
|
16
|
-
publicKey: keys.publicKey,
|
|
17
|
-
privateKey: keys.privateKey,
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
getPublicKey(): Uint8Array | null {
|
|
22
|
-
return this.keyPair ? this.keyPair.publicKey : null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
getPrivateKey(): Uint8Array | null {
|
|
26
|
-
return this.keyPair ? this.keyPair.privateKey : null;
|
|
27
|
-
}
|
|
28
|
-
}
|