package platform import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/http" "strings" "time" ) // 代铸 (proxy-mint) on the desktop. The device-authorization flow yields a bootstrap // machine token (scope app:cdrop, no meta) that proves the user's identity but is not a // cdrop-managed device. This call exchanges it for a managed device session bound to this // device's stable device_id, so the desktop joins cdrop's unified device list and is // managed exactly like a browser — the same model, not a separate native-only track. // DeviceSessionResult is the managed device session minted by 代铸: the device tokens that // supersede the bootstrap token, plus the verified identity (so a native client, which only // ever sees nameless machine tokens, shows the real display name instead of the sub UUID). type DeviceSessionResult struct { AccessToken string RefreshToken string ExpiresIn int DeviceID string UserID string Name string Avatar string } // MintDeviceSession POSTs to {apiBase}/api/auth/device-session with the bootstrap token as // Bearer; cdrop vouches for the verified subject and has the broker mint (R2-idempotent by // user+app+meta) a session bound to deviceID. Re-login with the same deviceID rotates the // one session rather than piling up duplicates. func MintDeviceSession(ctx context.Context, apiBase, bootstrapToken, deviceID, deviceName, deviceType string) (*DeviceSessionResult, error) { body, err := json.Marshal(map[string]string{ "device_id": deviceID, "device_name": deviceName, "device_type": deviceType, }) if err != nil { return nil, fmt.Errorf("device-session: marshal: %w", err) } endpoint := strings.TrimRight(apiBase, "/") + "/api/auth/device-session" req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("device-session: build request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+bootstrapToken) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("device-session: request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("device-session: status %d", resp.StatusCode) } var dr struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` DeviceID string `json:"device_id"` UserID string `json:"user_id"` Name string `json:"name"` Avatar string `json:"avatar"` } if err := json.NewDecoder(resp.Body).Decode(&dr); err != nil { return nil, fmt.Errorf("device-session: decode: %w", err) } if dr.AccessToken == "" { return nil, errors.New("device-session: response missing access token") } return &DeviceSessionResult{ AccessToken: dr.AccessToken, RefreshToken: dr.RefreshToken, ExpiresIn: dr.ExpiresIn, DeviceID: dr.DeviceID, UserID: dr.UserID, Name: dr.Name, Avatar: dr.Avatar, }, nil }