Client. You construct it with your token, attach event listeners, and call login().
serverId as their first argument. If a handler does several things in one server, guild(serverId) binds that id once so you can drop it from every call.
Constructor options
Identity and storage
string
required
Your bot token, in the form
botid.tokenid.secret. Create it in the Cloak app under Settings > My Bots. Treat it like a password. Read it from an environment variable, never hardcode it.string
Path to a file where the SDK persists your bot’s identity, its conversation keys, and its peer pins. Shorthand for
keystore: fileKeystore(path). Mutually exclusive with keystore: passing both throws.A bot with no keystore of any kind is single-run only. Its second start fails permanently, because identity publication is write-once on the server. See Keys and the keystore.KeystoreAdapter
Where to persist identity and keys when a file will not do. Use
envKeystore() on a host with no writable volume, or supply your own adapter. Mutually exclusive with keystorePath. See Keystore backends.string
A stable device id used for clean takeover on reconnect. Defaults to a value derived from your token. Set it only if you have a specific reason to.
Behavior
PermissionName[]
The permissions your bot declares it needs, sent to the server on login. Declaring is a request, not a grant. A human approves a subset per server, and the resolved grant arrives on
permissionsUpdate. See Permissions.string
default:"/"
The prefix the text lane of the slash-command parser matches. Only the text lane uses it. A structured invocation carries the command name outright and is prefix-independent. See Slash commands.
string
default:"Bot"
Display name for the webhook that
sendEmbed() mints. Cosmetic only. Reuse is matched on creator plus channel, never on name, so renaming this does not orphan an existing webhook.WebhookFetch
An injected
fetch for the webhook ingress, for tests or a proxy. Defaults to globalThis.fetch. This is the only outbound HTTP the SDK performs on its own.boolean
default:"false"
Logs every frame the SDK sends and receives, with your bot token redacted. Useful while developing, noisy in production.
Endpoints and transport
Advanced: self-hosted, local dev, and lane selection
Advanced: self-hosted, local dev, and lane selection
Bots against the hosted Cloak service rarely set any of these.
string
The connection endpoint. Defaults to the hosted service on the lane implied by
transport: DEFAULT_URL for 'ws', DEFAULT_WEBTRANSPORT_URL for 'webtransport'. A wss:// url speaks the WebSocket lane, an https:// url the WebTransport lane.'ws' | 'webtransport'
default:"ws"
Which wire to speak. The default WebSocket lane rides the pure-JavaScript
ws package, so npm install compiles nothing. 'webtransport' needs the optional Quiche packages installed. See Installing.boolean
default:"false"
Permit a plain
ws:// url, which sends the bot token in cleartext. Local development stacks only.number
default:"60000"
Treat the connection as dead after this long with no inbound data, then reconnect. Defaults to
60000 on the WebSocket lane and is disabled on WebTransport, which has its own QUIC idle timeout. 0 disables it.{ algorithm: 'sha-256'; value: Uint8Array }[]
WebTransport lane only: a self-signed QUIC certificate pin for a self-hosted or local stack. Throws on the WebSocket lane. Not needed for the hosted service.
string
The REST base that
client.rest talks to. Defaults to DEFAULT_REST_URL. HTTPS only, except loopback. A bad value throws on the first client.rest call.string
The LiveKit signalling url handed back on
VoiceConnection.url. Defaults to livekitUrlFor(<transport url>), which is the same host on port 7880. The SDK never connects to it. See Joining voice.Properties
BotUser | null
Your bot’s identity after login, or
null before login() completes. Its id is normalized, so you can compare it against msg.authorId to detect your bot’s own messages. See BotUser.CommandRegistry
The bot’s slash-command registry.
register(decl, handler) is synchronous.Register before login(). Whatever is registered at that moment is published as the bot’s menu. A command added afterwards still dispatches locally, but it does not appear in the published menu until the next login, and the SDK never re-publishes on its own. See Slash commands.WebhookApi
The incoming-webhook surface:
list, create, edit, regenerate, delete, and post. The five CRUD calls require manage_server. post(url, payload) requires nothing at all and is the recommended path. See Webhooks.RestApi
The authenticated REST lane:
request, json, and binary. Every call transparently mints a short-lived credential over the realtime session, so your long-lived bot token never rides an HTTP header.This is off by default server-side, so the first call usually rejects with a CloakActionError carrying code -7. See REST.RawApi
The escape hatch to opcodes the SDK does not map:
send(frame, opts?) and request(frame, replyOpcode, opts?). Raw frames run on the same serialized chain as every mapped action.request() refuses reply opcodes the SDK correlates internally unless you pass { allowReserved: true }, because awaiting one can claim a reply the SDK was waiting for. See Raw frames.The voice-key lane, and nothing else:
getVoiceKey, acquireVoiceKey, voiceFrameKeyFor, and forgetVoiceKey. Hand this to a media session so it can install frame keys without reaching your conversation keys. See Voice keys.This accessor’s interface type is intentionally not exported, so annotate with typeof client.voiceKeys if you need a name for it.Lifecycle
login
guildCreate for existing servers, then emits ready. Call it once, after attaching your listeners and registering your commands.
watch
groupId is auto-resolved from the server’s channel list when omitted. The target is remembered and re-established after a reconnect.
destroy
Sending
send
opts.groupId is auto-resolved for any channel the bot has already seen, so you usually omit it.
SendOptions is { groupId?, mentions?, replyTo? }.
send() is fire-and-forget. The wire has no success acknowledgement for it, so the promise resolves once the frame is out. A server-side denial does not reject the promise: it arrives later on the sendRejected event. Silence does not mean delivered.Error when your bot does not hold the server’s conversation key yet, which happens in the moments right after a first join.
Most handlers use msg.reply() or msg.channel.send() instead, which already know where the message came from. See Sending messages.
sendDM
CloakActionError before anything is encrypted.
Inside a DM, replyTo works, and role, @everyone, and @here mention pairs are dropped server-side. See Direct messages.
sendCard
message_send and nothing else.
card.content is required and must be non-blank. Card media must be Cloak-hosted, and anything else throws a CloakEmbedError locally before a frame leaves your process. See Rich cards and Embeds.
sendEmbed
manage_server.
sendCommand
opts.botId names the target bot and has no default. Targeting a foreign bot also requires opts.decl, because the SDK cannot know another bot’s option order. See Slash commands.
sendTyping
isTyping defaults to true. The SDK never sends this implicitly, not even around send().
Receivers clear the indicator themselves about ten seconds after the last true, so re-send it every few seconds during long work. A repeat of the same boolean for the same channel inside about a second is dropped without touching the wire. It moves the server-side selection cursor exactly like a send does, and it is fire-and-forget. See Typing, presence, and profile.
Presence and profile
setStatus
BotStatus is 'invisible' | 'online' | 'away' | 'busy' | 'dnd'. dnd also suppresses push notifications on every device.
This writes an account-level status to a database column, so it survives reconnects, restarts, and redeploys. Set it once on ready, never on a timer. Rejects with a CloakActionError if the server does not acknowledge it.
online means “defer to liveness” rather than “show a green dot”. A bot with no live session displays as offline whatever its stored base says. The other four values pin the display outright.setAvatar
miniIconUrl defaults to iconUrl. Passing null clears both. Urls are capped at 512 characters server-side. The SDK never uploads images, because file upload is closed to bots.
History and reads
fetchMessages
message_read_history. Cursors are a Date or a Message, never a message id, because Cloak message ids are random UUIDs and not time-sortable. around returns a single window and is not auto-paged.
Content decrypts per row against the key for that row’s own epoch, and is '' (never raw ciphertext) when the key is missing. Returned messages are fully actionable and carry pinned. See Message history.
messageLocation
undefined. This is a bounded, best-effort cache. See MessageLocation.
fetchEmojis
serverId because the catalog is server-agnostic, and it does not move the selection cursor. Its ids are what msg.react() accepts. react() itself stays an opaque pass-through with no client-side validation. See Reactions.
fetchRoles
fetchBans
member_ban, and some older Cloak servers also require owner or admin standing. Usernames are resolved server-side and may be null. See BanInfo.
fetchMembers
Moderation
All of these are server-scoped and reject withCloakActionError on a server deny. See Moderation.
kick
member_kick, a rank above the target, and the target not being the owner or the bot itself.
ban
member_ban. reason is capped at 256 characters server-side.
unban
member_ban. Some older Cloak servers also require owner or admin standing.
timeout
durationSeconds. A timed-out member cannot send or react in that server. Passing 0 lifts the timeout. Requires member_timeout.
timeout() needs an up-to-date Cloak server. An older server drops the opcode, so the call times out rather than resolving.removeTimeout
timeout() with 0 seconds.
Channels and groups
All of these requiremanage_channels and reject with CloakActionError on a server deny. See Channels and groups.
createChannel
channelType defaults to 0, a text channel.
editChannel
deleteChannel
moveChannel
newOrder is clamped to the range 0 to 126. Moving to the same group and order is a no-op the server rejects.
createGroup
undefined case.
editGroup
deleteGroup
There are no methods to edit or delete a server. Those actions are owner-level and surface only as the
serverUpdate and serverDelete events.Voice
This is the control plane. It mints a LiveKit credential and stops there. The SDK sends no media of its own. See Voice overview.joinVoice
VoiceConnection the backend minted, including the LiveKit token and url. Requires voice_connect server-wide plus channel-level view.
opts.groupId must be the channel’s real group and must never equal the channel id. That shape is Cloak’s DM-call form, and the SDK refuses it locally rather than round-tripping a rejection. The group is resolved from the server’s channel list when omitted.
leaveVoice
voiceConnection
null. A synchronous cache read.
setVoiceSelfState
selfDeafened is a broadcast flag, not an answer to “can I hear”. Whether the bot receives audio is decided by voice_listen and the scope of the voice key it was handed.voiceRoster
refreshVoiceRoster
Voice roster events are cursor-gated, not firehose. Your bot sees them only for the server it currently has selected or is in voice in. Do not build on them as a firehose.
Permissions
These are advisory. They reflect the last grant the server sent, and the server re-checks every action. See Permissions.can
permission in serverId per the last grant. administrator implies every permission here. Returns false when no grant has arrived yet, so a false means “not permitted, or not known yet”.
permissions
undefined when no grant has arrived. See PermissionSet.
visibleChannelIds
Encryption helpers
You need these only when you are building a frame by hand throughclient.raw.
encryptFor
send() would put on the wire. Throws when the bot holds no key for that server.
decryptFrom
'', never raw ciphertext, when the key is missing or stale.
Events
Client extends Node’s EventEmitter, and on, once, off, addListener, prependListener, prependOnceListener, removeListener, and emit all carry typed overloads over ClientEvents. Your listener arguments are inferred.
error event instead of vanishing. That means client.listeners(event) returns the wrappers rather than your original functions. off() and removeListener() still work normally: pass the same function you registered.
guild
serverId onto every server-scoped method. Pure sugar over the flat methods, with no cache and no state.
id, plus:
See The guild() handle.
Exported constants
string
Cloak’s hosted WebSocket endpoint, used when you omit
url.string
Cloak’s hosted WebTransport endpoint, used when you omit
url and set transport: 'webtransport'.string
The default origin for
client.rest.(transportUrl: string) => string
Derives the LiveKit signalling url from a transport url: same host, port 7880. Returns
'' for an unparseable url. This is what livekitUrl defaults to.Next
Events
All 32 events a Client emits.
Types
Message, SendOptions, Member, and the rest.
Errors
Every error class an action can reject with.
The guild() handle
Bind a server id once.