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

@webzaytsev/support-sdk

v1.1.1

Published

TypeScript SDK for Support Bot Web API

Readme

@webzaytsev/support-sdk

TypeScript SDK for the Support Bot Web API — a Telegram-based customer support system with a REST/SSE web interface.

Note: The support bot itself is a private, commercial product. This SDK is published publicly so that frontend teams can integrate the support widget without needing access to the backend source code.


Installation

pnpm add @webzaytsev/support-sdk
# or
npm install @webzaytsev/support-sdk

Runtime requirements: Node.js 18+ or any modern browser (Fetch API). The /react entry additionally requires React ≥ 18 (peer dependency, optional).


Quick Start — High-level Chat Client

The createSupportChat factory wraps all real-time transport, state management, and error handling so you don't have to.

0. Configure once at app startup

import { configure } from '@webzaytsev/support-sdk';

configure({
  baseUrl: 'https://sup.example.com',
  token: () => getOrRefreshJwt(), // called per-request; cache + refresh logic lives here
});

Always use an async token provider for SPAs — it handles token expiry transparently.
Never put the token in localStorage; keep it in memory only.

1. Issue a JWT (server-side only)

Your backend exchanges the server-to-server API key for a short-lived user JWT. Never call this from the browser — it requires WEB_API_KEY which must stay secret.

// Called from YOUR backend, not the browser
import { webAuthControllerIssueToken } from '@webzaytsev/support-sdk';

const { data } = await webAuthControllerIssueToken({
  headers: { 'X-Internal-Api-Key': process.env.WEB_API_KEY },
  body: {
    userId: 'user_42',
    userProfile: { name: 'Ivan Petrov', email: '[email protected]' },
  },
});

// data.token — pass to the frontend securely (e.g. via your session cookie)
// data.expiresAt — ISO timestamp, use for proactive refresh

2. Create a chat session

import { createSupportChat } from '@webzaytsev/support-sdk/chat';

const chat = createSupportChat({
  // All options are optional — these are the defaults:
  pollIntervalMs: 5000,          // polling fallback interval
  backoffBaseMs: 1000,           // initial reconnect delay
  backoffMaxMs: 30000,           // maximum reconnect delay (with jitter)
  maxSseAttempts: 5,             // SSE failures before switching to polling
  sseRetryAfterPollingMs: 60000, // how long to stay in polling before probing SSE again
  maxMessages: 200,              // messages kept in memory
  enablePolling: true,           // enable polling fallback when SSE is unavailable
});

3. Subscribe and render

const unsub = chat.subscribe(() => {
  const { conversation, messages, status, error, isSending } = chat.getSnapshot();
  renderWidget({ conversation, messages, status, error, isSending });
});

4. Start (hydrate + connect realtime)

await chat.start();
// Fetches the current conversation and message history, then opens an SSE stream.
// Falls back to polling automatically if SSE is unavailable (e.g. buffering proxy).

5. Send a message

// Plain text
await chat.send('My subscription is not working');

// With file attachments (validated client-side: jpeg/png/webp, ≤10 MB)
await chat.send('See the screenshot', { files: [selectedFile] });

Messages appear immediately as "pending" (optimistic update), then confirmed once the server responds.

6. Conversation actions

await chat.escalate(); // User pressed «Передать оператору»
await chat.resolve();  // User pressed «Проблема решена»

Error codes (conversation actions):

  • CONVERSATION_CLOSED (409) — returned by resolve() when the ticket is already closed server-side. Safe to ignore or surface as a no-op toast; no widget change required.
  • escalate() on a closed ticket is valid: the backend explicitly reopens the Telegram topic and labels the moderator notification as a reopen.

7. Cleanup

// On widget unmount / page unload:
unsub();
chat.destroy(); // closes SSE, clears timers, releases all resources

React Integration

Recommended: @webzaytsev/support-sdk/react

The /react entry ships a ready-made hook. Requires React ≥ 18.

import { useSupportChat } from '@webzaytsev/support-sdk/react';

function SupportWidget() {
  const { conversation, messages, status, error, isSending, send } = useSupportChat();
  // render...
}

useSupportChat accepts the same optional ChatSessionOptions as createSupportChat:

const chat = useSupportChat({ maxMessages: 100, pollIntervalMs: 3000 });

The hook is StrictMode-safe (session restarts cleanly on double-mount), SSR-safe (returns an idle snapshot on the server with no network calls), and passes react-hooks/exhaustive-deps / react-hooks/rules-of-hooks.

Build-your-own hook (framework-agnostic, no /react entry)

