RawtohRawtoh/Docs
Documentation

Créer le vôtre

Créez un module sur mesure pour connecter n’importe quel outil ou service à Rawtoh.

Un module est un programme, quel qu’il soit, qui se connecte à Rawtoh via WebSocket. Il peut émettre des événements (ce qui se passe) et recevoir des appels d’action (ce qu’il faut faire). Si vous savez écrire un script qui ouvre un WebSocket, vous savez créer un module.

Fonctionnement d’un module

Votre module se connecte au serveur WebSocket de Rawtoh et communique en JSON-RPC 2.0. Le déroulement est simple :

  1. Connexion — ouvrez un WebSocket vers le serveur RPC de Rawtoh
  2. Enregistrement — prouvez que vous détenez la clé privée du module en signant un challenge émis par le serveur
  3. Abonnement — le serveur indique à votre module quels événements il doit émettre
  4. Émission d’événements — envoyez des données à Rawtoh dès qu’il se passe quelque chose
  5. Réception d’appels — Rawtoh appelle votre module lorsqu’une action a besoin qu’il fasse quelque chose

Utiliser un SDK

Tout ce qui suit est déjà implémenté dans les SDK officiels — l’enrôlement, l’échange challenge/réponse, la reconnexion avec backoff et la table d’abonnements derrière emit() :

use rawtoh_module_sdk::{enroll, Handler, HubConnection, HubOptions, Identity, MethodFuture};
use serde_json::{json, Value};

struct Lights;
impl Handler for Lights {
    fn call(&self, method: &str, params: Value) -> MethodFuture {
        let method = method.to_owned();
        Box::pin(async move {
            match method.as_str() {
                "lights.set_color" => Ok(json!({ "status": "ok" })),
                _ => Err(rawtoh_module_sdk::RpcError::method_not_found(&method)),
            }
        })
    }
    fn subscribe(&self, event: &str) -> MethodFuture<bool> {
        let known = event == "sensor.temperature";
        Box::pin(async move { known })
    }
}

// First run: redeem the one-shot token, persist `identity` (mode 600).
let enrolled = enroll("https://app.rawtoh.io", "rth_e_...").await?;
let identity: Identity = enrolled.identity;

let hub = HubConnection::new(
    HubOptions { url: "wss://rpc.rawtoh.io".into(), identity, label: None },
    Lights,
);
hub.start().await?;  // challenge/response, then reconnects on its own
hub.emit("sensor.temperature", json!({ "celsius": 21.5 }), None);

Le reste de cette page décrit le protocole qu’implémentent les SDK — lisez-le pour comprendre ce qui se passe, ou pour écrire un module dans un autre langage.

Étape 1 : enrôler votre module

Les modules s’authentifient avec une paire de clés Ed25519 qu’ils génèrent eux-mêmes — jamais avec un secret qu’on leur remet. Avant que votre module puisse se connecter, associez sa clé publique à une instance :

  1. Allez dans Module → Définitions et créez une nouvelle définition (ou utilisez-en une existante)
  2. Allez dans Module → Instances et ajoutez-lui une instance
  3. Copiez le jeton d’enrôlement (rth_e_…) — il n’est affiché qu’une seule fois, ne fonctionne qu’une seule fois et expire au bout de 15 minutes. Par convention, les modules sans interface le lisent dans RAWTOH_ENROLL_TOKEN au premier lancement
  4. Dans votre module, générez une paire de clés Ed25519 et utilisez le jeton
// → POST <hub>/api/module-enroll  (no authentication — the token is the credential)
{
  "token": "rth_e_kR3vQ8mT...",
  "public_key": "<raw Ed25519 public key, 32 bytes, base64url>"
}

// ← 200
{
  "instance_id": "0f3c...",
  "instance_name": "production",
  "organization_id": "org_...",
  "module_id": "mod_...",
  "module_slug": "my-module"
}

// ← 401 unknown token · 410 already used or expired · 400 bad public key

Conservez instance_id et la clé privée, puis oubliez le jeton — il est consommé. Le jeton ne contient aucune adresse de hub : les URL de l’API et du WebSocket du hub se configurent séparément dans votre module (RAWTOH_API_URL, RAWTOH_WS_URL dans les SDK).

La clé privée ne quitte jamais votre module, et il est impossible de la récupérer si elle est perdue. Utiliser un nouveau jeton (Réenrôler sur l’instance) remplace la clé publique enregistrée — l’ancien module est déconnecté avec le code de fermeture 4001 et ne peut plus se reconnecter.

Étape 2 : se connecter et s’enregistrer

Ouvrez une connexion WebSocket vers le serveur RPC de Rawtoh. Vous avez 5 secondes pour mener à bien un échange challenge/réponse en deux temps : demandez un nonce, puis signez-le avec votre clé privée.

// → Send to server
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "session.challenge",
  "params": { "instance_id": "0f3c..." }
}

// ← Server responds
{ "jsonrpc": "2.0", "id": 1, "result": { "nonce": "kR3v...", "expires_in": 60 } }

// → Sign "rawtoh-module-register:v1\n<instance_id>\n<nonce>" with your Ed25519
//   private key, base64url-encode it, and send:
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "session.register",
  "params": { "instance_id": "0f3c...", "signature": "<base64url signature>" }
}

// ← Server responds
{ "jsonrpc": "2.0", "id": 2, "result": true }

Si la signature est invalide, le serveur renvoie une erreur et ferme la connexion. Si l’enregistrement n’est pas terminé dans les 5 secondes, la connexion est fermée elle aussi. Un nonce ne vaut que pour une seule tentative — après un enregistrement raté, il faut un nouveau session.challenge.

Étape 3 : gérer les demandes d’abonnement

Juste après l’enregistrement, le serveur indique à votre module de quels événements il a besoin. Pour chaque événement, il envoie une requête de ce type :

// ← Server asks your module to subscribe to an event
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "event.subscribe",
  "params": ["chat.message"]
}

// → Your module responds with a subscription ID (any unique string)
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": "sub_abc123"
}

Règles :

Étape 4 : émettre des événements

Lorsqu’il se passe quelque chose (un message dans le chat, un clic sur un bouton, un relevé de capteur — n’importe quoi), envoyez une notification au serveur. Les notifications n’ont pas de champ id — elles sont envoyées sans attendre de réponse.

// → Send to server (notification — no "id" field)
{
  "jsonrpc": "2.0",
  "method": "event.subscription",
  "params": {
    "subscription": "sub_abc123",
    "result": {
      "user": "alice",
      "message": "hello world"
    }
  }
}

Le débit des événements est régulé par organisation (1 000 par seconde par défaut, partagés entre tous ses modules). Au-delà de ce plafond, le hub retient simplement l’événement jusqu’à la seconde suivante — rien n’est refusé ni perdu.

Étape 5 : exposer des méthodes (facultatif)

Si vous voulez que les actions puissent appeler votre module (par exemple module("my-tool").request("do.something", params)), votre module doit traiter les requêtes JSON-RPC entrantes :

// ← Server sends a request to your module
{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "lights.set_color",
  "params": { "color": "#ff0000" }
}

// → Your module responds
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": { "status": "ok" }
}

C’est vous qui définissez les noms des méthodes et leurs paramètres — c’est votre API. Les utilisateurs pourront les appeler depuis leurs scripts d’action.

Méthodes obligatoires

Votre module doit traiter ces 3 méthodes appelées par le serveur :

MéthodeCe que vous renvoyezRôle
ping"pong"Vérification de présence (heartbeat)
event.subscribeidentifiant d’abonnement ou nullCommencer à émettre un événement
event.unsubscribetrueCesser d’émettre un événement

Le manifeste

Pour que les événements et les méthodes de votre module apparaissent dans l’éditeur de scripts de Rawtoh (autocomplétion, documentation), vous pouvez fournir un manifeste — un document OpenRPC 1.3.2 décrivant ce que votre module sait faire. Renseignez-le sur la définition de module, dans Module → Définitions.

OpenRPC est à JSON-RPC ce qu’OpenAPI est à REST : la partie methods est donc standard et fonctionne avec les outils OpenRPC existants. Les événements qu’émet votre module vont sous x-events — voir l’explication juste en dessous.

{
  "openrpc": "1.3.2",
  "info": { "title": "My Module", "version": "1.0.0" },
  "methods": [
    {
      "name": "lights.set_color",
      "summary": "Set the light color",
      "params": [
        {
          "name": "color",
          "summary": "CSS color value",
          "required": true,
          "schema": { "type": "string" }
        }
      ],
      "result": {
        "name": "result",
        "schema": {
          "type": "object",
          "properties": { "status": { "type": "string" } }
        }
      },
      "paramStructure": "by-name"
    }
  ],
  "x-events": [
    {
      "name": "sensor.temperature",
      "summary": "Temperature reading from sensor",
      "payload": {
        "type": "object",
        "properties": { "celsius": { "type": "number" } }
      }
    }
  ]
}

