mirror of
https://github.com/ruvnet/RuView
synced 2026-08-11 20:41:44 +00:00
websocket.service.js (used by the pose/event streams) opened a bare `new WebSocket(url)` with no ADR-272 ticket exchange, unlike sensing.service.js which already mints a ticket per connect. Since a browser cannot set an Authorization header on a WebSocket upgrade, and the server rejects a long-lived bearer passed as a query string (CWE-598), the pose stream 401'd whenever auth was on — visible in the Live Demo tab as "Failed to create WebSocket connection". Fix: createWebSocketWithTimeout() now strips any `token` query param a caller put on the URL (pose.service.js does this) and exchanges the stored bearer for a single-use `?ticket=` via withWsTicket(), done at this one choke point so every consumer (pose, events, training) and every reconnect attempt gets a fresh ticket. Second, smaller bug: pose-fusion/js/main.js auto-connected to a hardcoded `ws://localhost:8765/ws/sensing`, but the Docker image serves the sensing WebSocket on :3001 (the same 3000->3001 mapping sensing.service.js already encodes) — the auto-connect dialed a port nothing listens on. Reuses that port mapping and tickets both the auto-connect and the manual "Connect" button. Bumped the pose-fusion.html cache-buster (v=13 -> v=14) so browsers actually fetch the updated main.js. Adds ui/services/websocket.service.test.mjs (4 executed Node tests, stubbed WebSocket/fetch/localStorage) covering: no-auth passthrough, ticket exchange + bearer-never-in-URL, stray ?token= stripping, and the pre-ADR-272 404 fallback. Wired into the CI "Run UI unit tests" step. Reported with a verified fix in #1461 by wsc7r4zcj4-collab; this PR implements the same fix against current main with an added regression test. Closes #1461
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { API_CONFIG, buildWsUrl } from '../config/api.config.js';
|
||||
import { backendDetector } from '../utils/backend-detector.js';
|
||||
import { withWsTicket } from './ws-ticket.js';
|
||||
|
||||
export class WebSocketService {
|
||||
constructor() {
|
||||
@@ -115,8 +116,19 @@ export class WebSocketService {
|
||||
}
|
||||
|
||||
async createWebSocketWithTimeout(url) {
|
||||
// ADR-272: the server gates /ws/* and /api/v1/stream/* behind bearer auth,
|
||||
// and a browser cannot set an Authorization header on an upgrade request.
|
||||
// Exchange the stored bearer for a single-use ?ticket= here — immediately
|
||||
// before the socket opens, on every attempt — so reconnects each get a
|
||||
// fresh ticket. Also strip any long-lived `token` param a caller put in
|
||||
// the URL (e.g. pose.service.js): the bearer itself must never travel in
|
||||
// a query string.
|
||||
const urlObj = new URL(url);
|
||||
urlObj.searchParams.delete('token');
|
||||
const connectUrl = await withWsTicket(urlObj.toString());
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(url);
|
||||
const ws = new WebSocket(connectUrl);
|
||||
const timeout = setTimeout(() => {
|
||||
ws.close();
|
||||
reject(new Error(`Connection timeout after ${this.config.connectionTimeout}ms`));
|
||||
|
||||
Reference in New Issue
Block a user