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
@@ -36,7 +36,7 @@ func (s *DesktopClientService) WithTaskService(taskSvc *DesktopTaskService) *Des
}
type RegisterDesktopClientRequest struct {
ClientID string `json:"client_id"`
ClientID string `json:"client_id" binding:"required"`
DeviceName string `json:"device_name" binding:"required"`
OS string `json:"os" binding:"required"`
CPUArch string `json:"cpu_arch"`
@@ -107,7 +107,11 @@ func (s *DesktopClientService) Register(ctx context.Context, actor auth.Actor, r
return nil, response.ErrInternal(50071, "desktop_client_token_failed", "failed to generate desktop client token")
}
clientID := resolveRegisterClientID(req.ClientID)
clientID, err := parseRegisterClientID(req.ClientID)
if err != nil {
return nil, response.ErrBadRequest(40033, "invalid_desktop_client_id", "desktop client_id must be a non-empty uuid generated by the desktop client")
}
client, err := s.repo.Register(ctx, repository.RegisterDesktopClientParams{
ID: clientID,
TenantID: actor.TenantID,
@@ -298,10 +302,10 @@ func parseDesktopAccountIDs(values []string) []uuid.UUID {
return accountIDs
}
func resolveRegisterClientID(value string) uuid.UUID {
func parseRegisterClientID(value string) (uuid.UUID, error) {
parsed, err := uuid.Parse(strings.TrimSpace(value))
if err == nil && parsed != uuid.Nil {
return parsed
if err != nil || parsed == uuid.Nil {
return uuid.Nil, fmt.Errorf("invalid desktop client id")
}
return uuid.New()
return parsed, nil
}