CLEMA protocol

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:

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