mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
Detect confirmed CLI, sdk-cli and VSCode callers before mutation so native software, system, tool, cache and beta shapes pass through, while unconfirmed OAuth clients receive a coherent minimum CLI identity. Persist each Claude OAuth credential's upstream account metadata and one stable device ID, derive one stable session per agent conversation, and keep body and header identity synchronized across Messages, streaming and count_tokens. Alias every cloaked third-party custom tool through caller-stable opaque MCP names and restore declarations, choices, history, references, non-stream responses and SSE events without changing tool ownership. Implement the Claude Code 2.1.220 CCH algorithm over the final serialized request bytes, align currentDate and first-user cache layout, update the official beta/header baseline, and use upstream count_tokens for OAuth and first-party Anthropic credentials. Match the 2.1.220 TLS ClientHello so the transport fingerprint agrees with the identity the request now claims, and document the CLI defaults and automatic OAuth signing / tool alias behaviour in config.example.yaml.
197 lines
5.7 KiB
Go
197 lines
5.7 KiB
Go
package helps
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func resetUserIDCache() {
|
|
userIDCacheMu.Lock()
|
|
userIDCache = make(map[string]userIDCacheEntry)
|
|
userIDCacheMu.Unlock()
|
|
}
|
|
|
|
func TestGenerateFakeUserIDUsesClaudeCode220JSONShape(t *testing.T) {
|
|
userID := GenerateFakeUserID()
|
|
if !IsValidUserID(userID) {
|
|
t.Fatalf("user ID %q is not valid", userID)
|
|
}
|
|
var value claudeMetadataUserID
|
|
if errUnmarshal := json.Unmarshal([]byte(userID), &value); errUnmarshal != nil {
|
|
t.Fatalf("unmarshal user ID: %v", errUnmarshal)
|
|
}
|
|
if value.AccountUUID != "" {
|
|
t.Fatalf("account_uuid = %q, want empty", value.AccountUUID)
|
|
}
|
|
}
|
|
|
|
func TestCachedUserIDUsesCachedClaudeSessionID(t *testing.T) {
|
|
resetUserIDCache()
|
|
resetSessionIDCache()
|
|
|
|
const key = "api-key-shared-session"
|
|
sessionID := CachedSessionID(key)
|
|
userID := CachedUserID(key)
|
|
var value claudeMetadataUserID
|
|
if errUnmarshal := json.Unmarshal([]byte(userID), &value); errUnmarshal != nil {
|
|
t.Fatalf("unmarshal user ID: %v", errUnmarshal)
|
|
}
|
|
if value.SessionID != sessionID {
|
|
t.Fatalf("metadata session_id = %q, header session ID = %q", value.SessionID, sessionID)
|
|
}
|
|
}
|
|
|
|
func TestCachedUserID_ReusesWithinTTL(t *testing.T) {
|
|
resetUserIDCache()
|
|
|
|
first := CachedUserID("api-key-1")
|
|
second := CachedUserID("api-key-1")
|
|
|
|
if first == "" {
|
|
t.Fatal("expected generated user_id to be non-empty")
|
|
}
|
|
if first != second {
|
|
t.Fatalf("expected cached user_id to be reused, got %q and %q", first, second)
|
|
}
|
|
}
|
|
|
|
func TestCachedUserID_ExpiresAfterTTL(t *testing.T) {
|
|
resetUserIDCache()
|
|
|
|
expiredID := CachedUserID("api-key-expired")
|
|
cacheKey := userIDCacheKey("api-key-expired")
|
|
userIDCacheMu.Lock()
|
|
userIDCache[cacheKey] = userIDCacheEntry{
|
|
value: expiredID,
|
|
expire: time.Now().Add(-time.Minute),
|
|
}
|
|
userIDCacheMu.Unlock()
|
|
|
|
newID := CachedUserID("api-key-expired")
|
|
if newID == expiredID {
|
|
t.Fatalf("expected expired user_id to be replaced, got %q", newID)
|
|
}
|
|
if newID == "" {
|
|
t.Fatal("expected regenerated user_id to be non-empty")
|
|
}
|
|
}
|
|
|
|
func TestCachedUserID_IsScopedByAPIKey(t *testing.T) {
|
|
resetUserIDCache()
|
|
|
|
first := CachedUserID("api-key-1")
|
|
second := CachedUserID("api-key-2")
|
|
|
|
if first == second {
|
|
t.Fatalf("expected different API keys to have different user_ids, got %q", first)
|
|
}
|
|
}
|
|
|
|
func TestCachedUserID_RenewsTTLOnHit(t *testing.T) {
|
|
resetUserIDCache()
|
|
|
|
key := "api-key-renew"
|
|
id := CachedUserID(key)
|
|
cacheKey := userIDCacheKey(key)
|
|
|
|
soon := time.Now()
|
|
userIDCacheMu.Lock()
|
|
userIDCache[cacheKey] = userIDCacheEntry{
|
|
value: id,
|
|
expire: soon.Add(2 * time.Second),
|
|
}
|
|
userIDCacheMu.Unlock()
|
|
|
|
if refreshed := CachedUserID(key); refreshed != id {
|
|
t.Fatalf("expected cached user_id to be reused before expiry, got %q", refreshed)
|
|
}
|
|
|
|
userIDCacheMu.RLock()
|
|
entry := userIDCache[cacheKey]
|
|
userIDCacheMu.RUnlock()
|
|
|
|
if entry.expire.Sub(soon) < 30*time.Minute {
|
|
t.Fatalf("expected TTL to renew, got %v remaining", entry.expire.Sub(soon))
|
|
}
|
|
}
|
|
|
|
func TestCachedUserIDRequiredHomeReusesKVAcrossLocalCacheReset(t *testing.T) {
|
|
resetUserIDCache()
|
|
client := newFakeClaudeIDKVClient()
|
|
useFakeClaudeIDKVClient(t, client, true, nil)
|
|
|
|
first, errFirst := CachedUserIDRequired(context.Background(), "api-key-1")
|
|
if errFirst != nil {
|
|
t.Fatalf("CachedUserIDRequired() first error = %v", errFirst)
|
|
}
|
|
resetUserIDCache()
|
|
second, errSecond := CachedUserIDRequired(context.Background(), "api-key-1")
|
|
if errSecond != nil {
|
|
t.Fatalf("CachedUserIDRequired() second error = %v", errSecond)
|
|
}
|
|
if first != second {
|
|
t.Fatalf("user id = %q then %q, want same Home KV value", first, second)
|
|
}
|
|
if !IsValidUserID(first) {
|
|
t.Fatalf("user id %q is not valid", first)
|
|
}
|
|
if client.setCount != 2 {
|
|
t.Fatalf("KVSetNX count = %d, want 2 (session and user ID)", client.setCount)
|
|
}
|
|
if client.expireCount != 1 || client.lastExpireTTL != userIDTTL {
|
|
t.Fatalf("KVExpire count/ttl = %d/%v, want 1/%v", client.expireCount, client.lastExpireTTL, userIDTTL)
|
|
}
|
|
if client.lastSetTTL != userIDTTL {
|
|
t.Fatalf("KVSetNX ttl = %v, want %v", client.lastSetTTL, userIDTTL)
|
|
}
|
|
}
|
|
|
|
func TestCachedUserIDRequiredEmptyAPIKeyDoesNotUseHomeKV(t *testing.T) {
|
|
client := newFakeClaudeIDKVClient()
|
|
useFakeClaudeIDKVClient(t, client, true, nil)
|
|
|
|
value, errValue := CachedUserIDRequired(context.Background(), "")
|
|
if errValue != nil {
|
|
t.Fatalf("CachedUserIDRequired(empty) error = %v", errValue)
|
|
}
|
|
if !IsValidUserID(value) {
|
|
t.Fatalf("user id %q is not valid", value)
|
|
}
|
|
if client.getCount != 0 || client.setCount != 0 || client.expireCount != 0 {
|
|
t.Fatalf("KV calls = get %d set %d expire %d, want all zero", client.getCount, client.setCount, client.expireCount)
|
|
}
|
|
}
|
|
|
|
func TestCachedUserIDRequiredHomeKVFailures(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
client *fakeClaudeIDKVClient
|
|
}{
|
|
{name: "get", client: &fakeClaudeIDKVClient{values: make(map[string][]byte), getErr: errors.New("get failed")}},
|
|
{name: "set", client: &fakeClaudeIDKVClient{values: make(map[string][]byte), setErr: errors.New("set failed")}},
|
|
{name: "expire", client: &fakeClaudeIDKVClient{values: map[string][]byte{
|
|
claudeUserIDKVKey("api-key-1"): []byte(GenerateFakeUserID()),
|
|
}, expireErr: errors.New("expire failed")}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
useFakeClaudeIDKVClient(t, tc.client, true, nil)
|
|
if _, errValue := CachedUserIDRequired(context.Background(), "api-key-1"); errValue == nil {
|
|
t.Fatalf("CachedUserIDRequired() error = nil, want error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCachedUserIDRequiredHomeRequiresReadAfterSet(t *testing.T) {
|
|
client := newFakeClaudeIDKVClient()
|
|
client.setNoPersist = true
|
|
useFakeClaudeIDKVClient(t, client, true, nil)
|
|
|
|
if _, errValue := CachedUserIDRequired(context.Background(), "api-key-1"); errValue == nil {
|
|
t.Fatalf("CachedUserIDRequired() error = nil, want missing-after-set error")
|
|
}
|
|
}
|