feat(desktop-client): derive stable client_id from machine identity

Generate the desktop client_id deterministically from OS machine identifiers
(IOPlatformUUID / MachineGuid / /etc/machine-id) scoped per
tenant+workspace+user, with a persisted UUID fallback when the OS lookup
fails. The renderer now requests the id over a new IPC bridge instead of
keeping a localStorage UUID, so reinstalls and storage clears no longer
fork into duplicate clients. Server enforces the id as required and
rejects empty/malformed UUIDs. Also harden bootstrap reveal flow and
single-instance exit so a second launch focuses the existing window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 20:06:37 +08:00
parent fee16d3ea9
commit 543fe0635f
7 changed files with 358 additions and 131 deletions
@@ -25,9 +25,14 @@ interface DesktopSession {
desktopClient: DesktopClientInfo | null;
}
interface ResolvedDeviceInfo {
device_name: string;
os: string;
cpu_arch: string;
}
const sessionStorageKey = "geo.desktop.session.v1";
const apiBaseURLStorageKey = "geo.desktop.api-base-url";
const clientRegistrationStoragePrefix = "geo.desktop.client-registration.v1";
const session = shallowRef<DesktopSession | null>(readStoredSession());
const apiBaseURL = shallowRef(readStoredApiBaseURL());
@@ -126,47 +131,6 @@ function createAuthenticatedClient() {
});
}
function clientRegistrationStorageKey(user: UserInfo): string {
return `${clientRegistrationStoragePrefix}:${user.primary_workspace_id}:${user.id}`;
}
function readStoredClientRegistrationID(user: UserInfo): string | null {
if (typeof window === "undefined") {
return null;
}
return window.localStorage.getItem(clientRegistrationStorageKey(user));
}
function persistClientRegistrationID(user: UserInfo, clientID: string): void {
if (typeof window === "undefined") {
return;
}
window.localStorage.setItem(clientRegistrationStorageKey(user), clientID);
}
function generateUUID(): string {
if (globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID();
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
const random = Math.floor(Math.random() * 16);
const value = char === "x" ? random : (random & 0x3) | 0x8;
return value.toString(16);
});
}
function getOrCreateClientRegistrationID(user: UserInfo): string {
const existing = readStoredClientRegistrationID(user)?.trim();
if (existing) {
return existing;
}
const generated = generateUUID();
persistClientRegistrationID(user, generated);
return generated;
}
function resolveFallbackPlatform(): string {
const currentNavigator = globalThis.navigator as Navigator & {
userAgentData?: { platform?: string };
@@ -202,14 +166,7 @@ function resolveFallbackDeviceName(platform: string): string {
return "Current Desktop";
}
async function resolveDeviceInfo(): Promise<{ device_name: string; os: string; cpu_arch: string }> {
const platform = resolveFallbackPlatform();
const fallback = {
device_name: resolveFallbackDeviceName(platform),
os: resolveFallbackOS(platform),
cpu_arch: "unknown",
};
async function resolveDeviceInfo(): Promise<ResolvedDeviceInfo> {
try {
const info = await window.desktopBridge?.app?.deviceInfo?.();
if (info?.device_name && info.os && info.cpu_arch) {
@@ -219,21 +176,60 @@ async function resolveDeviceInfo(): Promise<{ device_name: string; os: string; c
console.warn("[desktop-session] deviceInfo bridge failed", err);
}
return fallback;
const platform = resolveFallbackPlatform();
return {
device_name: resolveFallbackDeviceName(platform),
os: resolveFallbackOS(platform),
cpu_arch: "unknown",
};
}
async function resolveDesktopClientID(user: UserInfo): Promise<string> {
const clientID = await window.desktopBridge?.app?.clientId?.({
tenant_id: user.tenant_id,
workspace_id: user.primary_workspace_id,
user_id: user.id,
});
if (!clientID) {
throw new Error("无法生成本机账号 client_id,请重启桌面客户端后重试");
}
return clientID;
}
async function syncStoredDesktopClientDeviceInfo(): Promise<void> {
const current = session.value;
if (typeof window === "undefined" || current?.mode !== "authenticated" || !current.desktopClient) {
if (typeof window === "undefined" || current?.mode !== "authenticated" || !current.desktopClient || !current.user) {
return;
}
const device = await resolveDeviceInfo();
const [device, clientID] = await Promise.all([
resolveDeviceInfo(),
resolveDesktopClientID(current.user),
]);
if (session.value !== current) {
return;
}
const desktopClient = current.desktopClient;
if (desktopClient.id !== clientID) {
try {
const registered = await registerDesktopClient(current.user, device);
const latest = session.value;
if (latest?.mode !== "authenticated" || latest.user?.id !== current.user?.id) {
return;
}
persistSession({
...latest,
clientToken: registered.client_token,
clientTokenExpiresAt: registered.expires_at,
desktopClient: registered.client,
});
} catch (err) {
console.warn("[desktop-session] stable client migration failed", err);
}
return;
}
if (
desktopClient.device_name === device.device_name
&& desktopClient.os === device.os
@@ -253,18 +249,32 @@ async function syncStoredDesktopClientDeviceInfo(): Promise<void> {
});
}
async function registerPayload(user: UserInfo): Promise<DesktopClientRegisterRequest> {
const device = await resolveDeviceInfo();
async function registerPayload(
user: UserInfo,
device?: ResolvedDeviceInfo,
): Promise<DesktopClientRegisterRequest> {
const resolvedDevice = device ?? await resolveDeviceInfo();
return {
client_id: getOrCreateClientRegistrationID(user),
device_name: device.device_name,
os: device.os,
cpu_arch: device.cpu_arch,
client_id: await resolveDesktopClientID(user),
device_name: resolvedDevice.device_name,
os: resolvedDevice.os,
cpu_arch: resolvedDevice.cpu_arch,
client_version: "0.1.0-dev",
channel: "dev",
};
}
async function registerDesktopClient(
user: UserInfo,
device?: ResolvedDeviceInfo,
): Promise<DesktopClientRegisterResponse> {
const resolvedDevice = device ?? await resolveDeviceInfo();
return createAuthenticatedClient().post<
DesktopClientRegisterResponse,
DesktopClientRegisterRequest
>("/api/desktop/clients/register", await registerPayload(user, resolvedDevice));
}
function createPreviewUser(): UserInfo {
return {
id: 0,
@@ -345,12 +355,7 @@ async function login(payload: LoginRequest): Promise<void> {
desktopClient: null,
});
const registered = await createAuthenticatedClient().post<
DesktopClientRegisterResponse,
DesktopClientRegisterRequest
>("/api/desktop/clients/register", await registerPayload(authData.user));
persistClientRegistrationID(authData.user, registered.client.id);
const registered = await registerDesktopClient(authData.user);
persistSession({
mode: "authenticated",