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.
- 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/README.md
CHANGED
|
@@ -1,74 +1,198 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ๐ Chatly SDK
|
|
2
|
+
|
|
3
|
+
Production-ready end-to-end encrypted chat SDK with WhatsApp-style features, event-driven architecture, and automatic reconnection.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/chatly-sdk)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## โจ Features
|
|
9
|
+
|
|
10
|
+
### ๐ Security
|
|
11
|
+
- **End-to-End Encryption** - ECDH (P-256) + AES-256-GCM
|
|
12
|
+
- **Per-User Identity Keys** - Unique cryptographic identity
|
|
13
|
+
- **Session-Based Encryption** - Secure 1:1 and group messaging
|
|
14
|
+
- **Input Validation** - Protection against injection attacks
|
|
15
|
+
|
|
16
|
+
### ๐ฌ Messaging
|
|
17
|
+
- **1:1 Chat** - Secure direct messaging
|
|
18
|
+
- **Group Chat** - Multi-user encrypted groups (2-256 members)
|
|
19
|
+
- **Message Queue** - Offline support with automatic retry
|
|
20
|
+
- **Delivery Tracking** - Message status (pending, sent, failed)
|
|
21
|
+
|
|
22
|
+
### ๐ Connectivity
|
|
23
|
+
- **Auto-Reconnection** - Exponential backoff (up to 5 attempts)
|
|
24
|
+
- **Heartbeat Monitoring** - Connection health checks
|
|
25
|
+
- **Connection States** - Disconnected, connecting, connected, reconnecting, failed
|
|
26
|
+
- **Event-Driven** - Real-time events for all state changes
|
|
27
|
+
|
|
28
|
+
### ๐ ๏ธ Developer Experience
|
|
29
|
+
- **TypeScript First** - Full type safety
|
|
30
|
+
- **Event Emitter** - React to SDK events
|
|
31
|
+
- **Adapter Pattern** - Flexible storage and transport
|
|
32
|
+
- **Comprehensive Tests** - 40+ test cases
|
|
33
|
+
- **Structured Logging** - Configurable log levels
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## ๐ฏ What Makes This SDK Production-Ready?
|
|
38
|
+
|
|
39
|
+
### 1. **Message Queue with Automatic Retry**
|
|
40
|
+
- Offline message support with persistent queue
|
|
41
|
+
- Configurable retry attempts (default: 3)
|
|
42
|
+
- Exponential backoff for failed messages
|
|
43
|
+
- Queue size management (default: 1000 messages)
|
|
44
|
+
- Message status tracking (pending, sent, failed)
|
|
45
|
+
|
|
46
|
+
### 2. **Event-Driven Architecture**
|
|
47
|
+
- Real-time event emissions for all state changes
|
|
48
|
+
- Extends Node.js `EventEmitter` for familiar API
|
|
49
|
+
- Events for messages, connections, users, groups, and errors
|
|
50
|
+
- Easy integration with React, Vue, or any framework
|
|
51
|
+
|
|
52
|
+
### 3. **Robust Connection Management**
|
|
53
|
+
- WebSocket support with automatic reconnection
|
|
54
|
+
- Exponential backoff strategy (up to 5 attempts)
|
|
55
|
+
- Heartbeat/ping-pong for connection health monitoring
|
|
56
|
+
- Connection state tracking (disconnected, connecting, connected, reconnecting, failed)
|
|
57
|
+
- Graceful degradation and error recovery
|
|
58
|
+
|
|
59
|
+
### 4. **Flexible Storage Adapters**
|
|
60
|
+
- Adapter pattern for any database (PostgreSQL, MySQL, MongoDB, Redis, etc.)
|
|
61
|
+
- In-memory stores for development and testing
|
|
62
|
+
- Easy migration from in-memory to production database
|
|
63
|
+
- Support for caching layers
|
|
64
|
+
|
|
65
|
+
### 5. **Enterprise-Grade Security**
|
|
66
|
+
- End-to-end encryption using ECDH (P-256) + AES-256-GCM
|
|
67
|
+
- Per-user cryptographic identity keys
|
|
68
|
+
- Session-based encryption for 1:1 and group chats
|
|
69
|
+
- Input validation to prevent injection attacks
|
|
70
|
+
- Secure key derivation and storage patterns
|
|
71
|
+
|
|
72
|
+
### 6. **Developer Experience**
|
|
73
|
+
- Full TypeScript support with comprehensive types
|
|
74
|
+
- Detailed error classes for better error handling
|
|
75
|
+
- Structured logging with configurable levels
|
|
76
|
+
- Extensive documentation and examples
|
|
77
|
+
- React hooks and context providers included
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## ๐ฆ Installation
|
|
2
83
|
|
|
3
|
-
|
|
84
|
+
```bash
|
|
85
|
+
npm install chatly-sdk
|
|
86
|
+
```
|
|
4
87
|
|
|
5
|
-
|
|
88
|
+
---
|
|
6
89
|
|
|
7
|
-
|
|
8
|
-
- ECDH key exchange (P-256 curve)
|
|
9
|
-
- AES-GCM message encryption
|
|
10
|
-
- Per-user identity keys
|
|
11
|
-
- Per-session ephemeral keys
|
|
12
|
-
- Group shared keys
|
|
90
|
+
## ๐๏ธ Architecture
|
|
13
91
|
|
|
14
|
-
|
|
15
|
-
- Secure key exchange
|
|
16
|
-
- Encrypt/decrypt functions
|
|
17
|
-
- Message payload schemas
|
|
92
|
+
### System Overview
|
|
18
93
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
94
|
+
```
|
|
95
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
96
|
+
โ ChatSDK โ
|
|
97
|
+
โ (EventEmitter) โ
|
|
98
|
+
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
|
|
99
|
+
โ โ ChatSession โ โ GroupSession โ โ Message Queueโ โ
|
|
100
|
+
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
|
|
101
|
+
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
|
|
102
|
+
โ โ Crypto (E2E) โ โ Validation โ โ Logger โ โ
|
|
103
|
+
โ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โ
|
|
104
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
105
|
+
โ
|
|
106
|
+
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโ
|
|
107
|
+
โ โ โ
|
|
108
|
+
โโโโโโผโโโโโ โโโโโโโผโโโโโโ โโโโโโโผโโโโโโ
|
|
109
|
+
โ User โ โ Message โ โ Group โ
|
|
110
|
+
โ Store โ โ Store โ โ Store โ
|
|
111
|
+
โโโโโโโโโโโ โโโโโโโโโโโโโ โโโโโโโโโโโโโ
|
|
112
|
+
โ
|
|
113
|
+
โโโโโโโผโโโโโโ
|
|
114
|
+
โ Transport โ
|
|
115
|
+
โ (WebSocketโ
|
|
116
|
+
โ /Memory) โ
|
|
117
|
+
โโโโโโโโโโโโโ
|
|
118
|
+
```
|
|
25
119
|
|
|
26
|
-
|
|
27
|
-
- Adapter pattern for flexible storage
|
|
28
|
-
- In-memory implementations included
|
|
29
|
-
- UserStoreAdapter, MessageStoreAdapter, GroupStoreAdapter
|
|
120
|
+
### Message Flow (1:1 Chat)
|
|
30
121
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
122
|
+
```
|
|
123
|
+
Alice SDK Bob
|
|
124
|
+
โ โ โ
|
|
125
|
+
โโ sendMessage() โโโโโโโถโ โ
|
|
126
|
+
โ โโ encrypt(ECDH+AES) โโโถโ
|
|
127
|
+
โ โโ store message โโโโโโโถโ
|
|
128
|
+
โ โโ queue if offline โโโโถโ
|
|
129
|
+
โ โโ send via transport โโถโโโโโโโโถ WebSocket
|
|
130
|
+
โ โ โ
|
|
131
|
+
โ โโโโโโ receive โโโโโโโโโโคโโโโโโโ WebSocket
|
|
132
|
+
โ โโ emit MESSAGE_RECEIVEDโ
|
|
133
|
+
โ โโ store message โโโโโโโถโ
|
|
134
|
+
โ โ โ
|
|
135
|
+
โ โ โโ decryptMessage()
|
|
136
|
+
โ โโโ decrypt(ECDH+AES) โโค
|
|
137
|
+
โ โ โ
|
|
138
|
+
โ โโ "Hello Bob!" โโโโโโโโถโ
|
|
139
|
+
```
|
|
35
140
|
|
|
36
|
-
|
|
141
|
+
### Connection Lifecycle
|
|
37
142
|
|
|
38
|
-
```bash
|
|
39
|
-
npm install chatly-sdk
|
|
40
143
|
```
|
|
144
|
+
โโโโโโโโโโโโโโโโ
|
|
145
|
+
โ DISCONNECTED โ
|
|
146
|
+
โโโโโโโโฌโโโโโโโโ
|
|
147
|
+
โ connect()
|
|
148
|
+
โผ
|
|
149
|
+
โโโโโโโโโโโโโโโโ timeout/error โโโโโโโโโโโโโโโโ
|
|
150
|
+
โ CONNECTING โโโโโโโโโโโโโโโโโโโโโโโถโ FAILED โ
|
|
151
|
+
โโโโโโโโฌโโโโโโโโ โโโโโโโโโโโโโโโโ
|
|
152
|
+
โ onopen
|
|
153
|
+
โผ
|
|
154
|
+
โโโโโโโโโโโโโโโโ onclose โโโโโโโโโโโโโโโโ
|
|
155
|
+
โ CONNECTED โโโโโโโโโโโโโโโโโโโโโโโถโ RECONNECTING โ
|
|
156
|
+
โโโโโโโโฌโโโโโโโโ โโโโโโโโฌโโโโโโโโ
|
|
157
|
+
โ โ
|
|
158
|
+
โ heartbeat (30s) โ exponential
|
|
159
|
+
โ ping/pong โ backoff
|
|
160
|
+
โ โ
|
|
161
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
41
165
|
|
|
42
|
-
## Quick Start
|
|
166
|
+
## ๐ Quick Start
|
|
43
167
|
|
|
44
168
|
### Basic Setup
|
|
45
169
|
|
|
46
170
|
```typescript
|
|
47
|
-
import {
|
|
48
|
-
|
|
171
|
+
import {
|
|
172
|
+
ChatSDK,
|
|
173
|
+
InMemoryUserStore,
|
|
174
|
+
InMemoryMessageStore,
|
|
175
|
+
InMemoryGroupStore,
|
|
176
|
+
LogLevel
|
|
177
|
+
} from 'chatly-sdk';
|
|
178
|
+
|
|
179
|
+
// Initialize SDK
|
|
49
180
|
const sdk = new ChatSDK({
|
|
50
181
|
userStore: new InMemoryUserStore(),
|
|
51
182
|
messageStore: new InMemoryMessageStore(),
|
|
52
183
|
groupStore: new InMemoryGroupStore(),
|
|
184
|
+
logLevel: LogLevel.INFO, // Optional: DEBUG, INFO, WARN, ERROR, NONE
|
|
53
185
|
});
|
|
54
186
|
|
|
55
187
|
// Create a user
|
|
56
|
-
const
|
|
57
|
-
sdk.setCurrentUser(
|
|
188
|
+
const alice = await sdk.createUser('alice');
|
|
189
|
+
sdk.setCurrentUser(alice);
|
|
58
190
|
```
|
|
59
191
|
|
|
60
192
|
### 1:1 Chat Example
|
|
61
193
|
|
|
62
194
|
```typescript
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const sdk = new ChatSDK({
|
|
66
|
-
userStore: new InMemoryUserStore(),
|
|
67
|
-
messageStore: new InMemoryMessageStore(),
|
|
68
|
-
groupStore: new InMemoryGroupStore(),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Create two users
|
|
195
|
+
// Create users
|
|
72
196
|
const alice = await sdk.createUser('alice');
|
|
73
197
|
const bob = await sdk.createUser('bob');
|
|
74
198
|
|
|
@@ -78,13 +202,14 @@ const session = await sdk.startSession(alice, bob);
|
|
|
78
202
|
|
|
79
203
|
// Send a message
|
|
80
204
|
const message = await sdk.sendMessage(session, 'Hello Bob!');
|
|
205
|
+
console.log('Message sent:', message.id);
|
|
81
206
|
|
|
82
207
|
// Bob receives and decrypts
|
|
83
208
|
sdk.setCurrentUser(bob);
|
|
84
209
|
const messages = await sdk.getMessagesForUser(bob.id);
|
|
85
210
|
for (const msg of messages) {
|
|
86
|
-
const
|
|
87
|
-
console.log(
|
|
211
|
+
const plaintext = await sdk.decryptMessage(msg, bob);
|
|
212
|
+
console.log('Received:', plaintext); // "Hello Bob!"
|
|
88
213
|
}
|
|
89
214
|
```
|
|
90
215
|
|
|
@@ -97,94 +222,394 @@ const bob = await sdk.createUser('bob');
|
|
|
97
222
|
const charlie = await sdk.createUser('charlie');
|
|
98
223
|
|
|
99
224
|
// Create a group
|
|
100
|
-
const group = await sdk.createGroup('
|
|
225
|
+
const group = await sdk.createGroup('Team Chat', [alice, bob, charlie]);
|
|
101
226
|
|
|
102
|
-
//
|
|
227
|
+
// Alice sends a message
|
|
103
228
|
sdk.setCurrentUser(alice);
|
|
104
|
-
|
|
229
|
+
await sdk.sendMessage(group, 'Hello team!');
|
|
105
230
|
|
|
106
|
-
//
|
|
231
|
+
// Bob and Charlie can decrypt
|
|
107
232
|
sdk.setCurrentUser(bob);
|
|
108
|
-
const
|
|
109
|
-
for (const msg of
|
|
110
|
-
const
|
|
111
|
-
console.log(
|
|
233
|
+
const messages = await sdk.getMessagesForGroup(group.group.id);
|
|
234
|
+
for (const msg of messages) {
|
|
235
|
+
const plaintext = await sdk.decryptMessage(msg, bob);
|
|
236
|
+
console.log('Bob received:', plaintext);
|
|
112
237
|
}
|
|
113
238
|
```
|
|
114
239
|
|
|
115
|
-
###
|
|
240
|
+
### Media Sharing Example
|
|
116
241
|
|
|
117
242
|
```typescript
|
|
118
|
-
|
|
119
|
-
const user = await sdk.createUser('john_doe');
|
|
120
|
-
const storedUser = {
|
|
121
|
-
...user,
|
|
122
|
-
createdAt: Date.now(),
|
|
123
|
-
};
|
|
124
|
-
await sdk.config.userStore.save(storedUser);
|
|
243
|
+
import { createMediaAttachment } from 'chatly-sdk';
|
|
125
244
|
|
|
126
|
-
//
|
|
127
|
-
const
|
|
128
|
-
sdk.
|
|
245
|
+
// Create users and session
|
|
246
|
+
const alice = await sdk.createUser('alice');
|
|
247
|
+
const bob = await sdk.createUser('bob');
|
|
248
|
+
const session = await sdk.startSession(alice, bob);
|
|
249
|
+
|
|
250
|
+
// Send an image
|
|
251
|
+
sdk.setCurrentUser(alice);
|
|
252
|
+
const imageFile = new File([imageBlob], 'photo.jpg', { type: 'image/jpeg' });
|
|
253
|
+
const imageMedia = await createMediaAttachment(imageFile);
|
|
254
|
+
await sdk.sendMediaMessage(session, 'Check out this photo!', imageMedia);
|
|
255
|
+
|
|
256
|
+
// Bob receives and decrypts
|
|
257
|
+
sdk.setCurrentUser(bob);
|
|
258
|
+
const messages = await sdk.getMessagesForUser(bob.id);
|
|
259
|
+
for (const msg of messages) {
|
|
260
|
+
if (msg.type === 'media' && msg.media) {
|
|
261
|
+
const { text, media } = await sdk.decryptMediaMessage(msg, bob);
|
|
262
|
+
console.log('Caption:', text);
|
|
263
|
+
console.log('Media type:', media.type);
|
|
264
|
+
console.log('Filename:', media.metadata.filename);
|
|
265
|
+
console.log('Size:', media.metadata.size);
|
|
266
|
+
|
|
267
|
+
// Convert back to file
|
|
268
|
+
const blob = decodeBase64ToBlob(media.data, media.metadata.mimeType);
|
|
269
|
+
// Use blob as needed (display, download, etc.)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
129
272
|
```
|
|
130
273
|
|
|
131
|
-
|
|
274
|
+
---
|
|
132
275
|
|
|
133
|
-
|
|
276
|
+
## ๐ Media Sharing
|
|
134
277
|
|
|
135
|
-
|
|
278
|
+
Send encrypted images, audio, video, and documents with full end-to-end encryption.
|
|
136
279
|
|
|
137
|
-
|
|
280
|
+
### Supported Media Types
|
|
281
|
+
|
|
282
|
+
| Type | Formats | Max Size |
|
|
283
|
+
|------|---------|----------|
|
|
284
|
+
| **Images** | JPEG, PNG, GIF, WebP | 10 MB |
|
|
285
|
+
| **Audio** | MP3, MP4, OGG, WAV, WebM | 16 MB |
|
|
286
|
+
| **Video** | MP4, WebM, OGG | 100 MB |
|
|
287
|
+
| **Documents** | PDF, DOC, DOCX, XLS, XLSX, TXT | 100 MB |
|
|
288
|
+
|
|
289
|
+
### Sending Media
|
|
138
290
|
|
|
139
291
|
```typescript
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
292
|
+
import { createMediaAttachment, MediaType } from 'chatly-sdk';
|
|
293
|
+
|
|
294
|
+
// From a File object
|
|
295
|
+
const file = new File([blob], 'document.pdf', { type: 'application/pdf' });
|
|
296
|
+
const media = await createMediaAttachment(file);
|
|
297
|
+
|
|
298
|
+
// Send in 1:1 chat
|
|
299
|
+
await sdk.sendMediaMessage(session, 'Here is the document', media);
|
|
300
|
+
|
|
301
|
+
// Send in group chat
|
|
302
|
+
await sdk.sendMediaMessage(groupSession, 'Team photo!', media);
|
|
146
303
|
```
|
|
147
304
|
|
|
148
|
-
|
|
305
|
+
### Receiving Media
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// Get messages
|
|
309
|
+
const messages = await sdk.getMessagesForUser(userId);
|
|
310
|
+
|
|
311
|
+
// Check for media messages
|
|
312
|
+
for (const msg of messages) {
|
|
313
|
+
if (msg.type === 'media' && msg.media) {
|
|
314
|
+
// Decrypt media message
|
|
315
|
+
const { text, media } = await sdk.decryptMediaMessage(msg, currentUser);
|
|
316
|
+
|
|
317
|
+
// Access media data
|
|
318
|
+
console.log('Caption:', text);
|
|
319
|
+
console.log('Type:', media.type); // 'image', 'audio', 'video', 'document'
|
|
320
|
+
console.log('Filename:', media.metadata.filename);
|
|
321
|
+
console.log('Size:', media.metadata.size);
|
|
322
|
+
console.log('MIME type:', media.metadata.mimeType);
|
|
323
|
+
|
|
324
|
+
// For images/videos
|
|
325
|
+
if (media.metadata.width) {
|
|
326
|
+
console.log('Dimensions:', media.metadata.width, 'x', media.metadata.height);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Thumbnail (for images/videos)
|
|
330
|
+
if (media.metadata.thumbnail) {
|
|
331
|
+
const thumbnailBlob = decodeBase64ToBlob(
|
|
332
|
+
media.metadata.thumbnail,
|
|
333
|
+
'image/jpeg'
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Convert to Blob for use
|
|
338
|
+
const blob = decodeBase64ToBlob(media.data, media.metadata.mimeType);
|
|
339
|
+
const url = URL.createObjectURL(blob);
|
|
340
|
+
// Use URL for display, download, etc.
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Media Utilities
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import {
|
|
349
|
+
createMediaAttachment,
|
|
350
|
+
encodeFileToBase64,
|
|
351
|
+
decodeBase64ToBlob,
|
|
352
|
+
validateMediaFile,
|
|
353
|
+
formatFileSize,
|
|
354
|
+
MediaType,
|
|
355
|
+
SUPPORTED_MIME_TYPES,
|
|
356
|
+
FILE_SIZE_LIMITS
|
|
357
|
+
} from 'chatly-sdk';
|
|
358
|
+
|
|
359
|
+
// Validate before sending
|
|
360
|
+
try {
|
|
361
|
+
validateMediaFile(file);
|
|
362
|
+
console.log('File is valid');
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error('Invalid file:', error.message);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Manual encoding/decoding
|
|
368
|
+
const base64 = await encodeFileToBase64(file);
|
|
369
|
+
const blob = decodeBase64ToBlob(base64, 'image/jpeg');
|
|
370
|
+
|
|
371
|
+
// Format file size
|
|
372
|
+
const sizeStr = formatFileSize(1024 * 1024); // "1.0 MB"
|
|
373
|
+
|
|
374
|
+
// Check supported types
|
|
375
|
+
console.log('Supported image types:', SUPPORTED_MIME_TYPES.image);
|
|
376
|
+
console.log('Max video size:', FILE_SIZE_LIMITS.video); // 100 MB
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Media Encryption
|
|
380
|
+
|
|
381
|
+
All media files are **fully encrypted end-to-end**:
|
|
382
|
+
|
|
383
|
+
1. **File data** is encrypted with the session/group key
|
|
384
|
+
2. **Metadata** (filename, size, etc.) is stored in plaintext for efficiency
|
|
385
|
+
3. **Thumbnails** (for images/videos) are encrypted
|
|
386
|
+
4. **No URL-based approach** - all files sent directly through SDK
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// Media encryption happens automatically
|
|
390
|
+
const media = await createMediaAttachment(file);
|
|
391
|
+
const message = await sdk.sendMediaMessage(session, caption, media);
|
|
392
|
+
|
|
393
|
+
// Message contains:
|
|
394
|
+
// - Encrypted caption (ciphertext)
|
|
395
|
+
// - Encrypted media data (media.data)
|
|
396
|
+
// - Plaintext metadata (media.metadata)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Example: Sending an Image
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// Browser environment
|
|
403
|
+
const input = document.querySelector('input[type="file"]');
|
|
404
|
+
const file = input.files[0];
|
|
405
|
+
|
|
406
|
+
// Create media attachment (validates, encodes, generates thumbnail)
|
|
407
|
+
const media = await createMediaAttachment(file);
|
|
408
|
+
|
|
409
|
+
// Send with caption
|
|
410
|
+
await sdk.sendMediaMessage(session, 'Check this out!', media);
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Example: Displaying Received Images
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// Get and decrypt media message
|
|
417
|
+
const { text, media } = await sdk.decryptMediaMessage(message, currentUser);
|
|
418
|
+
|
|
419
|
+
// Create blob and display
|
|
420
|
+
const blob = decodeBase64ToBlob(media.data, media.metadata.mimeType);
|
|
421
|
+
const url = URL.createObjectURL(blob);
|
|
422
|
+
|
|
423
|
+
// Show image
|
|
424
|
+
const img = document.createElement('img');
|
|
425
|
+
img.src = url;
|
|
426
|
+
document.body.appendChild(img);
|
|
427
|
+
|
|
428
|
+
// Show thumbnail first (faster)
|
|
429
|
+
if (media.metadata.thumbnail) {
|
|
430
|
+
const thumbBlob = decodeBase64ToBlob(media.metadata.thumbnail, 'image/jpeg');
|
|
431
|
+
const thumbUrl = URL.createObjectURL(thumbBlob);
|
|
432
|
+
img.src = thumbUrl; // Show thumbnail
|
|
433
|
+
|
|
434
|
+
// Load full image
|
|
435
|
+
img.onload = () => {
|
|
436
|
+
URL.revokeObjectURL(thumbUrl);
|
|
437
|
+
img.src = url; // Replace with full image
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## ๐ฏ Event-Driven Architecture
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
The SDK extends `EventEmitter` and emits events for all state changes:
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
import { EVENTS, ConnectionState } from 'chatly-sdk';
|
|
451
|
+
|
|
452
|
+
// Message events
|
|
453
|
+
sdk.on(EVENTS.MESSAGE_SENT, (message) => {
|
|
454
|
+
console.log('โ
Message sent:', message.id);
|
|
455
|
+
updateUI('sent', message);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
sdk.on(EVENTS.MESSAGE_RECEIVED, (message) => {
|
|
459
|
+
console.log('๐จ Message received:', message.id);
|
|
460
|
+
notifyUser(message);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
sdk.on(EVENTS.MESSAGE_FAILED, (message, error) => {
|
|
464
|
+
console.error('โ Message failed:', message.id, error);
|
|
465
|
+
showRetryButton(message);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Connection events
|
|
469
|
+
sdk.on(EVENTS.CONNECTION_STATE_CHANGED, (state) => {
|
|
470
|
+
switch (state) {
|
|
471
|
+
case ConnectionState.CONNECTED:
|
|
472
|
+
console.log('๐ข Connected');
|
|
473
|
+
break;
|
|
474
|
+
case ConnectionState.RECONNECTING:
|
|
475
|
+
console.log('๐ก Reconnecting...');
|
|
476
|
+
break;
|
|
477
|
+
case ConnectionState.DISCONNECTED:
|
|
478
|
+
console.log('๐ด Disconnected');
|
|
479
|
+
break;
|
|
480
|
+
case ConnectionState.FAILED:
|
|
481
|
+
console.log('๐ฅ Connection failed');
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// User and group events
|
|
487
|
+
sdk.on(EVENTS.USER_CREATED, (user) => {
|
|
488
|
+
console.log('๐ค User created:', user.username);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
sdk.on(EVENTS.SESSION_CREATED, (session) => {
|
|
492
|
+
console.log('๐ฌ Session created:', session.id);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
sdk.on(EVENTS.GROUP_CREATED, (group) => {
|
|
496
|
+
console.log('๐ฅ Group created:', group.group.name);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Error handling
|
|
500
|
+
sdk.on(EVENTS.ERROR, (error) => {
|
|
501
|
+
console.error('โ ๏ธ SDK error:', error);
|
|
502
|
+
if (error.retryable) {
|
|
503
|
+
// Retry the operation
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
```
|
|
149
507
|
|
|
150
|
-
|
|
151
|
-
- `importUser(userData: StoredUser): Promise<User>` - Import an existing user
|
|
152
|
-
- `setCurrentUser(user: User): void` - Set the active user
|
|
153
|
-
- `getCurrentUser(): User | null` - Get the current user
|
|
154
|
-
- `startSession(userA: User, userB: User): Promise<ChatSession>` - Start a 1:1 chat
|
|
155
|
-
- `createGroup(name: string, members: User[]): Promise<GroupSession>` - Create a group
|
|
156
|
-
- `loadGroup(id: string): Promise<GroupSession>` - Load an existing group
|
|
157
|
-
- `sendMessage(session: ChatSession | GroupSession, plaintext: string): Promise<Message>` - Send a message
|
|
158
|
-
- `decryptMessage(message: Message, user: User): Promise<string>` - Decrypt a message
|
|
159
|
-
- `getMessagesForUser(userId: string): Promise<Message[]>` - Get user's messages
|
|
160
|
-
- `getMessagesForGroup(groupId: string): Promise<Message[]>` - Get group messages
|
|
508
|
+
---
|
|
161
509
|
|
|
162
|
-
|
|
510
|
+
## ๐ WebSocket Integration
|
|
163
511
|
|
|
164
|
-
|
|
512
|
+
### Client-Side Setup
|
|
165
513
|
|
|
166
514
|
```typescript
|
|
515
|
+
import { ChatSDK, WebSocketClient } from 'chatly-sdk';
|
|
516
|
+
|
|
517
|
+
// Create WebSocket transport
|
|
518
|
+
const transport = new WebSocketClient('wss://your-server.com/ws');
|
|
519
|
+
|
|
520
|
+
const sdk = new ChatSDK({
|
|
521
|
+
userStore: new InMemoryUserStore(),
|
|
522
|
+
messageStore: new InMemoryMessageStore(),
|
|
523
|
+
groupStore: new InMemoryGroupStore(),
|
|
524
|
+
transport, // Add transport
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Set current user (automatically connects WebSocket)
|
|
528
|
+
await sdk.setCurrentUser(user);
|
|
529
|
+
|
|
530
|
+
// Listen for connection state
|
|
531
|
+
sdk.on(EVENTS.CONNECTION_STATE_CHANGED, (state) => {
|
|
532
|
+
console.log('Connection:', state);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Receive messages in real-time
|
|
536
|
+
sdk.on(EVENTS.MESSAGE_RECEIVED, async (message) => {
|
|
537
|
+
const plaintext = await sdk.decryptMessage(message, currentUser);
|
|
538
|
+
displayMessage(plaintext);
|
|
539
|
+
});
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Server-Side Setup (Node.js)
|
|
543
|
+
|
|
544
|
+
```javascript
|
|
545
|
+
const WebSocket = require('ws');
|
|
546
|
+
const wss = new WebSocket.Server({ port: 8080 });
|
|
547
|
+
|
|
548
|
+
const clients = new Map(); // userId -> WebSocket
|
|
549
|
+
|
|
550
|
+
wss.on('connection', (ws, req) => {
|
|
551
|
+
const userId = new URL(req.url, 'ws://localhost').searchParams.get('userId');
|
|
552
|
+
|
|
553
|
+
if (!userId) {
|
|
554
|
+
ws.close(4001, 'Missing userId');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
clients.set(userId, ws);
|
|
559
|
+
console.log(`User ${userId} connected`);
|
|
560
|
+
|
|
561
|
+
// Handle ping/pong
|
|
562
|
+
ws.on('message', (data) => {
|
|
563
|
+
const message = JSON.parse(data.toString());
|
|
564
|
+
|
|
565
|
+
if (message.type === 'ping') {
|
|
566
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Forward message to recipient
|
|
571
|
+
const recipientId = message.receiverId || message.groupId;
|
|
572
|
+
const recipientWs = clients.get(recipientId);
|
|
573
|
+
|
|
574
|
+
if (recipientWs && recipientWs.readyState === WebSocket.OPEN) {
|
|
575
|
+
recipientWs.send(JSON.stringify(message));
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
ws.on('close', () => {
|
|
580
|
+
clients.delete(userId);
|
|
581
|
+
console.log(`User ${userId} disconnected`);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
## ๐๏ธ Database Integration
|
|
589
|
+
|
|
590
|
+
The SDK uses the **Adapter Pattern** to support any database. You can implement custom storage adapters for your preferred database.
|
|
591
|
+
|
|
592
|
+
### Storage Adapter Interfaces
|
|
593
|
+
|
|
594
|
+
The SDK defines three adapter interfaces:
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
// User storage
|
|
167
598
|
interface UserStoreAdapter {
|
|
168
599
|
create(user: User): Promise<User>;
|
|
169
600
|
findById(id: string): Promise<User | undefined>;
|
|
170
601
|
save(user: StoredUser): Promise<void>;
|
|
171
602
|
list(): Promise<User[]>;
|
|
172
603
|
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
#### MessageStoreAdapter
|
|
176
604
|
|
|
177
|
-
|
|
605
|
+
// Message storage
|
|
178
606
|
interface MessageStoreAdapter {
|
|
179
607
|
create(message: Message): Promise<Message>;
|
|
180
608
|
listByUser(userId: string): Promise<Message[]>;
|
|
181
609
|
listByGroup(groupId: string): Promise<Message[]>;
|
|
182
610
|
}
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
#### GroupStoreAdapter
|
|
186
611
|
|
|
187
|
-
|
|
612
|
+
// Group storage
|
|
188
613
|
interface GroupStoreAdapter {
|
|
189
614
|
create(group: Group): Promise<Group>;
|
|
190
615
|
findById(id: string): Promise<Group | undefined>;
|
|
@@ -192,121 +617,1070 @@ interface GroupStoreAdapter {
|
|
|
192
617
|
}
|
|
193
618
|
```
|
|
194
619
|
|
|
195
|
-
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
### PostgreSQL Implementation
|
|
623
|
+
|
|
624
|
+
#### Database Schema
|
|
625
|
+
|
|
626
|
+
```sql
|
|
627
|
+
-- Users table
|
|
628
|
+
CREATE TABLE users (
|
|
629
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
630
|
+
username VARCHAR(50) NOT NULL UNIQUE,
|
|
631
|
+
public_key TEXT NOT NULL,
|
|
632
|
+
private_key TEXT NOT NULL,
|
|
633
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
634
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
-- Messages table
|
|
638
|
+
CREATE TABLE messages (
|
|
639
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
640
|
+
sender_id VARCHAR(255) NOT NULL REFERENCES users(id),
|
|
641
|
+
receiver_id VARCHAR(255) REFERENCES users(id),
|
|
642
|
+
group_id VARCHAR(255),
|
|
643
|
+
ciphertext TEXT NOT NULL,
|
|
644
|
+
iv VARCHAR(255) NOT NULL,
|
|
645
|
+
timestamp BIGINT NOT NULL,
|
|
646
|
+
status VARCHAR(20) DEFAULT 'pending',
|
|
647
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
648
|
+
INDEX idx_receiver (receiver_id),
|
|
649
|
+
INDEX idx_group (group_id),
|
|
650
|
+
INDEX idx_timestamp (timestamp)
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
-- Groups table
|
|
654
|
+
CREATE TABLE groups (
|
|
655
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
656
|
+
name VARCHAR(100) NOT NULL,
|
|
657
|
+
shared_secret TEXT NOT NULL,
|
|
658
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
-- Group members table
|
|
662
|
+
CREATE TABLE group_members (
|
|
663
|
+
group_id VARCHAR(255) NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
|
664
|
+
user_id VARCHAR(255) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
665
|
+
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
666
|
+
PRIMARY KEY (group_id, user_id)
|
|
667
|
+
);
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
#### Adapter Implementation
|
|
196
671
|
|
|
197
672
|
```typescript
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
673
|
+
import { Pool } from 'pg';
|
|
674
|
+
import { UserStoreAdapter, MessageStoreAdapter, GroupStoreAdapter } from 'chatly-sdk';
|
|
675
|
+
import type { User, StoredUser, Message, Group } from 'chatly-sdk';
|
|
676
|
+
|
|
677
|
+
// PostgreSQL User Store
|
|
678
|
+
export class PostgreSQLUserStore implements UserStoreAdapter {
|
|
679
|
+
constructor(private pool: Pool) {}
|
|
680
|
+
|
|
681
|
+
async create(user: User): Promise<User> {
|
|
682
|
+
await this.pool.query(
|
|
683
|
+
`INSERT INTO users (id, username, public_key, private_key)
|
|
684
|
+
VALUES ($1, $2, $3, $4)`,
|
|
685
|
+
[user.id, user.username, user.publicKey, user.privateKey]
|
|
686
|
+
);
|
|
687
|
+
return user;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async findById(id: string): Promise<User | undefined> {
|
|
691
|
+
const result = await this.pool.query(
|
|
692
|
+
'SELECT id, username, public_key as "publicKey", private_key as "privateKey" FROM users WHERE id = $1',
|
|
693
|
+
[id]
|
|
694
|
+
);
|
|
695
|
+
return result.rows[0];
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async save(user: StoredUser): Promise<void> {
|
|
699
|
+
await this.pool.query(
|
|
700
|
+
`UPDATE users
|
|
701
|
+
SET username = $1, public_key = $2, updated_at = CURRENT_TIMESTAMP
|
|
702
|
+
WHERE id = $3`,
|
|
703
|
+
[user.username, user.publicKey, user.id]
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async list(): Promise<User[]> {
|
|
708
|
+
const result = await this.pool.query(
|
|
709
|
+
'SELECT id, username, public_key as "publicKey", private_key as "privateKey" FROM users'
|
|
710
|
+
);
|
|
711
|
+
return result.rows;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// PostgreSQL Message Store
|
|
716
|
+
export class PostgreSQLMessageStore implements MessageStoreAdapter {
|
|
717
|
+
constructor(private pool: Pool) {}
|
|
718
|
+
|
|
719
|
+
async create(message: Message): Promise<Message> {
|
|
720
|
+
await this.pool.query(
|
|
721
|
+
`INSERT INTO messages (id, sender_id, receiver_id, group_id, ciphertext, iv, timestamp, status)
|
|
722
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
723
|
+
[
|
|
724
|
+
message.id,
|
|
725
|
+
message.senderId,
|
|
726
|
+
message.receiverId || null,
|
|
727
|
+
message.groupId || null,
|
|
728
|
+
message.ciphertext,
|
|
729
|
+
message.iv,
|
|
730
|
+
message.timestamp,
|
|
731
|
+
message.status || 'pending'
|
|
732
|
+
]
|
|
733
|
+
);
|
|
734
|
+
return message;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async listByUser(userId: string): Promise<Message[]> {
|
|
738
|
+
const result = await this.pool.query(
|
|
739
|
+
`SELECT id, sender_id as "senderId", receiver_id as "receiverId",
|
|
740
|
+
group_id as "groupId", ciphertext, iv, timestamp, status
|
|
741
|
+
FROM messages
|
|
742
|
+
WHERE receiver_id = $1 OR sender_id = $1
|
|
743
|
+
ORDER BY timestamp ASC`,
|
|
744
|
+
[userId]
|
|
745
|
+
);
|
|
746
|
+
return result.rows;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async listByGroup(groupId: string): Promise<Message[]> {
|
|
750
|
+
const result = await this.pool.query(
|
|
751
|
+
`SELECT id, sender_id as "senderId", receiver_id as "receiverId",
|
|
752
|
+
group_id as "groupId", ciphertext, iv, timestamp, status
|
|
753
|
+
FROM messages
|
|
754
|
+
WHERE group_id = $1
|
|
755
|
+
ORDER BY timestamp ASC`,
|
|
756
|
+
[groupId]
|
|
757
|
+
);
|
|
758
|
+
return result.rows;
|
|
759
|
+
}
|
|
202
760
|
}
|
|
761
|
+
|
|
762
|
+
// PostgreSQL Group Store
|
|
763
|
+
export class PostgreSQLGroupStore implements GroupStoreAdapter {
|
|
764
|
+
constructor(private pool: Pool) {}
|
|
765
|
+
|
|
766
|
+
async create(group: Group): Promise<Group> {
|
|
767
|
+
const client = await this.pool.connect();
|
|
768
|
+
try {
|
|
769
|
+
await client.query('BEGIN');
|
|
770
|
+
|
|
771
|
+
// Insert group
|
|
772
|
+
await client.query(
|
|
773
|
+
'INSERT INTO groups (id, name, shared_secret) VALUES ($1, $2, $3)',
|
|
774
|
+
[group.id, group.name, group.sharedSecret]
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
// Insert members
|
|
778
|
+
for (const userId of group.members) {
|
|
779
|
+
await client.query(
|
|
780
|
+
'INSERT INTO group_members (group_id, user_id) VALUES ($1, $2)',
|
|
781
|
+
[group.id, userId]
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
await client.query('COMMIT');
|
|
786
|
+
return group;
|
|
787
|
+
} catch (error) {
|
|
788
|
+
await client.query('ROLLBACK');
|
|
789
|
+
throw error;
|
|
790
|
+
} finally {
|
|
791
|
+
client.release();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async findById(id: string): Promise<Group | undefined> {
|
|
796
|
+
const groupResult = await this.pool.query(
|
|
797
|
+
'SELECT id, name, shared_secret as "sharedSecret" FROM groups WHERE id = $1',
|
|
798
|
+
[id]
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
if (groupResult.rows.length === 0) return undefined;
|
|
802
|
+
|
|
803
|
+
const membersResult = await this.pool.query(
|
|
804
|
+
'SELECT user_id FROM group_members WHERE group_id = $1',
|
|
805
|
+
[id]
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
return {
|
|
809
|
+
...groupResult.rows[0],
|
|
810
|
+
members: membersResult.rows.map(row => row.user_id)
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async list(): Promise<Group[]> {
|
|
815
|
+
const groupsResult = await this.pool.query(
|
|
816
|
+
'SELECT id, name, shared_secret as "sharedSecret" FROM groups'
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
const groups: Group[] = [];
|
|
820
|
+
for (const group of groupsResult.rows) {
|
|
821
|
+
const membersResult = await this.pool.query(
|
|
822
|
+
'SELECT user_id FROM group_members WHERE group_id = $1',
|
|
823
|
+
[group.id]
|
|
824
|
+
);
|
|
825
|
+
groups.push({
|
|
826
|
+
...group,
|
|
827
|
+
members: membersResult.rows.map(row => row.user_id)
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return groups;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Usage
|
|
836
|
+
import { Pool } from 'pg';
|
|
837
|
+
import { ChatSDK } from 'chatly-sdk';
|
|
838
|
+
|
|
839
|
+
const pool = new Pool({
|
|
840
|
+
host: 'localhost',
|
|
841
|
+
port: 5432,
|
|
842
|
+
database: 'chatly',
|
|
843
|
+
user: 'your_user',
|
|
844
|
+
password: 'your_password',
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
const sdk = new ChatSDK({
|
|
848
|
+
userStore: new PostgreSQLUserStore(pool),
|
|
849
|
+
messageStore: new PostgreSQLMessageStore(pool),
|
|
850
|
+
groupStore: new PostgreSQLGroupStore(pool),
|
|
851
|
+
});
|
|
203
852
|
```
|
|
204
853
|
|
|
205
|
-
|
|
854
|
+
---
|
|
206
855
|
|
|
207
|
-
###
|
|
856
|
+
### MongoDB Implementation
|
|
208
857
|
|
|
209
|
-
|
|
858
|
+
#### Adapter Implementation
|
|
210
859
|
|
|
211
860
|
```typescript
|
|
212
|
-
import {
|
|
861
|
+
import { Collection, MongoClient } from 'mongodb';
|
|
862
|
+
import { UserStoreAdapter, MessageStoreAdapter, GroupStoreAdapter } from 'chatly-sdk';
|
|
863
|
+
import type { User, StoredUser, Message, Group } from 'chatly-sdk';
|
|
864
|
+
|
|
865
|
+
// MongoDB User Store
|
|
866
|
+
export class MongoDBUserStore implements UserStoreAdapter {
|
|
867
|
+
constructor(private collection: Collection) {}
|
|
213
868
|
|
|
214
|
-
class PostgreSQLUserStore implements UserStoreAdapter {
|
|
215
869
|
async create(user: User): Promise<User> {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
870
|
+
await this.collection.insertOne({
|
|
871
|
+
_id: user.id,
|
|
872
|
+
username: user.username,
|
|
873
|
+
publicKey: user.publicKey,
|
|
874
|
+
privateKey: user.privateKey,
|
|
875
|
+
createdAt: new Date(),
|
|
876
|
+
});
|
|
877
|
+
return user;
|
|
219
878
|
}
|
|
220
|
-
|
|
879
|
+
|
|
221
880
|
async findById(id: string): Promise<User | undefined> {
|
|
222
|
-
const
|
|
223
|
-
return
|
|
881
|
+
const doc = await this.collection.findOne({ _id: id });
|
|
882
|
+
if (!doc) return undefined;
|
|
883
|
+
|
|
884
|
+
return {
|
|
885
|
+
id: doc._id,
|
|
886
|
+
username: doc.username,
|
|
887
|
+
publicKey: doc.publicKey,
|
|
888
|
+
privateKey: doc.privateKey,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
async save(user: StoredUser): Promise<void> {
|
|
893
|
+
await this.collection.updateOne(
|
|
894
|
+
{ _id: user.id },
|
|
895
|
+
{
|
|
896
|
+
$set: {
|
|
897
|
+
username: user.username,
|
|
898
|
+
publicKey: user.publicKey,
|
|
899
|
+
updatedAt: new Date()
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async list(): Promise<User[]> {
|
|
906
|
+
const docs = await this.collection.find({}).toArray();
|
|
907
|
+
return docs.map(doc => ({
|
|
908
|
+
id: doc._id,
|
|
909
|
+
username: doc.username,
|
|
910
|
+
publicKey: doc.publicKey,
|
|
911
|
+
privateKey: doc.privateKey,
|
|
912
|
+
}));
|
|
224
913
|
}
|
|
225
|
-
|
|
226
|
-
// ... implement other methods
|
|
227
914
|
}
|
|
915
|
+
|
|
916
|
+
// MongoDB Message Store
|
|
917
|
+
export class MongoDBMessageStore implements MessageStoreAdapter {
|
|
918
|
+
constructor(private collection: Collection) {}
|
|
919
|
+
|
|
920
|
+
async create(message: Message): Promise<Message> {
|
|
921
|
+
await this.collection.insertOne({
|
|
922
|
+
_id: message.id,
|
|
923
|
+
senderId: message.senderId,
|
|
924
|
+
receiverId: message.receiverId,
|
|
925
|
+
groupId: message.groupId,
|
|
926
|
+
ciphertext: message.ciphertext,
|
|
927
|
+
iv: message.iv,
|
|
928
|
+
timestamp: message.timestamp,
|
|
929
|
+
status: message.status || 'pending',
|
|
930
|
+
createdAt: new Date(),
|
|
931
|
+
});
|
|
932
|
+
return message;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
async listByUser(userId: string): Promise<Message[]> {
|
|
936
|
+
const docs = await this.collection
|
|
937
|
+
.find({
|
|
938
|
+
$or: [{ receiverId: userId }, { senderId: userId }]
|
|
939
|
+
})
|
|
940
|
+
.sort({ timestamp: 1 })
|
|
941
|
+
.toArray();
|
|
942
|
+
|
|
943
|
+
return docs.map(doc => ({
|
|
944
|
+
id: doc._id,
|
|
945
|
+
senderId: doc.senderId,
|
|
946
|
+
receiverId: doc.receiverId,
|
|
947
|
+
groupId: doc.groupId,
|
|
948
|
+
ciphertext: doc.ciphertext,
|
|
949
|
+
iv: doc.iv,
|
|
950
|
+
timestamp: doc.timestamp,
|
|
951
|
+
status: doc.status,
|
|
952
|
+
}));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
async listByGroup(groupId: string): Promise<Message[]> {
|
|
956
|
+
const docs = await this.collection
|
|
957
|
+
.find({ groupId })
|
|
958
|
+
.sort({ timestamp: 1 })
|
|
959
|
+
.toArray();
|
|
960
|
+
|
|
961
|
+
return docs.map(doc => ({
|
|
962
|
+
id: doc._id,
|
|
963
|
+
senderId: doc.senderId,
|
|
964
|
+
receiverId: doc.receiverId,
|
|
965
|
+
groupId: doc.groupId,
|
|
966
|
+
ciphertext: doc.ciphertext,
|
|
967
|
+
iv: doc.iv,
|
|
968
|
+
timestamp: doc.timestamp,
|
|
969
|
+
status: doc.status,
|
|
970
|
+
}));
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// MongoDB Group Store
|
|
975
|
+
export class MongoDBGroupStore implements GroupStoreAdapter {
|
|
976
|
+
constructor(private collection: Collection) {}
|
|
977
|
+
|
|
978
|
+
async create(group: Group): Promise<Group> {
|
|
979
|
+
await this.collection.insertOne({
|
|
980
|
+
_id: group.id,
|
|
981
|
+
name: group.name,
|
|
982
|
+
sharedSecret: group.sharedSecret,
|
|
983
|
+
members: group.members,
|
|
984
|
+
createdAt: new Date(),
|
|
985
|
+
});
|
|
986
|
+
return group;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
async findById(id: string): Promise<Group | undefined> {
|
|
990
|
+
const doc = await this.collection.findOne({ _id: id });
|
|
991
|
+
if (!doc) return undefined;
|
|
992
|
+
|
|
993
|
+
return {
|
|
994
|
+
id: doc._id,
|
|
995
|
+
name: doc.name,
|
|
996
|
+
sharedSecret: doc.sharedSecret,
|
|
997
|
+
members: doc.members,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
async list(): Promise<Group[]> {
|
|
1002
|
+
const docs = await this.collection.find({}).toArray();
|
|
1003
|
+
return docs.map(doc => ({
|
|
1004
|
+
id: doc._id,
|
|
1005
|
+
name: doc.name,
|
|
1006
|
+
sharedSecret: doc.sharedSecret,
|
|
1007
|
+
members: doc.members,
|
|
1008
|
+
}));
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Usage
|
|
1013
|
+
import { MongoClient } from 'mongodb';
|
|
1014
|
+
import { ChatSDK } from 'chatly-sdk';
|
|
1015
|
+
|
|
1016
|
+
const client = new MongoClient('mongodb://localhost:27017');
|
|
1017
|
+
await client.connect();
|
|
1018
|
+
const db = client.db('chatly');
|
|
1019
|
+
|
|
1020
|
+
// Create indexes for better performance
|
|
1021
|
+
await db.collection('messages').createIndex({ receiverId: 1, timestamp: 1 });
|
|
1022
|
+
await db.collection('messages').createIndex({ groupId: 1, timestamp: 1 });
|
|
1023
|
+
await db.collection('users').createIndex({ username: 1 }, { unique: true });
|
|
1024
|
+
|
|
1025
|
+
const sdk = new ChatSDK({
|
|
1026
|
+
userStore: new MongoDBUserStore(db.collection('users')),
|
|
1027
|
+
messageStore: new MongoDBMessageStore(db.collection('messages')),
|
|
1028
|
+
groupStore: new MongoDBGroupStore(db.collection('groups')),
|
|
1029
|
+
});
|
|
228
1030
|
```
|
|
229
1031
|
|
|
230
|
-
|
|
1032
|
+
---
|
|
1033
|
+
|
|
1034
|
+
### MySQL Implementation
|
|
1035
|
+
|
|
1036
|
+
#### Database Schema
|
|
1037
|
+
|
|
1038
|
+
```sql
|
|
1039
|
+
CREATE DATABASE chatly CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
1040
|
+
USE chatly;
|
|
1041
|
+
|
|
1042
|
+
CREATE TABLE users (
|
|
1043
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
1044
|
+
username VARCHAR(50) NOT NULL UNIQUE,
|
|
1045
|
+
public_key TEXT NOT NULL,
|
|
1046
|
+
private_key TEXT NOT NULL,
|
|
1047
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1048
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
1049
|
+
INDEX idx_username (username)
|
|
1050
|
+
) ENGINE=InnoDB;
|
|
1051
|
+
|
|
1052
|
+
CREATE TABLE messages (
|
|
1053
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
1054
|
+
sender_id VARCHAR(255) NOT NULL,
|
|
1055
|
+
receiver_id VARCHAR(255),
|
|
1056
|
+
group_id VARCHAR(255),
|
|
1057
|
+
ciphertext MEDIUMTEXT NOT NULL,
|
|
1058
|
+
iv VARCHAR(255) NOT NULL,
|
|
1059
|
+
timestamp BIGINT NOT NULL,
|
|
1060
|
+
status VARCHAR(20) DEFAULT 'pending',
|
|
1061
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1062
|
+
INDEX idx_receiver_time (receiver_id, timestamp),
|
|
1063
|
+
INDEX idx_group_time (group_id, timestamp),
|
|
1064
|
+
INDEX idx_sender (sender_id),
|
|
1065
|
+
FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE
|
|
1066
|
+
) ENGINE=InnoDB;
|
|
1067
|
+
|
|
1068
|
+
CREATE TABLE groups (
|
|
1069
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
1070
|
+
name VARCHAR(100) NOT NULL,
|
|
1071
|
+
shared_secret TEXT NOT NULL,
|
|
1072
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
1073
|
+
) ENGINE=InnoDB;
|
|
1074
|
+
|
|
1075
|
+
CREATE TABLE group_members (
|
|
1076
|
+
group_id VARCHAR(255) NOT NULL,
|
|
1077
|
+
user_id VARCHAR(255) NOT NULL,
|
|
1078
|
+
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1079
|
+
PRIMARY KEY (group_id, user_id),
|
|
1080
|
+
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE,
|
|
1081
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
1082
|
+
) ENGINE=InnoDB;
|
|
1083
|
+
```
|
|
231
1084
|
|
|
232
|
-
|
|
1085
|
+
#### Adapter Implementation
|
|
233
1086
|
|
|
234
1087
|
```typescript
|
|
235
|
-
import
|
|
1088
|
+
import mysql from 'mysql2/promise';
|
|
1089
|
+
import { UserStoreAdapter, MessageStoreAdapter, GroupStoreAdapter } from 'chatly-sdk';
|
|
1090
|
+
import type { User, StoredUser, Message, Group } from 'chatly-sdk';
|
|
236
1091
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
1092
|
+
// MySQL User Store (similar to PostgreSQL, with minor syntax differences)
|
|
1093
|
+
export class MySQLUserStore implements UserStoreAdapter {
|
|
1094
|
+
constructor(private pool: mysql.Pool) {}
|
|
1095
|
+
|
|
1096
|
+
async create(user: User): Promise<User> {
|
|
1097
|
+
await this.pool.execute(
|
|
1098
|
+
'INSERT INTO users (id, username, public_key, private_key) VALUES (?, ?, ?, ?)',
|
|
1099
|
+
[user.id, user.username, user.publicKey, user.privateKey]
|
|
1100
|
+
);
|
|
1101
|
+
return user;
|
|
242
1102
|
}
|
|
243
|
-
|
|
244
|
-
async
|
|
245
|
-
this.
|
|
1103
|
+
|
|
1104
|
+
async findById(id: string): Promise<User | undefined> {
|
|
1105
|
+
const [rows] = await this.pool.execute(
|
|
1106
|
+
'SELECT id, username, public_key as publicKey, private_key as privateKey FROM users WHERE id = ?',
|
|
1107
|
+
[id]
|
|
1108
|
+
);
|
|
1109
|
+
return (rows as any[])[0];
|
|
246
1110
|
}
|
|
1111
|
+
|
|
1112
|
+
async save(user: StoredUser): Promise<void> {
|
|
1113
|
+
await this.pool.execute(
|
|
1114
|
+
'UPDATE users SET username = ?, public_key = ? WHERE id = ?',
|
|
1115
|
+
[user.username, user.publicKey, user.id]
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
async list(): Promise<User[]> {
|
|
1120
|
+
const [rows] = await this.pool.execute(
|
|
1121
|
+
'SELECT id, username, public_key as publicKey, private_key as privateKey FROM users'
|
|
1122
|
+
);
|
|
1123
|
+
return rows as User[];
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Usage
|
|
1128
|
+
import mysql from 'mysql2/promise';
|
|
1129
|
+
import { ChatSDK } from 'chatly-sdk';
|
|
1130
|
+
|
|
1131
|
+
const pool = mysql.createPool({
|
|
1132
|
+
host: 'localhost',
|
|
1133
|
+
user: 'your_user',
|
|
1134
|
+
password: 'your_password',
|
|
1135
|
+
database: 'chatly',
|
|
1136
|
+
waitForConnections: true,
|
|
1137
|
+
connectionLimit: 10,
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
const sdk = new ChatSDK({
|
|
1141
|
+
userStore: new MySQLUserStore(pool),
|
|
1142
|
+
messageStore: new MySQLMessageStore(pool),
|
|
1143
|
+
groupStore: new MySQLGroupStore(pool),
|
|
1144
|
+
});
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
---
|
|
1148
|
+
|
|
1149
|
+
### Redis (Caching Layer)
|
|
1150
|
+
|
|
1151
|
+
Use Redis as a caching layer on top of your primary database:
|
|
1152
|
+
|
|
1153
|
+
```typescript
|
|
1154
|
+
import { createClient } from 'redis';
|
|
1155
|
+
import { UserStoreAdapter } from 'chatly-sdk';
|
|
1156
|
+
import type { User, StoredUser } from 'chatly-sdk';
|
|
1157
|
+
|
|
1158
|
+
export class CachedUserStore implements UserStoreAdapter {
|
|
1159
|
+
private redis: ReturnType<typeof createClient>;
|
|
1160
|
+
private primaryStore: UserStoreAdapter;
|
|
1161
|
+
private ttl: number = 3600; // 1 hour
|
|
1162
|
+
|
|
1163
|
+
constructor(primaryStore: UserStoreAdapter, redisClient: ReturnType<typeof createClient>) {
|
|
1164
|
+
this.primaryStore = primaryStore;
|
|
1165
|
+
this.redis = redisClient;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async create(user: User): Promise<User> {
|
|
1169
|
+
const result = await this.primaryStore.create(user);
|
|
1170
|
+
// Cache the user
|
|
1171
|
+
await this.redis.setEx(
|
|
1172
|
+
`user:${user.id}`,
|
|
1173
|
+
this.ttl,
|
|
1174
|
+
JSON.stringify(result)
|
|
1175
|
+
);
|
|
1176
|
+
return result;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
async findById(id: string): Promise<User | undefined> {
|
|
1180
|
+
// Try cache first
|
|
1181
|
+
const cached = await this.redis.get(`user:${id}`);
|
|
1182
|
+
if (cached) {
|
|
1183
|
+
return JSON.parse(cached);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Fallback to primary store
|
|
1187
|
+
const user = await this.primaryStore.findById(id);
|
|
1188
|
+
if (user) {
|
|
1189
|
+
await this.redis.setEx(
|
|
1190
|
+
`user:${id}`,
|
|
1191
|
+
this.ttl,
|
|
1192
|
+
JSON.stringify(user)
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
return user;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
async save(user: StoredUser): Promise<void> {
|
|
1199
|
+
await this.primaryStore.save(user);
|
|
1200
|
+
// Invalidate cache
|
|
1201
|
+
await this.redis.del(`user:${user.id}`);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
async list(): Promise<User[]> {
|
|
1205
|
+
return this.primaryStore.list();
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Usage
|
|
1210
|
+
import { createClient } from 'redis';
|
|
1211
|
+
|
|
1212
|
+
const redis = createClient({ url: 'redis://localhost:6379' });
|
|
1213
|
+
await redis.connect();
|
|
1214
|
+
|
|
1215
|
+
const sdk = new ChatSDK({
|
|
1216
|
+
userStore: new CachedUserStore(new PostgreSQLUserStore(pool), redis),
|
|
1217
|
+
messageStore: new PostgreSQLMessageStore(pool),
|
|
1218
|
+
groupStore: new PostgreSQLGroupStore(pool),
|
|
1219
|
+
});
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
---
|
|
1223
|
+
|
|
1224
|
+
### Best Practices
|
|
1225
|
+
|
|
1226
|
+
#### 1. Connection Pooling
|
|
1227
|
+
|
|
1228
|
+
```typescript
|
|
1229
|
+
// โ
DO: Use connection pooling
|
|
1230
|
+
const pool = new Pool({ max: 20, min: 5 });
|
|
1231
|
+
|
|
1232
|
+
// โ DON'T: Create new connections for each query
|
|
1233
|
+
const client = new Client();
|
|
1234
|
+
await client.connect();
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
#### 2. Error Handling
|
|
1238
|
+
|
|
1239
|
+
```typescript
|
|
1240
|
+
export class PostgreSQLUserStore implements UserStoreAdapter {
|
|
1241
|
+
async create(user: User): Promise<User> {
|
|
1242
|
+
try {
|
|
1243
|
+
await this.pool.query(/* ... */);
|
|
1244
|
+
return user;
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
if (error.code === '23505') { // Unique violation
|
|
1247
|
+
throw new Error(`User ${user.username} already exists`);
|
|
1248
|
+
}
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
```
|
|
1254
|
+
|
|
1255
|
+
#### 3. Transactions
|
|
1256
|
+
|
|
1257
|
+
```typescript
|
|
1258
|
+
// Use transactions for multi-step operations
|
|
1259
|
+
async create(group: Group): Promise<Group> {
|
|
1260
|
+
const client = await this.pool.connect();
|
|
1261
|
+
try {
|
|
1262
|
+
await client.query('BEGIN');
|
|
1263
|
+
// Multiple operations...
|
|
1264
|
+
await client.query('COMMIT');
|
|
1265
|
+
return group;
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
await client.query('ROLLBACK');
|
|
1268
|
+
throw error;
|
|
1269
|
+
} finally {
|
|
1270
|
+
client.release();
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
#### 4. Indexing
|
|
1276
|
+
|
|
1277
|
+
```sql
|
|
1278
|
+
-- Index frequently queried fields
|
|
1279
|
+
CREATE INDEX idx_messages_receiver_time ON messages(receiver_id, timestamp);
|
|
1280
|
+
CREATE INDEX idx_messages_group_time ON messages(group_id, timestamp);
|
|
1281
|
+
CREATE INDEX idx_users_username ON users(username);
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
#### 5. Data Migration
|
|
1285
|
+
|
|
1286
|
+
When migrating from in-memory to database storage:
|
|
1287
|
+
|
|
1288
|
+
```typescript
|
|
1289
|
+
// Export data from in-memory store
|
|
1290
|
+
const users = await inMemoryStore.list();
|
|
1291
|
+
|
|
1292
|
+
// Import to database
|
|
1293
|
+
for (const user of users) {
|
|
1294
|
+
await dbStore.create(user);
|
|
1295
|
+
}
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
---
|
|
1299
|
+
|
|
1300
|
+
## โ๏ธ React Integration
|
|
1301
|
+
|
|
1302
|
+
### Context Provider
|
|
1303
|
+
|
|
1304
|
+
```typescript
|
|
1305
|
+
// contexts/SDKContext.tsx
|
|
1306
|
+
import { createContext, useContext, useState, useEffect } from 'react';
|
|
1307
|
+
import { ChatSDK, User, EVENTS, ConnectionState } from 'chatly-sdk';
|
|
1308
|
+
|
|
1309
|
+
interface SDKContextType {
|
|
1310
|
+
sdk: ChatSDK;
|
|
1311
|
+
currentUser: User | null;
|
|
1312
|
+
connectionState: ConnectionState;
|
|
1313
|
+
setCurrentUser: (user: User) => Promise<void>;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const SDKContext = createContext<SDKContextType | undefined>(undefined);
|
|
1317
|
+
|
|
1318
|
+
export function SDKProvider({ children }: { children: React.ReactNode }) {
|
|
1319
|
+
const [sdk] = useState(() => new ChatSDK({
|
|
1320
|
+
userStore: new InMemoryUserStore(),
|
|
1321
|
+
messageStore: new InMemoryMessageStore(),
|
|
1322
|
+
groupStore: new InMemoryGroupStore(),
|
|
1323
|
+
transport: new WebSocketClient('wss://your-server.com/ws'),
|
|
1324
|
+
}));
|
|
247
1325
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1326
|
+
const [currentUser, setCurrentUserState] = useState<User | null>(null);
|
|
1327
|
+
const [connectionState, setConnectionState] = useState<ConnectionState>(
|
|
1328
|
+
ConnectionState.DISCONNECTED
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
useEffect(() => {
|
|
1332
|
+
// Listen for connection state changes
|
|
1333
|
+
sdk.on(EVENTS.CONNECTION_STATE_CHANGED, setConnectionState);
|
|
1334
|
+
|
|
1335
|
+
return () => {
|
|
1336
|
+
sdk.off(EVENTS.CONNECTION_STATE_CHANGED, setConnectionState);
|
|
1337
|
+
};
|
|
1338
|
+
}, [sdk]);
|
|
1339
|
+
|
|
1340
|
+
const setCurrentUser = async (user: User) => {
|
|
1341
|
+
setCurrentUserState(user);
|
|
1342
|
+
await sdk.setCurrentUser(user);
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
return (
|
|
1346
|
+
<SDKContext.Provider value={{ sdk, currentUser, connectionState, setCurrentUser }}>
|
|
1347
|
+
{children}
|
|
1348
|
+
</SDKContext.Provider>
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
export function useSDK() {
|
|
1353
|
+
const context = useContext(SDKContext);
|
|
1354
|
+
if (!context) throw new Error('useSDK must be used within SDKProvider');
|
|
1355
|
+
return context;
|
|
1356
|
+
}
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
### Custom Hooks
|
|
1360
|
+
|
|
1361
|
+
```typescript
|
|
1362
|
+
// hooks/useMessages.ts
|
|
1363
|
+
import { useState, useEffect } from 'react';
|
|
1364
|
+
import { Message, ChatSession, EVENTS } from 'chatly-sdk';
|
|
1365
|
+
import { useSDK } from '../contexts/SDKContext';
|
|
1366
|
+
|
|
1367
|
+
export function useMessages(session: ChatSession | null) {
|
|
1368
|
+
const { sdk, currentUser } = useSDK();
|
|
1369
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
1370
|
+
const [decrypted, setDecrypted] = useState<Map<string, string>>(new Map());
|
|
1371
|
+
|
|
1372
|
+
useEffect(() => {
|
|
1373
|
+
if (!session || !currentUser) return;
|
|
1374
|
+
|
|
1375
|
+
// Load existing messages
|
|
1376
|
+
const loadMessages = async () => {
|
|
1377
|
+
const msgs = await sdk.getMessagesForUser(currentUser.id);
|
|
1378
|
+
setMessages(msgs);
|
|
1379
|
+
|
|
1380
|
+
// Decrypt messages
|
|
1381
|
+
const decryptedMap = new Map();
|
|
1382
|
+
for (const msg of msgs) {
|
|
1383
|
+
const plaintext = await sdk.decryptMessage(msg, currentUser);
|
|
1384
|
+
decryptedMap.set(msg.id, plaintext);
|
|
1385
|
+
}
|
|
1386
|
+
setDecrypted(decryptedMap);
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
loadMessages();
|
|
1390
|
+
|
|
1391
|
+
// Listen for new messages
|
|
1392
|
+
const handleNewMessage = async (message: Message) => {
|
|
1393
|
+
setMessages(prev => [...prev, message]);
|
|
1394
|
+
const plaintext = await sdk.decryptMessage(message, currentUser);
|
|
1395
|
+
setDecrypted(prev => new Map(prev).set(message.id, plaintext));
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
sdk.on(EVENTS.MESSAGE_RECEIVED, handleNewMessage);
|
|
1399
|
+
|
|
1400
|
+
return () => {
|
|
1401
|
+
sdk.off(EVENTS.MESSAGE_RECEIVED, handleNewMessage);
|
|
1402
|
+
};
|
|
1403
|
+
}, [session, currentUser, sdk]);
|
|
1404
|
+
|
|
1405
|
+
const sendMessage = async (text: string) => {
|
|
1406
|
+
if (!session) return;
|
|
1407
|
+
const message = await sdk.sendMessage(session, text);
|
|
1408
|
+
setMessages(prev => [...prev, message]);
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
return { messages, decrypted, sendMessage };
|
|
1412
|
+
}
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
### Component Usage
|
|
1416
|
+
|
|
1417
|
+
```typescript
|
|
1418
|
+
// components/ChatView.tsx
|
|
1419
|
+
import { useSDK } from '../contexts/SDKContext';
|
|
1420
|
+
import { useMessages } from '../hooks/useMessages';
|
|
1421
|
+
|
|
1422
|
+
function ChatView() {
|
|
1423
|
+
const { sdk, currentUser, connectionState } = useSDK();
|
|
1424
|
+
const [session, setSession] = useState<ChatSession | null>(null);
|
|
1425
|
+
const { messages, decrypted, sendMessage } = useMessages(session);
|
|
1426
|
+
const [input, setInput] = useState('');
|
|
1427
|
+
|
|
1428
|
+
const handleSend = async () => {
|
|
1429
|
+
await sendMessage(input);
|
|
1430
|
+
setInput('');
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
return (
|
|
1434
|
+
<div>
|
|
1435
|
+
<div className="connection-status">
|
|
1436
|
+
{connectionState === ConnectionState.CONNECTED ? '๐ข' : '๐ด'} {connectionState}
|
|
1437
|
+
</div>
|
|
1438
|
+
|
|
1439
|
+
<div className="messages">
|
|
1440
|
+
{messages.map(msg => (
|
|
1441
|
+
<div key={msg.id}>
|
|
1442
|
+
{decrypted.get(msg.id) || 'Decrypting...'}
|
|
1443
|
+
</div>
|
|
1444
|
+
))}
|
|
1445
|
+
</div>
|
|
1446
|
+
|
|
1447
|
+
<input
|
|
1448
|
+
value={input}
|
|
1449
|
+
onChange={(e) => setInput(e.target.value)}
|
|
1450
|
+
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
|
|
1451
|
+
/>
|
|
1452
|
+
</div>
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
---
|
|
1458
|
+
|
|
1459
|
+
## ๐ก๏ธ Error Handling
|
|
1460
|
+
|
|
1461
|
+
The SDK uses typed errors for better error handling:
|
|
1462
|
+
|
|
1463
|
+
```typescript
|
|
1464
|
+
import {
|
|
1465
|
+
ValidationError,
|
|
1466
|
+
NetworkError,
|
|
1467
|
+
SessionError,
|
|
1468
|
+
EncryptionError,
|
|
1469
|
+
StorageError
|
|
1470
|
+
} from 'chatly-sdk';
|
|
1471
|
+
|
|
1472
|
+
try {
|
|
1473
|
+
await sdk.sendMessage(session, message);
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
if (error instanceof ValidationError) {
|
|
1476
|
+
// Show validation error to user
|
|
1477
|
+
alert(`Invalid input: ${error.message}`);
|
|
1478
|
+
} else if (error instanceof NetworkError) {
|
|
1479
|
+
// Network error - check if retryable
|
|
1480
|
+
if (error.retryable) {
|
|
1481
|
+
console.log('Will retry automatically');
|
|
1482
|
+
} else {
|
|
1483
|
+
alert('Network error - please check your connection');
|
|
1484
|
+
}
|
|
1485
|
+
} else if (error instanceof SessionError) {
|
|
1486
|
+
// Session error - user not logged in
|
|
1487
|
+
redirectToLogin();
|
|
1488
|
+
} else if (error instanceof EncryptionError) {
|
|
1489
|
+
// Encryption failed - keys may be corrupted
|
|
1490
|
+
console.error('Encryption error:', error.details);
|
|
1491
|
+
} else if (error instanceof StorageError) {
|
|
1492
|
+
// Database error
|
|
1493
|
+
console.error('Storage error:', error.details);
|
|
252
1494
|
}
|
|
253
1495
|
}
|
|
254
1496
|
```
|
|
255
1497
|
|
|
256
|
-
|
|
1498
|
+
---
|
|
257
1499
|
|
|
258
|
-
|
|
1500
|
+
## ๐ API Reference
|
|
259
1501
|
|
|
260
|
-
|
|
261
|
-
- **Encryption**: AES-256-GCM
|
|
262
|
-
- **Key Derivation**: PBKDF2 with SHA-256
|
|
263
|
-
- **Key Storage**: Base64-encoded strings
|
|
1502
|
+
### ChatSDK
|
|
264
1503
|
|
|
265
|
-
|
|
1504
|
+
#### Constructor
|
|
266
1505
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
- All messages use unique IVs for encryption
|
|
1506
|
+
```typescript
|
|
1507
|
+
new ChatSDK(config: ChatSDKConfig)
|
|
1508
|
+
```
|
|
271
1509
|
|
|
272
|
-
|
|
1510
|
+
**Config Options:**
|
|
1511
|
+
- `userStore: UserStoreAdapter` - User storage adapter
|
|
1512
|
+
- `messageStore: MessageStoreAdapter` - Message storage adapter
|
|
1513
|
+
- `groupStore: GroupStoreAdapter` - Group storage adapter
|
|
1514
|
+
- `transport?: TransportAdapter` - Optional transport layer
|
|
1515
|
+
- `logLevel?: LogLevel` - Optional log level (DEBUG, INFO, WARN, ERROR, NONE)
|
|
273
1516
|
|
|
274
|
-
|
|
1517
|
+
#### Methods
|
|
275
1518
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
1519
|
+
| Method | Description | Returns |
|
|
1520
|
+
|--------|-------------|---------|
|
|
1521
|
+
| `createUser(username)` | Create a new user | `Promise<User>` |
|
|
1522
|
+
| `importUser(userData)` | Import existing user | `Promise<User>` |
|
|
1523
|
+
| `setCurrentUser(user)` | Set active user | `Promise<void>` |
|
|
1524
|
+
| `getCurrentUser()` | Get active user | `User \| null` |
|
|
1525
|
+
| `startSession(userA, userB)` | Start 1:1 chat | `Promise<ChatSession>` |
|
|
1526
|
+
| `createGroup(name, members)` | Create group | `Promise<GroupSession>` |
|
|
1527
|
+
| `loadGroup(id)` | Load existing group | `Promise<GroupSession>` |
|
|
1528
|
+
| `sendMessage(session, text)` | Send message | `Promise<Message>` |
|
|
1529
|
+
| `decryptMessage(message, user)` | Decrypt message | `Promise<string>` |
|
|
1530
|
+
| `getMessagesForUser(userId)` | Get user messages | `Promise<Message[]>` |
|
|
1531
|
+
| `getMessagesForGroup(groupId)` | Get group messages | `Promise<Message[]>` |
|
|
1532
|
+
| `listUsers()` | Get all users | `Promise<User[]>` |
|
|
1533
|
+
| `getUserById(id)` | Get user by ID | `Promise<User \| undefined>` |
|
|
1534
|
+
| `listGroups()` | Get all groups | `Promise<Group[]>` |
|
|
1535
|
+
| `getConnectionState()` | Get connection state | `ConnectionState` |
|
|
1536
|
+
| `isConnected()` | Check if connected | `boolean` |
|
|
1537
|
+
| `disconnect()` | Disconnect transport | `Promise<void>` |
|
|
1538
|
+
| `reconnect()` | Reconnect transport | `Promise<void>` |
|
|
1539
|
+
| `getQueueStatus()` | Get message queue status | `QueueStatus` |
|
|
1540
|
+
|
|
1541
|
+
#### Events
|
|
1542
|
+
|
|
1543
|
+
| Event | Payload | Description |
|
|
1544
|
+
|-------|---------|-------------|
|
|
1545
|
+
| `message:sent` | `Message` | Message sent successfully |
|
|
1546
|
+
| `message:received` | `Message` | Message received |
|
|
1547
|
+
| `message:failed` | `Message, Error` | Message send failed |
|
|
1548
|
+
| `connection:state` | `ConnectionState` | Connection state changed |
|
|
1549
|
+
| `session:created` | `ChatSession` | Chat session created |
|
|
1550
|
+
| `group:created` | `GroupSession` | Group created |
|
|
1551
|
+
| `user:created` | `User` | User created |
|
|
1552
|
+
| `error` | `Error` | SDK error occurred |
|
|
1553
|
+
|
|
1554
|
+
---
|
|
1555
|
+
|
|
1556
|
+
## ๐ Security Best Practices
|
|
1557
|
+
|
|
1558
|
+
### 1. Secure Key Storage
|
|
279
1559
|
|
|
280
|
-
|
|
1560
|
+
```typescript
|
|
1561
|
+
// โ DON'T: Store private keys in plaintext
|
|
1562
|
+
localStorage.setItem('privateKey', user.privateKey);
|
|
281
1563
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
1564
|
+
// โ
DO: Encrypt private keys with user password
|
|
1565
|
+
import { encryptWithPassword } from './crypto';
|
|
1566
|
+
const encrypted = await encryptWithPassword(user.privateKey, userPassword);
|
|
1567
|
+
localStorage.setItem('encryptedKey', encrypted);
|
|
285
1568
|
```
|
|
286
1569
|
|
|
287
|
-
|
|
1570
|
+
### 2. Use HTTPS/WSS
|
|
288
1571
|
|
|
289
|
-
```
|
|
290
|
-
|
|
1572
|
+
```typescript
|
|
1573
|
+
// โ DON'T: Use unencrypted connections
|
|
1574
|
+
const transport = new WebSocketClient('ws://server.com');
|
|
1575
|
+
|
|
1576
|
+
// โ
DO: Use secure WebSocket
|
|
1577
|
+
const transport = new WebSocketClient('wss://server.com');
|
|
1578
|
+
```
|
|
1579
|
+
|
|
1580
|
+
### 3. Validate All Input
|
|
1581
|
+
|
|
1582
|
+
```typescript
|
|
1583
|
+
// โ
SDK automatically validates
|
|
1584
|
+
await sdk.createUser('alice'); // โ
Valid
|
|
1585
|
+
await sdk.createUser('ab'); // โ Throws ValidationError
|
|
1586
|
+
await sdk.sendMessage(session, ''); // โ Throws ValidationError
|
|
291
1587
|
```
|
|
292
1588
|
|
|
293
|
-
|
|
294
|
-
- `dist/index.js` - ES module bundle
|
|
295
|
-
- `dist/index.d.ts` - TypeScript definitions
|
|
1589
|
+
### 4. Handle Errors Properly
|
|
296
1590
|
|
|
297
|
-
|
|
1591
|
+
```typescript
|
|
1592
|
+
// โ
Use typed errors
|
|
1593
|
+
sdk.on(EVENTS.ERROR, (error) => {
|
|
1594
|
+
if (error instanceof NetworkError && error.retryable) {
|
|
1595
|
+
// Will retry automatically
|
|
1596
|
+
} else {
|
|
1597
|
+
// Log to error tracking service
|
|
1598
|
+
Sentry.captureException(error);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
```
|
|
298
1602
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
1603
|
+
---
|
|
1604
|
+
|
|
1605
|
+
## ๐งช Testing
|
|
302
1606
|
|
|
1607
|
+
```bash
|
|
303
1608
|
# Run tests
|
|
304
1609
|
npm test
|
|
305
1610
|
|
|
306
|
-
#
|
|
307
|
-
npm run
|
|
1611
|
+
# Watch mode
|
|
1612
|
+
npm run test:watch
|
|
1613
|
+
|
|
1614
|
+
# Coverage
|
|
1615
|
+
npm run test:coverage
|
|
308
1616
|
```
|
|
309
1617
|
|
|
310
|
-
|
|
1618
|
+
### Example Test
|
|
1619
|
+
|
|
1620
|
+
```typescript
|
|
1621
|
+
import { ChatSDK, InMemoryUserStore } from 'chatly-sdk';
|
|
1622
|
+
|
|
1623
|
+
describe('ChatSDK', () => {
|
|
1624
|
+
it('should create and decrypt messages', async () => {
|
|
1625
|
+
const sdk = new ChatSDK({
|
|
1626
|
+
userStore: new InMemoryUserStore(),
|
|
1627
|
+
messageStore: new InMemoryMessageStore(),
|
|
1628
|
+
groupStore: new InMemoryGroupStore(),
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
const alice = await sdk.createUser('alice');
|
|
1632
|
+
const bob = await sdk.createUser('bob');
|
|
1633
|
+
const session = await sdk.startSession(alice, bob);
|
|
1634
|
+
|
|
1635
|
+
sdk.setCurrentUser(alice);
|
|
1636
|
+
const message = await sdk.sendMessage(session, 'Hello!');
|
|
1637
|
+
|
|
1638
|
+
const decrypted = await sdk.decryptMessage(message, bob);
|
|
1639
|
+
expect(decrypted).toBe('Hello!');
|
|
1640
|
+
});
|
|
1641
|
+
});
|
|
1642
|
+
```
|
|
1643
|
+
|
|
1644
|
+
---
|
|
1645
|
+
|
|
1646
|
+
## ๐ Examples
|
|
1647
|
+
|
|
1648
|
+
Check out the [examples](./examples) directory for complete implementations:
|
|
1649
|
+
|
|
1650
|
+
- **Basic Chat** - Simple 1:1 messaging
|
|
1651
|
+
- **Group Chat** - Multi-user groups
|
|
1652
|
+
- **React App** - Full React integration
|
|
1653
|
+
- **WebSocket Server** - Node.js WebSocket server
|
|
1654
|
+
- **MongoDB Integration** - Database persistence
|
|
1655
|
+
|
|
1656
|
+
---
|
|
1657
|
+
|
|
1658
|
+
## ๐ค Contributing
|
|
1659
|
+
|
|
1660
|
+
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
1661
|
+
|
|
1662
|
+
---
|
|
1663
|
+
|
|
1664
|
+
## ๐ License
|
|
1665
|
+
|
|
1666
|
+
MIT ยฉ [Bharath](https://github.com/bharath-arch)
|
|
1667
|
+
|
|
1668
|
+
---
|
|
1669
|
+
|
|
1670
|
+
## ๐ Links
|
|
1671
|
+
|
|
1672
|
+
- [NPM Package](https://www.npmjs.com/package/chatly-sdk)
|
|
1673
|
+
- [GitHub Repository](https://github.com/bharath-arch/chatly-sdk)
|
|
1674
|
+
- [Documentation](https://github.com/bharath-arch/chatly-sdk#readme)
|
|
1675
|
+
- [Issue Tracker](https://github.com/bharath-arch/chatly-sdk/issues)
|
|
1676
|
+
|
|
1677
|
+
---
|
|
1678
|
+
|
|
1679
|
+
## ๐ Support
|
|
1680
|
+
|
|
1681
|
+
- **Issues**: [GitHub Issues](https://github.com/bharath-arch/chatly-sdk/issues)
|
|
1682
|
+
- **Discussions**: [GitHub Discussions](https://github.com/bharath-arch/chatly-sdk/discussions)
|
|
1683
|
+
|
|
1684
|
+
---
|
|
311
1685
|
|
|
312
|
-
|
|
1686
|
+
**Built with โค๏ธ for secure, private messaging**
|