client.raw lets you reach them without vendoring the SDK, and the raw event lets you read every inbound frame, including the ones the SDK consumes itself.
Reach for this when a newer backend grows an opcode before a release maps it, or when your bot needs a subsystem the SDK does not cover (calendar channels are the usual example).
The surface
RawApi and RawTarget are exported types. The frame parameter is typed Frame in the source, which is an alias for unknown[].
Aiming a frame
Cloak’s protocol has a server-side selection cursor: many opcodes act on “the currently selected server and channel” rather than taking ids.RawTarget moves that cursor the same way every mapped method does.
Everything runs on the same serialized action chain as
send() and every mapped method, so a raw frame can never interleave with another action’s select and execute pair.
The unwrapping rule
request() resolves the payload the way the protocol layer produces it: the opcode is stripped, and when exactly one element remains it is unwrapped to that scalar.
This is the single most surprising thing about the surface. A handler that always returns an array will still hand you a bare number on the one-element case, so normalize before you index:
Reserved reply opcodes
Reply correlation is positional by opcode. The protocol has no request ids, so a waiter on an opcode the SDK correlates itself can claim, or lose, the SDK’s own reply.raw.request refuses those opcodes before anything touches the cursor or the wire:
RESERVED_REPLY_OPCODES, a ReadonlySet<number>. Check it before you write a request:
raw.send is unrestricted, because a send registers no waiter.
Passing allowReserved: true is a deliberate risk transfer, not a workaround. You own the correlation from that point on.
The raw event
Carrying encrypted content
Message bodies on Cloak are end-to-end encrypted, so a raw frame that carries user-visible text has to encrypt it the same waysend() does. Two helpers wrap the exact same lanes, with no separate crypto:
string
The same wire form
send() produces, tagged with the server’s current key epoch. Throws when the bot holds no key for that server.string
Decrypts with the key for the ciphertext’s own epoch. Returns
'', never raw ciphertext, when that key is missing or stale.You own the frame
client.raw is not covered by the SDK’s compatibility promises. The frame shape, the opcode constant, and any encryption of the payload are yours. Nothing is validated on the way out.
That is the trade. A mapped method survives a backend change because the SDK updates with it. A raw frame is your contract with a specific backend version, and it breaks silently when that version moves.
Practical hygiene:
- Keep opcode numbers in one constant block in your bot, not scattered through handlers.
- Prefer
Opfrom the SDK for any opcode that is already named there, so you inherit the SDK’s own audit of it. - Treat every payload as hostile input. Validate arity and types before you index.
- If the SDK later maps the opcode, move to the mapped method.
Next
Protocol constants
Op, the exported column maps, and the reserved set.The REST lane
The other escape hatch, over HTTP.
Message delivery
Why the cursor exists and what the firehose sends you.
Encryption lanes
What is encrypted, and what rides in plaintext.