WIP: feat(station): a print station authenticates as its own identity (printing_station, station enrollment code) #37

Draft
james.maes wants to merge 1 commits from feat/station-identity-20261110 into feat/multi-node-realtime-20261110
pull from: feat/station-identity-20261110
Owner

Draft. Stacked on #36 (feat/multi-node-realtime-20261110): this branch starts at its head and the base of this pull request is that branch, so the diff is only the station change. Retarget to main after #36 merges.

Why

A print station is a computer beside a printer with nobody signed in. Today it runs under the session of the person who paired it and renews that session every five minutes. Hosts are adopting automatic logoff (15 minutes without a real user action, 12 hours at most; QQQ feat/session-user-activity). A station never has a user action to report, so it would be signed out 15 minutes after every sign-in, and inactivity logoff cannot be switched on while stations use people's sessions. It is also hard to defend a named person's session running unattended around the clock.

What changes

A station can authenticate as its own identity, issued by the host's identity provider. Full description, guarantees, the host contract, what the other repositories need, the release order and the dev runbook: docs/STATION-IDENTITY.md.

  • Permission printing_station. Alone it opens nothing. On the device routes, after the request proves the one device bound to the station's identity, the station may heartbeat, report printers, claim, download and acknowledge its own jobs and answer control commands. Every row it reads must belong to that device. Processes (so test print, retry, cancel, enrollment, device actions) and the administrative, operator and generic table routes stay closed.
  • Station enrollment. Process printingEnrollStation (printing administrators) creates a one-use, 15-minute code. POST /qqq/v1/qbit/printing/stations/enroll redeems it with no session: the code is the authority. The answer carries the device proof and the identity's credential once. A station code cannot pair a person; a person's code cannot create a station.
  • Host SPI PrintingStationIdentities (issue, revoke), set with PrintingService.withStationIdentities. Without it no station code can be created or redeemed (503). Revoking or removing a station's device revokes its identity too; a provider failure answers 502 and the action can be repeated.
  • Realtime. Exactly one credential on the upgrade: a session cookie, or, for a station only, Authorization: Bearer. It is verified again on every frame.
  • Schema. Two additive nullable columns, printing_enrollment.station_name and printing_device.station_name. Print devices shows the station name.
  • Extension 2.4.0 → 2.5.0. Wire v1 and the subprotocol are unchanged; existing connectors and pairings behave as before.

The claim and acknowledgement rules of #36 are untouched: the readiness gate, the guarded QUEUED → CLAIMED update, at-least-once delivery with no second hand-out of a received job, and the connection record owned by the connection it names.

Tests

PrintingStationTest (5 tests, real QQQ, H2, HTTP and WebSocket):

  • a station code issues one identity bound to one device; nothing else redeems it; a provider outage consumes nothing; a failure after issuing revokes the identity; a live station is never issued or revoked a second time;
  • the permission without a proved device is refused everywhere; with it, the station reads only its own rows, runs no process, writes nothing generically, and the proof does not carry to another session;
  • revocation ends the identity at the provider, reports and repeats after a provider failure, and the name can be enrolled again;
  • the administrator process creates and replays a station code and cannot change its kind;
  • end to end: enroll with the code alone, then heartbeat, inventory, claim, content, RECEIVED over HTTP and SPOOLED over the socket with a bearer credential and no cookie; the administrative, pairing, operator and generic table routes refuse it; a person's bearer never holds the realtime channel; revocation stops it.

Gate

./mvnw -B -Pui-demo verify on this head (254f6fd), run locally on 2026-11-10 with no skip flags: BUILD SUCCESS, 220 tests, 0 failures, 0 errors, 1 skipped (PrintingWorkloadTest, not touched here). Checkstyle ran at validate; PrintingClaimPostgresTest (4, PostgreSQL 17 in Docker) and PrintingMultiNodeTest (5) ran. The run used a settings file that only names the QRun-IO Gitea Maven registry, because this workstation's default settings resolve Central only. Hosted CI has not run on this head.

Not in this pull request

The host wiring (gg-forge), the identity account API (qbit-authentik), the Authentik declarations and the Windows client are specified in docs/STATION-IDENTITY.md section 6 and are not built. No release, tag or deployment.

🤖 Generated with Claude Code

