@droponair/sdk-js 0.3.0 → 0.3.1
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/README.md +125 -830
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,865 +1,160 @@
|
|
|
1
1
|
# DropOnAir JS SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
End-to-end encrypted messaging, group chat, voice/video calls, and broadcast channels for any app.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
DropOnAir operates as a **blind encrypted relay** - your users' encryption keys and message content never leave their devices.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Getting Started
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
1. Create a free account at [panel.droponair.com](https://panel.droponair.com)
|
|
10
|
+
2. Create an app in the dashboard to get your **App ID** and **Public API Key**
|
|
11
|
+
3. Install the SDK:
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- **Client-Side:** SDK generates X25519 keypairs locally, stores private keys in IndexedDB/SecureStorage
|
|
14
|
-
- **Your Backend:** Serves as public key directory (users publish public keys via `/api/messaging/keys/me`)
|
|
15
|
-
- **DropOnAir Service:** **NEVER** stores, validates, or accesses user keys - operates as blind relay
|
|
16
|
-
|
|
17
|
-
### Encryption Flow
|
|
18
|
-
|
|
19
|
-
1. **Identity Generation:** SDK generates X25519 keypair on first use, stores in local secure storage
|
|
20
|
-
2. **Key Exchange:** SDK fetches recipient's public key from key directory (not DropOnAir)
|
|
21
|
-
3. **Shared Secret Derivation:** SDK derives shared secret via X25519 ECDH, caches in-memory per peer
|
|
22
|
-
4. **Message Encryption:** SDK encrypts message with AES-256-GCM using derived shared secret
|
|
23
|
-
5. **Wire Format:** Binary protobuf Envelope with opaque `encryptedPayload` bytes
|
|
24
|
-
6. **Decryption:** Recipient SDK derives same shared secret, decrypts payload client-side
|
|
25
|
-
|
|
26
|
-
### Cryptographic Primitives
|
|
27
|
-
|
|
28
|
-
- **X25519 (Curve25519 ECDH):** Key agreement for deriving shared secrets
|
|
29
|
-
- **HKDF-SHA256:** Shared secret → AES-256 symmetric key derivation
|
|
30
|
-
- **AES-256-GCM:** Authenticated encryption with 96-bit nonce, 128-bit auth tag
|
|
31
|
-
- **Additional Authenticated Data (AAD):** Binds messageId, senderId, recipientId, timestamp to ciphertext
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## 📦 Installation
|
|
13
|
+
## Installation
|
|
36
14
|
|
|
37
15
|
```bash
|
|
38
16
|
npm install @droponair/sdk-js
|
|
39
17
|
```
|
|
40
18
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
## 🚀 Quick Start
|
|
44
|
-
|
|
45
|
-
### Basic Usage (TypeScript)
|
|
19
|
+
## Quick Start
|
|
46
20
|
|
|
47
21
|
```typescript
|
|
48
22
|
import { initialize } from '@droponair/sdk-js';
|
|
49
23
|
|
|
50
|
-
// Initialize SDK with backend integration
|
|
51
24
|
const client = await initialize({
|
|
52
|
-
appId: 'your-app-id',
|
|
53
|
-
publicApiKey: 'your-public-api-key',
|
|
54
|
-
|
|
55
|
-
// JWT callback: fetch fresh JWT from your backend
|
|
25
|
+
appId: 'your-app-id', // From the DropOnAir dashboard
|
|
26
|
+
publicApiKey: 'your-public-api-key', // From the DropOnAir dashboard
|
|
56
27
|
getUserJwt: async () => {
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
return
|
|
28
|
+
const res = await fetch('/api/auth/me', { credentials: 'include' });
|
|
29
|
+
const { jwt } = await res.json();
|
|
30
|
+
return jwt;
|
|
60
31
|
},
|
|
61
|
-
|
|
62
|
-
// Key directory endpoint (your backend serves public keys)
|
|
63
32
|
keyDirectoryEndpoint: '/api/messaging/keys',
|
|
64
|
-
|
|
65
|
-
// Token exchange endpoint (your backend → DropOnAir JWT)
|
|
66
33
|
tokenExchangeEndpoint: '/api/messaging/token-exchange',
|
|
67
|
-
|
|
68
|
-
// Optional: custom WebSocket/HTTP endpoints
|
|
69
|
-
messagingWsUrl: 'wss://sdk.droponair.com/ws/messages',
|
|
70
|
-
messagingHttpUrl: 'https://sdk.droponair.com',
|
|
71
|
-
|
|
72
|
-
// Auto-connect on initialization (default: true)
|
|
73
|
-
autoConnect: true
|
|
74
34
|
});
|
|
75
35
|
|
|
76
|
-
// Listen for incoming messages (
|
|
77
|
-
client.onMessage(({
|
|
78
|
-
console.log(
|
|
36
|
+
// Listen for incoming messages (already decrypted)
|
|
37
|
+
client.onMessage(({ fromUserId, plaintext, timestamp }) => {
|
|
38
|
+
console.log(`${fromUserId}: ${plaintext}`);
|
|
79
39
|
});
|
|
80
40
|
|
|
81
|
-
//
|
|
82
|
-
client.
|
|
83
|
-
if (type === 'LIMIT_REACHED') {
|
|
84
|
-
console.error('Rate limit reached');
|
|
85
|
-
} else if (type === 'ERROR') {
|
|
86
|
-
console.error('Error:', reason);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Send encrypted message (SDK handles encryption automatically)
|
|
91
|
-
const { messageId } = await client.sendMessage('recipient-user-id', 'Hello from Alice!');
|
|
92
|
-
console.log('Message sent:', messageId);
|
|
93
|
-
|
|
94
|
-
// Disconnect when done
|
|
95
|
-
client.disconnect();
|
|
41
|
+
// Send an E2EE message
|
|
42
|
+
await client.sendMessage('recipient-user-id', 'Hello!');
|
|
96
43
|
```
|
|
97
44
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
const client = await initialize({
|
|
102
|
-
appId: 'your-app-id',
|
|
103
|
-
publicApiKey: 'your-public-api-key',
|
|
104
|
-
getUserJwt: async () => fetchJwt(),
|
|
105
|
-
autoConnect: false // Don't connect immediately
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Connect manually when ready
|
|
109
|
-
await client.connect();
|
|
110
|
-
|
|
111
|
-
// Disconnect and prevent auto-reconnect
|
|
112
|
-
client.disconnect();
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## 🔧 API Reference
|
|
118
|
-
|
|
119
|
-
### `initialize(options)`
|
|
120
|
-
|
|
121
|
-
Initializes the DropOnAir client with end-to-end encryption.
|
|
45
|
+
## Features
|
|
122
46
|
|
|
123
|
-
**
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
- `autoConnect` (boolean, optional): Auto-connect on initialization (default: `true`)
|
|
132
|
-
- `storage` (KeyStorageAdapter, optional): Custom storage adapter (default: IndexedDB with in-memory fallback)
|
|
133
|
-
- `fetchFn` (function, optional): Custom fetch implementation (default: `globalThis.fetch`)
|
|
47
|
+
- **E2EE Messaging** - X25519 key agreement, AES-256-GCM encryption, multi-device support
|
|
48
|
+
- **Cleartext Messaging** - Lightweight messages without E2EE overhead
|
|
49
|
+
- **Broadcast Channels** - Pub/sub for announcements and notifications
|
|
50
|
+
- **Group Messaging** - Server-managed groups with member roles
|
|
51
|
+
- **Voice & Video Calls** - 1-to-1 WebRTC call signaling with TURN support
|
|
52
|
+
- **Group Calls** - Mesh WebRTC group calls with per-participant signaling
|
|
53
|
+
- **Offline Delivery** - Messages queued and delivered when recipients reconnect
|
|
54
|
+
- **Multi-Device** - Per-device encryption with automatic self-sync
|
|
134
55
|
|
|
135
|
-
|
|
56
|
+
## API Reference
|
|
136
57
|
|
|
137
|
-
|
|
58
|
+
### Initialization
|
|
138
59
|
|
|
139
|
-
### `client.connect()`
|
|
140
|
-
|
|
141
|
-
Connects to DropOnAir messaging service. Automatically:
|
|
142
|
-
- Generates or retrieves X25519 identity keypair from secure storage
|
|
143
|
-
- Publishes public key to key directory (PUT `/api/messaging/keys/me`)
|
|
144
|
-
- Exchanges user JWT for DropOnAir JWT
|
|
145
|
-
- Opens WebSocket connection with JWT authentication
|
|
146
|
-
- Fetches and decrypts offline messages
|
|
147
|
-
|
|
148
|
-
**Returns:** `Promise<void>`
|
|
149
|
-
|
|
150
|
-
---
|
|
151
|
-
|
|
152
|
-
### `client.disconnect()`
|
|
153
|
-
|
|
154
|
-
Disconnects from DropOnAir and stops auto-reconnect.
|
|
155
|
-
|
|
156
|
-
**Returns:** `void`
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
### `client.sendMessage(toUserId, plaintext)`
|
|
161
|
-
|
|
162
|
-
Encrypts and sends a message to a recipient.
|
|
163
|
-
|
|
164
|
-
**Parameters:**
|
|
165
|
-
- `toUserId` (string): Recipient's user ID (must match JWT subject in their their identity)
|
|
166
|
-
- `plaintext` (string): Message content (will be encrypted client-side)
|
|
167
|
-
|
|
168
|
-
**Flow:**
|
|
169
|
-
1. Fetch recipient's public key from key directory (if not cached)
|
|
170
|
-
2. Derive shared secret via X25519 ECDH
|
|
171
|
-
3. Encrypt plaintext with AES-256-GCM
|
|
172
|
-
4. Build protobuf Envelope with opaque encrypted payload
|
|
173
|
-
5. Send binary WebSocket frame to DropOnAir
|
|
174
|
-
|
|
175
|
-
**Returns:** `Promise<{ messageId: string }>`
|
|
176
|
-
|
|
177
|
-
**Throws:**
|
|
178
|
-
- WebSocket not connected
|
|
179
|
-
- Sender identity missing
|
|
180
|
-
- Rate limit reached (`LIMIT_REACHED` event)
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
### `client.onMessage(callback)`
|
|
185
|
-
|
|
186
|
-
Registers a callback for incoming decrypted messages.
|
|
187
|
-
|
|
188
|
-
**Callback signature:**
|
|
189
60
|
```typescript
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
case 'CALL_SDP_ANSWER':
|
|
284
|
-
await handleSdpAnswer(event.callId!, event.payload!);
|
|
285
|
-
break;
|
|
286
|
-
|
|
287
|
-
case 'CALL_ICE_CANDIDATE':
|
|
288
|
-
await handleIceCandidate(event.callId!, event.payload!);
|
|
289
|
-
break;
|
|
290
|
-
|
|
291
|
-
case 'CALL_VIDEO_TOGGLE':
|
|
292
|
-
handleVideoToggle(event.callId!, event.payload!);
|
|
293
|
-
break;
|
|
294
|
-
|
|
295
|
-
case 'CALL_REJECTED':
|
|
296
|
-
case 'CALL_ENDED':
|
|
297
|
-
closeCall(event.callId!);
|
|
298
|
-
break;
|
|
299
|
-
|
|
300
|
-
case 'CALL_DENIED_LIMIT_REACHED':
|
|
301
|
-
showError('Call limit reached. Upgrade your plan to continue.');
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// ── TURN credentials (required for calls behind NAT/firewall) ────────────────
|
|
307
|
-
const turn = await client.fetchTurnCredentials();
|
|
308
|
-
const pc = new RTCPeerConnection({
|
|
309
|
-
iceServers: [{ urls: turn.uri, username: turn.username, credential: turn.password }]
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// ── Initiate an outgoing call ────────────────────────────────────────────────
|
|
313
|
-
const callId = await client.startCall('recipient-user-id');
|
|
314
|
-
|
|
315
|
-
// ── Accept / reject incoming call ────────────────────────────────────────────
|
|
316
|
-
await client.acceptCall(callId);
|
|
317
|
-
await client.rejectCall(callId);
|
|
318
|
-
|
|
319
|
-
// ── Send WebRTC signals through DropOnAir relay ──────────────────────────────
|
|
320
|
-
pc.onicecandidate = ({ candidate }) => {
|
|
321
|
-
if (candidate) {
|
|
322
|
-
client.sendCallSignal('CALL_ICE_CANDIDATE', callId, JSON.stringify(candidate));
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const offer = await pc.createOffer();
|
|
327
|
-
await pc.setLocalDescription(offer);
|
|
328
|
-
client.sendCallSignal('CALL_SDP_OFFER', callId, JSON.stringify(offer));
|
|
329
|
-
|
|
330
|
-
// ── Toggle video on/off during a call ────────────────────────────────────────
|
|
331
|
-
client.toggleVideo(callId, true); // Enable video
|
|
332
|
-
client.toggleVideo(callId, false); // Disable video (voice-only)
|
|
333
|
-
|
|
334
|
-
// ── End call ─────────────────────────────────────────────────────────────────
|
|
335
|
-
await client.endCall(callId);
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
---
|
|
339
|
-
|
|
340
|
-
### Call API Reference
|
|
341
|
-
|
|
342
|
-
#### `client.startCall(targetUserId)`
|
|
343
|
-
|
|
344
|
-
Initiates an outgoing call to a user. Resolves with the server-assigned `callId` once the remote party starts ringing.
|
|
345
|
-
|
|
346
|
-
| Parameter | Type | Description |
|
|
347
|
-
|-----------|------|-------------|
|
|
348
|
-
| `targetUserId` | string | Recipient's DropOnAir user ID |
|
|
349
|
-
|
|
350
|
-
**Returns:** `Promise<string>`, the `callId`
|
|
351
|
-
|
|
352
|
-
---
|
|
353
|
-
|
|
354
|
-
#### `client.acceptCall(callId)`
|
|
355
|
-
|
|
356
|
-
Accepts an incoming call. After resolving, initiate WebRTC negotiation via `sendCallSignal`.
|
|
357
|
-
|
|
358
|
-
**Returns:** `Promise<void>`
|
|
359
|
-
|
|
360
|
-
---
|
|
361
|
-
|
|
362
|
-
#### `client.rejectCall(callId)`
|
|
363
|
-
|
|
364
|
-
Rejects an incoming call invitation.
|
|
365
|
-
|
|
366
|
-
**Returns:** `Promise<void>`
|
|
367
|
-
|
|
368
|
-
---
|
|
369
|
-
|
|
370
|
-
#### `client.endCall(callId)`
|
|
371
|
-
|
|
372
|
-
Ends an active call or cancels an unanswered outgoing invite.
|
|
373
|
-
|
|
374
|
-
**Returns:** `Promise<void>`
|
|
375
|
-
|
|
376
|
-
---
|
|
377
|
-
|
|
378
|
-
#### `client.toggleVideo(callId, enabled)`
|
|
379
|
-
|
|
380
|
-
Toggles the local video track on or off. Sends a `CALL_VIDEO_TOGGLE` signal to the peer so their UI can reflect the change.
|
|
381
|
-
|
|
382
|
-
| Parameter | Type | Description |
|
|
383
|
-
|-----------|------|-------------|
|
|
384
|
-
| `callId` | string | Active call ID |
|
|
385
|
-
| `enabled` | boolean | `true` = video on, `false` = voice-only |
|
|
386
|
-
|
|
387
|
-
**Returns:** `void`
|
|
388
|
-
|
|
389
|
-
---
|
|
390
|
-
|
|
391
|
-
#### `client.sendCallSignal(type, callId, payload)`
|
|
392
|
-
|
|
393
|
-
Sends a raw WebRTC signaling frame to the peer over the DropOnAir relay.
|
|
394
|
-
|
|
395
|
-
| Parameter | Type | Description |
|
|
396
|
-
|-----------|------|-------------|
|
|
397
|
-
| `type` | `'CALL_SDP_OFFER'` \| `'CALL_SDP_ANSWER'` \| `'CALL_ICE_CANDIDATE'` | Signal type |
|
|
398
|
-
| `callId` | string | Active call ID |
|
|
399
|
-
| `payload` | string | JSON-serialized SDP or ICE candidate |
|
|
400
|
-
|
|
401
|
-
**Returns:** `void`
|
|
402
|
-
|
|
403
|
-
---
|
|
404
|
-
|
|
405
|
-
#### `client.onCallEvent(callback)`
|
|
406
|
-
|
|
407
|
-
Registers a listener for all call lifecycle and signaling events.
|
|
408
|
-
|
|
409
|
-
**Callback signature:**
|
|
410
|
-
```typescript
|
|
411
|
-
(event: CallEvent) => void
|
|
412
|
-
|
|
413
|
-
interface CallEvent {
|
|
414
|
-
type: CallEventType | string;
|
|
415
|
-
callId?: string;
|
|
416
|
-
targetUserId?: string;
|
|
417
|
-
payload?: string; // JSON, SDP, ICE candidate, or metadata
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
type CallEventType =
|
|
421
|
-
| 'CALL_INVITE' // Incoming call from another user
|
|
422
|
-
| 'CALL_RINGING' // Remote party is ringing
|
|
423
|
-
| 'CALL_ACCEPTED' // Call accepted, begin WebRTC negotiation
|
|
424
|
-
| 'CALL_REJECTED' // Call rejected by remote party
|
|
425
|
-
| 'CALL_ENDED' // Call ended (by either party)
|
|
426
|
-
| 'CALL_SDP_OFFER' // WebRTC SDP offer received
|
|
427
|
-
| 'CALL_SDP_ANSWER' // WebRTC SDP answer received
|
|
428
|
-
| 'CALL_ICE_CANDIDATE' // ICE candidate received
|
|
429
|
-
| 'CALL_VIDEO_TOGGLE' // Remote peer toggled video
|
|
430
|
-
| 'CALL_DENIED_LIMIT_REACHED'; // Call rejected, plan limit reached
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
**Returns:** Unsubscribe function `() => void`
|
|
434
|
-
|
|
435
|
-
---
|
|
436
|
-
|
|
437
|
-
#### `client.fetchTurnCredentials()`
|
|
438
|
-
|
|
439
|
-
Fetches short-lived TURN server credentials from DropOnAir for ICE negotiation. Always call this before creating an `RTCPeerConnection` to ensure NAT traversal works reliably.
|
|
440
|
-
|
|
441
|
-
**Returns:** `Promise<TurnCredentials>`
|
|
442
|
-
|
|
443
|
-
```typescript
|
|
444
|
-
interface TurnCredentials {
|
|
445
|
-
username: string;
|
|
446
|
-
password: string;
|
|
447
|
-
uri: string; // e.g. "turn:turn.droponair.com:3478"
|
|
448
|
-
ttlSeconds: number;
|
|
449
|
-
}
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
---
|
|
453
|
-
|
|
454
|
-
## 👥 Group Messaging & Calls
|
|
455
|
-
|
|
456
|
-
Groups support both E2EE and cleartext messaging, plus mesh WebRTC group calls.
|
|
457
|
-
|
|
458
|
-
### Group Management
|
|
459
|
-
|
|
460
|
-
```typescript
|
|
461
|
-
// Create a group
|
|
462
|
-
const group = await client.createGroup('Project Chat', ['user-1', 'user-2']);
|
|
463
|
-
|
|
464
|
-
// List your groups
|
|
465
|
-
const groups = await client.listGroups();
|
|
466
|
-
|
|
467
|
-
// Manage members
|
|
468
|
-
await client.addGroupMembers(group.groupId, ['user-3']);
|
|
469
|
-
await client.removeGroupMembers(group.groupId, ['user-1']);
|
|
470
|
-
|
|
471
|
-
// Get group details
|
|
472
|
-
const members = await client.getGroupMembers(group.groupId);
|
|
473
|
-
|
|
474
|
-
// Delete a group (owner only)
|
|
475
|
-
await client.deleteGroup(group.groupId);
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### Sending Group Messages
|
|
479
|
-
|
|
480
|
-
```typescript
|
|
481
|
-
// E2EE group message (sender-side fan-out, encrypts per member per device)
|
|
482
|
-
await client.sendGroupMessage(groupId, 'Hello team!');
|
|
483
|
-
|
|
484
|
-
// Cleartext group message (no crypto overhead)
|
|
485
|
-
await client.sendGroupMessage(groupId, 'Public announcement', { cleartext: true });
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
### Receiving Group Messages
|
|
489
|
-
|
|
490
|
-
```typescript
|
|
491
|
-
client.onGroupMessage((msg) => {
|
|
492
|
-
console.log(`[${msg.groupId}] ${msg.senderId}: ${msg.plaintext}`);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
// Remove listener
|
|
496
|
-
client.offGroupMessage(handler);
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
### Group Calls (Mesh WebRTC)
|
|
500
|
-
|
|
501
|
-
```typescript
|
|
502
|
-
// Start a group call
|
|
503
|
-
await client.startGroupCall(groupId, 'video');
|
|
504
|
-
|
|
505
|
-
// Join an existing group call
|
|
506
|
-
await client.joinGroupCall(groupId);
|
|
507
|
-
|
|
508
|
-
// Listen for group call events (OFFER, ANSWER, ICE, JOIN, LEAVE, END)
|
|
509
|
-
client.onGroupCallEvent((event) => {
|
|
510
|
-
switch (event.type) {
|
|
511
|
-
case 'OFFER':
|
|
512
|
-
// Handle incoming SDP offer from a participant
|
|
513
|
-
break;
|
|
514
|
-
case 'ANSWER':
|
|
515
|
-
// Handle SDP answer
|
|
516
|
-
break;
|
|
517
|
-
case 'ICE':
|
|
518
|
-
// Handle ICE candidate
|
|
519
|
-
break;
|
|
520
|
-
case 'JOIN':
|
|
521
|
-
// Participant joined
|
|
522
|
-
break;
|
|
523
|
-
case 'LEAVE':
|
|
524
|
-
// Participant left
|
|
525
|
-
break;
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// Leave a group call
|
|
530
|
-
await client.leaveGroupCall(groupId);
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### Group API Reference
|
|
534
|
-
|
|
535
|
-
| Method | Description |
|
|
536
|
-
|--------|-------------|
|
|
537
|
-
| `createGroup(name, memberIds)` | Create group with initial members |
|
|
538
|
-
| `listGroups()` | List groups the user belongs to |
|
|
539
|
-
| `getGroupMembers(groupId)` | Get group member list |
|
|
540
|
-
| `addGroupMembers(groupId, userIds)` | Add members (owner/admin) |
|
|
541
|
-
| `removeGroupMembers(groupId, userIds)` | Remove members (owner/admin) |
|
|
542
|
-
| `deleteGroup(groupId)` | Delete group (owner only) |
|
|
543
|
-
| `sendGroupMessage(groupId, text, opts?)` | Send E2EE or cleartext group message |
|
|
544
|
-
| `onGroupMessage(callback)` | Listen for inbound group messages |
|
|
545
|
-
| `offGroupMessage(callback)` | Remove group message listener |
|
|
546
|
-
| `startGroupCall(groupId, type)` | Start a group voice/video call |
|
|
547
|
-
| `joinGroupCall(groupId)` | Join an existing group call |
|
|
548
|
-
| `leaveGroupCall(groupId)` | Leave a group call |
|
|
549
|
-
| `endGroupCall(groupId)` | End a group call (initiator) |
|
|
550
|
-
| `sendGroupCallSignal(type, callId, target, payload)` | Send SDP/ICE to a peer |
|
|
551
|
-
| `onGroupCallEvent(callback)` | Listen for group call events |
|
|
552
|
-
| `offGroupCallEvent(callback)` | Remove group call event listener |
|
|
553
|
-
|
|
554
|
-
### Per-Plan Group Limits
|
|
555
|
-
|
|
556
|
-
| Limit | FREE | PRO | GROWTH | PAYG | ENTERPRISE |
|
|
557
|
-
|-------|------|-----|--------|------|------------|
|
|
558
|
-
| Groups / app | 1 | 20 | 100 | ∞ | ∞ |
|
|
559
|
-
| Members / group | 5 | 50 | 100 | ∞ | ∞ |
|
|
560
|
-
| Group messages / month | 500 | 10,000 | 50,000 | ∞ | ∞ |
|
|
561
|
-
| Group call minutes / month | 50 | 1,000 | 5,000 | ∞ | ∞ |
|
|
562
|
-
| Group call participants | 4 | 8 | 16 | 32 | Custom |
|
|
563
|
-
|
|
564
|
-
---
|
|
565
|
-
|
|
566
|
-
## 🏗️ Architecture
|
|
567
|
-
|
|
568
|
-
### Module Structure
|
|
569
|
-
|
|
570
|
-
```
|
|
571
|
-
droponair-sdk-js/
|
|
572
|
-
├── src/
|
|
573
|
-
│ ├── index.ts # Public API exports
|
|
574
|
-
│ ├── version.ts # SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION
|
|
575
|
-
│ ├── core/
|
|
576
|
-
│ │ ├── messaging-client.ts # WebSocket client + message routing
|
|
577
|
-
│ │ ├── session-manager.ts # Shared secret caching
|
|
578
|
-
│ │ ├── types.ts # TypeScript interfaces
|
|
579
|
-
│ │ └── bytes.ts # Encoding utilities
|
|
580
|
-
│ ├── crypto/
|
|
581
|
-
│ │ ├── crypto-service.ts # X25519 + AES-256-GCM encryption
|
|
582
|
-
│ │ └── payload-format.ts # Binary payload packing
|
|
583
|
-
│ ├── transport/
|
|
584
|
-
│ │ └── protobuf-codec.ts # Envelope/Ack/Event encoding
|
|
585
|
-
│ └── storage/
|
|
586
|
-
│ ├── indexeddb-key-storage.ts # Browser key storage
|
|
587
|
-
│ └── memory-key-storage.ts # In-memory fallback
|
|
588
|
-
└── test-e2ee.js # E2EE validation test
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
### Versioning
|
|
592
|
-
|
|
593
|
-
The SDK uses three distinct version numbers to ensure backward compatibility:
|
|
594
|
-
|
|
595
|
-
| Version | Location | Purpose |
|
|
596
|
-
|---------|----------|---------|
|
|
597
|
-
| **SDK_VERSION** (`0.2.0`) | `src/version.ts`, `package.json` | Semver of the JS/TS SDK package itself. Sent as `X-SDK-Version` header on token exchange and `sdkVersion` query param on WebSocket handshake. |
|
|
598
|
-
| **PROTOCOL_VERSION** (`1`) | `src/version.ts` | Wire protocol version, sent as `protocolVersion` query param on WebSocket connect. Increment when adding required proto fields or changing frame semantics. |
|
|
599
|
-
| **PAYLOAD_FORMAT_VERSION** (`1`) | `src/version.ts` | First byte of every encrypted payload. Receivers check this before decrypting. Increment when changing the ciphertext binary layout. |
|
|
600
|
-
|
|
601
|
-
**Version negotiation:** The server exposes `GET /api/info` (no auth required) which returns `protocolVersion`, `minSdkVersion`, and supported `features`. Clients can call this at startup to validate compatibility.
|
|
602
|
-
|
|
603
|
-
**Backward compatibility rules:**
|
|
604
|
-
- New proto fields are always additive (proto3 silently ignores unknown fields)
|
|
605
|
-
- The `encryptedPayload` (field 6) legacy path is preserved alongside multi-device `devicePayloads` (field 8)
|
|
606
|
-
- Encrypted payload version byte is checked on decrypt; unknown versions throw a clear error
|
|
607
|
-
- Bumping `SDK_VERSION` or `PROTOCOL_VERSION` must update this README and `CHANGELOG.md`
|
|
608
|
-
|
|
609
|
-
### Multi-Device Support
|
|
610
|
-
|
|
611
|
-
The SDK supports per-device encryption for multi-device messaging:
|
|
612
|
-
|
|
613
|
-
**How it works:**
|
|
614
|
-
1. Each device generates its own X25519 identity keypair on first use
|
|
615
|
-
2. Device public keys are published to the key directory (your backend) with a unique `deviceId`
|
|
616
|
-
3. When sending a message, the SDK fetches ALL device keys for the recipient and encrypts once per device
|
|
617
|
-
4. The sender also encrypts for their OWN other devices (self-sync)
|
|
618
|
-
5. Each `Envelope` carries `devicePayloads[]`, one entry per device with device-specific ciphertext
|
|
619
|
-
6. The receiver picks the `DeviceEncryptedPayload` matching their `deviceId` and decrypts
|
|
620
|
-
|
|
621
|
-
**Wire format (Envelope proto):**
|
|
622
|
-
```protobuf
|
|
623
|
-
message Envelope {
|
|
624
|
-
string messageId = 1;
|
|
625
|
-
string appId = 2;
|
|
626
|
-
string fromUserId = 3;
|
|
627
|
-
string toUserId = 4;
|
|
628
|
-
int64 timestamp = 5;
|
|
629
|
-
bytes encryptedPayload = 6; // legacy single-device (empty when devicePayloads used)
|
|
630
|
-
string clientMessageId = 7;
|
|
631
|
-
repeated DeviceEncryptedPayload devicePayloads = 8; // multi-device
|
|
632
|
-
string senderDeviceId = 9;
|
|
633
|
-
}
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
**Backward compatibility:**
|
|
637
|
-
- If the recipient has no device keys (old SDK), the sender falls back to the legacy single `encryptedPayload` path
|
|
638
|
-
- Incoming envelopes with `devicePayloads.length > 0` use the multi-device decryption path; otherwise the legacy path is used
|
|
639
|
-
|
|
640
|
-
### Encrypted Payload Format
|
|
641
|
-
|
|
642
|
-
```
|
|
643
|
-
+--------+--------+--------+--------+----------+-------------+-------+------------+
|
|
644
|
-
| Version| Algor | Nonce | Flags | Cipher | Signature | Nonce | Ciphertext |
|
|
645
|
-
| (1B) | (1B) | Len(1B)| (1B) | Len(4B) | Len(4B) | (12B) | (variable) |
|
|
646
|
-
+--------+--------+--------+--------+----------+-------------+-------+------------+
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
- **Version:** Payload format version (1)
|
|
650
|
-
- **Algorithm:** Encryption algorithm (1 = AES-256-GCM)
|
|
651
|
-
- **Nonce Length:** GCM nonce length (12 bytes)
|
|
652
|
-
- **Flags:** Optional signature flag (0 = no signature, 1 = Ed25519 signature)
|
|
653
|
-
- **Cipher Length:** Ciphertext length (big-endian uint32)
|
|
654
|
-
- **Signature Length:** Signature length if present (big-endian uint32)
|
|
655
|
-
- **Nonce:** 96-bit GCM nonce (random per message)
|
|
656
|
-
- **Ciphertext:** AES-256-GCM encrypted plaintext + 128-bit auth tag
|
|
657
|
-
|
|
658
|
-
**Forward Compatibility:** Future versions may add Ed25519 signatures for sender authenticity.
|
|
659
|
-
|
|
660
|
-
---
|
|
661
|
-
|
|
662
|
-
## 🔒 Security Considerations
|
|
663
|
-
|
|
664
|
-
### ✅ What DropOnAir SDK Protects
|
|
665
|
-
|
|
666
|
-
- **Confidentiality:** All message content encrypted with AES-256-GCM before leaving device
|
|
667
|
-
- **Integrity:** GCM authentication tag prevents tampering
|
|
668
|
-
- **Forward Secrecy:** Shared secrets derived per peer, cleared on disconnect
|
|
669
|
-
- **Context Binding:** AAD prevents message replay/substitution attacks
|
|
670
|
-
- **Key Isolation:** Private keys never leave device, never sent to DropOnAir
|
|
671
|
-
|
|
672
|
-
### ⚠️ Security Responsibilities
|
|
673
|
-
|
|
674
|
-
1. **JWT Security:** Protect user JWT in secure HTTP-only cookies (never localStorage)
|
|
675
|
-
2. **HTTPS Required:** Use HTTPS for your backend to protect JWT during token exchange
|
|
676
|
-
3. **CSP Headers:** Enable Content Security Policy to prevent XSS attacks on IndexedDB
|
|
677
|
-
4. **Key Backup:** SDK does NOT support key backup - users who lose device lose message access
|
|
678
|
-
5. **Public Key Authenticity:** your backend must verify user identity before accepting public key uploads
|
|
679
|
-
|
|
680
|
-
### 🚨 Known Limitations
|
|
681
|
-
|
|
682
|
-
- **No Forward Secrecy Between Messages:** Uses static X25519 keys (no ratcheting like Signal Protocol)
|
|
683
|
-
- **No Sender Authentication:** Recipient cannot verify sender identity (consider adding Ed25519 signatures)
|
|
684
|
-
- **Metadata Leakage:** DropOnAir sees fromUserId, toUserId, timestamp, message size
|
|
685
|
-
- **IndexedDB Vulnerability:** Browser storage vulnerable to XSS (use secure CSP)
|
|
686
|
-
- **No Multi-Device Key Sharing:** Each device generates a separate identity; the SDK encrypts per-device but does not synchronize private keys across devices
|
|
687
|
-
|
|
688
|
-
---
|
|
689
|
-
|
|
690
|
-
## 🧪 Testing
|
|
691
|
-
|
|
692
|
-
Run the E2EE validation test:
|
|
693
|
-
|
|
694
|
-
```bash
|
|
695
|
-
npm run build
|
|
696
|
-
node test-e2ee.js
|
|
697
|
-
```
|
|
698
|
-
|
|
699
|
-
**Test Coverage:**
|
|
700
|
-
- ✅ Local identity generation (X25519 keypairs)
|
|
701
|
-
- ✅ ECDH shared secret derivation
|
|
702
|
-
- ✅ AES-256-GCM encryption/decryption
|
|
703
|
-
- ✅ Binary payload format validation
|
|
704
|
-
- ✅ Tampering detection (GCM auth tag)
|
|
705
|
-
- ✅ AAD binding (prevents context manipulation)
|
|
706
|
-
- ✅ Session caching (performance optimization)
|
|
707
|
-
|
|
708
|
-
---
|
|
709
|
-
|
|
710
|
-
## 📚 Integration Examples
|
|
711
|
-
|
|
712
|
-
### Angular/Ionic Example
|
|
713
|
-
|
|
714
|
-
```typescript
|
|
715
|
-
import { initialize, DropOnAirClient } from '@droponair/sdk-js';
|
|
716
|
-
import { Injectable } from '@angular/core';
|
|
717
|
-
import { HttpClient } from '@angular/common/http';
|
|
718
|
-
import { firstValueFrom } from 'rxjs';
|
|
719
|
-
|
|
720
|
-
@Injectable({ providedIn: 'root' })
|
|
721
|
-
export class MessagingService {
|
|
722
|
-
private client?: DropOnAirClient;
|
|
723
|
-
|
|
724
|
-
constructor(private http: HttpClient) {}
|
|
725
|
-
|
|
726
|
-
async connect() {
|
|
727
|
-
this.client = await initialize({
|
|
728
|
-
appId: environment.appId,
|
|
729
|
-
publicApiKey: environment.droponairApiKey,
|
|
730
|
-
getUserJwt: async () => {
|
|
731
|
-
const { jwt } = await firstValueFrom(this.http.get<{ jwt: string }>('/api/auth/me'));
|
|
732
|
-
return jwt;
|
|
733
|
-
},
|
|
734
|
-
keyDirectoryEndpoint: '/api/messaging/keys',
|
|
735
|
-
tokenExchangeEndpoint: '/api/messaging/token-exchange'
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
this.client.onMessage(({ fromUserId, plaintext, timestamp }) => {
|
|
739
|
-
this.handleIncomingMessage(fromUserId, plaintext, timestamp);
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
this.client.onEvent(({ type, reason }) => {
|
|
743
|
-
if (type === 'LIMIT_REACHED') {
|
|
744
|
-
this.showRateLimitWarning();
|
|
745
|
-
}
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
async sendMessage(recipientId: string, message: string) {
|
|
750
|
-
if (!this.client) throw new Error('Not connected');
|
|
751
|
-
return this.client.sendMessage(recipientId, message);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
disconnect() {
|
|
755
|
-
this.client?.disconnect();
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
private handleIncomingMessage(fromUserId: string, plaintext: string, timestamp: number) {
|
|
759
|
-
// Update UI, store in local DB, etc.
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
private showRateLimitWarning() {
|
|
763
|
-
// Show user notification
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
```
|
|
767
|
-
|
|
768
|
-
### React Example
|
|
769
|
-
|
|
770
|
-
```typescript
|
|
771
|
-
import { initialize, DropOnAirClient } from '@droponair/sdk-js';
|
|
772
|
-
import { useEffect, useState } from 'react';
|
|
773
|
-
|
|
774
|
-
export function useMessaging() {
|
|
775
|
-
const [client, setClient] = useState<DropOnAirClient | null>(null);
|
|
776
|
-
const [messages, setMessages] = useState<any[]>([]);
|
|
777
|
-
|
|
778
|
-
useEffect(() => {
|
|
779
|
-
initialize({
|
|
780
|
-
appId: process.env.REACT_APP_APP_ID!,
|
|
781
|
-
publicApiKey: process.env.REACT_APP_DROPONAIR_API_KEY!,
|
|
782
|
-
getUserJwt: async () => {
|
|
783
|
-
const res = await fetch('/api/auth/me', { credentials: 'include' });
|
|
784
|
-
const { jwt } = await res.json();
|
|
785
|
-
return jwt;
|
|
786
|
-
},
|
|
787
|
-
keyDirectoryEndpoint: '/api/messaging/keys',
|
|
788
|
-
tokenExchangeEndpoint: '/api/messaging/token-exchange'
|
|
789
|
-
}).then((c) => {
|
|
790
|
-
c.onMessage((msg) => {
|
|
791
|
-
setMessages((prev) => [...prev, msg]);
|
|
792
|
-
});
|
|
793
|
-
setClient(c);
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
return () => client?.disconnect();
|
|
797
|
-
}, []);
|
|
798
|
-
|
|
799
|
-
const sendMessage = async (toUserId: string, text: string) => {
|
|
800
|
-
if (!client) throw new Error('Not connected');
|
|
801
|
-
await client.sendMessage(toUserId, text);
|
|
802
|
-
};
|
|
803
|
-
|
|
804
|
-
return { messages, sendMessage, connected: !!client };
|
|
805
|
-
}
|
|
806
|
-
```
|
|
807
|
-
|
|
808
|
-
---
|
|
809
|
-
|
|
810
|
-
## 🛠️ Development
|
|
811
|
-
|
|
812
|
-
### Build
|
|
813
|
-
|
|
814
|
-
```bash
|
|
815
|
-
npm run build
|
|
816
|
-
```
|
|
817
|
-
|
|
818
|
-
### Run Tests
|
|
819
|
-
|
|
820
|
-
```bash
|
|
821
|
-
npm run build
|
|
822
|
-
node test-e2ee.js
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
### Lint
|
|
826
|
-
|
|
827
|
-
```bash
|
|
828
|
-
npm run lint
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
---
|
|
832
|
-
|
|
833
|
-
## 📄 License
|
|
61
|
+
const client = await initialize(options);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Option | Type | Required | Description |
|
|
65
|
+
|--------|------|----------|-------------|
|
|
66
|
+
| `appId` | string | Yes | Your DropOnAir app ID |
|
|
67
|
+
| `publicApiKey` | string | Yes | Your DropOnAir public API key |
|
|
68
|
+
| `getUserJwt` | `() => Promise<string>` | Yes | Returns a fresh user JWT from your backend |
|
|
69
|
+
| `keyDirectoryEndpoint` | string | No | Public key directory URL (default: `/api/messaging/keys`) |
|
|
70
|
+
| `tokenExchangeEndpoint` | string | No | Token exchange URL (default: `/api/messaging/token-exchange`) |
|
|
71
|
+
| `messagingWsUrl` | string | No | WebSocket URL (default: `wss://sdk.droponair.com/ws/messages`) |
|
|
72
|
+
| `messagingHttpUrl` | string | No | HTTP URL (default: `https://sdk.droponair.com`) |
|
|
73
|
+
| `autoConnect` | boolean | No | Connect on init (default: `true`) |
|
|
74
|
+
| `storage` | KeyStorageAdapter | No | Custom key storage (default: IndexedDB) |
|
|
75
|
+
| `debug` | boolean | No | Enable debug logging (default: `false`) |
|
|
76
|
+
|
|
77
|
+
### Connection
|
|
78
|
+
|
|
79
|
+
| Method | Returns | Description |
|
|
80
|
+
|--------|---------|-------------|
|
|
81
|
+
| `connect()` | `Promise<void>` | Connect to DropOnAir (generates keys, exchanges JWT, opens WebSocket) |
|
|
82
|
+
| `disconnect()` | `void` | Disconnect and stop auto-reconnect |
|
|
83
|
+
|
|
84
|
+
### E2EE Messaging
|
|
85
|
+
|
|
86
|
+
| Method | Returns | Description |
|
|
87
|
+
|--------|---------|-------------|
|
|
88
|
+
| `sendMessage(toUserId, plaintext)` | `Promise<{ messageId }>` | Send an encrypted message |
|
|
89
|
+
| `sendCleartextMessage(toUserId, plaintext)` | `Promise<{ messageId }>` | Send a cleartext message (no E2EE) |
|
|
90
|
+
| `onMessage(callback)` | `() => void` | Listen for incoming messages. Returns unsubscribe function |
|
|
91
|
+
| `onEvent(callback)` | `() => void` | Listen for system events (CONNECTED, DELIVERED, ERROR, etc.) |
|
|
92
|
+
| `ack(messageId)` | `Promise<void>` | Manually acknowledge a message |
|
|
93
|
+
|
|
94
|
+
### Broadcast Channels
|
|
95
|
+
|
|
96
|
+
| Method | Returns | Description |
|
|
97
|
+
|--------|---------|-------------|
|
|
98
|
+
| `subscribeBroadcast(channelId)` | `Promise<void>` | Subscribe to a channel |
|
|
99
|
+
| `unsubscribeBroadcast(channelId)` | `Promise<void>` | Unsubscribe from a channel |
|
|
100
|
+
| `publishBroadcast(channelId, plaintext)` | `Promise<{ broadcastId }>` | Publish to a channel |
|
|
101
|
+
| `onBroadcast(callback)` | `() => void` | Listen for broadcast messages |
|
|
102
|
+
|
|
103
|
+
### Groups
|
|
104
|
+
|
|
105
|
+
| Method | Returns | Description |
|
|
106
|
+
|--------|---------|-------------|
|
|
107
|
+
| `createGroup(name, memberUserIds?)` | `Promise<GroupInfo>` | Create a group with optional members |
|
|
108
|
+
| `listGroups()` | `Promise<GroupInfo[]>` | List your groups |
|
|
109
|
+
| `getGroup(groupId)` | `Promise<GroupInfo>` | Get group details |
|
|
110
|
+
| `addGroupMembers(groupId, userIds)` | `Promise<GroupInfo>` | Add members (owner/admin) |
|
|
111
|
+
| `removeGroupMember(groupId, userId)` | `Promise<GroupInfo>` | Remove a member (owner/admin) |
|
|
112
|
+
| `deleteGroup(groupId)` | `Promise<void>` | Delete a group (owner only) |
|
|
113
|
+
| `sendGroupMessage(groupId, plaintext)` | `Promise<{ messageId }>` | Send a group message |
|
|
114
|
+
| `onGroupMessage(callback)` | `() => void` | Listen for group messages |
|
|
115
|
+
|
|
116
|
+
### 1-to-1 Calls
|
|
117
|
+
|
|
118
|
+
| Method | Returns | Description |
|
|
119
|
+
|--------|---------|-------------|
|
|
120
|
+
| `startCall(targetUserId)` | `Promise<string>` | Start a call, returns callId |
|
|
121
|
+
| `acceptCall(callId)` | `Promise<void>` | Accept incoming call |
|
|
122
|
+
| `rejectCall(callId)` | `Promise<void>` | Reject incoming call |
|
|
123
|
+
| `endCall(callId)` | `Promise<void>` | End or cancel a call |
|
|
124
|
+
| `toggleVideo(callId, enabled)` | `void` | Toggle video on/off |
|
|
125
|
+
| `sendCallSignal(type, callId, payload)` | `void` | Send SDP/ICE signaling data |
|
|
126
|
+
| `onCallEvent(callback)` | `() => void` | Listen for call events |
|
|
127
|
+
| `fetchTurnCredentials()` | `Promise<TurnCredentials>` | Get TURN server credentials for NAT traversal |
|
|
128
|
+
|
|
129
|
+
### Group Calls
|
|
130
|
+
|
|
131
|
+
| Method | Returns | Description |
|
|
132
|
+
|--------|---------|-------------|
|
|
133
|
+
| `startGroupCall(groupId)` | `Promise<string>` | Start a group call, returns callId |
|
|
134
|
+
| `joinGroupCall(callId, groupId)` | `Promise<void>` | Join an active group call |
|
|
135
|
+
| `leaveGroupCall(callId)` | `Promise<void>` | Leave a group call |
|
|
136
|
+
| `endGroupCall(callId)` | `Promise<void>` | End a group call for all |
|
|
137
|
+
| `sendGroupCallSignal(type, callId, groupId, targetUserId, payload)` | `void` | Send SDP/ICE to a peer |
|
|
138
|
+
| `onGroupCallEvent(callback)` | `() => void` | Listen for group call events |
|
|
139
|
+
|
|
140
|
+
## Security
|
|
141
|
+
|
|
142
|
+
- **X25519 ECDH** key agreement for shared secrets
|
|
143
|
+
- **HKDF-SHA256** key derivation
|
|
144
|
+
- **AES-256-GCM** authenticated encryption with AAD binding (messageId, senderId, recipientId, timestamp)
|
|
145
|
+
- Private keys generated and stored locally, never sent to DropOnAir
|
|
146
|
+
- Multi-device: each device has its own keypair, messages encrypted per-device
|
|
147
|
+
|
|
148
|
+
## Requirements
|
|
149
|
+
|
|
150
|
+
- Node.js >= 18.0.0
|
|
151
|
+
- Browser with Web Crypto API support (all modern browsers)
|
|
152
|
+
|
|
153
|
+
## License
|
|
834
154
|
|
|
835
155
|
MIT
|
|
836
156
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
## 🤝 Support
|
|
840
|
-
|
|
841
|
-
For issues or questions:
|
|
842
|
-
- **GitHub Issues:** https://github.com/droponair/droponair-sdk-js
|
|
843
|
-
- **Documentation:** https://docs.droponair.com
|
|
844
|
-
- **Email:** support@droponair.com
|
|
845
|
-
|
|
846
|
-
---
|
|
847
|
-
|
|
848
|
-
## 🎯 Final Security Validation Checklist
|
|
849
|
-
|
|
850
|
-
Validation test results:
|
|
851
|
-
|
|
852
|
-
```
|
|
853
|
-
✅ SDK generates identity keys locally (X25519)
|
|
854
|
-
✅ DropOnAir never stores user keys (server refactored)
|
|
855
|
-
✅ Encryption fully client-side (AES-256-GCM)
|
|
856
|
-
✅ Payload opaque over wire (binary protobuf)
|
|
857
|
-
✅ Two users exchange encrypted messages
|
|
858
|
-
✅ Recipient decrypts correctly
|
|
859
|
-
✅ DropOnAir never accesses plaintext (blind relay)
|
|
860
|
-
✅ Tampering detection (GCM auth tag)
|
|
861
|
-
✅ AAD binding prevents context attacks
|
|
862
|
-
✅ Session caching for performance
|
|
863
|
-
```
|
|
157
|
+
## Support
|
|
864
158
|
|
|
865
|
-
|
|
159
|
+
- **Website:** [droponair.com](https://droponair.com)
|
|
160
|
+
- **Email:** info@droponair.com
|
package/dist/version.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* MINOR, additive feature (e.g. multi-device payloads, new call event type)
|
|
8
8
|
* PATCH, bug-fix / perf improvement with no wire or API change
|
|
9
9
|
*/
|
|
10
|
-
export declare const SDK_VERSION = "0.3.
|
|
10
|
+
export declare const SDK_VERSION = "0.3.1";
|
|
11
11
|
/**
|
|
12
12
|
* Binary encrypted-payload format version.
|
|
13
13
|
* Included as the first byte of every encrypted payload so receivers can
|
package/dist/version.js
CHANGED
|
@@ -10,7 +10,7 @@ exports.PROTOCOL_VERSION = exports.PAYLOAD_FORMAT_VERSION = exports.SDK_VERSION
|
|
|
10
10
|
* MINOR, additive feature (e.g. multi-device payloads, new call event type)
|
|
11
11
|
* PATCH, bug-fix / perf improvement with no wire or API change
|
|
12
12
|
*/
|
|
13
|
-
exports.SDK_VERSION = '0.3.
|
|
13
|
+
exports.SDK_VERSION = '0.3.1';
|
|
14
14
|
/**
|
|
15
15
|
* Binary encrypted-payload format version.
|
|
16
16
|
* Included as the first byte of every encrypted payload so receivers can
|