mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +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>
70 lines
2.7 KiB
TypeScript
70 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useMemo } from "react";
|
|
import { CoreProvider } from "@multica/core/platform";
|
|
import packageJson from "../package.json";
|
|
import { WebNavigationProvider } from "@/platform/navigation";
|
|
import {
|
|
setLoggedInCookie,
|
|
clearLoggedInCookie,
|
|
} from "@/features/auth/auth-cookie";
|
|
import { PageviewTracker } from "./pageview-tracker";
|
|
|
|
// Legacy token in localStorage → keep this session in token mode so users who
|
|
// logged in before the cookie-auth migration stay authed. They migrate to
|
|
// cookie mode on their next logout/login cycle (logout clears multica_token).
|
|
// Sunset: once telemetry shows <1% of sessions still carry multica_token,
|
|
// delete this branch and hard-code `cookieAuth` — the localStorage token is
|
|
// XSS-exposed and is the exact thing the cookie migration exists to remove.
|
|
function hasLegacyToken(): boolean {
|
|
if (typeof window === "undefined") return false;
|
|
try {
|
|
return Boolean(window.localStorage.getItem("multica_token"));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Derive WebSocket URL from the page origin so self-hosted / LAN deployments
|
|
// work without explicit NEXT_PUBLIC_WS_URL. The Next.js rewrite rule
|
|
// (/ws → backend) handles proxying.
|
|
function deriveWsUrl(): string | undefined {
|
|
if (process.env.NEXT_PUBLIC_WS_URL) return process.env.NEXT_PUBLIC_WS_URL;
|
|
if (typeof window === "undefined") return undefined;
|
|
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
return `${proto}//${window.location.host}/ws`;
|
|
}
|
|
|
|
// Build-time version preferred (CI sets NEXT_PUBLIC_APP_VERSION to a git tag
|
|
// or sha so different deploys are distinguishable in server logs); fall back
|
|
// to the package.json version so local dev still reports something useful.
|
|
const WEB_VERSION =
|
|
process.env.NEXT_PUBLIC_APP_VERSION || packageJson.version || "dev";
|
|
|
|
export function WebProviders({ children }: { children: React.ReactNode }) {
|
|
const cookieAuth = !hasLegacyToken();
|
|
// Stable identity reference so downstream effects keyed on it don't see a
|
|
// new object on every parent render.
|
|
const identity = useMemo(
|
|
() => ({ platform: "web", version: WEB_VERSION }),
|
|
[],
|
|
);
|
|
return (
|
|
<CoreProvider
|
|
apiBaseUrl={process.env.NEXT_PUBLIC_API_URL}
|
|
wsUrl={deriveWsUrl()}
|
|
cookieAuth={cookieAuth}
|
|
onLogin={setLoggedInCookie}
|
|
onLogout={clearLoggedInCookie}
|
|
identity={identity}
|
|
>
|
|
{/* Suspense boundary is required by Next.js for useSearchParams in
|
|
a client component mounted this high in the tree. */}
|
|
<Suspense fallback={null}>
|
|
<PageviewTracker />
|
|
</Suspense>
|
|
<WebNavigationProvider>{children}</WebNavigationProvider>
|
|
</CoreProvider>
|
|
);
|
|
}
|