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

@narcisbodea/smstunnel-sdk

v1.2.1

Published

SMSTunnel SDK - Node.js + NestJS + React + Vue + Angular + framework-agnostic client for SMS pairing & E2E encryption

Readme

@narcisbodea/smstunnel-sdk

SMSTunnel SDK - Node.js + NestJS + React + Vue + Angular + framework-agnostic client for SMS pairing & E2E encryption.

| Import path | Scope | |---|---| | @narcisbodea/smstunnel-sdk | SmsTunnelClient class + types + labels (Svelte, Astro, Solid, Vanilla JS) | | @narcisbodea/smstunnel-sdk/server | NestJS module (backend proxy) | | @narcisbodea/smstunnel-sdk/react | React components + hook (Next.js, Remix, Gatsby) | | @narcisbodea/smstunnel-sdk/vue | Vue 3 components + composable (Nuxt) | | @narcisbodea/smstunnel-sdk/angular | Angular 17+ injectable service | | @narcisbodea/smstunnel-sdk/node | Standalone Node.js client (no framework, API key auth) |

Install

npm install @narcisbodea/smstunnel-sdk

Node.js (Standalone)

Works without NestJS or any framework. Uses native fetch (Node 18+), zero external dependencies.

import { SmsTunnelNodeClient } from '@narcisbodea/smstunnel-sdk/node';

const client = new SmsTunnelNodeClient({
  apiKey: 'sk_live_xxx',
  serverUrl: 'https://smstunnel.io',
});

// Send SMS
const result = await client.sendSms('+40721123456', 'Hello!');

// Send 2FA
await client.send2FaSms('+40721123456', 'Your code is 123456');

// Send bulk SMS
await client.sendBulkSms(['+40721111111', '+40722222222'], 'Bulk message');

// Get message status
const status = await client.getMessageStatus('msg-id');

// Get received SMS (inbox)
const inbox = await client.getReceivedSms({ limit: 20, page: 1 });

// List devices
const devices = await client.getDevices();

// E2E Encryption
const e2eStatus = await client.getDeviceE2EStatus('device-id');
const publicKey = await client.getDevicePublicKey('device-id');
const verification = await client.verifyDeviceKey('device-id', 'stored-fingerprint');
await client.sendEncryptedSms('encrypted-payload', 'device-id', {
  is2FA: false,
  expectedFingerprint: 'a1b2c3d4',
});

// Unified Pairing v2
const pairing = await client.createPairingToken({
  source: 'api',
  displayName: 'My App',
});
const pairingStatus = await client.getPairingTokenStatus(pairing.data.token);

// E2E Pairing (with public key exchange)
const e2ePairing = await client.createE2EPairing({ siteName: 'My App' });
const e2eResult = await client.pollE2EPairing(e2ePairing.data.token);
// e2eResult.data.publicKey contains the device's RSA public key

Server (NestJS)

1. Implement a Storage Adapter

The SDK is database-agnostic. You provide a simple adapter with 3 methods:

MongoDB (Mongoose)

import { SmsTunnelStorageAdapter, SmsTunnelConfig } from '@narcisbodea/smstunnel-sdk/server';
import { Model } from 'mongoose';

export class MongoSmsTunnelStorage implements SmsTunnelStorageAdapter {
  constructor(private model: Model<any>) {}

  async getConfig(): Promise<Partial<SmsTunnelConfig>> {
    const doc = await this.model.findOne().lean().exec();
    return {
      serverUrl: doc?.smstunnelServerUrl || '',
      apiKey: doc?.smstunnelApiKey || '',
      siteToken: doc?.smstunnelSiteToken || '',
      deviceName: doc?.smstunnelDeviceName || '',
    };
  }

