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

@siymo/otp-sdk

v2.1.0

Published

Umbrella package for the Siymo OTP SDKs. Re-exports @siymo/otp-server (server-side); use @siymo/otp-browser directly in the browser.

Readme

siymo-otp-sdk

TypeScript SDK for the REST and WebSocket contract exposed by siymo-otp-service.

This repository ships two purpose-built SDKs plus a shared core. Pick the one that matches where your code runs — never use the legacy SiymoOtpClient in a browser, since it is not designed to keep your API key out of the bundle.

| Package | Where it runs | Holds API key? | Use it for | |---|---|---|---| | @siymo/otp-server | Your backend (Node) | Yes | Initiating sessions, verifying OTPs, confirming verificationTokens, account/history endpoints | | @siymo/otp-browser | The end-user's browser | No | Subscribing to /ws/otp, rendering QR codes, awaiting otp.verified | | @siymo/otp-core | Either | n/a | Shared types, error classes, WebSocket frame parser |

The umbrella package @siymo/otp-sdk (this repo's root package.json) re-exports @siymo/otp-server for backward compatibility with code that imported siymo-otp-sdk. Browser code MUST import @siymo/otp-browser directly; importing the umbrella in a browser will trigger a runtime warning when an API key is detected.


End-to-end flow (inbound SMS)

This is the canonical browser-SDK use case. The customer's browser shows a QR code; the user scans it; their SMS app pre-fills with a base64-encoded OTP and the service number; once delivered, the service emits otp.verified over the WebSocket with a short-lived verificationToken JWT.

// 1. Customer's backend (Node) — uses @siymo/otp-server
import { SiymoOtpServer } from '@siymo/otp-server';

const otp = new SiymoOtpServer({
  apiKey: process.env.SIYMO_OTP_API_KEY!, // siymo_<48-hex>
  baseUrl: 'https://otp.siymo.com',
});

// POST /api/start — called by the browser
app.post('/api/start', async (req, res) => {
  const session = await otp.inbound.sms.initiate({
    phone: req.body.phone,
    qrCode: true,
  });

  // Stash the phone we requested so we can compare on confirm.
  req.session.pendingPhone = req.body.phone;

  res.json({
    sessionId: session.sessionId,
    clientToken: session.clientToken,
    qrCodeImage: session.qrCodeImage,
  });
});

// POST /api/confirm — called by the browser when the WS reports verified
app.post('/api/confirm', async (req, res) => {
  const result = await otp.inbound.confirm({
    verificationToken: req.body.verificationToken,
    expectedPhone: req.session.pendingPhone, // throws on mismatch
  });

  await db.users.update(req.user.id, { phoneVerified: true, phone: result.phone });
  res.sendStatus(204);
});
// 2. End-user's browser — uses @siymo/otp-browser
import { SiymoOtpBrowser } from '@siymo/otp-browser';

const { sessionId, clientToken, qrCodeImage } = await fetch('/api/start', {
  method: 'POST',
  body: JSON.stringify({ phone: '+998901234567' }),
  headers: { 'content-type': 'application/json' },
}).then((r) => r.json());

const client = new SiymoOtpBrowser({
  baseUrl: 'https://otp.siymo.com',
  sessionId,
  clientToken,
});

client.renderQr('#qr', qrCodeImage);

const verified = await client.waitForConfirmation();
await fetch('/api/confirm', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ verificationToken: verified.data.verificationToken }),
});

The browser SDK never sees the API key, never calls /otp/initiate/* directly, and only forwards the short-lived verificationToken JWT back to the customer backend. The backend re-verifies the JWT signature server-to- server via POST /otp/confirm and only then trusts the verified state.


Outbound flows

These use traditional "enter the code" UIs and don't need the browser SDK. The user types the OTP into the customer's frontend; the customer backend calls verify on their behalf.

// Customer backend
const session = await otp.outbound.sms.initiate({
  phone: '+998901234567',
  language: 'uz',
});
// Send `session.sessionId` to the frontend along with whatever UI prompts the user.

const result = await otp.outbound.verify({
  sessionId: incomingSessionId,
  otp: '482031',
});
if (result.success) {
  // verified
}

If you'd rather wait for verification rather than long-poll, use otp.outbound.wait({ sessionId }) — it parks the request server-side until the session resolves (TTL up to 10 minutes).


Migration from the previous single-package SDK

If your code today does:

import { SiymoOtpClient } from 'siymo-otp-sdk';
const client = new SiymoOtpClient({
  baseUrl: 'https://otp.siymo.com',
  defaultHeaders: { Authorization: `Bearer ${process.env.SIYMO_OTP_API_KEY}` },
});
await client.initiateInboundSms({ phone, qrCode: true });

…it still works (the legacy SiymoOtpClient is preserved in this repo's src/), but is deprecated. Migrate to:

// Backend
import { SiymoOtpServer } from '@siymo/otp-server';
const otp = new SiymoOtpServer({
  apiKey: process.env.SIYMO_OTP_API_KEY!,
  baseUrl: 'https://otp.siymo.com',
});
await otp.inbound.sms.initiate({ phone, qrCode: true });

The legacy client emits a console.warn when it detects an API key passed via defaultHeaders while running inside a browser-like environment (window/self defined). API keys must never leave your backend.


What's new in this version

  • Per-session clientToken: every initiate* response now includes a clientToken. The browser SDK requires it on /ws/otp upgrade. Plain text only travels backend → browser; the OTP service stores its SHA-256.
  • Stateless verificationToken JWT: emitted in otp.verified, valid for ~60 seconds, signed with OTP_VERIFICATION_JWT_SECRET. Forward it from the browser to your backend; your backend confirms it via POST /otp/confirm (@siymo/otp-server's inbound.confirm).
  • Phone-free WebSocket payloads: otp.attempt, otp.locked, and otp.verified no longer include the user's phone number — the canonical phone is delivered to your backend by the JWT confirm round-trip.

Notes

  • The SDK uses the service's existing WebSocket contract at /ws/otp?sessionId=<uuid>&token=<clientToken>.
  • The long-poll endpoint /otp/wait?sessionId=<uuid> requires Bearer auth and is therefore only callable from @siymo/otp-server. The browser SDK can reach it through a customer-controlled proxy (waitForConfirmationLongPoll).
  • Inbound call and SMS initiation requests accept an optional qrCode: true flag. When set, the response includes qrCodeImage as a data URL.
  • The WebSocket stream can emit otp.subscribed, otp.attempt, otp.locked, otp.verified, and otp.expired.
  • In modern Node runtimes such as Node 22.x, WebSocket is available globally. To override it, pass webSocketFactory to the browser SDK.
  • waitForConfirmation() keeps waiting through otp.attempt events, forwards WebSocket callbacks like onAttempt, and rejects on otp.locked or otp.expired.
  • Verification endpoints intentionally return 200 OK for both success and logical verification failures. Inspect typed response fields like verified, message, and triesLeft.