@zi2/relay-sdk 1.0.1 → 1.0.3
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.
|
@@ -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-sIoVYfJj.js';
|
|
2
2
|
import 'ws';
|
|
3
3
|
|
|
4
4
|
/**
|
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-sIoVYfJj.js';
|
|
2
|
+
export { M as MessageList, i as PairingCompleteResult, j as PairingResult, k as RelayDetail, l as RelayListItem, m as SendResult } from './types-sIoVYfJj.js';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { WebSocket } from 'ws';
|
|
5
5
|
|
|
@@ -335,6 +335,7 @@ declare function createRelayError(key: keyof typeof RELAY_ERRORS): RelayError;
|
|
|
335
335
|
declare class AesGcmEncryption implements EncryptionAdapter {
|
|
336
336
|
private keyBuffer;
|
|
337
337
|
private allowPlaintextFallback;
|
|
338
|
+
private destroyed;
|
|
338
339
|
/**
|
|
339
340
|
* @param key 64-char hex string OR 32-byte Buffer for AES-256
|
|
340
341
|
* @param options.allowPlaintextFallback Allow decrypting legacy unencrypted values.
|
package/dist/index.js
CHANGED
|
@@ -326,9 +326,17 @@ function createPairingService(deps) {
|
|
|
326
326
|
const sharedKey = deriveSharedKey(serverPrivateKey, devicePublicKey);
|
|
327
327
|
const authToken = generateSecureToken(64);
|
|
328
328
|
const authTokenHashed = hashToken(authToken);
|
|
329
|
+
const existingRelays = await db.findRelays(pairing.organizationId, "revoked");
|
|
330
|
+
let uniqueName = deviceName;
|
|
331
|
+
const existingNames = existingRelays.map((r) => r.deviceName);
|
|
332
|
+
if (existingNames.includes(uniqueName)) {
|
|
333
|
+
let counter = 2;
|
|
334
|
+
while (existingNames.includes(`${deviceName} (${counter})`)) counter++;
|
|
335
|
+
uniqueName = `${deviceName} (${counter})`;
|
|
336
|
+
}
|
|
329
337
|
const relay = await db.createRelay({
|
|
330
338
|
organizationId: pairing.organizationId,
|
|
331
|
-
deviceName,
|
|
339
|
+
deviceName: uniqueName,
|
|
332
340
|
platform,
|
|
333
341
|
phoneNumber: phoneNumber || null,
|
|
334
342
|
devicePublicKey,
|
|
@@ -348,7 +356,7 @@ function createPairingService(deps) {
|
|
|
348
356
|
});
|
|
349
357
|
await db.updatePairingStatus(pairingId, RELAY_PAIRING_STATUS.COMPLETED);
|
|
350
358
|
try {
|
|
351
|
-
await deps.onPairingComplete?.({ relayId: relay.id, orgId: pairing.organizationId, deviceName, phoneNumber });
|
|
359
|
+
await deps.onPairingComplete?.({ relayId: relay.id, orgId: pairing.organizationId, deviceName: uniqueName, phoneNumber });
|
|
352
360
|
} catch (err) {
|
|
353
361
|
deps.logger.error("onPairingComplete callback failed", { relayId: relay.id, error: String(err) });
|
|
354
362
|
}
|
|
@@ -953,6 +961,7 @@ var AUTH_TAG_HEX_LENGTH = AUTH_TAG_LENGTH * 2;
|
|
|
953
961
|
var AesGcmEncryption = class {
|
|
954
962
|
keyBuffer;
|
|
955
963
|
allowPlaintextFallback;
|
|
964
|
+
destroyed = false;
|
|
956
965
|
/**
|
|
957
966
|
* @param key 64-char hex string OR 32-byte Buffer for AES-256
|
|
958
967
|
* @param options.allowPlaintextFallback Allow decrypting legacy unencrypted values.
|
|
@@ -979,6 +988,7 @@ var AesGcmEncryption = class {
|
|
|
979
988
|
this.allowPlaintextFallback = options?.allowPlaintextFallback ?? false;
|
|
980
989
|
}
|
|
981
990
|
encrypt(plaintext) {
|
|
991
|
+
if (this.destroyed) throw new Error("Encryption instance has been destroyed");
|
|
982
992
|
const iv = crypto4.randomBytes(IV_LENGTH);
|
|
983
993
|
const cipher = crypto4.createCipheriv(ALGORITHM, this.keyBuffer, iv);
|
|
984
994
|
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
@@ -986,6 +996,7 @@ var AesGcmEncryption = class {
|
|
|
986
996
|
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
|
|
987
997
|
}
|
|
988
998
|
decrypt(ciphertext) {
|
|
999
|
+
if (this.destroyed) throw new Error("Encryption instance has been destroyed");
|
|
989
1000
|
const parts = ciphertext.split(":");
|
|
990
1001
|
if (parts.length !== 3 || parts[0].length !== IV_HEX_LENGTH || parts[1].length !== AUTH_TAG_HEX_LENGTH) {
|
|
991
1002
|
if (this.allowPlaintextFallback) {
|
|
@@ -1015,6 +1026,7 @@ var AesGcmEncryption = class {
|
|
|
1015
1026
|
*/
|
|
1016
1027
|
destroy() {
|
|
1017
1028
|
this.keyBuffer.fill(0);
|
|
1029
|
+
this.destroyed = true;
|
|
1018
1030
|
}
|
|
1019
1031
|
};
|
|
1020
1032
|
|
|
@@ -2442,6 +2454,38 @@ function createRelay(config) {
|
|
|
2442
2454
|
async sendSMS(options) {
|
|
2443
2455
|
return queue.enqueueAndWait(options);
|
|
2444
2456
|
},
|
|
2457
|
+
async sendSMSToOrg(options) {
|
|
2458
|
+
const { orgId, to, body, timeoutMs } = options;
|
|
2459
|
+
const allRelays = await config.db.findRelays(orgId, "revoked");
|
|
2460
|
+
const activeRelays = allRelays.filter((r) => r.status === "active" || r.status === "degraded");
|
|
2461
|
+
if (activeRelays.length === 0) {
|
|
2462
|
+
throw new Error("No active relays available for this organization");
|
|
2463
|
+
}
|
|
2464
|
+
const todayUTC = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2465
|
+
const onlineRelays = activeRelays.filter((r) => isRelayOnline(r.id));
|
|
2466
|
+
const offlineRelays = activeRelays.filter((r) => !isRelayOnline(r.id));
|
|
2467
|
+
const sortedOnline = [...onlineRelays].sort((a, b) => {
|
|
2468
|
+
const aDaily = a.dailyResetAt && a.dailyResetAt.toISOString().slice(0, 10) === todayUTC ? a.dailySmsSent : 0;
|
|
2469
|
+
const bDaily = b.dailyResetAt && b.dailyResetAt.toISOString().slice(0, 10) === todayUTC ? b.dailySmsSent : 0;
|
|
2470
|
+
return aDaily - bDaily;
|
|
2471
|
+
});
|
|
2472
|
+
const sortedOffline = [...offlineRelays].sort((a, b) => {
|
|
2473
|
+
const aTime = a.lastSeenAt ? a.lastSeenAt.getTime() : 0;
|
|
2474
|
+
const bTime = b.lastSeenAt ? b.lastSeenAt.getTime() : 0;
|
|
2475
|
+
return bTime - aTime;
|
|
2476
|
+
});
|
|
2477
|
+
const candidates = [...sortedOnline, ...sortedOffline];
|
|
2478
|
+
let lastError;
|
|
2479
|
+
for (const candidate of candidates) {
|
|
2480
|
+
try {
|
|
2481
|
+
return await queue.enqueueAndWait({ relayId: candidate.id, orgId, to, body, timeoutMs });
|
|
2482
|
+
} catch (err) {
|
|
2483
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2484
|
+
logger.warn("sendSMSToOrg: relay failed, trying next", { relayId: candidate.id, error: lastError.message });
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
throw lastError || new Error("All relays failed");
|
|
2488
|
+
},
|
|
2445
2489
|
async getMessages(relayId, orgId, limit = 50, offset = 0) {
|
|
2446
2490
|
const relay = await config.db.findRelay(relayId, orgId);
|
|
2447
2491
|
if (!relay) throw new Error("Relay not found");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as RelaySDK } from '../types-
|
|
1
|
+
import { h as RelaySDK } from '../types-sIoVYfJj.js';
|
|
2
2
|
import 'ws';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -15,6 +15,7 @@ import 'ws';
|
|
|
15
15
|
*
|
|
16
16
|
* const app = Fastify();
|
|
17
17
|
* await app.register(websocket);
|
|
18
|
+
* // Use Fastify's prefix option for route namespacing:
|
|
18
19
|
* await app.register(relayPlugin, { sdk, prefix: '/phone-relay' });
|
|
19
20
|
* ```
|
|
20
21
|
*/
|
|
@@ -32,14 +32,14 @@ function createRouteHandlers(sdk) {
|
|
|
32
32
|
},
|
|
33
33
|
async completePairing(body) {
|
|
34
34
|
const parsed = completePairingSchema.parse(body);
|
|
35
|
-
const result = await sdk.completePairing(
|
|
36
|
-
parsed.pairingId,
|
|
37
|
-
parsed.pairingToken,
|
|
38
|
-
parsed.devicePublicKey,
|
|
39
|
-
parsed.deviceName,
|
|
40
|
-
parsed.platform,
|
|
41
|
-
parsed.phoneNumber
|
|
42
|
-
);
|
|
35
|
+
const result = await sdk.completePairing({
|
|
36
|
+
pairingId: parsed.pairingId,
|
|
37
|
+
pairingToken: parsed.pairingToken,
|
|
38
|
+
devicePublicKey: parsed.devicePublicKey,
|
|
39
|
+
deviceName: parsed.deviceName,
|
|
40
|
+
platform: parsed.platform,
|
|
41
|
+
phoneNumber: parsed.phoneNumber
|
|
42
|
+
});
|
|
43
43
|
return result;
|
|
44
44
|
},
|
|
45
45
|
async listRelays(orgId) {
|
|
@@ -61,7 +61,7 @@ function createRouteHandlers(sdk) {
|
|
|
61
61
|
async testSms(id, orgId, body) {
|
|
62
62
|
const parsed = testRelaySmsSchema.parse(body);
|
|
63
63
|
const testBody = parsed.body || "ZI2 Relay test message";
|
|
64
|
-
const result = await sdk.sendSMS(id, orgId, parsed.to, testBody);
|
|
64
|
+
const result = await sdk.sendSMS({ relayId: id, orgId, to: parsed.to, body: testBody });
|
|
65
65
|
return result;
|
|
66
66
|
},
|
|
67
67
|
async getMessages(id, orgId, query) {
|
|
@@ -75,40 +75,39 @@ function createRouteHandlers(sdk) {
|
|
|
75
75
|
// src/server/fastify-plugin.ts
|
|
76
76
|
async function relayPlugin(fastify, opts) {
|
|
77
77
|
const handlers = createRouteHandlers(opts.sdk);
|
|
78
|
-
|
|
79
|
-
fastify.post(`${prefix}/pair/initiate`, async (request, reply) => {
|
|
78
|
+
fastify.post("/pair/initiate", async (request, reply) => {
|
|
80
79
|
const result = await handlers.initiatePairing(request.orgId, request.userId);
|
|
81
80
|
reply.send({ success: true, data: result });
|
|
82
81
|
});
|
|
83
|
-
fastify.post(
|
|
82
|
+
fastify.post("/pair/complete", async (request, reply) => {
|
|
84
83
|
const result = await handlers.completePairing(request.body);
|
|
85
84
|
reply.send({ success: true, data: result });
|
|
86
85
|
});
|
|
87
|
-
fastify.get(
|
|
86
|
+
fastify.get("/", async (request, reply) => {
|
|
88
87
|
const relays = await handlers.listRelays(request.orgId);
|
|
89
88
|
reply.send({ success: true, data: relays });
|
|
90
89
|
});
|
|
91
|
-
fastify.get(
|
|
90
|
+
fastify.get("/:id", async (request, reply) => {
|
|
92
91
|
const { id } = request.params;
|
|
93
92
|
const relay = await handlers.getRelay(id, request.orgId);
|
|
94
93
|
reply.send({ success: true, data: relay });
|
|
95
94
|
});
|
|
96
|
-
fastify.patch(
|
|
95
|
+
fastify.patch("/:id", async (request, reply) => {
|
|
97
96
|
const { id } = request.params;
|
|
98
97
|
await handlers.updateRelay(id, request.orgId, request.body);
|
|
99
98
|
reply.send({ success: true });
|
|
100
99
|
});
|
|
101
|
-
fastify.delete(
|
|
100
|
+
fastify.delete("/:id", async (request, reply) => {
|
|
102
101
|
const { id } = request.params;
|
|
103
102
|
await handlers.revokeRelay(id, request.orgId);
|
|
104
103
|
reply.send({ success: true });
|
|
105
104
|
});
|
|
106
|
-
fastify.post(
|
|
105
|
+
fastify.post("/:id/test", async (request, reply) => {
|
|
107
106
|
const { id } = request.params;
|
|
108
107
|
const result = await handlers.testSms(id, request.orgId, request.body);
|
|
109
108
|
reply.send({ success: true, data: result });
|
|
110
109
|
});
|
|
111
|
-
fastify.get(
|
|
110
|
+
fastify.get("/:id/messages", async (request, reply) => {
|
|
112
111
|
const { id } = request.params;
|
|
113
112
|
const result = await handlers.getMessages(id, request.orgId, request.query);
|
|
114
113
|
reply.send({ success: true, data: result });
|
|
@@ -263,6 +263,12 @@ interface RelaySDK {
|
|
|
263
263
|
body: string;
|
|
264
264
|
timeoutMs?: number;
|
|
265
265
|
}): Promise<SendResult>;
|
|
266
|
+
sendSMSToOrg(options: {
|
|
267
|
+
orgId: string;
|
|
268
|
+
to: string;
|
|
269
|
+
body: string;
|
|
270
|
+
timeoutMs?: number;
|
|
271
|
+
}): Promise<SendResult>;
|
|
266
272
|
getMessages(relayId: string, orgId: string, limit?: number, offset?: number): Promise<MessageList>;
|
|
267
273
|
isRelayOnline(relayId: string): boolean;
|
|
268
274
|
getRelaySocket(relayId: string): WebSocket | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zi2/relay-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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",
|