286 lines
8.5 KiB
Go
286 lines
8.5 KiB
Go
package claude
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
const (
|
|
ClaudeDeviceIDsMetadataKey = "claude_device_ids"
|
|
ClaudeDevicePoolSize = 1
|
|
claudeDeviceIDByteSize = 32
|
|
)
|
|
|
|
// claudeDevicePoolMu guards every concurrent access to a Claude credential's
|
|
// Auth.Metadata map, not just the device pool. A single Auth is shared by all
|
|
// in-flight requests using that credential, and Go maps are not safe for
|
|
// concurrent read/write, so the account-profile and refresh paths have to take
|
|
// the same lock as the pool paths. Reaching into Auth.Metadata directly from a
|
|
// request path is a data race even when the keys differ.
|
|
var claudeDevicePoolMu sync.Mutex
|
|
|
|
// GenerateDeviceIDPool creates the fixed-size device pool stored with a Claude credential.
|
|
func GenerateDeviceIDPool() ([]string, error) {
|
|
deviceIDs := make([]string, 0, ClaudeDevicePoolSize)
|
|
seen := make(map[string]struct{}, ClaudeDevicePoolSize)
|
|
for len(deviceIDs) < ClaudeDevicePoolSize {
|
|
deviceID, errDeviceID := generateDeviceID()
|
|
if errDeviceID != nil {
|
|
return nil, errDeviceID
|
|
}
|
|
if _, exists := seen[deviceID]; exists {
|
|
continue
|
|
}
|
|
seen[deviceID] = struct{}{}
|
|
deviceIDs = append(deviceIDs, deviceID)
|
|
}
|
|
return deviceIDs, nil
|
|
}
|
|
|
|
func generateDeviceID() (string, error) {
|
|
data := make([]byte, claudeDeviceIDByteSize)
|
|
if _, errRead := rand.Read(data); errRead != nil {
|
|
return "", fmt.Errorf("generate Claude device ID: %w", errRead)
|
|
}
|
|
return hex.EncodeToString(data), nil
|
|
}
|
|
|
|
// NormalizeDeviceIDPool returns the first valid device ID in canonical form.
|
|
func NormalizeDeviceIDPool(raw any) []string {
|
|
var values []string
|
|
switch typed := raw.(type) {
|
|
case []string:
|
|
values = typed
|
|
case []any:
|
|
values = make([]string, 0, len(typed))
|
|
for _, value := range typed {
|
|
if text, ok := value.(string); ok {
|
|
values = append(values, text)
|
|
}
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
deviceIDs := make([]string, 0, min(len(values), ClaudeDevicePoolSize))
|
|
seen := make(map[string]struct{}, ClaudeDevicePoolSize)
|
|
for _, value := range values {
|
|
deviceID := strings.ToLower(strings.TrimSpace(value))
|
|
if !ValidDeviceID(deviceID) {
|
|
continue
|
|
}
|
|
if _, exists := seen[deviceID]; exists {
|
|
continue
|
|
}
|
|
seen[deviceID] = struct{}{}
|
|
deviceIDs = append(deviceIDs, deviceID)
|
|
if len(deviceIDs) == ClaudeDevicePoolSize {
|
|
break
|
|
}
|
|
}
|
|
return deviceIDs
|
|
}
|
|
|
|
// HasCanonicalDeviceIDPool reports whether raw stores exactly one valid device ID.
|
|
func HasCanonicalDeviceIDPool(raw any) bool {
|
|
var values []string
|
|
switch typed := raw.(type) {
|
|
case []string:
|
|
values = typed
|
|
case []any:
|
|
values = make([]string, 0, len(typed))
|
|
for _, value := range typed {
|
|
text, ok := value.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
values = append(values, text)
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
normalized := NormalizeDeviceIDPool(values)
|
|
return len(values) == ClaudeDevicePoolSize && len(normalized) == ClaudeDevicePoolSize && values[0] == normalized[0]
|
|
}
|
|
|
|
// EnsureDeviceIDPool repairs or creates the single-device pool in credential metadata.
|
|
func EnsureDeviceIDPool(metadata map[string]any) ([]string, bool, error) {
|
|
claudeDevicePoolMu.Lock()
|
|
defer claudeDevicePoolMu.Unlock()
|
|
|
|
return ensureDeviceIDPoolLocked(metadata)
|
|
}
|
|
|
|
// EnsureDeviceIDPoolFor lazily initializes the metadata map and then ensures the
|
|
// pool, both under the device pool lock.
|
|
//
|
|
// A single *Auth is shared by every concurrent request that selects the same
|
|
// credential, so initializing the map field outside this lock races with the
|
|
// writes below and can abort the process with "concurrent map writes". Callers
|
|
// holding a shared credential must reach the pool through this package rather
|
|
// than touching the map directly.
|
|
func EnsureDeviceIDPoolFor(metadata *map[string]any) ([]string, bool, error) {
|
|
if metadata == nil {
|
|
return nil, false, fmt.Errorf("ensure Claude device pool: metadata pointer is nil")
|
|
}
|
|
claudeDevicePoolMu.Lock()
|
|
defer claudeDevicePoolMu.Unlock()
|
|
|
|
if *metadata == nil {
|
|
*metadata = make(map[string]any)
|
|
}
|
|
return ensureDeviceIDPoolLocked(*metadata)
|
|
}
|
|
|
|
// ReadDeviceIDPool returns the stored pool value, initializing the map when
|
|
// needed, under the device pool lock. Slice values are copied so a caller can
|
|
// never mutate the stored credential identity after the lock is released.
|
|
func ReadDeviceIDPool(metadata *map[string]any) any {
|
|
if metadata == nil {
|
|
return nil
|
|
}
|
|
claudeDevicePoolMu.Lock()
|
|
defer claudeDevicePoolMu.Unlock()
|
|
|
|
if *metadata == nil {
|
|
*metadata = make(map[string]any)
|
|
return nil
|
|
}
|
|
switch stored := (*metadata)[ClaudeDeviceIDsMetadataKey].(type) {
|
|
case []string:
|
|
return append([]string(nil), stored...)
|
|
case []any:
|
|
return append([]any(nil), stored...)
|
|
default:
|
|
return stored
|
|
}
|
|
}
|
|
|
|
// StoreDeviceIDPool writes a defensive copy of deviceIDs under the device pool lock.
|
|
func StoreDeviceIDPool(metadata *map[string]any, deviceIDs []string) {
|
|
if metadata == nil {
|
|
return
|
|
}
|
|
claudeDevicePoolMu.Lock()
|
|
defer claudeDevicePoolMu.Unlock()
|
|
|
|
if *metadata == nil {
|
|
*metadata = make(map[string]any)
|
|
}
|
|
(*metadata)[ClaudeDeviceIDsMetadataKey] = append([]string(nil), deviceIDs...)
|
|
}
|
|
|
|
// ReadMetadataString reads a string-valued metadata entry under the metadata
|
|
// lock, so it cannot observe a map being concurrently written by another path.
|
|
func ReadMetadataString(metadata *map[string]any, key string) string {
|
|
if metadata == nil {
|
|
return ""
|
|
}
|
|
claudeDevicePoolMu.Lock()
|
|
defer claudeDevicePoolMu.Unlock()
|
|
|
|
if *metadata == nil {
|
|
return ""
|
|
}
|
|
value, _ := (*metadata)[key].(string)
|
|
return value
|
|
}
|
|
|
|
// StoreMetadataString writes a string-valued metadata entry under the metadata
|
|
// lock, initializing the map when needed. Empty values are skipped so callers can
|
|
// forward optional fields without erasing a previously resolved value.
|
|
func StoreMetadataString(metadata *map[string]any, key, value string) {
|
|
if metadata == nil || strings.TrimSpace(value) == "" {
|
|
return
|
|
}
|
|
claudeDevicePoolMu.Lock()
|
|
defer claudeDevicePoolMu.Unlock()
|
|
|
|
if *metadata == nil {
|
|
*metadata = make(map[string]any)
|
|
}
|
|
(*metadata)[key] = value
|
|
}
|
|
|
|
// StoreMetadataValue writes an arbitrary metadata entry under the metadata lock,
|
|
// initializing the map when needed.
|
|
func StoreMetadataValue(metadata *map[string]any, key string, value any) {
|
|
if metadata == nil {
|
|
return
|
|
}
|
|
claudeDevicePoolMu.Lock()
|
|
defer claudeDevicePoolMu.Unlock()
|
|
|
|
if *metadata == nil {
|
|
*metadata = make(map[string]any)
|
|
}
|
|
(*metadata)[key] = value
|
|
}
|
|
|
|
// EnsureMetadataMap initializes the metadata map under the metadata lock.
|
|
func EnsureMetadataMap(metadata *map[string]any) {
|
|
if metadata == nil {
|
|
return
|
|
}
|
|
claudeDevicePoolMu.Lock()
|
|
defer claudeDevicePoolMu.Unlock()
|
|
|
|
if *metadata == nil {
|
|
*metadata = make(map[string]any)
|
|
}
|
|
}
|
|
|
|
// ensureDeviceIDPoolLocked requires claudeDevicePoolMu to be held.
|
|
func ensureDeviceIDPoolLocked(metadata map[string]any) ([]string, bool, error) {
|
|
if metadata == nil {
|
|
return nil, false, fmt.Errorf("ensure Claude device pool: metadata is nil")
|
|
}
|
|
rawDeviceIDs := metadata[ClaudeDeviceIDsMetadataKey]
|
|
deviceIDs := NormalizeDeviceIDPool(rawDeviceIDs)
|
|
changed := !HasCanonicalDeviceIDPool(rawDeviceIDs)
|
|
seen := make(map[string]struct{}, ClaudeDevicePoolSize)
|
|
for _, deviceID := range deviceIDs {
|
|
seen[deviceID] = struct{}{}
|
|
}
|
|
for len(deviceIDs) < ClaudeDevicePoolSize {
|
|
deviceID, errDeviceID := generateDeviceID()
|
|
if errDeviceID != nil {
|
|
return nil, false, errDeviceID
|
|
}
|
|
if _, exists := seen[deviceID]; exists {
|
|
continue
|
|
}
|
|
seen[deviceID] = struct{}{}
|
|
deviceIDs = append(deviceIDs, deviceID)
|
|
}
|
|
|
|
if changed {
|
|
metadata[ClaudeDeviceIDsMetadataKey] = append([]string(nil), deviceIDs...)
|
|
}
|
|
return append([]string(nil), deviceIDs...), changed, nil
|
|
}
|
|
|
|
// SelectDeviceID returns the credential's sole device ID after validating the conversation session.
|
|
func SelectDeviceID(deviceIDs []string, sessionID string) (string, error) {
|
|
deviceIDs = NormalizeDeviceIDPool(deviceIDs)
|
|
if len(deviceIDs) != ClaudeDevicePoolSize {
|
|
return "", fmt.Errorf("select Claude device ID: device pool has %d entries, want %d", len(deviceIDs), ClaudeDevicePoolSize)
|
|
}
|
|
sessionID = strings.TrimSpace(sessionID)
|
|
if sessionID == "" {
|
|
return "", fmt.Errorf("select Claude device ID: session ID is empty")
|
|
}
|
|
return deviceIDs[0], nil
|
|
}
|
|
|
|
// ValidDeviceID reports whether a value matches Claude Code's lowercase 64-hex device format.
|
|
func ValidDeviceID(value string) bool {
|
|
if len(value) != claudeDeviceIDByteSize*2 || value != strings.ToLower(value) {
|
|
return false
|
|
}
|
|
decoded, errDecode := hex.DecodeString(value)
|
|
return errDecode == nil && len(decoded) == claudeDeviceIDByteSize
|
|
}
|