npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zi2/relay-sdk

v2.0.0

Published

Enterprise SMS relay SDK with E2E encryption, provider fallback, and PCI DSS v4 compliance

Readme

@zi2/relay-sdk

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.

Key Features

  • End-to-end encryption — X25519 key exchange + HKDF-SHA256 + AES-256-GCM. The server never sees plaintext message content.
  • Provider fallback — Automatically fail over to Twilio, Vonage, or any cloud provider when a device goes offline.
  • Multi-tenant — Organization-scoped relay devices, rate limits, and audit trails.
  • 10 languages — Built-in i18n for EN, FR, ES, DE, JA, AR, ZH, PT, KO, IT.
  • PCI DSS v4 + SOC 2 compliant — Encryption at rest, TLS enforcement, brute-force protection, audit logging, auto-redacting logger.
  • Database-agnostic — Ships with Prisma and in-memory adapters; implement the DatabaseAdapter interface for any storage backend.
  • White-label Android app — Rebrand and deploy custom relay apps with a single config file.

Table of Contents

  1. Installation
  2. Quick Start
  3. Architecture
  4. Configuration
  5. Database Adapters
  6. Pairing Flow
  7. Sending SMS
  8. Provider Fallback
  9. WebSocket Protocol
  10. Fastify Plugin
  11. Error Codes
  12. Events
  13. Health Service
  14. Security & Compliance
  15. i18n
  16. White-Label Android App
  17. License

Installation

pnpm add @zi2/relay-sdk

Optional peer dependencies — install only what you need:

# For PrismaAdapter (production database)
pnpm add @prisma/client

# For Fastify plugin (REST + WebSocket server)
pnpm add fastify @fastify/websocket

# For WebSocket support (Node.js server)
pnpm add ws

Quick Start

import { createRelay, AesGcmEncryption } from '@zi2/relay-sdk';
import { PrismaAdapter } from '@zi2/relay-sdk/adapters/prisma';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

const sdk = createRelay({
  db: new PrismaAdapter(prisma),
  encryption: new AesGcmEncryption(process.env.RELAY_ENCRYPTION_KEY!),
  apiUrl: 'https://api.yourapp.com',
  wsUrl: 'wss://api.yourapp.com/ws/relay',
});

// 1. Pair a device (returns QR code data for the Android app to scan)
const pairing = await sdk.initiatePairing('org_123', 'user_456');
// → { pairingId, serverPublicKey, pairingToken, wsUrl }

// 2. Send an SMS through the paired relay device
const result = await sdk.sendSMS({
  relayId: 'relay_abc',
  orgId: 'org_123',
  to: '+1234567890',
  body: 'Your verification code is 847291',
});
// → { messageId: 'msg_xyz', status: 'delivered' }

// 3. Check device status
const online = sdk.isRelayOnline('relay_abc'); // true

Architecture

The SDK is organized into six layers:

┌─────────────────────────────────────────────────────────┐
│                      Your Application                    │
├─────────────────────────────────────────────────────────┤
│  Server Layer     │  Routes, Fastify plugin, WS handler  │
├─────────────────────────────────────────────────────────┤
│  Core Layer       │  Crypto, Queue, Pairing, Health      │
├─────────────────────────────────────────────────────────┤
│  Adapter Layer    │  DB, Encryption, Broadcast, Audit    │
├─────────────────────────────────────────────────────────┤
│  Provider Layer   │  PhoneRelay, Fallback                │
├─────────────────────────────────────────────────────────┤
│  i18n Layer       │  10 locales, error translations      │
└─────────────────────────────────────────────────────────┘

Message flow:

