Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
286
backend/internal/auth/claude/identity.go
Normal file
286
backend/internal/auth/claude/identity.go
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in a new issue