Move the edge credential out of the hello message

The `hello` frame carried a `token` field holding the edge JWT. It is
gone: the token now travels in an `Authorization: Bearer` header on the
WebSocket upgrade request.

A token in the first frame means the backend has already accepted a
socket, allocated state for it and run its JSON through this schema and
a switch statement for a peer that has proved nothing. Every message type
refused to act without a verified farm, so nothing could be done with
such a connection - but "it cannot do anything" is a weaker guarantee
than "it does not exist", and it needed a timeout to stop anyone holding
sockets open for free. Verified at the handshake, an unauthenticated peer
gets an HTTP 401 and no socket at all.

The alternative of a `?token=` query parameter was rejected outright: a
bearer credential in a URL lands in access logs, proxy logs and error
breadcrumbs, which is exactly what continuum-proxy's hand-written `Debug`
for `PrinterCommand` exists to avoid for presigned URLs. `Authorization`
is the header every log tool already knows to redact.

`farm_id` stays on the message even though the token carries the same
claim. It is checked against the token rather than trusted, so sending it
is how a misconfigured gateway gets caught instead of silently working
against the wrong farm.

A browser cannot set headers on a WebSocket, so a future user-facing
socket cannot copy this exactly; its equivalent is the subprotocol field,
`new WebSocket(url, ["bearer", token])`, which is also verified at the
upgrade.

Breaking change to the edge protocol, which is free right now: nothing is
deployed. `npm run typecheck` and `npm run build` clean.
This commit is contained in:
2026-10-17 13:59:41 +00:00
parent c667e16ded
commit 9fd3a628f8
2 changed files with 12 additions and 7 deletions
@@ -7,7 +7,7 @@
{
"type": "object",
"title": "Hello",
"description": "First message on every connection, before anything else is accepted. Carries the edge JWT minted by POST /auth/edge-token.",
"description": "First message on every connection, before anything else is accepted. Carries NO credential: the edge JWT minted by POST /auth/edge-token goes in an Authorization: Bearer header on the WebSocket upgrade request, and is verified before the socket exists, so an unauthenticated peer gets an HTTP 401 and no socket at all. This message used to carry a 'token' field; it was removed, because a token in the first frame means a live socket and a parsed message for a peer that has proved nothing. farm_id here is redundant with the token's own claim and is checked against it rather than trusted - a gateway naming a farm its token was not minted for is refused.",
"properties": {
"type": {
"const": "hello"
@@ -18,9 +18,6 @@
"farm_id": {
"type": "string"
},
"token": {
"type": "string"
},
"stream_base_url": {
"type": "string",
"format": "uri",
@@ -30,8 +27,7 @@
"required": [
"type",
"gateway_id",
"farm_id",
"token"
"farm_id"
]
},
{