Completes ADR-272. Three parts.
1. PREFIX MATCHING, not an allowlist. Anything under `/ws/` is a WebSocket path,
plus `/api/v1/stream/pose` which lives outside it. An allowlist means every
WebSocket route added later ships ungated until someone remembers to extend
it — the same bug reintroduced on a delay. Not hypothetical:
`/ws/train/progress` (ADR-186, arriving with PR #1387) is ALREADY referenced
by ui/services/training.service.js and would have shipped unauthenticated.
Pinned by a test that asserts it is gated before it exists.
2. UI wiring. A shared `withWsTicket()` helper mints a ticket immediately before
each connection attempt — never cached, because a ticket is single-use and
expires in seconds, so reusing one across reconnects fails on the second
attempt. Wired into the three sites that open gated sockets:
sensing.service.js, websocket-client.js, observatory/js/main.js.
It degrades in both directions on purpose: no stored token means auth is off
and no ticket is needed; a 404 from /api/v1/ws-ticket means a server
predating this ADR, which still exempts WebSockets, so connecting without a
ticket is correct there. The same UI therefore works against old and new
servers, which is what makes the escape hatch removable later rather than
permanent.
The long-lived bearer token is still never put in a URL — only the ticket is.
3. ADR-272 itself. Previously cited in five places without existing; the same
dangling-reference mistake made with ADR-271 earlier today, so it is written
before this lands rather than after someone notices.
It records the measured before/after, why a credential in a URL is
acceptable here specifically (single use, seconds-long, not the credential),
why the escape hatch exists and why it is deliberately uncomfortable, and
what is deliberately NOT done — including that /health/metrics stays ungated,
with the caveat that this should be revisited if metrics ever carry
occupancy-derived values, since that would make them sensing data wearing an
ops label.
Tests: 526 sensing-server (4 new path-matching), 82 ruview-auth. JS
syntax-checked with `node --check`; there is no UI test suite to extend.
Co-Authored-By: Ruflo & AQE