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,104 @@
1
+ import {
2
+ ChatSDK,
3
+ InMemoryUserStore,
4
+ InMemoryMessageStore,
5
+ InMemoryGroupStore,
6
+ MemoryTransport,
7
+ EVENTS,
8
+ LogLevel
9
+ } from 'chatly-sdk';
10
+
11
+ async function main() {
12
+ console.log('🎧 Chatly SDK - Customer Support Example');
13
+ console.log('========================================\n');
14
+
15
+ // Initialize support system
16
+ const transport = new MemoryTransport();
17
+ const sdk = new ChatSDK({
18
+ userStore: new InMemoryUserStore(),
19
+ messageStore: new InMemoryMessageStore(),
20
+ groupStore: new InMemoryGroupStore(),
21
+ transport,
22
+ logLevel: LogLevel.NONE,
23
+ });
24
+
25
+ console.log('Setting up support system...');
26
+
27
+ // Create support agent and customer
28
+ const agent = await sdk.createUser('support-agent');
29
+ const customer = await sdk.createUser('customer-john');
30
+ console.log('✅ Support agent created');
31
+ console.log('✅ Customer created\n');
32
+
33
+ // Create support session
34
+ const session = await sdk.startSession(customer, agent);
35
+
36
+ // Scenario 1: Customer sends message (agent offline)
37
+ console.log('Scenario 1: Customer sends message (agent offline)');
38
+ sdk.setCurrentUser(customer);
39
+
40
+ await sdk.sendMessage(session, 'I need help with my order');
41
+ console.log('📝 Customer: I need help with my order');
42
+ console.log('⏳ Message queued (no agents available)\n');
43
+
44
+ await new Promise(resolve => setTimeout(resolve, 1000));
45
+
46
+ // Scenario 2: Agent comes online and sees pending messages
47
+ console.log('Scenario 2: Agent comes online');
48
+ sdk.setCurrentUser(agent);
49
+ await sdk.setCurrentUser(agent); // Connect agent
50
+
51
+ console.log('🟢 Agent online');
52
+
53
+ const pendingMessages = await sdk.getMessagesForUser(agent.id);
54
+ console.log(`📬 Agent has ${pendingMessages.length} pending message(s)`);
55
+
56
+ for (const msg of pendingMessages) {
57
+ const text = await sdk.decryptMessage(msg, agent);
58
+ console.log(`📨 Agent sees: ${text}`);
59
+ }
60
+ console.log();
61
+
62
+ // Scenario 3: Live chat (both online)
63
+ console.log('Scenario 3: Live chat (both online)');
64
+ console.log('💬 Real-time conversation started\n');
65
+
66
+ // Set up real-time event listeners
67
+ sdk.on(EVENTS.MESSAGE_RECEIVED, async (message) => {
68
+ const currentUser = sdk.getCurrentUser();
69
+ if (currentUser) {
70
+ const plaintext = await sdk.decryptMessage(message, currentUser);
71
+ console.log(`📨 ${currentUser.username} received: ${plaintext}`);
72
+ }
73
+ });
74
+
75
+ // Agent responds
76
+ sdk.setCurrentUser(agent);
77
+ await sdk.sendMessage(session, 'Hi! How can I help?');
78
+ console.log('📤 Agent: Hi! How can I help?');
79
+ await new Promise(resolve => setTimeout(resolve, 300));
80
+
81
+ // Customer replies
82
+ sdk.setCurrentUser(customer);
83
+ await sdk.sendMessage(session, 'My order #12345 is delayed');
84
+ console.log('📤 Customer: My order #12345 is delayed');
85
+ await new Promise(resolve => setTimeout(resolve, 300));
86
+
87
+ // Agent helps
88
+ sdk.setCurrentUser(agent);
89
+ await sdk.sendMessage(session, 'Let me check that for you');
90
+ console.log('📤 Agent: Let me check that for you');
91
+ await new Promise(resolve => setTimeout(resolve, 300));
92
+
93
+ console.log();
94
+ console.log('✅ Support system works perfectly!');
95
+ console.log('\n💡 Key Features:');
96
+ console.log(' - 24/7 availability (customers message anytime)');
97
+ console.log(' - Offline queue (messages saved for agents)');
98
+ console.log(' - Real-time chat when both online');
99
+ console.log(' - Full conversation history');
100
+
101
+ await sdk.disconnect();
102
+ }
103
+
104
+ main().catch(console.error);
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "chatly-sdk-customer-support-example",
3
+ "version": "1.0.0",
4
+ "description": "Customer support chat system example using Chatly SDK",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "start": "node index.js"
9
+ },
10
+ "dependencies": {
11
+ "chatly-sdk": "^0.0.5"
12
+ }
13
+ }
@@ -0,0 +1,105 @@
1
+ # Chatly SDK - Examples
2
+
3
+ This directory contains practical examples demonstrating different use cases for the Chatly SDK.
4
+
5
+ ## 📚 Examples
6
+
7
+ ### 1. [Basic Chat](./01-basic-chat)
8
+ **Difficulty**: Beginner
9
+ **Topics**: User creation, sessions, encryption, message sending
10
+
11
+ Simple 1:1 encrypted chat between two users.
12
+
13
+ ---
14
+
15
+ ### 2. [Group Chat](./02-group-chat)
16
+ **Difficulty**: Beginner
17
+ **Topics**: Group creation, multi-user messaging, group encryption
18
+
19
+ Multi-user encrypted group messaging with 3+ participants.
20
+
21
+ ---
22
+
23
+ ### 3. [Offline Messaging](./03-offline-messaging)
24
+ Asynchronous messaging without WebSocket
25
+
26
+ ### 2. **Real-time Examples**
27
+ - [`04-live-chat/`](./04-live-chat/) - Live chat with WebSocket (WhatsApp-style)
28
+ - [`05-hybrid-messaging/`](./05-hybrid-messaging/) - Hybrid online/offline messaging
29
+
30
+ ### 3. **Database Integration**
31
+ - [`06-postgresql-integration/`](./06-postgresql-integration/) - PostgreSQL storage adapter
32
+ - [`07-mongodb-integration/`](./07-mongodb-integration/) - MongoDB storage adapter
33
+
34
+ ### 4. **Real-world Applications**
35
+ - [`08-customer-support/`](./08-customer-support/) - Customer support chat system
36
+ - [`09-react-chat-app/`](./09-react-chat-app/) - Full React chat application
37
+ - [`10-websocket-server/`](./10-websocket-server/) - Node.js WebSocket server
38
+
39
+ ## 🚀 Quick Start
40
+
41
+ Each example includes:
42
+ - ✅ Complete source code
43
+ - ✅ README with setup instructions
44
+ - ✅ package.json for dependencies
45
+ - ✅ Comments explaining key concepts
46
+
47
+ ### Running an Example
48
+
49
+ ```bash
50
+ # Navigate to an example
51
+ cd examples/01-basic-chat
52
+
53
+ # Install dependencies
54
+ npm install
55
+
56
+ # Run the example
57
+ npm start
58
+ ```
59
+
60
+ ## 📚 Learning Path
61
+
62
+ **Beginner**: Start here
63
+ 1. `01-basic-chat` - Learn the basics
64
+ 2. `02-group-chat` - Understand group messaging
65
+ 3. `03-offline-messaging` - See how offline works
66
+
67
+ **Intermediate**: Real-time features
68
+ 4. `04-live-chat` - Add WebSocket support
69
+ 5. `05-hybrid-messaging` - Combine online/offline
70
+
71
+ **Advanced**: Production setup
72
+ 6. `06-postgresql-integration` - Database persistence
73
+ 7. `08-customer-support` - Real-world application
74
+ 8. `09-react-chat-app` - Full frontend integration
75
+
76
+ ## 🎯 Use Case Guide
77
+
78
+ | Use Case | Example | Description |
79
+ |----------|---------|-------------|
80
+ | **Messaging App** | `04-live-chat` | WhatsApp-style real-time chat |
81
+ | **Customer Support** | `08-customer-support` | Live support with offline fallback |
82
+ | **Team Collaboration** | `02-group-chat` + `04-live-chat` | Slack-style team chat |
83
+ | **Social Media DMs** | `05-hybrid-messaging` | Instagram-style direct messages |
84
+ | **Email-like System** | `03-offline-messaging` | Asynchronous messaging |
85
+
86
+ ## 💡 Key Concepts Demonstrated
87
+
88
+ - ✅ **End-to-end encryption** - All examples use E2E encryption
89
+ - ✅ **Message queue** - Automatic retry and offline support
90
+ - ✅ **Event handling** - Real-time UI updates
91
+ - ✅ **Database integration** - PostgreSQL and MongoDB
92
+ - ✅ **WebSocket** - Real-time bidirectional communication
93
+ - ✅ **React integration** - Hooks and context providers
94
+
95
+ ## 🔧 Prerequisites
96
+
97
+ - Node.js >= 16.x
98
+ - npm >= 8.x
99
+ - (Optional) PostgreSQL or MongoDB for database examples
100
+
101
+ ## 📞 Need Help?
102
+
103
+ - Check the main [README](../README.md)
104
+ - Read [CONTRIBUTING.md](../CONTRIBUTING.md)
105
+ - Open an [issue](https://github.com/bharath-arch/chatly-sdk/issues)
@@ -0,0 +1,28 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/test'],
5
+ testMatch: ['**/*.test.ts'],
6
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
7
+ transform: {
8
+ '^.+\\.ts$': ['ts-jest', {
9
+ tsconfig: 'tsconfig.test.json',
10
+ }],
11
+ },
12
+ collectCoverageFrom: [
13
+ 'src/**/*.ts',
14
+ '!src/**/*.d.ts',
15
+ '!src/**/index.ts',
16
+ ],
17
+ coverageThreshold: {
18
+ global: {
19
+ branches: 70,
20
+ functions: 70,
21
+ lines: 70,
22
+ statements: 70,
23
+ },
24
+ },
25
+ moduleNameMapper: {
26
+ '^(\\.{1,2}/.*)\\.js$': '$1',
27
+ },
28
+ };
package/package.json CHANGED
@@ -1,19 +1,23 @@
1
1
  {
2
2
  "name": "chatly-sdk",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsup src/index.ts --dts --format esm",
9
9
  "start": "ts-node src/index.ts",
10
- "test": "ts-node test/crypto.test.ts"
10
+ "test": "jest",
11
+ "test:watch": "jest --watch",
12
+ "test:coverage": "jest --coverage"
11
13
  },
