arizuko

arizuko › components › route tokens

route tokens

A route token in a URL opens one web chat or one webhook intake with no login. The token row names the JID and the folder.

A route token is a secret string in a URL

A route token is a secret string in a URL. A person with the URL reaches one specific chat or intake, with no login and no account.

Route tokens are the route_tokens table and the two URL prefixes that read it: /chat/<token>/ for visitor web chat and /hook/<token> for inbound webhooks. Each token has one row under the hash of the token. The row holds the JID, the owner folder, the target folder, an optional context and the creation time. The JID prefix tells webd which kind of URL a token belongs to.

Each token is 32 random bytes, base64url-encoded. The store keeps sha256(token), and the issuer gets the raw token once.

One row type serves chat links and webhooks

Two needs use the same thing: a URL bound to a target JID. The needs are “give my site a chat URL” and “let GitHub post events to the agent”. A token as its own row gives every inbound URL the same shape. Every URL is created and revoked in the same way.

Two URLs read the same table

GET  /chat/<token>/            → chat widget (HTML)
POST /chat/<token>/            → message in; SSE reply stream out
POST /hook/<token>             → one inbound at the JID; 204; no reply

Chat tokens carry JIDs with the prefix web:, and hook tokens carry hook:. Both URL prefixes accept any valid token: the kind is metadata for the agent, not a URL gate. The inbound JID is what the token row stores. The URL fixes the contract: widget and SSE for /chat/, and no reply for /hook/.

webd reads the token and routd delivers the message

browser / curl / GitHub / Linear / ...
        │
        ▼   /chat/<token>/...   or   /hook/<token>
        │
      proxyd     (public path, auth optional)
        │
      webd       (hash token, look up row, rate limit, append at row.jid)
        │
      routd      (route + spawn the agent; SSE replies only for /chat/)

routd handles issuance. The MCP tools issue_chat_link and issue_webhook and the REST endpoints POST /v1/route_tokens/chat and POST /v1/route_tokens/hook all call one writer, store.InsertRouteToken. webd never writes the table, and routd never serves the /chat/ or /hook/ URLs.

Grants decide who can create a token

The grants of the caller limit the folders it can create tokens for. The default role cannot create a public endpoint, so a plain folder cannot create one at all. It needs /root or a delegated mcp:issue_* row, and the scope glob of the row decides the reachable folders. Over REST, the target must equal owner_folder or be below it. The owner_folder of the row records the folder of the issuer, not the JID target, and a revoke requires admin on owner_folder.

webd limits the rate per token

webd keeps an in-memory bucket per token. The JID prefix sets the size of the bucket. web: uses a smaller bucket for human typing, and hook: uses a larger bucket for bursts from machines. The body limit is 1 MiB. A request over the limit gets 429.

The URL is the whole credential

Route tokens have no body signature check, such as X-Hub-Signature, no second factor on the URL and no fingerprint per IP. The bearer model is deliberate: the URL is the credential. A skill on the agent side can validate an upstream HMAC.

Nothing issues tokens again on a schedule. /hook/ has no reply channel. An agent that wants to acknowledge a webhook replies on another channel, such as Slack or email.

Route tokens do not run alone

The table and its writer live in routd, and the URL handlers live in webd. Neither runs alone: both need the tables of routd and its /v1/messages endpoint to deliver messages. The unit that runs alone is webd itself. See components/webd.

Five pages cover the details