@zi2/relay-sdk 1.0.2 → 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/README.md +1 -2
- package/dist/adapters/prisma-adapter.d.ts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +2 -3
- package/dist/index.d.ts +15 -8
- package/dist/index.js +184 -45
- package/dist/server/fastify-plugin.d.ts +6 -2
- package/dist/server/fastify-plugin.js +3 -5
- package/dist/{types-CPJUrmcy.d.ts → types-BlrN83F-.d.ts} +10 -5
- package/package.json +1 -1
- package/prisma/schema.prisma +1 -1
package/README.md
CHANGED
|
@@ -228,7 +228,7 @@ model PhoneRelay {
|
|
|
228
228
|
organizationId String
|
|
229
229
|
deviceName String
|
|
230
230
|
platform String
|
|
231
|
-
|
|
231
|
+
relayIdentifier String @unique
|
|
232
232
|
devicePublicKey String
|
|
233
233
|
sharedKeyEncrypted String
|
|
234
234
|
authTokenHash String
|
|
@@ -364,7 +364,6 @@ const result = await sdk.completePairing({
|
|
|
364
364
|
devicePublicKey: '<device X25519 public key>',
|
|
365
365
|
deviceName: 'Samsung Galaxy S24',
|
|
366
366
|
platform: 'android',
|
|
367
|
-
phoneNumber: '+15551234567',
|
|
368
367
|
});
|
|
369
368
|
// Returns: { relayId, authToken }
|
|
370
369
|
```
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as DatabaseAdapter, P as PhoneRelayRecord, c as CreateRelayInput, d as PhoneRelayPairingRecord, e as CreatePairingInput, R as RelayMessageRecord, f as CreateMessageInput } from '../types-
|
|
1
|
+
import { D as DatabaseAdapter, P as PhoneRelayRecord, c as CreateRelayInput, d as PhoneRelayPairingRecord, e as CreatePairingInput, R as RelayMessageRecord, f as CreateMessageInput } from '../types-BlrN83F-.js';
|
|
2
2
|
import 'ws';
|
|
3
3
|
|
|
4
4
|
/**
|
package/dist/client/index.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ interface QrPayload {
|
|
|
39
39
|
url: string;
|
|
40
40
|
}
|
|
41
41
|
declare function parseQrPayload(data: string): QrPayload;
|
|
42
|
-
declare function completePairing(qr: QrPayload, deviceName: string, platform: string
|
|
42
|
+
declare function completePairing(qr: QrPayload, deviceName: string, platform: string): Promise<any>;
|
|
43
43
|
|
|
44
44
|
declare function requestSmsPermission(): Promise<boolean>;
|
|
45
45
|
declare function sendNativeSms(to: string, body: string): Promise<boolean>;
|
package/dist/client/index.js
CHANGED
|
@@ -208,7 +208,7 @@ function parseQrPayload(data) {
|
|
|
208
208
|
if (!parsed.pid || !parsed.spk || !parsed.pt || !parsed.url) throw new Error("Invalid QR data");
|
|
209
209
|
return parsed;
|
|
210
210
|
}
|
|
211
|
-
async function completePairing(qr, deviceName, platform
|
|
211
|
+
async function completePairing(qr, deviceName, platform) {
|
|
212
212
|
console.log("[Pairing] Starting pairing...");
|
|
213
213
|
console.log("[Pairing] QR url:", qr.url);
|
|
214
214
|
const keyPair = await generateX25519KeyPair();
|
|
@@ -226,8 +226,7 @@ async function completePairing(qr, deviceName, platform, phoneNumber) {
|
|
|
226
226
|
pairingToken: qr.pt,
|
|
227
227
|
devicePublicKey: keyPair.publicKey,
|
|
228
228
|
deviceName,
|
|
229
|
-
platform
|
|
230
|
-
phoneNumber
|
|
229
|
+
platform
|
|
231
230
|
};
|
|
232
231
|
const response = await fetch(endpoint, {
|
|
233
232
|
method: "POST",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as EncryptionAdapter, B as BroadcastAdapter, L as LoggerAdapter, A as AuditAdapter, a as AuditEntry, D as DatabaseAdapter, S as SmsProviderAdapter, b as SmsSendResponse, F as FallbackConfig, C as ConnectionBroker, P as PhoneRelayRecord, c as CreateRelayInput, d as PhoneRelayPairingRecord, e as CreatePairingInput, R as RelayMessageRecord, f as CreateMessageInput, g as RelaySDKConfig, h as RelaySDK } from './types-
|
|
2
|
-
export { M as MessageList, i as PairingCompleteResult, j as PairingResult, k as RelayDetail, l as RelayListItem, m as SendResult } from './types-
|
|
1
|
+
import { E as EncryptionAdapter, B as BroadcastAdapter, L as LoggerAdapter, A as AuditAdapter, a as AuditEntry, D as DatabaseAdapter, S as SmsProviderAdapter, b as SmsSendResponse, F as FallbackConfig, C as ConnectionBroker, P as PhoneRelayRecord, c as CreateRelayInput, d as PhoneRelayPairingRecord, e as CreatePairingInput, R as RelayMessageRecord, f as CreateMessageInput, g as RelaySDKConfig, h as RelaySDK } from './types-BlrN83F-.js';
|
|
2
|
+
export { M as MessageList, i as PairingCompleteResult, j as PairingResult, k as RelayDetail, l as RelayListItem, m as SendResult } from './types-BlrN83F-.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { WebSocket } from 'ws';
|
|
5
5
|
|
|
@@ -63,21 +63,18 @@ declare const completePairingSchema: z.ZodObject<{
|
|
|
63
63
|
devicePublicKey: z.ZodString;
|
|
64
64
|
deviceName: z.ZodString;
|
|
65
65
|
platform: z.ZodEnum<["android", "ios"]>;
|
|
66
|
-
phoneNumber: z.ZodOptional<z.ZodString>;
|
|
67
66
|
}, "strip", z.ZodTypeAny, {
|
|
68
67
|
deviceName: string;
|
|
69
68
|
platform: "android" | "ios";
|
|
70
69
|
devicePublicKey: string;
|
|
71
70
|
pairingToken: string;
|
|
72
71
|
pairingId: string;
|
|
73
|
-
phoneNumber?: string | undefined;
|
|
74
72
|
}, {
|
|
75
73
|
deviceName: string;
|
|
76
74
|
platform: "android" | "ios";
|
|
77
75
|
devicePublicKey: string;
|
|
78
76
|
pairingToken: string;
|
|
79
77
|
pairingId: string;
|
|
80
|
-
phoneNumber?: string | undefined;
|
|
81
78
|
}>;
|
|
82
79
|
declare const updatePhoneRelaySchema: z.ZodObject<{
|
|
83
80
|
deviceName: z.ZodOptional<z.ZodString>;
|
|
@@ -453,8 +450,8 @@ declare function createQueueService(deps: QueueServiceDeps): {
|
|
|
453
450
|
messageId: string;
|
|
454
451
|
status: string;
|
|
455
452
|
}>;
|
|
456
|
-
handleAck: (messageId: string, status: string, nativeMessageId?: string, errorMessage?: string) => Promise<void>;
|
|
457
|
-
handleDeliveryReceipt: (messageId: string, deliveryStatus: string) => Promise<void>;
|
|
453
|
+
handleAck: (messageId: string, relayId: string, status: string, nativeMessageId?: string, errorMessage?: string) => Promise<void>;
|
|
454
|
+
handleDeliveryReceipt: (messageId: string, relayId: string, deliveryStatus: string) => Promise<void>;
|
|
458
455
|
drainQueue: (relayId: string) => Promise<void>;
|
|
459
456
|
};
|
|
460
457
|
type QueueService = ReturnType<typeof createQueueService>;
|
|
@@ -565,6 +562,16 @@ declare function decryptE2E(ciphertext: string, sharedKey: Buffer): string;
|
|
|
565
562
|
declare function hashToken(token: string): string;
|
|
566
563
|
declare function generateSecureToken(bytes?: number): string;
|
|
567
564
|
declare function timingSafeCompare(a: string, b: string): boolean;
|
|
565
|
+
/**
|
|
566
|
+
* Generate a unique anonymous relay identifier.
|
|
567
|
+
* Format: RELAY-XXXX (4 random hex chars, uppercase)
|
|
568
|
+
*
|
|
569
|
+
* Patent claim: The relay server assigns cryptographic identifiers
|
|
570
|
+
* to physical devices WITHOUT ever collecting or storing the device's
|
|
571
|
+
* phone number. The identifier has no mathematical relationship to
|
|
572
|
+
* the phone number.
|
|
573
|
+
*/
|
|
574
|
+
declare function generateRelayIdentifier(): string;
|
|
568
575
|
|
|
569
576
|
/**
|
|
570
577
|
* @zi2/relay-sdk — Standalone SMS Relay SDK
|
|
@@ -590,4 +597,4 @@ declare function timingSafeCompare(a: string, b: string): boolean;
|
|
|
590
597
|
|
|
591
598
|
declare function createRelay(config: RelaySDKConfig): RelaySDK;
|
|
592
599
|
|
|
593
|
-
export { AesGcmEncryption, AuditAdapter, AuditEntry, AuthLimiter, BroadcastAdapter, type CompletePairingInput, ConnectionBroker, ConsoleAudit, ConsoleLogger, CreateMessageInput, CreatePairingInput, CreateRelayInput, DatabaseAdapter, EncryptionAdapter, FallbackConfig, FallbackProvider, LoggerAdapter, MemoryAdapter, NoopAudit, NoopBroadcast, PhoneRelayPairingRecord, PhoneRelayProvider, PhoneRelayRecord, RELAY_DELIVERY_STATUS, RELAY_ERRORS, RELAY_ERRORS_EXTENDED, RELAY_LIMITS, RELAY_MESSAGE_STATUS, RELAY_MESSAGE_TYPES, RELAY_PAIRING_STATUS, RELAY_PLATFORMS, RELAY_STATUS, RedisBroker, RelayError, RelayMessageRecord, type RelayMessageStatus, type RelayMessageType, type RelayPlatform, RelaySDK, RelaySDKConfig, type RelayStatus, SmsProviderAdapter, SmsSendResponse, type TestRelaySmsInput, type UpdatePhoneRelayInput, completePairingSchema, createRelay, createRelayError, decryptE2E, deriveSharedKey, encryptE2E, generateSecureToken, generateX25519KeyPair, getRelaySocket, getRelayTranslations, getSupportedLocales, getTranslation, hashToken, isRelayOnline, relayMessagesQuerySchema, testRelaySmsSchema, timingSafeCompare, updatePhoneRelaySchema, withRedaction };
|
|
600
|
+
export { AesGcmEncryption, AuditAdapter, AuditEntry, AuthLimiter, BroadcastAdapter, type CompletePairingInput, ConnectionBroker, ConsoleAudit, ConsoleLogger, CreateMessageInput, CreatePairingInput, CreateRelayInput, DatabaseAdapter, EncryptionAdapter, FallbackConfig, FallbackProvider, LoggerAdapter, MemoryAdapter, NoopAudit, NoopBroadcast, PhoneRelayPairingRecord, PhoneRelayProvider, PhoneRelayRecord, RELAY_DELIVERY_STATUS, RELAY_ERRORS, RELAY_ERRORS_EXTENDED, RELAY_LIMITS, RELAY_MESSAGE_STATUS, RELAY_MESSAGE_TYPES, RELAY_PAIRING_STATUS, RELAY_PLATFORMS, RELAY_STATUS, RedisBroker, RelayError, RelayMessageRecord, type RelayMessageStatus, type RelayMessageType, type RelayPlatform, RelaySDK, RelaySDKConfig, type RelayStatus, SmsProviderAdapter, SmsSendResponse, type TestRelaySmsInput, type UpdatePhoneRelayInput, completePairingSchema, createRelay, createRelayError, decryptE2E, deriveSharedKey, encryptE2E, generateRelayIdentifier, generateSecureToken, generateX25519KeyPair, getRelaySocket, getRelayTranslations, getSupportedLocales, getTranslation, hashToken, isRelayOnline, relayMessagesQuerySchema, testRelaySmsSchema, timingSafeCompare, updatePhoneRelaySchema, withRedaction };
|
package/dist/index.js
CHANGED
|
@@ -128,11 +128,21 @@ function createQueueService(deps) {
|
|
|
128
128
|
if (!relay || relay.status !== RELAY_STATUS.ACTIVE && relay.status !== RELAY_STATUS.DEGRADED) {
|
|
129
129
|
throw new Error("Relay not found or inactive");
|
|
130
130
|
}
|
|
131
|
+
const todayUTC = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
132
|
+
const dailyCount = relay.dailyResetAt && relay.dailyResetAt.toISOString().slice(0, 10) === todayUTC ? relay.dailySmsSent : 0;
|
|
133
|
+
if (dailyCount >= relay.maxSmsPerDay) {
|
|
134
|
+
throw new Error(`Daily SMS limit reached (${relay.maxSmsPerDay}/day)`);
|
|
135
|
+
}
|
|
131
136
|
let sharedKeyHex;
|
|
132
137
|
try {
|
|
133
|
-
|
|
138
|
+
const decrypted = encryption.decrypt(relay.sharedKeyEncrypted);
|
|
139
|
+
if (decrypted.startsWith("{")) {
|
|
140
|
+
throw new Error("Relay is in pending key rotation \u2014 retry after rotation completes");
|
|
141
|
+
}
|
|
142
|
+
sharedKeyHex = decrypted;
|
|
134
143
|
} catch (err) {
|
|
135
|
-
|
|
144
|
+
logger.error("Failed to decrypt relay shared key", { relayId, error: String(err) });
|
|
145
|
+
throw new Error("Failed to prepare message for relay");
|
|
136
146
|
}
|
|
137
147
|
const sharedKey = Buffer.from(sharedKeyHex, "hex");
|
|
138
148
|
const payload = JSON.stringify({ to, body });
|
|
@@ -170,7 +180,12 @@ function createQueueService(deps) {
|
|
|
170
180
|
});
|
|
171
181
|
});
|
|
172
182
|
}
|
|
173
|
-
async function handleAck(messageId, status, nativeMessageId, errorMessage) {
|
|
183
|
+
async function handleAck(messageId, relayId, status, nativeMessageId, errorMessage) {
|
|
184
|
+
const message = await db.findMessage(messageId).catch(() => null);
|
|
185
|
+
if (!message || message.relayId !== relayId) {
|
|
186
|
+
logger.warn("ACK rejected \u2014 message does not belong to relay", { messageId, relayId });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
174
189
|
const updateData = {
|
|
175
190
|
status: status === "sent" ? RELAY_MESSAGE_STATUS.DELIVERED : RELAY_MESSAGE_STATUS.FAILED,
|
|
176
191
|
ackedAt: /* @__PURE__ */ new Date()
|
|
@@ -178,32 +193,37 @@ function createQueueService(deps) {
|
|
|
178
193
|
if (nativeMessageId) updateData.nativeMessageId = nativeMessageId;
|
|
179
194
|
if (errorMessage) updateData.errorMessage = errorMessage;
|
|
180
195
|
await db.updateMessage(messageId, updateData);
|
|
181
|
-
const message = await db.findMessage(messageId).catch(() => null);
|
|
182
196
|
try {
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
await db.incrementRelayStat(message.relayId, "totalSmsFailed");
|
|
189
|
-
}
|
|
197
|
+
if (status === "sent") {
|
|
198
|
+
await db.incrementRelayStat(message.relayId, "totalSmsSent");
|
|
199
|
+
await db.incrementRelayStat(message.relayId, "dailySmsSent");
|
|
200
|
+
} else {
|
|
201
|
+
await db.incrementRelayStat(message.relayId, "totalSmsFailed");
|
|
190
202
|
}
|
|
191
203
|
} catch (err) {
|
|
192
204
|
logger.error("Failed to update relay stats after ack", { messageId, error: String(err) });
|
|
193
205
|
}
|
|
194
206
|
ackEmitter.emit(`ack:${messageId}`, status);
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
events.emit("message:failed", messageId, message.relayId, errorMessage || "Unknown error");
|
|
200
|
-
}
|
|
207
|
+
if (status === "sent") {
|
|
208
|
+
events.emit("message:delivered", messageId, message.relayId);
|
|
209
|
+
} else {
|
|
210
|
+
events.emit("message:failed", messageId, message.relayId, errorMessage || "Unknown error");
|
|
201
211
|
}
|
|
202
212
|
}
|
|
203
|
-
async function handleDeliveryReceipt(messageId, deliveryStatus) {
|
|
213
|
+
async function handleDeliveryReceipt(messageId, relayId, deliveryStatus) {
|
|
214
|
+
const message = await db.findMessage(messageId).catch(() => null);
|
|
215
|
+
if (!message || message.relayId !== relayId) {
|
|
216
|
+
logger.warn("Delivery receipt rejected \u2014 message does not belong to relay", { messageId, relayId });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
204
219
|
await db.updateMessage(messageId, { deliveryStatus });
|
|
205
220
|
}
|
|
206
221
|
async function drainQueue(relayId) {
|
|
222
|
+
const relay = await db.findRelay(relayId);
|
|
223
|
+
if (!relay || relay.status === "revoked") {
|
|
224
|
+
logger.warn("drainQueue skipped \u2014 relay revoked or not found", { relayId });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
207
227
|
const pendingMessages = await db.findPendingMessages(relayId, 50);
|
|
208
228
|
const ws = getRelaySocket(relayId);
|
|
209
229
|
if (!ws || ws.readyState !== 1) return;
|
|
@@ -240,6 +260,9 @@ function timingSafeCompare(a, b) {
|
|
|
240
260
|
return false;
|
|
241
261
|
}
|
|
242
262
|
}
|
|
263
|
+
function generateRelayIdentifier() {
|
|
264
|
+
return `RELAY-${crypto3.randomBytes(2).toString("hex").toUpperCase()}`;
|
|
265
|
+
}
|
|
243
266
|
|
|
244
267
|
// src/errors.ts
|
|
245
268
|
var RelayError = class extends Error {
|
|
@@ -311,11 +334,12 @@ function createPairingService(deps) {
|
|
|
311
334
|
wsUrl
|
|
312
335
|
};
|
|
313
336
|
}
|
|
314
|
-
async function completePairing(pairingId, pairingToken, devicePublicKey, deviceName, platform
|
|
337
|
+
async function completePairing(pairingId, pairingToken, devicePublicKey, deviceName, platform) {
|
|
315
338
|
const pairing = await db.findPairing(pairingId);
|
|
316
339
|
if (!pairing) throw createRelayError("PAIRING_NOT_FOUND");
|
|
317
340
|
if (pairing.status !== RELAY_PAIRING_STATUS.PENDING) throw createRelayError("PAIRING_EXPIRED");
|
|
318
341
|
if (!timingSafeCompare(pairing.pairingToken, pairingToken)) {
|
|
342
|
+
await db.updatePairingStatus(pairingId, RELAY_PAIRING_STATUS.EXPIRED);
|
|
319
343
|
throw createRelayError("PAIRING_INVALID_TOKEN");
|
|
320
344
|
}
|
|
321
345
|
if (/* @__PURE__ */ new Date() > pairing.expiresAt) {
|
|
@@ -326,11 +350,20 @@ function createPairingService(deps) {
|
|
|
326
350
|
const sharedKey = deriveSharedKey(serverPrivateKey, devicePublicKey);
|
|
327
351
|
const authToken = generateSecureToken(64);
|
|
328
352
|
const authTokenHashed = hashToken(authToken);
|
|
353
|
+
const existingRelays = await db.findRelays(pairing.organizationId, "revoked");
|
|
354
|
+
let uniqueName = deviceName;
|
|
355
|
+
const existingNames = existingRelays.map((r) => r.deviceName);
|
|
356
|
+
if (existingNames.includes(uniqueName)) {
|
|
357
|
+
let counter = 2;
|
|
358
|
+
while (existingNames.includes(`${deviceName} (${counter})`)) counter++;
|
|
359
|
+
uniqueName = `${deviceName} (${counter})`;
|
|
360
|
+
}
|
|
361
|
+
const relayIdentifier = generateRelayIdentifier();
|
|
329
362
|
const relay = await db.createRelay({
|
|
330
363
|
organizationId: pairing.organizationId,
|
|
331
|
-
deviceName,
|
|
364
|
+
deviceName: uniqueName,
|
|
332
365
|
platform,
|
|
333
|
-
|
|
366
|
+
relayIdentifier,
|
|
334
367
|
devicePublicKey,
|
|
335
368
|
sharedKeyEncrypted: encryption.encrypt(sharedKey.toString("hex")),
|
|
336
369
|
authTokenHash: authTokenHashed,
|
|
@@ -348,7 +381,7 @@ function createPairingService(deps) {
|
|
|
348
381
|
});
|
|
349
382
|
await db.updatePairingStatus(pairingId, RELAY_PAIRING_STATUS.COMPLETED);
|
|
350
383
|
try {
|
|
351
|
-
await deps.onPairingComplete?.({ relayId: relay.id, orgId: pairing.organizationId, deviceName,
|
|
384
|
+
await deps.onPairingComplete?.({ relayId: relay.id, orgId: pairing.organizationId, deviceName: uniqueName, relayIdentifier });
|
|
352
385
|
} catch (err) {
|
|
353
386
|
deps.logger.error("onPairingComplete callback failed", { relayId: relay.id, error: String(err) });
|
|
354
387
|
}
|
|
@@ -376,14 +409,32 @@ function createPairingService(deps) {
|
|
|
376
409
|
}
|
|
377
410
|
const { publicKey, privateKey } = generateX25519KeyPair();
|
|
378
411
|
ws.send(JSON.stringify({ type: "rekey", serverPublicKey: publicKey }));
|
|
412
|
+
const previousKey = relay.sharedKeyEncrypted;
|
|
379
413
|
await db.updateRelay(relayId, {
|
|
380
414
|
sharedKeyEncrypted: encryption.encrypt(JSON.stringify({
|
|
381
415
|
pending: true,
|
|
382
416
|
serverPrivateKey: privateKey,
|
|
383
417
|
serverPublicKey: publicKey,
|
|
384
|
-
previousSharedKey:
|
|
418
|
+
previousSharedKey: previousKey
|
|
385
419
|
}))
|
|
386
420
|
});
|
|
421
|
+
setTimeout(async () => {
|
|
422
|
+
try {
|
|
423
|
+
const current = await db.findRelay(relayId, orgId);
|
|
424
|
+
if (!current) return;
|
|
425
|
+
const decrypted = encryption.decrypt(current.sharedKeyEncrypted);
|
|
426
|
+
try {
|
|
427
|
+
const parsed = JSON.parse(decrypted);
|
|
428
|
+
if (parsed.pending) {
|
|
429
|
+
logger.warn("Rekey timeout \u2014 rolling back to previous key", { relayId });
|
|
430
|
+
await db.updateRelay(relayId, { sharedKeyEncrypted: parsed.previousSharedKey });
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
} catch (err) {
|
|
435
|
+
logger.error("Rekey rollback failed", { relayId, error: String(err) });
|
|
436
|
+
}
|
|
437
|
+
}, 6e4);
|
|
387
438
|
}
|
|
388
439
|
return { initiatePairing, completePairing, revokeRelay, rotateKeys };
|
|
389
440
|
}
|
|
@@ -635,7 +686,7 @@ var FallbackProvider = class {
|
|
|
635
686
|
var RATE_LIMIT_WINDOW_MS = 1e4;
|
|
636
687
|
var RATE_LIMIT_MAX_MESSAGES = 100;
|
|
637
688
|
function createWebSocketHandler(deps) {
|
|
638
|
-
const { db, encryption, broadcast, logger, events, queue, limits } = deps;
|
|
689
|
+
const { db, encryption, broadcast, logger, events, queue, limits, audit } = deps;
|
|
639
690
|
return function handleWebSocket(socket, request) {
|
|
640
691
|
const log = request.log || logger;
|
|
641
692
|
let authenticated = false;
|
|
@@ -661,6 +712,8 @@ function createWebSocketHandler(deps) {
|
|
|
661
712
|
messageCount++;
|
|
662
713
|
if (messageCount > RATE_LIMIT_MAX_MESSAGES) {
|
|
663
714
|
log.warn("Relay WebSocket rate limit exceeded", { relayId, ip });
|
|
715
|
+
audit.log({ timestamp: /* @__PURE__ */ new Date(), action: "relay.rate_limited", organizationId: relayOrgId || "", relayId: relayId || void 0, ip }).catch(() => {
|
|
716
|
+
});
|
|
664
717
|
socket.close(4008, "Rate limit exceeded");
|
|
665
718
|
return;
|
|
666
719
|
}
|
|
@@ -680,6 +733,11 @@ function createWebSocketHandler(deps) {
|
|
|
680
733
|
socket.close(4003, "Missing relayId or token");
|
|
681
734
|
return;
|
|
682
735
|
}
|
|
736
|
+
if (deps.authLimiter.isLocked(ip)) {
|
|
737
|
+
log.warn("WebSocket auth blocked \u2014 IP locked out", { ip });
|
|
738
|
+
socket.close(4009, "Too many failed attempts");
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
683
741
|
const tokenHash = hashToken(token);
|
|
684
742
|
const verifiedRelay = await db.findRelayByTokenHash(
|
|
685
743
|
rid,
|
|
@@ -687,10 +745,14 @@ function createWebSocketHandler(deps) {
|
|
|
687
745
|
[RELAY_STATUS.ACTIVE, RELAY_STATUS.DEGRADED]
|
|
688
746
|
).catch(() => null);
|
|
689
747
|
if (!verifiedRelay) {
|
|
748
|
+
deps.authLimiter.recordFailure(ip);
|
|
690
749
|
log.warn("Relay WebSocket auth failed", { relayId: rid });
|
|
750
|
+
audit.log({ timestamp: /* @__PURE__ */ new Date(), action: "relay.auth_failed", organizationId: "", relayId: rid, ip }).catch(() => {
|
|
751
|
+
});
|
|
691
752
|
socket.close(4004, "Invalid credentials");
|
|
692
753
|
return;
|
|
693
754
|
}
|
|
755
|
+
deps.authLimiter.recordSuccess(ip);
|
|
694
756
|
clearTimeout(authTimeout);
|
|
695
757
|
authenticated = true;
|
|
696
758
|
relayId = rid;
|
|
@@ -701,12 +763,14 @@ function createWebSocketHandler(deps) {
|
|
|
701
763
|
existing.close(4005, "Replaced by new connection");
|
|
702
764
|
}
|
|
703
765
|
setRelaySocket(rid, socket);
|
|
704
|
-
await db.
|
|
766
|
+
await db.updateRelayByOrg(rid, relayOrgId, {
|
|
705
767
|
lastSeenAt: /* @__PURE__ */ new Date(),
|
|
706
768
|
lastIpAddress: ip,
|
|
707
769
|
status: RELAY_STATUS.ACTIVE
|
|
708
770
|
});
|
|
709
771
|
socket.send(JSON.stringify({ type: "auth_ok" }));
|
|
772
|
+
audit.log({ timestamp: /* @__PURE__ */ new Date(), action: "relay.connected", organizationId: relayOrgId, relayId: rid, ip }).catch(() => {
|
|
773
|
+
});
|
|
710
774
|
broadcast.broadcast(verifiedRelay.organizationId, {
|
|
711
775
|
type: "relay:status_change",
|
|
712
776
|
relayId: rid,
|
|
@@ -739,42 +803,52 @@ function createWebSocketHandler(deps) {
|
|
|
739
803
|
const nativeId = isValidStr(message.nativeMessageId) ? message.nativeMessageId : void 0;
|
|
740
804
|
const errorMsg = isValidStr(message.errorMessage, 500) ? message.errorMessage : void 0;
|
|
741
805
|
log.debug("Relay SMS ACK", { relayId, messageId: message.messageId, status });
|
|
742
|
-
queue.handleAck(message.messageId, status, nativeId, errorMsg).catch(() => {
|
|
806
|
+
queue.handleAck(message.messageId, relayId, status, nativeId, errorMsg).catch(() => {
|
|
743
807
|
});
|
|
744
808
|
}
|
|
745
809
|
break;
|
|
746
810
|
case RELAY_MESSAGE_TYPES.DELIVERY_RECEIPT:
|
|
747
811
|
if (isValidStr(message.messageId) && isValidStr(message.deliveryStatus, 50)) {
|
|
748
|
-
queue.handleDeliveryReceipt(message.messageId, message.deliveryStatus).catch(() => {
|
|
812
|
+
queue.handleDeliveryReceipt(message.messageId, relayId, message.deliveryStatus).catch(() => {
|
|
749
813
|
});
|
|
750
814
|
}
|
|
751
815
|
break;
|
|
752
816
|
case RELAY_MESSAGE_TYPES.PONG:
|
|
753
|
-
if (relayId) {
|
|
754
|
-
db.
|
|
817
|
+
if (relayId && relayOrgId) {
|
|
818
|
+
db.updateRelayByOrg(relayId, relayOrgId, { lastSeenAt: /* @__PURE__ */ new Date(), lastIpAddress: ip }).catch(() => {
|
|
755
819
|
});
|
|
756
820
|
}
|
|
757
821
|
break;
|
|
758
822
|
case RELAY_MESSAGE_TYPES.STATUS:
|
|
759
|
-
if (relayId) {
|
|
823
|
+
if (relayId && relayOrgId) {
|
|
760
824
|
const battery = typeof message.batteryLevel === "number" && Number.isFinite(message.batteryLevel) ? Math.max(0, Math.min(100, Math.round(message.batteryLevel))) : null;
|
|
761
825
|
const signal = typeof message.signalStrength === "number" && Number.isFinite(message.signalStrength) ? Math.max(0, Math.min(100, Math.round(message.signalStrength))) : null;
|
|
762
826
|
const statusUpdate = { lastSeenAt: /* @__PURE__ */ new Date() };
|
|
763
827
|
if (battery !== null) statusUpdate.batteryLevel = battery;
|
|
764
828
|
if (signal !== null) statusUpdate.signalStrength = signal;
|
|
765
|
-
db.
|
|
829
|
+
db.updateRelayByOrg(relayId, relayOrgId, statusUpdate).catch(() => {
|
|
766
830
|
});
|
|
767
831
|
}
|
|
768
832
|
break;
|
|
769
833
|
case RELAY_MESSAGE_TYPES.REKEY_ACK:
|
|
770
|
-
if (relayId && isValidStr(message.devicePublicKey, 500)) {
|
|
834
|
+
if (relayId && relayOrgId && isValidStr(message.devicePublicKey, 500)) {
|
|
771
835
|
try {
|
|
772
836
|
const relay = await db.findRelay(relayId);
|
|
773
837
|
if (!relay) break;
|
|
774
838
|
const rekeyData = JSON.parse(encryption.decrypt(relay.sharedKeyEncrypted));
|
|
775
839
|
if (!rekeyData.pending) break;
|
|
840
|
+
try {
|
|
841
|
+
const keyBuf = Buffer.from(message.devicePublicKey, "hex");
|
|
842
|
+
if (keyBuf.length < 32) throw new Error("Key too short");
|
|
843
|
+
} catch {
|
|
844
|
+
socket.send(JSON.stringify({ type: "rekey_failed" }));
|
|
845
|
+
if (rekeyData.previousSharedKey) {
|
|
846
|
+
await db.updateRelayByOrg(relayId, relayOrgId, { sharedKeyEncrypted: rekeyData.previousSharedKey });
|
|
847
|
+
}
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
776
850
|
const newSharedKey = deriveSharedKey(rekeyData.serverPrivateKey, message.devicePublicKey);
|
|
777
|
-
await db.
|
|
851
|
+
await db.updateRelayByOrg(relayId, relayOrgId, {
|
|
778
852
|
sharedKeyEncrypted: encryption.encrypt(newSharedKey.toString("hex")),
|
|
779
853
|
devicePublicKey: message.devicePublicKey,
|
|
780
854
|
keyVersion: relay.keyVersion + 1,
|
|
@@ -782,8 +856,18 @@ function createWebSocketHandler(deps) {
|
|
|
782
856
|
});
|
|
783
857
|
socket.send(JSON.stringify({ type: "rekey_complete" }));
|
|
784
858
|
} catch {
|
|
785
|
-
log.warn("Rekey failed,
|
|
859
|
+
log.warn("Rekey failed, rolling back to previous key", { relayId });
|
|
786
860
|
socket.send(JSON.stringify({ type: "rekey_failed" }));
|
|
861
|
+
try {
|
|
862
|
+
const current = await deps.db.findRelay(relayId);
|
|
863
|
+
if (current) {
|
|
864
|
+
const data = JSON.parse(deps.encryption.decrypt(current.sharedKeyEncrypted));
|
|
865
|
+
if (data.pending && data.previousSharedKey) {
|
|
866
|
+
await deps.db.updateRelayByOrg(relayId, relayOrgId, { sharedKeyEncrypted: data.previousSharedKey });
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
} catch {
|
|
870
|
+
}
|
|
787
871
|
}
|
|
788
872
|
}
|
|
789
873
|
break;
|
|
@@ -795,6 +879,10 @@ function createWebSocketHandler(deps) {
|
|
|
795
879
|
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
796
880
|
if (relayId) {
|
|
797
881
|
deleteRelaySocket(relayId);
|
|
882
|
+
if (relayOrgId) {
|
|
883
|
+
audit.log({ timestamp: /* @__PURE__ */ new Date(), action: "relay.disconnected", organizationId: relayOrgId, relayId, ip }).catch(() => {
|
|
884
|
+
});
|
|
885
|
+
}
|
|
798
886
|
try {
|
|
799
887
|
if (relayOrgId) {
|
|
800
888
|
broadcast.broadcast(relayOrgId, {
|
|
@@ -925,8 +1013,7 @@ var completePairingSchema = z.object({
|
|
|
925
1013
|
pairingToken: z.string().min(1),
|
|
926
1014
|
devicePublicKey: z.string().min(44).max(5e3),
|
|
927
1015
|
deviceName: z.string().min(1).max(200),
|
|
928
|
-
platform: z.enum(["android", "ios"])
|
|
929
|
-
phoneNumber: z.string().max(30).optional()
|
|
1016
|
+
platform: z.enum(["android", "ios"])
|
|
930
1017
|
});
|
|
931
1018
|
var updatePhoneRelaySchema = z.object({
|
|
932
1019
|
deviceName: z.string().min(1).max(200).optional(),
|
|
@@ -935,7 +1022,7 @@ var updatePhoneRelaySchema = z.object({
|
|
|
935
1022
|
maxSmsPerDay: z.number().int().min(1).max(1e4).optional()
|
|
936
1023
|
});
|
|
937
1024
|
var testRelaySmsSchema = z.object({
|
|
938
|
-
to: z.string().min(1),
|
|
1025
|
+
to: z.string().min(1).regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format (E.164)"),
|
|
939
1026
|
body: z.string().min(1).max(160).optional()
|
|
940
1027
|
});
|
|
941
1028
|
var relayMessagesQuerySchema = z.object({
|
|
@@ -2393,24 +2480,33 @@ function createRelay(config) {
|
|
|
2393
2480
|
encryption: config.encryption,
|
|
2394
2481
|
broadcast,
|
|
2395
2482
|
logger,
|
|
2483
|
+
audit,
|
|
2396
2484
|
events,
|
|
2397
2485
|
queue,
|
|
2398
|
-
limits
|
|
2486
|
+
limits,
|
|
2487
|
+
authLimiter
|
|
2399
2488
|
});
|
|
2400
2489
|
const sdk = {
|
|
2401
2490
|
// Pairing
|
|
2402
2491
|
async initiatePairing(orgId, userId) {
|
|
2403
|
-
|
|
2492
|
+
const result = await pairing.initiatePairing(orgId, userId);
|
|
2493
|
+
audit.log({ timestamp: /* @__PURE__ */ new Date(), action: "pairing.initiated", organizationId: orgId, userId }).catch(() => {
|
|
2494
|
+
});
|
|
2495
|
+
return result;
|
|
2404
2496
|
},
|
|
2405
2497
|
async completePairing(input) {
|
|
2406
|
-
|
|
2498
|
+
const result = await pairing.completePairing(
|
|
2407
2499
|
input.pairingId,
|
|
2408
2500
|
input.pairingToken,
|
|
2409
2501
|
input.devicePublicKey,
|
|
2410
2502
|
input.deviceName,
|
|
2411
|
-
input.platform
|
|
2412
|
-
input.phoneNumber
|
|
2503
|
+
input.platform
|
|
2413
2504
|
);
|
|
2505
|
+
const relay = await config.db.findRelay(result.relayId);
|
|
2506
|
+
const orgId = relay?.organizationId || "";
|
|
2507
|
+
audit.log({ timestamp: /* @__PURE__ */ new Date(), action: "pairing.completed", organizationId: orgId, relayId: result.relayId }).catch(() => {
|
|
2508
|
+
});
|
|
2509
|
+
return result;
|
|
2414
2510
|
},
|
|
2415
2511
|
// Relay management
|
|
2416
2512
|
async listRelays(orgId) {
|
|
@@ -2435,16 +2531,58 @@ function createRelay(config) {
|
|
|
2435
2531
|
},
|
|
2436
2532
|
async updateRelay(id, orgId, data) {
|
|
2437
2533
|
await config.db.updateRelayByOrg(id, orgId, data);
|
|
2534
|
+
audit.log({ timestamp: /* @__PURE__ */ new Date(), action: "relay.updated", organizationId: orgId, relayId: id }).catch(() => {
|
|
2535
|
+
});
|
|
2438
2536
|
},
|
|
2439
2537
|
async revokeRelay(id, orgId) {
|
|
2440
|
-
|
|
2538
|
+
await pairing.revokeRelay(id, orgId);
|
|
2539
|
+
audit.log({ timestamp: /* @__PURE__ */ new Date(), action: "relay.revoked", organizationId: orgId, relayId: id }).catch(() => {
|
|
2540
|
+
});
|
|
2441
2541
|
},
|
|
2442
2542
|
async rotateKeys(id, orgId) {
|
|
2443
|
-
|
|
2543
|
+
await pairing.rotateKeys(id, orgId);
|
|
2544
|
+
audit.log({ timestamp: /* @__PURE__ */ new Date(), action: "relay.keys_rotated", organizationId: orgId, relayId: id }).catch(() => {
|
|
2545
|
+
});
|
|
2444
2546
|
},
|
|
2445
2547
|
// Messaging
|
|
2446
2548
|
async sendSMS(options) {
|
|
2447
|
-
|
|
2549
|
+
const result = await queue.enqueueAndWait(options);
|
|
2550
|
+
const action = result.status === "failed" ? "sms.failed" : "sms.sent";
|
|
2551
|
+
audit.log({ timestamp: /* @__PURE__ */ new Date(), action, organizationId: options.orgId, relayId: options.relayId, metadata: { messageId: result.messageId } }).catch(() => {
|
|
2552
|
+
});
|
|
2553
|
+
return result;
|
|
2554
|
+
},
|
|
2555
|
+
async sendSMSToOrg(options) {
|
|
2556
|
+
const { orgId, to, body, timeoutMs } = options;
|
|
2557
|
+
const allRelays = await config.db.findRelays(orgId, "revoked");
|
|
2558
|
+
const activeRelays = allRelays.filter((r) => r.status === "active" || r.status === "degraded");
|
|
2559
|
+
if (activeRelays.length === 0) {
|
|
2560
|
+
throw new Error("No active relays available for this organization");
|
|
2561
|
+
}
|
|
2562
|
+
const todayUTC = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2563
|
+
const onlineRelays = activeRelays.filter((r) => isRelayOnline(r.id));
|
|
2564
|
+
const offlineRelays = activeRelays.filter((r) => !isRelayOnline(r.id));
|
|
2565
|
+
const sortedOnline = [...onlineRelays].sort((a, b) => {
|
|
2566
|
+
const aDaily = a.dailyResetAt && a.dailyResetAt.toISOString().slice(0, 10) === todayUTC ? a.dailySmsSent : 0;
|
|
2567
|
+
const bDaily = b.dailyResetAt && b.dailyResetAt.toISOString().slice(0, 10) === todayUTC ? b.dailySmsSent : 0;
|
|
2568
|
+
return aDaily - bDaily;
|
|
2569
|
+
});
|
|
2570
|
+
const sortedOffline = [...offlineRelays].sort((a, b) => {
|
|
2571
|
+
const aTime = a.lastSeenAt ? a.lastSeenAt.getTime() : 0;
|
|
2572
|
+
const bTime = b.lastSeenAt ? b.lastSeenAt.getTime() : 0;
|
|
2573
|
+
return bTime - aTime;
|
|
2574
|
+
});
|
|
2575
|
+
const candidates = [...sortedOnline, ...sortedOffline];
|
|
2576
|
+
let lastError;
|
|
2577
|
+
for (const candidate of candidates) {
|
|
2578
|
+
try {
|
|
2579
|
+
return await queue.enqueueAndWait({ relayId: candidate.id, orgId, to, body, timeoutMs });
|
|
2580
|
+
} catch (err) {
|
|
2581
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2582
|
+
logger.warn("sendSMSToOrg: relay failed, trying next", { relayId: candidate.id, error: lastError.message });
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
throw lastError || new Error("All relays failed");
|
|
2448
2586
|
},
|
|
2449
2587
|
async getMessages(relayId, orgId, limit = 50, offset = 0) {
|
|
2450
2588
|
const relay = await config.db.findRelay(relayId, orgId);
|
|
@@ -2525,6 +2663,7 @@ export {
|
|
|
2525
2663
|
decryptE2E,
|
|
2526
2664
|
deriveSharedKey,
|
|
2527
2665
|
encryptE2E,
|
|
2666
|
+
generateRelayIdentifier,
|
|
2528
2667
|
generateSecureToken,
|
|
2529
2668
|
generateX25519KeyPair,
|
|
2530
2669
|
getRelaySocket,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as RelaySDK } from '../types-
|
|
1
|
+
import { h as RelaySDK } from '../types-BlrN83F-.js';
|
|
2
2
|
import 'ws';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -7,6 +7,10 @@ import 'ws';
|
|
|
7
7
|
*
|
|
8
8
|
* Requires @fastify/websocket for WebSocket support.
|
|
9
9
|
*
|
|
10
|
+
* **WebSocket maxPayload:** To limit inbound WebSocket frame sizes (recommended
|
|
11
|
+
* 64 KB for relay messages), configure `maxPayload` when registering the
|
|
12
|
+
* `@fastify/websocket` plugin — it cannot be set per-route:
|
|
13
|
+
*
|
|
10
14
|
* @example
|
|
11
15
|
* ```typescript
|
|
12
16
|
* import Fastify from 'fastify';
|
|
@@ -14,7 +18,7 @@ import 'ws';
|
|
|
14
18
|
* import { relayPlugin } from '@zi2/relay-sdk/fastify';
|
|
15
19
|
*
|
|
16
20
|
* const app = Fastify();
|
|
17
|
-
* await app.register(websocket);
|
|
21
|
+
* await app.register(websocket, { options: { maxPayload: 65536 } });
|
|
18
22
|
* // Use Fastify's prefix option for route namespacing:
|
|
19
23
|
* await app.register(relayPlugin, { sdk, prefix: '/phone-relay' });
|
|
20
24
|
* ```
|
|
@@ -5,8 +5,7 @@ var completePairingSchema = z.object({
|
|
|
5
5
|
pairingToken: z.string().min(1),
|
|
6
6
|
devicePublicKey: z.string().min(44).max(5e3),
|
|
7
7
|
deviceName: z.string().min(1).max(200),
|
|
8
|
-
platform: z.enum(["android", "ios"])
|
|
9
|
-
phoneNumber: z.string().max(30).optional()
|
|
8
|
+
platform: z.enum(["android", "ios"])
|
|
10
9
|
});
|
|
11
10
|
var updatePhoneRelaySchema = z.object({
|
|
12
11
|
deviceName: z.string().min(1).max(200).optional(),
|
|
@@ -15,7 +14,7 @@ var updatePhoneRelaySchema = z.object({
|
|
|
15
14
|
maxSmsPerDay: z.number().int().min(1).max(1e4).optional()
|
|
16
15
|
});
|
|
17
16
|
var testRelaySmsSchema = z.object({
|
|
18
|
-
to: z.string().min(1),
|
|
17
|
+
to: z.string().min(1).regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number format (E.164)"),
|
|
19
18
|
body: z.string().min(1).max(160).optional()
|
|
20
19
|
});
|
|
21
20
|
var relayMessagesQuerySchema = z.object({
|
|
@@ -37,8 +36,7 @@ function createRouteHandlers(sdk) {
|
|
|
37
36
|
pairingToken: parsed.pairingToken,
|
|
38
37
|
devicePublicKey: parsed.devicePublicKey,
|
|
39
38
|
deviceName: parsed.deviceName,
|
|
40
|
-
platform: parsed.platform
|
|
41
|
-
phoneNumber: parsed.phoneNumber
|
|
39
|
+
platform: parsed.platform
|
|
42
40
|
});
|
|
43
41
|
return result;
|
|
44
42
|
},
|
|
@@ -5,7 +5,7 @@ interface PhoneRelayRecord {
|
|
|
5
5
|
organizationId: string;
|
|
6
6
|
deviceName: string;
|
|
7
7
|
platform: string;
|
|
8
|
-
|
|
8
|
+
relayIdentifier: string;
|
|
9
9
|
devicePublicKey: string;
|
|
10
10
|
sharedKeyEncrypted: string;
|
|
11
11
|
authTokenHash: string;
|
|
@@ -149,7 +149,7 @@ interface RelaySDKConfig {
|
|
|
149
149
|
relayId: string;
|
|
150
150
|
orgId: string;
|
|
151
151
|
deviceName: string;
|
|
152
|
-
|
|
152
|
+
relayIdentifier: string;
|
|
153
153
|
}) => Promise<void>;
|
|
154
154
|
/** Called after relay is revoked. Use to clean up SmsProvider records. */
|
|
155
155
|
onRelayRevoked?: (relay: {
|
|
@@ -188,7 +188,7 @@ interface RelayListItem {
|
|
|
188
188
|
id: string;
|
|
189
189
|
deviceName: string;
|
|
190
190
|
platform: string;
|
|
191
|
-
|
|
191
|
+
relayIdentifier: string;
|
|
192
192
|
status: string;
|
|
193
193
|
lastSeenAt: Date | null;
|
|
194
194
|
batteryLevel: number | null;
|
|
@@ -207,7 +207,7 @@ interface RelayDetail {
|
|
|
207
207
|
organizationId: string;
|
|
208
208
|
deviceName: string;
|
|
209
209
|
platform: string;
|
|
210
|
-
|
|
210
|
+
relayIdentifier: string;
|
|
211
211
|
status: string;
|
|
212
212
|
keyVersion: number;
|
|
213
213
|
lastKeyRotation: Date;
|
|
@@ -249,7 +249,6 @@ interface RelaySDK {
|
|
|
249
249
|
devicePublicKey: string;
|
|
250
250
|
deviceName: string;
|
|
251
251
|
platform: string;
|
|
252
|
-
phoneNumber?: string;
|
|
253
252
|
}): Promise<PairingCompleteResult>;
|
|
254
253
|
listRelays(orgId: string): Promise<RelayListItem[]>;
|
|
255
254
|
getRelay(id: string, orgId: string): Promise<RelayDetail | null>;
|
|
@@ -263,6 +262,12 @@ interface RelaySDK {
|
|
|
263
262
|
body: string;
|
|
264
263
|
timeoutMs?: number;
|
|
265
264
|
}): Promise<SendResult>;
|
|
265
|
+
sendSMSToOrg(options: {
|
|
266
|
+
orgId: string;
|
|
267
|
+
to: string;
|
|
268
|
+
body: string;
|
|
269
|
+
timeoutMs?: number;
|
|
270
|
+
}): Promise<SendResult>;
|
|
266
271
|
getMessages(relayId: string, orgId: string, limit?: number, offset?: number): Promise<MessageList>;
|
|
267
272
|
isRelayOnline(relayId: string): boolean;
|
|
268
273
|
getRelaySocket(relayId: string): WebSocket | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zi2/relay-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Enterprise SMS relay SDK with E2E encryption, provider fallback, and PCI DSS v4 compliance",
|
|
5
5
|
"author": "Zenith Intelligence Technologies <dev@zisquare.app>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
package/prisma/schema.prisma
CHANGED
|
@@ -20,7 +20,7 @@ model PhoneRelay {
|
|
|
20
20
|
organizationId String @map("organization_id") @db.Uuid
|
|
21
21
|
deviceName String @map("device_name") @db.VarChar(200)
|
|
22
22
|
platform String @db.VarChar(20)
|
|
23
|
-
|
|
23
|
+
relayIdentifier String @unique @map("relay_identifier") @db.VarChar(10)
|
|
24
24
|
devicePublicKey String @map("device_public_key") @db.Text
|
|
25
25
|
sharedKeyEncrypted String @map("shared_key_encrypted") @db.Text
|
|
26
26
|
authTokenHash String @map("auth_token_hash") @db.VarChar(128)
|