Witbitz docs HomeTrustAll docs

Where the keys live — the confidential tier, split keys and vault mode

By default a room's key is made in your page and kept there — in its link, and in whatever you save. Three choices change that: the confidential tier seals the key to a verified enclave for the room's turns, a split-key room leaves half of the key with your own server, and vault mode keeps every key out of your page.

Part of the SDK · every call: the reference.


The confidential tier

A confidential room's turns run inside an attested AWS Nitro enclave, and the room key is sealed to that enclave rather than handed to Witbitz's server:

javascript
const space = await wb.createSpace({ agent: { name: 'Vault' }, tier: 'enclave' })

What a room can prove

Checking an enclave with a standard token (EAT)

Witbitz's enclaves can prove themselves with an Entity Attestation Token (RFC 9711): a JWT signed by a key the enclave made, carrying the AWS Nitro attestation that binds that key to the enclave's measured build, for a nonce you choose.

javascript
import { fetchAndVerifyEat } from 'https://witbitz-spaces.pages.dev/sdk/3.10.0/witbitz-sdk.js'
const r = await fetchAndVerifyEat('https://agents.witbitz.chat/eat', { expectedPCRs: { 0: [/* the build you trust */] } })
// r.ok, r.claims (eat_profile, swname, …), r.doc.pcrs, r.userData.siri: the door's TLS key pin

Split-key rooms

javascript
const wb = new Witbitz({ appShare: '/api/appshare' })   // your function, on your origin
const room = await wb.createSpace({ key: 'split', admission: { emails: [...] } })
room.shareLink   // …#us=<half a key>&spc=<commitment>: the link alone does not open the room

A split room's key is two halves. The link carries one; your function holds the other. The function seals its half under a key only you have (K_eco) before Witbitz stores it, and unseals it only for a page on your own origin. openSpace(link) fetches that half, combines the two, and checks them against the commitment in the link before using the key.

Your function. Copy the template — appshare.js (a plain Request → Response handler), e2ee.js beside it, and the wrapper for your host (cloudflare-pages.js or node-express.js) — and give it two secrets:

Secret What it is
WITBITZ_TENANT_KEY your tenant secret key (the one your server passes as tenantKey): it registers the room and reads back the sealed half
WITBITZ_ECO_KEY K_eco: 32 random bytes, base64 (node -e "console.log(require('crypto').randomBytes(32).toString('base64'))")

If you cannot host a function, leave appShare out and name your tenant (new Witbitz({ tenant })). Witbitz then holds K_eco for your tenant — ask us to turn it on — and serves the half to your declared origins. Everything above holds except the first point: Witbitz could open the half. With neither, the create is refused (tenant_required, split_unavailable or seal_unconfigured) rather than making a room that cannot open.

Vault mode: no key in your page

In vault mode the room keys live in a vault: a small site you host on a second origin of your own (say vault.acme.com), which shows your app (app.acme.com) inside it. Your app uses the SDK as usual and never holds a key.

javascript
// your app page, served on app.acme.com and opened inside the vault
import { Witbitz } from 'https://witbitz-spaces.pages.dev/sdk/3.10.0/witbitz-sdk.js'

const wb = await Witbitz.inVault({ vault: 'https://vault.acme.com' })   // not_in_vault when not framed by it
const space = await wb.createSpace({ agent: { name: 'Assistant' } })    // the key is made in the vault and stays there
space.on('message', (m) => show(m))
await space.send('Hello')

await wb.vault.share(space)            // the vault shows the invite link, which carries the key
const rooms = await wb.vault.rooms()   // [{ room, title, at }]: no links
const again = await wb.vault.openRoom(rooms[0].room)
await wb.vault.signIn()                // the vault's sign-in screen; your page learns the email, never the token

wb.vault is a VaultSession: the vault's origin, what it said when the page connected (info), and these calls — also whoami(), signOut(), forget(space), showSiriKey() and on('rooms' | 'joined' | 'signedIn' | 'signedOut' | 'account', cb).

Building the vault

The vault is a kit you build once and host yourself; Witbitz hosts nothing for it:

