import type { ServerWebSocket } from 'bun'; import { getMusicServerWsUrl } from '../api/router'; // Platform side of the two cliamp sockets. Both used to spawn processes here — the `cliamp` player and a // `parec` capture — which put the whole local-audio pipeline inside the thin proxy. They now live in the // music PLUGIN (`plugins/music/cliamp/cliamp-ws.ts`), and this is what is left of them: authenticate the browser // (done before the upgrade, in server.tsx), then pass frames through in both directions without reading // them. Text or binary, no inspection — same dumb-pipe shape as the vault notifications relay. export type CliampWSData = { provider: 'cliamp' | 'cliamp-audio'; search?: string; // the browser's query string, forwarded minus the platform token }; type UpstreamState = { ws: WebSocket | null; queue: (string | Uint8Array)[]; ready: boolean; }; // Bun hands frames over as `string | Buffer`; a Buffer is a Uint8Array at runtime, so forward as-is // rather than copying every PCM chunk. const asPayload = (raw: string | Buffer): string | Uint8Array => typeof raw === 'string' ? raw : (raw as Uint8Array); // The sidecar has no use for the platform JWT and should not see it. const forwardedQuery = (search: string | undefined): string => { const params = new URLSearchParams(search ?? ''); params.delete('token'); const qs = params.toString(); return qs ? `?${qs}` : ''; }; function createCliampRelay(path: string) { const upstreams = new Map, UpstreamState>(); return { open(ws: ServerWebSocket) { const base = getMusicServerWsUrl(); if (!base) { try { ws.close(1011, 'Music sidecar not available'); } catch { /* already closed */ } return; } const state: UpstreamState = { ws: null, queue: [], ready: false }; upstreams.set(ws, state); const upstream = new WebSocket(`${base}${path}${forwardedQuery(ws.data.search)}`); upstream.binaryType = 'arraybuffer'; state.ws = upstream; upstream.addEventListener('open', () => { state.ready = true; for (const m of state.queue) upstream.send(m); state.queue.length = 0; }); upstream.addEventListener('message', (ev) => { try { ws.send(ev.data as string | ArrayBuffer); } catch { /* client gone */ } }); upstream.addEventListener('close', (ev) => { upstreams.delete(ws); try { ws.close(ev.code || 1000, ev.reason || ''); } catch { /* already closed */ } }); upstream.addEventListener('error', () => { upstreams.delete(ws); try { ws.close(1011, 'upstream error'); } catch { /* already closed */ } }); }, message(ws: ServerWebSocket, raw: string | Buffer) { const state = upstreams.get(ws); if (!state) return; const payload = asPayload(raw); if (state.ready && state.ws) state.ws.send(payload); else state.queue.push(payload); // buffer until the upstream socket opens }, close(ws: ServerWebSocket) { const state = upstreams.get(ws); if (!state) return; try { state.ws?.close(); } catch { /* already closed */ } upstreams.delete(ws); }, drain() {}, }; } export const cliampWebsocket = createCliampRelay('/cliamp/ws'); export const cliampAudioWebsocket = createCliampRelay('/cliamp/audio/ws');