A gateway can find printers on its own network and USB ports, but nothing carried that back to the cloud, so adding one in the web app still meant an operator reading an IP address and a serial number off a machine by hand. Two schemas, both gateway -> cloud: - `DiscoveredPrinter`, a candidate the gateway found but cannot drive, because it is not in that gateway's fleet file. - A `discovered_printers` variant on the client message, carrying a whole batch that replaces what that gateway last reported. A printer that has gone is reported by its absence, and a gateway seeing nothing sends an empty list. A candidate deliberately carries no credential, and cannot: a Bambu access code is on the printer's screen and a PrusaLink API key is in its web UI, so no probe learns either. What this buys is the address and the identity, which are the parts an operator would otherwise have to go and look up. Two details worth keeping in mind downstream: `ambiguous_with` exists because PrusaLink serves OctoPrint's API, so a probe that saw only that API cannot honestly claim PrusaLink. The candidate says `octoprint` and names the alternative, and the operator settles it. Every string here was chosen by a device on the customer's network - anything answering an SSDP query or an HTTP probe names itself - so they are untrusted display strings. Hence the length caps, and hence `DiscoveredPrinter` being closed where its sibling variants are open: an unbounded LAN-supplied `name` should not reach a database column or a React tree.
continuum-schemas
Single source of truth for the Continuum data model. Everything here is JSON Schema, compiled into two consumer packages:
packages/ts-types- hand-written TypeBox schemas undersrc/schemas/used for runtime validation incontinuum-backend's ElysiaJS routes.packages/rust-types- a Rust crate (continuum-rust-types) with hand-written,serde-derived structs forcontinuum-proxyandcontinuum-ai-worker.
Layout
schemas/continuum/v1/ Continuum's own model
ai/ continuum-backend <-> continuum-ai-worker: the
stream:camera_frames work queue and the
channel:print_alerts notification channel
edge/ continuum-backend <-> continuum-proxy: the
/ws/edge/v1 gateway uplink protocol
material/ printer/ domain objects
vendor.schema.json
schemas/bambulab/ Bambu Lab MQTT wire schemas (v1/v2, report + request)
packages/ts-types/ TypeBox schemas (hand-written, mirrors schemas/)
packages/rust-types/ serde structs (hand-written, mirrors schemas/)
The two inter-service protocols - edge/ and ai/ - are here for a
specific reason: each has two independent implementations, one per repo, and
when there was nothing written down between them they drifted completely
while every test passed. /ws/edge/v1 had an auth handshake the gateway
never sent; stream:camera_frames had a producer writing printerId and a
consumer reading printer_id, so every frame was attributed to a printer
called "unknown". Neither failure was visible from either side alone, which
is what a canonical schema plus a contract test on each end is for.
schemas/continuum/v1/*.schema.json is the canonical shape for each
domain object; packages/ts-types/src/schemas/ and
packages/rust-types/src/lib.rs both mirror it by hand. A mirror is added
per schema only once something actually consumes it, so not every file here
has one, and the two inter-service protocols keep their Rust mirror in the
single repo that consumes it rather than in packages/rust-types:
edge/ in continuum-proxy/src/uplink/protocol.rs, ai/ in
continuum-ai-worker/src/task.rs. The set is small and changes slowly,
which is why keeping hand-synced copies in sync is less overhead than a
codegen pipeline (protoc/ts-proto/prost-build) nothing actually
consumed - see git history for the
protobuf version this replaced, and why it was dropped: every service in
this system talks JSON (JSON Schema for the wire, TypeBox/serde for
runtime validation), so protobuf's actual value - a single schema compiled
into a compact binary wire format for both languages - never applied.
Whenever a domain schema changes, update all three by hand: the JSON Schema file, the TypeBox schema, and the Rust struct.
Build / verify
Requires npm install at the repo root, and a Rust toolchain, first.
npm install
npm run build:rust # cargo build, confirms the Rust structs compile
npm run typecheck # tsc -b, confirms the TypeBox schemas compile
There's no build:ts step - the TS side is hand-written, not generated.