mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +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>
81 lines
2.9 KiB
Go
81 lines
2.9 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
)
|
|
|
|
// Client metadata context keys.
|
|
//
|
|
// Populated by ClientMetadata middleware from X-Client-Platform / X-Client-Version /
|
|
// X-Client-OS request headers. Sent by every first-party client (Web, Desktop, CLI,
|
|
// Daemon) so the server can split logs / metrics / gating decisions by caller
|
|
// without having to reverse-engineer User-Agent strings or upgrade payloads.
|
|
//
|
|
// All three values are best-effort: handlers must treat missing values as
|
|
// "unknown" and never make security decisions based on them — these headers
|
|
// are client-controlled and trivial to spoof.
|
|
type clientMetadataKey int
|
|
|
|
const (
|
|
ctxKeyClientPlatform clientMetadataKey = iota
|
|
ctxKeyClientVersion
|
|
ctxKeyClientOS
|
|
)
|
|
|
|
// Header names — exported so other packages (request logger, realtime hub)
|
|
// can stay in sync without re-declaring magic strings.
|
|
const (
|
|
HeaderClientPlatform = "X-Client-Platform"
|
|
HeaderClientVersion = "X-Client-Version"
|
|
HeaderClientOS = "X-Client-OS"
|
|
)
|
|
|
|
// ClientMetadata extracts X-Client-Platform / X-Client-Version / X-Client-OS
|
|
// from the request and stashes them in the request context so downstream
|
|
// handlers and the request logger can read them via ClientMetadataFromContext.
|
|
//
|
|
// Wired in router.go before route mounting so every authenticated and
|
|
// unauthenticated handler benefits from the same observability dimensions.
|
|
func ClientMetadata(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
if v := r.Header.Get(HeaderClientPlatform); v != "" {
|
|
ctx = context.WithValue(ctx, ctxKeyClientPlatform, v)
|
|
}
|
|
if v := r.Header.Get(HeaderClientVersion); v != "" {
|
|
ctx = context.WithValue(ctx, ctxKeyClientVersion, v)
|
|
}
|
|
if v := r.Header.Get(HeaderClientOS); v != "" {
|
|
ctx = context.WithValue(ctx, ctxKeyClientOS, v)
|
|
}
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// ClientMetadataFromContext returns the platform/version/os captured from
|
|
// X-Client-* headers. Empty strings are returned for any value that wasn't
|
|
// sent — callers must treat missing values as "unknown" rather than failing.
|
|
func ClientMetadataFromContext(ctx context.Context) (platform, version, os string) {
|
|
platform, _ = ctx.Value(ctxKeyClientPlatform).(string)
|
|
version, _ = ctx.Value(ctxKeyClientVersion).(string)
|
|
os, _ = ctx.Value(ctxKeyClientOS).(string)
|
|
return platform, version, os
|
|
}
|
|
|
|
// SetClientMetadata explicitly attaches client metadata to a context. Used
|
|
// by the realtime hub, where metadata arrives via WS query parameters
|
|
// (`client_platform`, `client_version`, `client_os`) instead of headers.
|
|
func SetClientMetadata(ctx context.Context, platform, version, os string) context.Context {
|
|
if platform != "" {
|
|
ctx = context.WithValue(ctx, ctxKeyClientPlatform, platform)
|
|
}
|
|
if version != "" {
|
|
ctx = context.WithValue(ctx, ctxKeyClientVersion, version)
|
|
}
|
|
if os != "" {
|
|
ctx = context.WithValue(ctx, ctxKeyClientOS, os)
|
|
}
|
|
return ctx
|
|
}
|