Skip to content

Channels

A channel is a live connection the host holds for the realm. Declare channels with connecting, which checks that every credential, channel and source a declaration names is declared right beside it. connecting takes all four blocks, credentials, channels, sources and consumers; pass {} for one you do not need.

import { connecting, defineRealm } from "@embabel/realm-types";
export default defineRealm({
name: "telegram",
version: "0.1.0",
host: "wasm",
execution: "captured",
handlers: {
updates: { namespace: "telegram" },
reply: { namespace: "telegram" },
},
apis: {
telegram: {
url: "telegram.json",
name: "telegram",
type: "openapi",
auth: "path",
credential: "bot",
operationIds: ["getMe"],
writeOperationIds: ["sendMessage"],
},
},
...connecting({
credentials: {
bot: { kind: "bearer", description: "The bot token BotFather gave you." },
},
channels: {
updates: {
transport: "long-poll",
url: "https://api.telegram.org/bot{credential}/getUpdates",
credential: "bot",
onResponse: "telegram.updates",
every: "2s",
},
},
sources: { messages: { channel: "updates", description: "Messages people send the bot." } },
consumers: { "assistant-reply": { source: "messages", handler: "telegram.reply", assistant: true } },
}),
});

Channel, source and consumer keys are file names: [a-z][a-z0-9-]{0,63}. The host owns reconnects, budgets, limits, cursors and whether a channel starts, so a channel refuses name, cursor, reconnect, autoStart, limits, budget, secret and secretRef. A source refuses name, kind and installationId, and a consumer refuses name and checkpoint. None of the files carries a name; the file name is the name. Synth refuses a channel, keepalive or consumer handler the realm does not declare, the same check defineRealm makes at compile time, and a realm with no handlers block declares none. A duration is a whole number of seconds or minutes, greater than zero: "30s" or "5m".

Websocket. The host dials url (wss://) with the bound credential and hands every frame to onFrame.

socket: {
transport: "websocket",
url: "wss://gateway.example.com/?v=10",
credential: "bot",
onFrame: "example.gateway",
keepalive: { every: "40s", handler: "example.heartbeat" },
handshake: { operation: "apps.connectionsOpen", field: "url" },
},
  • A frame is the provider’s text, or a control frame: {kind: "open"}, {kind: "keepalive"} or {kind: "close", reason}, with reason one of remote, rotation, budget, revoked, error or guest. Binary frames are dropped and counted.
  • open arrives on every real connection, so authenticate there each time.
  • keepalive.every is between 10 seconds and 5 minutes. Only the keepalive handler gets the tick, and only while the socket is up.
  • handshake is for a provider that hands out its socket URL from an API call. The host calls the named operation, reads field from the answer and connects there. Under defineRealm, a realm that declares apis gets a compile error for an operation none of them lists as a read or a write; the operation is written <name>.<operationId>.
  • ctx.stream.send(text) writes a frame; write {credential} where the token belongs and the host substitutes it on the way out. ctx.stream.close() ends the connection once the handler returns; the next frames are a close with reason guest and then an open.

Long-poll. The host fetches url (https://) every every and hands the body to onResponse.

  • every is at least 1s and defaults to 5s, which synth writes into the file. The operator’s floor wins over what the realm declares, so a realm cannot poll faster than the host allows.
  • The handler returns {cursor, cursorParam} to say where the next fetch starts: cursor is the value and cursorParam the query parameter it travels as. Return cursorParam on every response so a restart keeps it.
  • A handler that throws leaves the cursor where it was, so the same batch is fetched again. Make the handler safe to run twice on one batch.
  • The host keeps a cursor only if it carries no secret. A cursor or parameter name that spells the credential refuses the request.
  • A 429 or 5xx waits out the backoff. A 401 or 403 stops the channel and tells the owner.

Webhook. The provider calls the host, which verifies the signature and hands the verified body to onRequest. The host mints the receiving URL when the owner approves the channel, so a webhook declares no url and no top-level credential; the signature names it.

intake: {
transport: "webhook",
onRequest: "intake.receive",
signature: {
scheme: "hmac-sha256",
credential: "signing-secret",
header: "X-Signature-256",
prefix: "sha256=",
encoding: "hex",
template: "{timestamp}.{body}",
timestamp: { header: "X-Timestamp", toleranceSeconds: 300 },
},
verification: { when: { field: "type", equals: "url_verification" }, reply: { echo: "challenge" } },
},
  • scheme is hmac-sha256 (needs header) or ed25519 (needs timestamp). encoding is hex or base64. template may use only {method}, {fullUrl}, {body} and {timestamp}. timestamp.unit is s or ms, and timestamp.toleranceSeconds is a whole number above zero.
  • A request is dispatched only after its signature verifies. A changed body or a timestamp outside the window is refused before any handler runs.
  • verification answers a provider’s endpoint check without dispatching anything: when when.field equals when.equals, the host replies with the named field’s value (echo) or a constant (body, at most 1 KiB of JSON). It applies only after the signature passes. Exactly one of echo or body. when.field and echo are dotted paths into the body with no empty step, and when.equals is a string or a finite number. A websocket or long-poll channel may not declare verification.

A websocket or long-poll URL may carry {credential} at most once, in its path or query, and nothing else in braces: {token}, {Credential} or a lone { or } is refused, since the host substitutes {credential} and nothing else. The URL may not point at localhost, a .localhost name or a bare IPv4 or IPv6 address. Each channels/<name>.yml synth writes is at most 64 KiB, the most the host reads from one channel file. A channel pointed at an origin the operator has not allowed refuses to start. Frame budgets, reconnect backoff and the credential rules on outbound bytes are in channels.