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
+10 -1
View File
@@ -166,7 +166,16 @@ export const EdgeClientMessageSchema = Type.Union([
type: Type.Literal("hello"),
gateway_id: Type.String(),
farm_id: Type.String(),
token: Type.String(),
/*
* No credential here, deliberately. The edge JWT travels in an
* `Authorization: Bearer` header on the upgrade request and is verified
* before the socket exists. A `token` field used to live on this
* message; 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` is redundant with the token's own claim, and is checked
* against it rather than trusted.
*/
/**
* Where this gateway serves camera restreams, e.g.
* `http://gateway.local:1984`. Omitted by a gateway with no cameras or