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}, withreasonone ofremote,rotation,budget,revoked,errororguest. Binary frames are dropped and counted. openarrives on every real connection, so authenticate there each time.keepalive.everyis between 10 seconds and 5 minutes. Only the keepalive handler gets the tick, and only while the socket is up.handshakeis for a provider that hands out its socket URL from an API call. The host calls the named operation, readsfieldfrom the answer and connects there. UnderdefineRealm, a realm that declaresapisgets 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 aclosewith reasonguestand then anopen.
Long-poll. The host fetches url (https://) every every and hands the body to
onResponse.
everyis at least1sand defaults to5s, 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:cursoris the value andcursorParamthe query parameter it travels as. ReturncursorParamon 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" } },},schemeishmac-sha256(needsheader) ored25519(needstimestamp).encodingishexorbase64.templatemay use only{method},{fullUrl},{body}and{timestamp}.timestamp.unitissorms, andtimestamp.toleranceSecondsis 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.
verificationanswers a provider’s endpoint check without dispatching anything: whenwhen.fieldequalswhen.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 ofechoorbody.when.fieldandechoare dotted paths into the body with no empty step, andwhen.equalsis a string or a finite number. A websocket or long-poll channel may not declareverification.
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.

