Files
vercel__chat/turbo.json
T
Santiago Medina caa63253c5 feat(x): add XChat encrypted messaging support (#745)
## 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`](https://www.npmjs.com/package/@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](https://juicebox.xyz) 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.

<details><summary>usage</summary>

```bash
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
```

```typescript
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);
}
```

</details>

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.

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-07-31 23:52:18 +01:00

66 lines
1.4 KiB
JSON

{
"$schema": "https://turbo.build/schema.json",
"globalEnv": [
"NODE_ENV",
"SLACK_BOT_TOKEN",
"SLACK_SIGNING_SECRET",
"TEAMS_APP_ID",
"TEAMS_APP_PASSWORD",
"TEAMS_APP_TENANT_ID",
"GOOGLE_CHAT_CREDENTIALS",
"GOOGLE_CHAT_PUBSUB_TOPIC",
"GOOGLE_CHAT_IMPERSONATE_USER",
"FACEBOOK_APP_SECRET",
"FACEBOOK_PAGE_ACCESS_TOKEN",
"FACEBOOK_VERIFY_TOKEN",
"WHATSAPP_ACCESS_TOKEN",
"WHATSAPP_APP_SECRET",
"WHATSAPP_PHONE_NUMBER_ID",
"WHATSAPP_VERIFY_TOKEN",
"TWILIO_ACCOUNT_SID",
"TWILIO_AUTH_TOKEN",
"TWILIO_PHONE_NUMBER",
"TWILIO_MESSAGING_SERVICE_SID",
"X_API_BASE_URL",
"X_CLIENT_ID",
"X_CLIENT_SECRET",
"X_CONSUMER_SECRET",
"X_ENCRYPTION_KEY",
"X_REFRESH_TOKEN",
"X_USER_ACCESS_TOKEN",
"X_USER_ID",
"X_USERNAME",
"XCHAT_BOT_TOKEN",
"XCHAT_PIN",
"X_ACCESS_TOKEN",
"X_BOT_USERNAME",
"X_VERIFY_SIGNATURES",
"X_DISABLE_WEBHOOK_VERIFICATION",
"X_SIGNING_KEY_VERSION",
"BOT_USERNAME",
"REDIS_URL",
"EDGE_CONFIG"
],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "docs/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"clean": {
"cache": false
}
}
}