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 :
- Connexion — ouvrez un WebSocket vers le serveur RPC de Rawtoh
- Enregistrement — prouvez que vous détenez la clé privée du module en signant un challenge émis par le serveur
- Abonnement — le serveur indique à votre module quels événements il doit émettre
- Émission d’événements — envoyez des données à Rawtoh dès qu’il se passe quelque chose
- 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() :
@rawtoh/module-sdk(npm) —enroll+HubConnection, plus une partie Hono pour la connexion des utilisateurs et l’installation en libre-service. Vous enregistrez vos méthodes JSON-RPC (ainsi queping/event.subscribe) dansonOpen. Les modules Twitch, OBS et Tableau reposent dessus.rawtoh-module-sdk(crate) — son pendant sans interface, pour les CLI et les démons. Il répond aussi àping,event.subscribeetevent.unsubscribeà votre place ; votreHandlerne voit que les méthodes du module.
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 :
- Allez dans Module → Définitions et créez une nouvelle définition (ou utilisez-en une existante)
- Allez dans Module → Instances et ajoutez-lui une instance
- 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 dansRAWTOH_ENROLL_TOKENau premier lancement - 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 keyConservez 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 :
- Renvoyez une chaîne unique comme identifiant d’abonnement — vous vous en servirez ensuite pour émettre des événements
- Renvoyez
nullsi vous ne reconnaissez pas le nom de l’événement — ne renvoyez jamais d’erreur - Le serveur peut aussi appeler
event.unsubscribelorsqu’un événement n’est plus nécessaire
É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"
}
}
}subscription— l’identifiant que vous avez généré lors deevent.subscriberesult— le payload de l’événement. N’importe quel JSON. Il devientevent.payloaddans les déclencheurs et les actions.emitted_by— facultatif : l’identifiant de l’utilisateur Rawtoh à l’origine de l’événement (un clic sur un tableau, une action dans le tableau de bord), affiché dans le panneau Activité. Omettez-le pour les événements venus du monde extérieur — messages de chat, changements de scène, minuteurs.
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éthode | Ce que vous renvoyez | Rôle |
|---|---|---|
ping | "pong" | Vérification de présence (heartbeat) |
event.subscribe | identifiant d’abonnement ou null | Commencer à émettre un événement |
event.unsubscribe | true | Cesser 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 :
- Un résultat est un Content Descriptor —
{ "name": …, "schema": … }, et non un schéma nu. C’est ce que prévoit la spécification. - Les paramètres sont passés par nom. Rawtoh appelle toujours votre module avec un objet de paramètres, jamais par position.
- Chaque méthode et chaque événement a besoin d’un
summary. C’est ce que lisent les utilisateurs dans le navigateur de modules, et ce que lit l’assistant IA lorsqu’il écrit des automatisations pour votre module — la seule partie qu’aucun générateur ne peut produire à votre place.
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 :
nameest la chaîne exacte que Rawtoh passera à votreevent.subscribe. Si vous déclarez un événement, votre module doit le reconnaître à cet endroit — sinon vous renvoyeznull, l’abonnement n’a jamais lieu et le déclencheur de l’utilisateur ne s’exécute jamais, sans le moindre message. À l’inverse, personne ne peut s’abonner à un événement que vous émettez sans jamais le déclarer.payloaddécrit l’objetresultde votre notificationevent.subscription— cet objet uniquement, pas l’enveloppe JSON-RPC qui l’entoure.
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 fermeture | Que faire |
|---|---|
4000 | Ne vous reconnectez pas. L’utilisateur a déconnecté le module volontairement. |
4001 | Ne 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 code | Reconnectez-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 :
- Événements :
sensor.temperature,door.opened,game.score_changed - Méthodes :
lights.set_color,display.show_text,motor.move
En résumé
Pour créer un module, votre programme doit :
- Utiliser une seule fois un jeton d’enrôlement sur
POST /api/module-enrollet conserver sa clé privée - Ouvrir un WebSocket vers le serveur RPC de Rawtoh
- Effectuer
session.challenge+session.registeren moins de 5 secondes - Traiter
ping,event.subscribeetevent.unsubscribe - Émettre des événements via des notifications
event.subscription - Éventuellement, traiter les appels de méthodes personnalisées provenant des actions
- 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.