Client generates an identity on first run, persists it to your keystore, fetches conversation keys, and encrypts and decrypts message bodies for you. A normal bot never imports anything on this page.
These exports exist for the cases that fall outside that: building a frame by hand through client.raw, attaching a voice media session, storing your keystore somewhere unusual, or writing a startup check.
The model, in one screen
Message bodies are encrypted with AES-256-GCM under a conversation key your bot holds. The server stores ciphertext and cannot read it.Conversation keys are wrapped with RFC 9180 HPKE against your bot’s published identity. The class that does this is still called
SignalSession for historical reasons. The Signal Protocol and libsignal are not involved.Voice frame keys are a separate slot. A bot never derives one. A human member seals one to your bot’s identity and the SDK fetches it. See Voice keys.Runtime guard
The crypto stack needsglobalThis.crypto.getRandomValues, which Node 18 does not define in ESM. Without it, key generation and message encryption fail at runtime.
SUPPORTED_NODE
engines.node. Read it literally: 20.x needs 20.19.0 or newer, 21.x never qualifies, 22.x needs 22.12.0 or newer, and 23 and up qualify.
This is a crypto requirement, not a transport one. The default lane is a plain
wss:// WebSocket over the pure-JavaScript ws package, and npm install compiles nothing.assertSupportedRuntime
CloakEnvironmentError with a remedy when the runtime is out of range. login() calls it as its first statement, so you rarely need to. Call it yourself in a startup health check if you want the failure earlier than your first login.
generateDependencyReport(). See Troubleshooting.
Keystore adapters
Where your bot’s identity and keys are persisted. Pass one asClientOptions.keystore, or pass a path as keystorePath and get fileKeystore for free.
KeystoreAdapter
() => Promise<string | null>
The stored keystore text, or
null when nothing is stored yet.(contents: string) => Promise<void>
Persists the full keystore text. Implementations should be atomic.
() => string
Optional human label used in operator-facing errors, such as
file ./bot.keystore.json.fileKeystore
0600 permissions. keystorePath: './bot.keystore.json' is shorthand for this.
envKeystore
EnvKeystoreOptions is exported, so you can name it in your own signatures.
A keystore held in an environment variable, for hosts with no writable volume. Reads either raw JSON or base64; writes always emit single-line base64 that is safe to paste into a config field. The variable defaults to CLOAK_KEYSTORE, exported as DEFAULT_KEYSTORE_ENV.
Voice keys
You reach these throughclient.voiceKeys, not by constructing anything.
VoiceKeyScope
'participant' means your bot holds only its own frame key: it can publish, and it can decrypt nobody. 'room' means your bot was granted voice_listen and holds the room key, from which a frame key for any participant is derived.
VoiceKey
Buffer
32 bytes whose meaning depends entirely on
scope, which is why the two never travel apart.number
The server’s voice key version this was sealed at. It survives a message-key rotation and changes only on a root cycle.
string
The human member who sealed it for your bot.
voiceKeyUpdate and re-pin when either the version or the scope changes. Never poll.
deriveParticipantVoiceKey
client.voiceKeys.voiceFrameKeyFor(serverId, identity) instead: it hides the scope, which is the point, because scope is invisible on the publish path and a mistake there is silent.
The derivation only goes downward, from the room key to a participant key. There is no function that walks back up, in this SDK or anywhere. A publish-only bot’s deafness is the absence of that path, not a check that refuses.
Lower-level exports
You need these only when building frames by hand.messageCipher
encryptToWire(key, text, epoch) and decryptFromWire(key, wire) are the two functions that matter, alongside wireEpoch(wire), which tells you which key epoch a stored value was encrypted under.
SignalSession
The name is historical. The engine is RFC 9180 HPKE, not the Signal Protocol and not libsignal, which the SDK no longer ships or depends on. The class kept its old name so existing keystores and imports keep working.
Client constructs and drives one. Constructing your own is only useful if you are reimplementing key management.
WrapContext
wrapKey() and unwrapKey().
KeyManager
Client builds one from your keystorePath or keystore option and exposes the voice-key part of it as client.voiceKeys.
Construction performs no I/O. await load() must run before anything reads or writes identity state, which Client.login() does for you. An unloaded manager refuses to write, because writing from empty state would replace a real keystore with one that has no identity.
KeyManagerOptions
keystorePath and keystore are mutually exclusive: supplying both throws. Omitting both gives an in-memory-only manager, which is fine for tests and fatal for a real bot. The callbacks are how Client wires key events into its own event surface, so a bot that lets Client build the manager never sets them.
Next
Keys and the keystore
Why every bot needs one, and what breaks without it.
Encryption lanes
What is encrypted, and the two exceptions.
Voice keys
Scopes, re-pinning, and why a bot can be deaf.
Keystore backends
Running without a writable volume.