Trois points à respecter :

La façon de produire ce document ne regarde que vous : générez-le à partir de vos types, de vos schémas, ou écrivez-le à la main. Seul le document fait office de contrat.

Une réserve concernant les schémas : Rawtoh les convertit en déclarations TypeScript pour l’éditeur de scripts, et comprend type, properties, required, items et enum. Tout le reste — $ref, oneOf, allOf, format — est accepté, mais apparaît comme unknown dans l’éditeur. Déclarez vos types en ligne plutôt que de référencer components/schemas, et vos utilisateurs bénéficieront d’une véritable autocomplétion.

Le manifeste est facultatif — votre module fonctionnera sans lui. Mais il améliore nettement l’expérience de quiconque écrit des automatisations.

Pourquoi les événements se trouvent sous x-events

OpenRPC ne décrit que le modèle requête/réponse : quelqu’un appelle une méthode, la méthode renvoie un résultat. Cela couvre tout ce que Rawtoh appelle sur votre module, mais pas l’autre sens — votre module qui envoie quelque chose à Rawtoh de sa propre initiative, ce qui est précisément un événement. La spécification n’a pas d’objet pour cela : il n’y a donc rien de standard à remplir.

Plutôt que de tordre les méthodes pour leur donner une forme qui ne leur convient pas, les événements sont placés dans une extension de spécification. OpenRPC réserve explicitement tout champ commençant par x- à cet usage précis : un outil conforme ignore ce qu’il ne reconnaît pas. Votre document reste donc un document OpenRPC valide — vous pouvez toujours le passer dans un validateur, un générateur ou un playground OpenRPC — et Rawtoh lit la partie supplémentaire qu’il connaît.

Comme il s’agit de notre extension et non de la spécification, nous la gardons simple : payload est un JSON Schema nu, sans l’enveloppe Content Descriptor qu’exige result.

Ce que vous déclarez est une promesse que vous devez tenir. Une entrée x-events est la déclaration du protocole que vous avez déjà implémenté aux étapes 3 et 4 :

Ce payload est ce que les utilisateurs reçoivent sous la forme event.payload dans leurs scripts, et c’est uniquement grâce à votre schéma qu’ils disposent de l’autocomplétion dessus :

// In an action script, typed from your x-events payload schema
log(`It is ${event.payload.celsius}°C`);

Reconnexion

Votre module doit se reconnecter automatiquement lorsque la connexion est interrompue. Mais vérifiez d’abord le code de fermeture :

Code de fermetureQue faire
4000Ne vous reconnectez pas. L’utilisateur a déconnecté le module volontairement.
4001Ne vous reconnectez pas. L’instance a été réenrôlée avec une nouvelle clé — la clé privée actuelle n’est plus valide.
Tout autre codeReconnectez-vous avec un backoff exponentiel : 1 s → 2 s → 4 s → 8 s → … jusqu’à 64 s. Réinitialisez-le après un enregistrement réussi.

Après la reconnexion, le serveur lance une nouvelle phase d’abonnement. Abandonnez tous les anciens identifiants d’abonnement.

Convention de nommage

Tous les noms d’événements et de méthodes utilisent la notation pointée :

En résumé

Pour créer un module, votre programme doit :

  1. Utiliser une seule fois un jeton d’enrôlement sur POST /api/module-enroll et conserver sa clé privée
  2. Ouvrir un WebSocket vers le serveur RPC de Rawtoh
  3. Effectuer session.challenge + session.register en moins de 5 secondes
  4. Traiter ping, event.subscribe et event.unsubscribe
  5. Émettre des événements via des notifications event.subscription
  6. Éventuellement, traiter les appels de méthodes personnalisées provenant des actions
  7. Se reconnecter en cas de déconnexion (en respectant les codes de fermeture)

C’est tout. Votre module peut être écrit dans n’importe quel langage — JavaScript, Python, Rust, Go — tout ce qui prend en charge WebSocket, JSON et Ed25519. En TypeScript ou en Rust, les SDK présentés plus haut font tout cela pour vous.