  async updateConfig(partial: Partial<SmsTunnelConfig>): Promise<void> {
    const $set: Record<string, string> = {};
    if (partial.serverUrl !== undefined) $set.smstunnelServerUrl = partial.serverUrl;
    if (partial.apiKey !== undefined) $set.smstunnelApiKey = partial.apiKey;
    if (partial.siteToken !== undefined) $set.smstunnelSiteToken = partial.siteToken;
    if (partial.deviceName !== undefined) $set.smstunnelDeviceName = partial.deviceName;
    await this.model.updateOne({}, { $set });
  }

  async clearConfig(): Promise<void> {
    await this.model.updateOne({}, {
      $set: { smstunnelApiKey: '', smstunnelDeviceName: '', smstunnelSiteToken: '' },
    });
  }
}

Prisma (PostgreSQL)

import { SmsTunnelStorageAdapter, SmsTunnelConfig } from '@narcisbodea/smstunnel-sdk/server';
import { PrismaClient } from '@prisma/client';

export class PrismaSmsTunnelStorage implements SmsTunnelStorageAdapter {
  constructor(private prisma: PrismaClient) {}

  async getConfig(): Promise<Partial<SmsTunnelConfig>> {
    const row = await this.prisma.appSettings.findFirst();
    return {
      serverUrl: row?.smstunnelServerUrl || '',
      apiKey: row?.smstunnelApiKey || '',
      siteToken: row?.smstunnelSiteToken || '',
      deviceName: row?.smstunnelDeviceName || '',
    };
  }

  async updateConfig(partial: Partial<SmsTunnelConfig>): Promise<void> {
    await this.prisma.appSettings.updateMany({ data: partial });
  }

  async clearConfig(): Promise<void> {
    await this.prisma.appSettings.updateMany({
      data: { apiKey: '', deviceName: '', siteToken: '' },
    });
  }
}

JSON File

import { SmsTunnelStorageAdapter, SmsTunnelConfig } from '@narcisbodea/smstunnel-sdk/server';
import { readFile, writeFile } from 'fs/promises';

export class JsonFileStorage implements SmsTunnelStorageAdapter {
  constructor(private filePath: string) {}

  async getConfig(): Promise<Partial<SmsTunnelConfig>> {
    try { return JSON.parse(await readFile(this.filePath, 'utf-8')); }
    catch { return {}; }
  }

  async updateConfig(partial: Partial<SmsTunnelConfig>): Promise<void> {
    const config = { ...(await this.getConfig()), ...partial };
    await writeFile(this.filePath, JSON.stringify(config, null, 2));
  }

  async clearConfig(): Promise<void> {
    await this.updateConfig({ apiKey: '', deviceName: '', siteToken: '' });
  }
}

2. Register the Module

import { SmsTunnelModule } from '@narcisbodea/smstunnel-sdk/server';

@Module({
  imports: [
    SmsTunnelModule.forRoot({
      storage: new MongoSmsTunnelStorage(settingsModel),
      callbackBaseUrl: 'https://myapp.com/api',
      displayName: 'My App',
    }),
  ],
})
export class AppModule {}

Or async:

SmsTunnelModule.forRootAsync({
  useFactory: (settingsModel) => ({
    storage: new MongoSmsTunnelStorage(settingsModel),
    callbackBaseUrl: process.env.CALLBACK_URL,
    displayName: 'My App',
  }),
  inject: [getModelToken('Settings')],
})

Multi-Tenant (Enterprise/SaaS) Mode

For multi-tenant applications (SaaS platforms, CRMs, ERPs) where each tenant needs their own SMS device:

SmsTunnelModule.forRootAsync({
  useFactory: (settingsModel, tenantService) => ({
    storage: new MongoSmsTunnelStorage(settingsModel),
    callbackBaseUrl: process.env.CALLBACK_URL,
    displayName: 'My SaaS App',
    enterpriseApiKey: process.env.SMSTUNNEL_ENTERPRISE_API_KEY,
    externalId: tenantService.getCurrentTenantId(),
  }),
  inject: [getModelToken('Settings'), TenantService],
})

3. Auth Guard Integration

import { SMSTUNNEL_PUBLIC_PATHS } from '@narcisbodea/smstunnel-sdk/server';

