| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- package service
- import (
- "encoding/json"
- "regexp"
- "strings"
- )
- // NewMetadataFormatMinVersion is the minimum Claude Code version that uses
- // JSON-formatted metadata.user_id instead of the legacy concatenated string.
- const NewMetadataFormatMinVersion = "2.1.78"
- // ParsedUserID represents the components extracted from a metadata.user_id value.
- type ParsedUserID struct {
- DeviceID string // 64-char hex (or arbitrary client id)
- AccountUUID string // may be empty
- SessionID string // UUID
- IsNewFormat bool // true if the original was JSON format
- }
- // legacyUserIDRegex matches the legacy user_id format:
- //
- // user_{64hex}_account_{optional_uuid}_session_{uuid}
- var legacyUserIDRegex = regexp.MustCompile(`^user_([a-fA-F0-9]{64})_account_([a-fA-F0-9-]*)_session_([a-fA-F0-9-]{36})$`)
- // jsonUserID is the JSON structure for the new metadata.user_id format.
- type jsonUserID struct {
- DeviceID string `json:"device_id"`
- AccountUUID string `json:"account_uuid"`
- SessionID string `json:"session_id"`
- }
- // ParseMetadataUserID parses a metadata.user_id string in either format.
- // Returns nil if the input cannot be parsed.
- func ParseMetadataUserID(raw string) *ParsedUserID {
- raw = strings.TrimSpace(raw)
- if raw == "" {
- return nil
- }
- // Try JSON format first (starts with '{')
- if raw[0] == '{' {
- var j jsonUserID
- if err := json.Unmarshal([]byte(raw), &j); err != nil {
- return nil
- }
- if j.DeviceID == "" || j.SessionID == "" {
- return nil
- }
- return &ParsedUserID{
- DeviceID: j.DeviceID,
- AccountUUID: j.AccountUUID,
- SessionID: j.SessionID,
- IsNewFormat: true,
- }
- }
- // Try legacy format
- matches := legacyUserIDRegex.FindStringSubmatch(raw)
- if matches == nil {
- return nil
- }
- return &ParsedUserID{
- DeviceID: matches[1],
- AccountUUID: matches[2],
- SessionID: matches[3],
- IsNewFormat: false,
- }
- }
- // FormatMetadataUserID builds a metadata.user_id string in the format
- // appropriate for the given CLI version. Components are the rewritten values
- // (not necessarily the originals).
- func FormatMetadataUserID(deviceID, accountUUID, sessionID, uaVersion string) string {
- if IsNewMetadataFormatVersion(uaVersion) {
- b, _ := json.Marshal(jsonUserID{
- DeviceID: deviceID,
- AccountUUID: accountUUID,
- SessionID: sessionID,
- })
- return string(b)
- }
- // Legacy format
- return "user_" + deviceID + "_account_" + accountUUID + "_session_" + sessionID
- }
- // IsNewMetadataFormatVersion returns true if the given CLI version uses the
- // new JSON metadata.user_id format (>= 2.1.78).
- func IsNewMetadataFormatVersion(version string) bool {
- if version == "" {
- return false
- }
- return CompareVersions(version, NewMetadataFormatMinVersion) >= 0
- }
- // ExtractCLIVersion extracts the Claude Code version from a User-Agent string.
- // Returns "" if the UA doesn't match the expected pattern.
- func ExtractCLIVersion(ua string) string {
- matches := claudeCodeUAVersionPattern.FindStringSubmatch(ua)
- if len(matches) >= 2 {
- return matches[1]
- }
- return ""
- }
|