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.
Files changed (63) hide show
  1. package/CONTRIBUTING.md +658 -0
  2. package/IMPROVEMENTS.md +402 -0
  3. package/LICENSE +21 -0
  4. package/README.md +1576 -162
  5. package/dist/index.d.ts +502 -11
  6. package/dist/index.js +1619 -66
  7. package/examples/01-basic-chat/README.md +61 -0
  8. package/examples/01-basic-chat/index.js +58 -0
  9. package/examples/01-basic-chat/package.json +13 -0
  10. package/examples/02-group-chat/README.md +78 -0
  11. package/examples/02-group-chat/index.js +76 -0
  12. package/examples/02-group-chat/package.json +13 -0
  13. package/examples/03-offline-messaging/README.md +73 -0
  14. package/examples/03-offline-messaging/index.js +80 -0
  15. package/examples/03-offline-messaging/package.json +13 -0
  16. package/examples/04-live-chat/README.md +80 -0
  17. package/examples/04-live-chat/index.js +114 -0
  18. package/examples/04-live-chat/package.json +13 -0
  19. package/examples/05-hybrid-messaging/README.md +71 -0
  20. package/examples/05-hybrid-messaging/index.js +106 -0
  21. package/examples/05-hybrid-messaging/package.json +13 -0
  22. package/examples/06-postgresql-integration/README.md +101 -0
  23. package/examples/06-postgresql-integration/adapters/groupStore.js +73 -0
  24. package/examples/06-postgresql-integration/adapters/messageStore.js +47 -0
  25. package/examples/06-postgresql-integration/adapters/userStore.js +40 -0
  26. package/examples/06-postgresql-integration/index.js +92 -0
  27. package/examples/06-postgresql-integration/package.json +14 -0
  28. package/examples/06-postgresql-integration/schema.sql +58 -0
  29. package/examples/08-customer-support/README.md +70 -0
  30. package/examples/08-customer-support/index.js +104 -0
  31. package/examples/08-customer-support/package.json +13 -0
  32. package/examples/README.md +105 -0
  33. package/jest.config.cjs +28 -0
  34. package/package.json +15 -6
  35. package/src/chat/ChatSession.ts +160 -3
  36. package/src/chat/GroupSession.ts +108 -1
  37. package/src/constants.ts +61 -0
  38. package/src/crypto/e2e.ts +9 -20
  39. package/src/crypto/utils.ts +3 -1
  40. package/src/index.ts +530 -63
  41. package/src/models/mediaTypes.ts +62 -0
  42. package/src/models/message.ts +4 -1
  43. package/src/storage/adapters.ts +36 -0
  44. package/src/storage/localStorage.ts +49 -0
  45. package/src/storage/s3Storage.ts +84 -0
  46. package/src/stores/adapters.ts +2 -0
  47. package/src/stores/memory/messageStore.ts +8 -0
  48. package/src/transport/adapters.ts +51 -1
  49. package/src/transport/memoryTransport.ts +75 -13
  50. package/src/transport/websocketClient.ts +269 -21
  51. package/src/transport/websocketServer.ts +26 -26
  52. package/src/utils/errors.ts +97 -0
  53. package/src/utils/logger.ts +96 -0
  54. package/src/utils/mediaUtils.ts +235 -0
  55. package/src/utils/messageQueue.ts +162 -0
  56. package/src/utils/validation.ts +99 -0
  57. package/test/crypto.test.ts +122 -35
  58. package/test/sdk.test.ts +276 -0
  59. package/test/validation.test.ts +64 -0
  60. package/tsconfig.json +11 -10
  61. package/tsconfig.test.json +11 -0
  62. package/src/ChatManager.ts +0 -103
  63. package/src/crypto/keyManager.ts +0 -28
@@ -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
- }
@@ -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
- }