mercuriod 0.5.0

Mercurio MQTT broker daemon
// 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;
}