@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-CPJUrmcy.js';
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-CPJUrmcy.js';
2
- export { M as MessageList, i as PairingCompleteResult, j as PairingResult, k as RelayDetail, l as RelayListItem, m as SendResult } from './types-CPJUrmcy.js';
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-CPJUrmcy.js';
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
- const prefix = opts.prefix || "";
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(`${prefix}/pair/complete`, async (request, reply) => {
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(`${prefix}/`, async (request, reply) => {
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(`${prefix}/:id`, async (request, reply) => {
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(`${prefix}/:id`, async (request, reply) => {
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(`${prefix}/:id`, async (request, reply) => {
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(`${prefix}/:id/test`, async (request, reply) => {
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(`${prefix}/:id/messages`, async (request, reply) => {
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.1",
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",