@quantumchat/bot-sdk
v0.1.0
Published
Node SDK for QuantumChat bot operators — hold the bot's PQ identity, receive PQ-encrypted envelopes, and reply through the relay. See ENG-1891.
Downloads
182
Readme
@quantumchat/bot-sdk
Node SDK for QuantumChat bot operators — build a Telegram-style bot that is end-to-end post-quantum encrypted underneath. Your bot server holds the bot's PQ messaging identity, receives encrypted messages addressed to the bot, and replies through the relay. Keys never leave your machine.
- Post-quantum E2E — ML-KEM-1024 + ML-DSA-65 + PQXDH via the native
@quantumchat/pqchat-nodebinding (same core as the QuantumChat clients). - BotFather-style — a user creates the bot in-app, you get an API token, paste it into your server; the SDK binds the bot's keys and runs.
- Batteries included — message + command routing, rich replies, mini-app data, retry/backoff, outbound rate-limiting, and launch-payload verification.
Requirements
- Node ≥ 18 (uses global
fetch, aWebSocket-style transport,crypto.randomUUID). - The native
@quantumchat/pqchat-nodebinding installed for your platform (provides ML-KEM/ML-DSA/PQXDH/AES). See Installation. - A running QuantumChat relay and a bot API token (from Bot Maker, or minted headlessly for dev — see below).
Installation
npm install @quantumchat/bot-sdk
# peer native binding (Rust/N-API) + shared types:
npm install @quantumchat/pqchat-node @quantumchat/webapp-typesMonorepo note: inside this repository the SDK consumes
@quantumchat/pqchat-nodeand@quantumchat/webapp-typesasworkspace:*dependencies. For external distribution those must be published (or bundled) first — see Deployment at the bottom.
Quick start
1. Get a bot + token
A QuantumChat user creates the bot in the in-app Bot Maker (BotFather flow)
and copies the API token shown once. Paste it into your server's env
(BOT_API_TOKEN). For local development you can skip Bot Maker and mint a bot
headlessly with createAndBindBot (see below).
2. Bind keys + run
import {
generateBotIdentity,
bindBot,
loadCredential,
saveCredential,
QuantumChatBot,
} from '@quantumchat/bot-sdk';
import { existsSync } from 'node:fs';
import { echoBotManifest } from './manifest'; // your signed manifest
const CRED = './bot-credential.json';
const relayUrl = process.env.RELAY_URL ?? 'http://localhost:8080';
// First run: generate the bot's identity locally (the mnemonic never leaves the
// machine) and bind its public keys to the pending bot using the API token.
const credential = existsSync(CRED)
? await loadCredential(CRED)
: await (async () => {
const identity = generateBotIdentity();
const cred = await bindBot(echoBotManifest, {
relayUrl,
apiToken: process.env.BOT_API_TOKEN!, // from Bot Maker
identity,
});
await saveCredential(CRED, cred); // mode 0600
return cred;
})();
const bot = new QuantumChatBot({ credential, manifest: echoBotManifest });
bot.onMessage(async (msg, ctx) => {
if (msg.command === '/start') return ctx.reply({ text: 'Hi! I echo what you say.' });
await ctx.reply({ text: `echo: ${msg.text ?? ''}` });
});
bot.onWebAppData(async (data, ctx) => {
await ctx.reply({ text: `got web-app data: ${JSON.stringify(data.data)}` });
});
await bot.start(); // restores identity, re-uploads prekeys, subscribes over WSSDev (headless, no Bot Maker)
import { generateBotIdentity, createAndBindBot } from '@quantumchat/bot-sdk';
// Registers an owner, mints a bot token, and binds — DEV/E2E ONLY.
const credential = await createAndBindBot(echoBotManifest, {
relayUrl,
ownerEmail: process.env.OWNER_EMAIL!,
ownerPassword: process.env.OWNER_PASSWORD!,
identity: generateBotIdentity(),
});A runnable end-to-end example lives in
examples/bot-server-stub.
Core concepts
| Concept | What it is |
|---|---|
| BotIdentity | The bot's PQ messaging identity (mnemonic-derived ML-KEM + ML-DSA + prekeys) plus a standalone ML-DSA key for manifest signing. Created by generateBotIdentity(); the mnemonic never leaves your server. |
| BotCredential | What your server persists to run the bot: { botIdentityId, slug, apiToken, mnemonic, relayUrl }. Save/load with saveCredential / loadCredential (mode 0600). |
| Create → Bind → Run | A user creates the bot in-app (reserves the slug + issues the token). Your SDK binds its locally-generated public keys (bindBot), flipping the bot active. Then it runs (QuantumChatBot.start()). |
| Token | The API token is a long-lived relay session bearer. The owner can revoke/rotate it from Bot Maker; a revoked token makes start() fail fast with a clear error. |
API reference
Identity & credential
generateBotIdentity(accountId?)→BotIdentity— generate locally.saveIdentity(path, id)/loadIdentity(path)— persist the identity (mode0600).saveCredential(path, c)/loadCredential(path)— persist theBotCredential(mode0600).
Registration
bindBot(manifest, { relayUrl, apiToken, identity })→Promise<BotCredential>— bind keys to a pending bot created in Bot Maker.createAndBindBot(manifest, { relayUrl, ownerEmail, ownerPassword, identity })→Promise<BotCredential>— dev/E2E only: register an owner, mint a token, then bind.signManifest(manifest, identity)→{ payload, signature }— ML-DSA-65 sign a manifest.
The bot runtime
new QuantumChatBot({ credential, manifest, onRetry? }).onMessage((msg, ctx) => …)— inbound chat;msg.command/msg.argsare pre-parsed..onWebAppData((data, ctx) => …)— a mini-app'sWebApp.sendData(...)..onStartChat(handler)— fires once on a sender's first message..onBlock / .onPermGrant / .onPermRevoke / .onWebAppOpen— lifecycle hooks (payload shapes per the webhook event spec)..start()/.stop()— connect / disconnect the relay transport..isRunning.ctx.reply({ text?, attachments?, inline_keyboard?, web_app? })— encrypt + send to the sender.
Mini-app launch verification
verifyInitData(initData, { relayPublicKey, maxAgeMs?, maxClockSkewMs? })→{ valid, data?, reason? }— verify a relay-signed mini-appinitDatapayload (ML-DSA-65) before trustinguser_id_hmac/user.fetchRelayInitDataPubkey(relayUrl)→ the relay's verification public key (base64).
Utilities
parseCommand(text)→{ command, args }.withRetry(fn, opts)/isRelayRetriable(err)— exponential backoff + jitter;RelayRateLimitErroron exhaustion.createRateLimiter({ perUserPerMinute, globalPerMinute })— outbound token bucket.- Errors:
BotConfigError,RelayError,RelayRateLimitError.
Configuration (env used by the example server)
| Var | Purpose |
|---|---|
| RELAY_URL | Relay base URL (default http://localhost:8080). |
| BOT_API_TOKEN | API token from Bot Maker (production path). |
| OWNER_EMAIL / OWNER_PASSWORD | Dev headless fallback (createAndBindBot). |
Security
- The mnemonic and API token are secrets.
saveCredential/saveIdentitywrite with file mode0600; never commitbot-credential.json(gitignore it), never log the mnemonic or token. - A revoked/expired token makes
start()throwBotConfigError: bot API token revoked or expired— re-issue from Bot Maker. - Inbound envelopes are verified cryptographically; a spoofed or stale frame is dropped, never tearing down a working session.
- Verify mini-app launch payloads with
verifyInitDatabefore trusting any user fields — a WebView page cannot forge a relay signature.
Resources
- Quickstart:
docs/bot-operator-quickstart.md - Webhook event shapes:
docs/bot-webhook-events.md - Runnable example:
examples/bot-server-stub
Deployment
Builds to dist/ (ESM + CJS + .d.ts) via npm run build. Before publishing to
a registry, the two workspace:* dependencies must be resolvable by consumers:
@quantumchat/webapp-types— pure TypeScript types; publish as-is.@quantumchat/pqchat-node— native (Rust/N-API); needs per-platform prebuilt binaries (napi-rsoptionalDependencies) before it can be installed off-registry.
Until both are published, the SDK is consumed workspace-locally within this monorepo (the example server imports it directly).
License
UNLICENSED — internal QuantumChat package.
