@monoverse/voicebot-node
v0.3.0
Published
Server-side ingest client for VoiceBot — push your catalog & structure to the signed ingest endpoint (HMAC-SHA256, protocol v2). The Node/TS twin of the Laravel/WordPress sync producers.
Downloads
385
Maintainers
Readme
@monoverse/voicebot-node
Push your catalog and site structure to VoiceBot from any Node.js backend, so the AI assistant can answer grounded in your real products, categories, pages and shipping/payment options.
This package is a server-side producer client for the VoiceBot canonical signed-ingest contract (Sync Protocol v2). It pairs with VoiceBot once, then streams your data over an HMAC-SHA256-signed protocol: a full snapshot for the baseline and small incremental deltas after that. It is the Node/TypeScript twin of the Laravel and WordPress sync producers.
This is not a browser widget. For the talking UI use
@monoverse/voicebot-react(or the rawpk_embed). This package only pushes data to the ingest endpoint — voice/chat stays on the server.
- Signed — every request is HMAC-SHA256 signed (protocol v2). The shared secret is stored encrypted at rest (AES-256-GCM) and is never logged, printed, or returned.
- Memory-safe — snapshots stream as gzipped NDJSON straight to/from a temp file; the full catalog is never held in memory.
- Correct under failure — signed requests are never auto-retried on 429/5xx (the server consumes the request nonce before it can error, so a blind retry is a replay). Resends are an explicit, opt-in resign-and-resend that mints a fresh nonce.
- Type-safe — TypeScript strict, zero
anyin the public API, ships.d.ts. - Dependency-light — zero runtime dependencies (uses built-in
node:crypto, globalfetch).
Install
npm install @monoverse/voicebot-nodeRequires Node.js 18+. The ingest base URL must be https:// (plaintext http:// is rejected —
an HMAC over http would leak the signature); localhost / 127.0.0.1 are allowed for local dev.
Push your catalog
import { VoiceBotClient, product, category } from '@monoverse/voicebot-node';
const client = new VoiceBotClient({
baseUrl: 'https://api.monoverse.tech',
siteUrl: 'https://shop.example.com',
// Full-entropy 32-byte key (hex/base64/Buffer) — `openssl rand -hex 32`. Encrypts the
// stored credentials at rest. A passphrase fallback is allowed but must be >= 16 chars.
encryptionKey: process.env.VOICEBOT_ENCRYPTION_KEY!,
credentialsFile: './.voicebot-credentials.enc', // omit for in-memory only
});
// 1. Pair once with the VB-XXXX-XXXX code from your VoiceBot dashboard.
// The tenant id + shared secret are stored encrypted; the secret is never returned.
await client.pair('VB-ABCD-1234');
// 2. Push a full snapshot (run once, then on a schedule). Stream entities — never
// materialise the whole catalog. Money is INTEGER MINOR UNITS; categories/tags are slugs.
async function* catalog() {
yield category({ externalId: 'cat:phones', name: 'Phones', slug: 'phones' });
yield product({
externalId: 'shop:product:iphone-15-pro',
name: 'iPhone 15 Pro',
priceAmount: 54999, // 549.99 → 54999 minor units
currency: 'UAH',
stockStatus: 'instock',
categories: ['phones'],
permalink: 'https://shop.example.com/p/iphone-15-pro',
});
}
const result = await client.snapshot(catalog());
console.log(`snapshot ${result.syncId}: ${result.total} records`, result.counts);Incremental deltas (the scheduled path)
After the baseline snapshot, push only what changed. Operations are batched (≤500 ops / ≤5 MB
per batch); each batch carries a fresh batch_id that doubles as its Idempotency-Key.
import { toUpsertOperation, deleteOperation, product } from '@monoverse/voicebot-node';
await client.sendEvents([
toUpsertOperation(product({ externalId: 'shop:product:iphone-15-pro', name: 'iPhone 15 Pro', priceAmount: 52999 })),
deleteOperation('product', 'shop:product:discontinued-sku'),
]);Heartbeat and unpair
const status = await client.status(); // entity counts + last sync session
await client.unpair(); // disconnect server-side, then wipe local credentialsMapping rules that matter
- Money is integer minor units.
199.99 UAH→19999. The builders throw on a float or a negative — convert withMath.round(price * 100). - Categories and tags are slug lists (
['phones', 'audio']), not objects. page.contentTextis plain text — strip HTML before sending.externalIdis your stable id. Use a namespaced shape, e.g.shop:product:{id}, and keep parent references consistent (category({ parentExternalId: 'shop:category:42' })).- A full snapshot is the source of truth: anything missing from it is tombstoned server-side, so an empty snapshot is refused (it would wipe the catalog).
Entity coverage
site, product, variation, category, tag, attribute, page, post, cpt, menu,
menu_item, shipping_method, payment_method (plus environment, form, popup via the
generic entity() builder). Send only the kinds you have.
Reliability & retries
| Path | Signed? | Retry policy |
| --- | --- | --- |
| pair | no | retries on 429/5xx/connection (no nonce spent) |
| init / finalize / events / status / unpair | yes | no auto-retry; opt into resignRetries to resign-and-resend with a fresh nonce |
| snapshot upload (PUT) | no (single-use URL) | not retried |
// Opt into resign-and-resend on idempotent calls (each retry mints a fresh nonce):
await client.sendEvents(ops, { resignRetries: 2 });Security
- The shared secret is stored encrypted at rest with AES-256-GCM (
node:crypto), keyed by yourencryptionKey, and is never logged, printed, or serialized —Credentialsredacts it fromJSON.stringify,console.log, andtoString. - Use a full-entropy 32-byte key for
encryptionKey— generate one withopenssl rand -hex 32and keep it in your secret manager / env. A 32-byteBuffer, 64-char hex, or 32-byte base64 string is used directly. A plain passphrase is an explicit weaker fallback (scrypt-stretched) and must be at least 16 chars — anything shorter is rejected so a low-entropy at-rest key never goes unnoticed. - Every signed request carries an HMAC over
METHOD\npath\nts\nnonce\nbody_sha256; the exact bytes hashed are the exact bytes sent, so the server's body check always matches. This is pinned by a cross-language conformance test against the shared signing vectors. - Requests use a fresh 16-byte nonce and a current timestamp (server enforces a 300s replay window).
License
MIT © Monoverse.
