chatly-sdk 0.0.5 → 0.0.7
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/README.md +1538 -164
- package/dist/index.d.ts +430 -9
- package/dist/index.js +1420 -63
- 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 +12 -8
- package/src/chat/ChatSession.ts +81 -0
- package/src/chat/GroupSession.ts +79 -0
- package/src/constants.ts +61 -0
- package/src/crypto/e2e.ts +0 -20
- package/src/index.ts +525 -63
- package/src/models/mediaTypes.ts +58 -0
- package/src/models/message.ts +4 -1
- 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/crypto.test.ts
CHANGED
|
@@ -1,45 +1,132 @@
|
|
|
1
|
+
import { encryptMessage, decryptMessage, deriveSharedSecret } from '../src/crypto/e2e';
|
|
2
|
+
import { generateIdentityKeyPair } from '../src/crypto/keys';
|
|
1
3
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
});
|
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
|
-
}
|