Your Server                    Android Device               Carrier
    │                               │                          │
    │  sdk.sendSMS(...)             │                          │
    │──encrypt payload──►           │                          │
    │  queue message                │                          │
    │                               │                          │
    │  ◄── WebSocket (wss://) ──►   │                          │
    │  send_sms (encrypted)  ──►    │                          │
    │                               │──native SMS──►           │
    │                               │                          │──► Recipient
    │                               │  ◄── delivery receipt ───│
    │  ◄── sms_ack ────────────     │                          │
    │  ◄── delivery_receipt ───     │                          │
    │                               │                          │
    │  broadcast to web UI          │                          │
    └───────────────────────────────┘──────────────────────────┘

Module Exports

| Import Path | Contents | |---|---| | @zi2/relay-sdk | createRelay(), types, constants, errors, crypto, adapters | | @zi2/relay-sdk/client | Android/client-side: WebSocket client, E2E crypto, pairing, SMS sender, secure storage | | @zi2/relay-sdk/adapters/prisma | PrismaAdapter | | @zi2/relay-sdk/fastify | relayPlugin for Fastify |


Configuration

interface RelaySDKConfig {
  /** Required — Database adapter (PrismaAdapter, MemoryAdapter, or custom) */
  db: DatabaseAdapter;

  /** Required — Encryption adapter for data at rest (AesGcmEncryption or custom) */
  encryption: EncryptionAdapter;

  /** Optional — Broadcast adapter for real-time UI updates (e.g., SSE or WebSocket push) */
  broadcast?: BroadcastAdapter;

  /** Optional — Audit trail adapter for PCI DSS v4 Req 10 / SOC 2 CC7.2 compliance */
  audit?: AuditAdapter;

  /** Optional — Logger adapter with auto-redaction of sensitive fields */
  logger?: LoggerAdapter;

  /** Base URL for the API server (used in QR code pairing data). Default: 'http://localhost:3000' */
  apiUrl?: string;

  /** WebSocket URL for device connections (included in QR code). Default: empty string (falls back to apiUrl-derived WSS URL in pairing service) */
  wsUrl?: string;

  /** Enforce TLS-only WebSocket connections (PCI DSS v4 Req 4.2.1). Default: true */
  enforceTls?: boolean;

  /** Maximum auth failures per IP before lockout (PCI DSS v4 Req 8.3.4). Default: 10 */
  maxAuthFailuresPerIp?: number;

  /** Overridable rate limits and timeouts (use SCREAMING_CASE keys) */
  limits?: {
    AUTH_TIMEOUT_MS?: number;           // Default: 5000
    HEARTBEAT_INTERVAL_MS?: number;     // Default: 30000
    PAIRING_EXPIRY_MINUTES?: number;    // Default: 5
    MESSAGE_EXPIRY_HOURS?: number;      // Default: 24
    QUEUE_DRAIN_DELAY_MS?: number;      // Default: 3000
    DEFAULT_SMS_TIMEOUT_MS?: number;    // Default: 30000
    MAX_SMS_PER_MINUTE?: number;        // Default: 20
    MAX_SMS_PER_HOUR?: number;          // Default: 200
    MAX_SMS_PER_DAY?: number;           // Default: 1000
    DEGRADED_THRESHOLD_MS?: number;     // Default: 300000 (5 minutes)
  };
}

Encryption Key Generation

Generate a 256-bit encryption key for AesGcmEncryption:

openssl rand -hex 32
# → e.g. a1b2c3d4e5f6...  (64 hex characters)

Store it securely in your environment:

RELAY_ENCRYPTION_KEY=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

Database Adapters

PrismaAdapter (Production)

import { PrismaAdapter } from '@zi2/relay-sdk/adapters/prisma';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();
const db = new PrismaAdapter(prisma);

Required Prisma schema models:

model PhoneRelay {
  id                    String    @id @default(uuid())
  organizationId        String
  deviceName            String
  platform              String
  relayIdentifier       String    @unique
  devicePublicKey       String
  sharedKeyEncrypted    String
  authTokenHash         String
  status                String    @default("active")
  keyVersion            Int       @default(1)
  lastKeyRotation       DateTime  @default(now())
  lastSeenAt            DateTime?
  lastIpAddress         String?
  batteryLevel          Int?
  signalStrength        Int?
  totalSmsSent          Int       @default(0)
  totalSmsFailed        Int       @default(0)
  dailySmsSent          Int       @default(0)
  dailyResetAt          DateTime  @default(now())
  maxSmsPerMinute       Int       @default(20)
  maxSmsPerHour         Int       @default(200)
  maxSmsPerDay          Int       @default(1000)
  pairedById            String
  createdAt             DateTime  @default(now())
  updatedAt             DateTime  @updatedAt

  organization          Organization @relation(fields: [organizationId], references: [id])
  messages              RelayMessageQueue[]
}

model PhoneRelayPairing {
  id                        String    @id @default(uuid())
  organizationId            String
  pairingToken              String
  serverPublicKey           String
  serverPrivateKeyEncrypted String
  status                    String    @default("pending")
  initiatedById             String
  expiresAt                 DateTime
  createdAt                 DateTime  @default(now())
}

model RelayMessageQueue {
  id                String    @id @default(uuid())
  relayId           String
  organizationId    String
  encryptedPayload  String
  status            String    @default("pending")
  attempts          Int       @default(0)
  maxAttempts       Int       @default(3)
  nativeMessageId   String?
  errorMessage      String?
  deliveryStatus    String?
  sentToDeviceAt    DateTime?
  ackedAt           DateTime?
  expiresAt         DateTime
  createdAt         DateTime  @default(now())
  updatedAt         DateTime  @updatedAt

  relay             PhoneRelay @relation(fields: [relayId], references: [id])
}

MemoryAdapter (Testing)

import { MemoryAdapter } from '@zi2/relay-sdk';

const db = new MemoryAdapter();
// Use db.clear() to reset between tests

Custom Adapter

Implement the DatabaseAdapter interface to use any storage backend:

import type { DatabaseAdapter } from '@zi2/relay-sdk';

class MyCustomAdapter implements DatabaseAdapter {
  // PhoneRelay CRUD
  findRelay(id: string, orgId?: string): Promise<PhoneRelayRecord | null>;
  findRelayByTokenHash(id: string, tokenHash: string, statuses: string[]): Promise<PhoneRelayRecord | null>;
  findRelays(orgId: string, excludeStatus?: string): Promise<PhoneRelayRecord[]>;
  createRelay(data: CreateRelayInput): Promise<PhoneRelayRecord>;
  updateRelay(id: string, data: Partial<PhoneRelayRecord>): Promise<void>;
  updateRelayByOrg(id: string, orgId: string, data: Partial<PhoneRelayRecord>): Promise<void>;
  deleteRelay(id: string): Promise<void>;
  incrementRelayStat(id: string, field: 'totalSmsSent' | 'totalSmsFailed' | 'dailySmsSent', amount?: number): Promise<void>;
  countRelays(orgId: string): Promise<number>;
  markDegradedRelays(threshold: Date): Promise<number>;
  resetDailyCounters(): Promise<number>;

  // PhoneRelayPairing
  findPairing(id: string): Promise<PhoneRelayPairingRecord | null>;
  createPairing(data: CreatePairingInput): Promise<PhoneRelayPairingRecord>;
  updatePairingStatus(id: string, status: string): Promise<void>;
  deleteExpiredPairings(): Promise<number>;

  // RelayMessageQueue
  findMessage(id: string): Promise<RelayMessageRecord | null>;
  createMessage(data: CreateMessageInput): Promise<RelayMessageRecord>;
  updateMessage(id: string, data: Partial<RelayMessageRecord>): Promise<void>;
  findPendingMessages(relayId: string, limit: number): Promise<RelayMessageRecord[]>;
  findMessages(relayId: string, limit: number, offset: number): Promise<{ messages: RelayMessageRecord[]; total: number }>;
  deleteMessagesByRelay(relayId: string): Promise<number>;
  expireStaleMessages(): Promise<number>;

  // Batch operations
  createMessages(batch: CreateMessageInput[]): Promise<RelayMessageRecord[]>;
  updateMessages(ids: string[], data: Partial<RelayMessageRecord>): Promise<number>;
}

Pairing Flow

Pairing establishes a secure E2E-encrypted channel between your server and an Android device.

Step-by-step

  1. Server initiates pairing — generates an X25519 key pair and a one-time pairing token.
const pairing = await sdk.initiatePairing('org_123', 'user_456');
// Returns: { pairingId, serverPublicKey, pairingToken, wsUrl }
  1. Display QR code — encode the pairing data as a QR code in your web UI. The Android app scans it.

  2. Device completes pairing — the app sends its public key and device info to the server.

const result = await sdk.completePairing({
  pairingId: pairing.pairingId,
  pairingToken: pairing.pairingToken,
  devicePublicKey: '<device X25519 public key>',
  deviceName: 'Samsung Galaxy S24',
  platform: 'android',
});
// Returns: { relayId, authToken }
  1. 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.

  2. Device connects via WebSocket — authenticates with the hashed auth token and begins relaying SMS.

Pairing expiry

Pairing sessions expire after 5 minutes (configurable via limits.PAIRING_EXPIRY_MINUTES). Expired sessions are cleaned up automatically by the health service.


Sending SMS

const result = await sdk.sendSMS({
  relayId: 'relay_abc',
  orgId: 'org_123',
  to: '+1234567890',
  body: 'Your verification code is 847291',
  timeoutMs: 30000, // optional, default 30s
});

console.log(result.messageId); // 'msg_xyz'
console.log(result.status);    // 'delivered' | 'queued' | 'sent_to_device' | 'failed'

Note: When a relay is offline, sendSMS does NOT throw RELAY_OFFLINE. Instead, it queues the message and returns status: "queued". The message is sent automatically when the device reconnects.

Message Statuses

| Status | Description | |---|---| | pending | Message queued, waiting to be sent to device | | sent_to_device | Encrypted payload delivered to device via WebSocket | | delivered | Device confirmed SMS was sent by the carrier | | failed | Delivery failed (device error, carrier rejection, etc.) | | expired | Message exceeded its TTL (default: 24 hours) |

Message History

const messages = await sdk.getMessages('relay_abc', 'org_123', 50, 0);
// → { messages: [...], pagination: { total: 142, limit: 50, offset: 0 } }

Rate Limits

Each relay device enforces three tiers of rate limiting (configurable per-device):

| Tier | Default | Max | |---|---|---| | Per minute | 20 | 60 | | Per hour | 200 | 1,000 | | Per day | 1,000 | 10,000 |


Provider Fallback

Use createFallbackProvider() to automatically fall back to cloud SMS providers when a relay device goes offline:

import { createRelay } from '@zi2/relay-sdk';

const sdk = createRelay({ /* ... */ });

const provider = sdk.createFallbackProvider({
  primary: sdk.createProvider('relay_abc', 'org_123'),
  fallbacks: [twilioProvider, vonageProvider],
  isOnline: () => sdk.isRelayOnline('relay_abc'),
  onFallback: (providerName) => {
    console.log(`Relay offline — fell back to ${providerName}`);
  },
});

// Use as a unified SMS provider
const result = await provider.sendSms('+1234567890', 'Hello from ZI2');

Implementing a Fallback Provider

Any cloud provider can serve as a fallback by implementing SmsProviderAdapter:

import type { SmsProviderAdapter, SmsSendResponse } from '@zi2/relay-sdk';

class TwilioProvider implements SmsProviderAdapter {
  readonly name = 'twilio';

  initialize(credentials: Record<string, string>): void {
    // Set up Twilio client with credentials.accountSid, credentials.authToken
  }

  async sendSms(to: string, body: string): Promise<SmsSendResponse> {
    // Call Twilio API
    return { success: true, messageId: 'SM...' };
  }

  async validateCredentials(credentials: Record<string, string>): Promise<boolean> {
    // Verify Twilio credentials are valid
    return true;
  }
}

WebSocket Protocol

Relay devices communicate with the server over a persistent WebSocket connection (wss://). All SMS payloads are E2E encrypted.

Message Types

| Type | Direction | Description | |---|---|---| | auth | Device -> Server | Authenticate with relayId and token | | auth_ok | Server -> Device | Authentication successful | | send_sms | Server -> Device | Encrypted SMS payload to deliver | | sms_ack | Device -> Server | SMS send result (messageId, status, nativeMessageId, errorMessage) | | delivery_receipt | Device -> Server | Carrier delivery confirmation (messageId, deliveryStatus) | | status | Device -> Server | Device telemetry (batteryLevel, signalStrength) | | rekey | Server -> Device | Initiate key rotation (new server public key) | | rekey_ack | Device -> Server | Device's new public key for key rotation |

Connection Lifecycle

  1. Device opens WebSocket to wss://your-server/ws/relay
  2. Server starts a 5-second auth timeout
  3. Device sends { type: "auth", relayId: "...", token: "..." }
  4. Server verifies token hash against database (timing-safe comparison)
  5. Server responds { type: "auth_ok" } and begins heartbeat pings every 30s
  6. Server drains any pending messages from the queue
  7. On disconnect, server broadcasts offline status to web clients

Rate Limiting

Each WebSocket connection is rate-limited to 100 messages per 10-second window. Exceeding this closes the connection with code 4008.

Close Codes

| Code | Meaning | |---|---| | 4001 | Authentication timeout (5s) | | 4002 | Message sent before authentication | | 4003 | Missing relayId or token in auth message | | 4004 | Invalid credentials | | 4005 | Replaced by new connection from same device | | 4008 | Rate limit exceeded | | 1000 | Pong timeout — device failed to respond to heartbeat ping |


Fastify Plugin

Register the Fastify plugin to expose all relay REST endpoints and the WebSocket upgrade handler:

import Fastify from 'fastify';
import websocket from '@fastify/websocket';
import { relayPlugin } from '@zi2/relay-sdk/fastify';
import { createRelay, AesGcmEncryption } from '@zi2/relay-sdk';
import { PrismaAdapter } from '@zi2/relay-sdk/adapters/prisma';

const app = Fastify();
const sdk = createRelay({
  db: new PrismaAdapter(prisma),
  encryption: new AesGcmEncryption(process.env.RELAY_ENCRYPTION_KEY!),
});

await app.register(websocket);

// Register with Fastify's prefix option
await app.register(async (instance) => {
  await relayPlugin(instance, { sdk });
}, { prefix: '/phone-relay' });

await app.listen({ port: 3000 });

Registered Routes

| Method | Path | Description | |---|---|---| | POST | /phone-relay/pair/initiate | Start a new pairing session | | POST | /phone-relay/pair/complete | Complete device pairing | | GET | /phone-relay/ | List all relay devices for the org | | GET | /phone-relay/:id | Get relay device details | | PATCH | /phone-relay/:id | Update relay settings (name, rate limits) | | DELETE | /phone-relay/:id | Revoke a relay device | | POST | /phone-relay/:id/test | Send a test SMS | | GET | /phone-relay/:id/messages | List messages (paginated) | | GET | /ws/relay | WebSocket upgrade for relay devices |


Error Codes

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).

| Code | HTTP | Message | i18n Key | |---|---|---|---| | RELAY_NOT_FOUND | 404 | Relay device not found | errors.relayNotFound | | RELAY_INACTIVE | 409 | Relay is not active | errors.relayInactive | | RELAY_OFFLINE | 503 | Relay device is offline | errors.relayOffline | | PAIRING_NOT_FOUND | 404 | Pairing session not found | errors.pairingNotFound | | PAIRING_EXPIRED | 410 | Pairing session has expired | errors.pairingExpired | | PAIRING_INVALID_TOKEN | 403 | Invalid pairing token | errors.pairingInvalidToken | | AUTH_TIMEOUT | 408 | WebSocket authentication timeout | errors.authTimeout | | AUTH_FAILED | 401 | Invalid relay credentials | errors.authFailed | | RATE_LIMITED | 429 | Rate limit exceeded | errors.rateLimited | | ENCRYPTION_FAILED | 500 | Encryption operation failed | errors.encryptionFailed | | REKEY_FAILED | 500 | Key rotation failed | errors.rekeyFailed | | SMS_SEND_FAILED | 502 | SMS delivery failed | errors.smsSendFailed | | SMS_TIMEOUT | 504 | SMS acknowledgement timeout | errors.smsTimeout |

Extended Error Codes (RELAY_ERRORS_EXTENDED) — These are exported separately from RELAY_ERRORS:

| Code | HTTP | Message | i18n Key | |---|---|---|---| | AUTH_LOCKED | 423 | Too many failed attempts. Try again later. | errors.authLocked | | TLS_REQUIRED | 426 | TLS/SSL connection required | errors.tlsRequired | | ORG_MISMATCH | 403 | Organization access denied | errors.orgMismatch |

Error Handling

import { RelayError, RELAY_ERRORS } from '@zi2/relay-sdk';

try {
  await sdk.sendSMS({ relayId, orgId, to, body });
} catch (err) {
  if (err instanceof RelayError) {
    console.log(err.code);       // 'RELAY_OFFLINE'
    console.log(err.statusCode); // 503
    console.log(err.i18nKey);    // 'errors.relayOffline'
    console.log(err.toJSON());   // Safe for client response (no stack trace)
  }
}

Events

Subscribe to real-time relay events for monitoring, alerting, or UI updates. Each subscription returns an unsubscribe function.

// Device came online
const unsub1 = sdk.onRelayOnline((relayId, orgId) => {
  console.log(`Relay ${relayId} is online`);
});

// Device went offline
const unsub2 = sdk.onRelayOffline((relayId, orgId) => {
  console.log(`Relay ${relayId} went offline`);
  // Trigger fallback provider switch, send alert, etc.
});

// SMS delivered successfully
const unsub3 = sdk.onMessageDelivered((messageId, relayId) => {
  console.log(`Message ${messageId} delivered via ${relayId}`);
});

// SMS delivery failed
const unsub4 = sdk.onMessageFailed((messageId, relayId, error) => {
  console.error(`Message ${messageId} failed on ${relayId}: ${error}`);
});

// Clean up when shutting down
unsub1();
unsub2();
unsub3();
unsub4();

Health Service

The health service performs periodic maintenance tasks required for compliance and reliability.

// Start automatic health checks (runs every 60 seconds)
sdk.startHealthService();

// Or run a manual check
const report = await sdk.runHealthCheck();
console.log(report);
// {
//   expiredMessages: 3,    — stale messages marked as expired
//   degradedRelays: 1,     — relays with no heartbeat for 5+ minutes
//   cleanedPairings: 0     — expired pairing sessions removed
// }

// Stop the service when shutting down
sdk.stopHealthService();

What the Health Service Does

| Task | Description | Compliance | |---|---|---| | Expire stale messages | Marks messages past their TTL as expired | Data retention | | Mark degraded relays | Flags devices not seen for 5+ minutes as degraded | Availability monitoring | | Clean expired pairings | Removes pending pairing sessions past their expiry | PCI DSS v4 Req 8 | | Reset daily counters | Resets per-device daily SMS counters at midnight | Rate limit enforcement |


Security & Compliance

PCI DSS v4

| Requirement | Implementation | |---|---| | Req 3.4.1 — Encryption at rest | AesGcmEncryption encrypts all stored keys and message payloads. No plaintext fallback in strict mode. | | 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. | | Req 3.7.1 — Key strength | Enforces 256-bit (32-byte) encryption keys at construction time. | | Req 4.2.1 — TLS enforcement | enforceTls: true (default) rejects non-TLS WebSocket connections. | | Req 6.2.4 — Error handling | RelayError.toJSON() returns only code + message. No stack traces, database details, or crypto internals leak to clients. | | Req 8.3.4 — Brute-force protection | AuthLimiter locks out IPs after configurable failed auth attempts (default: 10). | | Req 10 — Audit trail | AuditAdapter interface logs all security-relevant actions: pairing, auth, SMS, revocation, key rotation. | | Req 11.3 — Security monitoring | Health service runs periodic checks on system integrity. |

SOC 2

| Control | Implementation | |---|---| | CC6.1 — Logical access | Organization-scoped queries. Auto-redacting logger strips tokens, keys, SMS body, and credentials from all log output. | | CC7.2 — Audit trail | AuditAdapter records timestamped entries with action, org, relay, user, IP, and metadata. |

E2E Encryption Details

Key Exchange:    X25519 (Curve25519 Diffie-Hellman)
Key Derivation:  HKDF-SHA256 with info string "zi2-relay-e2e-v1"
Payload Cipher:  AES-256-GCM (12-byte IV, 16-byte auth tag)
Token Security:  SHA-256 hashing, timing-safe comparison
Key Rotation:    Server-initiated rekey via WebSocket protocol

Auto-Redacting Logger

The built-in ConsoleLogger (and the withRedaction() wrapper for custom loggers) automatically redacts these fields from all log output:

token, authToken, pairingToken, authTokenHash, sharedKey, sharedKeyEncrypted, privateKey, serverPrivateKey, serverPrivateKeyEncrypted, devicePublicKey, serverPublicKey, encryptedPayload, password, secret, credentials, authorization, cookie, body

import { withRedaction } from '@zi2/relay-sdk';

const safeLogger = withRedaction(myPinoLogger);
const sdk = createRelay({ logger: safeLogger, /* ... */ });

i18n

The SDK ships with translations for 10 languages covering all UI strings and error messages.

import { getTranslation, getSupportedLocales, getRelayTranslations } from '@zi2/relay-sdk';

// Get a single translation
getTranslation('fr', 'relay.connected');
// → "Connect\u00e9"

getTranslation('ja', 'relay.e2eEncrypted');
// → "E2E\u6697\u53f7\u5316"

// List all supported locales
getSupportedLocales();
// → ['en', 'fr', 'es', 'de', 'ja', 'ar', 'zh', 'pt', 'ko', 'it']

// Get all translations for a locale (useful for client-side hydration)
const strings = getRelayTranslations('es');

Supported Locales

| Code | Language | |---|---| | en | English | | fr | French | | es | Spanish | | de | German | | ja | Japanese | | ar | Arabic | | zh | Chinese (Simplified) | | pt | Portuguese | | ko | Korean | | it | Italian |


White-Label Android App

The relay Android app can be white-labeled for custom deployments. A configuration file and build script handle rebranding.

Configuration

Create a relay.config.json in your project:

{
  "appId": "com.yourcompany.smsrelay",
  "appName": "YourBrand SMS Relay",
  "serverUrl": "wss://api.yourcompany.com/ws/relay",
  "brandColor": "#FF6600",
  "notificationTitle": "YourBrand SMS Relay",
  "notificationText": "Relay is active — ready to send SMS"
}

| Field | Description | |---|---| | appId | Android application ID (reverse domain). Must be unique on Google Play. | | appName | Display name shown on the device home screen and in settings. | | serverUrl | WebSocket URL the app connects to. Must use wss:// in production. | | brandColor | Primary brand color (hex). Applied to the app theme. | | notificationTitle | Title of the persistent foreground service notification. | | notificationText | Body text of the foreground service notification. |

Building

# Default config (./relay.config.json)
./build-whitelabel.sh

# Custom config path
./build-whitelabel.sh /path/to/client-config.json

The script:

  1. Reads the config JSON
  2. Generates capacitor.config.ts with the custom appId and appName
  3. Updates android/app/src/main/res/values/strings.xml with notification strings
  4. Runs pnpm build (Vite production build)
  5. Syncs the web assets to the Android project via npx cap sync android
  6. Builds a release APK via Gradle
  7. Outputs the final APK path

Requirements

  • Node.js 18+
  • pnpm
  • Android SDK (API level 24+)
  • Java 17+ (for Gradle)
  • jq (for JSON parsing in the build script)

Relay Management

List Devices

const relays = await sdk.listRelays('org_123');
// Returns: RelayListItem[] with isOnline status, stats, rate limits

Update Settings

await sdk.updateRelay('relay_abc', 'org_123', {
  deviceName: 'Office Phone',
  maxSmsPerDay: 500,
});

Revoke a Device

await sdk.revokeRelay('relay_abc', 'org_123');
// Device is immediately disconnected and cannot reconnect

Rotate Encryption Keys

await sdk.rotateKeys('relay_abc', 'org_123');
// Triggers rekey protocol over WebSocket — both sides derive new shared key

License

Proprietary. Copyright Zenith Intelligence Technologies / ZI2 Systems. All rights reserved.

This software is licensed exclusively for use within authorized ZI2 deployments. Unauthorized copying, modification, distribution, or use of this software is strictly prohibited. Contact [email protected] for licensing inquiries.