mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
* feat: identify clients via X-Client-Platform/Version/OS
Adds client identification headers (and matching WS query params) across
all first-party clients so the server can split logs/metrics/gating by
caller without parsing User-Agent.
- HTTP: X-Client-Platform, X-Client-Version, X-Client-OS
- WS: client_platform, client_version, client_os query params
- Platform ∈ {web, desktop, cli, daemon}; OS ∈ {macos, windows, linux}
Wired through the shared TS ApiClient/WSClient via a new identity option
on CoreProvider. Web reads its version from package.json/env; Desktop
captures version + OS synchronously in preload via sendSync IPC. Go CLI
and daemon clients populate the same headers using runtime.GOOS
(normalized darwin → macos).
Server-side adds a ClientMetadata middleware that stashes the headers in
request context; the request logger and logger.RequestAttrs surface them
on every access log and handler-level log. Realtime hub logs the same
fields on websocket connect.
CORS allowlist extended for the new headers.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: address client-identity PR nits
- Memoize the CoreProvider identity object on Web and Desktop, and key
WSProvider's effect on identity primitives instead of the object
reference, so unrelated parent re-renders no longer tear down and
reconnect the WebSocket.
- Add direct header-injection tests for the CLI and daemon Go HTTP
clients (X-Client-Platform/Version/OS) and a normalizeGOOS unit test
on both packages.
- Add a TS test for WSClient that asserts client_platform/client_version/
client_os land on the upgrade URL and never leak the auth token.
- Add a hub test that dials the WS endpoint with client_* query params
and asserts the "websocket connected" log entry surfaces them as
structured attributes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { WSClient } from "./ws-client";
|
|
|
|
// Capture URL passed to WebSocket so we can assert the connect-time
|
|
// query string. We don't simulate the full WS lifecycle here — only the
|
|
// upgrade URL construction, which is what carries client identity.
|
|
class FakeWebSocket {
|
|
static lastUrl: string | null = null;
|
|
// Fields read by WSClient.connect()/disconnect(), all no-op here.
|
|
onopen: (() => void) | null = null;
|
|
onmessage: ((ev: { data: string }) => void) | null = null;
|
|
onclose: (() => void) | null = null;
|
|
onerror: (() => void) | null = null;
|
|
readyState = 0;
|
|
constructor(url: string) {
|
|
FakeWebSocket.lastUrl = url;
|
|
}
|
|
close() {}
|
|
send() {}
|
|
}
|
|
|
|
describe("WSClient", () => {
|
|
beforeEach(() => {
|
|
FakeWebSocket.lastUrl = null;
|
|
vi.stubGlobal("WebSocket", FakeWebSocket as unknown as typeof WebSocket);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("includes client identity in the upgrade URL when configured", () => {
|
|
const ws = new WSClient("ws://example.test/ws", {
|
|
identity: { platform: "desktop", version: "1.2.3", os: "macos" },
|
|
});
|
|
ws.setAuth("tok", "acme");
|
|
ws.connect();
|
|
|
|
const url = new URL(FakeWebSocket.lastUrl!);
|
|
expect(url.searchParams.get("workspace_slug")).toBe("acme");
|
|
expect(url.searchParams.get("client_platform")).toBe("desktop");
|
|
expect(url.searchParams.get("client_version")).toBe("1.2.3");
|
|
expect(url.searchParams.get("client_os")).toBe("macos");
|
|
// Token must never appear in the URL — it is delivered as the first
|
|
// WS message in token mode.
|
|
expect(url.searchParams.has("token")).toBe(false);
|
|
});
|
|
|
|
it("omits client_* params when identity is not configured", () => {
|
|
const ws = new WSClient("ws://example.test/ws");
|
|
ws.setAuth("tok", "acme");
|
|
ws.connect();
|
|
|
|
const url = new URL(FakeWebSocket.lastUrl!);
|
|
expect(url.searchParams.has("client_platform")).toBe(false);
|
|
expect(url.searchParams.has("client_version")).toBe(false);
|
|
expect(url.searchParams.has("client_os")).toBe(false);
|
|
});
|
|
|
|
it("only includes the identity fields that are set", () => {
|
|
const ws = new WSClient("ws://example.test/ws", {
|
|
identity: { platform: "cli" },
|
|
});
|
|
ws.setAuth("tok", "acme");
|
|
ws.connect();
|
|
|
|
const url = new URL(FakeWebSocket.lastUrl!);
|
|
expect(url.searchParams.get("client_platform")).toBe("cli");
|
|
expect(url.searchParams.has("client_version")).toBe(false);
|
|
expect(url.searchParams.has("client_os")).toBe(false);
|
|
});
|
|
});
|