@zi2/relay-sdk 1.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/CHANGELOG.md +22 -0
- package/LICENSE +64 -0
- package/README.md +855 -0
- package/dist/adapters/prisma-adapter.d.ts +57 -0
- package/dist/adapters/prisma-adapter.js +181 -0
- package/dist/client/index.d.ts +63 -0
- package/dist/client/index.js +298 -0
- package/dist/index.d.ts +592 -0
- package/dist/index.js +2537 -0
- package/dist/server/fastify-plugin.d.ts +26 -0
- package/dist/server/fastify-plugin.js +125 -0
- package/dist/types-CPJUrmcy.d.ts +288 -0
- package/package.json +75 -0
- package/prisma/schema.prisma +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
# @zi2/relay-sdk
|
|
2
|
+
|
|
3
|
+
Enterprise-grade SMS relay SDK that routes messages through physical Android devices using end-to-end encryption. Instead of paying per-message fees to cloud SMS providers, deploy your own relay devices and send SMS through their native SIM cards — with full compliance, multi-tenant isolation, and automatic provider fallback.
|
|
4
|
+
|
|
5
|
+
## Key Features
|
|
6
|
+
|
|
7
|
+
- **End-to-end encryption** — X25519 key exchange + HKDF-SHA256 + AES-256-GCM. The server never sees plaintext message content.
|
|
8
|
+
- **Provider fallback** — Automatically fail over to Twilio, Vonage, or any cloud provider when a device goes offline.
|
|
9
|
+
- **Multi-tenant** — Organization-scoped relay devices, rate limits, and audit trails.
|
|
10
|
+
- **10 languages** — Built-in i18n for EN, FR, ES, DE, JA, AR, ZH, PT, KO, IT.
|
|
11
|
+
- **PCI DSS v4 + SOC 2 compliant** — Encryption at rest, TLS enforcement, brute-force protection, audit logging, auto-redacting logger.
|
|
12
|
+
- **Database-agnostic** — Ships with Prisma and in-memory adapters; implement the `DatabaseAdapter` interface for any storage backend.
|
|
13
|
+
- **White-label Android app** — Rebrand and deploy custom relay apps with a single config file.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Table of Contents
|
|
18
|
+
|
|
19
|
+
1. [Installation](#installation)
|
|
20
|
+
2. [Quick Start](#quick-start)
|
|
21
|
+
3. [Architecture](#architecture)
|
|
22
|
+
4. [Configuration](#configuration)
|
|
23
|
+
5. [Database Adapters](#database-adapters)
|
|
24
|
+
6. [Pairing Flow](#pairing-flow)
|
|
25
|
+
7. [Sending SMS](#sending-sms)
|
|
26
|
+
8. [Provider Fallback](#provider-fallback)
|
|
27
|
+
9. [WebSocket Protocol](#websocket-protocol)
|
|
28
|
+
10. [Fastify Plugin](#fastify-plugin)
|
|
29
|
+
11. [Error Codes](#error-codes)
|
|
30
|
+
12. [Events](#events)
|
|
31
|
+
13. [Health Service](#health-service)
|
|
32
|
+
14. [Security & Compliance](#security--compliance)
|
|
33
|
+
15. [i18n](#i18n)
|
|
34
|
+
16. [White-Label Android App](#white-label-android-app)
|
|
35
|
+
17. [License](#license)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add @zi2/relay-sdk
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Optional peer dependencies — install only what you need:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# For PrismaAdapter (production database)
|
|
49
|
+
pnpm add @prisma/client
|
|
50
|
+
|
|
51
|
+
# For Fastify plugin (REST + WebSocket server)
|
|
52
|
+
pnpm add fastify @fastify/websocket
|
|
53
|
+
|
|
54
|
+
# For WebSocket support (Node.js server)
|
|
55
|
+
pnpm add ws
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { createRelay, AesGcmEncryption } from '@zi2/relay-sdk';
|
|
64
|
+
import { PrismaAdapter } from '@zi2/relay-sdk/adapters/prisma';
|
|
65
|
+
import { PrismaClient } from '@prisma/client';
|
|
66
|
+
|
|
67
|
+
const prisma = new PrismaClient();
|
|
68
|
+
|
|
69
|
+
const sdk = createRelay({
|
|
70
|
+
db: new PrismaAdapter(prisma),
|
|
71
|
+
encryption: new AesGcmEncryption(process.env.RELAY_ENCRYPTION_KEY!),
|
|
72
|
+
apiUrl: 'https://api.yourapp.com',
|
|
73
|
+
wsUrl: 'wss://api.yourapp.com/ws/relay',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 1. Pair a device (returns QR code data for the Android app to scan)
|
|
77
|
+
const pairing = await sdk.initiatePairing('org_123', 'user_456');
|
|
78
|
+
// → { pairingId, serverPublicKey, pairingToken, wsUrl }
|
|
79
|
+
|
|
80
|
+
// 2. Send an SMS through the paired relay device
|
|
81
|
+
const result = await sdk.sendSMS({
|
|
82
|
+
relayId: 'relay_abc',
|
|
83
|
+
orgId: 'org_123',
|
|
84
|
+
to: '+1234567890',
|
|
85
|
+
body: 'Your verification code is 847291',
|
|
86
|
+
});
|
|
87
|
+
// → { messageId: 'msg_xyz', status: 'delivered' }
|
|
88
|
+
|
|
89
|
+
// 3. Check device status
|
|
90
|
+
const online = sdk.isRelayOnline('relay_abc'); // true
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Architecture
|
|
96
|
+
|
|
97
|
+
The SDK is organized into six layers:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
┌─────────────────────────────────────────────────────────┐
|
|
101
|
+
│ Your Application │
|
|
102
|
+
├─────────────────────────────────────────────────────────┤
|
|
103
|
+
│ Server Layer │ Routes, Fastify plugin, WS handler │
|
|
104
|
+
├─────────────────────────────────────────────────────────┤
|
|
105
|
+
│ Core Layer │ Crypto, Queue, Pairing, Health │
|
|
106
|
+
├─────────────────────────────────────────────────────────┤
|
|
107
|
+
│ Adapter Layer │ DB, Encryption, Broadcast, Audit │
|
|
108
|
+
├─────────────────────────────────────────────────────────┤
|
|
109
|
+
│ Provider Layer │ PhoneRelay, Fallback │
|
|
110
|
+
├─────────────────────────────────────────────────────────┤
|
|
111
|
+
│ i18n Layer │ 10 locales, error translations │
|
|
112
|
+
└─────────────────────────────────────────────────────────┘
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Message flow:**
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Your Server Android Device Carrier
|
|
119
|
+
│ │ │
|
|
120
|
+
│ sdk.sendSMS(...) │ │
|
|
121
|
+
│──encrypt payload──► │ │
|
|
122
|
+
│ queue message │ │
|
|
123
|
+
│ │ │
|
|
124
|
+
│ ◄── WebSocket (wss://) ──► │ │
|
|
125
|
+
│ send_sms (encrypted) ──► │ │
|
|
126
|
+
│ │──native SMS──► │
|
|
127
|
+
│ │ │──► Recipient
|
|
128
|
+
│ │ ◄── delivery receipt ───│
|
|
129
|
+
│ ◄── sms_ack ──────────── │ │
|
|
130
|
+
│ ◄── delivery_receipt ─── │ │
|
|
131
|
+
│ │ │
|
|
132
|
+
│ broadcast to web UI │ │
|
|
133
|
+
└───────────────────────────────┘──────────────────────────┘
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Module Exports
|
|
137
|
+
|
|
138
|
+
| Import Path | Contents |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `@zi2/relay-sdk` | `createRelay()`, types, constants, errors, crypto, adapters |
|
|
141
|
+
| `@zi2/relay-sdk/client` | Android/client-side: WebSocket client, E2E crypto, pairing, SMS sender, secure storage |
|
|
142
|
+
| `@zi2/relay-sdk/adapters/prisma` | `PrismaAdapter` |
|
|
143
|
+
| `@zi2/relay-sdk/fastify` | `relayPlugin` for Fastify |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
interface RelaySDKConfig {
|
|
151
|
+
/** Required — Database adapter (PrismaAdapter, MemoryAdapter, or custom) */
|
|
152
|
+
db: DatabaseAdapter;
|
|
153
|
+
|
|
154
|
+
/** Required — Encryption adapter for data at rest (AesGcmEncryption or custom) */
|
|
155
|
+
encryption: EncryptionAdapter;
|
|
156
|
+
|
|
157
|
+
/** Optional — Broadcast adapter for real-time UI updates (e.g., SSE or WebSocket push) */
|
|
158
|
+
broadcast?: BroadcastAdapter;
|
|
159
|
+
|
|
160
|
+
/** Optional — Audit trail adapter for PCI DSS v4 Req 10 / SOC 2 CC7.2 compliance */
|
|
161
|
+
audit?: AuditAdapter;
|
|
162
|
+
|
|
163
|
+
/** Optional — Logger adapter with auto-redaction of sensitive fields */
|
|
164
|
+
logger?: LoggerAdapter;
|
|
165
|
+
|
|
166
|
+
/** Base URL for the API server (used in QR code pairing data). Default: 'http://localhost:3000' */
|
|
167
|
+
apiUrl?: string;
|
|
168
|
+
|
|
169
|
+
/** WebSocket URL for device connections (included in QR code). Default: empty string (falls back to apiUrl-derived WSS URL in pairing service) */
|
|
170
|
+
wsUrl?: string;
|
|
171
|
+
|
|
172
|
+
/** Enforce TLS-only WebSocket connections (PCI DSS v4 Req 4.2.1). Default: true */
|
|
173
|
+
enforceTls?: boolean;
|
|
174
|
+
|
|
175
|
+
/** Maximum auth failures per IP before lockout (PCI DSS v4 Req 8.3.4). Default: 10 */
|
|
176
|
+
maxAuthFailuresPerIp?: number;
|
|
177
|
+
|
|
178
|
+
/** Overridable rate limits and timeouts (use SCREAMING_CASE keys) */
|
|
179
|
+
limits?: {
|
|
180
|
+
AUTH_TIMEOUT_MS?: number; // Default: 5000
|
|
181
|
+
HEARTBEAT_INTERVAL_MS?: number; // Default: 30000
|
|
182
|
+
PAIRING_EXPIRY_MINUTES?: number; // Default: 5
|
|
183
|
+
MESSAGE_EXPIRY_HOURS?: number; // Default: 24
|
|
184
|
+
QUEUE_DRAIN_DELAY_MS?: number; // Default: 3000
|
|
185
|
+
DEFAULT_SMS_TIMEOUT_MS?: number; // Default: 30000
|
|
186
|
+
MAX_SMS_PER_MINUTE?: number; // Default: 20
|
|
187
|
+
MAX_SMS_PER_HOUR?: number; // Default: 200
|
|
188
|
+
MAX_SMS_PER_DAY?: number; // Default: 1000
|
|
189
|
+
DEGRADED_THRESHOLD_MS?: number; // Default: 300000 (5 minutes)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Encryption Key Generation
|
|
195
|
+
|
|
196
|
+
Generate a 256-bit encryption key for `AesGcmEncryption`:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
openssl rand -hex 32
|
|
200
|
+
# → e.g. a1b2c3d4e5f6... (64 hex characters)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Store it securely in your environment:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
RELAY_ENCRYPTION_KEY=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Database Adapters
|
|
212
|
+
|
|
213
|
+
### PrismaAdapter (Production)
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { PrismaAdapter } from '@zi2/relay-sdk/adapters/prisma';
|
|
217
|
+
import { PrismaClient } from '@prisma/client';
|
|
218
|
+
|
|
219
|
+
const prisma = new PrismaClient();
|
|
220
|
+
const db = new PrismaAdapter(prisma);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Required Prisma schema models:
|
|
224
|
+
|
|
225
|
+
```prisma
|
|
226
|
+
model PhoneRelay {
|
|
227
|
+
id String @id @default(uuid())
|
|
228
|
+
organizationId String
|
|
229
|
+
deviceName String
|
|
230
|
+
platform String
|
|
231
|
+
phoneNumber String?
|
|
232
|
+
devicePublicKey String
|
|
233
|
+
sharedKeyEncrypted String
|
|
234
|
+
authTokenHash String
|
|
235
|
+
status String @default("active")
|
|
236
|
+
keyVersion Int @default(1)
|
|
237
|
+
lastKeyRotation DateTime @default(now())
|
|
238
|
+
lastSeenAt DateTime?
|
|
239
|
+
lastIpAddress String?
|
|
240
|
+
batteryLevel Int?
|
|
241
|
+
signalStrength Int?
|
|
242
|
+
totalSmsSent Int @default(0)
|
|
243
|
+
totalSmsFailed Int @default(0)
|
|
244
|
+
dailySmsSent Int @default(0)
|
|
245
|
+
dailyResetAt DateTime @default(now())
|
|
246
|
+
maxSmsPerMinute Int @default(20)
|
|
247
|
+
maxSmsPerHour Int @default(200)
|
|
248
|
+
maxSmsPerDay Int @default(1000)
|
|
249
|
+
pairedById String
|
|
250
|
+
createdAt DateTime @default(now())
|
|
251
|
+
updatedAt DateTime @updatedAt
|
|
252
|
+
|
|
253
|
+
organization Organization @relation(fields: [organizationId], references: [id])
|
|
254
|
+
messages RelayMessageQueue[]
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
model PhoneRelayPairing {
|
|
258
|
+
id String @id @default(uuid())
|
|
259
|
+
organizationId String
|
|
260
|
+
pairingToken String
|
|
261
|
+
serverPublicKey String
|
|
262
|
+
serverPrivateKeyEncrypted String
|
|
263
|
+
status String @default("pending")
|
|
264
|
+
initiatedById String
|
|
265
|
+
expiresAt DateTime
|
|
266
|
+
createdAt DateTime @default(now())
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
model RelayMessageQueue {
|
|
270
|
+
id String @id @default(uuid())
|
|
271
|
+
relayId String
|
|
272
|
+
organizationId String
|
|
273
|
+
encryptedPayload String
|
|
274
|
+
status String @default("pending")
|
|
275
|
+
attempts Int @default(0)
|
|
276
|
+
maxAttempts Int @default(3)
|
|
277
|
+
nativeMessageId String?
|
|
278
|
+
errorMessage String?
|
|
279
|
+
deliveryStatus String?
|
|
280
|
+
sentToDeviceAt DateTime?
|
|
281
|
+
ackedAt DateTime?
|
|
282
|
+
expiresAt DateTime
|
|
283
|
+
createdAt DateTime @default(now())
|
|
284
|
+
updatedAt DateTime @updatedAt
|
|
285
|
+
|
|
286
|
+
relay PhoneRelay @relation(fields: [relayId], references: [id])
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### MemoryAdapter (Testing)
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { MemoryAdapter } from '@zi2/relay-sdk';
|
|
294
|
+
|
|
295
|
+
const db = new MemoryAdapter();
|
|
296
|
+
// Use db.clear() to reset between tests
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Custom Adapter
|
|
300
|
+
|
|
301
|
+
Implement the `DatabaseAdapter` interface to use any storage backend:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import type { DatabaseAdapter } from '@zi2/relay-sdk';
|
|
305
|
+
|
|
306
|
+
class MyCustomAdapter implements DatabaseAdapter {
|
|
307
|
+
// PhoneRelay CRUD
|
|
308
|
+
findRelay(id: string, orgId?: string): Promise<PhoneRelayRecord | null>;
|
|
309
|
+
findRelayByTokenHash(id: string, tokenHash: string, statuses: string[]): Promise<PhoneRelayRecord | null>;
|
|
310
|
+
findRelays(orgId: string, excludeStatus?: string): Promise<PhoneRelayRecord[]>;
|
|
311
|
+
createRelay(data: CreateRelayInput): Promise<PhoneRelayRecord>;
|
|
312
|
+
updateRelay(id: string, data: Partial<PhoneRelayRecord>): Promise<void>;
|
|
313
|
+
updateRelayByOrg(id: string, orgId: string, data: Partial<PhoneRelayRecord>): Promise<void>;
|
|
314
|
+
deleteRelay(id: string): Promise<void>;
|
|
315
|
+
incrementRelayStat(id: string, field: 'totalSmsSent' | 'totalSmsFailed' | 'dailySmsSent', amount?: number): Promise<void>;
|
|
316
|
+
countRelays(orgId: string): Promise<number>;
|
|
317
|
+
markDegradedRelays(threshold: Date): Promise<number>;
|
|
318
|
+
resetDailyCounters(): Promise<number>;
|
|
319
|
+
|
|
320
|
+
// PhoneRelayPairing
|
|
321
|
+
findPairing(id: string): Promise<PhoneRelayPairingRecord | null>;
|
|
322
|
+
createPairing(data: CreatePairingInput): Promise<PhoneRelayPairingRecord>;
|
|
323
|
+
updatePairingStatus(id: string, status: string): Promise<void>;
|
|
324
|
+
deleteExpiredPairings(): Promise<number>;
|
|
325
|
+
|
|
326
|
+
// RelayMessageQueue
|
|
327
|
+
findMessage(id: string): Promise<RelayMessageRecord | null>;
|
|
328
|
+
createMessage(data: CreateMessageInput): Promise<RelayMessageRecord>;
|
|
329
|
+
updateMessage(id: string, data: Partial<RelayMessageRecord>): Promise<void>;
|
|
330
|
+
findPendingMessages(relayId: string, limit: number): Promise<RelayMessageRecord[]>;
|
|
331
|
+
findMessages(relayId: string, limit: number, offset: number): Promise<{ messages: RelayMessageRecord[]; total: number }>;
|
|
332
|
+
deleteMessagesByRelay(relayId: string): Promise<number>;
|
|
333
|
+
expireStaleMessages(): Promise<number>;
|
|
334
|
+
|
|
335
|
+
// Batch operations
|
|
336
|
+
createMessages(batch: CreateMessageInput[]): Promise<RelayMessageRecord[]>;
|
|
337
|
+
updateMessages(ids: string[], data: Partial<RelayMessageRecord>): Promise<number>;
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Pairing Flow
|
|
344
|
+
|
|
345
|
+
Pairing establishes a secure E2E-encrypted channel between your server and an Android device.
|
|
346
|
+
|
|
347
|
+
### Step-by-step
|
|
348
|
+
|
|
349
|
+
1. **Server initiates pairing** — generates an X25519 key pair and a one-time pairing token.
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
const pairing = await sdk.initiatePairing('org_123', 'user_456');
|
|
353
|
+
// Returns: { pairingId, serverPublicKey, pairingToken, wsUrl }
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
2. **Display QR code** — encode the pairing data as a QR code in your web UI. The Android app scans it.
|
|
357
|
+
|
|
358
|
+
3. **Device completes pairing** — the app sends its public key and device info to the server.
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
const result = await sdk.completePairing({
|
|
362
|
+
pairingId: pairing.pairingId,
|
|
363
|
+
pairingToken: pairing.pairingToken,
|
|
364
|
+
devicePublicKey: '<device X25519 public key>',
|
|
365
|
+
deviceName: 'Samsung Galaxy S24',
|
|
366
|
+
platform: 'android',
|
|
367
|
+
phoneNumber: '+15551234567',
|
|
368
|
+
});
|
|
369
|
+
// Returns: { relayId, authToken }
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
4. **X25519 key exchange** — the server derives a shared key from its private key and the device's public key using HKDF-SHA256. Both sides now hold the same AES-256-GCM key.
|
|
373
|
+
|
|
374
|
+
5. **Device connects via WebSocket** — authenticates with the hashed auth token and begins relaying SMS.
|
|
375
|
+
|
|
376
|
+
### Pairing expiry
|
|
377
|
+
|
|
378
|
+
Pairing sessions expire after 5 minutes (configurable via `limits.PAIRING_EXPIRY_MINUTES`). Expired sessions are cleaned up automatically by the health service.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Sending SMS
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
const result = await sdk.sendSMS({
|
|
386
|
+
relayId: 'relay_abc',
|
|
387
|
+
orgId: 'org_123',
|
|
388
|
+
to: '+1234567890',
|
|
389
|
+
body: 'Your verification code is 847291',
|
|
390
|
+
timeoutMs: 30000, // optional, default 30s
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
console.log(result.messageId); // 'msg_xyz'
|
|
394
|
+
console.log(result.status); // 'delivered'
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Message Statuses
|
|
398
|
+
|
|
399
|
+
| Status | Description |
|
|
400
|
+
|---|---|
|
|
401
|
+
| `pending` | Message queued, waiting to be sent to device |
|
|
402
|
+
| `sent_to_device` | Encrypted payload delivered to device via WebSocket |
|
|
403
|
+
| `delivered` | Device confirmed SMS was sent by the carrier |
|
|
404
|
+
| `failed` | Delivery failed (device error, carrier rejection, etc.) |
|
|
405
|
+
| `expired` | Message exceeded its TTL (default: 24 hours) |
|
|
406
|
+
|
|
407
|
+
### Message History
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
const messages = await sdk.getMessages('relay_abc', 'org_123', 50, 0);
|
|
411
|
+
// → { messages: [...], pagination: { total: 142, limit: 50, offset: 0 } }
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Rate Limits
|
|
415
|
+
|
|
416
|
+
Each relay device enforces three tiers of rate limiting (configurable per-device):
|
|
417
|
+
|
|
418
|
+
| Tier | Default | Max |
|
|
419
|
+
|---|---|---|
|
|
420
|
+
| Per minute | 20 | 60 |
|
|
421
|
+
| Per hour | 200 | 1,000 |
|
|
422
|
+
| Per day | 1,000 | 10,000 |
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Provider Fallback
|
|
427
|
+
|
|
428
|
+
Use `createFallbackProvider()` to automatically fall back to cloud SMS providers when a relay device goes offline:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { createRelay } from '@zi2/relay-sdk';
|
|
432
|
+
|
|
433
|
+
const sdk = createRelay({ /* ... */ });
|
|
434
|
+
|
|
435
|
+
const provider = sdk.createFallbackProvider({
|
|
436
|
+
primary: sdk.createProvider('relay_abc', 'org_123'),
|
|
437
|
+
fallbacks: [twilioProvider, vonageProvider],
|
|
438
|
+
isOnline: () => sdk.isRelayOnline('relay_abc'),
|
|
439
|
+
onFallback: (providerName) => {
|
|
440
|
+
console.log(`Relay offline — fell back to ${providerName}`);
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Use as a unified SMS provider
|
|
445
|
+
const result = await provider.sendSms('+1234567890', 'Hello from ZI2');
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Implementing a Fallback Provider
|
|
449
|
+
|
|
450
|
+
Any cloud provider can serve as a fallback by implementing `SmsProviderAdapter`:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
import type { SmsProviderAdapter, SmsSendResponse } from '@zi2/relay-sdk';
|
|
454
|
+
|
|
455
|
+
class TwilioProvider implements SmsProviderAdapter {
|
|
456
|
+
readonly name = 'twilio';
|
|
457
|
+
|
|
458
|
+
initialize(credentials: Record<string, string>): void {
|
|
459
|
+
// Set up Twilio client with credentials.accountSid, credentials.authToken
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async sendSms(to: string, body: string): Promise<SmsSendResponse> {
|
|
463
|
+
// Call Twilio API
|
|
464
|
+
return { success: true, messageId: 'SM...' };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async validateCredentials(credentials: Record<string, string>): Promise<boolean> {
|
|
468
|
+
// Verify Twilio credentials are valid
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## WebSocket Protocol
|
|
477
|
+
|
|
478
|
+
Relay devices communicate with the server over a persistent WebSocket connection (`wss://`). All SMS payloads are E2E encrypted.
|
|
479
|
+
|
|
480
|
+
### Message Types
|
|
481
|
+
|
|
482
|
+
| Type | Direction | Description |
|
|
483
|
+
|---|---|---|
|
|
484
|
+
| `auth` | Device -> Server | Authenticate with `relayId` and `token` |
|
|
485
|
+
| `auth_ok` | Server -> Device | Authentication successful |
|
|
486
|
+
| `send_sms` | Server -> Device | Encrypted SMS payload to deliver |
|
|
487
|
+
| `sms_ack` | Device -> Server | SMS send result (`messageId`, `status`, `nativeMessageId`, `errorMessage`) |
|
|
488
|
+
| `delivery_receipt` | Device -> Server | Carrier delivery confirmation (`messageId`, `deliveryStatus`) |
|
|
489
|
+
| `status` | Device -> Server | Device telemetry (`batteryLevel`, `signalStrength`) |
|
|
490
|
+
| `rekey` | Server -> Device | Initiate key rotation (new server public key) |
|
|
491
|
+
| `rekey_ack` | Device -> Server | Device's new public key for key rotation |
|
|
492
|
+
|
|
493
|
+
### Connection Lifecycle
|
|
494
|
+
|
|
495
|
+
1. Device opens WebSocket to `wss://your-server/ws/relay`
|
|
496
|
+
2. Server starts a 5-second auth timeout
|
|
497
|
+
3. Device sends `{ type: "auth", relayId: "...", token: "..." }`
|
|
498
|
+
4. Server verifies token hash against database (timing-safe comparison)
|
|
499
|
+
5. Server responds `{ type: "auth_ok" }` and begins heartbeat pings every 30s
|
|
500
|
+
6. Server drains any pending messages from the queue
|
|
501
|
+
7. On disconnect, server broadcasts offline status to web clients
|
|
502
|
+
|
|
503
|
+
### Rate Limiting
|
|
504
|
+
|
|
505
|
+
Each WebSocket connection is rate-limited to 100 messages per 10-second window. Exceeding this closes the connection with code `4008`.
|
|
506
|
+
|
|
507
|
+
### Close Codes
|
|
508
|
+
|
|
509
|
+
| Code | Meaning |
|
|
510
|
+
|---|---|
|
|
511
|
+
| `4001` | Authentication timeout (5s) |
|
|
512
|
+
| `4002` | Message sent before authentication |
|
|
513
|
+
| `4003` | Missing relayId or token in auth message |
|
|
514
|
+
| `4004` | Invalid credentials |
|
|
515
|
+
| `4005` | Replaced by new connection from same device |
|
|
516
|
+
| `4008` | Rate limit exceeded |
|
|
517
|
+
| `1000` | Pong timeout — device failed to respond to heartbeat ping |
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## Fastify Plugin
|
|
522
|
+
|
|
523
|
+
Register the Fastify plugin to expose all relay REST endpoints and the WebSocket upgrade handler:
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
import Fastify from 'fastify';
|
|
527
|
+
import websocket from '@fastify/websocket';
|
|
528
|
+
import { relayPlugin } from '@zi2/relay-sdk/fastify';
|
|
529
|
+
import { createRelay, AesGcmEncryption } from '@zi2/relay-sdk';
|
|
530
|
+
import { PrismaAdapter } from '@zi2/relay-sdk/adapters/prisma';
|
|
531
|
+
|
|
532
|
+
const app = Fastify();
|
|
533
|
+
const sdk = createRelay({
|
|
534
|
+
db: new PrismaAdapter(prisma),
|
|
535
|
+
encryption: new AesGcmEncryption(process.env.RELAY_ENCRYPTION_KEY!),
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
await app.register(websocket);
|
|
539
|
+
await app.register(relayPlugin, { sdk, prefix: '/phone-relay' });
|
|
540
|
+
|
|
541
|
+
await app.listen({ port: 3000 });
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Registered Routes
|
|
545
|
+
|
|
546
|
+
| Method | Path | Description |
|
|
547
|
+
|---|---|---|
|
|
548
|
+
| `POST` | `/phone-relay/pair/initiate` | Start a new pairing session |
|
|
549
|
+
| `POST` | `/phone-relay/pair/complete` | Complete device pairing |
|
|
550
|
+
| `GET` | `/phone-relay/` | List all relay devices for the org |
|
|
551
|
+
| `GET` | `/phone-relay/:id` | Get relay device details |
|
|
552
|
+
| `PATCH` | `/phone-relay/:id` | Update relay settings (name, rate limits) |
|
|
553
|
+
| `DELETE` | `/phone-relay/:id` | Revoke a relay device |
|
|
554
|
+
| `POST` | `/phone-relay/:id/test` | Send a test SMS |
|
|
555
|
+
| `GET` | `/phone-relay/:id/messages` | List messages (paginated) |
|
|
556
|
+
| `GET` | `/ws/relay` | WebSocket upgrade for relay devices |
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## Error Codes
|
|
561
|
+
|
|
562
|
+
All errors are returned as `RelayError` instances with a machine-readable code, HTTP status, user-safe message, and i18n key. Error messages are intentionally generic to comply with PCI DSS v4 Req 6.2.4 (no internal details leaked).
|
|
563
|
+
|
|
564
|
+
| Code | HTTP | Message | i18n Key |
|
|
565
|
+
|---|---|---|---|
|
|
566
|
+
| `RELAY_NOT_FOUND` | 404 | Relay device not found | `errors.relayNotFound` |
|
|
567
|
+
| `RELAY_INACTIVE` | 409 | Relay is not active | `errors.relayInactive` |
|
|
568
|
+
| `RELAY_OFFLINE` | 503 | Relay device is offline | `errors.relayOffline` |
|
|
569
|
+
| `PAIRING_NOT_FOUND` | 404 | Pairing session not found | `errors.pairingNotFound` |
|
|
570
|
+
| `PAIRING_EXPIRED` | 410 | Pairing session has expired | `errors.pairingExpired` |
|
|
571
|
+
| `PAIRING_INVALID_TOKEN` | 403 | Invalid pairing token | `errors.pairingInvalidToken` |
|
|
572
|
+
| `AUTH_TIMEOUT` | 408 | WebSocket authentication timeout | `errors.authTimeout` |
|
|
573
|
+
| `AUTH_FAILED` | 401 | Invalid relay credentials | `errors.authFailed` |
|
|
574
|
+
| `RATE_LIMITED` | 429 | Rate limit exceeded | `errors.rateLimited` |
|
|
575
|
+
| `ENCRYPTION_FAILED` | 500 | Encryption operation failed | `errors.encryptionFailed` |
|
|
576
|
+
| `REKEY_FAILED` | 500 | Key rotation failed | `errors.rekeyFailed` |
|
|
577
|
+
| `SMS_SEND_FAILED` | 502 | SMS delivery failed | `errors.smsSendFailed` |
|
|
578
|
+
| `SMS_TIMEOUT` | 504 | SMS acknowledgement timeout | `errors.smsTimeout` |
|
|
579
|
+
|
|
580
|
+
**Extended Error Codes (`RELAY_ERRORS_EXTENDED`)** — These are exported separately from `RELAY_ERRORS`:
|
|
581
|
+
|
|
582
|
+
| Code | HTTP | Message | i18n Key |
|
|
583
|
+
|---|---|---|---|
|
|
584
|
+
| `AUTH_LOCKED` | 423 | Too many failed attempts. Try again later. | `errors.authLocked` |
|
|
585
|
+
| `TLS_REQUIRED` | 426 | TLS/SSL connection required | `errors.tlsRequired` |
|
|
586
|
+
| `ORG_MISMATCH` | 403 | Organization access denied | `errors.orgMismatch` |
|
|
587
|
+
|
|
588
|
+
### Error Handling
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
import { RelayError, RELAY_ERRORS } from '@zi2/relay-sdk';
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
await sdk.sendSMS({ relayId, orgId, to, body });
|
|
595
|
+
} catch (err) {
|
|
596
|
+
if (err instanceof RelayError) {
|
|
597
|
+
console.log(err.code); // 'RELAY_OFFLINE'
|
|
598
|
+
console.log(err.statusCode); // 503
|
|
599
|
+
console.log(err.i18nKey); // 'errors.relayOffline'
|
|
600
|
+
console.log(err.toJSON()); // Safe for client response (no stack trace)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## Events
|
|
608
|
+
|
|
609
|
+
Subscribe to real-time relay events for monitoring, alerting, or UI updates. Each subscription returns an unsubscribe function.
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
// Device came online
|
|
613
|
+
const unsub1 = sdk.onRelayOnline((relayId, orgId) => {
|
|
614
|
+
console.log(`Relay ${relayId} is online`);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Device went offline
|
|
618
|
+
const unsub2 = sdk.onRelayOffline((relayId, orgId) => {
|
|
619
|
+
console.log(`Relay ${relayId} went offline`);
|
|
620
|
+
// Trigger fallback provider switch, send alert, etc.
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// SMS delivered successfully
|
|
624
|
+
const unsub3 = sdk.onMessageDelivered((messageId, relayId) => {
|
|
625
|
+
console.log(`Message ${messageId} delivered via ${relayId}`);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// SMS delivery failed
|
|
629
|
+
const unsub4 = sdk.onMessageFailed((messageId, relayId, error) => {
|
|
630
|
+
console.error(`Message ${messageId} failed on ${relayId}: ${error}`);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Clean up when shutting down
|
|
634
|
+
unsub1();
|
|
635
|
+
unsub2();
|
|
636
|
+
unsub3();
|
|
637
|
+
unsub4();
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## Health Service
|
|
643
|
+
|
|
644
|
+
The health service performs periodic maintenance tasks required for compliance and reliability.
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
// Start automatic health checks (runs every 60 seconds)
|
|
648
|
+
sdk.startHealthService();
|
|
649
|
+
|
|
650
|
+
// Or run a manual check
|
|
651
|
+
const report = await sdk.runHealthCheck();
|
|
652
|
+
console.log(report);
|
|
653
|
+
// {
|
|
654
|
+
// expiredMessages: 3, — stale messages marked as expired
|
|
655
|
+
// degradedRelays: 1, — relays with no heartbeat for 5+ minutes
|
|
656
|
+
// cleanedPairings: 0 — expired pairing sessions removed
|
|
657
|
+
// }
|
|
658
|
+
|
|
659
|
+
// Stop the service when shutting down
|
|
660
|
+
sdk.stopHealthService();
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### What the Health Service Does
|
|
664
|
+
|
|
665
|
+
| Task | Description | Compliance |
|
|
666
|
+
|---|---|---|
|
|
667
|
+
| Expire stale messages | Marks messages past their TTL as `expired` | Data retention |
|
|
668
|
+
| Mark degraded relays | Flags devices not seen for 5+ minutes as `degraded` | Availability monitoring |
|
|
669
|
+
| Clean expired pairings | Removes pending pairing sessions past their expiry | PCI DSS v4 Req 8 |
|
|
670
|
+
| Reset daily counters | Resets per-device daily SMS counters at midnight | Rate limit enforcement |
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Security & Compliance
|
|
675
|
+
|
|
676
|
+
### PCI DSS v4
|
|
677
|
+
|
|
678
|
+
| Requirement | Implementation |
|
|
679
|
+
|---|---|
|
|
680
|
+
| **Req 3.4.1** — Encryption at rest | `AesGcmEncryption` encrypts all stored keys and message payloads. No plaintext fallback in strict mode. |
|
|
681
|
+
| **Req 3.5.1** — Key management | X25519 key pairs generated per pairing. Shared keys encrypted at rest. Key material zero-filled after use. `destroy()` method for cleanup. |
|
|
682
|
+
| **Req 3.7.1** — Key strength | Enforces 256-bit (32-byte) encryption keys at construction time. |
|
|
683
|
+
| **Req 4.2.1** — TLS enforcement | `enforceTls: true` (default) rejects non-TLS WebSocket connections. |
|
|
684
|
+
| **Req 6.2.4** — Error handling | `RelayError.toJSON()` returns only code + message. No stack traces, database details, or crypto internals leak to clients. |
|
|
685
|
+
| **Req 8.3.4** — Brute-force protection | `AuthLimiter` locks out IPs after configurable failed auth attempts (default: 10). |
|
|
686
|
+
| **Req 10** — Audit trail | `AuditAdapter` interface logs all security-relevant actions: pairing, auth, SMS, revocation, key rotation. |
|
|
687
|
+
| **Req 11.3** — Security monitoring | Health service runs periodic checks on system integrity. |
|
|
688
|
+
|
|
689
|
+
### SOC 2
|
|
690
|
+
|
|
691
|
+
| Control | Implementation |
|
|
692
|
+
|---|---|
|
|
693
|
+
| **CC6.1** — Logical access | Organization-scoped queries. Auto-redacting logger strips tokens, keys, SMS body, and credentials from all log output. |
|
|
694
|
+
| **CC7.2** — Audit trail | `AuditAdapter` records timestamped entries with action, org, relay, user, IP, and metadata. |
|
|
695
|
+
|
|
696
|
+
### E2E Encryption Details
|
|
697
|
+
|
|
698
|
+
```
|
|
699
|
+
Key Exchange: X25519 (Curve25519 Diffie-Hellman)
|
|
700
|
+
Key Derivation: HKDF-SHA256 with info string "zi2-relay-e2e-v1"
|
|
701
|
+
Payload Cipher: AES-256-GCM (12-byte IV, 16-byte auth tag)
|
|
702
|
+
Token Security: SHA-256 hashing, timing-safe comparison
|
|
703
|
+
Key Rotation: Server-initiated rekey via WebSocket protocol
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### Auto-Redacting Logger
|
|
707
|
+
|
|
708
|
+
The built-in `ConsoleLogger` (and the `withRedaction()` wrapper for custom loggers) automatically redacts these fields from all log output:
|
|
709
|
+
|
|
710
|
+
`token`, `authToken`, `pairingToken`, `authTokenHash`, `sharedKey`, `sharedKeyEncrypted`, `privateKey`, `serverPrivateKey`, `serverPrivateKeyEncrypted`, `devicePublicKey`, `serverPublicKey`, `encryptedPayload`, `password`, `secret`, `credentials`, `authorization`, `cookie`, `body`
|
|
711
|
+
|
|
712
|
+
```typescript
|
|
713
|
+
import { withRedaction } from '@zi2/relay-sdk';
|
|
714
|
+
|
|
715
|
+
const safeLogger = withRedaction(myPinoLogger);
|
|
716
|
+
const sdk = createRelay({ logger: safeLogger, /* ... */ });
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## i18n
|
|
722
|
+
|
|
723
|
+
The SDK ships with translations for 10 languages covering all UI strings and error messages.
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
import { getTranslation, getSupportedLocales, getRelayTranslations } from '@zi2/relay-sdk';
|
|
727
|
+
|
|
728
|
+
// Get a single translation
|
|
729
|
+
getTranslation('fr', 'relay.connected');
|
|
730
|
+
// → "Connect\u00e9"
|
|
731
|
+
|
|
732
|
+
getTranslation('ja', 'relay.e2eEncrypted');
|
|
733
|
+
// → "E2E\u6697\u53f7\u5316"
|
|
734
|
+
|
|
735
|
+
// List all supported locales
|
|
736
|
+
getSupportedLocales();
|
|
737
|
+
// → ['en', 'fr', 'es', 'de', 'ja', 'ar', 'zh', 'pt', 'ko', 'it']
|
|
738
|
+
|
|
739
|
+
// Get all translations for a locale (useful for client-side hydration)
|
|
740
|
+
const strings = getRelayTranslations('es');
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### Supported Locales
|
|
744
|
+
|
|
745
|
+
| Code | Language |
|
|
746
|
+
|---|---|
|
|
747
|
+
| `en` | English |
|
|
748
|
+
| `fr` | French |
|
|
749
|
+
| `es` | Spanish |
|
|
750
|
+
| `de` | German |
|
|
751
|
+
| `ja` | Japanese |
|
|
752
|
+
| `ar` | Arabic |
|
|
753
|
+
| `zh` | Chinese (Simplified) |
|
|
754
|
+
| `pt` | Portuguese |
|
|
755
|
+
| `ko` | Korean |
|
|
756
|
+
| `it` | Italian |
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
## White-Label Android App
|
|
761
|
+
|
|
762
|
+
The relay Android app can be white-labeled for custom deployments. A configuration file and build script handle rebranding.
|
|
763
|
+
|
|
764
|
+
### Configuration
|
|
765
|
+
|
|
766
|
+
Create a `relay.config.json` in your project:
|
|
767
|
+
|
|
768
|
+
```json
|
|
769
|
+
{
|
|
770
|
+
"appId": "com.yourcompany.smsrelay",
|
|
771
|
+
"appName": "YourBrand SMS Relay",
|
|
772
|
+
"serverUrl": "wss://api.yourcompany.com/ws/relay",
|
|
773
|
+
"brandColor": "#FF6600",
|
|
774
|
+
"notificationTitle": "YourBrand SMS Relay",
|
|
775
|
+
"notificationText": "Relay is active — ready to send SMS"
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
| Field | Description |
|
|
780
|
+
|---|---|
|
|
781
|
+
| `appId` | Android application ID (reverse domain). Must be unique on Google Play. |
|
|
782
|
+
| `appName` | Display name shown on the device home screen and in settings. |
|
|
783
|
+
| `serverUrl` | WebSocket URL the app connects to. Must use `wss://` in production. |
|
|
784
|
+
| `brandColor` | Primary brand color (hex). Applied to the app theme. |
|
|
785
|
+
| `notificationTitle` | Title of the persistent foreground service notification. |
|
|
786
|
+
| `notificationText` | Body text of the foreground service notification. |
|
|
787
|
+
|
|
788
|
+
### Building
|
|
789
|
+
|
|
790
|
+
```bash
|
|
791
|
+
# Default config (./relay.config.json)
|
|
792
|
+
./build-whitelabel.sh
|
|
793
|
+
|
|
794
|
+
# Custom config path
|
|
795
|
+
./build-whitelabel.sh /path/to/client-config.json
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
The script:
|
|
799
|
+
1. Reads the config JSON
|
|
800
|
+
2. Generates `capacitor.config.ts` with the custom `appId` and `appName`
|
|
801
|
+
3. Updates `android/app/src/main/res/values/strings.xml` with notification strings
|
|
802
|
+
4. Runs `pnpm build` (Vite production build)
|
|
803
|
+
5. Syncs the web assets to the Android project via `npx cap sync android`
|
|
804
|
+
6. Builds a release APK via Gradle
|
|
805
|
+
7. Outputs the final APK path
|
|
806
|
+
|
|
807
|
+
### Requirements
|
|
808
|
+
|
|
809
|
+
- Node.js 18+
|
|
810
|
+
- pnpm
|
|
811
|
+
- Android SDK (API level 24+)
|
|
812
|
+
- Java 17+ (for Gradle)
|
|
813
|
+
- `jq` (for JSON parsing in the build script)
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## Relay Management
|
|
818
|
+
|
|
819
|
+
### List Devices
|
|
820
|
+
|
|
821
|
+
```typescript
|
|
822
|
+
const relays = await sdk.listRelays('org_123');
|
|
823
|
+
// Returns: RelayListItem[] with isOnline status, stats, rate limits
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### Update Settings
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
await sdk.updateRelay('relay_abc', 'org_123', {
|
|
830
|
+
deviceName: 'Office Phone',
|
|
831
|
+
maxSmsPerDay: 500,
|
|
832
|
+
});
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
### Revoke a Device
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
await sdk.revokeRelay('relay_abc', 'org_123');
|
|
839
|
+
// Device is immediately disconnected and cannot reconnect
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### Rotate Encryption Keys
|
|
843
|
+
|
|
844
|
+
```typescript
|
|
845
|
+
await sdk.rotateKeys('relay_abc', 'org_123');
|
|
846
|
+
// Triggers rekey protocol over WebSocket — both sides derive new shared key
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## License
|
|
852
|
+
|
|
853
|
+
Proprietary. Copyright Zenith Intelligence Technologies / ZI2 Systems. All rights reserved.
|
|
854
|
+
|
|
855
|
+
This software is licensed exclusively for use within authorized ZI2 deployments. Unauthorized copying, modification, distribution, or use of this software is strictly prohibited. Contact [support@zisquare.app](mailto:support@zisquare.app) for licensing inquiries.
|