bash
curl -O https://witbitz-spaces.pages.dev/sdk/3.10.0/vault-kit.mjs     # Node 18+, no packages
node vault-kit.mjs ./vault --page https://app.acme.com/chat --vault https://vault.acme.com --tenant t_… --brand Acme
Release manifest.json
3.10.0 sha384-yZ7C+jV2zKD4bVPDJuiYJi7CSEqXjCHiuLTp+Mri6WsXl4rrhYDKLqMbBb6r7Sa5
3.9.0 sha384-O54x1kV/gFYQMcY+R1rB4mhzFfc362yuPJl6YO6xlNPDIrWIoOoIF+xBXi0Tubzh
3.8.1 sha384-BOgk3f2wDJetxUQoNcTgb1T+nQzm0TKQ39nbgJch5GSCBUAeGjXhelrQ1tCG8UW0
3.8.0 sha384-3UnAqfkT/reUouZuZ16dTHQs0IP1frHlxW+DfcYmScyrNhxMND7K4PHDYXG5gW0S
3.7.0 sha384-ErXcFYkF4PLKU9pl57Vj+S8GThFSSg93ibg1NaGjmMcihb1vQMpIbf0KCuMV8rdT
3.6.1 sha384-6cvolS5S4MYFbdvvz2Lh5em6wqAWoEO8rs1nnaTY5qF0IzNs+02T6rF5ZKg+9Tye
3.6.0 sha384-TInR2vsZxqAfsnx/sagRDrxbA1UVadEfVnGQ41F4kNuQOjw6oRLMhfNNtN5rvpjf
3.5.0 sha384-IaPuVH2xvt7SR3PaeYWnWvgPK/cPxCF30lqksLNnXElYtACtz4RHOTo+BCLRvjKX
3.4.0 sha384-nkO/egzP6J9E2WK35h0b+WBqB8KdprT8Kb5431tunYmnKcJK/6qk+0gySas9Y1D/
3.3.0 sha384-nFk1GhoYEumCTysMDHAwO5m+wO0uoGykpMSAg0TegXjUFiheqh0BHw6TWTqcr/2+
3.2.0 sha384-czJHGURTz8He5lvVQuPB4ejnl0B29zMyRCFMSZIGWPeEz+ppQMpIrGBQ/22hWdmC
3.1.0 sha384-UxVCdP2RxjJulee/A7DPESm5nLy7jKgedy2/CWTWbOT4Fo1td33THy2e0ndwlqa2
3.0.0 sha384-uvG9uEylZQwDNynITog8HmsiI6kSEMa2zd5xx7p9qIsalUK8Y49riKLDATEagHqR
2.2.4 sha384-kX9P3Te3vMdh0UF6pvM4/0GiYcfR+ot8EBfWzLaLapPPrm1W3vnYbXrPsJqj8SnS
2.2.3 sha384-qC77F7CYYpYb5+nQkeGpruc0myKyPP9GYLe+IyMrjDu2qQQsnch9ef9a+ALTdHxX
2.2.2 sha384-tNPte4rW41HFwG9r/UoP1YKz1HKNrNLE2apSdKtbs8M90q+ZJkiHoT7FVgnnJRe0
2.2.1 sha384-jnKIjgueEQsAfvA8xsEqaXhE2HJnR/lOrrjOUmQmJr4eJQM3gHT+qzX0TfrcpVy4
2.2.0 sha384-DZL9h9OKKg/UKsmpajI7XPuUwF2FOqPq1Ir6othFmMKDLt5qlt++zdebDV+lp0yV
2.1.2 sha384-yJcmJ7MH5JBasfLeu0QXTUjkO7WvBOAfpblhXnWyhPeMQXgnf33VOHtFrdXeeiQO
2.1.1 sha384-J++TLx7Yhl2bxeLklJGsrHBTB74kk3/k3mUfUQYVs1fESrdGDal0wx5Gcry5irld
2.1.0 sha384-rYHqCNRtVUN5lh2DXbvPbuwYYsuUMKyRlpOYm1fuiPhkgOKJ9W69evU9Kl2tD++p

