docs: load balancer affinity, and what the client does when a server node stops

Changelog (unreleased), REALTIME.md (a server that runs as several nodes, with
the 401 / 5xx / dropped-connection table) and the threat model (the only
cookies besides the session that the client ever sends, and their bounds).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
This commit is contained in:
james.maesandClaude Fable 5.2 committed 2026-11-10 16:49:59 -06:00
1 parent 530e2e302b
commit 87e3f18b83
3 files changed
+43 -1

No files matched your search

+12
View File
@@ -13,6 +13,18 @@ capability gets the ordinary logout, and the log says the provider session was n
`sign-out` cannot open the companion's callback inbox, so it still ends only the QQQ session and says so.
No change to pairing, history or the printing protocol.
Servers that run as several nodes. A load balancer in front of such a server names the node it chose in a
cookie; the client attached only the session cookie, so its realtime socket could open on one node while its
claims and downloads went to another, and a server whose claim gate needs the socket on the same node
(QQQ Printing 1.1.x) answered 409. The client now keeps the Application Load Balancer's cookies (names
beginning `AWSALB`) from any answer of the server, the WebSocket upgrade answer included, and sends them on
every later request and on the upgrade, so everything reaches the node that holds the socket. When that node
stops, the load balancer rewrites the cookie on the next answer and the client follows it. The cookies are
held in memory only, never saved with the session, limited to eight bounded values scoped to the server's
host and root path, and sent nowhere but the server. Cookies under other names no longer decide whether a
session answer is valid, because a load balancer adds its own to every answer. Nothing changes against a
single-node server. No change to pairing, history or the printing protocol.
## 1.2.1
Release-pipeline fix only; same source as 1.2.0 otherwise. The stable release workflow's per-edition
+30
View File
@@ -23,6 +23,36 @@ apply. No credentials are placed in URLs or messages. WSS and HTTPS share the
configured origin; redirects cannot select another document host. Plain WS/HTTP
is restricted to explicit loopback development.
### A server that runs as several nodes
A QQQ server may run as several nodes behind a load balancer with cookie
stickiness. Sessions are rows in its database, so any node accepts the session;
what differs per node is which one holds this device's socket. The client keeps
the load balancer's own cookies (Application Load Balancer names, beginning
`AWSALB`) from every answer of the configured origin, including the answer to
the WebSocket upgrade, and sends them on every later request and on the next
upgrade. Discovery runs immediately before each upgrade, so the socket opens on
the node the HTTP requests already reach, and claims and downloads then go to
the node that holds the socket. The cookies are held in memory only, are never
saved with the session, and are bounded (eight values of at most 4096
characters, scoped to the origin's host and root path).
When that node stops:
| What the client sees | What it does | Needs a person |
|---|---|---|
| The socket closes or stops answering heartbeats | Marks realtime not ready, cancels the claim wave, and reconnects after the configured backoff (1 to 30 seconds by default). The load balancer routes the reconnect to a healthy node and rewrites its cookie; the client follows it. After the new socket's welcome and applied state the server announces outstanding work again and claims resume; a claimed job replays, a job already recorded locally is not submitted twice | No |
| 502, 503 or 504 (or any 5xx, or 409) on a claim, download or event | The request fails; the session is kept. The lane logs `Print lane stopped` and ends; the job stays on the server and is claimed on the next job notification. A job caught between claim and submission is recorded FAILED locally, with no bytes submitted | No (the FAILED job needs an operator retry) |
| A dropped connection with no answer | The same as a 5xx: a transport failure, session kept | No |
| 5xx on the WebSocket upgrade | A failed connection attempt; retried after backoff | No |
| 401 on any request, or 401/403 on the WebSocket upgrade | The server withdrew authorization. The session is cleared (401) and reconnecting stops until someone signs in | Yes |
A server at QQQ Printing 1.1.x refuses a claim (409) unless the socket is on
the node that received it; the cookie keeps the two together except while a
node is being drained (the load balancer already sends new requests elsewhere
while the socket is still open). A server with the several-node support in
QQQ Printing after 1.1.2 accepts the claim on any node.
The service owns separate connection, receive, send, synchronization, command
and job workers. Settings, desired state, commands, inventory, job events and
receipts use small bounded WebSocket messages. A job notification wakes a finite
+1 -1
View File
@@ -10,7 +10,7 @@ The remote-control adapter accepts only inventory refresh and cancellation of on
The native QQQ session adapter requires discovery before credentials and never treats a native control 404 as legacy permission. After support is negotiated, 404 does not remove restrictions. Authorization denial becomes authorization-required, without inferring that every 401/403 proves revocation. New claims wait for an authenticated, bound QQQ user and valid control state. Existing installations without a protected QQQ session wait for explicit sign-in; device proof alone cannot authorize normal service printing. Explicit isolated CLI diagnostics may use the compatibility bearer adapter with `--legacy-compat`; it is not a fallback from native authentication.
The service stores only the QQQ session cookie and stable authoritative identity under service-identity DPAPI, and periodically resumes the standard session. Passwords stay in the companion; provider refresh/access tokens remain server-side. Normal CLI operations use the existing authenticated service pipe. The private browser callback is fixed and MSI-owned, forwarding bounded data only into an active same-user/session companion inbox after executable verification. It carries a short-lived authorization code, never a session cookie or PKCE verifier. Core validates exact advertised redirect, random state, PKCE and single-use expiry; when the server advertises `nonceValidation: REQUIRED` it also sends only that attempt's own authorization nonce as `expectedNonce` (never a callback-supplied or regenerated value) and refuses any other non-empty nonce policy; no callback creates an authentication attempt. Provider registration, installed callback delivery and long-duration renewal remain deployment qualifications.
The service stores only the QQQ session cookie and stable authoritative identity under service-identity DPAPI, and periodically resumes the standard session. The only other cookies the session client ever sends are a load balancer's affinity cookies (Application Load Balancer names beginning `AWSALB`), which select a server node and carry no identity: they are taken only from answers of the configured origin, accepted only when scoped to that host and the root path with a plain value of at most 4096 characters (eight at most, a `Secure` one only over HTTPS), sent only to that origin, never to the identity provider, held in memory and never written to disk. A hostile server could set them, but it already controls every answer the client trusts; the bounds keep it from growing the request. Cookies under any other name are neither stored nor sent. Passwords stay in the companion; provider refresh/access tokens remain server-side. Normal CLI operations use the existing authenticated service pipe. The private browser callback is fixed and MSI-owned, forwarding bounded data only into an active same-user/session companion inbox after executable verification. It carries a short-lived authorization code, never a session cookie or PKCE verifier. Core validates exact advertised redirect, random state, PKCE and single-use expiry; when the server advertises `nonceValidation: REQUIRED` it also sends only that attempt's own authorization nonce as `expectedNonce` (never a callback-supplied or regenerated value) and refuses any other non-empty nonce policy; no callback creates an authentication attempt. Provider registration, installed callback delivery and long-duration renewal remain deployment qualifications.
Sign-out also ends the identity provider's browser session. The companion sends only the fixed registered callback with `sign-out`; the service refuses any other value, asks QQQ to log out with `nativeLogout.postLogoutRedirectUri`, and returns the provider end-session request to the companion over the administrator-only pipe. That request carries the provider's ID token hint (sensitive, bounded to 32 KiB, no whitespace or control characters), so it is never logged, the record prints redacted, and only the provider host is logged. Core refuses the data unless the issuer equals the one QQQ advertises, the end-session endpoint is HTTPS on the issuer's origin and identical to the provider's own discovery, and the callback is the one requested. The browser return is accepted only on the exact callback with the state this sign-out generated. Browser sign-in also sends `max_age=0` beside `prompt=login`.