// In your JwtAuthGuard:
if (SMSTUNNEL_PUBLIC_PATHS.some(p => request.url.includes(p))) return true;

REST Endpoints

| Route | Method | Auth | Purpose | |-------|--------|------|---------| | GET /smstunnel/status | getStatus | Yes | Show pairing status | | POST /smstunnel/create-token | createToken | Yes | Generate QR | | GET /smstunnel/pairing-status/:token | pairingStatus | No | Polling proxy | | POST /smstunnel/callback | callback | No | Receive API key | | POST /smstunnel/unpair | unpair | Yes | Disconnect device | | POST /smstunnel/send | sendSms | Yes | Send SMS | | POST /smstunnel/update-config | updateConfig | Yes | Set server URL | | GET /smstunnel/e2e-status/:deviceId | getDeviceE2EStatus | Yes | E2E encryption status | | GET /smstunnel/public-key/:deviceId | getDevicePublicKey | Yes | Get device public key | | POST /smstunnel/verify-key | verifyDeviceKey | Yes | Verify key fingerprint | | POST /smstunnel/send-encrypted | sendEncryptedSms | Yes | Send encrypted SMS | | GET /smstunnel/pairings | getPairings | Yes | List active pairings |


React

Works with: React, Next.js, Remix, Gatsby

Plug and Play Component

import { SmsTunnelPairing, RO_LABELS } from '@narcisbodea/smstunnel-sdk/react';

function SettingsPage() {
  return (
    <SmsTunnelPairing
      apiBaseUrl="/api"
      getAuthHeaders={() => ({
        Authorization: `Bearer ${localStorage.getItem('token')}`,
      })}
      labels={RO_LABELS}
      onPaired={(device) => console.log('Paired:', device)}
      showTestSms={true}
      showServerUrlInput={true}
      qrSize={220}
    />
  );
}

Next.js

Add 'use client' in your page/component:

'use client';
import { SmsTunnelPairing, RO_LABELS } from '@narcisbodea/smstunnel-sdk/react';

export default function SmsSettings() {
  return <SmsTunnelPairing apiBaseUrl="/api" labels={RO_LABELS} />;
}

Custom UI with Hook

import { useSmsTunnel, QrCodeCanvas } from '@narcisbodea/smstunnel-sdk/react';

function MySmsSettings() {
  const tunnel = useSmsTunnel({
    apiBaseUrl: '/api',
    getAuthHeaders: () => ({ Authorization: `Bearer ${token}` }),
  });

  // E2E Encryption
  const e2eStatus = await tunnel.getDeviceE2EStatus('device-id');
  const publicKey = await tunnel.getDevicePublicKey('device-id');
  const keyValid = await tunnel.verifyDeviceKey('device-id', 'fingerprint');
  await tunnel.sendEncryptedSms('payload', 'device-id');

  if (tunnel.status === 'paired') {
    return <div>Connected: {tunnel.deviceName}</div>;
  }

  return (
    <div>
      {tunnel.showQr ? (
        <>
          <QrCodeCanvas value={tunnel.qrData} size={200} />
          <button onClick={tunnel.cancelPairing}>Cancel</button>
        </>
      ) : (
        <button onClick={tunnel.generateQr} disabled={tunnel.generating}>
          Connect Phone
        </button>
      )}
    </div>
  );
}

Vue

Works with: Vue 3, Nuxt 3

Plug and Play Component

<script setup>
import { SmsTunnelPairing, RO_LABELS } from '@narcisbodea/smstunnel-sdk/vue';

function onPaired(device) {
  console.log('Paired:', device);
}
</script>

<template>
  <SmsTunnelPairing
    api-base-url="/api"
    :labels="RO_LABELS"
    :show-test-sms="true"
    :show-server-url-input="true"
    @paired="onPaired"
  />
</template>

Nuxt 3

Wrap in <ClientOnly> since pairing uses browser APIs:

<template>
  <ClientOnly>
    <SmsTunnelPairing api-base-url="/api" :labels="RO_LABELS" />
  </ClientOnly>
</template>

<script setup>
import { SmsTunnelPairing, RO_LABELS } from '@narcisbodea/smstunnel-sdk/vue';
</script>

Custom UI with Composable

<script setup>
import { useSmsTunnel, QrCodeCanvas } from '@narcisbodea/smstunnel-sdk/vue';

const tunnel = useSmsTunnel({
  apiBaseUrl: '/api',
  getAuthHeaders: () => ({ Authorization: `Bearer ${token}` }),
});

// E2E Encryption
const e2eStatus = await tunnel.getDeviceE2EStatus('device-id');
const publicKey = await tunnel.getDevicePublicKey('device-id');
await tunnel.sendEncryptedSms('payload', 'device-id');
</script>

<template>
  <div v-if="tunnel.status.value === 'paired'">
    Connected: {{ tunnel.deviceName.value }}
    <button @click="tunnel.unpair()">Disconnect</button>
  </div>
  <div v-else>
    <div v-if="tunnel.showQr.value">
      <QrCodeCanvas :value="tunnel.qrData.value" :size="200" />
      <button @click="tunnel.cancelPairing()">Cancel</button>
    </div>
    <button v-else @click="tunnel.generateQr()" :disabled="tunnel.generating.value">
      Connect Phone
    </button>
  </div>
</template>

Angular

Works with: Angular 17+ (standalone)

Setup

// app.config.ts
import { provideSmsTunnel } from '@narcisbodea/smstunnel-sdk/angular';

export const appConfig: ApplicationConfig = {
  providers: [
    provideSmsTunnel({
      apiBaseUrl: 'https://your-backend.com',
      getAuthHeaders: () => ({
        Authorization: `Bearer ${localStorage.getItem('token')}`,
      }),
    }),
  ],
};

Usage

import { Component, inject } from '@angular/core';
import { SmsTunnelService } from '@narcisbodea/smstunnel-sdk/angular';

@Component({ /* ... */ })
export class SmsPairingComponent {
  private sms = inject(SmsTunnelService);

  async connect() {
    // Pairing
    const status = await this.sms.getStatus();
    const token = await this.sms.createToken();
    const pairings = await this.sms.getPairings();
    await this.sms.unpair();

    // SMS
    await this.sms.sendSms('+40721123456', 'Hello!');
    await this.sms.send2fa('+40721123456', '123456');
    await this.sms.sendBulk([{ to: '+40721111111', message: 'Hi' }]);
    const msgStatus = await this.sms.getSmsStatus('msg-id');
    const inbox = await this.sms.getReceivedSms();

    // Devices
    const devices = await this.sms.getDevices();
    const usage = await this.sms.getUsage();

    // E2E Encryption
    const e2eStatus = await this.sms.getDeviceE2EStatus('device-id');
    const publicKey = await this.sms.getDevicePublicKey('device-id');
    const verification = await this.sms.verifyDeviceKey('device-id', 'fingerprint');
    await this.sms.sendEncryptedSms('payload', 'device-id');
  }
}

Svelte

<script>
  import { SmsTunnelClient } from '@narcisbodea/smstunnel-sdk';
  import { onMount, onDestroy } from 'svelte';

  let status = 'loading';
  let qrData = '';
  let deviceName = '';

  const client = new SmsTunnelClient({
    apiBaseUrl: '/api',
    getAuthHeaders: () => ({
      Authorization: `Bearer ${localStorage.getItem('token')}`,
    }),
  });

  onMount(async () => {
    const s = await client.getStatus();
    status = s.paired ? 'paired' : 'unpaired';
    deviceName = s.deviceName || '';
  });

  onDestroy(() => client.destroy());

  async function connect() {
    const result = await client.createToken();
    if (result.success) {
      qrData = result.qrData;
      client.startPolling(result.token, async (event) => {
        if (event === 'completed') {
          const s = await client.getStatus();
          status = 'paired';
          deviceName = s.deviceName || '';
        }
      });
    }
  }
