chatly-sdk 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CONTRIBUTING.md +658 -0
  2. package/IMPROVEMENTS.md +402 -0
  3. package/README.md +1538 -164
  4. package/dist/index.d.ts +430 -9
  5. package/dist/index.js +1420 -63
  6. package/examples/01-basic-chat/README.md +61 -0
  7. package/examples/01-basic-chat/index.js +58 -0
  8. package/examples/01-basic-chat/package.json +13 -0
  9. package/examples/02-group-chat/README.md +78 -0
  10. package/examples/02-group-chat/index.js +76 -0
  11. package/examples/02-group-chat/package.json +13 -0
  12. package/examples/03-offline-messaging/README.md +73 -0
  13. package/examples/03-offline-messaging/index.js +80 -0
  14. package/examples/03-offline-messaging/package.json +13 -0
  15. package/examples/04-live-chat/README.md +80 -0
  16. package/examples/04-live-chat/index.js +114 -0
  17. package/examples/04-live-chat/package.json +13 -0
  18. package/examples/05-hybrid-messaging/README.md +71 -0
  19. package/examples/05-hybrid-messaging/index.js +106 -0
  20. package/examples/05-hybrid-messaging/package.json +13 -0
  21. package/examples/06-postgresql-integration/README.md +101 -0
  22. package/examples/06-postgresql-integration/adapters/groupStore.js +73 -0
  23. package/examples/06-postgresql-integration/adapters/messageStore.js +47 -0
  24. package/examples/06-postgresql-integration/adapters/userStore.js +40 -0
  25. package/examples/06-postgresql-integration/index.js +92 -0
  26. package/examples/06-postgresql-integration/package.json +14 -0
  27. package/examples/06-postgresql-integration/schema.sql +58 -0
  28. package/examples/08-customer-support/README.md +70 -0
  29. package/examples/08-customer-support/index.js +104 -0
  30. package/examples/08-customer-support/package.json +13 -0
  31. package/examples/README.md +105 -0
  32. package/jest.config.cjs +28 -0
  33. package/package.json +12 -8
  34. package/src/chat/ChatSession.ts +81 -0
  35. package/src/chat/GroupSession.ts +79 -0
  36. package/src/constants.ts +61 -0
  37. package/src/crypto/e2e.ts +0 -20
  38. package/src/index.ts +525 -63
  39. package/src/models/mediaTypes.ts +58 -0
  40. package/src/models/message.ts +4 -1
  41. package/src/transport/adapters.ts +51 -1
  42. package/src/transport/memoryTransport.ts +75 -13
  43. package/src/transport/websocketClient.ts +269 -21
  44. package/src/transport/websocketServer.ts +26 -26
  45. package/src/utils/errors.ts +97 -0
  46. package/src/utils/logger.ts +96 -0
  47. package/src/utils/mediaUtils.ts +235 -0
  48. package/src/utils/messageQueue.ts +162 -0
  49. package/src/utils/validation.ts +99 -0
  50. package/test/crypto.test.ts +122 -35
  51. package/test/sdk.test.ts +276 -0
  52. package/test/validation.test.ts +64 -0
  53. package/tsconfig.json +11 -10
  54. package/tsconfig.test.json +11 -0
  55. package/src/ChatManager.ts +0 -103
  56. package/src/crypto/keyManager.ts +0 -28
@@ -1,45 +1,132 @@
1
+ import { encryptMessage, decryptMessage, deriveSharedSecret } from '../src/crypto/e2e';
2
+ import { generateIdentityKeyPair } from '../src/crypto/keys';
1
3
 
