Two encrypted chat clients built on the chat-xdk WASM binding: an
interactive browser app and a headless bot. Encryption runs in JavaScript (in
the browser, or in Node for the bot); REST calls go through the XDK
(@xdevplatform/xdk).
- Browser app (
public/) — an interactive encrypted chat client. All crypto runs in the browser via WASM; a tiny dev server (server.mjs) relays encrypted blobs to the X Chat API with the XDK, so the access token never reaches the page. - Headless bot (
src/bot.mjs) — the automated counterpart that auto-replies.
| File | Purpose |
|---|---|
src/chat-core.mjs |
All encryption logic — the only file that touches the WASM binding. |
src/x-api.mjs |
The X Chat API I/O layer (XDK client). |
src/bot.mjs |
The receive → decrypt → reply → encrypt → send loop. |
src/register.mjs |
One-time public-key registration (Juicebox-backed). |
server.mjs |
Dev server: serves the browser app + proxies the X API. |
public/ |
The browser chat UI. |
test/chat-core.test.mjs |
Offline test driving the real binding (no mocks). |
- Node.js 18+
- Rust +
wasm-packto build the WASM package (crates/wasm/pkg/is not committed): runnpm run build:wasmonce before the offline test or the browser app. The example then imports the chat-xdk WASM package from this repo by relative path; for a standalone project, install the package and import from@xdevplatform/chat-xdkinstead. The REST client uses@xdevplatform/xdk(npm install @xdevplatform/xdk).
The bot imports its raw private-key blob into a chat from createChat(),
which reads the X API's feature switches once, when it is created, and sends
v1 if v1_send was on then. The browser app drives the lower-level WASM
Chat from crates/wasm/pkg/, which doesn't read the switches, so its
messages go out in the v0 format.
npm run build:wasm # once per Rust change; needs Rust + wasm-pack
node --testThe test imports the fixture private keys, checks the binding reproduces the committed public-key/signature vectors, and runs a real encrypt → decrypt round-trip plus a conversation-key ECIES round-trip.
cp .env.example .env # set X_ACCESS_TOKEN
npm run build:wasm # build a browser (web-target) WASM package
npm start # serve on http://localhost:8787Paste a private key blob and its key version (the public_key_version the X
API returned when the key was registered), or generate one in the page — see
the note below. Then enter a conversation ID and chat.
cp .env.example .env # set X_ACCESS_TOKEN, CHAT_CONVERSATION_ID, CHAT_PRIVATE_KEYS_B64,
# and CHAT_SIGNING_KEY_VERSION (the key's public_key_version)
npm run botThe bot echoes messages back (pong for ping, otherwise You said: …).
Customize generateReply in src/bot.mjs to change the reply logic.
Before a bot can send or receive, its public key must be registered on the account. This is a rate-limited write (only a few per 24h), so run it once:
npm run build:wasm # once per Rust change
X_ACCESS_TOKEN=... CHAT_PIN=... npm run register -- --confirmUnlike the other language examples, this script shows the Juicebox flow: it
persists the identity in Juicebox under CHAT_PIN (required) instead of a
local blob, and recovers it with unlock(pin).
It needs the optional juicebox-sdk peer dependency installed. A brand-new
account works too: the account's juicebox_config is created by the
public-key POST itself, so the script creates the chat with no config,
generates keys, POSTs the public key, then fetches the config and stores the
keys with updateConfig + setup(pin). Before POSTing, it checks whether the
key is already on the account and skips the write if so; on HTTP 429 it stops
and prints when the window resets rather than retrying. A marker under
state/ records that registration is done (--force overrides to mint a new
identity). state/ is gitignored — never commit key material.
The wasm32-unknown-unknown target has no system clock, so the binding reads
conversation key versions from JavaScript's Date.now(). Key generation
(generateKeypairs), conversation-key creation (prepareConversationKeyChange),
encryption, and both decrypt paths therefore all run directly in the browser —
the sample is fully self-contained and never needs another language to mint keys.
MIT