client.rest is the narrow relaxation of that rule.
What it does
The bot mints a short-lived, scoped credential over its already-authenticated realtime session and uses that credential for HTTP. The long-lived bot token stays where it is used once, at login. It never rides an HTTP header, never reaches a proxy log, never reaches a crash dump.
The credential is minted, cached and refreshed for you. There is no accessor for it. It is never logged, not even under
debug: true, and never written to the keystore.
Why the mint takes two round trips
Getting the credential is two phases, and the second one cannot be collapsed into the first.1
Opcode 638 over the realtime session
The backend answers with a one-time secret plus the row ids it belongs to. The secret leaves the server exactly once, and the server stores only its hash.
2
POST /auth/verify-session
That exchanges the one-time secret for the
verification header every later request carries. This endpoint is deliberately not behind the auth middleware, which is what makes it usable as the bootstrap.The second phase is not overhead you can optimize away. The REST service accepts a request when the
verification header equals a digest that hashes a JavaScript Date’s toString(), rendered in the REST process’s own timezone. Neither the SDK nor the backend can reproduce that string. Any “optimization” that derives the digest locally is a timezone-dependent, silently wrong bug.Not every route is reachable when it is on
The credential is scoped, behind a default-deny allowlist. The backend’s route classification marks file upload, delete and permissions as intended for bots. It marks both obvious read surfaces,GET /api/emoji/rooms (custom emoji) and GET /api/audit-log/:roomId, as unauthorized: neither route does per-resource authorization today, so a scoped credential must be refused them.
That is why the SDK ships no fetchCustomEmojis() and no fetchAuditLog(). Named methods that always fail would be worse than none. If the allowlist opens those routes, they get real wrappers then.
client.fetchEmojis() is unrelated and works fine. It rides opcode 240 and returns the static unicode catalog, never per-server custom emoji.Errors
401 mid-session is expected and handled. Revoking a bot purges its credential rows, so the lane drops the credential, re-mints once, and retries once. A second 401 throws rather than looping: an unconditional retry against a revoked bot burns the mint rate limit and wedges.
A -7 is not your bug
The message the SDK raises says it plainly:bot-rest token scope, and that middleware is not built yet. If you need file storage or the audit log today, the answer is that a bot cannot reach them, not that your configuration is wrong.
Pointing at another stack
restUrl defaults to DEFAULT_REST_URL (https://api.cloak.chat). That is a different service from the opcode wire, so url and restUrl are set independently.
Next
REST client reference
Every type and constant on the lane.
Handling errors
CloakRestError next to the other four classes.Raw frames
The other escape hatch, on the opcode wire.
Known limitations
What a bot cannot do today, and why.