If you prefer not to add the /react entry, you can wire the session manually using useState as the lazy initialiser — this is the only lint-clean way to hold a stable instance without reading or writing a ref during render:

import { useState, useEffect, useCallback, useSyncExternalStore } from 'react';
import { createSupportChat } from '@webzaytsev/support-sdk/chat';
import type { ChatSessionOptions } from '@webzaytsev/support-sdk/chat';

function useSupportChatManual(options?: ChatSessionOptions) {
  // Lazy initialiser — runs once, never touches ref.current during render
  const [chat] = useState(() => createSupportChat(options));

  useEffect(() => {
    void chat.start();
    return () => chat.destroy();
  }, [chat]);

  const subscribe = useCallback((cb: () => void) => chat.subscribe(cb), [chat]);
  const getSnapshot = useCallback(() => chat.getSnapshot(), [chat]);

  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

Snapshot shape

interface ChatSnapshot {
  conversation: ConversationInfoDto | null;  // current conversation or null
  messages: ChatMessage[];           // chronological, deduped, sorted
  status: ConnectionStatus;          // 'idle' | 'connecting' | 'online' | 'polling' | 'offline'
  error: ChatError | null;           // sanitised error (no tokens or PII)
  isSending: boolean;                // true while any send() is in flight
}

interface ChatMessage extends MessageDto {
  deliveryStatus: 'pending' | 'sent' | 'failed';
  tempId?: string;  // only present on optimistic messages
}

Exports

| Import path | Contents | |---|---| | @webzaytsev/support-sdk | SDK functions, TypeScript types, configure(), TokenProvider, plugin types | | @webzaytsev/support-sdk/chat | createSupportChat, ChatSession, snapshot/status types — zero extra dependencies | | @webzaytsev/support-sdk/react | useSupportChat hook — peer: React ≥ 18 | | @webzaytsev/support-sdk/client | client singleton, createClient — advanced interceptor access | | @webzaytsev/support-sdk/zod | Zod schemas for all DTOs — optional, adds ~30 KB | | @webzaytsev/support-sdk/safe-render | renderSafeHtml via DOMPurify — only if you render HTML content |

Both CJS (require) and ESM (import) are supported.


Security

  • Tokens in memory only. Never store the JWT in localStorage or sessionStorage — XSS can read them. The async token provider pattern (configure({ token: () => getJwt() })) keeps tokens in closure scope.
  • Authorization header only. The SDK uses Authorization: Bearer for all requests including SSE. The ?access_token= query fallback has been removed — it leaked tokens into server logs, browser history, and Referer headers.
  • issueToken is server-to-server only. It requires WEB_API_KEY which must never reach the browser. Call it exclusively from your backend.
  • CSRF does not apply. Bearer tokens must be explicitly added to requests; browsers do not auto-attach them the way they do cookies.
  • Recommended CSP headers:
    connect-src 'self' https://sup.example.com;
    img-src     'self' https://sup.example.com;
    script-src  'self';
  • Rendering message content: Never insert message.content via innerHTML without sanitising first. Use @webzaytsev/support-sdk/safe-render or your own DOMPurify call. Render attachments only as <img> with a known src — never embed as SVG or raw HTML.

XSS-safe rendering (optional)

Install DOMPurify if your widget renders content as HTML (links, basic markdown):

pnpm add dompurify
import { renderSafeHtml } from '@webzaytsev/support-sdk/safe-render';

element.innerHTML = renderSafeHtml(message.content ?? '');
// → Sanitised: strips on* handlers, javascript: URLs, forces rel="noopener noreferrer" on links

SSE Event Types

| type | Description | data fields | |---|---|---| | message | New message from moderator or bot | messageId, text, attachmentIds?, createdAt | | reopened | Conversation reopened after resolution | — | | escalation_ack | Escalation acknowledged | escalatableMessageId, source | | closed | Conversation closed by moderator | — | | heartbeat | Keep-alive ping (~15 s interval) | — |

Recommended widget copy (Russian)

The backend does not ship UI strings for the embeddable widget — your frontend owns button labels and banners. Suggested copy aligned with the Telegram bot:

| UI element | Suggested text (RU) | |---|---| | Escalation button | Передать оператору | | User resolved action | Проблема решена | | Input placeholder | У меня не работает подписка | | closed event banner | Ваше обращение закрыто. | | escalation_ack event banner | Подключаем дополнительного специалиста. |

Show the escalation button when message.escalatableMessageId is present, then call chat.escalate() or POST /web/conversation/:id/escalate.

Migrating from 0.x

| Old (≤0.4.x) | New (1.0.0+) | |---|---| | GET /web/tickets/current | GET /web/conversation | | GET /web/tickets/:id/messages | GET /web/conversation/:id/messages | | GET /web/tickets/:id/stream | GET /web/conversation/:id/stream | | POST /web/tickets/:id/escalate | POST /web/conversation/:id/escalate | | POST /web/tickets/:id/resolve | POST /web/conversation/:id/resolve | | TicketInfoDto ({ id, status, createdAt, updatedAt }) | ConversationInfoDto ({ id, createdAt, updatedAt } — no status) | | CurrentTicketResponseDto.ticket | ConversationResponseDto.conversation | | SendMessageResponseDto.ticketId | SendMessageResponseDto.conversationId | | WebSseEventDto.ticketId | WebSseEventDto.conversationId | | ChatSnapshot.ticket (had .status) | ChatSnapshot.conversation (no status) | | webTicketsController* functions | webConversationController* functions |

There is no open/closed status anymore. Never gate the composer, hide messages, or clear history based on any lifecycle flag — the backend always returns the full conversation regardless of internal ticket state. escalate()/resolve() are always-available soft actions.


Advanced / Low-Level API

Use these only if you need direct control over individual requests (interceptors, custom retry logic, etc.). For most use cases, createSupportChat is sufficient.

Raw SDK functions

import {
  webConversationControllerSendMessage,
  webConversationControllerGetCurrent,
  webConversationControllerGetMessages,
  webConversationControllerSseStream,
  webConversationControllerEscalate,
  webConversationControllerResolveByUser,
  webUploadsControllerUpload,
  webUploadsControllerGetUpload,
} from '@webzaytsev/support-sdk';

Raw client access (interceptors)

import { client } from '@webzaytsev/support-sdk/client';

client.interceptors.request.use((req) => {
  req.headers.set('X-Trace-Id', crypto.randomUUID());
  return req;
});

SSE stream (manual)

import { webConversationControllerSseStream } from '@webzaytsev/support-sdk';

const { stream } = await webConversationControllerSseStream({ path: { id: conversationId } });

for await (const event of stream) {
  switch (event.type) {
    case 'message':
      appendMessage({ id: event.data?.messageId, text: event.data?.text });
      break;
    case 'heartbeat':
      // keep-alive, no action needed
      break;
  }
}

Polling fallback (manual)

let cursor: string | null = null;

setInterval(async () => {
  const { data } = await webConversationControllerGetMessages({
    path: { id: conversationId },
    query: { since: cursor ?? undefined, limit: '50' },
  });

  if (data.messages.length > 0) {
    appendMessages(data.messages); // oldest-first
    cursor = data.nextCursor;      // string | null
  }
}, 5000);

Zod schemas

import { zSendMessageDto, zConversationResponseDto } from '@webzaytsev/support-sdk/zod';

const parsed = zSendMessageDto.parse({ text: 'Hello' });
const conversation = zConversationResponseDto.parse(apiResponse);

API Reference

| Function | Method | Description | |---|---|---| | webAuthControllerIssueToken | POST /web/auth/token | Issue user JWT (server-side, requires API key) | | webConversationControllerSendMessage | POST /web/messages | Send message (creates or continues conversation) | | webConversationControllerGetCurrent | GET /web/conversation | Get conversation + message history | | webConversationControllerGetMessages | GET /web/conversation/:id/messages | Poll messages since cursor | | webConversationControllerSseStream | GET /web/conversation/:id/stream | SSE real-time event stream | | webConversationControllerEscalate | POST /web/conversation/:id/escalate | Escalate the conversation | | webConversationControllerResolveByUser | POST /web/conversation/:id/resolve | Confirm issue is resolved | | webUploadsControllerUpload | POST /web/uploads | Upload an image (jpeg/png/webp, max 10 MB) | | webUploadsControllerGetUpload | GET /web/uploads/:id | Stream an uploaded image |


Plugin Types

The extra field in WebProfileDto is an open string key-value bag. Plugins read specific keys from it. Typed helpers are exported from the main package.

Remnawave plugin

The Remnawave plugin (/sub command, ticket info card) looks for a user identifier using this priority order: extra.userIdextra.remnawave_user_idextra.user_id → Telegram ID (TG channel only).

import type { RemnawaveWebProfile } from '@webzaytsev/support-sdk';

const profile: RemnawaveWebProfile = {
  name: 'Ivan Petrov',
  email: '[email protected]',
  extra: {
    userId: 'ddc9c1c9-973c-46d9-acd7-8db629e7bc98',
  },
};

License

MIT — the SDK is free to use. The support bot backend is a private commercial product.