// Versioning: the broker matches a plugin's imports and exports to these
// types by structure, so any change to a function, record, variant, enum or
// flags breaks plugins built for the old shape. New hooks, fields or cases
// therefore need a new minor version. Releases 0.2.x change only
// documentation. The broker keeps loading plugins built for 0.1, whose
// interface is frozen in `mercuriod/wit-0.1`.
package mercurio:plugin@0.2.0;
interface types {
enum level { trace, debug, info, warn, error }
enum access { read, write, subscribe }
enum decision { allow, deny, defer }
enum qos { at-most-once, at-least-once, exactly-once }
/// The connection a client came in on.
record connection {
/// The listener's address, such as `0.0.0.0:1883`, or its Unix socket
/// path; `local` for clients inside the broker's process
listener: string,
/// The MQTT version of the CONNECT: 3 for 3.1, 4 for 3.1.1, 5 for 5.0
protocol-version: u8,
tls: bool,
/// The name taken from the client's certificate, on a listener that
/// takes user names from certificates
certificate: option<string>,
/// The listener's mount point. It prefixes every topic the client
/// uses, and topics reach plugins with it.
mount-point: option<string>,
}
/// `username` is set only for a user the broker verified, except in
/// `authenticate` and in `auth-step` during CONNECT, where it is the user
/// name the client claims.
record client-info {
client-id: string,
username: option<string>,
peer: option<string>,
/// None where no connection is known, such as for the publisher of a
/// retained message
connection: option<connection>,
}
/// A published message. The properties are MQTT 5's application
/// properties; MQTT 3.x messages have none.
record message {
topic: string,
payload: list<u8>,
qos: qos,
retain: bool,
payload-is-utf8: bool,
message-expiry: option<u32>,
content-type: option<string>,
response-topic: option<string>,
correlation-data: option<list<u8>>,
user-properties: list<tuple<string, string>>,
}
/// `replace` carries the whole changed message.
variant message-action { keep, drop, replace(message) }
record disconnected { client: client-info, reason: option<u8> }
record subscribed { client: client-info, filter: string, qos: qos }
record unsubscribed { client: client-info, filter: string }
/// A session that ended as its expiry passed. The same client id can
/// have a session under each mount point, so both name it.
record session-expired { client-id: string, mount-point: option<string> }
variant event {
connected(client-info),
disconnected(disconnected),
subscribed(subscribed),
unsubscribed(unsubscribed),
session-expired(session-expired),
}
/// The hooks a plugin implements; the broker calls no others.
flags hooks {
authenticate,
authorize,
on-message,
on-event,
on-tick,
on-control,
authorize-control,
}
record registration {
hooks: hooks,
/// MQTT 5 enhanced authentication methods it answers `auth-step` for
auth-methods: list<string>,
}
/// What `continue` sends: `data` goes to the client, and the broker
/// keeps `state` for the next step.
record challenge { data: list<u8>, state: list<u8> }
/// What `success` accepts the client as: the user it names, if any, and
/// `data`, such as SCRAM's final server message, sent to the client as
/// Authentication Data in the CONNACK or in the AUTH that ends a
/// re-authentication.
record success { username: option<string>, data: option<list<u8>> }
/// What `failed` refuses the client with: an MQTT reason code, and a
/// short text saying why, sent as the Reason String of the CONNACK, or
/// of the DISCONNECT that ends a re-authentication, unless the client
/// asked for no problem information or it doesn't fit the client's
/// Maximum Packet Size. Anyone connecting can read it, so it must not
/// tell, for example, whether a user exists.
record failure { reason: u8, reason-string: option<string> }
/// The result of one step of enhanced authentication. `continue` sends
/// a challenge to the client; `success` accepts it; `failed` refuses it.
variant auth-result { %continue(challenge), success(success), failed(failure) }
}
interface host {
use types.{level, message};
log: func(level: level, message: string);
/// A store shared by every copy of the plugin, kept in memory until the
/// broker stops. It is shared but not transactional: a get followed by a
/// set can race with another copy.
kv-get: func(key: string) -> option<list<u8>>;
/// Fails when the store refuses the value, for example over a quota.
kv-set: func(key: string, value: list<u8>) -> result<_, string>;
kv-delete: func(key: string);
kv-list: func(prefix: string) -> list<string>;
/// Publishes `message` as the client `mercurio-plugin-<name>`, inside
/// the broker, which skips access control. It returns once the message
/// is queued; it reaches subscribers shortly after, once the broker
/// serves clients. It fails, and nothing is published, for a topic that
/// starts with `$` or isn't a valid topic name, for a message over
/// 64 KiB counting its topic, payload and properties, and when the
/// plugin publishes faster than its rate limit allows: bursts of 100,
/// 100 messages a second on average, for all copies together. The other
/// plugins' `on-message` see the message; the publishing plugin's own
/// does not.
publish: func(message: message) -> result<_, string>;
}
interface guest {
use types.{client-info, access, decision, message, message-action, event, registration, auth-result};
init: func(config: list<tuple<string, string>>) -> result<registration, string>;
authenticate: func(client: client-info, username: string, password: list<u8>) -> decision;
/// `state` is None on the first step of an exchange. During CONNECT,
/// `client` carries the user name the client claims; on
/// re-authentication, the verified user.
auth-step: func(client: client-info, method: string, state: option<list<u8>>, data: option<list<u8>>) -> auth-result;
authorize: func(client: client-info, topic: string, access: access) -> decision;
/// Whether `client` may publish to `topic`, a `$CONTROL/...` topic,
/// which administers the broker or a plugin. A request to
/// `$CONTROL/broker/v1` is asked about twice: for that topic, then for
/// each of its commands as `$CONTROL/broker/v1/<command>`. When every
/// plugin defers, the client is refused.
authorize-control: func(client: client-info, topic: string) -> decision;
on-message: func(client: client-info, message: message) -> message-action;
on-event: func(event: event);
on-tick: func();
on-control: func(client: client-info, path: string, payload: list<u8>) -> option<list<u8>>;
}
world plugin {
import host;
export guest;
}