12
-
13
14
  "publishConfig": {
14
15
  "access": "public"
15
16
  },
16
-
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/bharath-arch/chatly-sdk.git"
20
+ },
17
21
  "keywords": [
18
22
  "chat",
19
23
  "sdk",
@@ -23,13 +27,18 @@
23
27
  "whatsapp"
24
28
  ],
25
29
  "author": "",
26
- "license": "ISC",
30
+ "license": "MIT",
27
31
  "description": "Production-ready end-to-end encrypted chat SDK with WhatsApp-style features",
28
32
  "dependencies": {
29
- "buffer": "^6.0.3"
33
+ "@aws-sdk/client-s3": "^3.958.0",
34
+ "buffer": "^6.0.3",
35
+ "events": "^3.3.0"
30
36
  },
31
37
  "devDependencies": {
38
+ "@types/jest": "^29.5.14",
32
39
  "@types/node": "^24.10.1",
40
+ "jest": "^29.5.0",
41
+ "ts-jest": "^29.1.0",
33
42
  "ts-node": "^10.9.2",
34
43
  "tsup": "^8.0.0",
35
44
  "typescript": "^5.9.3"
@@ -1,8 +1,11 @@
1
1
  import type { User } from "../models/user.js";
2
2
  import type { Message } from "../models/message.js";
3
- import { deriveSharedSecret, encryptMessage, decryptMessage } from "../crypto/e2e.js";
3
+ import type { MediaAttachment } from "../models/mediaTypes.js";
4
+ import { deriveSharedSecret, deriveLegacySharedSecret, encryptMessage, decryptMessage } from "../crypto/e2e.js";
4
5
  import type { KeyPair } from "../crypto/keys.js";
5
6
  import { generateUUID } from "../crypto/uuid.js";
7
+ import type { StorageProvider } from "../storage/adapters.js";
8
+ import { logger } from "../utils/logger.js";
6
9
 
7
10
  export class ChatSession {
8
11
  private sharedSecret: Buffer | null = null;
@@ -11,7 +14,8 @@ export class ChatSession {
11
14
  constructor(
12
15
  public readonly id: string,
13
16
  public readonly userA: User,
14
- public readonly userB: User
17
+ public readonly userB: User,
18
+ private storageProvider?: StorageProvider
15
19
  ) {}
16
20
 
17
21
  /**
@@ -40,6 +44,13 @@ export class ChatSession {
40
44
  privateKey: user.privateKey,
41
45
  };
42
46
 
47
+ logger.debug(`[ChatSession] Initializing for user ${user.id}`, {
48
+ hasLocalPriv: !!user.privateKey,
49
+ privType: typeof user.privateKey,
50
+ hasRemotePub: !!otherUser.publicKey,
51
+ pubType: typeof otherUser.publicKey
52
+ });
53
+
43
54
  this.sharedSecret = deriveSharedSecret(localKeyPair, otherUser.publicKey);
44
55
  }
45
56
 
@@ -68,6 +79,65 @@ export class ChatSession {
68
79
  };
69
80
  }
70
81
 
82
+ /**
83
+ * Encrypt a media message for this session
84
+ */
85
+ async encryptMedia(
86
+ plaintext: string,
87
+ media: MediaAttachment,
88
+ senderId: string
89
+ ): Promise<Message> {
90
+ if (!this.sharedSecret) {
91
+ await this.initialize();
92
+ }
93
+
94
+ if (!this.sharedSecret) {
95
+ throw new Error("Failed to initialize session");
96
+ }
97
+
98
+ // Encrypt the message text (could be caption)
99
+ const { ciphertext, iv } = encryptMessage(plaintext, this.sharedSecret);
100
+
101
+ // Encrypt the media data with its own IV
102
+ const { ciphertext: encryptedMediaData, iv: mediaIv } = encryptMessage(
103
+ media.data || "",
104
+ this.sharedSecret
105
+ );
106
+
107
+ // Create encrypted media attachment
108
+ const encryptedMedia: MediaAttachment = {
109
+ ...media,
110
+ data: encryptedMediaData,
111
+ iv: mediaIv,
112
+ };
113
+
114
+ // If storage provider is available, upload the encrypted data
115
+ if (this.storageProvider) {
116
+ const filename = `${this.id}/${generateUUID()}-${media.metadata.filename}`;
117
+ const uploadResult = await this.storageProvider.upload(
118
+ encryptedMediaData,
119
+ filename,
120
+ media.metadata.mimeType
121
+ );
122
+
123
+ encryptedMedia.storage = this.storageProvider.name as 'local' | 's3';
124
+ encryptedMedia.storageKey = uploadResult.storageKey;
125
+ encryptedMedia.url = uploadResult.url;
126
+ encryptedMedia.data = undefined; // Remove data from attachment if stored remotely
127
+ }
128
+
129
+ return {
130
+ id: generateUUID(),
131
+ senderId,
132
+ receiverId: senderId === this.userA.id ? this.userB.id : this.userA.id,
133
+ ciphertext,
134
+ iv,
135
+ timestamp: Date.now(),
136
+ type: "media",
137
+ media: encryptedMedia,
138
+ };
139
+ }
140
+
71
141
  /**
72
142
  * Decrypt a message in this session
73
143
  */
@@ -83,6 +153,93 @@ export class ChatSession {
83
153
  throw new Error("Failed to initialize session");
84
154
  }
85
155
 
86
- return decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
156
+ try {
157
+ return decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
158
+ } catch (error) {
159
+ // Fallback for legacy messages (before salt logic)
160
+ const legacySecret = this.deriveLegacySecret(user);
161
+ try {
162
+ return decryptMessage(message.ciphertext, message.iv, legacySecret);
163
+ } catch (innerError) {
164
+ throw error; // Throw original error if fallback also fails
165
+ }
166
+ }
167
+ }
168
+
169
+ private deriveLegacySecret(user: User): Buffer {
170
+ const otherUser = user.id === this.userA.id ? this.userB : this.userA;
171
+ logger.debug(`[ChatSession] Deriving legacy secret for user ${user.id}`, {
172
+ hasPriv: !!user.privateKey,
173
+ privType: typeof user.privateKey,
174
+ remotePubType: typeof otherUser.publicKey
175
+ });
176
+ const localKeyPair: KeyPair = {
177
+ publicKey: user.publicKey,
178
+ privateKey: user.privateKey,
179
+ };
180
+ return deriveLegacySharedSecret(localKeyPair, otherUser.publicKey);
181
+ }
182
+
183
+ /**
184
+ * Decrypt a media message in this session
185
+ */
186
+ async decryptMedia(message: Message, user: User): Promise<{ text: string; media: MediaAttachment }> {
187
+ if (!message.media) {
188
+ throw new Error("Message does not contain media");
189
+ }
190
+
191
+ // Re-initialize if needed
192
+ if (!this.sharedSecret ||
193
+ (user.id !== this.userA.id && user.id !== this.userB.id)) {
194
+ await this.initializeForUser(user);
195
+ }
196
+
197
+ if (!this.sharedSecret) {
198
+ throw new Error("Failed to initialize session");
199
+ }
200
+
201
+ // Decrypt the message text
202
+ const text = decryptMessage(message.ciphertext, message.iv, this.sharedSecret);
203
+
204
+ let encryptedMediaData = message.media.data;
205
+
206
+ // If data is missing but storageKey is present, download it
207
+ if (!encryptedMediaData && message.media.storageKey && this.storageProvider) {
208
+ encryptedMediaData = await this.storageProvider.download(message.media.storageKey);
209
+ }
210
+
211
+ // Decrypt the media data using its own IV
212
+ if (!message.media.iv && !encryptedMediaData) {
213
+ throw new Error("Media data or IV missing");
214
+ }
215
+
216
+ let decryptedMediaData: string;
217
+ try {
218
+ decryptedMediaData = decryptMessage(
219
+ encryptedMediaData || "",
220
+ message.media.iv || message.iv,
221
+ this.sharedSecret
222
+ );
223
+ } catch (error) {
224
+ // Fallback for legacy media
225
+ const legacySecret = this.deriveLegacySecret(user);
226
+ try {
227
+ decryptedMediaData = decryptMessage(
228
+ encryptedMediaData || "",
229
+ message.media.iv || message.iv,
230
+ legacySecret
231
+ );
232
+ } catch (innerError) {
233
+ throw error;
234
+ }
235
+ }
236
+
237
+ // Create decrypted media attachment
238
+ const decryptedMedia: MediaAttachment = {
239
+ ...message.media,
240
+ data: decryptedMediaData,
241
+ };
242
+
243
+ return { text, media: decryptedMedia };
87
244
  }
88
245
  }
@@ -1,14 +1,16 @@
1
1
  import type { Group } from "../models/group.js";
2
2
  import type { Message } from "../models/message.js";
3
+ import type { MediaAttachment } from "../models/mediaTypes.js";
3
4
  import { deriveGroupKey } from "../crypto/group.js";
4
5
  import { encryptMessage, decryptMessage } from "../crypto/e2e.js";
5
6
  import { base64ToBuffer } from "../crypto/utils.js";
6
7
  import { generateUUID } from "../crypto/uuid.js";
8
+ import type { StorageProvider } from "../storage/adapters.js";
7
9
 
8
10
  export class GroupSession {
9
11
  private groupKey: Buffer | null = null;
10
12
 
11
- constructor(public readonly group: Group) {}
13
+ constructor(public readonly group: Group, private storageProvider?: StorageProvider) {}
12
14
 
13
15
  /**
14
16
  * Initialize the session by deriving the group key
@@ -43,6 +45,65 @@ export class GroupSession {
43
45
  };
44
46
  }
45
47
 
48
+ /**
49
+ * Encrypt a media message for this group
50
+ */
51
+ async encryptMedia(
52
+ plaintext: string,
53
+ media: MediaAttachment,
54
+ senderId: string
55
+ ): Promise<Message> {
56
+ if (!this.groupKey) {
57
+ await this.initialize();
58
+ }
59
+
60
+ if (!this.groupKey) {
61
+ throw new Error("Failed to initialize group session");
62
+ }
63
+
64
+ // Encrypt the message text (could be caption)
65
+ const { ciphertext, iv } = encryptMessage(plaintext, this.groupKey);
66
+
67
+ // Encrypt the media data with its own IV
68
+ const { ciphertext: encryptedMediaData, iv: mediaIv } = encryptMessage(
69
+ media.data || "",
70
+ this.groupKey
71
+ );
72
+
73
+ // Create encrypted media attachment
74
+ const encryptedMedia: MediaAttachment = {
75
+ ...media,
76
+ data: encryptedMediaData,
77
+ iv: mediaIv,
78
+ };
79
+
80
+ // If storage provider is available, upload the encrypted data
81
+ if (this.storageProvider) {
82
+ const filename = `groups/${this.group.id}/${generateUUID()}-${media.metadata.filename}`;
83
+ const uploadResult = await this.storageProvider.upload(
84
+ encryptedMediaData,
85
+ filename,
86
+ media.metadata.mimeType
87
+ );
88
+
89
+ encryptedMedia.storage = this.storageProvider.name as 'local' | 's3';
90
+ encryptedMedia.storageKey = uploadResult.storageKey;
91
+ encryptedMedia.url = uploadResult.url;
92
+ encryptedMedia.data = undefined; // Remove data from attachment if stored remotely
93
+ }
94
+
95
+ return {
96
+ id: generateUUID(),
97
+ senderId,
98
+ groupId: this.group.id,
99
+ ciphertext,
100
+ iv,
101
+ timestamp: Date.now(),
102
+ type: "media",
103
+ media: encryptedMedia,
104
+ };
105
+ }
106
+
46
107
  /**
47
108
  * Decrypt a message in this group
48
109
  */
@@ -57,4 +118,50 @@ export class GroupSession {
57
118
 
58
119
  return decryptMessage(message.ciphertext, message.iv, this.groupKey);
59
120
  }
121
+
122
+ /**
123
+ * Decrypt a media message in this group
124
+ */
125
+ async decryptMedia(message: Message): Promise<{ text: string; media: MediaAttachment }> {
126
+ if (!message.media) {
127
+ throw new Error("Message does not contain media");
128
+ }
129
+
130
+ if (!this.groupKey) {
131
+ await this.initialize();
132
+ }
133
+
134
+ if (!this.groupKey) {
135
+ throw new Error("Failed to initialize group session");
136
+ }
137
+
138
+ // Decrypt the message text
139
+ const text = decryptMessage(message.ciphertext, message.iv, this.groupKey);
140
+
141
+ let encryptedMediaData = message.media.data;
142
+
143
+ // If data is missing but storageKey is present, download it
144
+ if (!encryptedMediaData && message.media.storageKey && this.storageProvider) {
145
+ encryptedMediaData = await this.storageProvider.download(message.media.storageKey);
146
+ }
147
+
148
+ // Decrypt the media data using its own IV
149
+ if (!message.media.iv && !encryptedMediaData) {
150
+ throw new Error("Media data or IV missing");
151
+ }
152
+
153
+ const decryptedMediaData = decryptMessage(
154
+ encryptedMediaData || "",
155
+ message.media.iv || message.iv, // Fallback to message IV for backward compatibility
156
+ this.groupKey
157
+ );
158
+
159
+ // Create decrypted media attachment
160
+ const decryptedMedia: MediaAttachment = {
161
+ ...message.media,
162
+ data: decryptedMediaData,
163
+ };
164
+
165
+ return { text, media: decryptedMedia };
166
+ }
60
167
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * SDK Configuration Constants
3
+ */
4
+
5
+ // Cryptography
6
+ export const SUPPORTED_CURVE = "prime256v1";
7
+ export const ALGORITHM = "aes-256-gcm";
8
+ export const IV_LENGTH = 12; // 96 bits for GCM
9
+ export const SALT_LENGTH = 16;
10
+ export const KEY_LENGTH = 32; // 256 bits
11
+ export const TAG_LENGTH = 16;
12
+ export const PBKDF2_ITERATIONS = 100000;
13
+
14
+ // Validation
15
+ export const USERNAME_MIN_LENGTH = 3;
16
+ export const USERNAME_MAX_LENGTH = 20;
17
+ export const MESSAGE_MAX_LENGTH = 10000;
18
+ export const GROUP_NAME_MAX_LENGTH = 100;
19
+ export const GROUP_MIN_MEMBERS = 2;
20
+ export const GROUP_MAX_MEMBERS = 256;
21
+
22
+ // Transport
23
+ export const RECONNECT_MAX_ATTEMPTS = 5;
24
+ export const RECONNECT_BASE_DELAY = 1000; // 1 second
25
+ export const RECONNECT_MAX_DELAY = 30000; // 30 seconds
26
+ export const HEARTBEAT_INTERVAL = 30000; // 30 seconds
27
+ export const CONNECTION_TIMEOUT = 10000; // 10 seconds
28
+
29
+ // Message Queue
30
+ export const MAX_QUEUE_SIZE = 1000;
31
+ export const MESSAGE_RETRY_ATTEMPTS = 3;
32
+ export const MESSAGE_RETRY_DELAY = 2000; // 2 seconds
33
+
34
+ // Events
35
+ export const EVENTS = {
36
+ MESSAGE_SENT: 'message:sent',
37
+ MESSAGE_RECEIVED: 'message:received',
38
+ MESSAGE_FAILED: 'message:failed',
39
+ CONNECTION_STATE_CHANGED: 'connection:state',
40
+ SESSION_CREATED: 'session:created',
41
+ GROUP_CREATED: 'group:created',
42
+ ERROR: 'error',
43
+ USER_CREATED: 'user:created',
44
+ } as const;
45
+
46
+ // Connection States
47
+ export enum ConnectionState {
48
+ DISCONNECTED = 'disconnected',
49
+ CONNECTING = 'connecting',
50
+ CONNECTED = 'connected',
51
+ RECONNECTING = 'reconnecting',
52
+ FAILED = 'failed',
53
+ }
54
+
55
+ // Message Status
56
+ export enum MessageStatus {
57
+ PENDING = 'pending',
58
+ SENT = 'sent',
59
+ DELIVERED = 'delivered',
60
+ FAILED = 'failed',
61
+ }