Commit Graph
3 Commits
Author SHA1 Message Date
octoturge 9fd3a628f8 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.
2026-10-17 13:59:41 +00:00
octoturge c667e16ded Carry camera stream information on the edge protocol
Two optional fields so a user can be pointed at a printer's camera.

`hello.stream_base_url` is where the gateway serves restreams - the base of
the go2rtc instance it already supervises. Reported by the gateway rather
than configured in the cloud, because only the gateway knows the address it
is actually reachable at.

That address is a LAN one, and the schema says so where someone will read it
rather than leaving it to be discovered in a support ticket. A browser on the
same network as the gateway can play the stream; one outside it cannot, and
the relay that would fix it does not exist. For a print farm whose operator
is in the workshop this is the normal case and it works; for anyone else it
is a limit, and anything surfacing the field has to cope with it being
unreachable.

`printer_telemetry.camera_stream` is the stream's name, present only when
there is one. Absent is the common answer today and the reason is worth
recording: a Bambu P1/A1 publishes no RTSP URL at all - a real P1P's ipcam
block carries `ipcam_dev` and `ipcam_record` and nothing to connect to -
while an H2C reports `rtsps://<ip>:322/streaming/live/1`. So RTSP restreaming
covers the X1/H2 class and nothing else; the rest need Bambu's proprietary
port-6000 JPEG stream, which is a client no gateway has yet.

Both optional and additive, so existing gateways are unaffected.
2026-10-15 22:55:52 +00:00
octoturge bb5a2680af Define the /ws/edge/v1 gateway uplink protocol
continuum-proxy and continuum-backend each had their own idea of what the
edge WebSocket carried, and the two did not overlap anywhere: the backend
expected an `auth` handshake the gateway never sends, telemetry shaped one
printer per message where the gateway sends a batch, and UUID printer ids a
gateway has no way to know. Neither end had a command path that matched the
other. Nothing failed, because nothing was written down for both sides to
mirror. This is that document.

schemas/continuum/v1/edge/ is canonical: client_message, server_message,
printer_telemetry and printer_command. packages/ts-types gets its first
real TypeBox mirror - the placeholder there said to add one "once something
actually consumes it", and continuum-backend's edge route now does. The
Rust mirror stays in continuum-proxy/src/uplink/protocol.rs rather than
packages/rust-types, since the gateway is its only Rust consumer.

Two decisions worth recording:

Field names are snake_case, unlike the backend's HTTP routes. The other end
is serde-derived Rust, and snake_case is what those structs produce without
a rename attribute on every field. It is also what the rest of this
repository already uses.

Optional fields accept an explicit null as well as being absent. A gateway
omits what it has no value for, which is what these schemas describe - but
serde emits null by default unless every field is annotated, and a single
such field would otherwise fail validation and take a whole farm's
telemetry batch down with it. Strict in what we send, liberal in what we
accept; the contract tests on both sides pin down each half.

Also bumps this package's @sinclair/typebox to ^0.33.12 to match what
continuum-backend already pins. It was on ^0.32.0, which resolved to a
separate 0.32.37 install - two TypeBox versions means two incompatible
TSchema identities, and Elysia rejected every schema from here on those
grounds alone.
2026-10-03 10:37:13 +00:00