arizukoconcepts › route tokens

route tokens

A route token is a 32-byte random string that maps to one JID. POST a body to a URL carrying that token and the body lands as an inbound message at the mapped JID. The JID prefix encodes routing intent at the data layer — web: for a browser chat, hook: for a webhook from GitHub, Linear, or anywhere else that knows how to POST JSON.

One table, two URL surfaces

All tokens live in the route_tokens table. Four columns:

token_hash    sha256 of the raw token; lookup key
jid           web:<folder>[/<suffix>]  |  hook:<folder>/<source>[/<suffix>]
owner_folder  the folder that minted the row; bounds revocation
created_at    when it was minted

Each URL is bound to its JID prefix kind; mechanics shared:

URLToken JIDMethodsWhat happens
/chat/<token>/web: onlyGET, POSTGET serves the chat widget. POST appends a message; SSE streams the reply for browsers, plain response for non-browser callers.
/chat/<token>/mcpweb: onlyGET, POSTPer-token MCP surface (send_message, get_round, get_round_status).
/hook/<token>hook: onlyGET, POSTSame widget on GET, same POST behavior, same SSE as /chat/<token>/. Different URL, different JID prefix kind.

The two URLs share their handler internals — same widget, same POST path, same SSE — but the binding is enforced at token lookup: a hook: token presented at /chat/<token>/ returns 404, and a web: token at /hook/<token> returns 404. Each issuance verb picks its URL: issue_chat_link returns /chat/<token>/, issue_webhook returns /hook/<token>. One URL per kind keeps each surface single-purpose.

What the JID encodes

web:<folder> means “anonymous visitor chat at this folder.” web:acme is acme's public chat URL. Add a suffix to split one folder across pages: web:acme/support and web:acme/sales are different conversations against the same agent, distinguishable in routing rules.

hook:<folder>/<source> means “webhook from source targeting folder.” hook:acme/eng/github is GitHub events for the acme/eng folder. hook:acme/eng/linear/comments partitions Linear into a comments stream separate from hook:acme/eng/linear/issues. The <source> segment is what the agent sees in the inbound's sender field.

JID prefixes split routing — a hook: message and a web: message can fire different rules even though one table issued both tokens. The URL the request arrived on matches the JID prefix one-to-one.

Issuance

Two sources mint tokens, one writer inserts the row.

Both paths hit the same insertRouteToken writer. Same row shape, same audit trail.

The raw token is returned exactly once at issue time. Only the sha256 is stored. Losing the token means reissuing — there is no recovery.

Folders do not get a chat token automatically when they are created. A folder gets a token when someone calls issue_chat_link for it. This keeps unused folders from accumulating live URLs.

Who can mint for which folder

Resolved by the unified grants tier. Lower tier = broader mint reach.

TierCan mint for
0any folder
1self and any descendant
2self only
3+no mint

Tier 1 at acme can mint hook:acme/eng/github on behalf of the acme/eng child. In that case the row's owner_folder stays at acme — the issuer, not the JID target. Revocation follows owner_folder, so acme retains control of the tokens it minted.

Revocation

One DELETE on the row. The next request to the URL returns 401 immediately — no grace period, no cache to wait out. An agent in folder A cannot delete a token whose owner_folder = B; the ACL gate at revoke_route_token requires admin on owner_folder.

The bearer model

Whoever holds the URL can POST to it. There is no second factor, no signing on the body, no per-caller identity at this surface. webd does not verify X-Hub-Signature or any other upstream HMAC — that belongs to a skill on the agent side, not the platform. Rotation is by reissue: mint a new token, swap the URL at the source, delete the old row.

webd applies a per-token rate limit in memory. The ceiling is chosen by JID prefix — web: sits at a lower bucket sized for humans, hook: at a higher bucket for machine bursts. Body size is capped at 1 MiB.

Name

Previously called slink; renamed to route tokens for clarity once webhooks joined the same primitive.

Go deeper