Skip to content

feat(xchat): add XChat encrypted messaging adapter - #745

Open
santiagomed wants to merge 7 commits into
vercel:mainfrom
santiagomed:xchat
Open

feat(xchat): add XChat encrypted messaging adapter#745
santiagomed wants to merge 7 commits into
vercel:mainfrom
santiagomed:xchat

Conversation

@santiagomed

@santiagomed santiagomed commented Jul 25, 2026

Copy link
Copy Markdown

summary

new @chat-adapter/xchat adapter for XChat, X's encrypted messaging. write bot logic once and hold encrypted 1:1 and group conversations like the other Chat SDK adapters — all crypto handled inside the adapter via @xdevplatform/chat-xdk (wasm), all REST via the typed @xdevplatform/xdk client.

background: chat-xdk

@xdevplatform/chat-xdk is the official XChat cryptography SDK — a Rust core compiled to WebAssembly that implements the XChat encryption protocol. it handles per-conversation symmetric keys and key exchange, message encryption/decryption, event signing and signature verification, and encrypted media (secretstream). the bot's private keys live in a PIN-protected Juicebox store (secret-shared across independent realms), so no key material sits in env vars or on disk — the adapter unlocks with a PIN at startup. this adapter is the glue: chat-xdk produces and consumes the encrypted envelopes, the typed @xdevplatform/xdk client moves them over the X API, and everything is normalized to the Chat SDK's Thread/Message model.

what it supports:

  • encrypted send/receive in DMs and groups (webhook push + polling), signature verification on by default
  • mention detection from structured mention entities, swipe-replies to the bot, and a plain-text @handle fallback; group replies go out as quoted replies with TTL propagated
  • openDM(userId): starts (or reuses) an encrypted 1:1 — cached/history-recovered conversation key, else a full key exchange so the bot can message first
  • media both ways: inbound attachments with lazy download+decrypt, outbound encrypted (secretstream) via the 3-step upload flow
  • edit and delete of the bot's own messages: edits are encrypted events targeting the original's sequence id; deletes are locally signed delete-for-all actions recipients verify
  • reactions in and out, typing keep-alive while handlers run, configurable group welcome message
  • read receipts sent per delivered inbound message (sendReadReceipts, default on)
  • cards by degradation: text + tappable entities, link buttons as label: url lines, primary link as a URL preview attachment with optional encrypted banner

key design decisions:

  • mdast stays the canonical format; markdown passes through as raw text (XChat clients render plain text — no markdown), with URLs and @mentions made tappable via entity spans and tables degraded to ASCII code blocks
  • thread ids are xchat:{conversationId} (groups g…, 1:1s the sorted participant pair)
  • the first edit of a fresh message is age-gated (editSafetyDelayMs, default 5000ms): receiving clients park an edit whose original hasn't arrived, leaving the message permanently invisible — the gate prevents that race
  • undecryptable or unverified events are dropped, never delivered as empty messages
  • no core changes: the adapter implements the standard Adapter interface only

also includes the chat/adapters catalog entry, docs page (with OG image), adapters.json registry entry, and create-chat-sdk scaffold spec, modeled on the x adapter's registration.

usage
XCHAT_BOT_TOKEN=...    # OAuth2 user access token (identity resolved from GET /2/users/me)
XCHAT_PIN=...          # Juicebox PIN that unlocks the bot's keys
X_CONSUMER_SECRET=...  # optional: verifies webhook signatures
import { Chat } from "chat";
import { createXchatAdapter } from "@chat-adapter/xchat";
import { createMemoryState } from "@chat-adapter/state-memory";

const bot = new Chat({
  userName: "mybot",
  adapters: { xchat: createXchatAdapter() }, // credentials from env
  state: createMemoryState(),
});

// DMs always
bot.onDirectMessage(async (thread, message) => {
  await thread.post(`You said: ${message.text}`);
});

// group chats when the bot is @mentioned
bot.onNewMention(async (thread, message) => {
  await thread.post("You rang?");
});

// wire the webhook (e.g. a Next.js route)
export async function POST(request: Request) {
  return bot.webhooks.xchat(request);
}

testing: 109 unit tests, including real-wasm-crypto round trips against vendored fixture vectors (decrypt + signature verification, webhook delivery, read receipts, edit age-gating, signed deletes). verified live against production XChat: DMs, group mentions, media, reactions, edits, deletes, openDM, cards.

note on the lockfile: @xdevplatform/xdk@0.6.6 was published <48h ago, so it was resolved with a one-shot --config.minimumReleaseAge=0 override; the locked integrity hash was verified against the npm registry. the repo policy file is untouched.

New @chat-adapter/xchat package for XChat, X's encrypted messaging.
All cryptography is handled inside the adapter via @xdevplatform/chat-xdk
(wasm) and all REST goes through the typed @xdevplatform/xdk client. The
adapter implements the standard Adapter interface only — no core changes.
@santiagomed
santiagomed requested a review from a team as a code owner July 25, 2026 08:10
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@santiagomed is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@socket-security

socket-security Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​xdevplatform/​xdk@​0.6.68010010096100
Addednpm/​juicebox-sdk@​0.3.6811009190100

View full report

Comment thread packages/adapter-xchat/src/index.ts Outdated
Comment thread .changeset/xchat-sdk-integration.md Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one changeset file is needed, please combine both of them. The content can be trimmed as well.

"@xdevplatform/chat-xdk": "^0.4.3",
"@xdevplatform/xdk": "^0.6.6",
"chat": "workspace:*",
"juicebox-sdk": "^0.3.6"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Juicebox SDK strictly required? The package appears to have minimal traction, posing a potential security risk for users who integrate this adapter with their Chat SDK app.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the X Chat integration will not work without it as it's part of the protocol unfortunately. It's what we use to manage private keys securely. Our native clients also rely on it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While a README is included, the adapter is missing other documentation, including for the website. Additionally, catalog files will need to be updated so this adapter is surfaced in the create-chat-sdk CLI.

Comment thread packages/adapter-xchat/src/types.ts Outdated
- Rename exported identifiers to Xchat casing (XchatAdapter,
  createXchatAdapter, XchatAdapterConfig, and secondary types) so the
  konsistent check passes; the factory now takes the exported
  XchatAdapterConfig like the other adapters
- Normalize the conversation id to the dash-joined form on the media
  upload initialize/append/finalize bodies
- Combine the two changesets into one
Adds the official docs page with OG image, the adapters.json registry
entry, the chat/adapters catalog entry, and the create-chat-sdk scaffold
spec, modeled on the x adapter's registration.
…ured

userId (and XCHAT_USER_ID / X_USER_ID) becomes optional: initialize()
resolves the id and @handle from a single GET /2/users/me call when
either is missing, so the only required credentials are the token and
the PIN.
The bot's identity is fully determined by the token, so the userId
config field and the XCHAT_USER_ID / X_USER_ID env vars are gone;
initialize() always resolves the id and @handle from GET /2/users/me.
The only required credentials are the bot token and the Juicebox PIN.
Also relabels the OG image row as X Chat.
The adapter prepends chat-sdk-xchat/<version> to the xdk client's
User-Agent (which already carries xdk-typescript/<version>), so X API
request logs can distinguish Chat SDK traffic from other xdk usage. A
User-Agent supplied via apiHeaders takes precedence and is left
untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants