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