metadata_userid.go 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. package service
  2. import (
  3. "encoding/json"
  4. "regexp"
  5. "strings"
  6. )
  7. // NewMetadataFormatMinVersion is the minimum Claude Code version that uses
  8. // JSON-formatted metadata.user_id instead of the legacy concatenated string.
  9. const NewMetadataFormatMinVersion = "2.1.78"
  10. // ParsedUserID represents the components extracted from a metadata.user_id value.
  11. type ParsedUserID struct {
  12. DeviceID string // 64-char hex (or arbitrary client id)
  13. AccountUUID string // may be empty
  14. SessionID string // UUID
  15. IsNewFormat bool // true if the original was JSON format
  16. }
  17. // legacyUserIDRegex matches the legacy user_id format:
  18. //
  19. // user_{64hex}_account_{optional_uuid}_session_{uuid}
  20. var legacyUserIDRegex = regexp.MustCompile(`^user_([a-fA-F0-9]{64})_account_([a-fA-F0-9-]*)_session_([a-fA-F0-9-]{36})$`)
  21. // jsonUserID is the JSON structure for the new metadata.user_id format.
  22. type jsonUserID struct {
  23. DeviceID string `json:"device_id"`
  24. AccountUUID string `json:"account_uuid"`
  25. SessionID string `json:"session_id"`
  26. }
  27. // ParseMetadataUserID parses a metadata.user_id string in either format.
  28. // Returns nil if the input cannot be parsed.
  29. func ParseMetadataUserID(raw string) *ParsedUserID {
  30. raw = strings.TrimSpace(raw)
  31. if raw == "" {
  32. return nil
  33. }
  34. // Try JSON format first (starts with '{')
  35. if raw[0] == '{' {
  36. var j jsonUserID
  37. if err := json.Unmarshal([]byte(raw), &j); err != nil {
  38. return nil
  39. }
  40. if j.DeviceID == "" || j.SessionID == "" {
  41. return nil
  42. }
  43. return &ParsedUserID{
  44. DeviceID: j.DeviceID,
  45. AccountUUID: j.AccountUUID,
  46. SessionID: j.SessionID,
  47. IsNewFormat: true,
  48. }
  49. }
  50. // Try legacy format
  51. matches := legacyUserIDRegex.FindStringSubmatch(raw)
  52. if matches == nil {
  53. return nil
  54. }
  55. return &ParsedUserID{
  56. DeviceID: matches[1],
  57. AccountUUID: matches[2],
  58. SessionID: matches[3],
  59. IsNewFormat: false,
  60. }
  61. }
  62. // FormatMetadataUserID builds a metadata.user_id string in the format
  63. // appropriate for the given CLI version. Components are the rewritten values
  64. // (not necessarily the originals).
  65. func FormatMetadataUserID(deviceID, accountUUID, sessionID, uaVersion string) string {
  66. if IsNewMetadataFormatVersion(uaVersion) {
  67. b, _ := json.Marshal(jsonUserID{
  68. DeviceID: deviceID,
  69. AccountUUID: accountUUID,
  70. SessionID: sessionID,
  71. })
  72. return string(b)
  73. }
  74. // Legacy format
  75. return "user_" + deviceID + "_account_" + accountUUID + "_session_" + sessionID
  76. }
  77. // IsNewMetadataFormatVersion returns true if the given CLI version uses the
  78. // new JSON metadata.user_id format (>= 2.1.78).
  79. func IsNewMetadataFormatVersion(version string) bool {
  80. if version == "" {
  81. return false
  82. }
  83. return CompareVersions(version, NewMetadataFormatMinVersion) >= 0
  84. }
  85. // ExtractCLIVersion extracts the Claude Code version from a User-Agent string.
  86. // Returns "" if the UA doesn't match the expected pattern.
  87. func ExtractCLIVersion(ua string) string {
  88. matches := claudeCodeUAVersionPattern.FindStringSubmatch(ua)
  89. if len(matches) >= 2 {
  90. return matches[1]
  91. }
  92. return ""
  93. }