</script>

{#if status === 'paired'}
  <p>Connected: {deviceName}</p>
  <button on:click={() => client.unpair()}>Disconnect</button>
{:else}
  <button on:click={connect}>Connect Phone</button>
{/if}

Astro

Astro supports React, Vue, and Svelte islands. Use any of the above:

---
// pages/settings.astro
---
<script>
  import { SmsTunnelPairing, RO_LABELS } from '@narcisbodea/smstunnel-sdk/react';
</script>

<SmsTunnelPairing client:only="react" apiBaseUrl="/api" labels={RO_LABELS} />

Or with Vue:

<SmsTunnelPairing client:only="vue" api-base-url="/api" />

E2E Encryption

All SDKs support end-to-end encryption using RSA-2048. The device holds the private key, the server stores the public key.

Flow

  1. Check if device has E2E enabled: getDeviceE2EStatus(deviceId)
  2. Get the device's public key: getDevicePublicKey(deviceId)
  3. Encrypt your message with the RSA public key (client-side)
  4. Send encrypted: sendEncryptedSms(encryptedPayload, deviceId)
  5. Device decrypts with its private key

Key Verification

Store the publicKeyFingerprint locally after first pairing. Before sending, verify it hasn't changed:

const result = await client.verifyDeviceKey('device-id', 'stored-fingerprint');
if (result.needsRePairing) {
  // Device was reinstalled, key changed - need to re-pair
}

SmsTunnelClient API (framework-agnostic)

import { SmsTunnelClient } from '@narcisbodea/smstunnel-sdk';

const client = new SmsTunnelClient({
  apiBaseUrl: '/api',
  getAuthHeaders: () => ({ Authorization: 'Bearer ...' }),
  routePrefix: 'smstunnel', // default
  pollInterval: 3000,       // default
});

// Pairing
await client.getStatus();                    // { paired, serverUrl, deviceName }
await client.createToken();                  // { success, token, qrData, ... }
await client.getPairingStatus(token);        // { status, displayName }
client.startPolling(token, (event) => ...);  // returns cleanup fn
client.stopPolling();
await client.unpair();

// SMS
await client.sendSms('+40741234567', 'Hi');
await client.updateServerUrl('https://...');

// E2E Encryption
await client.getDeviceE2EStatus('device-id');
await client.getDevicePublicKey('device-id');
await client.verifyDeviceKey('device-id', 'fingerprint');
await client.sendEncryptedSms('payload', 'device-id');

client.destroy();                            // cleanup

Labels (i18n)

Built-in presets: EN_LABELS (default), RO_LABELS.

Available from any import path:

import { EN_LABELS, RO_LABELS } from '@narcisbodea/smstunnel-sdk';
import { EN_LABELS, RO_LABELS } from '@narcisbodea/smstunnel-sdk/react';
import { EN_LABELS, RO_LABELS } from '@narcisbodea/smstunnel-sdk/vue';

Custom labels: implement the SmsTunnelLabels interface.


Exports Summary

| Path | Exports | |------|---------| | @narcisbodea/smstunnel-sdk | SmsTunnelClient, EN_LABELS, RO_LABELS, types | | @narcisbodea/smstunnel-sdk/server | SmsTunnelModule, SmsTunnelService, SMSTUNNEL_PUBLIC_PATHS | | @narcisbodea/smstunnel-sdk/react | SmsTunnelPairing, QrCodeCanvas, useSmsTunnel | | @narcisbodea/smstunnel-sdk/vue | SmsTunnelPairing, QrCodeCanvas, useSmsTunnel | | @narcisbodea/smstunnel-sdk/angular | SmsTunnelService, provideSmsTunnel, SMSTUNNEL_CONFIG | | @narcisbodea/smstunnel-sdk/node | SmsTunnelNodeClient |

License

MIT