Draft. Stacked on #36 (`feat/multi-node-realtime-20261110`): this branch starts at its head and the base of this pull request is that branch, so the diff is only the station change. Retarget to `main` after #36 merges. ## Why A print station is a computer beside a printer with nobody signed in. Today it runs under the session of the person who paired it and renews that session every five minutes. Hosts are adopting automatic logoff (15 minutes without a real user action, 12 hours at most; QQQ `feat/session-user-activity`). A station never has a user action to report, so it would be signed out 15 minutes after every sign-in, and inactivity logoff cannot be switched on while stations use people's sessions. It is also hard to defend a named person's session running unattended around the clock. ## What changes A station can authenticate as its own identity, issued by the host's identity provider. Full description, guarantees, the host contract, what the other repositories need, the release order and the dev runbook: `docs/STATION-IDENTITY.md`. - **Permission `printing_station`.** Alone it opens nothing. On the device routes, after the request proves the one device bound to the station's identity, the station may heartbeat, report printers, claim, download and acknowledge its own jobs and answer control commands. Every row it reads must belong to that device. Processes (so test print, retry, cancel, enrollment, device actions) and the administrative, operator and generic table routes stay closed. - **Station enrollment.** Process `printingEnrollStation` (printing administrators) creates a one-use, 15-minute code. `POST /qqq/v1/qbit/printing/stations/enroll` redeems it with no session: the code is the authority. The answer carries the device proof and the identity's credential once. A station code cannot pair a person; a person's code cannot create a station. - **Host SPI `PrintingStationIdentities`** (`issue`, `revoke`), set with `PrintingService.withStationIdentities`. Without it no station code can be created or redeemed (503). Revoking or removing a station's device revokes its identity too; a provider failure answers 502 and the action can be repeated. - **Realtime.** Exactly one credential on the upgrade: a session cookie, or, for a station only, `Authorization: Bearer`. It is verified again on every frame. - **Schema.** Two additive nullable columns, `printing_enrollment.station_name` and `printing_device.station_name`. `Print devices` shows the station name. - Extension 2.4.0 → 2.5.0. Wire v1 and the subprotocol are unchanged; existing connectors and pairings behave as before. The claim and acknowledgement rules of #36 are untouched: the readiness gate, the guarded `QUEUED` → `CLAIMED` update, at-least-once delivery with no second hand-out of a received job, and the connection record owned by the connection it names. ## Tests `PrintingStationTest` (5 tests, real QQQ, H2, HTTP and WebSocket): - a station code issues one identity bound to one device; nothing else redeems it; a provider outage consumes nothing; a failure after issuing revokes the identity; a live station is never issued or revoked a second time; - the permission without a proved device is refused everywhere; with it, the station reads only its own rows, runs no process, writes nothing generically, and the proof does not carry to another session; - revocation ends the identity at the provider, reports and repeats after a provider failure, and the name can be enrolled again; - the administrator process creates and replays a station code and cannot change its kind; - end to end: enroll with the code alone, then heartbeat, inventory, claim, content, `RECEIVED` over HTTP and `SPOOLED` over the socket with a bearer credential and no cookie; the administrative, pairing, operator and generic table routes refuse it; a person's bearer never holds the realtime channel; revocation stops it. ## Gate `./mvnw -B -Pui-demo verify` on this head (254f6fd), run locally on 2026-11-10 with no skip flags: **BUILD SUCCESS, 220 tests, 0 failures, 0 errors, 1 skipped** (`PrintingWorkloadTest`, not touched here). Checkstyle ran at validate; `PrintingClaimPostgresTest` (4, PostgreSQL 17 in Docker) and `PrintingMultiNodeTest` (5) ran. The run used a settings file that only names the QRun-IO Gitea Maven registry, because this workstation's default settings resolve Central only. Hosted CI has not run on this head. ## Not in this pull request The host wiring (gg-forge), the identity account API (qbit-authentik), the Authentik declarations and the Windows client are specified in `docs/STATION-IDENTITY.md` section 6 and are not built. No release, tag or deployment. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
james.maes added 1 commit 2026-11-10 23:55:52 +00:00
feat(station): a print station authenticates as its own identity
maven / build (push) Successful in 7m38s
build / maven (push) Successful in 7m38s
254f6fd918
A print station is a computer beside a printer with nobody signed in. Until
now it ran under the session of the person who paired it and renewed that
session every five minutes. A host that ends sessions after user inactivity
would sign it out 15 minutes after every sign-in, and a named person's
session running unattended is hard to defend.

- printing_station: a permission that alone opens nothing. On the device
  routes, after the request proves the one device bound to the station's
  identity, the station may heartbeat, report printers, claim, download and
  acknowledge its own jobs and answer control commands. The proof lives in
  the request's QContext, tied to that session object, never on the session.
  Every row read must belong to that device. Processes and the
  administrative, operator and generic table routes stay closed.
- printingEnrollStation creates a one-use, 15-minute station code.
  POST /qqq/v1/qbit/printing/stations/enroll redeems it with no session and
  answers once with the device proof and the identity's credential. A station
  code cannot pair a person and a person's code cannot create a station.
- PrintingStationIdentities (issue, revoke) is the host's issuer, set with
  PrintingService.withStationIdentities. Without it there is no station
  enrollment (503). An identity issued for an enrollment that then fails is
  revoked. Revoking or removing a station's device revokes its identity; a
  provider failure answers 502 and the action can be repeated.
- The realtime upgrade accepts exactly one credential: a session cookie, or,
  for a station only, a bearer token, verified again on every frame.
- Two additive nullable columns: printing_enrollment.station_name and
  printing_device.station_name. Extension 2.5.0; wire v1 and the subprotocol
  are unchanged, and existing pairings behave as before.

The claim and acknowledgement rules of realtime printing on several nodes are
untouched. docs/STATION-IDENTITY.md describes the flow, the guarantees, the
host contract, what the host, the identity QBit, the provider and the Windows
client still need, the release order and the first enrollment on dev.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
All checks were successful
maven / build (push) Successful in 7m38s
build / maven (push) Successful in 7m38s
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This pull request is marked as a work in progress.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/station-identity-20261110:feat/station-identity-20261110
git checkout feat/station-identity-20261110
Sign in to join this conversation.