Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
|
|
@ -0,0 +1,457 @@
|
|||
package helps
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
claudeauth "github.com/router-for-me/CLIProxyAPI/v7/internal/auth/claude"
|
||||
homekv "github.com/router-for-me/CLIProxyAPI/v7/internal/home"
|
||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ClaudeAgentSessionUUID maps the downstream agent conversation to one stable UUID,
|
||||
// preserving native Claude Code session signals.
|
||||
func ClaudeAgentSessionUUID(headers http.Header, originalPayload, translatedPayload []byte, metadataSets ...map[string]any) string {
|
||||
return claudeAgentSessionUUID(headers, originalPayload, translatedPayload, metadataSets...)
|
||||
}
|
||||
|
||||
// ClaudeAgentSessionUUIDForRequest preserves Claude-specific session signals only
|
||||
// for a confirmed native caller. Other callers use protocol session fields,
|
||||
// execution metadata, or the stable derived conversation root.
|
||||
func ClaudeAgentSessionUUIDForRequest(headers http.Header, originalPayload, translatedPayload []byte, confirmedClaudeCode bool, metadataSets ...map[string]any) string {
|
||||
if !confirmedClaudeCode {
|
||||
headers = headers.Clone()
|
||||
for key := range headers {
|
||||
if strings.EqualFold(key, "X-Claude-Code-Session-Id") {
|
||||
delete(headers, key)
|
||||
}
|
||||
}
|
||||
originalPayload = withoutClaudeMetadataUserID(originalPayload)
|
||||
translatedPayload = withoutClaudeMetadataUserID(translatedPayload)
|
||||
}
|
||||
return claudeAgentSessionUUID(headers, originalPayload, translatedPayload, metadataSets...)
|
||||
}
|
||||
|
||||
func claudeAgentSessionUUID(headers http.Header, originalPayload, translatedPayload []byte, metadataSets ...map[string]any) string {
|
||||
metadata := mergeClaudeSessionMetadata(metadataSets...)
|
||||
identity := cliproxyauth.ExtractSessionID(headers, originalPayload, metadata)
|
||||
if identity == "" && len(translatedPayload) > 0 {
|
||||
identity = cliproxyauth.ExtractSessionID(headers, translatedPayload, metadata)
|
||||
}
|
||||
if identity == "" {
|
||||
return uuid.NewString()
|
||||
}
|
||||
if strings.HasPrefix(identity, "claude:") {
|
||||
if parsed, errParse := uuid.Parse(strings.TrimPrefix(identity, "claude:")); errParse == nil {
|
||||
return parsed.String()
|
||||
}
|
||||
}
|
||||
if parsed, errParse := uuid.Parse(identity); errParse == nil {
|
||||
return parsed.String()
|
||||
}
|
||||
stableInput := "cli-proxy-api\x00claude\x00agent-conversation\x00" + identity
|
||||
return uuid.NewSHA1(uuid.NameSpaceOID, []byte(stableInput)).String()
|
||||
}
|
||||
|
||||
func withoutClaudeMetadataUserID(payload []byte) []byte {
|
||||
if len(payload) == 0 {
|
||||
return payload
|
||||
}
|
||||
updated, errDelete := sjson.DeleteBytes(payload, "metadata.user_id")
|
||||
if errDelete != nil {
|
||||
return payload
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func mergeClaudeSessionMetadata(metadataSets ...map[string]any) map[string]any {
|
||||
var merged map[string]any
|
||||
for _, metadata := range metadataSets {
|
||||
if len(metadata) == 0 {
|
||||
continue
|
||||
}
|
||||
if merged == nil {
|
||||
merged = make(map[string]any)
|
||||
}
|
||||
for key, value := range metadata {
|
||||
if _, exists := merged[key]; !exists {
|
||||
merged[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
type claudeCredentialDevicePoolKVClient interface {
|
||||
KVGet(context.Context, string) ([]byte, bool, error)
|
||||
KVSet(context.Context, string, []byte, homekv.KVSetOptions) (bool, error)
|
||||
}
|
||||
|
||||
var currentClaudeCredentialDevicePoolKVClient = func() (claudeCredentialDevicePoolKVClient, bool, error) {
|
||||
client, homeMode, errClient := homekv.CurrentKVClient()
|
||||
return client, homeMode, errClient
|
||||
}
|
||||
|
||||
// EnsureClaudeCredentialDevicePoolRequired initializes a credential pool locally,
|
||||
// or coordinates it through Home KV when the selected auth is a remote dispatch clone.
|
||||
func EnsureClaudeCredentialDevicePoolRequired(ctx context.Context, auth *cliproxyauth.Auth) ([]string, error) {
|
||||
if auth == nil {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: auth is nil")
|
||||
}
|
||||
rawCredentialDeviceIDs := claudeauth.ReadDeviceIDPool(&auth.Metadata)
|
||||
if claudeauth.HasCanonicalDeviceIDPool(rawCredentialDeviceIDs) {
|
||||
return claudeauth.NormalizeDeviceIDPool(rawCredentialDeviceIDs), nil
|
||||
}
|
||||
credentialCandidate := claudeauth.NormalizeDeviceIDPool(rawCredentialDeviceIDs)
|
||||
|
||||
client, homeMode, errClient := currentClaudeCredentialDevicePoolKVClient()
|
||||
if !homeMode {
|
||||
deviceIDs, _, errEnsure := claudeauth.EnsureDeviceIDPoolFor(&auth.Metadata)
|
||||
return deviceIDs, errEnsure
|
||||
}
|
||||
if errClient != nil {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: Home KV client: %w", errClient)
|
||||
}
|
||||
identity := strings.TrimSpace(auth.EnsureIndex())
|
||||
if identity == "" {
|
||||
identity = strings.TrimSpace(auth.ID)
|
||||
}
|
||||
if identity == "" {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: credential identity is empty")
|
||||
}
|
||||
key := "cpa:claude:credential-device-pool:" + homekv.HashKeyPart(identity)
|
||||
if raw, found, errGet := client.KVGet(ctx, key); errGet != nil {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: Home KV get: %w", errGet)
|
||||
} else if found {
|
||||
var stored []string
|
||||
if errUnmarshal := json.Unmarshal(raw, &stored); errUnmarshal == nil {
|
||||
if deviceIDs := claudeauth.NormalizeDeviceIDPool(stored); len(deviceIDs) == claudeauth.ClaudeDevicePoolSize {
|
||||
if !claudeauth.HasCanonicalDeviceIDPool(stored) {
|
||||
canonicalRaw, errMarshal := json.Marshal(deviceIDs)
|
||||
if errMarshal != nil {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: marshal canonical Home KV value: %w", errMarshal)
|
||||
}
|
||||
written, errSet := client.KVSet(ctx, key, canonicalRaw, homekv.KVSetOptions{XX: true})
|
||||
if errSet != nil {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: canonicalize Home KV value: %w", errSet)
|
||||
}
|
||||
if !written {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: canonical Home KV value was not written")
|
||||
}
|
||||
}
|
||||
claudeauth.StoreDeviceIDPool(&auth.Metadata, deviceIDs)
|
||||
return deviceIDs, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deviceIDs := credentialCandidate
|
||||
if len(deviceIDs) != claudeauth.ClaudeDevicePoolSize {
|
||||
var errGenerate error
|
||||
deviceIDs, errGenerate = claudeauth.GenerateDeviceIDPool()
|
||||
if errGenerate != nil {
|
||||
return nil, errGenerate
|
||||
}
|
||||
}
|
||||
raw, errMarshal := json.Marshal(deviceIDs)
|
||||
if errMarshal != nil {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: marshal Home KV value: %w", errMarshal)
|
||||
}
|
||||
if _, errSet := client.KVSet(ctx, key, raw, homekv.KVSetOptions{NX: true}); errSet != nil {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: Home KV set: %w", errSet)
|
||||
}
|
||||
raw, found, errGet := client.KVGet(ctx, key)
|
||||
if errGet != nil {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: Home KV reread: %w", errGet)
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: Home KV value missing after set")
|
||||
}
|
||||
var stored []string
|
||||
if errUnmarshal := json.Unmarshal(raw, &stored); errUnmarshal != nil {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: decode Home KV value: %w", errUnmarshal)
|
||||
}
|
||||
deviceIDs = claudeauth.NormalizeDeviceIDPool(stored)
|
||||
if len(deviceIDs) != claudeauth.ClaudeDevicePoolSize {
|
||||
return nil, fmt.Errorf("ensure Claude credential device pool: Home KV pool has %d entries, want %d", len(deviceIDs), claudeauth.ClaudeDevicePoolSize)
|
||||
}
|
||||
claudeauth.StoreDeviceIDPool(&auth.Metadata, deviceIDs)
|
||||
return deviceIDs, nil
|
||||
}
|
||||
|
||||
// ClaudeCredentialAccountUUID returns the selected upstream credential's account UUID.
|
||||
func ClaudeCredentialAccountUUID(auth *cliproxyauth.Auth) string {
|
||||
if auth == nil {
|
||||
return ""
|
||||
}
|
||||
for _, key := range []string{"account_uuid", "accountUuid"} {
|
||||
value := strings.TrimSpace(claudeauth.ReadMetadataString(&auth.Metadata, key))
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type claudeCredentialMetadataRequestError struct {
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e *claudeCredentialMetadataRequestError) Error() string {
|
||||
if e == nil || e.cause == nil {
|
||||
return ""
|
||||
}
|
||||
return e.cause.Error()
|
||||
}
|
||||
|
||||
func (e *claudeCredentialMetadataRequestError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.cause
|
||||
}
|
||||
|
||||
func (e *claudeCredentialMetadataRequestError) StatusCode() int {
|
||||
if e == nil {
|
||||
return 0
|
||||
}
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
func (e *claudeCredentialMetadataRequestError) IsRequestScoped() bool {
|
||||
return e != nil
|
||||
}
|
||||
|
||||
func newClaudeCredentialMetadataRequestError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &claudeCredentialMetadataRequestError{cause: err}
|
||||
}
|
||||
|
||||
// ApplyClaudeCredentialMetadata rewrites the identity exception shared by native and cloaked OAuth requests.
|
||||
func ApplyClaudeCredentialMetadata(payload []byte, auth *cliproxyauth.Auth, sessionID string) ([]byte, string, error) {
|
||||
if auth == nil {
|
||||
return nil, "", fmt.Errorf("apply Claude credential metadata: auth is nil")
|
||||
}
|
||||
metadata, metadataPresent, errMetadata := uniqueClaudeJSONObjectMember(payload, "metadata")
|
||||
if errMetadata != nil {
|
||||
return nil, "", newClaudeCredentialMetadataRequestError(fmt.Errorf("apply Claude credential metadata: %w", errMetadata))
|
||||
}
|
||||
var existing string
|
||||
if metadataPresent {
|
||||
trimmedMetadata := bytes.TrimSpace(metadata)
|
||||
if len(trimmedMetadata) >= 2 && trimmedMetadata[0] == '{' {
|
||||
userID, userIDPresent, errUserID := uniqueClaudeJSONObjectMember(trimmedMetadata, "user_id")
|
||||
if errUserID != nil {
|
||||
return nil, "", newClaudeCredentialMetadataRequestError(fmt.Errorf("apply Claude credential metadata: metadata: %w", errUserID))
|
||||
}
|
||||
if userIDPresent && json.Unmarshal(userID, &existing) != nil {
|
||||
existing = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deviceIDs, _, errDeviceIDs := claudeauth.EnsureDeviceIDPoolFor(&auth.Metadata)
|
||||
if errDeviceIDs != nil {
|
||||
return nil, "", errDeviceIDs
|
||||
}
|
||||
deviceID, errDeviceID := claudeauth.SelectDeviceID(deviceIDs, sessionID)
|
||||
if errDeviceID != nil {
|
||||
return nil, "", errDeviceID
|
||||
}
|
||||
accountUUID := ClaudeCredentialAccountUUID(auth)
|
||||
if accountUUID == "" {
|
||||
return nil, "", fmt.Errorf("apply Claude credential metadata: account UUID is empty")
|
||||
}
|
||||
|
||||
encoded, errIdentity := rebuildClaudeMetadataUserID(existing, deviceID, accountUUID, sessionID)
|
||||
if errIdentity != nil {
|
||||
return nil, "", newClaudeCredentialMetadataRequestError(fmt.Errorf("apply Claude credential metadata: %w", errIdentity))
|
||||
}
|
||||
updated, errSet := sjson.SetBytes(payload, "metadata.user_id", string(encoded))
|
||||
if errSet != nil {
|
||||
return nil, "", fmt.Errorf("set Claude credential metadata: %w", errSet)
|
||||
}
|
||||
return updated, deviceID, nil
|
||||
}
|
||||
|
||||
type claudeJSONMember struct {
|
||||
key string
|
||||
value json.RawMessage
|
||||
}
|
||||
|
||||
func uniqueClaudeJSONObjectMember(raw []byte, target string) ([]byte, bool, error) {
|
||||
raw = bytes.TrimSpace(raw)
|
||||
if !json.Valid(raw) || len(raw) < 2 || raw[0] != '{' {
|
||||
return nil, false, fmt.Errorf("request must be a JSON object")
|
||||
}
|
||||
|
||||
position := 1
|
||||
found := false
|
||||
var value []byte
|
||||
for {
|
||||
position = skipClaudeJSONWhitespace(raw, position)
|
||||
if position >= len(raw) {
|
||||
return nil, false, fmt.Errorf("unterminated JSON object")
|
||||
}
|
||||
if raw[position] == '}' {
|
||||
break
|
||||
}
|
||||
keyStart := position
|
||||
keyEnd := skipClaudeJSONString(raw, keyStart)
|
||||
var key string
|
||||
if errUnmarshal := json.Unmarshal(raw[keyStart:keyEnd], &key); errUnmarshal != nil {
|
||||
return nil, false, fmt.Errorf("decode JSON object key: %w", errUnmarshal)
|
||||
}
|
||||
position = skipClaudeJSONWhitespace(raw, keyEnd)
|
||||
if position >= len(raw) || raw[position] != ':' {
|
||||
return nil, false, fmt.Errorf("JSON object key %q is missing a value", key)
|
||||
}
|
||||
position = skipClaudeJSONWhitespace(raw, position+1)
|
||||
valueStart := position
|
||||
position = skipClaudeJSONValue(raw, position)
|
||||
if key == target {
|
||||
if found {
|
||||
return nil, false, fmt.Errorf("duplicate JSON object key %q", target)
|
||||
}
|
||||
found = true
|
||||
value = raw[valueStart:position]
|
||||
}
|
||||
position = skipClaudeJSONWhitespace(raw, position)
|
||||
if position < len(raw) && raw[position] == ',' {
|
||||
position++
|
||||
continue
|
||||
}
|
||||
if position >= len(raw) || raw[position] != '}' {
|
||||
return nil, false, fmt.Errorf("JSON object key %q has an invalid terminator", key)
|
||||
}
|
||||
}
|
||||
return value, found, nil
|
||||
}
|
||||
|
||||
func skipClaudeJSONWhitespace(raw []byte, position int) int {
|
||||
for position < len(raw) {
|
||||
switch raw[position] {
|
||||
case ' ', '\t', '\r', '\n':
|
||||
position++
|
||||
default:
|
||||
return position
|
||||
}
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
func skipClaudeJSONString(raw []byte, position int) int {
|
||||
if position >= len(raw) || raw[position] != '"' {
|
||||
return position
|
||||
}
|
||||
position++
|
||||
for position < len(raw) {
|
||||
switch raw[position] {
|
||||
case '\\':
|
||||
position += 2
|
||||
case '"':
|
||||
return position + 1
|
||||
default:
|
||||
position++
|
||||
}
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
func skipClaudeJSONValue(raw []byte, position int) int {
|
||||
if position >= len(raw) {
|
||||
return position
|
||||
}
|
||||
switch raw[position] {
|
||||
case '"':
|
||||
return skipClaudeJSONString(raw, position)
|
||||
case '{', '[':
|
||||
stack := []byte{raw[position]}
|
||||
position++
|
||||
for position < len(raw) && len(stack) > 0 {
|
||||
switch raw[position] {
|
||||
case '"':
|
||||
position = skipClaudeJSONString(raw, position)
|
||||
continue
|
||||
case '{', '[':
|
||||
stack = append(stack, raw[position])
|
||||
case '}', ']':
|
||||
stack = stack[:len(stack)-1]
|
||||
}
|
||||
position++
|
||||
}
|
||||
return position
|
||||
default:
|
||||
for position < len(raw) {
|
||||
switch raw[position] {
|
||||
case ',', '}', ']', ' ', '\t', '\r', '\n':
|
||||
return position
|
||||
default:
|
||||
position++
|
||||
}
|
||||
}
|
||||
return position
|
||||
}
|
||||
}
|
||||
|
||||
func rebuildClaudeMetadataUserID(existing, deviceID, accountUUID, sessionID string) ([]byte, error) {
|
||||
extras := make([]claudeJSONMember, 0)
|
||||
rawExisting := []byte(strings.TrimSpace(existing))
|
||||
if json.Valid(rawExisting) && len(rawExisting) >= 2 && rawExisting[0] == '{' {
|
||||
decoder := json.NewDecoder(bytes.NewReader(rawExisting))
|
||||
_, _ = decoder.Token()
|
||||
seen := make(map[string]bool)
|
||||
for decoder.More() {
|
||||
token, errToken := decoder.Token()
|
||||
if errToken != nil {
|
||||
return nil, errToken
|
||||
}
|
||||
key, ok := token.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("metadata.user_id contains a non-string key")
|
||||
}
|
||||
if seen[key] {
|
||||
return nil, fmt.Errorf("metadata.user_id contains duplicate key %q", key)
|
||||
}
|
||||
seen[key] = true
|
||||
var value json.RawMessage
|
||||
if errDecode := decoder.Decode(&value); errDecode != nil {
|
||||
return nil, errDecode
|
||||
}
|
||||
switch key {
|
||||
case "device_id", "account_uuid", "session_id":
|
||||
default:
|
||||
extras = append(extras, claudeJSONMember{key: key, value: value})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var output bytes.Buffer
|
||||
output.WriteString(`{"device_id":`)
|
||||
writeClaudeJSONQuoted(&output, deviceID)
|
||||
output.WriteString(`,"account_uuid":`)
|
||||
writeClaudeJSONQuoted(&output, accountUUID)
|
||||
output.WriteString(`,"session_id":`)
|
||||
writeClaudeJSONQuoted(&output, sessionID)
|
||||
for _, extra := range extras {
|
||||
output.WriteByte(',')
|
||||
writeClaudeJSONQuoted(&output, extra.key)
|
||||
output.WriteByte(':')
|
||||
output.Write(extra.value)
|
||||
}
|
||||
output.WriteByte('}')
|
||||
return output.Bytes(), nil
|
||||
}
|
||||
|
||||
func writeClaudeJSONQuoted(output *bytes.Buffer, value string) {
|
||||
encoded, _ := json.Marshal(value)
|
||||
output.Write(encoded)
|
||||
}
|
||||
Loading…
Reference in a new issue