What it protects, and what it doesn't

The SDK inside a vault

Everything the SDK offers works in a vault, except cloud computers. What differs:

In a vault
Links openSpace refuses (vault_opens_links) — the vault opens links. wb.vault.openRoom(room) takes a room id from wb.vault.rooms(); a room the vault does not hold is not_in_vault. openLane takes the lane's room id, not a link; wb.vault.rooms() marks a lane with lane: <its room>.
Sign-in wb.signIn refuses (vault_signs_in); use wb.vault.signIn().
Voice wb.dictation(), wb.speak() and wb.speech get their short-lived speech key from the vault (its sign-in, or your wb.useUserToken()).
Live updates Replies stream as they are written, as outside a vault: the socket is the vault's, since it needs the key, and the page gets its events.
Lanes space.privateLane() works as outside: the vault makes the lane, keeps its key and records it as that room's lane, and the lane reads and posts into the room through the vault. setLinks, link and sealedProof() work too.
Confidential rooms createSpace({ tier: 'enclave' }) works: the vault checks the enclave against Witbitz's published build and seals the key to it — only to a build Witbitz publishes. A lane of a confidential room is confidential; wb.vault.rooms() marks one tier: 'enclave'. Serve the vault with connect-src allowing the claim's origin (the kit's headers do).
Split-key rooms createSpace({ key: 'split' }) works when your vault serves your app-half function at /api/appshare on its own origin and is built with --app-share (or startVault({ appShare: true })); otherwise vault_unsupported. The vault keeps only the person's half in its link and asks the function for the other.
Direct chats createSpace({ direct: email }) works, with the vault's signed-in member as owner.
Members changeMembers and rotateKey go through the vault, which asks the person (members).
Backup and account wb.backup, wb.account and wb.erasure are the vault's: it records every room it holds, keeps the account's list in step across devices, and files the erase list. The recovery code is shown and typed only in the vault's screens: wb.backup.enable() resolves { enabled: true } with no code, and restore() opens the vault's restore screen and resolves { rooms }. The getters are the vault's last view, never a link or a key: state is null, keyFor() refuses, add() does nothing. One backup per account — another person signing in on the same vault sees none of it. On wb.account, archive, rename, forget and signOut return promises, and forget and signOut ask the person first; wb.backup.remove(room) returns a promise too (the vault asks the person; a no rejects with cancelled).
Connections Connections, @usernames, the Siri inbox and outside agents work through the vault's sign-in, a lane's too. wb.connections.attach() returns { id, label, host, account }, plus mcp, webhook and job when the connection has them — the callKey stays in the vault — and space.connections.grant(conn) grants by id. MCP servers (startMcp, attachMcp, finishMcp, mcpTools) run through the vault the same way; startMcp's sign-in comes back to your page's /mcp-callback unless you pass redirectUri.
Siri wb.siri.state is null and restore() refuses: the vault keeps the inbox's private keys. wb.siri.shortcut.key is null, because that key posts words into the person's rooms; the vault shows it in its own screen, wb.vault.showSiriKey(), and rotateKey() resolves null. space.siri.collect({ deliver }) hands each drop to your deliver.
Outside agents wb.a2a.state is null (the vault keeps the lanes' inbox keys); lane.a2a.cardUrl, .mcpUrl and .peers are what the vault last said; addPeer returns { id, token, cardUrl, mcpUrl } as outside a vault. The vault asks the person before an outside agent may reach a lane, and before its answers may go without them.
Moves An older client's move is followed by the vault, and the page gets 'moved' with { room, link: null }; a confidential room's move is refused (vault_unsupported), and one whose new room cannot be read yet gives 'moveRefused' with code: 'unreadable' and nothing is followed.
Cloud computers Not in a vault yet: every wb.computers call rejects with vault_mode, because the computer's keys would have to sit in the page.

Machine-readable source: sdk-keys.md · Generated 2026-10-27T07:05:42Z · build f301388c · every doc in one fetch: llms-full.txt (HTML)