2
- import assert from 'assert';
3
- import { KeyManager } from '../src/crypto/keyManager';
4
- import { encrypt } from '../src/crypto/encrypt';
5
- import { decrypt } from '../src/crypto/decrypt';
4
+ describe('End-to-End Encryption', () => {
5
+ describe('Key Generation', () => {
6
+ it('should generate a valid key pair', () => {
7
+ const keyPair = generateIdentityKeyPair();
8
+
9
+ expect(keyPair).toHaveProperty('publicKey');
10
+ expect(keyPair).toHaveProperty('privateKey');
11
+ expect(typeof keyPair.publicKey).toBe('string');
12
+ expect(typeof keyPair.privateKey).toBe('string');
13
+ expect(keyPair.publicKey.length).toBeGreaterThan(0);
14
+ expect(keyPair.privateKey.length).toBeGreaterThan(0);
15
+ });
6
16
 
7
- async function testCrypto() {
8
- console.log('Running crypto tests...');
17
+ it('should generate unique key pairs', () => {
18
+ const keyPair1 = generateIdentityKeyPair();
19
+ const keyPair2 = generateIdentityKeyPair();
20
+
21
+ expect(keyPair1.publicKey).not.toBe(keyPair2.publicKey);
22
+ expect(keyPair1.privateKey).not.toBe(keyPair2.privateKey);
23
+ });
24
+ });
9
25
 
10
- // Test KeyManager
11
- const keyManager1 = new KeyManager();
12
- const keyManager2 = new KeyManager();
26
+ describe('Shared Secret Derivation', () => {
27
+ it('should derive the same shared secret for both parties', async () => {
28
+ const aliceKeys = generateIdentityKeyPair();
29
+ const bobKeys = generateIdentityKeyPair();
30
+
31
+ const aliceShared = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
32
+ const bobShared = await deriveSharedSecret(bobKeys.privateKey, aliceKeys.publicKey);
33
+
34
+ expect(aliceShared).toBe(bobShared);
35
+ });
13
36
 
14
- await keyManager1.generateKeys();
15
- await keyManager2.generateKeys();
37
+ it('should derive different secrets for different key pairs', async () => {
38
+ const aliceKeys = generateIdentityKeyPair();
39
+ const bobKeys = generateIdentityKeyPair();
40
+ const charlieKeys = generateIdentityKeyPair();
41
+
42
+ const aliceBobSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
43
+ const aliceCharlieSecret = await deriveSharedSecret(aliceKeys.privateKey, charlieKeys.publicKey);
44
+
45
+ expect(aliceBobSecret).not.toBe(aliceCharlieSecret);
46
+ });
47
+ });
16
48
 
17
- const publicKey1 = keyManager1.getPublicKey();
18
- const privateKey1 = keyManager1.getPrivateKey();
19
- const publicKey2 = keyManager2.getPublicKey();
20
- const privateKey2 = keyManager2.getPrivateKey();
49
+ describe('Message Encryption and Decryption', () => {
50
+ it('should encrypt and decrypt a message correctly', async () => {
51
+ const aliceKeys = generateIdentityKeyPair();
52
+ const bobKeys = generateIdentityKeyPair();
53
+ const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
54
+
55
+ const plaintext = 'Hello, Bob!';
56
+ const encrypted = await encryptMessage(plaintext, sharedSecret);
57
+
58
+ expect(encrypted).toHaveProperty('ciphertext');
59
+ expect(encrypted).toHaveProperty('iv');
60
+ expect(typeof encrypted.ciphertext).toBe('string');
61
+ expect(typeof encrypted.iv).toBe('string');
62
+
63
+ const decrypted = await decryptMessage(encrypted.ciphertext, encrypted.iv, sharedSecret);
64
+ expect(decrypted).toBe(plaintext);
65
+ });
21
66
 
22
- assert(publicKey1, 'publicKey1 should not be null');
23
- assert(privateKey1, 'privateKey1 should not be null');
24
- assert(publicKey2, 'publicKey2 should not be null');
25
- assert(privateKey2, 'privateKey2 should not be null');
67
+ it('should produce different ciphertexts for the same plaintext', async () => {
68
+ const aliceKeys = generateIdentityKeyPair();
69
+ const bobKeys = generateIdentityKeyPair();
70
+ const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
71
+
72
+ const plaintext = 'Hello, Bob!';
73
+ const encrypted1 = await encryptMessage(plaintext, sharedSecret);
74
+ const encrypted2 = await encryptMessage(plaintext, sharedSecret);
75
+
76
+ expect(encrypted1.ciphertext).not.toBe(encrypted2.ciphertext);
77
+ expect(encrypted1.iv).not.toBe(encrypted2.iv);
78
+ });
26
79
 
27
- console.log('KeyManager test passed.');
80
+ it('should fail to decrypt with wrong shared secret', async () => {
81
+ const aliceKeys = generateIdentityKeyPair();
82
+ const bobKeys = generateIdentityKeyPair();
83
+ const charlieKeys = generateIdentityKeyPair();
84
+
85
+ const aliceBobSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
86
+ const aliceCharlieSecret = await deriveSharedSecret(aliceKeys.privateKey, charlieKeys.publicKey);
87
+
88
+ const plaintext = 'Secret message';
89
+ const encrypted = await encryptMessage(plaintext, aliceBobSecret);
90
+
91
+ await expect(
92
+ decryptMessage(encrypted.ciphertext, encrypted.iv, aliceCharlieSecret)
93
+ ).rejects.toThrow();
94
+ });
28
95
 
29
- // Test encrypt and decrypt
30
- const message = 'This is a secret message.';
31
- if (publicKey1 && privateKey1 && publicKey2 && privateKey2) {
32
- const encryptedMessage = await encrypt(message, publicKey2, privateKey1);
33
- const decryptedMessage = await decrypt(encryptedMessage, publicKey1, privateKey2);
96
+ it('should handle empty messages', async () => {
97
+ const aliceKeys = generateIdentityKeyPair();
98
+ const bobKeys = generateIdentityKeyPair();
99
+ const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
100
+
101
+ const plaintext = '';
102
+ const encrypted = await encryptMessage(plaintext, sharedSecret);
103
+ const decrypted = await decryptMessage(encrypted.ciphertext, encrypted.iv, sharedSecret);
104
+
105
+ expect(decrypted).toBe(plaintext);
106
+ });
34
107
 
35
- assert.strictEqual(decryptedMessage, message, 'Decrypted message should match original message.');
36
- } else {
37
- assert.fail('Keys should not be null');
38
- }
108
+ it('should handle long messages', async () => {
109
+ const aliceKeys = generateIdentityKeyPair();
110
+ const bobKeys = generateIdentityKeyPair();
111
+ const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
112
+
113
+ const plaintext = 'A'.repeat(10000);
114
+ const encrypted = await encryptMessage(plaintext, sharedSecret);
115
+ const decrypted = await decryptMessage(encrypted.ciphertext, encrypted.iv, sharedSecret);
116
+
117
+ expect(decrypted).toBe(plaintext);
118
+ });
39
119
 
40
- console.log('Encrypt/decrypt test passed.');
41
-
42
- console.log('All crypto tests passed.');
43
- }
44
-
45
- testCrypto().catch(console.error);
120
+ it('should handle special characters and emojis', async () => {
121
+ const aliceKeys = generateIdentityKeyPair();
122
+ const bobKeys = generateIdentityKeyPair();
123
+ const sharedSecret = await deriveSharedSecret(aliceKeys.privateKey, bobKeys.publicKey);
124
+
125
+ const plaintext = 'Hello 👋 World! 🌍 Special chars: @#$%^&*()';
126
+ const encrypted = await encryptMessage(plaintext, sharedSecret);
127
+ const decrypted = await decryptMessage(encrypted.ciphertext, encrypted.iv, sharedSecret);
128
+
129
+ expect(decrypted).toBe(plaintext);
130
+ });
131
+ });
132
+ });
@@ -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": ["node"],
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
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "module": "commonjs",
5
+ "esModuleInterop": true
6
+ },
7
+ "include": [
8
+ "test/**/*.ts",
9
+ "src/**/*.ts"
10
+ ]
11
+ }
@@ -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
- }