Transport Accounts
CLEMA is one of several protocols a server may speak. An adapter for a protocol that carries a credential of its own — OSCAR (ICQ/AIM), for instance — cannot authenticate with CLEMA tokens, because its clients have nowhere to put one. Such an adapter instead publishes its account rules, and the user manages the account over CLEMA itself: choosing its password and switching it on and off from any CLEMA client.
An account exists per (user, transport) pair — one UIN, one account — and carries:
- an ID, assigned once when the account is created and never reissued. On OSCAR it is the user's own ID, which is why IDs are capped at ten digits: a user ID is already a valid UIN. Other transports will draw identifiers from namespaces of their own.
- a password, chosen by the user. A CLEMA account has none of its own: this is the only password it has, and it opens that transport alone. The server stores only the material the adapter needs to verify it and never returns the password, not even to its owner: it can be replaced, not read.
- an enabled flag, which withdraws and restores access without giving up the ID.
Mandarin tells the account when its password is set, changed or removed, when a client signs on from an address it has not signed on from for a month, and when wrong passwords come thick; a code to sign in with it shows no transport's client.
Only adapters this server actually runs are listed and can be acted on. The password rules are the adapter's: an ICQ client accepts 6 to 8 characters and nothing longer, so each transport publishes its own bounds for a client to enforce before sending.
List Transports
{"id":"transports-1","type":"transport.list"}
{
"id":"transports-1","type":"transport.list","ok":true,
"data":{"transports":[
{"transport":"oscar","title":"ICQ / AIM","id":"123456","enabled":true,"password_set":true,
"password":{"min_length":6,"max_length":8},
"created_at":"2026-08-05T10:00:00Z","updated_at":"2026-08-05T10:00:00Z","last_used_at":"2026-08-06T09:12:00Z"}
]}
}
| Field | Type | Description |
|---|---|---|
transport |
string | Adapter name, the value used by every other command here |
title |
string | Human-readable name for display |
id |
string | Account identifier; absent until the account is created |
enabled |
boolean | Whether the transport currently accepts this account |
password_set |
boolean | Whether a usable secret is stored |
password |
object | The adapter's min_length/max_length, counted in characters |
created_at, updated_at |
string | RFC 3339, absent before creation |
last_used_at |
string | RFC 3339 of the last successful login through the adapter; absent until then |
Every registered adapter appears, whether or not it has an account: an empty
id is what "not set up yet" looks like. Adapters are listed in the order the
server registered them, so a settings screen keeps a stable layout.
Create an Account
{"id":"transports-2","type":"transport.setup","data":{"transport":"oscar","password":"s3cret"}}
The response, and that of every other command below, is the whole record:
{"id":"transports-2","type":"transport.setup","ok":true,
"data":{"transport":{"transport":"oscar","title":"ICQ / AIM","id":"123456","enabled":true,"password_set":true,"password":{"min_length":6,"max_length":8},"created_at":"2026-08-05T10:00:00Z","updated_at":"2026-08-05T10:00:00Z"}}}
The account is created enabled. A second transport.setup on the same
transport returns conflict rather than silently replacing the password —
changing it has its own command.
Change the Password
{"id":"transports-3","type":"transport.password","data":{"transport":"oscar","password":"s3cret2"}}
The ID and the enabled state are unaffected. An authenticated CLEMA session is authority enough.
Enable and Disable
{"id":"transports-4","type":"transport.disable","data":{"transport":"oscar"}}
{"id":"transports-5","type":"transport.enable","data":{"transport":"oscar"}}
Disabling refuses new logins and immediately closes the live ones made through
that adapter, which are deauthenticated exactly as session.revoke would leave
them. The ID and the stored secret are kept. Enabling an account whose password
was reset returns invalid_state: there would be nothing to authenticate
against, and reporting success would suggest access had been restored.
Reset
{"id":"transports-6","type":"transport.reset","data":{"transport":"oscar"}}
Discards the stored secret and disables the account, keeping the ID reserved for
the same user. Use it to withdraw a stored password-equivalent outright; the
account comes back with transport.password, under the same ID.
An ID is never released, even by a reset. Handing a retired identifier to somebody else would let them inherit the conversations and contact-list entries that still point at it.
Sessions Opened Through a Transport
A client signing in through an adapter gets an ordinary session, listed by
session.list with transport naming the adapter, and revocable like any
other. It differs in one way: the session lasts exactly as long as the
connection. Such a client stores its password rather than a token, so a session
that outlived its connection could never be resumed and would only consume the
account's session budget. Closing the connection ends the session; signing in
again opens a new one.
Idle handling belongs to the adapter too. These clients send no CLEMA requests, so the inactivity timeout that counts them does not apply; the adapter enforces whatever its own protocol provides.
A client that was away is given what it missed when it signs on again: the messages still unread. Reading is the account's own state, shared by every device it signs in from, and handing a message over counts as reading it, since the client puts it on screen; the account's other devices are not shown it again.
Whether a request is answered follows one rule, and adapters depend on it: a
request carrying an id is always answered, successfully or not, while one
without an id is never answered. The second form is how a signal nobody waits
for is sent — typing.set is the only one today — and a caller that waits on it
waits forever.
An adapter declares which message types it can render, in place of the
connection.hello negotiation its protocol has no room for. The server sends
it no other signal; the events of its user's streams all reach it, as they
reach any connection, and the adapter passes over what its protocol cannot
show — reactions, edits, groups — which is how a client that has no notion of
them simply never hears about them. What such a client does not support is not
degraded — it is absent.
Updates
The account owner's other connections receive transport.updated after every
change:
{"type":"transport.updated","data":{"transport":{"transport":"oscar","title":"ICQ / AIM","id":"123456","enabled":false,"password_set":true,"password":{"min_length":6,"max_length":8},"created_at":"2026-08-05T10:00:00Z","updated_at":"2026-08-06T12:00:00Z"}}}
It carries the whole record, so it is idempotent and a client never merges a
partial update. It is not a durable event and carries no seq: the list is
short, fully re-readable with transport.list, and belongs to no
stream. Clients call transport.list after authenticating,
just as they call session.list.
Errors
| Code | Cause |
|---|---|
invalid_format |
No transport given, or a password outside the adapter's bounds or unusable on it |
not_found |
Unknown transport, or a command that needs an account when none exists |
conflict |
transport.setup on a transport already set up |
invalid_state |
transport.enable on an account with no password |