diff --git a/cmd/mcpdebug/main.go b/cmd/mcpdebug/main.go new file mode 100644 index 00000000..51f1b64f --- /dev/null +++ b/cmd/mcpdebug/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "encoding/hex" + "fmt" + "os" + + cursorproto "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/cursor/proto" +) + +func main() { + // Encode MCP result with empty execId + resultBytes := cursorproto.EncodeExecMcpResult(1, "", `{"test": "data"}`, false) + fmt.Printf("Result protobuf hex: %s\n", hex.EncodeToString(resultBytes)) + fmt.Printf("Result length: %d bytes\n", len(resultBytes)) + + // Write to file for analysis + os.WriteFile("mcp_result.bin", resultBytes) + fmt.Println("Wrote mcp_result.bin") +} diff --git a/cmd/protocheck/main.go b/cmd/protocheck/main.go new file mode 100644 index 00000000..af68bbc6 --- /dev/null +++ b/cmd/protocheck/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + cursorproto "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/cursor/proto" +) + +func main() { + ecm := cursorproto.NewMsg("ExecClientMessage") + + // Try different field names + names := []string{ + "mcp_result", "mcpResult", "McpResult", "MCP_RESULT", + "shell_result", "shellResult", + } + + for _, name := range names { + fd := ecm.Descriptor().Fields().ByName(name) + if fd != nil { + fmt.Printf("Found field %q: number=%d, kind=%s\n", name, fd.Number(), fd.Kind()) + } else { + fmt.Printf("Field %q NOT FOUND\n", name) + } + } + + // List all fields + fmt.Println("\nAll fields in ExecClientMessage:") + for i := 0; i < ecm.Descriptor().Fields().Len(); i++ { + f := ecm.Descriptor().Fields().Get(i) + fmt.Printf(" %d: %q (number=%d)\n", i, f.Name(), f.Number()) + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 9228d2a4..b6f5f47a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -85,6 +85,7 @@ func main() { var oauthCallbackPort int var antigravityLogin bool var kimiLogin bool + var cursorLogin bool var kiroLogin bool var kiroGoogleLogin bool var kiroAWSLogin bool @@ -123,6 +124,7 @@ func main() { flag.BoolVar(&noIncognito, "no-incognito", false, "Force disable incognito mode (uses existing browser session)") flag.BoolVar(&antigravityLogin, "antigravity-login", false, "Login to Antigravity using OAuth") flag.BoolVar(&kimiLogin, "kimi-login", false, "Login to Kimi using OAuth") + flag.BoolVar(&cursorLogin, "cursor-login", false, "Login to Cursor using OAuth") flag.BoolVar(&kiroLogin, "kiro-login", false, "Login to Kiro using Google OAuth") flag.BoolVar(&kiroGoogleLogin, "kiro-google-login", false, "Login to Kiro using Google OAuth (same as --kiro-login)") flag.BoolVar(&kiroAWSLogin, "kiro-aws-login", false, "Login to Kiro using AWS Builder ID (device code flow)") @@ -544,6 +546,8 @@ func main() { cmd.DoGitLabTokenLogin(cfg, options) } else if kimiLogin { cmd.DoKimiLogin(cfg, options) + } else if cursorLogin { + cmd.DoCursorLogin(cfg, options) } else if kiroLogin { // For Kiro auth, default to incognito mode for multi-account support // Users can explicitly override with --no-incognito diff --git a/internal/auth/cursor/oauth.go b/internal/auth/cursor/oauth.go new file mode 100644 index 00000000..065eff7e --- /dev/null +++ b/internal/auth/cursor/oauth.go @@ -0,0 +1,218 @@ +// Package cursor implements Cursor OAuth PKCE authentication and token refresh. +package cursor + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "strings" + "time" +) + +const ( + CursorLoginURL = "https://cursor.com/loginDeepControl" + CursorPollURL = "https://api2.cursor.sh/auth/poll" + CursorRefreshURL = "https://api2.cursor.sh/auth/exchange_user_api_key" + + pollMaxAttempts = 150 + pollBaseDelay = 1 * time.Second + pollMaxDelay = 10 * time.Second + pollBackoffMultiply = 1.2 + maxConsecutiveErrors = 10 +) + +// AuthParams holds the PKCE parameters for Cursor login. +type AuthParams struct { + Verifier string + Challenge string + UUID string + LoginURL string +} + +// TokenPair holds the access and refresh tokens from Cursor. +type TokenPair struct { + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` +} + +// GeneratePKCE creates a PKCE verifier and challenge pair. +func GeneratePKCE() (verifier, challenge string, err error) { + verifierBytes := make([]byte, 96) + if _, err = rand.Read(verifierBytes); err != nil { + return "", "", fmt.Errorf("cursor: failed to generate PKCE verifier: %w", err) + } + verifier = base64.RawURLEncoding.EncodeToString(verifierBytes) + + h := sha256.Sum256([]byte(verifier)) + challenge = base64.RawURLEncoding.EncodeToString(h[:]) + return verifier, challenge, nil +} + +// GenerateAuthParams creates the full set of auth params for Cursor login. +func GenerateAuthParams() (*AuthParams, error) { + verifier, challenge, err := GeneratePKCE() + if err != nil { + return nil, err + } + + uuidBytes := make([]byte, 16) + if _, err = rand.Read(uuidBytes); err != nil { + return nil, fmt.Errorf("cursor: failed to generate UUID: %w", err) + } + uuid := fmt.Sprintf("%x-%x-%x-%x-%x", + uuidBytes[0:4], uuidBytes[4:6], uuidBytes[6:8], uuidBytes[8:10], uuidBytes[10:16]) + + loginURL := fmt.Sprintf("%s?challenge=%s&uuid=%s&mode=login&redirectTarget=cli", + CursorLoginURL, challenge, uuid) + + return &AuthParams{ + Verifier: verifier, + Challenge: challenge, + UUID: uuid, + LoginURL: loginURL, + }, nil +} + +// PollForAuth polls the Cursor auth endpoint until the user completes login. +func PollForAuth(ctx context.Context, uuid, verifier string) (*TokenPair, error) { + delay := pollBaseDelay + consecutiveErrors := 0 + + client := &http.Client{Timeout: 10 * time.Second} + + for attempt := 0; attempt < pollMaxAttempts; attempt++ { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(delay): + } + + url := fmt.Sprintf("%s?uuid=%s&verifier=%s", CursorPollURL, uuid, verifier) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("cursor: failed to create poll request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + consecutiveErrors++ + if consecutiveErrors >= maxConsecutiveErrors { + return nil, fmt.Errorf("cursor: too many consecutive poll errors (last: %v)", err) + } + delay = minDuration(time.Duration(float64(delay)*pollBackoffMultiply), pollMaxDelay) + continue + } + + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + // Still waiting for user to authorize + consecutiveErrors = 0 + delay = minDuration(time.Duration(float64(delay)*pollBackoffMultiply), pollMaxDelay) + continue + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + var tokens TokenPair + if err := json.Unmarshal(body, &tokens); err != nil { + return nil, fmt.Errorf("cursor: failed to parse auth response: %w", err) + } + return &tokens, nil + } + + return nil, fmt.Errorf("cursor: poll failed with status %d: %s", resp.StatusCode, string(body)) + } + + return nil, fmt.Errorf("cursor: authentication polling timeout (waited ~%.0f seconds)", + float64(pollMaxAttempts)*pollMaxDelay.Seconds()/2) +} + +// RefreshToken refreshes a Cursor access token using the refresh token. +func RefreshToken(ctx context.Context, refreshToken string) (*TokenPair, error) { + client := &http.Client{Timeout: 10 * time.Second} + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, CursorRefreshURL, + strings.NewReader("{}")) + if err != nil { + return nil, fmt.Errorf("cursor: failed to create refresh request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+refreshToken) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("cursor: token refresh request failed: %w", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("cursor: token refresh failed (status %d): %s", resp.StatusCode, string(body)) + } + + var tokens TokenPair + if err := json.Unmarshal(body, &tokens); err != nil { + return nil, fmt.Errorf("cursor: failed to parse refresh response: %w", err) + } + + // Keep original refresh token if not returned + if tokens.RefreshToken == "" { + tokens.RefreshToken = refreshToken + } + + return &tokens, nil +} + +// GetTokenExpiry extracts the JWT expiry from an access token with a 5-minute safety margin. +// Falls back to 1 hour from now if the token can't be parsed. +func GetTokenExpiry(token string) time.Time { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return time.Now().Add(1 * time.Hour) + } + + // Decode the payload (middle part) + payload := parts[1] + // Add padding if needed + switch len(payload) % 4 { + case 2: + payload += "==" + case 3: + payload += "=" + } + // Replace URL-safe characters + payload = strings.ReplaceAll(payload, "-", "+") + payload = strings.ReplaceAll(payload, "_", "/") + + decoded, err := base64.StdEncoding.DecodeString(payload) + if err != nil { + return time.Now().Add(1 * time.Hour) + } + + var claims struct { + Exp float64 `json:"exp"` + } + if err := json.Unmarshal(decoded, &claims); err != nil || claims.Exp == 0 { + return time.Now().Add(1 * time.Hour) + } + + sec, frac := math.Modf(claims.Exp) + expiry := time.Unix(int64(sec), int64(frac*1e9)) + // Subtract 5-minute safety margin + return expiry.Add(-5 * time.Minute) +} + +func minDuration(a, b time.Duration) time.Duration { + if a < b { + return a + } + return b +} diff --git a/internal/auth/cursor/proto/connect.go b/internal/auth/cursor/proto/connect.go new file mode 100644 index 00000000..db9e4288 --- /dev/null +++ b/internal/auth/cursor/proto/connect.go @@ -0,0 +1,71 @@ +package proto + +import ( + "encoding/binary" + "encoding/json" + "fmt" +) + +const ( + // ConnectEndStreamFlag marks the end-of-stream frame (trailers). + ConnectEndStreamFlag byte = 0x02 + // ConnectCompressionFlag indicates the payload is compressed (not supported). + ConnectCompressionFlag byte = 0x01 + // ConnectFrameHeaderSize is the fixed 5-byte frame header. + ConnectFrameHeaderSize = 5 +) + +// FrameConnectMessage wraps a protobuf payload in a Connect frame. +// Frame format: [1 byte flags][4 bytes payload length (big-endian)][payload] +func FrameConnectMessage(data []byte, flags byte) []byte { + frame := make([]byte, ConnectFrameHeaderSize+len(data)) + frame[0] = flags + binary.BigEndian.PutUint32(frame[1:5], uint32(len(data))) + copy(frame[5:], data) + return frame +} + +// ParseConnectFrame extracts one frame from a buffer. +// Returns (flags, payload, bytesConsumed, ok). +// ok is false when the buffer is too short for a complete frame. +func ParseConnectFrame(buf []byte) (flags byte, payload []byte, consumed int, ok bool) { + if len(buf) < ConnectFrameHeaderSize { + return 0, nil, 0, false + } + flags = buf[0] + length := binary.BigEndian.Uint32(buf[1:5]) + total := ConnectFrameHeaderSize + int(length) + if len(buf) < total { + return 0, nil, 0, false + } + return flags, buf[5:total], total, true +} + +// ParseConnectEndStream parses a Connect end-of-stream frame payload (JSON). +// Returns nil if there is no error in the trailer. +func ParseConnectEndStream(data []byte) error { + if len(data) == 0 { + return nil + } + var trailer struct { + Error *struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error"` + } + if err := json.Unmarshal(data, &trailer); err != nil { + return fmt.Errorf("failed to parse Connect end stream: %w", err) + } + if trailer.Error != nil { + code := trailer.Error.Code + if code == "" { + code = "unknown" + } + msg := trailer.Error.Message + if msg == "" { + msg = "Unknown error" + } + return fmt.Errorf("Connect error %s: %s", code, msg) + } + return nil +} diff --git a/internal/auth/cursor/proto/decode.go b/internal/auth/cursor/proto/decode.go new file mode 100644 index 00000000..cc10d483 --- /dev/null +++ b/internal/auth/cursor/proto/decode.go @@ -0,0 +1,507 @@ +package proto + +import ( + "encoding/hex" + "fmt" + + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/encoding/protowire" +) + +// ServerMessageType identifies the kind of decoded server message. +type ServerMessageType int + +const ( + ServerMsgUnknown ServerMessageType = iota + ServerMsgTextDelta // Text content delta + ServerMsgThinkingDelta // Thinking/reasoning delta + ServerMsgThinkingCompleted // Thinking completed + ServerMsgKvGetBlob // Server wants a blob + ServerMsgKvSetBlob // Server wants to store a blob + ServerMsgExecRequestCtx // Server requests context (tools, etc.) + ServerMsgExecMcpArgs // Server wants MCP tool execution + ServerMsgExecShellArgs // Rejected: shell command + ServerMsgExecReadArgs // Rejected: file read + ServerMsgExecWriteArgs // Rejected: file write + ServerMsgExecDeleteArgs // Rejected: file delete + ServerMsgExecLsArgs // Rejected: directory listing + ServerMsgExecGrepArgs // Rejected: grep search + ServerMsgExecFetchArgs // Rejected: HTTP fetch + ServerMsgExecDiagnostics // Respond with empty diagnostics + ServerMsgExecShellStream // Rejected: shell stream + ServerMsgExecBgShellSpawn // Rejected: background shell + ServerMsgExecWriteShellStdin // Rejected: write shell stdin + ServerMsgExecOther // Other exec types (respond with empty) +) + +// DecodedServerMessage holds parsed data from an AgentServerMessage. +type DecodedServerMessage struct { + Type ServerMessageType + + // For text/thinking deltas + Text string + + // For KV messages + KvId uint32 + BlobId []byte // hex-encoded blob ID + BlobData []byte // for setBlobArgs + + // For exec messages + ExecMsgId uint32 + ExecId string + + // For MCP args + McpToolName string + McpToolCallId string + McpArgs map[string][]byte // arg name -> protobuf-encoded value + + // For rejection context + Path string + Command string + WorkingDirectory string + Url string + + // For other exec - the raw field number for building a response + ExecFieldNumber int +} + +// DecodeAgentServerMessage parses an AgentServerMessage and returns +// a structured representation of the first meaningful message found. +func DecodeAgentServerMessage(data []byte) (*DecodedServerMessage, error) { + msg := &DecodedServerMessage{Type: ServerMsgUnknown} + + for len(data) > 0 { + num, typ, n := protowire.ConsumeTag(data) + if n < 0 { + return msg, fmt.Errorf("invalid tag") + } + data = data[n:] + + switch typ { + case protowire.BytesType: + val, n := protowire.ConsumeBytes(data) + if n < 0 { + return msg, fmt.Errorf("invalid bytes field %d", num) + } + data = data[n:] + + // Debug: log top-level ASM fields + log.Debugf("DecodeAgentServerMessage: found ASM field %d, len=%d", num, len(val)) + + switch num { + case ASM_InteractionUpdate: + log.Debugf("DecodeAgentServerMessage: calling decodeInteractionUpdate") + decodeInteractionUpdate(val, msg) + case ASM_ExecServerMessage: + log.Debugf("DecodeAgentServerMessage: calling decodeExecServerMessage") + decodeExecServerMessage(val, msg) + case ASM_KvServerMessage: + decodeKvServerMessage(val, msg) + case ASM_ConversationCheckpoint: + // Ignore checkpoint updates + log.Debugf("DecodeAgentServerMessage: ignoring ConversationCheckpoint") + } + + case protowire.VarintType: + _, n := protowire.ConsumeVarint(data) + if n < 0 { + return msg, fmt.Errorf("invalid varint field %d", num) + } + data = data[n:] + + default: + // Skip unknown wire types + n := protowire.ConsumeFieldValue(num, typ, data) + if n < 0 { + return msg, fmt.Errorf("invalid field %d", num) + } + data = data[n:] + } + } + + return msg, nil +} + +func decodeInteractionUpdate(data []byte, msg *DecodedServerMessage) { + log.Debugf("decodeInteractionUpdate: input len=%d, hex=%x", len(data), data) + for len(data) > 0 { + num, typ, n := protowire.ConsumeTag(data) + if n < 0 { + log.Debugf("decodeInteractionUpdate: invalid tag, remaining=%x", data) + return + } + data = data[n:] + log.Debugf("decodeInteractionUpdate: field=%d wire=%d remaining=%d bytes", num, typ, len(data)) + + if typ == protowire.BytesType { + val, n := protowire.ConsumeBytes(data) + if n < 0 { + log.Debugf("decodeInteractionUpdate: invalid bytes field %d", num) + return + } + data = data[n:] + log.Debugf("decodeInteractionUpdate: field %d content len=%d, first 20 bytes: %x", num, len(val), val[:min(20, len(val))]) + + switch num { + case IU_TextDelta: + msg.Type = ServerMsgTextDelta + msg.Text = decodeStringField(val, TDU_Text) + log.Debugf("decodeInteractionUpdate: TextDelta text=%q", msg.Text) + case IU_ThinkingDelta: + msg.Type = ServerMsgThinkingDelta + msg.Text = decodeStringField(val, TKD_Text) + log.Debugf("decodeInteractionUpdate: ThinkingDelta text=%q", msg.Text) + case IU_ThinkingCompleted: + msg.Type = ServerMsgThinkingCompleted + log.Debugf("decodeInteractionUpdate: ThinkingCompleted") + case 2: + // tool_call_started - ignore but log + log.Debugf("decodeInteractionUpdate: ToolCallStarted (ignored)") + case 3: + // tool_call_completed - ignore but log + log.Debugf("decodeInteractionUpdate: ToolCallCompleted (ignored)") + default: + log.Debugf("decodeInteractionUpdate: unknown field %d", num) + } + } else { + n := protowire.ConsumeFieldValue(num, typ, data) + if n < 0 { + return + } + data = data[n:] + } + } +} + +func decodeKvServerMessage(data []byte, msg *DecodedServerMessage) { + for len(data) > 0 { + num, typ, n := protowire.ConsumeTag(data) + if n < 0 { + return + } + data = data[n:] + + switch typ { + case protowire.VarintType: + val, n := protowire.ConsumeVarint(data) + if n < 0 { + return + } + data = data[n:] + if num == KSM_Id { + msg.KvId = uint32(val) + } + + case protowire.BytesType: + val, n := protowire.ConsumeBytes(data) + if n < 0 { + return + } + data = data[n:] + + switch num { + case KSM_GetBlobArgs: + msg.Type = ServerMsgKvGetBlob + msg.BlobId = decodeBytesField(val, GBA_BlobId) + case KSM_SetBlobArgs: + msg.Type = ServerMsgKvSetBlob + decodeSetBlobArgs(val, msg) + } + + default: + n := protowire.ConsumeFieldValue(num, typ, data) + if n < 0 { + return + } + data = data[n:] + } + } +} + +func decodeSetBlobArgs(data []byte, msg *DecodedServerMessage) { + for len(data) > 0 { + num, typ, n := protowire.ConsumeTag(data) + if n < 0 { + return + } + data = data[n:] + + if typ == protowire.BytesType { + val, n := protowire.ConsumeBytes(data) + if n < 0 { + return + } + data = data[n:] + switch num { + case SBA_BlobId: + msg.BlobId = val + case SBA_BlobData: + msg.BlobData = val + } + } else { + n := protowire.ConsumeFieldValue(num, typ, data) + if n < 0 { + return + } + data = data[n:] + } + } +} + +func decodeExecServerMessage(data []byte, msg *DecodedServerMessage) { + for len(data) > 0 { + num, typ, n := protowire.ConsumeTag(data) + if n < 0 { + return + } + data = data[n:] + + switch typ { + case protowire.VarintType: + val, n := protowire.ConsumeVarint(data) + if n < 0 { + return + } + data = data[n:] + if num == ESM_Id { + msg.ExecMsgId = uint32(val) + log.Debugf("decodeExecServerMessage: ESM_Id = %d", val) + } + + case protowire.BytesType: + val, n := protowire.ConsumeBytes(data) + if n < 0 { + return + } + data = data[n:] + + // Debug: log all fields found in ExecServerMessage + log.Debugf("decodeExecServerMessage: found field %d, len=%d, first 20 bytes: %x", num, len(val), val[:min(20, len(val))]) + + switch num { + case ESM_ExecId: + msg.ExecId = string(val) + log.Debugf("decodeExecServerMessage: ESM_ExecId = %q", msg.ExecId) + case ESM_RequestContextArgs: + msg.Type = ServerMsgExecRequestCtx + case ESM_McpArgs: + msg.Type = ServerMsgExecMcpArgs + decodeMcpArgs(val, msg) + case ESM_ShellArgs: + msg.Type = ServerMsgExecShellArgs + decodeShellArgs(val, msg) + case ESM_ShellStreamArgs: + msg.Type = ServerMsgExecShellStream + decodeShellArgs(val, msg) + case ESM_ReadArgs: + msg.Type = ServerMsgExecReadArgs + msg.Path = decodeStringField(val, RA_Path) + case ESM_WriteArgs: + msg.Type = ServerMsgExecWriteArgs + msg.Path = decodeStringField(val, WA_Path) + case ESM_DeleteArgs: + msg.Type = ServerMsgExecDeleteArgs + msg.Path = decodeStringField(val, DA_Path) + case ESM_LsArgs: + msg.Type = ServerMsgExecLsArgs + msg.Path = decodeStringField(val, LA_Path) + case ESM_GrepArgs: + msg.Type = ServerMsgExecGrepArgs + case ESM_FetchArgs: + msg.Type = ServerMsgExecFetchArgs + msg.Url = decodeStringField(val, FA_Url) + case ESM_DiagnosticsArgs: + msg.Type = ServerMsgExecDiagnostics + case ESM_BackgroundShellSpawn: + msg.Type = ServerMsgExecBgShellSpawn + decodeShellArgs(val, msg) // same structure + case ESM_WriteShellStdinArgs: + msg.Type = ServerMsgExecWriteShellStdin + default: + // Unknown exec types - only set if we haven't identified the type yet + // (other fields like span_context (19) come after the exec type field) + if msg.Type == ServerMsgUnknown { + msg.Type = ServerMsgExecOther + msg.ExecFieldNumber = int(num) + } + } + + default: + n := protowire.ConsumeFieldValue(num, typ, data) + if n < 0 { + return + } + data = data[n:] + } + } +} + +func decodeMcpArgs(data []byte, msg *DecodedServerMessage) { + msg.McpArgs = make(map[string][]byte) + for len(data) > 0 { + num, typ, n := protowire.ConsumeTag(data) + if n < 0 { + return + } + data = data[n:] + + if typ == protowire.BytesType { + val, n := protowire.ConsumeBytes(data) + if n < 0 { + return + } + data = data[n:] + + switch num { + case MCA_Name: + msg.McpToolName = string(val) + case MCA_Args: + // Map entries are encoded as submessages with key=1, value=2 + decodeMapEntry(val, msg.McpArgs) + case MCA_ToolCallId: + msg.McpToolCallId = string(val) + case MCA_ToolName: + // ToolName takes precedence if present + if msg.McpToolName == "" || string(val) != "" { + msg.McpToolName = string(val) + } + } + } else { + n := protowire.ConsumeFieldValue(num, typ, data) + if n < 0 { + return + } + data = data[n:] + } + } +} + +func decodeMapEntry(data []byte, m map[string][]byte) { + var key string + var value []byte + for len(data) > 0 { + num, typ, n := protowire.ConsumeTag(data) + if n < 0 { + return + } + data = data[n:] + + if typ == protowire.BytesType { + val, n := protowire.ConsumeBytes(data) + if n < 0 { + return + } + data = data[n:] + if num == 1 { + key = string(val) + } else if num == 2 { + value = append([]byte(nil), val...) + } + } else { + n := protowire.ConsumeFieldValue(num, typ, data) + if n < 0 { + return + } + data = data[n:] + } + } + if key != "" { + m[key] = value + } +} + +func decodeShellArgs(data []byte, msg *DecodedServerMessage) { + for len(data) > 0 { + num, typ, n := protowire.ConsumeTag(data) + if n < 0 { + return + } + data = data[n:] + + if typ == protowire.BytesType { + val, n := protowire.ConsumeBytes(data) + if n < 0 { + return + } + data = data[n:] + switch num { + case SHA_Command: + msg.Command = string(val) + case SHA_WorkingDirectory: + msg.WorkingDirectory = string(val) + } + } else { + n := protowire.ConsumeFieldValue(num, typ, data) + if n < 0 { + return + } + data = data[n:] + } + } +} + +// --- Helper decoders --- + +// decodeStringField extracts a string from the first matching field in a submessage. +func decodeStringField(data []byte, targetField protowire.Number) string { + for len(data) > 0 { + num, typ, n := protowire.ConsumeTag(data) + if n < 0 { + return "" + } + data = data[n:] + + if typ == protowire.BytesType { + val, n := protowire.ConsumeBytes(data) + if n < 0 { + return "" + } + data = data[n:] + if num == targetField { + return string(val) + } + } else { + n := protowire.ConsumeFieldValue(num, typ, data) + if n < 0 { + return "" + } + data = data[n:] + } + } + return "" +} + +// decodeBytesField extracts bytes from the first matching field in a submessage. +func decodeBytesField(data []byte, targetField protowire.Number) []byte { + for len(data) > 0 { + num, typ, n := protowire.ConsumeTag(data) + if n < 0 { + return nil + } + data = data[n:] + + if typ == protowire.BytesType { + val, n := protowire.ConsumeBytes(data) + if n < 0 { + return nil + } + data = data[n:] + if num == targetField { + return append([]byte(nil), val...) + } + } else { + n := protowire.ConsumeFieldValue(num, typ, data) + if n < 0 { + return nil + } + data = data[n:] + } + } + return nil +} + +// BlobIdHex returns the hex string of a blob ID for use as a map key. +func BlobIdHex(blobId []byte) string { + return hex.EncodeToString(blobId) +} + diff --git a/internal/auth/cursor/proto/descriptor.go b/internal/auth/cursor/proto/descriptor.go new file mode 100644 index 00000000..a24b3fa9 --- /dev/null +++ b/internal/auth/cursor/proto/descriptor.go @@ -0,0 +1,1244 @@ +package proto + +import ( + "encoding/base64" + "sync" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoreflect" + descrptorpb "google.golang.org/protobuf/types/descriptorpb" +) + +// agentDescriptorB64 is the base64-encoded FileDescriptorProto for agent.proto. +// Extracted from alma-plugins/plugins/cursor-auth/proto/agent_pb.ts. +const agentDescriptorB64 = "" + + "CgthZ2VudC5wcm90bxIIYWdlbnQudjEicgoOR2xvYlRvb2xSZXN1bHQSLAoHc3VjY2VzcxgBIAEo" + + "CzIZLmFnZW50LnYxLkdsb2JUb29sU3VjY2Vzc0gAEigKBWVycm9yGAIgASgLMhcuYWdlbnQudjEu" + + "R2xvYlRvb2xFcnJvckgAQggKBnJlc3VsdCIeCg1HbG9iVG9vbEVycm9yEg0KBWVycm9yGAEgASgJ" + + "IokBCg9HbG9iVG9vbFN1Y2Nlc3MSDwoHcGF0dGVybhgBIAEoCRIMCgRwYXRoGAIgASgJEg0KBWZp" + + "bGVzGAMgAygJEhMKC3RvdGFsX2ZpbGVzGAQgASgFEhgKEGNsaWVudF90cnVuY2F0ZWQYBSABKAgS" + + "GQoRcmlwZ3JlcF90cnVuY2F0ZWQYBiABKAgiRgoMR2xvYlRvb2xDYWxsEgwKBGFyZ3MYASABKAwS" + + "KAoGcmVzdWx0GAIgASgLMhguYWdlbnQudjEuR2xvYlRvb2xSZXN1bHQibQoRUmVhZExpbnRzVG9v" + + "bENhbGwSKQoEYXJncxgBIAEoCzIbLmFnZW50LnYxLlJlYWRMaW50c1Rvb2xBcmdzEi0KBnJlc3Vs" + + "dBgCIAEoCzIdLmFnZW50LnYxLlJlYWRMaW50c1Rvb2xSZXN1bHQiIgoRUmVhZExpbnRzVG9vbEFy" + + "Z3MSDQoFcGF0aHMYASADKAkigQEKE1JlYWRMaW50c1Rvb2xSZXN1bHQSMQoHc3VjY2VzcxgBIAEo" + + "CzIeLmFnZW50LnYxLlJlYWRMaW50c1Rvb2xTdWNjZXNzSAASLQoFZXJyb3IYAiABKAsyHC5hZ2Vu" + + "dC52MS5SZWFkTGludHNUb29sRXJyb3JIAEIICgZyZXN1bHQiewoUUmVhZExpbnRzVG9vbFN1Y2Nl" + + "c3MSMwoQZmlsZV9kaWFnbm9zdGljcxgBIAMoCzIZLmFnZW50LnYxLkZpbGVEaWFnbm9zdGljcxIT" + + "Cgt0b3RhbF9maWxlcxgCIAEoBRIZChF0b3RhbF9kaWFnbm9zdGljcxgDIAEoBSJpCg9GaWxlRGlh" + + "Z25vc3RpY3MSDAoEcGF0aBgBIAEoCRItCgtkaWFnbm9zdGljcxgCIAMoCzIYLmFnZW50LnYxLkRp" + + "YWdub3N0aWNJdGVtEhkKEWRpYWdub3N0aWNzX2NvdW50GAMgASgFIqsBCg5EaWFnbm9zdGljSXRl" + + "bRIuCghzZXZlcml0eRgBIAEoDjIcLmFnZW50LnYxLkRpYWdub3N0aWNTZXZlcml0eRIoCgVyYW5n" + + "ZRgCIAEoCzIZLmFnZW50LnYxLkRpYWdub3N0aWNSYW5nZRIPCgdtZXNzYWdlGAMgASgJEg4KBnNv" + + "dXJjZRgEIAEoCRIMCgRjb2RlGAUgASgJEhAKCGlzX3N0YWxlGAYgASgIIlUKD0RpYWdub3N0aWNS" + + "YW5nZRIhCgVzdGFydBgBIAEoCzISLmFnZW50LnYxLlBvc2l0aW9uEh8KA2VuZBgCIAEoCzISLmFn" + + "ZW50LnYxLlBvc2l0aW9uIisKElJlYWRMaW50c1Rvb2xFcnJvchIVCg1lcnJvcl9tZXNzYWdlGAEg" + + "ASgJIh0KDE1jcFRvb2xFcnJvchINCgVlcnJvchgBIAEoCSLSAQoNTWNwVG9vbFJlc3VsdBInCgdz" + + "dWNjZXNzGAEgASgLMhQuYWdlbnQudjEuTWNwU3VjY2Vzc0gAEicKBWVycm9yGAIgASgLMhYuYWdl" + + "bnQudjEuTWNwVG9vbEVycm9ySAASKQoIcmVqZWN0ZWQYAyABKAsyFS5hZ2VudC52MS5NY3BSZWpl" + + "Y3RlZEgAEjoKEXBlcm1pc3Npb25fZGVuaWVkGAQgASgLMh0uYWdlbnQudjEuTWNwUGVybWlzc2lv" + + "bkRlbmllZEgAQggKBnJlc3VsdCJXCgtNY3BUb29sQ2FsbBIfCgRhcmdzGAEgASgLMhEuYWdlbnQu" + + "djEuTWNwQXJncxInCgZyZXN1bHQYAiABKAsyFy5hZ2VudC52MS5NY3BUb29sUmVzdWx0Im0KEVNl" + + "bVNlYXJjaFRvb2xDYWxsEikKBGFyZ3MYASABKAsyGy5hZ2VudC52MS5TZW1TZWFyY2hUb29sQXJn" + + "cxItCgZyZXN1bHQYAiABKAsyHS5hZ2VudC52MS5TZW1TZWFyY2hUb29sUmVzdWx0IlMKEVNlbVNl" + + "YXJjaFRvb2xBcmdzEg0KBXF1ZXJ5GAEgASgJEhoKEnRhcmdldF9kaXJlY3RvcmllcxgCIAMoCRIT" + + "CgtleHBsYW5hdGlvbhgDIAEoCSKBAQoTU2VtU2VhcmNoVG9vbFJlc3VsdBIxCgdzdWNjZXNzGAEg" + + "ASgLMh4uYWdlbnQudjEuU2VtU2VhcmNoVG9vbFN1Y2Nlc3NIABItCgVlcnJvchgCIAEoCzIcLmFn" + + "ZW50LnYxLlNlbVNlYXJjaFRvb2xFcnJvckgAQggKBnJlc3VsdCI9ChRTZW1TZWFyY2hUb29sU3Vj" + + "Y2VzcxIPCgdyZXN1bHRzGAEgASgJEhQKDGNvZGVfcmVzdWx0cxgCIAMoDCIrChJTZW1TZWFyY2hU" + + "b29sRXJyb3ISFQoNZXJyb3JfbWVzc2FnZRgBIAEoCSKCAQoYTGlzdE1jcFJlc291cmNlc1Rvb2xD" + + "YWxsEjAKBGFyZ3MYASABKAsyIi5hZ2VudC52MS5MaXN0TWNwUmVzb3VyY2VzRXhlY0FyZ3MSNAoG" + + "cmVzdWx0GAIgASgLMiQuYWdlbnQudjEuTGlzdE1jcFJlc291cmNlc0V4ZWNSZXN1bHQifwoXUmVh" + + "ZE1jcFJlc291cmNlVG9vbENhbGwSLwoEYXJncxgBIAEoCzIhLmFnZW50LnYxLlJlYWRNY3BSZXNv" + + "dXJjZUV4ZWNBcmdzEjMKBnJlc3VsdBgCIAEoCzIjLmFnZW50LnYxLlJlYWRNY3BSZXNvdXJjZUV4" + + "ZWNSZXN1bHQiWQoNRmV0Y2hUb29sQ2FsbBIhCgRhcmdzGAEgASgLMhMuYWdlbnQudjEuRmV0Y2hB" + + "cmdzEiUKBnJlc3VsdBgCIAEoCzIVLmFnZW50LnYxLkZldGNoUmVzdWx0Im4KFFJlY29yZFNjcmVl" + + "blRvb2xDYWxsEigKBGFyZ3MYASABKAsyGi5hZ2VudC52MS5SZWNvcmRTY3JlZW5BcmdzEiwKBnJl" + + "c3VsdBgCIAEoCzIcLmFnZW50LnYxLlJlY29yZFNjcmVlblJlc3VsdCJ3ChdXcml0ZVNoZWxsU3Rk" + + "aW5Ub29sQ2FsbBIrCgRhcmdzGAEgASgLMh0uYWdlbnQudjEuV3JpdGVTaGVsbFN0ZGluQXJncxIv" + + "CgZyZXN1bHQYAiABKAsyHy5hZ2VudC52MS5Xcml0ZVNoZWxsU3RkaW5SZXN1bHQisQEKC1JlZmxl" + + "Y3RBcmdzEiIKGnVuZXhwZWN0ZWRfYWN0aW9uX291dGNvbWVzGAEgASgJEh0KFXJlbGV2YW50X2lu" + + "c3RydWN0aW9ucxgCIAEoCRIZChFzY2VuYXJpb19hbmFseXNpcxgDIAEoCRIaChJjcml0aWNhbF9z" + + "eW50aGVzaXMYBCABKAkSEgoKbmV4dF9zdGVwcxgFIAEoCRIUCgx0b29sX2NhbGxfaWQYBiABKAki" + + "bwoNUmVmbGVjdFJlc3VsdBIrCgdzdWNjZXNzGAEgASgLMhguYWdlbnQudjEuUmVmbGVjdFN1Y2Nl" + + "c3NIABInCgVlcnJvchgCIAEoCzIWLmFnZW50LnYxLlJlZmxlY3RFcnJvckgAQggKBnJlc3VsdCIQ" + + "Cg5SZWZsZWN0U3VjY2VzcyIdCgxSZWZsZWN0RXJyb3ISDQoFZXJyb3IYASABKAkiXwoPUmVmbGVj" + + "dFRvb2xDYWxsEiMKBGFyZ3MYASABKAsyFS5hZ2VudC52MS5SZWZsZWN0QXJncxInCgZyZXN1bHQY" + + "AiABKAsyFy5hZ2VudC52MS5SZWZsZWN0UmVzdWx0IlkKF1N0YXJ0R3JpbmRFeGVjdXRpb25Bcmdz" + + "EhgKC2V4cGxhbmF0aW9uGAEgASgJSACIAQESFAoMdG9vbF9jYWxsX2lkGAIgASgJQg4KDF9leHBs" + + "YW5hdGlvbiKTAQoZU3RhcnRHcmluZEV4ZWN1dGlvblJlc3VsdBI3CgdzdWNjZXNzGAEgASgLMiQu" + + "YWdlbnQudjEuU3RhcnRHcmluZEV4ZWN1dGlvblN1Y2Nlc3NIABIzCgVlcnJvchgCIAEoCzIiLmFn" + + "ZW50LnYxLlN0YXJ0R3JpbmRFeGVjdXRpb25FcnJvckgAQggKBnJlc3VsdCIcChpTdGFydEdyaW5k" + + "RXhlY3V0aW9uU3VjY2VzcyIpChhTdGFydEdyaW5kRXhlY3V0aW9uRXJyb3ISDQoFZXJyb3IYASAB" + + "KAkigwEKG1N0YXJ0R3JpbmRFeGVjdXRpb25Ub29sQ2FsbBIvCgRhcmdzGAEgASgLMiEuYWdlbnQu" + + "djEuU3RhcnRHcmluZEV4ZWN1dGlvbkFyZ3MSMwoGcmVzdWx0GAIgASgLMiMuYWdlbnQudjEuU3Rh" + + "cnRHcmluZEV4ZWN1dGlvblJlc3VsdCJYChZTdGFydEdyaW5kUGxhbm5pbmdBcmdzEhgKC2V4cGxh" + + "bmF0aW9uGAEgASgJSACIAQESFAoMdG9vbF9jYWxsX2lkGAIgASgJQg4KDF9leHBsYW5hdGlvbiKQ" + + "AQoYU3RhcnRHcmluZFBsYW5uaW5nUmVzdWx0EjYKB3N1Y2Nlc3MYASABKAsyIy5hZ2VudC52MS5T" + + "dGFydEdyaW5kUGxhbm5pbmdTdWNjZXNzSAASMgoFZXJyb3IYAiABKAsyIS5hZ2VudC52MS5TdGFy" + + "dEdyaW5kUGxhbm5pbmdFcnJvckgAQggKBnJlc3VsdCIbChlTdGFydEdyaW5kUGxhbm5pbmdTdWNj" + + "ZXNzIigKF1N0YXJ0R3JpbmRQbGFubmluZ0Vycm9yEg0KBWVycm9yGAEgASgJIoABChpTdGFydEdy" + + "aW5kUGxhbm5pbmdUb29sQ2FsbBIuCgRhcmdzGAEgASgLMiAuYWdlbnQudjEuU3RhcnRHcmluZFBs" + + "YW5uaW5nQXJncxIyCgZyZXN1bHQYAiABKAsyIi5hZ2VudC52MS5TdGFydEdyaW5kUGxhbm5pbmdS" + + "ZXN1bHQinAEKCFRhc2tBcmdzEhMKC2Rlc2NyaXB0aW9uGAEgASgJEg4KBnByb21wdBgCIAEoCRIt" + + "Cg1zdWJhZ2VudF90eXBlGAMgASgLMhYuYWdlbnQudjEuU3ViYWdlbnRUeXBlEhIKBW1vZGVsGAQg" + + "ASgJSACIAQESEwoGcmVzdW1lGAUgASgJSAGIAQFCCAoGX21vZGVsQgkKB19yZXN1bWUiqgEKC1Rh" + + "c2tTdWNjZXNzEjYKEmNvbnZlcnNhdGlvbl9zdGVwcxgBIAMoCzIaLmFnZW50LnYxLkNvbnZlcnNh" + + "dGlvblN0ZXASFQoIYWdlbnRfaWQYAiABKAlIAIgBARIVCg1pc19iYWNrZ3JvdW5kGAMgASgIEhgK" + + "C2R1cmF0aW9uX21zGAQgASgESAGIAQFCCwoJX2FnZW50X2lkQg4KDF9kdXJhdGlvbl9tcyIaCglU" + + "YXNrRXJyb3ISDQoFZXJyb3IYASABKAkiZgoKVGFza1Jlc3VsdBIoCgdzdWNjZXNzGAEgASgLMhUu" + + "YWdlbnQudjEuVGFza1N1Y2Nlc3NIABIkCgVlcnJvchgCIAEoCzITLmFnZW50LnYxLlRhc2tFcnJv" + + "ckgAQggKBnJlc3VsdCJWCgxUYXNrVG9vbENhbGwSIAoEYXJncxgBIAEoCzISLmFnZW50LnYxLlRh" + + "c2tBcmdzEiQKBnJlc3VsdBgCIAEoCzIULmFnZW50LnYxLlRhc2tSZXN1bHQiTAoRVGFza1Rvb2xD" + + "YWxsRGVsdGESNwoSaW50ZXJhY3Rpb25fdXBkYXRlGAEgASgLMhsuYWdlbnQudjEuSW50ZXJhY3Rp" + + "b25VcGRhdGUiyw8KCFRvb2xDYWxsEjIKD3NoZWxsX3Rvb2xfY2FsbBgBIAEoCzIXLmFnZW50LnYx" + + "LlNoZWxsVG9vbENhbGxIABI0ChBkZWxldGVfdG9vbF9jYWxsGAMgASgLMhguYWdlbnQudjEuRGVs" + + "ZXRlVG9vbENhbGxIABIwCg5nbG9iX3Rvb2xfY2FsbBgEIAEoCzIWLmFnZW50LnYxLkdsb2JUb29s" + + "Q2FsbEgAEjAKDmdyZXBfdG9vbF9jYWxsGAUgASgLMhYuYWdlbnQudjEuR3JlcFRvb2xDYWxsSAAS" + + "MAoOcmVhZF90b29sX2NhbGwYCCABKAsyFi5hZ2VudC52MS5SZWFkVG9vbENhbGxIABI/ChZ1cGRh" + + "dGVfdG9kb3NfdG9vbF9jYWxsGAkgASgLMh0uYWdlbnQudjEuVXBkYXRlVG9kb3NUb29sQ2FsbEgA" + + "EjsKFHJlYWRfdG9kb3NfdG9vbF9jYWxsGAogASgLMhsuYWdlbnQudjEuUmVhZFRvZG9zVG9vbENh" + + "bGxIABIwCg5lZGl0X3Rvb2xfY2FsbBgMIAEoCzIWLmFnZW50LnYxLkVkaXRUb29sQ2FsbEgAEiwK" + + "DGxzX3Rvb2xfY2FsbBgNIAEoCzIULmFnZW50LnYxLkxzVG9vbENhbGxIABI7ChRyZWFkX2xpbnRz" + + "X3Rvb2xfY2FsbBgOIAEoCzIbLmFnZW50LnYxLlJlYWRMaW50c1Rvb2xDYWxsSAASLgoNbWNwX3Rv" + + "b2xfY2FsbBgPIAEoCzIVLmFnZW50LnYxLk1jcFRvb2xDYWxsSAASOwoUc2VtX3NlYXJjaF90b29s" + + "X2NhbGwYECABKAsyGy5hZ2VudC52MS5TZW1TZWFyY2hUb29sQ2FsbEgAEj0KFWNyZWF0ZV9wbGFu" + + "X3Rvb2xfY2FsbBgRIAEoCzIcLmFnZW50LnYxLkNyZWF0ZVBsYW5Ub29sQ2FsbEgAEjsKFHdlYl9z" + + "ZWFyY2hfdG9vbF9jYWxsGBIgASgLMhsuYWdlbnQudjEuV2ViU2VhcmNoVG9vbENhbGxIABIwCg50" + + "YXNrX3Rvb2xfY2FsbBgTIAEoCzIWLmFnZW50LnYxLlRhc2tUb29sQ2FsbEgAEkoKHGxpc3RfbWNw" + + "X3Jlc291cmNlc190b29sX2NhbGwYFCABKAsyIi5hZ2VudC52MS5MaXN0TWNwUmVzb3VyY2VzVG9v" + + "bENhbGxIABJIChtyZWFkX21jcF9yZXNvdXJjZV90b29sX2NhbGwYFSABKAsyIS5hZ2VudC52MS5S" + + "ZWFkTWNwUmVzb3VyY2VUb29sQ2FsbEgAEkYKGmFwcGx5X2FnZW50X2RpZmZfdG9vbF9jYWxsGBYg" + + "ASgLMiAuYWdlbnQudjEuQXBwbHlBZ2VudERpZmZUb29sQ2FsbEgAEj8KFmFza19xdWVzdGlvbl90" + + "b29sX2NhbGwYFyABKAsyHS5hZ2VudC52MS5Bc2tRdWVzdGlvblRvb2xDYWxsSAASMgoPZmV0Y2hf" + + "dG9vbF9jYWxsGBggASgLMhcuYWdlbnQudjEuRmV0Y2hUb29sQ2FsbEgAEj0KFXN3aXRjaF9tb2Rl" + + "X3Rvb2xfY2FsbBgZIAEoCzIcLmFnZW50LnYxLlN3aXRjaE1vZGVUb29sQ2FsbEgAEjsKFGV4YV9z" + + "ZWFyY2hfdG9vbF9jYWxsGBogASgLMhsuYWdlbnQudjEuRXhhU2VhcmNoVG9vbENhbGxIABI5ChNl" + + "eGFfZmV0Y2hfdG9vbF9jYWxsGBsgASgLMhouYWdlbnQudjEuRXhhRmV0Y2hUb29sQ2FsbEgAEkMK" + + "GGdlbmVyYXRlX2ltYWdlX3Rvb2xfY2FsbBgcIAEoCzIfLmFnZW50LnYxLkdlbmVyYXRlSW1hZ2VU" + + "b29sQ2FsbEgAEkEKF3JlY29yZF9zY3JlZW5fdG9vbF9jYWxsGB0gASgLMh4uYWdlbnQudjEuUmVj" + + "b3JkU2NyZWVuVG9vbENhbGxIABI/ChZjb21wdXRlcl91c2VfdG9vbF9jYWxsGB4gASgLMh0uYWdl" + + "bnQudjEuQ29tcHV0ZXJVc2VUb29sQ2FsbEgAEkgKG3dyaXRlX3NoZWxsX3N0ZGluX3Rvb2xfY2Fs" + + "bBgfIAEoCzIhLmFnZW50LnYxLldyaXRlU2hlbGxTdGRpblRvb2xDYWxsSAASNgoRcmVmbGVjdF90" + + "b29sX2NhbGwYICABKAsyGS5hZ2VudC52MS5SZWZsZWN0VG9vbENhbGxIABJOCh5zZXR1cF92bV9l" + + "bnZpcm9ubWVudF90b29sX2NhbGwYISABKAsyJC5hZ2VudC52MS5TZXR1cFZtRW52aXJvbm1lbnRU" + + "b29sQ2FsbEgAEjoKE3RydW5jYXRlZF90b29sX2NhbGwYIiABKAsyGy5hZ2VudC52MS5UcnVuY2F0" + + "ZWRUb29sQ2FsbEgAElAKH3N0YXJ0X2dyaW5kX2V4ZWN1dGlvbl90b29sX2NhbGwYIyABKAsyJS5h" + + "Z2VudC52MS5TdGFydEdyaW5kRXhlY3V0aW9uVG9vbENhbGxIABJOCh5zdGFydF9ncmluZF9wbGFu" + + "bmluZ190b29sX2NhbGwYJCABKAsyJC5hZ2VudC52MS5TdGFydEdyaW5kUGxhbm5pbmdUb29sQ2Fs" + + "bEgAQgYKBHRvb2wiFwoVVHJ1bmNhdGVkVG9vbENhbGxBcmdzIhoKGFRydW5jYXRlZFRvb2xDYWxs" + + "U3VjY2VzcyInChZUcnVuY2F0ZWRUb29sQ2FsbEVycm9yEg0KBWVycm9yGAEgASgJIo0BChdUcnVu" + + "Y2F0ZWRUb29sQ2FsbFJlc3VsdBI1CgdzdWNjZXNzGAEgASgLMiIuYWdlbnQudjEuVHJ1bmNhdGVk" + + "VG9vbENhbGxTdWNjZXNzSAASMQoFZXJyb3IYAiABKAsyIC5hZ2VudC52MS5UcnVuY2F0ZWRUb29s" + + "Q2FsbEVycm9ySABCCAoGcmVzdWx0IpQBChFUcnVuY2F0ZWRUb29sQ2FsbBIdChVvcmlnaW5hbF9z" + + "dGVwX2Jsb2JfaWQYASABKAwSLQoEYXJncxgCIAEoCzIfLmFnZW50LnYxLlRydW5jYXRlZFRvb2xD" + + "YWxsQXJncxIxCgZyZXN1bHQYAyABKAsyIS5hZ2VudC52MS5UcnVuY2F0ZWRUb29sQ2FsbFJlc3Vs" + + "dCLRAQoNVG9vbENhbGxEZWx0YRI9ChVzaGVsbF90b29sX2NhbGxfZGVsdGEYASABKAsyHC5hZ2Vu" + + "dC52MS5TaGVsbFRvb2xDYWxsRGVsdGFIABI7ChR0YXNrX3Rvb2xfY2FsbF9kZWx0YRgCIAEoCzIb" + + "LmFnZW50LnYxLlRhc2tUb29sQ2FsbERlbHRhSAASOwoUZWRpdF90b29sX2NhbGxfZGVsdGEYAyAB" + + "KAsyGy5hZ2VudC52MS5FZGl0VG9vbENhbGxEZWx0YUgAQgcKBWRlbHRhIrYBChBDb252ZXJzYXRp" + + "b25TdGVwEjcKEWFzc2lzdGFudF9tZXNzYWdlGAEgASgLMhouYWdlbnQudjEuQXNzaXN0YW50TWVz" + + "c2FnZUgAEicKCXRvb2xfY2FsbBgCIAEoCzISLmFnZW50LnYxLlRvb2xDYWxsSAASNQoQdGhpbmtp" + + "bmdfbWVzc2FnZRgDIAEoCzIZLmFnZW50LnYxLlRoaW5raW5nTWVzc2FnZUgAQgkKB21lc3NhZ2Ui" + + "gQQKEkNvbnZlcnNhdGlvbkFjdGlvbhI6ChN1c2VyX21lc3NhZ2VfYWN0aW9uGAEgASgLMhsuYWdl" + + "bnQudjEuVXNlck1lc3NhZ2VBY3Rpb25IABIvCg1yZXN1bWVfYWN0aW9uGAIgASgLMhYuYWdlbnQu" + + "djEuUmVzdW1lQWN0aW9uSAASLwoNY2FuY2VsX2FjdGlvbhgDIAEoCzIWLmFnZW50LnYxLkNhbmNl" + + "bEFjdGlvbkgAEjUKEHN1bW1hcml6ZV9hY3Rpb24YBCABKAsyGS5hZ2VudC52MS5TdW1tYXJpemVB" + + "Y3Rpb25IABI8ChRzaGVsbF9jb21tYW5kX2FjdGlvbhgFIAEoCzIcLmFnZW50LnYxLlNoZWxsQ29t" + + "bWFuZEFjdGlvbkgAEjYKEXN0YXJ0X3BsYW5fYWN0aW9uGAYgASgLMhkuYWdlbnQudjEuU3RhcnRQ" + + "bGFuQWN0aW9uSAASOgoTZXhlY3V0ZV9wbGFuX2FjdGlvbhgHIAEoCzIbLmFnZW50LnYxLkV4ZWN1" + + "dGVQbGFuQWN0aW9uSAASWgokYXN5bmNfYXNrX3F1ZXN0aW9uX2NvbXBsZXRpb25fYWN0aW9uGAgg" + + "ASgLMiouYWdlbnQudjEuQXN5bmNBc2tRdWVzdGlvbkNvbXBsZXRpb25BY3Rpb25IAEIICgZhY3Rp" + + "b24ivwEKEVVzZXJNZXNzYWdlQWN0aW9uEisKDHVzZXJfbWVzc2FnZRgBIAEoCzIVLmFnZW50LnYx" + + "LlVzZXJNZXNzYWdlEjEKD3JlcXVlc3RfY29udGV4dBgCIAEoCzIYLmFnZW50LnYxLlJlcXVlc3RD" + + "b250ZXh0EikKHHNlbmRfdG9faW50ZXJhY3Rpb25fbGlzdGVuZXIYAyABKAhIAIgBAUIfCh1fc2Vu" + + "ZF90b19pbnRlcmFjdGlvbl9saXN0ZW5lciIOCgxDYW5jZWxBY3Rpb24iQQoMUmVzdW1lQWN0aW9u" + + "EjEKD3JlcXVlc3RfY29udGV4dBgCIAEoCzIYLmFnZW50LnYxLlJlcXVlc3RDb250ZXh0IqABCiBB" + + "c3luY0Fza1F1ZXN0aW9uQ29tcGxldGlvbkFjdGlvbhIdChVvcmlnaW5hbF90b29sX2NhbGxfaWQY" + + "ASABKAkSMAoNb3JpZ2luYWxfYXJncxgCIAEoCzIZLmFnZW50LnYxLkFza1F1ZXN0aW9uQXJncxIr" + + "CgZyZXN1bHQYAyABKAsyGy5hZ2VudC52MS5Bc2tRdWVzdGlvblJlc3VsdCIRCg9TdW1tYXJpemVB" + + "Y3Rpb24iVAoSU2hlbGxDb21tYW5kQWN0aW9uEi0KDXNoZWxsX2NvbW1hbmQYASABKAsyFi5hZ2Vu" + + "dC52MS5TaGVsbENvbW1hbmQSDwoHZXhlY19pZBgCIAEoCSKCAQoPU3RhcnRQbGFuQWN0aW9uEisK" + + "DHVzZXJfbWVzc2FnZRgBIAEoCzIVLmFnZW50LnYxLlVzZXJNZXNzYWdlEjEKD3JlcXVlc3RfY29u" + + "dGV4dBgCIAEoCzIYLmFnZW50LnYxLlJlcXVlc3RDb250ZXh0Eg8KB2lzX3NwZWMYAyABKAgi4gEK" + + "EUV4ZWN1dGVQbGFuQWN0aW9uEjEKD3JlcXVlc3RfY29udGV4dBgBIAEoCzIYLmFnZW50LnYxLlJl" + + "cXVlc3RDb250ZXh0Ei0KBHBsYW4YAiABKAsyGi5hZ2VudC52MS5Db252ZXJzYXRpb25QbGFuSACI" + + "AQESGgoNcGxhbl9maWxlX3VyaRgDIAEoCUgBiAEBEh4KEXBsYW5fZmlsZV9jb250ZW50GAQgASgJ" + + "SAKIAQFCBwoFX3BsYW5CEAoOX3BsYW5fZmlsZV91cmlCFAoSX3BsYW5fZmlsZV9jb250ZW50IugC" + + "CgtVc2VyTWVzc2FnZRIMCgR0ZXh0GAEgASgJEhIKCm1lc3NhZ2VfaWQYAiABKAkSOAoQc2VsZWN0" + + "ZWRfY29udGV4dBgDIAEoCzIZLmFnZW50LnYxLlNlbGVjdGVkQ29udGV4dEgAiAEBEgwKBG1vZGUY" + + "BCABKAUSHQoQaXNfc2ltdWxhdGVkX21zZxgFIAEoCEgBiAEBEh8KEmJlc3Rfb2Zfbl9ncm91cF9p" + + "ZBgGIAEoCUgCiAEBEigKG3RyeV91c2VfYmVzdF9vZl9uX3Byb21vdGlvbhgHIAEoCEgDiAEBEhYK" + + "CXJpY2hfdGV4dBgIIAEoCUgEiAEBQhMKEV9zZWxlY3RlZF9jb250ZXh0QhMKEV9pc19zaW11bGF0" + + "ZWRfbXNnQhUKE19iZXN0X29mX25fZ3JvdXBfaWRCHgocX3RyeV91c2VfYmVzdF9vZl9uX3Byb21v" + + "dGlvbkIMCgpfcmljaF90ZXh0IiAKEEFzc2lzdGFudE1lc3NhZ2USDAoEdGV4dBgBIAEoCSI0Cg9U" + + "aGlua2luZ01lc3NhZ2USDAoEdGV4dBgBIAEoCRITCgtkdXJhdGlvbl9tcxgCIAEoDSIfCgxTaGVs" + + "bENvbW1hbmQSDwoHY29tbWFuZBgBIAEoCSJACgtTaGVsbE91dHB1dBIOCgZzdGRvdXQYASABKAkS" + + "DgoGc3RkZXJyGAIgASgJEhEKCWV4aXRfY29kZRgDIAEoBSKiAQoQQ29udmVyc2F0aW9uVHVybhJC" + + "ChdhZ2VudF9jb252ZXJzYXRpb25fdHVybhgBIAEoCzIfLmFnZW50LnYxLkFnZW50Q29udmVyc2F0" + + "aW9uVHVybkgAEkIKF3NoZWxsX2NvbnZlcnNhdGlvbl90dXJuGAIgASgLMh8uYWdlbnQudjEuU2hl" + + "bGxDb252ZXJzYXRpb25UdXJuSABCBgoEdHVybiIgChBDb252ZXJzYXRpb25QbGFuEgwKBHBsYW4Y" + + "ASABKAkivQEKGUNvbnZlcnNhdGlvblR1cm5TdHJ1Y3R1cmUSSwoXYWdlbnRfY29udmVyc2F0aW9u" + + "X3R1cm4YASABKAsyKC5hZ2VudC52MS5BZ2VudENvbnZlcnNhdGlvblR1cm5TdHJ1Y3R1cmVIABJL" + + "ChdzaGVsbF9jb252ZXJzYXRpb25fdHVybhgCIAEoCzIoLmFnZW50LnYxLlNoZWxsQ29udmVyc2F0" + + "aW9uVHVyblN0cnVjdHVyZUgAQgYKBHR1cm4ilwEKFUFnZW50Q29udmVyc2F0aW9uVHVybhIrCgx1" + + "c2VyX21lc3NhZ2UYASABKAsyFS5hZ2VudC52MS5Vc2VyTWVzc2FnZRIpCgVzdGVwcxgCIAMoCzIa" + + "LmFnZW50LnYxLkNvbnZlcnNhdGlvblN0ZXASFwoKcmVxdWVzdF9pZBgDIAEoCUgAiAEBQg0KC19y" + + "ZXF1ZXN0X2lkIm0KHkFnZW50Q29udmVyc2F0aW9uVHVyblN0cnVjdHVyZRIUCgx1c2VyX21lc3Nh" + + "Z2UYASABKAwSDQoFc3RlcHMYAiADKAwSFwoKcmVxdWVzdF9pZBgDIAEoCUgAiAEBQg0KC19yZXF1" + + "ZXN0X2lkInMKFVNoZWxsQ29udmVyc2F0aW9uVHVybhItCg1zaGVsbF9jb21tYW5kGAEgASgLMhYu" + + "YWdlbnQudjEuU2hlbGxDb21tYW5kEisKDHNoZWxsX291dHB1dBgCIAEoCzIVLmFnZW50LnYxLlNo" + + "ZWxsT3V0cHV0Ik0KHlNoZWxsQ29udmVyc2F0aW9uVHVyblN0cnVjdHVyZRIVCg1zaGVsbF9jb21t" + + "YW5kGAEgASgMEhQKDHNoZWxsX291dHB1dBgCIAEoDCImChNDb252ZXJzYXRpb25TdW1tYXJ5Eg8K" + + "B3N1bW1hcnkYASABKAkieAoaQ29udmVyc2F0aW9uU3VtbWFyeUFyY2hpdmUSGwoTc3VtbWFyaXpl" + + "ZF9tZXNzYWdlcxgBIAMoDBIPCgdzdW1tYXJ5GAIgASgJEhMKC3dpbmRvd190YWlsGAMgASgNEhcK" + + "D3N1bW1hcnlfbWVzc2FnZRgEIAEoDCJDChhDb252ZXJzYXRpb25Ub2tlbkRldGFpbHMSEwoLdXNl" + + "ZF90b2tlbnMYASABKA0SEgoKbWF4X3Rva2VucxgCIAEoDSJfCglGaWxlU3RhdGUSFAoHY29udGVu" + + "dBgBIAEoCUgAiAEBEhwKD2luaXRpYWxfY29udGVudBgCIAEoCUgBiAEBQgoKCF9jb250ZW50QhIK" + + "EF9pbml0aWFsX2NvbnRlbnQiaAoSRmlsZVN0YXRlU3RydWN0dXJlEhQKB2NvbnRlbnQYASABKAxI" + + "AIgBARIcCg9pbml0aWFsX2NvbnRlbnQYAiABKAxIAYgBAUIKCghfY29udGVudEISChBfaW5pdGlh" + + "bF9jb250ZW50IjcKClN0ZXBUaW1pbmcSEwoLZHVyYXRpb25fbXMYASABKAQSFAoMdGltZXN0YW1w" + + "X21zGAIgASgEIvYEChFDb252ZXJzYXRpb25TdGF0ZRIhChlyb290X3Byb21wdF9tZXNzYWdlc19q" + + "c29uGAEgAygJEikKBXR1cm5zGAggAygLMhouYWdlbnQudjEuQ29udmVyc2F0aW9uVHVybhIhCgV0" + + "b2RvcxgDIAMoCzISLmFnZW50LnYxLlRvZG9JdGVtEhoKEnBlbmRpbmdfdG9vbF9jYWxscxgEIAMo" + + "CRI5Cg10b2tlbl9kZXRhaWxzGAUgASgLMiIuYWdlbnQudjEuQ29udmVyc2F0aW9uVG9rZW5EZXRh" + + "aWxzEjMKB3N1bW1hcnkYBiABKAsyHS5hZ2VudC52MS5Db252ZXJzYXRpb25TdW1tYXJ5SACIAQES" + + "LQoEcGxhbhgHIAEoCzIaLmFnZW50LnYxLkNvbnZlcnNhdGlvblBsYW5IAYgBARJCCg9zdW1tYXJ5" + + "X2FyY2hpdmUYCSABKAsyJC5hZ2VudC52MS5Db252ZXJzYXRpb25TdW1tYXJ5QXJjaGl2ZUgCiAEB" + + "EkAKC2ZpbGVfc3RhdGVzGAogAygLMisuYWdlbnQudjEuQ29udmVyc2F0aW9uU3RhdGUuRmlsZVN0" + + "YXRlc0VudHJ5Ej4KEHN1bW1hcnlfYXJjaGl2ZXMYCyADKAsyJC5hZ2VudC52MS5Db252ZXJzYXRp" + + "b25TdW1tYXJ5QXJjaGl2ZRpGCg9GaWxlU3RhdGVzRW50cnkSCwoDa2V5GAEgASgJEiIKBXZhbHVl" + + "GAIgASgLMhMuYWdlbnQudjEuRmlsZVN0YXRlOgI4AUIKCghfc3VtbWFyeUIHCgVfcGxhbkISChBf" + + "c3VtbWFyeV9hcmNoaXZlIscBChZTdWJhZ2VudFBlcnNpc3RlZFN0YXRlEkAKEmNvbnZlcnNhdGlv" + + "bl9zdGF0ZRgBIAEoCzIkLmFnZW50LnYxLkNvbnZlcnNhdGlvblN0YXRlU3RydWN0dXJlEhwKFGNy" + + "ZWF0ZWRfdGltZXN0YW1wX21zGAIgASgEEh4KFmxhc3RfdXNlZF90aW1lc3RhbXBfbXMYAyABKAQS" + + "LQoNc3ViYWdlbnRfdHlwZRgEIAEoCzIWLmFnZW50LnYxLlN1YmFnZW50VHlwZSK3BwoaQ29udmVy" + + "c2F0aW9uU3RhdGVTdHJ1Y3R1cmUSEQoJdHVybnNfb2xkGAIgAygMEiEKGXJvb3RfcHJvbXB0X21l" + + "c3NhZ2VzX2pzb24YASADKAwSDQoFdHVybnMYCCADKAwSDQoFdG9kb3MYAyADKAwSGgoScGVuZGlu" + + "Z190b29sX2NhbGxzGAQgAygJEjkKDXRva2VuX2RldGFpbHMYBSABKAsyIi5hZ2VudC52MS5Db252" + + "ZXJzYXRpb25Ub2tlbkRldGFpbHMSFAoHc3VtbWFyeRgGIAEoDEgAiAEBEhEKBHBsYW4YByABKAxI" + + "AYgBARIfChdwcmV2aW91c193b3Jrc3BhY2VfdXJpcxgJIAMoCRIRCgRtb2RlGAogASgFSAKIAQES" + + "HAoPc3VtbWFyeV9hcmNoaXZlGAsgASgMSAOIAQESSQoLZmlsZV9zdGF0ZXMYDCADKAsyNC5hZ2Vu" + + "dC52MS5Db252ZXJzYXRpb25TdGF0ZVN0cnVjdHVyZS5GaWxlU3RhdGVzRW50cnkSTgoOZmlsZV9z" + + "dGF0ZXNfdjIYDyADKAsyNi5hZ2VudC52MS5Db252ZXJzYXRpb25TdGF0ZVN0cnVjdHVyZS5GaWxl" + + "U3RhdGVzVjJFbnRyeRIYChBzdW1tYXJ5X2FyY2hpdmVzGA0gAygMEioKDHR1cm5fdGltaW5ncxgO" + + "IAMoCzIULmFnZW50LnYxLlN0ZXBUaW1pbmcSUQoPc3ViYWdlbnRfc3RhdGVzGBAgAygLMjguYWdl" + + "bnQudjEuQ29udmVyc2F0aW9uU3RhdGVTdHJ1Y3R1cmUuU3ViYWdlbnRTdGF0ZXNFbnRyeRIaChJz" + + "ZWxmX3N1bW1hcnlfY291bnQYESABKA0SEgoKcmVhZF9wYXRocxgSIAMoCRoxCg9GaWxlU3RhdGVz" + + "RW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgMOgI4ARpRChFGaWxlU3RhdGVzVjJFbnRy" + + "eRILCgNrZXkYASABKAkSKwoFdmFsdWUYAiABKAsyHC5hZ2VudC52MS5GaWxlU3RhdGVTdHJ1Y3R1" + + "cmU6AjgBGlcKE1N1YmFnZW50U3RhdGVzRW50cnkSCwoDa2V5GAEgASgJEi8KBXZhbHVlGAIgASgL" + + "MiAuYWdlbnQudjEuU3ViYWdlbnRQZXJzaXN0ZWRTdGF0ZToCOAFCCgoIX3N1bW1hcnlCBwoFX3Bs" + + "YW5CBwoFX21vZGVCEgoQX3N1bW1hcnlfYXJjaGl2ZSIRCg9UaGlua2luZ0RldGFpbHMiSAoRQXBp" + + "S2V5Q3JlZGVudGlhbHMSDwoHYXBpX2tleRgBIAEoCRIVCghiYXNlX3VybBgCIAEoCUgAiAEBQgsK" + + "CV9iYXNlX3VybCJJChBBenVyZUNyZWRlbnRpYWxzEg8KB2FwaV9rZXkYASABKAkSEAoIYmFzZV91" + + "cmwYAiABKAkSEgoKZGVwbG95bWVudBgDIAEoCSJ6ChJCZWRyb2NrQ3JlZGVudGlhbHMSEgoKYWNj" + + "ZXNzX2tleRgBIAEoCRISCgpzZWNyZXRfa2V5GAIgASgJEg4KBnJlZ2lvbhgDIAEoCRIaCg1zZXNz" + + "aW9uX3Rva2VuGAQgASgJSACIAQFCEAoOX3Nlc3Npb25fdG9rZW4isQMKDE1vZGVsRGV0YWlscxIQ" + + "Cghtb2RlbF9pZBgBIAEoCRIYChBkaXNwbGF5X21vZGVsX2lkGAMgASgJEhQKDGRpc3BsYXlfbmFt" + + "ZRgEIAEoCRIaChJkaXNwbGF5X25hbWVfc2hvcnQYBSABKAkSDwoHYWxpYXNlcxgGIAMoCRI4ChB0" + + "aGlua2luZ19kZXRhaWxzGAIgASgLMhkuYWdlbnQudjEuVGhpbmtpbmdEZXRhaWxzSAGIAQESFQoI" + + "bWF4X21vZGUYByABKAhIAogBARI6ChNhcGlfa2V5X2NyZWRlbnRpYWxzGAggASgLMhsuYWdlbnQu" + + "djEuQXBpS2V5Q3JlZGVudGlhbHNIABI3ChFhenVyZV9jcmVkZW50aWFscxgJIAEoCzIaLmFnZW50" + + "LnYxLkF6dXJlQ3JlZGVudGlhbHNIABI7ChNiZWRyb2NrX2NyZWRlbnRpYWxzGAogASgLMhwuYWdl" + + "bnQudjEuQmVkcm9ja0NyZWRlbnRpYWxzSABCDQoLY3JlZGVudGlhbHNCEwoRX3RoaW5raW5nX2Rl" + + "dGFpbHNCCwoJX21heF9tb2RlIrcCCg5SZXF1ZXN0ZWRNb2RlbBIQCghtb2RlbF9pZBgBIAEoCRIQ" + + "CghtYXhfbW9kZRgCIAEoCBJACgpwYXJhbWV0ZXJzGAMgAygLMiwuYWdlbnQudjEuUmVxdWVzdGVk" + + "TW9kZWxfTW9kZWxQYXJhbWV0ZXJieXRlcxI6ChNhcGlfa2V5X2NyZWRlbnRpYWxzGAQgASgLMhsu" + + "YWdlbnQudjEuQXBpS2V5Q3JlZGVudGlhbHNIABI3ChFhenVyZV9jcmVkZW50aWFscxgFIAEoCzIa" + + "LmFnZW50LnYxLkF6dXJlQ3JlZGVudGlhbHNIABI7ChNiZWRyb2NrX2NyZWRlbnRpYWxzGAYgASgL" + + "MhwuYWdlbnQudjEuQmVkcm9ja0NyZWRlbnRpYWxzSABCDQoLY3JlZGVudGlhbHMiPwoiUmVxdWVz" + + "dGVkTW9kZWxfTW9kZWxQYXJhbWV0ZXJieXRlcxIKCgJpZBgBIAEoCRINCgV2YWx1ZRgCIAEoCSK5" + + "BAoPQWdlbnRSdW5SZXF1ZXN0EkAKEmNvbnZlcnNhdGlvbl9zdGF0ZRgBIAEoCzIkLmFnZW50LnYx" + + "LkNvbnZlcnNhdGlvblN0YXRlU3RydWN0dXJlEiwKBmFjdGlvbhgCIAEoCzIcLmFnZW50LnYxLkNv" + + "bnZlcnNhdGlvbkFjdGlvbhItCg1tb2RlbF9kZXRhaWxzGAMgASgLMhYuYWdlbnQudjEuTW9kZWxE" + + "ZXRhaWxzEjYKD3JlcXVlc3RlZF9tb2RlbBgJIAEoCzIYLmFnZW50LnYxLlJlcXVlc3RlZE1vZGVs" + + "SACIAQESJQoJbWNwX3Rvb2xzGAQgASgLMhIuYWdlbnQudjEuTWNwVG9vbHMSHAoPY29udmVyc2F0" + + "aW9uX2lkGAUgASgJSAGIAQESRAoXbWNwX2ZpbGVfc3lzdGVtX29wdGlvbnMYBiABKAsyHi5hZ2Vu" + + "dC52MS5NY3BGaWxlU3lzdGVtT3B0aW9uc0gCiAEBEjIKDXNraWxsX29wdGlvbnMYByABKAsyFi5h" + + "Z2VudC52MS5Ta2lsbE9wdGlvbnNIA4gBARIhChRjdXN0b21fc3lzdGVtX3Byb21wdBgIIAEoCUgE" + + "iAEBQhIKEF9yZXF1ZXN0ZWRfbW9kZWxCEgoQX2NvbnZlcnNhdGlvbl9pZEIaChhfbWNwX2ZpbGVf" + + "c3lzdGVtX29wdGlvbnNCEAoOX3NraWxsX29wdGlvbnNCFwoVX2N1c3RvbV9zeXN0ZW1fcHJvbXB0" + + "Ih8KD1RleHREZWx0YVVwZGF0ZRIMCgR0ZXh0GAEgASgJImYKFVRvb2xDYWxsU3RhcnRlZFVwZGF0" + + "ZRIPCgdjYWxsX2lkGAEgASgJEiUKCXRvb2xfY2FsbBgCIAEoCzISLmFnZW50LnYxLlRvb2xDYWxs" + + "EhUKDW1vZGVsX2NhbGxfaWQYAyABKAkiaAoXVG9vbENhbGxDb21wbGV0ZWRVcGRhdGUSDwoHY2Fs" + + "bF9pZBgBIAEoCRIlCgl0b29sX2NhbGwYAiABKAsyEi5hZ2VudC52MS5Ub29sQ2FsbBIVCg1tb2Rl" + + "bF9jYWxsX2lkGAMgASgJIm8KE1Rvb2xDYWxsRGVsdGFVcGRhdGUSDwoHY2FsbF9pZBgBIAEoCRIw" + + "Cg90b29sX2NhbGxfZGVsdGEYAiABKAsyFy5hZ2VudC52MS5Ub29sQ2FsbERlbHRhEhUKDW1vZGVs" + + "X2NhbGxfaWQYAyABKAkifwoVUGFydGlhbFRvb2xDYWxsVXBkYXRlEg8KB2NhbGxfaWQYASABKAkS" + + "JQoJdG9vbF9jYWxsGAIgASgLMhIuYWdlbnQudjEuVG9vbENhbGwSFwoPYXJnc190ZXh0X2RlbHRh" + + "GAMgASgJEhUKDW1vZGVsX2NhbGxfaWQYBCABKAkiIwoTVGhpbmtpbmdEZWx0YVVwZGF0ZRIMCgR0" + + "ZXh0GAEgASgJIjcKF1RoaW5raW5nQ29tcGxldGVkVXBkYXRlEhwKFHRoaW5raW5nX2R1cmF0aW9u" + + "X21zGAEgASgFIiIKEFRva2VuRGVsdGFVcGRhdGUSDgoGdG9rZW5zGAEgASgFIiAKDVN1bW1hcnlV" + + "cGRhdGUSDwoHc3VtbWFyeRgBIAEoCSIWChRTdW1tYXJ5U3RhcnRlZFVwZGF0ZSIRCg9IZWFydGJl" + + "YXRVcGRhdGUiGAoWU3VtbWFyeUNvbXBsZXRlZFVwZGF0ZSLXAQoWU2hlbGxPdXRwdXREZWx0YVVw" + + "ZGF0ZRItCgZzdGRvdXQYASABKAsyGy5hZ2VudC52MS5TaGVsbFN0cmVhbVN0ZG91dEgAEi0KBnN0" + + "ZGVychgCIAEoCzIbLmFnZW50LnYxLlNoZWxsU3RyZWFtU3RkZXJySAASKQoEZXhpdBgDIAEoCzIZ" + + "LmFnZW50LnYxLlNoZWxsU3RyZWFtRXhpdEgAEisKBXN0YXJ0GAQgASgLMhouYWdlbnQudjEuU2hl" + + "bGxTdHJlYW1TdGFydEgAQgcKBWV2ZW50IhEKD1R1cm5FbmRlZFVwZGF0ZSJIChlVc2VyTWVzc2Fn" + + "ZUFwcGVuZGVkVXBkYXRlEisKDHVzZXJfbWVzc2FnZRgBIAEoCzIVLmFnZW50LnYxLlVzZXJNZXNz" + + "YWdlIiQKEVN0ZXBTdGFydGVkVXBkYXRlEg8KB3N0ZXBfaWQYASABKAQiQAoTU3RlcENvbXBsZXRl" + + "ZFVwZGF0ZRIPCgdzdGVwX2lkGAEgASgEEhgKEHN0ZXBfZHVyYXRpb25fbXMYAiABKAMi7wcKEUlu" + + "dGVyYWN0aW9uVXBkYXRlEi8KCnRleHRfZGVsdGEYASABKAsyGS5hZ2VudC52MS5UZXh0RGVsdGFV" + + "cGRhdGVIABI8ChFwYXJ0aWFsX3Rvb2xfY2FsbBgHIAEoCzIfLmFnZW50LnYxLlBhcnRpYWxUb29s" + + "Q2FsbFVwZGF0ZUgAEjgKD3Rvb2xfY2FsbF9kZWx0YRgPIAEoCzIdLmFnZW50LnYxLlRvb2xDYWxs" + + "RGVsdGFVcGRhdGVIABI8ChF0b29sX2NhbGxfc3RhcnRlZBgCIAEoCzIfLmFnZW50LnYxLlRvb2xD" + + "YWxsU3RhcnRlZFVwZGF0ZUgAEkAKE3Rvb2xfY2FsbF9jb21wbGV0ZWQYAyABKAsyIS5hZ2VudC52" + + "MS5Ub29sQ2FsbENvbXBsZXRlZFVwZGF0ZUgAEjcKDnRoaW5raW5nX2RlbHRhGAQgASgLMh0uYWdl" + + "bnQudjEuVGhpbmtpbmdEZWx0YVVwZGF0ZUgAEj8KEnRoaW5raW5nX2NvbXBsZXRlZBgFIAEoCzIh" + + "LmFnZW50LnYxLlRoaW5raW5nQ29tcGxldGVkVXBkYXRlSAASRAoVdXNlcl9tZXNzYWdlX2FwcGVu" + + "ZGVkGAYgASgLMiMuYWdlbnQudjEuVXNlck1lc3NhZ2VBcHBlbmRlZFVwZGF0ZUgAEjEKC3Rva2Vu" + + "X2RlbHRhGAggASgLMhouYWdlbnQudjEuVG9rZW5EZWx0YVVwZGF0ZUgAEioKB3N1bW1hcnkYCSAB" + + "KAsyFy5hZ2VudC52MS5TdW1tYXJ5VXBkYXRlSAASOQoPc3VtbWFyeV9zdGFydGVkGAogASgLMh4u" + + "YWdlbnQudjEuU3VtbWFyeVN0YXJ0ZWRVcGRhdGVIABI9ChFzdW1tYXJ5X2NvbXBsZXRlZBgLIAEo" + + "CzIgLmFnZW50LnYxLlN1bW1hcnlDb21wbGV0ZWRVcGRhdGVIABI+ChJzaGVsbF9vdXRwdXRfZGVs" + + "dGEYDCABKAsyIC5hZ2VudC52MS5TaGVsbE91dHB1dERlbHRhVXBkYXRlSAASLgoJaGVhcnRiZWF0" + + "GA0gASgLMhkuYWdlbnQudjEuSGVhcnRiZWF0VXBkYXRlSAASLwoKdHVybl9lbmRlZBgOIAEoCzIZ" + + "LmFnZW50LnYxLlR1cm5FbmRlZFVwZGF0ZUgAEjMKDHN0ZXBfc3RhcnRlZBgQIAEoCzIbLmFnZW50" + + "LnYxLlN0ZXBTdGFydGVkVXBkYXRlSAASNwoOc3RlcF9jb21wbGV0ZWQYESABKAsyHS5hZ2VudC52" + + "MS5TdGVwQ29tcGxldGVkVXBkYXRlSABCCQoHbWVzc2FnZSKaBAoQSW50ZXJhY3Rpb25RdWVyeRIK" + + "CgJpZBgBIAEoDRJDChh3ZWJfc2VhcmNoX3JlcXVlc3RfcXVlcnkYAiABKAsyHy5hZ2VudC52MS5X" + + "ZWJTZWFyY2hSZXF1ZXN0UXVlcnlIABJPCh5hc2tfcXVlc3Rpb25faW50ZXJhY3Rpb25fcXVlcnkY" + + "AyABKAsyJS5hZ2VudC52MS5Bc2tRdWVzdGlvbkludGVyYWN0aW9uUXVlcnlIABJFChlzd2l0Y2hf" + + "bW9kZV9yZXF1ZXN0X3F1ZXJ5GAQgASgLMiAuYWdlbnQudjEuU3dpdGNoTW9kZVJlcXVlc3RRdWVy" + + "eUgAEkMKGGV4YV9zZWFyY2hfcmVxdWVzdF9xdWVyeRgFIAEoCzIfLmFnZW50LnYxLkV4YVNlYXJj" + + "aFJlcXVlc3RRdWVyeUgAEkEKF2V4YV9mZXRjaF9yZXF1ZXN0X3F1ZXJ5GAYgASgLMh4uYWdlbnQu" + + "djEuRXhhRmV0Y2hSZXF1ZXN0UXVlcnlIABJFChljcmVhdGVfcGxhbl9yZXF1ZXN0X3F1ZXJ5GAcg" + + "ASgLMiAuYWdlbnQudjEuQ3JlYXRlUGxhblJlcXVlc3RRdWVyeUgAEkUKGXNldHVwX3ZtX2Vudmly" + + "b25tZW50X2FyZ3MYCCABKAsyIC5hZ2VudC52MS5TZXR1cFZtRW52aXJvbm1lbnRBcmdzSABCBwoF" + + "cXVlcnkixgQKE0ludGVyYWN0aW9uUmVzcG9uc2USCgoCaWQYASABKA0SSQobd2ViX3NlYXJjaF9y" + + "ZXF1ZXN0X3Jlc3BvbnNlGAIgASgLMiIuYWdlbnQudjEuV2ViU2VhcmNoUmVxdWVzdFJlc3BvbnNl" + + "SAASVQohYXNrX3F1ZXN0aW9uX2ludGVyYWN0aW9uX3Jlc3BvbnNlGAMgASgLMiguYWdlbnQudjEu" + + "QXNrUXVlc3Rpb25JbnRlcmFjdGlvblJlc3BvbnNlSAASSwocc3dpdGNoX21vZGVfcmVxdWVzdF9y" + + "ZXNwb25zZRgEIAEoCzIjLmFnZW50LnYxLlN3aXRjaE1vZGVSZXF1ZXN0UmVzcG9uc2VIABJJChtl" + + "eGFfc2VhcmNoX3JlcXVlc3RfcmVzcG9uc2UYBSABKAsyIi5hZ2VudC52MS5FeGFTZWFyY2hSZXF1" + + "ZXN0UmVzcG9uc2VIABJHChpleGFfZmV0Y2hfcmVxdWVzdF9yZXNwb25zZRgGIAEoCzIhLmFnZW50" + + "LnYxLkV4YUZldGNoUmVxdWVzdFJlc3BvbnNlSAASSwocY3JlYXRlX3BsYW5fcmVxdWVzdF9yZXNw" + + "b25zZRgHIAEoCzIjLmFnZW50LnYxLkNyZWF0ZVBsYW5SZXF1ZXN0UmVzcG9uc2VIABJJChtzZXR1" + + "cF92bV9lbnZpcm9ubWVudF9yZXN1bHQYCCABKAsyIi5hZ2VudC52MS5TZXR1cFZtRW52aXJvbm1l" + + "bnRSZXN1bHRIAEIICgZyZXN1bHQiXAobQXNrUXVlc3Rpb25JbnRlcmFjdGlvblF1ZXJ5EicKBGFy" + + "Z3MYASABKAsyGS5hZ2VudC52MS5Bc2tRdWVzdGlvbkFyZ3MSFAoMdG9vbF9jYWxsX2lkGAIgASgJ" + + "Ik0KHkFza1F1ZXN0aW9uSW50ZXJhY3Rpb25SZXNwb25zZRIrCgZyZXN1bHQYASABKAsyGy5hZ2Vu" + + "dC52MS5Bc2tRdWVzdGlvblJlc3VsdCIRCg9DbGllbnRIZWFydGJlYXQixgQKDlByZXdhcm1SZXF1" + + "ZXN0Ei0KDW1vZGVsX2RldGFpbHMYASABKAsyFi5hZ2VudC52MS5Nb2RlbERldGFpbHMSNgoPcmVx" + + "dWVzdGVkX21vZGVsGAkgASgLMhguYWdlbnQudjEuUmVxdWVzdGVkTW9kZWxIAIgBARIcCg9jb252" + + "ZXJzYXRpb25faWQYAiABKAlIAYgBARJAChJjb252ZXJzYXRpb25fc3RhdGUYAyABKAsyJC5hZ2Vu" + + "dC52MS5Db252ZXJzYXRpb25TdGF0ZVN0cnVjdHVyZRIlCgltY3BfdG9vbHMYBCABKAsyEi5hZ2Vu" + + "dC52MS5NY3BUb29scxJEChdtY3BfZmlsZV9zeXN0ZW1fb3B0aW9ucxgFIAEoCzIeLmFnZW50LnYx" + + "Lk1jcEZpbGVTeXN0ZW1PcHRpb25zSAKIAQESHwoSYmVzdF9vZl9uX2dyb3VwX2lkGAYgASgJSAOI" + + "AQESKAobdHJ5X3VzZV9iZXN0X29mX25fcHJvbW90aW9uGAcgASgISASIAQESIQoUY3VzdG9tX3N5" + + "c3RlbV9wcm9tcHQYCCABKAlIBYgBAUISChBfcmVxdWVzdGVkX21vZGVsQhIKEF9jb252ZXJzYXRp" + + "b25faWRCGgoYX21jcF9maWxlX3N5c3RlbV9vcHRpb25zQhUKE19iZXN0X29mX25fZ3JvdXBfaWRC" + + "HgocX3RyeV91c2VfYmVzdF9vZl9uX3Byb21vdGlvbkIXChVfY3VzdG9tX3N5c3RlbV9wcm9tcHQi" + + "HQoPRXhlY1NlcnZlckFib3J0EgoKAmlkGAEgASgNIlEKGEV4ZWNTZXJ2ZXJDb250cm9sTWVzc2Fn" + + "ZRIqCgVhYm9ydBgBIAEoCzIZLmFnZW50LnYxLkV4ZWNTZXJ2ZXJBYm9ydEgAQgkKB21lc3NhZ2Ui" + + "+AMKEkFnZW50Q2xpZW50TWVzc2FnZRIwCgtydW5fcmVxdWVzdBgBIAEoCzIZLmFnZW50LnYxLkFn" + + "ZW50UnVuUmVxdWVzdEgAEjoKE2V4ZWNfY2xpZW50X21lc3NhZ2UYAiABKAsyGy5hZ2VudC52MS5F" + + "eGVjQ2xpZW50TWVzc2FnZUgAEkkKG2V4ZWNfY2xpZW50X2NvbnRyb2xfbWVzc2FnZRgFIAEoCzIi" + + "LmFnZW50LnYxLkV4ZWNDbGllbnRDb250cm9sTWVzc2FnZUgAEjYKEWt2X2NsaWVudF9tZXNzYWdl" + + "GAMgASgLMhkuYWdlbnQudjEuS3ZDbGllbnRNZXNzYWdlSAASOwoTY29udmVyc2F0aW9uX2FjdGlv" + + "bhgEIAEoCzIcLmFnZW50LnYxLkNvbnZlcnNhdGlvbkFjdGlvbkgAEj0KFGludGVyYWN0aW9uX3Jl" + + "c3BvbnNlGAYgASgLMh0uYWdlbnQudjEuSW50ZXJhY3Rpb25SZXNwb25zZUgAEjUKEGNsaWVudF9o" + + "ZWFydGJlYXQYByABKAsyGS5hZ2VudC52MS5DbGllbnRIZWFydGJlYXRIABIzCg9wcmV3YXJtX3Jl" + + "cXVlc3QYCCABKAsyGC5hZ2VudC52MS5QcmV3YXJtUmVxdWVzdEgAQgkKB21lc3NhZ2UiogMKEkFn" + + "ZW50U2VydmVyTWVzc2FnZRI5ChJpbnRlcmFjdGlvbl91cGRhdGUYASABKAsyGy5hZ2VudC52MS5J" + + "bnRlcmFjdGlvblVwZGF0ZUgAEjoKE2V4ZWNfc2VydmVyX21lc3NhZ2UYAiABKAsyGy5hZ2VudC52" + + "MS5FeGVjU2VydmVyTWVzc2FnZUgAEkkKG2V4ZWNfc2VydmVyX2NvbnRyb2xfbWVzc2FnZRgFIAEo" + + "CzIiLmFnZW50LnYxLkV4ZWNTZXJ2ZXJDb250cm9sTWVzc2FnZUgAEk4KHmNvbnZlcnNhdGlvbl9j" + + "aGVja3BvaW50X3VwZGF0ZRgDIAEoCzIkLmFnZW50LnYxLkNvbnZlcnNhdGlvblN0YXRlU3RydWN0" + + "dXJlSAASNgoRa3Zfc2VydmVyX21lc3NhZ2UYBCABKAsyGS5hZ2VudC52MS5LdlNlcnZlck1lc3Nh" + + "Z2VIABI3ChFpbnRlcmFjdGlvbl9xdWVyeRgHIAEoCzIaLmFnZW50LnYxLkludGVyYWN0aW9uUXVl" + + "cnlIAEIJCgdtZXNzYWdlIigKEE5hbWVBZ2VudFJlcXVlc3QSFAoMdXNlcl9tZXNzYWdlGAEgASgJ" + + "IiEKEU5hbWVBZ2VudFJlc3BvbnNlEgwKBG5hbWUYASABKAkiMgoWR2V0VXNhYmxlTW9kZWxzUmVx" + + "dWVzdBIYChBjdXN0b21fbW9kZWxfaWRzGAEgAygJIkEKF0dldFVzYWJsZU1vZGVsc1Jlc3BvbnNl" + + "EiYKBm1vZGVscxgBIAMoCzIWLmFnZW50LnYxLk1vZGVsRGV0YWlscyIeChxHZXREZWZhdWx0TW9k" + + "ZWxGb3JDbGlSZXF1ZXN0IkYKHUdldERlZmF1bHRNb2RlbEZvckNsaVJlc3BvbnNlEiUKBW1vZGVs" + + "GAEgASgLMhYuYWdlbnQudjEuTW9kZWxEZXRhaWxzIh8KHUdldEFsbG93ZWRNb2RlbEludGVudHNS" + + "ZXF1ZXN0IjcKHkdldEFsbG93ZWRNb2RlbEludGVudHNSZXNwb25zZRIVCg1tb2RlbF9pbnRlbnRz" + + "GAEgAygJIpcCChNJZGVFZGl0b3JzU3RhdGVGaWxlEhUKDXJlbGF0aXZlX3BhdGgYASABKAkSFQoN" + + "YWJzb2x1dGVfcGF0aBgCIAEoCRIhChRpc19jdXJyZW50bHlfZm9jdXNlZBgDIAEoCEgAiAEBEiAK" + + "E2N1cnJlbnRfbGluZV9udW1iZXIYBCABKAVIAYgBARIeChFjdXJyZW50X2xpbmVfdGV4dBgFIAEo" + + "CUgCiAEBEhcKCmxpbmVfY291bnQYBiABKAVIA4gBAUIXChVfaXNfY3VycmVudGx5X2ZvY3VzZWRC" + + "FgoUX2N1cnJlbnRfbGluZV9udW1iZXJCFAoSX2N1cnJlbnRfbGluZV90ZXh0Qg0KC19saW5lX2Nv" + + "dW50IlMKE0lkZUVkaXRvcnNTdGF0ZUxpdGUSPAoVcmVjZW50bHlfdmlld2VkX2ZpbGVzGAEgAygL" + + "Mh0uYWdlbnQudjEuSWRlRWRpdG9yc1N0YXRlRmlsZSJ0ChZBcHBseUFnZW50RGlmZlRvb2xDYWxs" + + "EioKBGFyZ3MYASABKAsyHC5hZ2VudC52MS5BcHBseUFnZW50RGlmZkFyZ3MSLgoGcmVzdWx0GAIg" + + "ASgLMh4uYWdlbnQudjEuQXBwbHlBZ2VudERpZmZSZXN1bHQiJgoSQXBwbHlBZ2VudERpZmZBcmdz" + + "EhAKCGFnZW50X2lkGAEgASgJIoQBChRBcHBseUFnZW50RGlmZlJlc3VsdBIyCgdzdWNjZXNzGAEg" + + "ASgLMh8uYWdlbnQudjEuQXBwbHlBZ2VudERpZmZTdWNjZXNzSAASLgoFZXJyb3IYAiABKAsyHS5h" + + "Z2VudC52MS5BcHBseUFnZW50RGlmZkVycm9ySABCCAoGcmVzdWx0Ik4KFUFwcGx5QWdlbnREaWZm" + + "U3VjY2VzcxI1Cg9hcHBsaWVkX2NoYW5nZXMYASADKAsyHC5hZ2VudC52MS5BcHBsaWVkQWdlbnRD" + + "aGFuZ2Ui6QEKEkFwcGxpZWRBZ2VudENoYW5nZRIMCgRwYXRoGAEgASgJEhMKC2NoYW5nZV90eXBl" + + "GAIgASgFEhsKDmJlZm9yZV9jb250ZW50GAMgASgJSACIAQESGgoNYWZ0ZXJfY29udGVudBgEIAEo" + + "CUgBiAEBEhIKBWVycm9yGAUgASgJSAKIAQESHgoRbWVzc2FnZV9mb3JfbW9kZWwYBiABKAlIA4gB" + + "AUIRCg9fYmVmb3JlX2NvbnRlbnRCEAoOX2FmdGVyX2NvbnRlbnRCCAoGX2Vycm9yQhQKEl9tZXNz" + + "YWdlX2Zvcl9tb2RlbCJbChNBcHBseUFnZW50RGlmZkVycm9yEg0KBWVycm9yGAEgASgJEjUKD2Fw" + + "cGxpZWRfY2hhbmdlcxgCIAMoCzIcLmFnZW50LnYxLkFwcGxpZWRBZ2VudENoYW5nZSJrChNBc2tR" + + "dWVzdGlvblRvb2xDYWxsEicKBGFyZ3MYASABKAsyGS5hZ2VudC52MS5Bc2tRdWVzdGlvbkFyZ3MS" + + "KwoGcmVzdWx0GAIgASgLMhsuYWdlbnQudjEuQXNrUXVlc3Rpb25SZXN1bHQijwEKD0Fza1F1ZXN0" + + "aW9uQXJncxINCgV0aXRsZRgBIAEoCRI1CglxdWVzdGlvbnMYAiADKAsyIi5hZ2VudC52MS5Bc2tR" + + "dWVzdGlvbkFyZ3NfUXVlc3Rpb24SEQoJcnVuX2FzeW5jGAUgASgIEiMKG2FzeW5jX29yaWdpbmFs" + + "X3Rvb2xfY2FsbF9pZBgGIAEoCSKBAQoYQXNrUXVlc3Rpb25BcmdzX1F1ZXN0aW9uEgoKAmlkGAEg" + + "ASgJEg4KBnByb21wdBgCIAEoCRIxCgdvcHRpb25zGAMgAygLMiAuYWdlbnQudjEuQXNrUXVlc3Rp" + + "b25BcmdzX09wdGlvbhIWCg5hbGxvd19tdWx0aXBsZRgEIAEoCCIzChZBc2tRdWVzdGlvbkFyZ3Nf" + + "T3B0aW9uEgoKAmlkGAEgASgJEg0KBWxhYmVsGAIgASgJIhIKEEFza1F1ZXN0aW9uQXN5bmMi2wEK" + + "EUFza1F1ZXN0aW9uUmVzdWx0Ei8KB3N1Y2Nlc3MYASABKAsyHC5hZ2VudC52MS5Bc2tRdWVzdGlv" + + "blN1Y2Nlc3NIABIrCgVlcnJvchgCIAEoCzIaLmFnZW50LnYxLkFza1F1ZXN0aW9uRXJyb3JIABIx" + + "CghyZWplY3RlZBgDIAEoCzIdLmFnZW50LnYxLkFza1F1ZXN0aW9uUmVqZWN0ZWRIABIrCgVhc3lu" + + "YxgEIAEoCzIaLmFnZW50LnYxLkFza1F1ZXN0aW9uQXN5bmNIAEIICgZyZXN1bHQiSgoSQXNrUXVl" + + "c3Rpb25TdWNjZXNzEjQKB2Fuc3dlcnMYASADKAsyIy5hZ2VudC52MS5Bc2tRdWVzdGlvblN1Y2Nl" + + "c3NfQW5zd2VyIk0KGUFza1F1ZXN0aW9uU3VjY2Vzc19BbnN3ZXISEwoLcXVlc3Rpb25faWQYASAB" + + "KAkSGwoTc2VsZWN0ZWRfb3B0aW9uX2lkcxgCIAMoCSIpChBBc2tRdWVzdGlvbkVycm9yEhUKDWVy" + + "cm9yX21lc3NhZ2UYASABKAkiJQoTQXNrUXVlc3Rpb25SZWplY3RlZBIOCgZyZWFzb24YASABKAki" + + "iQIKGEJhY2tncm91bmRTaGVsbFNwYXduQXJncxIPCgdjb21tYW5kGAEgASgJEhkKEXdvcmtpbmdf" + + "ZGlyZWN0b3J5GAIgASgJEhQKDHRvb2xfY2FsbF9pZBgDIAEoCRI7Cg5wYXJzaW5nX3Jlc3VsdBgE" + + "IAEoCzIjLmFnZW50LnYxLlNoZWxsQ29tbWFuZFBhcnNpbmdSZXN1bHQSNAoOc2FuZGJveF9wb2xp" + + "Y3kYBSABKAsyFy5hZ2VudC52MS5TYW5kYm94UG9saWN5SACIAQESJQodZW5hYmxlX3dyaXRlX3No" + + "ZWxsX3N0ZGluX3Rvb2wYBiABKAhCEQoPX3NhbmRib3hfcG9saWN5IoECChpCYWNrZ3JvdW5kU2hl" + + "bGxTcGF3blJlc3VsdBI4CgdzdWNjZXNzGAEgASgLMiUuYWdlbnQudjEuQmFja2dyb3VuZFNoZWxs" + + "U3Bhd25TdWNjZXNzSAASNAoFZXJyb3IYAiABKAsyIy5hZ2VudC52MS5CYWNrZ3JvdW5kU2hlbGxT" + + "cGF3bkVycm9ySAASKwoIcmVqZWN0ZWQYAyABKAsyFy5hZ2VudC52MS5TaGVsbFJlamVjdGVkSAAS" + + "PAoRcGVybWlzc2lvbl9kZW5pZWQYBCABKAsyHy5hZ2VudC52MS5TaGVsbFBlcm1pc3Npb25EZW5p" + + "ZWRIAEIICgZyZXN1bHQidQobQmFja2dyb3VuZFNoZWxsU3Bhd25TdWNjZXNzEhAKCHNoZWxsX2lk" + + "GAEgASgNEg8KB2NvbW1hbmQYAiABKAkSGQoRd29ya2luZ19kaXJlY3RvcnkYAyABKAkSEAoDcGlk" + + "GAQgASgNSACIAQFCBgoEX3BpZCJWChlCYWNrZ3JvdW5kU2hlbGxTcGF3bkVycm9yEg8KB2NvbW1h" + + "bmQYASABKAkSGQoRd29ya2luZ19kaXJlY3RvcnkYAiABKAkSDQoFZXJyb3IYAyABKAkiNgoTV3Jp" + + "dGVTaGVsbFN0ZGluQXJncxIQCghzaGVsbF9pZBgBIAEoDRINCgVjaGFycxgCIAEoCSKHAQoVV3Jp" + + "dGVTaGVsbFN0ZGluUmVzdWx0EjMKB3N1Y2Nlc3MYASABKAsyIC5hZ2VudC52MS5Xcml0ZVNoZWxs" + + "U3RkaW5TdWNjZXNzSAASLwoFZXJyb3IYAiABKAsyHi5hZ2VudC52MS5Xcml0ZVNoZWxsU3RkaW5F" + + "cnJvckgAQggKBnJlc3VsdCJdChZXcml0ZVNoZWxsU3RkaW5TdWNjZXNzEhAKCHNoZWxsX2lkGAEg" + + "ASgNEjEKKXRlcm1pbmFsX2ZpbGVfbGVuZ3RoX2JlZm9yZV9pbnB1dF93cml0dGVuGAIgASgNIiUK" + + "FFdyaXRlU2hlbGxTdGRpbkVycm9yEg0KBWVycm9yGAEgASgJIiIKCkNvb3JkaW5hdGUSCQoBeBgB" + + "IAEoBRIJCgF5GAIgASgFIlUKD0NvbXB1dGVyVXNlQXJncxIUCgx0b29sX2NhbGxfaWQYASABKAkS" + + "LAoHYWN0aW9ucxgCIAMoCzIbLmFnZW50LnYxLkNvbXB1dGVyVXNlQWN0aW9uIoEEChFDb21wdXRl" + + "clVzZUFjdGlvbhIvCgptb3VzZV9tb3ZlGAEgASgLMhkuYWdlbnQudjEuTW91c2VNb3ZlQWN0aW9u" + + "SAASJgoFY2xpY2sYAiABKAsyFS5hZ2VudC52MS5DbGlja0FjdGlvbkgAEi8KCm1vdXNlX2Rvd24Y" + + "AyABKAsyGS5hZ2VudC52MS5Nb3VzZURvd25BY3Rpb25IABIrCghtb3VzZV91cBgEIAEoCzIXLmFn" + + "ZW50LnYxLk1vdXNlVXBBY3Rpb25IABIkCgRkcmFnGAUgASgLMhQuYWdlbnQudjEuRHJhZ0FjdGlv" + + "bkgAEigKBnNjcm9sbBgGIAEoCzIWLmFnZW50LnYxLlNjcm9sbEFjdGlvbkgAEiQKBHR5cGUYByAB" + + "KAsyFC5hZ2VudC52MS5UeXBlQWN0aW9uSAASIgoDa2V5GAggASgLMhMuYWdlbnQudjEuS2V5QWN0" + + "aW9uSAASJAoEd2FpdBgJIAEoCzIULmFnZW50LnYxLldhaXRBY3Rpb25IABIwCgpzY3JlZW5zaG90" + + "GAogASgLMhouYWdlbnQudjEuU2NyZWVuc2hvdEFjdGlvbkgAEjkKD2N1cnNvcl9wb3NpdGlvbhgL" + + "IAEoCzIeLmFnZW50LnYxLkN1cnNvclBvc2l0aW9uQWN0aW9uSABCCAoGYWN0aW9uIjsKD01vdXNl" + + "TW92ZUFjdGlvbhIoCgpjb29yZGluYXRlGAEgASgLMhQuYWdlbnQudjEuQ29vcmRpbmF0ZSKYAQoL" + + "Q2xpY2tBY3Rpb24SLQoKY29vcmRpbmF0ZRgBIAEoCzIULmFnZW50LnYxLkNvb3JkaW5hdGVIAIgB" + + "ARIOCgZidXR0b24YAiABKAUSDQoFY291bnQYAyABKAUSGgoNbW9kaWZpZXJfa2V5cxgEIAEoCUgB" + + "iAEBQg0KC19jb29yZGluYXRlQhAKDl9tb2RpZmllcl9rZXlzIiEKD01vdXNlRG93bkFjdGlvbhIO" + + "CgZidXR0b24YASABKAUiHwoNTW91c2VVcEFjdGlvbhIOCgZidXR0b24YASABKAUiQAoKRHJhZ0Fj" + + "dGlvbhIiCgRwYXRoGAEgAygLMhQuYWdlbnQudjEuQ29vcmRpbmF0ZRIOCgZidXR0b24YAiABKAUi" + + "nQEKDFNjcm9sbEFjdGlvbhItCgpjb29yZGluYXRlGAEgASgLMhQuYWdlbnQudjEuQ29vcmRpbmF0" + + "ZUgAiAEBEhEKCWRpcmVjdGlvbhgCIAEoBRIOCgZhbW91bnQYAyABKAUSGgoNbW9kaWZpZXJfa2V5" + + "cxgEIAEoCUgBiAEBQg0KC19jb29yZGluYXRlQhAKDl9tb2RpZmllcl9rZXlzIhoKClR5cGVBY3Rp" + + "b24SDAoEdGV4dBgBIAEoCSJMCglLZXlBY3Rpb24SCwoDa2V5GAEgASgJEh0KEGhvbGRfZHVyYXRp" + + "b25fbXMYAiABKAVIAIgBAUITChFfaG9sZF9kdXJhdGlvbl9tcyIhCgpXYWl0QWN0aW9uEhMKC2R1" + + "cmF0aW9uX21zGAEgASgFIhIKEFNjcmVlbnNob3RBY3Rpb24iFgoUQ3Vyc29yUG9zaXRpb25BY3Rp" + + "b24iewoRQ29tcHV0ZXJVc2VSZXN1bHQSLwoHc3VjY2VzcxgBIAEoCzIcLmFnZW50LnYxLkNvbXB1" + + "dGVyVXNlU3VjY2Vzc0gAEisKBWVycm9yGAIgASgLMhouYWdlbnQudjEuQ29tcHV0ZXJVc2VFcnJv" + + "ckgAQggKBnJlc3VsdCL7AQoSQ29tcHV0ZXJVc2VTdWNjZXNzEhQKDGFjdGlvbl9jb3VudBgBIAEo" + + "BRITCgtkdXJhdGlvbl9tcxgCIAEoBRIXCgpzY3JlZW5zaG90GAMgASgJSACIAQESEAoDbG9nGAQg" + + "ASgJSAGIAQESHAoPc2NyZWVuc2hvdF9wYXRoGAUgASgJSAKIAQESMgoPY3Vyc29yX3Bvc2l0aW9u" + + "GAYgASgLMhQuYWdlbnQudjEuQ29vcmRpbmF0ZUgDiAEBQg0KC19zY3JlZW5zaG90QgYKBF9sb2dC" + + "EgoQX3NjcmVlbnNob3RfcGF0aEISChBfY3Vyc29yX3Bvc2l0aW9uIsABChBDb21wdXRlclVzZUVy" + + "cm9yEg0KBWVycm9yGAEgASgJEhQKDGFjdGlvbl9jb3VudBgCIAEoBRITCgtkdXJhdGlvbl9tcxgD" + + "IAEoBRIQCgNsb2cYBCABKAlIAIgBARIXCgpzY3JlZW5zaG90GAUgASgJSAGIAQESHAoPc2NyZWVu" + + "c2hvdF9wYXRoGAYgASgJSAKIAQFCBgoEX2xvZ0INCgtfc2NyZWVuc2hvdEISChBfc2NyZWVuc2hv" + + "dF9wYXRoImsKE0NvbXB1dGVyVXNlVG9vbENhbGwSJwoEYXJncxgBIAEoCzIZLmFnZW50LnYxLkNv" + + "bXB1dGVyVXNlQXJncxIrCgZyZXN1bHQYAiABKAsyGy5hZ2VudC52MS5Db21wdXRlclVzZVJlc3Vs" + + "dCJoChJDcmVhdGVQbGFuVG9vbENhbGwSJgoEYXJncxgBIAEoCzIYLmFnZW50LnYxLkNyZWF0ZVBs" + + "YW5BcmdzEioKBnJlc3VsdBgCIAEoCzIaLmFnZW50LnYxLkNyZWF0ZVBsYW5SZXN1bHQiOAoFUGhh" + + "c2USDAoEbmFtZRgBIAEoCRIhCgV0b2RvcxgCIAMoCzISLmFnZW50LnYxLlRvZG9JdGVtIpYBCg5D" + + "cmVhdGVQbGFuQXJncxIMCgRwbGFuGAEgASgJEiEKBXRvZG9zGAIgAygLMhIuYWdlbnQudjEuVG9k" + + "b0l0ZW0SEAoIb3ZlcnZpZXcYAyABKAkSDAoEbmFtZRgEIAEoCRISCgppc19wcm9qZWN0GAUgASgI" + + "Eh8KBnBoYXNlcxgGIAMoCzIPLmFnZW50LnYxLlBoYXNlIooBChBDcmVhdGVQbGFuUmVzdWx0EhAK" + + "CHBsYW5fdXJpGAMgASgJEi4KB3N1Y2Nlc3MYASABKAsyGy5hZ2VudC52MS5DcmVhdGVQbGFuU3Vj" + + "Y2Vzc0gAEioKBWVycm9yGAIgASgLMhkuYWdlbnQudjEuQ3JlYXRlUGxhbkVycm9ySABCCAoGcmVz" + + "dWx0IhMKEUNyZWF0ZVBsYW5TdWNjZXNzIiAKD0NyZWF0ZVBsYW5FcnJvchINCgVlcnJvchgBIAEo" + + "CSJWChZDcmVhdGVQbGFuUmVxdWVzdFF1ZXJ5EiYKBGFyZ3MYASABKAsyGC5hZ2VudC52MS5DcmVh" + + "dGVQbGFuQXJncxIUCgx0b29sX2NhbGxfaWQYAiABKAkiRwoZQ3JlYXRlUGxhblJlcXVlc3RSZXNw" + + "b25zZRIqCgZyZXN1bHQYASABKAsyGi5hZ2VudC52MS5DcmVhdGVQbGFuUmVzdWx0IhYKFEN1cnNv" + + "clJ1bGVUeXBlR2xvYmFsIigKF0N1cnNvclJ1bGVUeXBlRmlsZUdsb2JzEg0KBWdsb2JzGAEgAygJ" + + "IjEKGkN1cnNvclJ1bGVUeXBlQWdlbnRGZXRjaGVkEhMKC2Rlc2NyaXB0aW9uGAEgASgJIiAKHkN1" + + "cnNvclJ1bGVUeXBlTWFudWFsbHlBdHRhY2hlZCKLAgoOQ3Vyc29yUnVsZVR5cGUSMAoGZ2xvYmFs" + + "GAEgASgLMh4uYWdlbnQudjEuQ3Vyc29yUnVsZVR5cGVHbG9iYWxIABI5CgxmaWxlX2dsb2JiZWQY" + + "AiABKAsyIS5hZ2VudC52MS5DdXJzb3JSdWxlVHlwZUZpbGVHbG9ic0gAEj0KDWFnZW50X2ZldGNo" + + "ZWQYAyABKAsyJC5hZ2VudC52MS5DdXJzb3JSdWxlVHlwZUFnZW50RmV0Y2hlZEgAEkUKEW1hbnVh" + + "bGx5X2F0dGFjaGVkGAQgASgLMiguYWdlbnQudjEuQ3Vyc29yUnVsZVR5cGVNYW51YWxseUF0dGFj" + + "aGVkSABCBgoEdHlwZSLIAQoKQ3Vyc29yUnVsZRIRCglmdWxsX3BhdGgYASABKAkSDwoHY29udGVu" + + "dBgCIAEoCRImCgR0eXBlGAMgASgLMhguYWdlbnQudjEuQ3Vyc29yUnVsZVR5cGUSDgoGc291cmNl" + + "GAQgASgFEh4KEWdpdF9yZW1vdGVfb3JpZ2luGAUgASgJSACIAQESGAoLcGFyc2VfZXJyb3IYBiAB" + + "KAlIAYgBAUIUChJfZ2l0X3JlbW90ZV9vcmlnaW5CDgoMX3BhcnNlX2Vycm9yIjAKCkRlbGV0ZUFy" + + "Z3MSDAoEcGF0aBgBIAEoCRIUCgx0b29sX2NhbGxfaWQYAiABKAki7QIKDERlbGV0ZVJlc3VsdBIq" + + "CgdzdWNjZXNzGAEgASgLMhcuYWdlbnQudjEuRGVsZXRlU3VjY2Vzc0gAEjYKDmZpbGVfbm90X2Zv" + + "dW5kGAIgASgLMhwuYWdlbnQudjEuRGVsZXRlRmlsZU5vdEZvdW5kSAASKwoIbm90X2ZpbGUYAyAB" + + "KAsyFy5hZ2VudC52MS5EZWxldGVOb3RGaWxlSAASPQoRcGVybWlzc2lvbl9kZW5pZWQYBCABKAsy" + + "IC5hZ2VudC52MS5EZWxldGVQZXJtaXNzaW9uRGVuaWVkSAASLQoJZmlsZV9idXN5GAUgASgLMhgu" + + "YWdlbnQudjEuRGVsZXRlRmlsZUJ1c3lIABIsCghyZWplY3RlZBgGIAEoCzIYLmFnZW50LnYxLkRl" + + "bGV0ZVJlamVjdGVkSAASJgoFZXJyb3IYByABKAsyFS5hZ2VudC52MS5EZWxldGVFcnJvckgAQggK" + + "BnJlc3VsdCJcCg1EZWxldGVTdWNjZXNzEgwKBHBhdGgYASABKAkSFAoMZGVsZXRlZF9maWxlGAIg" + + "ASgJEhEKCWZpbGVfc2l6ZRgDIAEoAxIUCgxwcmV2X2NvbnRlbnQYBCABKAkiIgoSRGVsZXRlRmls" + + "ZU5vdEZvdW5kEgwKBHBhdGgYASABKAkiMgoNRGVsZXRlTm90RmlsZRIMCgRwYXRoGAEgASgJEhMK" + + "C2FjdHVhbF90eXBlGAIgASgJIlkKFkRlbGV0ZVBlcm1pc3Npb25EZW5pZWQSDAoEcGF0aBgBIAEo" + + "CRIcChRjbGllbnRfdmlzaWJsZV9lcnJvchgCIAEoCRITCgtpc19yZWFkb25seRgDIAEoCCIeCg5E" + + "ZWxldGVGaWxlQnVzeRIMCgRwYXRoGAEgASgJIi4KDkRlbGV0ZVJlamVjdGVkEgwKBHBhdGgYASAB" + + "KAkSDgoGcmVhc29uGAIgASgJIioKC0RlbGV0ZUVycm9yEgwKBHBhdGgYASABKAkSDQoFZXJyb3IY" + + "AiABKAkiXAoORGVsZXRlVG9vbENhbGwSIgoEYXJncxgBIAEoCzIULmFnZW50LnYxLkRlbGV0ZUFy" + + "Z3MSJgoGcmVzdWx0GAIgASgLMhYuYWdlbnQudjEuRGVsZXRlUmVzdWx0IjUKD0RpYWdub3N0aWNz" + + "QXJncxIMCgRwYXRoGAEgASgJEhQKDHRvb2xfY2FsbF9pZBgCIAEoCSKvAgoRRGlhZ25vc3RpY3NS" + + "ZXN1bHQSLwoHc3VjY2VzcxgBIAEoCzIcLmFnZW50LnYxLkRpYWdub3N0aWNzU3VjY2Vzc0gAEisK" + + "BWVycm9yGAIgASgLMhouYWdlbnQudjEuRGlhZ25vc3RpY3NFcnJvckgAEjEKCHJlamVjdGVkGAMg" + + "ASgLMh0uYWdlbnQudjEuRGlhZ25vc3RpY3NSZWplY3RlZEgAEjsKDmZpbGVfbm90X2ZvdW5kGAQg" + + "ASgLMiEuYWdlbnQudjEuRGlhZ25vc3RpY3NGaWxlTm90Rm91bmRIABJCChFwZXJtaXNzaW9uX2Rl" + + "bmllZBgFIAEoCzIlLmFnZW50LnYxLkRpYWdub3N0aWNzUGVybWlzc2lvbkRlbmllZEgAQggKBnJl" + + "c3VsdCJoChJEaWFnbm9zdGljc1N1Y2Nlc3MSDAoEcGF0aBgBIAEoCRIpCgtkaWFnbm9zdGljcxgC" + + "IAMoCzIULmFnZW50LnYxLkRpYWdub3N0aWMSGQoRdG90YWxfZGlhZ25vc3RpY3MYAyABKAUifwoK" + + "RGlhZ25vc3RpYxIQCghzZXZlcml0eRgBIAEoBRIeCgVyYW5nZRgCIAEoCzIPLmFnZW50LnYxLlJh" + + "bmdlEg8KB21lc3NhZ2UYAyABKAkSDgoGc291cmNlGAQgASgJEgwKBGNvZGUYBSABKAkSEAoIaXNf" + + "c3RhbGUYBiABKAgiLwoQRGlhZ25vc3RpY3NFcnJvchIMCgRwYXRoGAEgASgJEg0KBWVycm9yGAIg" + + "ASgJIjMKE0RpYWdub3N0aWNzUmVqZWN0ZWQSDAoEcGF0aBgBIAEoCRIOCgZyZWFzb24YAiABKAki" + + "JwoXRGlhZ25vc3RpY3NGaWxlTm90Rm91bmQSDAoEcGF0aBgBIAEoCSIrChtEaWFnbm9zdGljc1Bl" + + "cm1pc3Npb25EZW5pZWQSDAoEcGF0aBgBIAEoCSJICghFZGl0QXJncxIMCgRwYXRoGAEgASgJEhsK" + + "DnN0cmVhbV9jb250ZW50GAYgASgJSACIAQFCEQoPX3N0cmVhbV9jb250ZW50ItYCCgpFZGl0UmVz" + + "dWx0EigKB3N1Y2Nlc3MYASABKAsyFS5hZ2VudC52MS5FZGl0U3VjY2Vzc0gAEjQKDmZpbGVfbm90" + + "X2ZvdW5kGAIgASgLMhouYWdlbnQudjEuRWRpdEZpbGVOb3RGb3VuZEgAEkQKFnJlYWRfcGVybWlz" + + "c2lvbl9kZW5pZWQYAyABKAsyIi5hZ2VudC52MS5FZGl0UmVhZFBlcm1pc3Npb25EZW5pZWRIABJG" + + "Chd3cml0ZV9wZXJtaXNzaW9uX2RlbmllZBgEIAEoCzIjLmFnZW50LnYxLkVkaXRXcml0ZVBlcm1p" + + "c3Npb25EZW5pZWRIABIqCghyZWplY3RlZBgGIAEoCzIWLmFnZW50LnYxLkVkaXRSZWplY3RlZEgA" + + "EiQKBWVycm9yGAcgASgLMhMuYWdlbnQudjEuRWRpdEVycm9ySABCCAoGcmVzdWx0IqQCCgtFZGl0" + + "U3VjY2VzcxIMCgRwYXRoGAEgASgJEhgKC2xpbmVzX2FkZGVkGAMgASgFSACIAQESGgoNbGluZXNf" + + "cmVtb3ZlZBgEIAEoBUgBiAEBEhgKC2RpZmZfc3RyaW5nGAUgASgJSAKIAQESJQoYYmVmb3JlX2Z1" + + "bGxfZmlsZV9jb250ZW50GAYgASgJSAOIAQESHwoXYWZ0ZXJfZnVsbF9maWxlX2NvbnRlbnQYByAB" + + "KAkSFAoHbWVzc2FnZRgIIAEoCUgEiAEBQg4KDF9saW5lc19hZGRlZEIQCg5fbGluZXNfcmVtb3Zl" + + "ZEIOCgxfZGlmZl9zdHJpbmdCGwoZX2JlZm9yZV9mdWxsX2ZpbGVfY29udGVudEIKCghfbWVzc2Fn" + + "ZSIgChBFZGl0RmlsZU5vdEZvdW5kEgwKBHBhdGgYASABKAkiKAoYRWRpdFJlYWRQZXJtaXNzaW9u" + + "RGVuaWVkEgwKBHBhdGgYASABKAkiTQoZRWRpdFdyaXRlUGVybWlzc2lvbkRlbmllZBIMCgRwYXRo" + + "GAEgASgJEg0KBWVycm9yGAIgASgJEhMKC2lzX3JlYWRvbmx5GAMgASgIIiwKDEVkaXRSZWplY3Rl" + + "ZBIMCgRwYXRoGAEgASgJEg4KBnJlYXNvbhgCIAEoCSJiCglFZGl0RXJyb3ISDAoEcGF0aBgBIAEo" + + "CRINCgVlcnJvchgCIAEoCRIgChNtb2RlbF92aXNpYmxlX2Vycm9yGAUgASgJSACIAQFCFgoUX21v" + + "ZGVsX3Zpc2libGVfZXJyb3IiVgoMRWRpdFRvb2xDYWxsEiAKBGFyZ3MYASABKAsyEi5hZ2VudC52" + + "MS5FZGl0QXJncxIkCgZyZXN1bHQYAiABKAsyFC5hZ2VudC52MS5FZGl0UmVzdWx0IjEKEUVkaXRU" + + "b29sQ2FsbERlbHRhEhwKFHN0cmVhbV9jb250ZW50X2RlbHRhGAEgASgJIjEKDEV4YUZldGNoQXJn" + + "cxILCgNpZHMYASADKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJIqIBCg5FeGFGZXRjaFJlc3VsdBIs" + + "CgdzdWNjZXNzGAEgASgLMhkuYWdlbnQudjEuRXhhRmV0Y2hTdWNjZXNzSAASKAoFZXJyb3IYAiAB" + + "KAsyFy5hZ2VudC52MS5FeGFGZXRjaEVycm9ySAASLgoIcmVqZWN0ZWQYAyABKAsyGi5hZ2VudC52" + + "MS5FeGFGZXRjaFJlamVjdGVkSABCCAoGcmVzdWx0Ij4KD0V4YUZldGNoU3VjY2VzcxIrCghjb250" + + "ZW50cxgBIAMoCzIZLmFnZW50LnYxLkV4YUZldGNoQ29udGVudCIeCg1FeGFGZXRjaEVycm9yEg0K" + + "BWVycm9yGAEgASgJIiIKEEV4YUZldGNoUmVqZWN0ZWQSDgoGcmVhc29uGAEgASgJIlMKD0V4YUZl" + + "dGNoQ29udGVudBINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkSDAoEdGV4dBgDIAEoCRIWCg5w" + + "dWJsaXNoZWRfZGF0ZRgEIAEoCSJiChBFeGFGZXRjaFRvb2xDYWxsEiQKBGFyZ3MYASABKAsyFi5h" + + "Z2VudC52MS5FeGFGZXRjaEFyZ3MSKAoGcmVzdWx0GAIgASgLMhguYWdlbnQudjEuRXhhRmV0Y2hS" + + "ZXN1bHQiPAoURXhhRmV0Y2hSZXF1ZXN0UXVlcnkSJAoEYXJncxgBIAEoCzIWLmFnZW50LnYxLkV4" + + "YUZldGNoQXJncyKjAQoXRXhhRmV0Y2hSZXF1ZXN0UmVzcG9uc2USPgoIYXBwcm92ZWQYASABKAsy" + + "Ki5hZ2VudC52MS5FeGFGZXRjaFJlcXVlc3RSZXNwb25zZV9BcHByb3ZlZEgAEj4KCHJlamVjdGVk" + + "GAIgASgLMiouYWdlbnQudjEuRXhhRmV0Y2hSZXF1ZXN0UmVzcG9uc2VfUmVqZWN0ZWRIAEIICgZy" + + "ZXN1bHQiIgogRXhhRmV0Y2hSZXF1ZXN0UmVzcG9uc2VfQXBwcm92ZWQiMgogRXhhRmV0Y2hSZXF1" + + "ZXN0UmVzcG9uc2VfUmVqZWN0ZWQSDgoGcmVhc29uGAEgASgJIlcKDUV4YVNlYXJjaEFyZ3MSDQoF" + + "cXVlcnkYASABKAkSDAoEdHlwZRgCIAEoCRITCgtudW1fcmVzdWx0cxgDIAEoBRIUCgx0b29sX2Nh" + + "bGxfaWQYBCABKAkipgEKD0V4YVNlYXJjaFJlc3VsdBItCgdzdWNjZXNzGAEgASgLMhouYWdlbnQu" + + "djEuRXhhU2VhcmNoU3VjY2Vzc0gAEikKBWVycm9yGAIgASgLMhguYWdlbnQudjEuRXhhU2VhcmNo" + + "RXJyb3JIABIvCghyZWplY3RlZBgDIAEoCzIbLmFnZW50LnYxLkV4YVNlYXJjaFJlamVjdGVkSABC" + + "CAoGcmVzdWx0IkQKEEV4YVNlYXJjaFN1Y2Nlc3MSMAoKcmVmZXJlbmNlcxgBIAMoCzIcLmFnZW50" + + "LnYxLkV4YVNlYXJjaFJlZmVyZW5jZSIfCg5FeGFTZWFyY2hFcnJvchINCgVlcnJvchgBIAEoCSIj" + + "ChFFeGFTZWFyY2hSZWplY3RlZBIOCgZyZWFzb24YASABKAkiVgoSRXhhU2VhcmNoUmVmZXJlbmNl" + + "Eg0KBXRpdGxlGAEgASgJEgsKA3VybBgCIAEoCRIMCgR0ZXh0GAMgASgJEhYKDnB1Ymxpc2hlZF9k" + + "YXRlGAQgASgJImUKEUV4YVNlYXJjaFRvb2xDYWxsEiUKBGFyZ3MYASABKAsyFy5hZ2VudC52MS5F" + + "eGFTZWFyY2hBcmdzEikKBnJlc3VsdBgCIAEoCzIZLmFnZW50LnYxLkV4YVNlYXJjaFJlc3VsdCI+" + + "ChVFeGFTZWFyY2hSZXF1ZXN0UXVlcnkSJQoEYXJncxgBIAEoCzIXLmFnZW50LnYxLkV4YVNlYXJj" + + "aEFyZ3MipgEKGEV4YVNlYXJjaFJlcXVlc3RSZXNwb25zZRI/CghhcHByb3ZlZBgBIAEoCzIrLmFn" + + "ZW50LnYxLkV4YVNlYXJjaFJlcXVlc3RSZXNwb25zZV9BcHByb3ZlZEgAEj8KCHJlamVjdGVkGAIg" + + "ASgLMisuYWdlbnQudjEuRXhhU2VhcmNoUmVxdWVzdFJlc3BvbnNlX1JlamVjdGVkSABCCAoGcmVz" + + "dWx0IiMKIUV4YVNlYXJjaFJlcXVlc3RSZXNwb25zZV9BcHByb3ZlZCIzCiFFeGFTZWFyY2hSZXF1" + + "ZXN0UmVzcG9uc2VfUmVqZWN0ZWQSDgoGcmVhc29uGAEgASgJIiMKFUV4ZWNDbGllbnRTdHJlYW1D" + + "bG9zZRIKCgJpZBgBIAEoDSJWCg9FeGVjQ2xpZW50VGhyb3cSCgoCaWQYASABKA0SDQoFZXJyb3IY" + + "AiABKAkSGAoLc3RhY2tfdHJhY2UYAyABKAlIAIgBAUIOCgxfc3RhY2tfdHJhY2UiIQoTRXhlY0Ns" + + "aWVudEhlYXJ0YmVhdBIKCgJpZBgBIAEoDSK+AQoYRXhlY0NsaWVudENvbnRyb2xNZXNzYWdlEjcK" + + "DHN0cmVhbV9jbG9zZRgBIAEoCzIfLmFnZW50LnYxLkV4ZWNDbGllbnRTdHJlYW1DbG9zZUgAEioK" + + "BXRocm93GAIgASgLMhkuYWdlbnQudjEuRXhlY0NsaWVudFRocm93SAASMgoJaGVhcnRiZWF0GAMg" + + "ASgLMh0uYWdlbnQudjEuRXhlY0NsaWVudEhlYXJ0YmVhdEgAQgkKB21lc3NhZ2UihAEKC1NwYW5D" + + "b250ZXh0EhAKCHRyYWNlX2lkGAEgASgJEg8KB3NwYW5faWQYAiABKAkSGAoLdHJhY2VfZmxhZ3MY" + + "AyABKA1IAIgBARIYCgt0cmFjZV9zdGF0ZRgEIAEoCUgBiAEBQg4KDF90cmFjZV9mbGFnc0IOCgxf" + + "dHJhY2Vfc3RhdGUiCwoJQWJvcnRBcmdzIg0KC0Fib3J0UmVzdWx0IoUIChFFeGVjU2VydmVyTWVz" + + "c2FnZRIKCgJpZBgBIAEoDRIPCgdleGVjX2lkGA8gASgJEjAKDHNwYW5fY29udGV4dBgTIAEoCzIV" + + "LmFnZW50LnYxLlNwYW5Db250ZXh0SAGIAQESKQoKc2hlbGxfYXJncxgCIAEoCzITLmFnZW50LnYx" + + "LlNoZWxsQXJnc0gAEikKCndyaXRlX2FyZ3MYAyABKAsyEy5hZ2VudC52MS5Xcml0ZUFyZ3NIABIr" + + "CgtkZWxldGVfYXJncxgEIAEoCzIULmFnZW50LnYxLkRlbGV0ZUFyZ3NIABInCglncmVwX2FyZ3MY" + + "BSABKAsyEi5hZ2VudC52MS5HcmVwQXJnc0gAEicKCXJlYWRfYXJncxgHIAEoCzISLmFnZW50LnYx" + + "LlJlYWRBcmdzSAASIwoHbHNfYXJncxgIIAEoCzIQLmFnZW50LnYxLkxzQXJnc0gAEjUKEGRpYWdu" + + "b3N0aWNzX2FyZ3MYCSABKAsyGS5hZ2VudC52MS5EaWFnbm9zdGljc0FyZ3NIABI8ChRyZXF1ZXN0" + + "X2NvbnRleHRfYXJncxgKIAEoCzIcLmFnZW50LnYxLlJlcXVlc3RDb250ZXh0QXJnc0gAEiUKCG1j" + + "cF9hcmdzGAsgASgLMhEuYWdlbnQudjEuTWNwQXJnc0gAEjAKEXNoZWxsX3N0cmVhbV9hcmdzGA4g" + + "ASgLMhMuYWdlbnQudjEuU2hlbGxBcmdzSAASSQobYmFja2dyb3VuZF9zaGVsbF9zcGF3bl9hcmdz" + + "GBAgASgLMiIuYWdlbnQudjEuQmFja2dyb3VuZFNoZWxsU3Bhd25BcmdzSAASSgocbGlzdF9tY3Bf" + + "cmVzb3VyY2VzX2V4ZWNfYXJncxgRIAEoCzIiLmFnZW50LnYxLkxpc3RNY3BSZXNvdXJjZXNFeGVj" + + "QXJnc0gAEkgKG3JlYWRfbWNwX3Jlc291cmNlX2V4ZWNfYXJncxgSIAEoCzIhLmFnZW50LnYxLlJl" + + "YWRNY3BSZXNvdXJjZUV4ZWNBcmdzSAASKQoKZmV0Y2hfYXJncxgUIAEoCzITLmFnZW50LnYxLkZl" + + "dGNoQXJnc0gAEjgKEnJlY29yZF9zY3JlZW5fYXJncxgVIAEoCzIaLmFnZW50LnYxLlJlY29yZFNj" + + "cmVlbkFyZ3NIABI2ChFjb21wdXRlcl91c2VfYXJncxgWIAEoCzIZLmFnZW50LnYxLkNvbXB1dGVy" + + "VXNlQXJnc0gAEj8KFndyaXRlX3NoZWxsX3N0ZGluX2FyZ3MYFyABKAsyHS5hZ2VudC52MS5Xcml0" + + "ZVNoZWxsU3RkaW5BcmdzSABCCQoHbWVzc2FnZUIPCg1fc3Bhbl9jb250ZXh0Iv8HChFFeGVjQ2xp" + + "ZW50TWVzc2FnZRIKCgJpZBgBIAEoDRIPCgdleGVjX2lkGA8gASgJEi0KDHNoZWxsX3Jlc3VsdBgC" + + "IAEoCzIVLmFnZW50LnYxLlNoZWxsUmVzdWx0SAASLQoMd3JpdGVfcmVzdWx0GAMgASgLMhUuYWdl" + + "bnQudjEuV3JpdGVSZXN1bHRIABIvCg1kZWxldGVfcmVzdWx0GAQgASgLMhYuYWdlbnQudjEuRGVs" + + "ZXRlUmVzdWx0SAASKwoLZ3JlcF9yZXN1bHQYBSABKAsyFC5hZ2VudC52MS5HcmVwUmVzdWx0SAAS" + + "KwoLcmVhZF9yZXN1bHQYByABKAsyFC5hZ2VudC52MS5SZWFkUmVzdWx0SAASJwoJbHNfcmVzdWx0" + + "GAggASgLMhIuYWdlbnQudjEuTHNSZXN1bHRIABI5ChJkaWFnbm9zdGljc19yZXN1bHQYCSABKAsy" + + "Gy5hZ2VudC52MS5EaWFnbm9zdGljc1Jlc3VsdEgAEkAKFnJlcXVlc3RfY29udGV4dF9yZXN1bHQY" + + "CiABKAsyHi5hZ2VudC52MS5SZXF1ZXN0Q29udGV4dFJlc3VsdEgAEikKCm1jcF9yZXN1bHQYCyAB" + + "KAsyEy5hZ2VudC52MS5NY3BSZXN1bHRIABItCgxzaGVsbF9zdHJlYW0YDiABKAsyFS5hZ2VudC52" + + "MS5TaGVsbFN0cmVhbUgAEk0KHWJhY2tncm91bmRfc2hlbGxfc3Bhd25fcmVzdWx0GBAgASgLMiQu" + + "YWdlbnQudjEuQmFja2dyb3VuZFNoZWxsU3Bhd25SZXN1bHRIABJOCh5saXN0X21jcF9yZXNvdXJj" + + "ZXNfZXhlY19yZXN1bHQYESABKAsyJC5hZ2VudC52MS5MaXN0TWNwUmVzb3VyY2VzRXhlY1Jlc3Vs" + + "dEgAEkwKHXJlYWRfbWNwX3Jlc291cmNlX2V4ZWNfcmVzdWx0GBIgASgLMiMuYWdlbnQudjEuUmVh" + + "ZE1jcFJlc291cmNlRXhlY1Jlc3VsdEgAEi0KDGZldGNoX3Jlc3VsdBgUIAEoCzIVLmFnZW50LnYx" + + "LkZldGNoUmVzdWx0SAASPAoUcmVjb3JkX3NjcmVlbl9yZXN1bHQYFSABKAsyHC5hZ2VudC52MS5S" + + "ZWNvcmRTY3JlZW5SZXN1bHRIABI6ChNjb21wdXRlcl91c2VfcmVzdWx0GBYgASgLMhsuYWdlbnQu" + + "djEuQ29tcHV0ZXJVc2VSZXN1bHRIABJDChh3cml0ZV9zaGVsbF9zdGRpbl9yZXN1bHQYFyABKAsy" + + "Hy5hZ2VudC52MS5Xcml0ZVNoZWxsU3RkaW5SZXN1bHRIAEIJCgdtZXNzYWdlIi4KCUZldGNoQXJn" + + "cxILCgN1cmwYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJImkKC0ZldGNoUmVzdWx0EikKB3N1" + + "Y2Nlc3MYASABKAsyFi5hZ2VudC52MS5GZXRjaFN1Y2Nlc3NIABIlCgVlcnJvchgCIAEoCzIULmFn" + + "ZW50LnYxLkZldGNoRXJyb3JIAEIICgZyZXN1bHQiVwoMRmV0Y2hTdWNjZXNzEgsKA3VybBgBIAEo" + + "CRIPCgdjb250ZW50GAIgASgJEhMKC3N0YXR1c19jb2RlGAMgASgFEhQKDGNvbnRlbnRfdHlwZRgE" + + "IAEoCSIoCgpGZXRjaEVycm9yEgsKA3VybBgBIAEoCRINCgVlcnJvchgCIAEoCSJtChFHZW5lcmF0" + + "ZUltYWdlQXJncxITCgtkZXNjcmlwdGlvbhgBIAEoCRIWCglmaWxlX3BhdGgYAiABKAlIAIgBARId" + + "ChVyZWZlcmVuY2VfaW1hZ2VfcGF0aHMYBSADKAlCDAoKX2ZpbGVfcGF0aCKBAQoTR2VuZXJhdGVJ" + + "bWFnZVJlc3VsdBIxCgdzdWNjZXNzGAEgASgLMh4uYWdlbnQudjEuR2VuZXJhdGVJbWFnZVN1Y2Nl" + + "c3NIABItCgVlcnJvchgCIAEoCzIcLmFnZW50LnYxLkdlbmVyYXRlSW1hZ2VFcnJvckgAQggKBnJl" + + "c3VsdCI9ChRHZW5lcmF0ZUltYWdlU3VjY2VzcxIRCglmaWxlX3BhdGgYASABKAkSEgoKaW1hZ2Vf" + + "ZGF0YRgCIAEoCSIjChJHZW5lcmF0ZUltYWdlRXJyb3ISDQoFZXJyb3IYASABKAkicQoVR2VuZXJh" + + "dGVJbWFnZVRvb2xDYWxsEikKBGFyZ3MYASABKAsyGy5hZ2VudC52MS5HZW5lcmF0ZUltYWdlQXJn" + + "cxItCgZyZXN1bHQYAiABKAsyHS5hZ2VudC52MS5HZW5lcmF0ZUltYWdlUmVzdWx0IsYECghHcmVw" + + "QXJncxIPCgdwYXR0ZXJuGAEgASgJEhEKBHBhdGgYAiABKAlIAIgBARIRCgRnbG9iGAMgASgJSAGI" + + "AQESGAoLb3V0cHV0X21vZGUYBCABKAlIAogBARIbCg5jb250ZXh0X2JlZm9yZRgFIAEoBUgDiAEB" + + "EhoKDWNvbnRleHRfYWZ0ZXIYBiABKAVIBIgBARIUCgdjb250ZXh0GAcgASgFSAWIAQESHQoQY2Fz" + + "ZV9pbnNlbnNpdGl2ZRgIIAEoCEgGiAEBEhEKBHR5cGUYCSABKAlIB4gBARIXCgpoZWFkX2xpbWl0" + + "GAogASgFSAiIAQESFgoJbXVsdGlsaW5lGAsgASgISAmIAQESEQoEc29ydBgMIAEoCUgKiAEBEhsK" + + "DnNvcnRfYXNjZW5kaW5nGA0gASgISAuIAQESFAoMdG9vbF9jYWxsX2lkGA4gASgJEjQKDnNhbmRi" + + "b3hfcG9saWN5GA8gASgLMhcuYWdlbnQudjEuU2FuZGJveFBvbGljeUgMiAEBQgcKBV9wYXRoQgcK" + + "BV9nbG9iQg4KDF9vdXRwdXRfbW9kZUIRCg9fY29udGV4dF9iZWZvcmVCEAoOX2NvbnRleHRfYWZ0" + + "ZXJCCgoIX2NvbnRleHRCEwoRX2Nhc2VfaW5zZW5zaXRpdmVCBwoFX3R5cGVCDQoLX2hlYWRfbGlt" + + "aXRCDAoKX211bHRpbGluZUIHCgVfc29ydEIRCg9fc29ydF9hc2NlbmRpbmdCEQoPX3NhbmRib3hf" + + "cG9saWN5ImYKCkdyZXBSZXN1bHQSKAoHc3VjY2VzcxgBIAEoCzIVLmFnZW50LnYxLkdyZXBTdWNj" + + "ZXNzSAASJAoFZXJyb3IYAiABKAsyEy5hZ2VudC52MS5HcmVwRXJyb3JIAEIICgZyZXN1bHQiGgoJ" + + "R3JlcEVycm9yEg0KBWVycm9yGAEgASgJIrQCCgtHcmVwU3VjY2VzcxIPCgdwYXR0ZXJuGAEgASgJ" + + "EgwKBHBhdGgYAiABKAkSEwoLb3V0cHV0X21vZGUYAyABKAkSRgoRd29ya3NwYWNlX3Jlc3VsdHMY" + + "BCADKAsyKy5hZ2VudC52MS5HcmVwU3VjY2Vzcy5Xb3Jrc3BhY2VSZXN1bHRzRW50cnkSPAoUYWN0" + + "aXZlX2VkaXRvcl9yZXN1bHQYBSABKAsyGS5hZ2VudC52MS5HcmVwVW5pb25SZXN1bHRIAIgBARpS" + + "ChVXb3Jrc3BhY2VSZXN1bHRzRW50cnkSCwoDa2V5GAEgASgJEigKBXZhbHVlGAIgASgLMhkuYWdl" + + "bnQudjEuR3JlcFVuaW9uUmVzdWx0OgI4AUIXChVfYWN0aXZlX2VkaXRvcl9yZXN1bHQiowEKD0dy" + + "ZXBVbmlvblJlc3VsdBIqCgVjb3VudBgBIAEoCzIZLmFnZW50LnYxLkdyZXBDb3VudFJlc3VsdEgA" + + "EioKBWZpbGVzGAIgASgLMhkuYWdlbnQudjEuR3JlcEZpbGVzUmVzdWx0SAASLgoHY29udGVudBgD" + + "IAEoCzIbLmFnZW50LnYxLkdyZXBDb250ZW50UmVzdWx0SABCCAoGcmVzdWx0IpsBCg9HcmVwQ291" + + "bnRSZXN1bHQSJwoGY291bnRzGAEgAygLMhcuYWdlbnQudjEuR3JlcEZpbGVDb3VudBITCgt0b3Rh" + + "bF9maWxlcxgCIAEoBRIVCg10b3RhbF9tYXRjaGVzGAMgASgFEhgKEGNsaWVudF90cnVuY2F0ZWQY" + + "BCABKAgSGQoRcmlwZ3JlcF90cnVuY2F0ZWQYBSABKAgiLAoNR3JlcEZpbGVDb3VudBIMCgRmaWxl" + + "GAEgASgJEg0KBWNvdW50GAIgASgFImoKD0dyZXBGaWxlc1Jlc3VsdBINCgVmaWxlcxgBIAMoCRIT" + + "Cgt0b3RhbF9maWxlcxgCIAEoBRIYChBjbGllbnRfdHJ1bmNhdGVkGAMgASgIEhkKEXJpcGdyZXBf" + + "dHJ1bmNhdGVkGAQgASgIIqQBChFHcmVwQ29udGVudFJlc3VsdBIoCgdtYXRjaGVzGAEgAygLMhcu" + + "YWdlbnQudjEuR3JlcEZpbGVNYXRjaBITCgt0b3RhbF9saW5lcxgCIAEoBRIbChN0b3RhbF9tYXRj" + + "aGVkX2xpbmVzGAMgASgFEhgKEGNsaWVudF90cnVuY2F0ZWQYBCABKAgSGQoRcmlwZ3JlcF90cnVu" + + "Y2F0ZWQYBSABKAgiSgoNR3JlcEZpbGVNYXRjaBIMCgRmaWxlGAEgASgJEisKB21hdGNoZXMYAiAD" + + "KAsyGi5hZ2VudC52MS5HcmVwQ29udGVudE1hdGNoImwKEEdyZXBDb250ZW50TWF0Y2gSEwoLbGlu" + + "ZV9udW1iZXIYASABKAUSDwoHY29udGVudBgCIAEoCRIZChFjb250ZW50X3RydW5jYXRlZBgDIAEo" + + "CBIXCg9pc19jb250ZXh0X2xpbmUYBCABKAgiHQoKR3JlcFN0cmVhbRIPCgdwYXR0ZXJuGAEgASgJ" + + "IlYKDEdyZXBUb29sQ2FsbBIgCgRhcmdzGAEgASgLMhIuYWdlbnQudjEuR3JlcEFyZ3MSJAoGcmVz" + + "dWx0GAIgASgLMhQuYWdlbnQudjEuR3JlcFJlc3VsdCIeCgtHZXRCbG9iQXJncxIPCgdibG9iX2lk" + + "GAEgASgMIjUKDUdldEJsb2JSZXN1bHQSFgoJYmxvYl9kYXRhGAEgASgMSACIAQFCDAoKX2Jsb2Jf" + + "ZGF0YSIxCgtTZXRCbG9iQXJncxIPCgdibG9iX2lkGAEgASgMEhEKCWJsb2JfZGF0YRgCIAEoDCI+" + + "Cg1TZXRCbG9iUmVzdWx0EiMKBWVycm9yGAEgASgLMg8uYWdlbnQudjEuRXJyb3JIAIgBAUIICgZf" + + "ZXJyb3IiywEKD0t2U2VydmVyTWVzc2FnZRIKCgJpZBgBIAEoDRIwCgxzcGFuX2NvbnRleHQYBCAB" + + "KAsyFS5hZ2VudC52MS5TcGFuQ29udGV4dEgBiAEBEi4KDWdldF9ibG9iX2FyZ3MYAiABKAsyFS5h" + + "Z2VudC52MS5HZXRCbG9iQXJnc0gAEi4KDXNldF9ibG9iX2FyZ3MYAyABKAsyFS5hZ2VudC52MS5T" + + "ZXRCbG9iQXJnc0gAQgkKB21lc3NhZ2VCDwoNX3NwYW5fY29udGV4dCKQAQoPS3ZDbGllbnRNZXNz" + + "YWdlEgoKAmlkGAEgASgNEjIKD2dldF9ibG9iX3Jlc3VsdBgCIAEoCzIXLmFnZW50LnYxLkdldEJs" + + "b2JSZXN1bHRIABIyCg9zZXRfYmxvYl9yZXN1bHQYAyABKAsyFy5hZ2VudC52MS5TZXRCbG9iUmVz" + + "dWx0SABCCQoHbWVzc2FnZSKtAQoGTHNBcmdzEgwKBHBhdGgYASABKAkSDgoGaWdub3JlGAIgAygJ" + + "EhQKDHRvb2xfY2FsbF9pZBgDIAEoCRI0Cg5zYW5kYm94X3BvbGljeRgEIAEoCzIXLmFnZW50LnYx" + + "LlNhbmRib3hQb2xpY3lIAIgBARIXCgp0aW1lb3V0X21zGAUgASgNSAGIAQFCEQoPX3NhbmRib3hf" + + "cG9saWN5Qg0KC190aW1lb3V0X21zIrIBCghMc1Jlc3VsdBImCgdzdWNjZXNzGAEgASgLMhMuYWdl" + + "bnQudjEuTHNTdWNjZXNzSAASIgoFZXJyb3IYAiABKAsyES5hZ2VudC52MS5Mc0Vycm9ySAASKAoI" + + "cmVqZWN0ZWQYAyABKAsyFC5hZ2VudC52MS5Mc1JlamVjdGVkSAASJgoHdGltZW91dBgEIAEoCzIT" + + "LmFnZW50LnYxLkxzVGltZW91dEgAQggKBnJlc3VsdCJHCglMc1N1Y2Nlc3MSOgoTZGlyZWN0b3J5" + + "X3RyZWVfcm9vdBgBIAEoCzIdLmFnZW50LnYxLkxzRGlyZWN0b3J5VHJlZU5vZGUi9gIKE0xzRGly" + + "ZWN0b3J5VHJlZU5vZGUSEAoIYWJzX3BhdGgYASABKAkSNAoNY2hpbGRyZW5fZGlycxgCIAMoCzId" + + "LmFnZW50LnYxLkxzRGlyZWN0b3J5VHJlZU5vZGUSOgoOY2hpbGRyZW5fZmlsZXMYAyADKAsyIi5h" + + "Z2VudC52MS5Mc0RpcmVjdG9yeVRyZWVOb2RlX0ZpbGUSHwoXY2hpbGRyZW5fd2VyZV9wcm9jZXNz" + + "ZWQYBCABKAgSZAodZnVsbF9zdWJ0cmVlX2V4dGVuc2lvbl9jb3VudHMYBSADKAsyPS5hZ2VudC52" + + "MS5Mc0RpcmVjdG9yeVRyZWVOb2RlLkZ1bGxTdWJ0cmVlRXh0ZW5zaW9uQ291bnRzRW50cnkSEQoJ" + + "bnVtX2ZpbGVzGAYgASgFGkEKH0Z1bGxTdWJ0cmVlRXh0ZW5zaW9uQ291bnRzRW50cnkSCwoDa2V5" + + "GAEgASgJEg0KBXZhbHVlGAIgASgFOgI4ASJ6ChhMc0RpcmVjdG9yeVRyZWVOb2RlX0ZpbGUSDAoE" + + "bmFtZRgBIAEoCRI6ChF0ZXJtaW5hbF9tZXRhZGF0YRgCIAEoCzIaLmFnZW50LnYxLlRlcm1pbmFs" + + "TWV0YWRhdGFIAIgBAUIUChJfdGVybWluYWxfbWV0YWRhdGEiJgoHTHNFcnJvchIMCgRwYXRoGAEg" + + "ASgJEg0KBWVycm9yGAIgASgJIioKCkxzUmVqZWN0ZWQSDAoEcGF0aBgBIAEoCRIOCgZyZWFzb24Y" + + "AiABKAkiRwoJTHNUaW1lb3V0EjoKE2RpcmVjdG9yeV90cmVlX3Jvb3QYASABKAsyHS5hZ2VudC52" + + "MS5Mc0RpcmVjdG9yeVRyZWVOb2RlIvEBChBUZXJtaW5hbE1ldGFkYXRhEhAKA2N3ZBgBIAEoCUgA" + + "iAEBEjkKDWxhc3RfY29tbWFuZHMYAiADKAsyIi5hZ2VudC52MS5UZXJtaW5hbE1ldGFkYXRhX0Nv" + + "bW1hbmQSHQoQbGFzdF9tb2RpZmllZF9tcxgDIAEoA0gBiAEBEkAKD2N1cnJlbnRfY29tbWFuZBgE" + + "IAEoCzIiLmFnZW50LnYxLlRlcm1pbmFsTWV0YWRhdGFfQ29tbWFuZEgCiAEBQgYKBF9jd2RCEwoR" + + "X2xhc3RfbW9kaWZpZWRfbXNCEgoQX2N1cnJlbnRfY29tbWFuZCKnAQoYVGVybWluYWxNZXRhZGF0" + + "YV9Db21tYW5kEg8KB2NvbW1hbmQYASABKAkSFgoJZXhpdF9jb2RlGAIgASgFSACIAQESGQoMdGlt" + + "ZXN0YW1wX21zGAMgASgDSAGIAQESGAoLZHVyYXRpb25fbXMYBCABKANIAogBAUIMCgpfZXhpdF9j" + + "b2RlQg8KDV90aW1lc3RhbXBfbXNCDgoMX2R1cmF0aW9uX21zIlAKCkxzVG9vbENhbGwSHgoEYXJn" + + "cxgBIAEoCzIQLmFnZW50LnYxLkxzQXJncxIiCgZyZXN1bHQYAiABKAsyEi5hZ2VudC52MS5Mc1Jl" + + "c3VsdCK1AQoHTWNwQXJncxIMCgRuYW1lGAEgASgJEikKBGFyZ3MYAiADKAsyGy5hZ2VudC52MS5N" + + "Y3BBcmdzLkFyZ3NFbnRyeRIUCgx0b29sX2NhbGxfaWQYAyABKAkSGwoTcHJvdmlkZXJfaWRlbnRp" + + "ZmllchgEIAEoCRIRCgl0b29sX25hbWUYBSABKAkaKwoJQXJnc0VudHJ5EgsKA2tleRgBIAEoCRIN" + + "CgV2YWx1ZRgCIAEoDDoCOAEi/wEKCU1jcFJlc3VsdBInCgdzdWNjZXNzGAEgASgLMhQuYWdlbnQu" + + "djEuTWNwU3VjY2Vzc0gAEiMKBWVycm9yGAIgASgLMhIuYWdlbnQudjEuTWNwRXJyb3JIABIpCghy" + + "ZWplY3RlZBgDIAEoCzIVLmFnZW50LnYxLk1jcFJlamVjdGVkSAASOgoRcGVybWlzc2lvbl9kZW5p" + + "ZWQYBCABKAsyHS5hZ2VudC52MS5NY3BQZXJtaXNzaW9uRGVuaWVkSAASMwoOdG9vbF9ub3RfZm91" + + "bmQYBSABKAsyGS5hZ2VudC52MS5NY3BUb29sTm90Rm91bmRIAEIICgZyZXN1bHQiOAoPTWNwVG9v" + + "bE5vdEZvdW5kEgwKBG5hbWUYASABKAkSFwoPYXZhaWxhYmxlX3Rvb2xzGAIgAygJImoKDk1jcFRl" + + "eHRDb250ZW50EgwKBHRleHQYASABKAkSNgoPb3V0cHV0X2xvY2F0aW9uGAIgASgLMhguYWdlbnQu" + + "djEuT3V0cHV0TG9jYXRpb25IAIgBAUISChBfb3V0cHV0X2xvY2F0aW9uIjIKD01jcEltYWdlQ29u" + + "dGVudBIMCgRkYXRhGAEgASgMEhEKCW1pbWVfdHlwZRgCIAEoCSJ7ChhNY3BUb29sUmVzdWx0Q29u" + + "dGVudEl0ZW0SKAoEdGV4dBgBIAEoCzIYLmFnZW50LnYxLk1jcFRleHRDb250ZW50SAASKgoFaW1h" + + "Z2UYAiABKAsyGS5hZ2VudC52MS5NY3BJbWFnZUNvbnRlbnRIAEIJCgdjb250ZW50IlMKCk1jcFN1" + + "Y2Nlc3MSMwoHY29udGVudBgBIAMoCzIiLmFnZW50LnYxLk1jcFRvb2xSZXN1bHRDb250ZW50SXRl" + + "bRIQCghpc19lcnJvchgCIAEoCCIZCghNY3BFcnJvchINCgVlcnJvchgBIAEoCSIyCgtNY3BSZWpl" + + "Y3RlZBIOCgZyZWFzb24YASABKAkSEwoLaXNfcmVhZG9ubHkYAiABKAgiOQoTTWNwUGVybWlzc2lv" + + "bkRlbmllZBINCgVlcnJvchgBIAEoCRITCgtpc19yZWFkb25seRgCIAEoCCI6ChhMaXN0TWNwUmVz" + + "b3VyY2VzRXhlY0FyZ3MSEwoGc2VydmVyGAEgASgJSACIAQFCCQoHX3NlcnZlciLGAQoaTGlzdE1j" + + "cFJlc291cmNlc0V4ZWNSZXN1bHQSNAoHc3VjY2VzcxgBIAEoCzIhLmFnZW50LnYxLkxpc3RNY3BS" + + "ZXNvdXJjZXNTdWNjZXNzSAASMAoFZXJyb3IYAiABKAsyHy5hZ2VudC52MS5MaXN0TWNwUmVzb3Vy" + + "Y2VzRXJyb3JIABI2CghyZWplY3RlZBgDIAEoCzIiLmFnZW50LnYxLkxpc3RNY3BSZXNvdXJjZXNS" + + "ZWplY3RlZEgAQggKBnJlc3VsdCK9AgomTGlzdE1jcFJlc291cmNlc0V4ZWNSZXN1bHRfTWNwUmVz" + + "b3VyY2USCwoDdXJpGAEgASgJEhEKBG5hbWUYAiABKAlIAIgBARIYCgtkZXNjcmlwdGlvbhgDIAEo" + + "CUgBiAEBEhYKCW1pbWVfdHlwZRgEIAEoCUgCiAEBEg4KBnNlcnZlchgFIAEoCRJWCgthbm5vdGF0" + + "aW9ucxgGIAMoCzJBLmFnZW50LnYxLkxpc3RNY3BSZXNvdXJjZXNFeGVjUmVzdWx0X01jcFJlc291" + + "cmNlLkFubm90YXRpb25zRW50cnkaMgoQQW5ub3RhdGlvbnNFbnRyeRILCgNrZXkYASABKAkSDQoF" + + "dmFsdWUYAiABKAk6AjgBQgcKBV9uYW1lQg4KDF9kZXNjcmlwdGlvbkIMCgpfbWltZV90eXBlIl4K" + + "F0xpc3RNY3BSZXNvdXJjZXNTdWNjZXNzEkMKCXJlc291cmNlcxgBIAMoCzIwLmFnZW50LnYxLkxp" + + "c3RNY3BSZXNvdXJjZXNFeGVjUmVzdWx0X01jcFJlc291cmNlIiYKFUxpc3RNY3BSZXNvdXJjZXNF" + + "cnJvchINCgVlcnJvchgBIAEoCSIqChhMaXN0TWNwUmVzb3VyY2VzUmVqZWN0ZWQSDgoGcmVhc29u" + + "GAEgASgJImQKF1JlYWRNY3BSZXNvdXJjZUV4ZWNBcmdzEg4KBnNlcnZlchgBIAEoCRILCgN1cmkY" + + "AiABKAkSGgoNZG93bmxvYWRfcGF0aBgDIAEoCUgAiAEBQhAKDl9kb3dubG9hZF9wYXRoIvoBChlS" + + "ZWFkTWNwUmVzb3VyY2VFeGVjUmVzdWx0EjMKB3N1Y2Nlc3MYASABKAsyIC5hZ2VudC52MS5SZWFk" + + "TWNwUmVzb3VyY2VTdWNjZXNzSAASLwoFZXJyb3IYAiABKAsyHi5hZ2VudC52MS5SZWFkTWNwUmVz" + + "b3VyY2VFcnJvckgAEjUKCHJlamVjdGVkGAMgASgLMiEuYWdlbnQudjEuUmVhZE1jcFJlc291cmNl" + + "UmVqZWN0ZWRIABI2Cglub3RfZm91bmQYBCABKAsyIS5hZ2VudC52MS5SZWFkTWNwUmVzb3VyY2VO" + + "b3RGb3VuZEgAQggKBnJlc3VsdCLmAgoWUmVhZE1jcFJlc291cmNlU3VjY2VzcxILCgN1cmkYASAB" + + "KAkSEQoEbmFtZRgCIAEoCUgBiAEBEhgKC2Rlc2NyaXB0aW9uGAMgASgJSAKIAQESFgoJbWltZV90" + + "eXBlGAQgASgJSAOIAQESRgoLYW5ub3RhdGlvbnMYByADKAsyMS5hZ2VudC52MS5SZWFkTWNwUmVz" + + "b3VyY2VTdWNjZXNzLkFubm90YXRpb25zRW50cnkSGgoNZG93bmxvYWRfcGF0aBgIIAEoCUgEiAEB" + + "Eg4KBHRleHQYBSABKAlIABIOCgRibG9iGAYgASgMSAAaMgoQQW5ub3RhdGlvbnNFbnRyeRILCgNr" + + "ZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgkKB2NvbnRlbnRCBwoFX25hbWVCDgoMX2Rlc2Ny" + + "aXB0aW9uQgwKCl9taW1lX3R5cGVCEAoOX2Rvd25sb2FkX3BhdGgiMgoUUmVhZE1jcFJlc291cmNl" + + "RXJyb3ISCwoDdXJpGAEgASgJEg0KBWVycm9yGAIgASgJIjYKF1JlYWRNY3BSZXNvdXJjZVJlamVj" + + "dGVkEgsKA3VyaRgBIAEoCRIOCgZyZWFzb24YAiABKAkiJgoXUmVhZE1jcFJlc291cmNlTm90Rm91" + + "bmQSCwoDdXJpGAEgASgJInwKEU1jcFRvb2xEZWZpbml0aW9uEgwKBG5hbWUYASABKAkSGwoTcHJv" + + "dmlkZXJfaWRlbnRpZmllchgEIAEoCRIRCgl0b29sX25hbWUYBSABKAkSEwoLZGVzY3JpcHRpb24Y" + + "AiABKAkSFAoMaW5wdXRfc2NoZW1hGAMgASgMIjoKCE1jcFRvb2xzEi4KCW1jcF90b29scxgBIAMo" + + "CzIbLmFnZW50LnYxLk1jcFRvb2xEZWZpbml0aW9uIjwKD01jcEluc3RydWN0aW9ucxITCgtzZXJ2" + + "ZXJfbmFtZRgBIAEoCRIUCgxpbnN0cnVjdGlvbnMYAiABKAki1wEKDU1jcERlc2NyaXB0b3ISEwoL" + + "c2VydmVyX25hbWUYASABKAkSGQoRc2VydmVyX2lkZW50aWZpZXIYAiABKAkSGAoLZm9sZGVyX3Bh" + + "dGgYAyABKAlIAIgBARIkChdzZXJ2ZXJfdXNlX2luc3RydWN0aW9ucxgEIAEoCUgBiAEBEioKBXRv" + + "b2xzGAUgAygLMhsuYWdlbnQudjEuTWNwVG9vbERlc2NyaXB0b3JCDgoMX2ZvbGRlcl9wYXRoQhoK" + + "GF9zZXJ2ZXJfdXNlX2luc3RydWN0aW9ucyJYChFNY3BUb29sRGVzY3JpcHRvchIRCgl0b29sX25h" + + "bWUYASABKAkSHAoPZGVmaW5pdGlvbl9wYXRoGAIgASgJSACIAQFCEgoQX2RlZmluaXRpb25fcGF0" + + "aCJ4ChRNY3BGaWxlU3lzdGVtT3B0aW9ucxIPCgdlbmFibGVkGAEgASgIEh0KFXdvcmtzcGFjZV9w" + + "cm9qZWN0X2RpchgCIAEoCRIwCg9tY3BfZGVzY3JpcHRvcnMYAyADKAsyFy5hZ2VudC52MS5NY3BE" + + "ZXNjcmlwdG9yIi4KCFJlYWRBcmdzEgwKBHBhdGgYASABKAkSFAoMdG9vbF9jYWxsX2lkGAIgASgJ" + + "IrgCCgpSZWFkUmVzdWx0EigKB3N1Y2Nlc3MYASABKAsyFS5hZ2VudC52MS5SZWFkU3VjY2Vzc0gA" + + "EiQKBWVycm9yGAIgASgLMhMuYWdlbnQudjEuUmVhZEVycm9ySAASKgoIcmVqZWN0ZWQYAyABKAsy" + + "Fi5hZ2VudC52MS5SZWFkUmVqZWN0ZWRIABI0Cg5maWxlX25vdF9mb3VuZBgEIAEoCzIaLmFnZW50" + + "LnYxLlJlYWRGaWxlTm90Rm91bmRIABI7ChFwZXJtaXNzaW9uX2RlbmllZBgFIAEoCzIeLmFnZW50" + + "LnYxLlJlYWRQZXJtaXNzaW9uRGVuaWVkSAASMQoMaW52YWxpZF9maWxlGAYgASgLMhkuYWdlbnQu" + + "djEuUmVhZEludmFsaWRGaWxlSABCCAoGcmVzdWx0IrMBCgtSZWFkU3VjY2VzcxIMCgRwYXRoGAEg" + + "ASgJEhMKC3RvdGFsX2xpbmVzGAMgASgFEhEKCWZpbGVfc2l6ZRgEIAEoAxIRCgl0cnVuY2F0ZWQY" + + "BiABKAgSGwoOb3V0cHV0X2Jsb2JfaWQYByABKAxIAYgBARIRCgdjb250ZW50GAIgASgJSAASDgoE" + + "ZGF0YRgFIAEoDEgAQggKBm91dHB1dEIRCg9fb3V0cHV0X2Jsb2JfaWQiKAoJUmVhZEVycm9yEgwK" + + "BHBhdGgYASABKAkSDQoFZXJyb3IYAiABKAkiLAoMUmVhZFJlamVjdGVkEgwKBHBhdGgYASABKAkS" + + "DgoGcmVhc29uGAIgASgJIiAKEFJlYWRGaWxlTm90Rm91bmQSDAoEcGF0aBgBIAEoCSIkChRSZWFk" + + "UGVybWlzc2lvbkRlbmllZBIMCgRwYXRoGAEgASgJIi8KD1JlYWRJbnZhbGlkRmlsZRIMCgRwYXRo" + + "GAEgASgJEg4KBnJlYXNvbhgCIAEoCSJeCgxSZWFkVG9vbENhbGwSJAoEYXJncxgBIAEoCzIWLmFn" + + "ZW50LnYxLlJlYWRUb29sQXJncxIoCgZyZXN1bHQYAiABKAsyGC5hZ2VudC52MS5SZWFkVG9vbFJl" + + "c3VsdCJaCgxSZWFkVG9vbEFyZ3MSDAoEcGF0aBgBIAEoCRITCgZvZmZzZXQYAiABKAVIAIgBARIS" + + "CgVsaW1pdBgDIAEoBUgBiAEBQgkKB19vZmZzZXRCCAoGX2xpbWl0InIKDlJlYWRUb29sUmVzdWx0" + + "EiwKB3N1Y2Nlc3MYASABKAsyGS5hZ2VudC52MS5SZWFkVG9vbFN1Y2Nlc3NIABIoCgVlcnJvchgC" + + "IAEoCzIXLmFnZW50LnYxLlJlYWRUb29sRXJyb3JIAEIICgZyZXN1bHQiMQoJUmVhZFJhbmdlEhIK" + + "CnN0YXJ0X2xpbmUYASABKA0SEAoIZW5kX2xpbmUYAiABKA0ijgIKD1JlYWRUb29sU3VjY2VzcxIQ" + + "Cghpc19lbXB0eRgCIAEoCBIWCg5leGNlZWRlZF9saW1pdBgDIAEoCBITCgt0b3RhbF9saW5lcxgE" + + "IAEoDRIRCglmaWxlX3NpemUYBSABKA0SDAoEcGF0aBgHIAEoCRIsCgpyZWFkX3JhbmdlGAggASgL" + + "MhMuYWdlbnQudjEuUmVhZFJhbmdlSAGIAQESEQoHY29udGVudBgBIAEoCUgAEg4KBGRhdGEYBiAB" + + "KAxIABIWCgxkYXRhX2Jsb2JfaWQYCSABKAxIABIZCg9jb250ZW50X2Jsb2JfaWQYCiABKAxIAEII" + + "CgZvdXRwdXRCDQoLX3JlYWRfcmFuZ2UiJgoNUmVhZFRvb2xFcnJvchIVCg1lcnJvcl9tZXNzYWdl" + + "GAEgASgJImoKEFJlY29yZFNjcmVlbkFyZ3MSDAoEbW9kZRgBIAEoBRIUCgx0b29sX2NhbGxfaWQY" + + "AiABKAkSHQoQc2F2ZV9hc19maWxlbmFtZRgDIAEoCUgAiAEBQhMKEV9zYXZlX2FzX2ZpbGVuYW1l" + + "IokCChJSZWNvcmRTY3JlZW5SZXN1bHQSOwoNc3RhcnRfc3VjY2VzcxgBIAEoCzIiLmFnZW50LnYx" + + "LlJlY29yZFNjcmVlblN0YXJ0U3VjY2Vzc0gAEjkKDHNhdmVfc3VjY2VzcxgCIAEoCzIhLmFnZW50" + + "LnYxLlJlY29yZFNjcmVlblNhdmVTdWNjZXNzSAASPwoPZGlzY2FyZF9zdWNjZXNzGAMgASgLMiQu" + + "YWdlbnQudjEuUmVjb3JkU2NyZWVuRGlzY2FyZFN1Y2Nlc3NIABIwCgdmYWlsdXJlGAQgASgLMh0u" + + "YWdlbnQudjEuUmVjb3JkU2NyZWVuRmFpbHVyZUgAQggKBnJlc3VsdCJnChhSZWNvcmRTY3JlZW5T" + + "dGFydFN1Y2Nlc3MSJQodd2FzX3ByaW9yX3JlY29yZGluZ19jYW5jZWxsZWQYASABKAgSJAocd2Fz" + + "X3NhdmVfYXNfZmlsZW5hbWVfaWdub3JlZBgCIAEoCCKgAQoXUmVjb3JkU2NyZWVuU2F2ZVN1Y2Nl" + + "c3MSDAoEcGF0aBgBIAEoCRIdChVyZWNvcmRpbmdfZHVyYXRpb25fbXMYAiABKAMSMAojcmVxdWVz" + + "dGVkX2ZpbGVfcGF0aF9yZWplY3RlZF9yZWFzb24YAyABKAVIAIgBAUImCiRfcmVxdWVzdGVkX2Zp" + + "bGVfcGF0aF9yZWplY3RlZF9yZWFzb24iHAoaUmVjb3JkU2NyZWVuRGlzY2FyZFN1Y2Nlc3MiJAoT" + + "UmVjb3JkU2NyZWVuRmFpbHVyZRINCgVlcnJvchgBIAEoCSI2ChNDdXJzb3JQYWNrYWdlUHJvbXB0" + + "EgwKBG5hbWUYASABKAkSEQoJZmlsZV9wYXRoGAIgASgJIuIBCg1DdXJzb3JQYWNrYWdlEgwKBG5h" + + "bWUYASABKAkSEwoLZGVzY3JpcHRpb24YAiABKAkSEwoLZm9sZGVyX3BhdGgYAyABKAkSDwoHZW5h" + + "YmxlZBgEIAEoCBIYCgtwYXJzZV9lcnJvchgFIAEoCUgAiAEBEi4KB3Byb21wdHMYBiADKAsyHS5h" + + "Z2VudC52MS5DdXJzb3JQYWNrYWdlUHJvbXB0EhgKEHJlYWRtZV9maWxlX3BhdGgYByABKAkSFAoM" + + "cGFja2FnZV90eXBlGAggASgFQg4KDF9wYXJzZV9lcnJvciKrAgoWUmVwb3NpdG9yeUluZGV4aW5n" + + "SW5mbxIfChdyZWxhdGl2ZV93b3Jrc3BhY2VfcGF0aBgBIAEoCRITCgtyZW1vdGVfdXJscxgCIAMo" + + "CRIUCgxyZW1vdGVfbmFtZXMYAyADKAkSEQoJcmVwb19uYW1lGAQgASgJEhIKCnJlcG9fb3duZXIY" + + "BSABKAkSEgoKaXNfdHJhY2tlZBgGIAEoCBIQCghpc19sb2NhbBgHIAEoCBImChlvcnRob2dvbmFs" + + "X3RyYW5zZm9ybV9zZWVkGAggASgBSACIAQESFQoNd29ya3NwYWNlX3VyaRgJIAEoCRIbChNwYXRo" + + "X2VuY3J5cHRpb25fa2V5GAogASgJQhwKGl9vcnRob2dvbmFsX3RyYW5zZm9ybV9zZWVkInQKElJl" + + "cXVlc3RDb250ZXh0QXJncxIdChBub3Rlc19zZXNzaW9uX2lkGAIgASgJSACIAQESGQoMd29ya3Nw" + + "YWNlX2lkGAMgASgJSAGIAQFCEwoRX25vdGVzX3Nlc3Npb25faWRCDwoNX3dvcmtzcGFjZV9pZCK6" + + "AQoUUmVxdWVzdENvbnRleHRSZXN1bHQSMgoHc3VjY2VzcxgBIAEoCzIfLmFnZW50LnYxLlJlcXVl" + + "c3RDb250ZXh0U3VjY2Vzc0gAEi4KBWVycm9yGAIgASgLMh0uYWdlbnQudjEuUmVxdWVzdENvbnRl" + + "eHRFcnJvckgAEjQKCHJlamVjdGVkGAMgASgLMiAuYWdlbnQudjEuUmVxdWVzdENvbnRleHRSZWpl" + + "Y3RlZEgAQggKBnJlc3VsdCJKChVSZXF1ZXN0Q29udGV4dFN1Y2Nlc3MSMQoPcmVxdWVzdF9jb250" + + "ZXh0GAEgASgLMhguYWdlbnQudjEuUmVxdWVzdENvbnRleHQiJAoTUmVxdWVzdENvbnRleHRFcnJv" + + "chINCgVlcnJvchgBIAEoCSIoChZSZXF1ZXN0Q29udGV4dFJlamVjdGVkEg4KBnJlYXNvbhgBIAEo" + + "CSLCAQoKSW1hZ2VQcm90bxIMCgRkYXRhGAEgASgMEgwKBHV1aWQYAiABKAkSDAoEcGF0aBgDIAEo" + + "CRIxCglkaW1lbnNpb24YBCABKAsyHi5hZ2VudC52MS5JbWFnZVByb3RvX0RpbWVuc2lvbhImChl0" + + "YXNrX3NwZWNpZmljX2Rlc2NyaXB0aW9uGAYgASgJSACIAQESEQoJbWltZV90eXBlGAcgASgJQhwK" + + "Gl90YXNrX3NwZWNpZmljX2Rlc2NyaXB0aW9uIjUKFEltYWdlUHJvdG9fRGltZW5zaW9uEg0KBXdp" + + "ZHRoGAEgASgFEg4KBmhlaWdodBgCIAEoBSJoCgtHaXRSZXBvSW5mbxIMCgRwYXRoGAEgASgJEg4K" + + "BnN0YXR1cxgCIAEoCRITCgticmFuY2hfbmFtZRgDIAEoCRIXCgpyZW1vdGVfdXJsGAQgASgJSACI" + + "AQFCDQoLX3JlbW90ZV91cmwimwIKEVJlcXVlc3RDb250ZXh0RW52EhIKCm9zX3ZlcnNpb24YASAB" + + "KAkSFwoPd29ya3NwYWNlX3BhdGhzGAIgAygJEg0KBXNoZWxsGAMgASgJEhcKD3NhbmRib3hfZW5h" + + "YmxlZBgFIAEoCBIYChB0ZXJtaW5hbHNfZm9sZGVyGAcgASgJEiEKGWFnZW50X3NoYXJlZF9ub3Rl" + + "c19mb2xkZXIYCCABKAkSJwofYWdlbnRfY29udmVyc2F0aW9uX25vdGVzX2ZvbGRlchgJIAEoCRIR" + + "Cgl0aW1lX3pvbmUYCiABKAkSFgoOcHJvamVjdF9mb2xkZXIYCyABKAkSIAoYYWdlbnRfdHJhbnNj" + + "cmlwdHNfZm9sZGVyGAwgASgJIjwKD0RlYnVnTW9kZUNvbmZpZxIQCghsb2dfcGF0aBgBIAEoCRIX" + + "Cg9zZXJ2ZXJfZW5kcG9pbnQYAiABKAkitAEKD1NraWxsRGVzY3JpcHRvchIMCgRuYW1lGAEgASgJ" + + "EhMKC2Rlc2NyaXB0aW9uGAIgASgJEhMKC2ZvbGRlcl9wYXRoGAMgASgJEg8KB2VuYWJsZWQYBCAB" + + "KAgSGAoLcGFyc2VfZXJyb3IYBSABKAlIAIgBARIYChByZWFkbWVfZmlsZV9wYXRoGAYgASgJEhQK" + + "DHBhY2thZ2VfdHlwZRgHIAEoBUIOCgxfcGFyc2VfZXJyb3IiRAoMU2tpbGxPcHRpb25zEjQKEXNr" + + "aWxsX2Rlc2NyaXB0b3JzGAEgAygLMhkuYWdlbnQudjEuU2tpbGxEZXNjcmlwdG9yIvYICg5SZXF1" + + "ZXN0Q29udGV4dBIjCgVydWxlcxgCIAMoCzIULmFnZW50LnYxLkN1cnNvclJ1bGUSKAoDZW52GAQg" + + "ASgLMhsuYWdlbnQudjEuUmVxdWVzdENvbnRleHRFbnYSOQoPcmVwb3NpdG9yeV9pbmZvGAYgAygL" + + "MiAuYWdlbnQudjEuUmVwb3NpdG9yeUluZGV4aW5nSW5mbxIqCgV0b29scxgHIAMoCzIbLmFnZW50" + + "LnYxLk1jcFRvb2xEZWZpbml0aW9uEicKGmNvbnZlcnNhdGlvbl9ub3Rlc19saXN0aW5nGAggASgJ" + + "SACIAQESIQoUc2hhcmVkX25vdGVzX2xpc3RpbmcYCSABKAlIAYgBARIoCglnaXRfcmVwb3MYCyAD" + + "KAsyFS5hZ2VudC52MS5HaXRSZXBvSW5mbxI2Cg9wcm9qZWN0X2xheW91dHMYDSADKAsyHS5hZ2Vu" + + "dC52MS5Mc0RpcmVjdG9yeVRyZWVOb2RlEjMKEG1jcF9pbnN0cnVjdGlvbnMYDiADKAsyGS5hZ2Vu" + + "dC52MS5NY3BJbnN0cnVjdGlvbnMSOQoRZGVidWdfbW9kZV9jb25maWcYDyABKAsyGS5hZ2VudC52" + + "MS5EZWJ1Z01vZGVDb25maWdIAogBARIXCgpjbG91ZF9ydWxlGBAgASgJSAOIAQESHwoSd2ViX3Nl" + + "YXJjaF9lbmFibGVkGBEgASgISASIAQESMgoNc2tpbGxfb3B0aW9ucxgSIAEoCzIWLmFnZW50LnYx" + + "LlNraWxsT3B0aW9uc0gFiAEBEi4KIXJlcG9zaXRvcnlfaW5mb19zaG91bGRfcXVlcnlfcHJvZBgT" + + "IAEoCEgGiAEBEkEKDWZpbGVfY29udGVudHMYFCADKAsyKi5hZ2VudC52MS5SZXF1ZXN0Q29udGV4" + + "dC5GaWxlQ29udGVudHNFbnRyeRIgChN1c2VyX2ludGVudF9zdW1tYXJ5GBUgASgJSAeIAQESMgoQ" + + "Y3VzdG9tX3N1YmFnZW50cxgWIAMoCzIYLmFnZW50LnYxLkN1c3RvbVN1YmFnZW50EkQKF21jcF9m" + + "aWxlX3N5c3RlbV9vcHRpb25zGBcgASgLMh4uYWdlbnQudjEuTWNwRmlsZVN5c3RlbU9wdGlvbnNI" + + "CIgBARozChFGaWxlQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgB" + + "Qh0KG19jb252ZXJzYXRpb25fbm90ZXNfbGlzdGluZ0IXChVfc2hhcmVkX25vdGVzX2xpc3RpbmdC" + + "FAoSX2RlYnVnX21vZGVfY29uZmlnQg0KC19jbG91ZF9ydWxlQhUKE193ZWJfc2VhcmNoX2VuYWJs" + + "ZWRCEAoOX3NraWxsX29wdGlvbnNCJAoiX3JlcG9zaXRvcnlfaW5mb19zaG91bGRfcXVlcnlfcHJv" + + "ZEIWChRfdXNlcl9pbnRlbnRfc3VtbWFyeUIaChhfbWNwX2ZpbGVfc3lzdGVtX29wdGlvbnMisgIK" + + "DVNhbmRib3hQb2xpY3kSDAoEdHlwZRgBIAEoBRIbCg5uZXR3b3JrX2FjY2VzcxgCIAEoCEgAiAEB" + + "EiIKGmFkZGl0aW9uYWxfcmVhZHdyaXRlX3BhdGhzGAMgAygJEiEKGWFkZGl0aW9uYWxfcmVhZG9u" + + "bHlfcGF0aHMYBCADKAkSHQoQZGVidWdfb3V0cHV0X2RpchgFIAEoCUgBiAEBEh0KEGJsb2NrX2dp" + + "dF93cml0ZXMYBiABKAhIAogBARIeChFkaXNhYmxlX3RtcF93cml0ZRgHIAEoCEgDiAEBQhEKD19u" + + "ZXR3b3JrX2FjY2Vzc0ITChFfZGVidWdfb3V0cHV0X2RpckITChFfYmxvY2tfZ2l0X3dyaXRlc0IU" + + "ChJfZGlzYWJsZV90bXBfd3JpdGUi7wEKDVNlbGVjdGVkSW1hZ2USDAoEdXVpZBgCIAEoCRIMCgRw" + + "YXRoGAMgASgJEjQKCWRpbWVuc2lvbhgEIAEoCzIhLmFnZW50LnYxLlNlbGVjdGVkSW1hZ2VfRGlt" + + "ZW5zaW9uEhEKCW1pbWVfdHlwZRgHIAEoCRIRCgdibG9iX2lkGAEgASgMSAASDgoEZGF0YRgIIAEo" + + "DEgAEkMKEWJsb2JfaWRfd2l0aF9kYXRhGAkgASgLMiYuYWdlbnQudjEuU2VsZWN0ZWRJbWFnZV9C" + + "bG9iSWRXaXRoRGF0YUgAQhEKD2RhdGFfb3JfYmxvYl9pZCI9ChxTZWxlY3RlZEltYWdlX0Jsb2JJ" + + "ZFdpdGhEYXRhEg8KB2Jsb2JfaWQYASABKAwSDAoEZGF0YRgCIAEoDCI4ChdTZWxlY3RlZEltYWdl" + + "X0RpbWVuc2lvbhINCgV3aWR0aBgBIAEoBRIOCgZoZWlnaHQYAiABKAUiSQoRRXh0cmFDb250ZXh0" + + "RW50cnkSDgoEZGF0YRgBIAEoCUgAEhEKB2Jsb2JfaWQYAiABKAxIAEIRCg9kYXRhX29yX2Jsb2Jf" + + "aWQiWwoMU2VsZWN0ZWRGaWxlEg8KB2NvbnRlbnQYASABKAkSDAoEcGF0aBgCIAEoCRIaCg1yZWxh" + + "dGl2ZV9wYXRoGAMgASgJSACIAQFCEAoOX3JlbGF0aXZlX3BhdGgihAEKFVNlbGVjdGVkQ29kZVNl" + + "bGVjdGlvbhIPCgdjb250ZW50GAEgASgJEgwKBHBhdGgYAiABKAkSGgoNcmVsYXRpdmVfcGF0aBgD" + + "IAEoCUgAiAEBEh4KBXJhbmdlGAQgASgLMg8uYWdlbnQudjEuUmFuZ2VCEAoOX3JlbGF0aXZlX3Bh" + + "dGgiXQoQU2VsZWN0ZWRUZXJtaW5hbBIPCgdjb250ZW50GAEgASgJEhIKBXRpdGxlGAIgASgJSACI" + + "AQESEQoEcGF0aBgDIAEoCUgBiAEBQggKBl90aXRsZUIHCgVfcGF0aCKGAQoZU2VsZWN0ZWRUZXJt" + + "aW5hbFNlbGVjdGlvbhIPCgdjb250ZW50GAEgASgJEhIKBXRpdGxlGAIgASgJSACIAQESEQoEcGF0" + + "aBgDIAEoCUgBiAEBEh4KBXJhbmdlGAQgASgLMg8uYWdlbnQudjEuUmFuZ2VCCAoGX3RpdGxlQgcK" + + "BV9wYXRoIoMBCg5TZWxlY3RlZEZvbGRlchIMCgRwYXRoGAEgASgJEhoKDXJlbGF0aXZlX3BhdGgY" + + "AiABKAlIAIgBARI1Cg5kaXJlY3RvcnlfdHJlZRgDIAEoCzIdLmFnZW50LnYxLkxzRGlyZWN0b3J5" + + "VHJlZU5vZGVCEAoOX3JlbGF0aXZlX3BhdGginwEKFFNlbGVjdGVkRXh0ZXJuYWxMaW5rEgsKA3Vy" + + "bBgBIAEoCRIMCgR1dWlkGAIgASgJEhgKC3BkZl9jb250ZW50GAMgASgJSACIAQESEwoGaXNfcGRm" + + "GAQgASgISAGIAQESFQoIZmlsZW5hbWUYBSABKAlIAogBAUIOCgxfcGRmX2NvbnRlbnRCCQoHX2lz" + + "X3BkZkILCglfZmlsZW5hbWUiOAoSU2VsZWN0ZWRDdXJzb3JSdWxlEiIKBHJ1bGUYASABKAsyFC5h" + + "Z2VudC52MS5DdXJzb3JSdWxlIiIKD1NlbGVjdGVkR2l0RGlmZhIPCgdjb250ZW50GAEgASgJIjIK" + + "H1NlbGVjdGVkR2l0RGlmZkZyb21CcmFuY2hUb01haW4SDwoHY29udGVudBgBIAEoCSJpChFTZWxl" + + "Y3RlZEdpdENvbW1pdBILCgNzaGEYASABKAkSDwoHbWVzc2FnZRgCIAEoCRIYCgtkZXNjcmlwdGlv" + + "bhgDIAEoCUgAiAEBEgwKBGRpZmYYBCABKAlCDgoMX2Rlc2NyaXB0aW9uIt0BChNTZWxlY3RlZFB1" + + "bGxSZXF1ZXN0Eg4KBm51bWJlchgBIAEoBRILCgN1cmwYAiABKAkSEgoFdGl0bGUYAyABKAlIAIgB" + + "ARITCgtmb2xkZXJfcGF0aBgEIAEoCRIZCgxzdW1tYXJ5X2pzb24YBSABKAlIAYgBARIYCgtkZXNj" + + "cmlwdGlvbhgGIAEoCUgCiAEBEhQKB2Jsb2JfaWQYByABKAxIA4gBAUIICgZfdGl0bGVCDwoNX3N1" + + "bW1hcnlfanNvbkIOCgxfZGVzY3JpcHRpb25CCgoIX2Jsb2JfaWQiswEKGlNlbGVjdGVkR2l0UFJE" + + "aWZmU2VsZWN0aW9uEg4KBnByX3VybBgBIAEoCRIRCglmaWxlX3BhdGgYAiABKAkSEgoKc3RhcnRf" + + "bGluZRgDIAEoBRIQCghlbmRfbGluZRgEIAEoBRIZCgxkaWZmX2NvbnRlbnQYBSABKAlIAIgBARIU" + + "CgdibG9iX2lkGAYgASgMSAGIAQFCDwoNX2RpZmZfY29udGVudEIKCghfYmxvYl9pZCI2ChVTZWxl" + + "Y3RlZEN1cnNvckNvbW1hbmQSDAoEbmFtZRgBIAEoCRIPCgdjb250ZW50GAIgASgJIjUKFVNlbGVj" + + "dGVkRG9jdW1lbnRhdGlvbhIOCgZkb2NfaWQYASABKAkSDAoEbmFtZRgCIAEoCSIyChBTZWxlY3Rl" + + "ZFBhc3RDaGF0EhAKCGFnZW50X2lkGAEgASgJEgwKBG5hbWUYAiABKAkiqwEKCUNhbGxGcmFtZRIa" + + "Cg1mdW5jdGlvbl9uYW1lGAEgASgJSACIAQESEAoDdXJsGAIgASgJSAGIAQESGAoLbGluZV9udW1i" + + "ZXIYAyABKAVIAogBARIaCg1jb2x1bW5fbnVtYmVyGAQgASgFSAOIAQFCEAoOX2Z1bmN0aW9uX25h" + + "bWVCBgoEX3VybEIOCgxfbGluZV9udW1iZXJCEAoOX2NvbHVtbl9udW1iZXIiaAoKU3RhY2tUcmFj" + + "ZRIoCgtjYWxsX2ZyYW1lcxgBIAMoCzITLmFnZW50LnYxLkNhbGxGcmFtZRIcCg9yYXdfc3RhY2tf" + + "dHJhY2UYAiABKAlIAIgBAUISChBfcmF3X3N0YWNrX3RyYWNlIuQBChJTZWxlY3RlZENvbnNvbGVM" + + "b2cSDwoHbWVzc2FnZRgBIAEoCRIRCgl0aW1lc3RhbXAYAiABKAESDQoFbGV2ZWwYAyABKAkSEwoL" + + "Y2xpZW50X25hbWUYBCABKAkSEgoKc2Vzc2lvbl9pZBgFIAEoCRIuCgtzdGFja190cmFjZRgGIAEo" + + "CzIULmFnZW50LnYxLlN0YWNrVHJhY2VIAIgBARIdChBvYmplY3RfZGF0YV9qc29uGAcgASgJSAGI" + + "AQFCDgoMX3N0YWNrX3RyYWNlQhMKEV9vYmplY3RfZGF0YV9qc29uIroBChFTZWxlY3RlZFVJRWxl" + + "bWVudBIPCgdlbGVtZW50GAEgASgJEg0KBXhwYXRoGAIgASgJEhQKDHRleHRfY29udGVudBgDIAEo" + + "CRINCgVleHRyYRgEIAEoCRIWCgljb21wb25lbnQYBSABKAlIAIgBARIhChRjb21wb25lbnRfcHJv" + + "cHNfanNvbhgGIAEoCUgBiAEBQgwKCl9jb21wb25lbnRCFwoVX2NvbXBvbmVudF9wcm9wc19qc29u" + + "IiAKEFNlbGVjdGVkU3ViYWdlbnQSDAoEbmFtZRgBIAEoCSKCCgoPU2VsZWN0ZWRDb250ZXh0EjAK" + + "D3NlbGVjdGVkX2ltYWdlcxgBIAMoCzIXLmFnZW50LnYxLlNlbGVjdGVkSW1hZ2USPAoSaW52b2Nh" + + "dGlvbl9jb250ZXh0GAIgASgLMhsuYWdlbnQudjEuSW52b2NhdGlvbkNvbnRleHRIAIgBARIVCg1l" + + "eHRyYV9jb250ZXh0GAMgAygJEjoKFWV4dHJhX2NvbnRleHRfZW50cmllcxgQIAMoCzIbLmFnZW50" + + "LnYxLkV4dHJhQ29udGV4dEVudHJ5EiUKBWZpbGVzGAQgAygLMhYuYWdlbnQudjEuU2VsZWN0ZWRG" + + "aWxlEjgKD2NvZGVfc2VsZWN0aW9ucxgFIAMoCzIfLmFnZW50LnYxLlNlbGVjdGVkQ29kZVNlbGVj" + + "dGlvbhItCgl0ZXJtaW5hbHMYBiADKAsyGi5hZ2VudC52MS5TZWxlY3RlZFRlcm1pbmFsEkAKE3Rl" + + "cm1pbmFsX3NlbGVjdGlvbnMYByADKAsyIy5hZ2VudC52MS5TZWxlY3RlZFRlcm1pbmFsU2VsZWN0" + + "aW9uEikKB2ZvbGRlcnMYCCADKAsyGC5hZ2VudC52MS5TZWxlY3RlZEZvbGRlchI2Cg5leHRlcm5h" + + "bF9saW5rcxgJIAMoCzIeLmFnZW50LnYxLlNlbGVjdGVkRXh0ZXJuYWxMaW5rEjIKDGN1cnNvcl9y" + + "dWxlcxgKIAMoCzIcLmFnZW50LnYxLlNlbGVjdGVkQ3Vyc29yUnVsZRIwCghnaXRfZGlmZhgSIAEo" + + "CzIZLmFnZW50LnYxLlNlbGVjdGVkR2l0RGlmZkgBiAEBElQKHGdpdF9kaWZmX2Zyb21fYnJhbmNo" + + "X3RvX21haW4YCyABKAsyKS5hZ2VudC52MS5TZWxlY3RlZEdpdERpZmZGcm9tQnJhbmNoVG9NYWlu" + + "SAKIAQESOAoPY3Vyc29yX2NvbW1hbmRzGAwgAygLMh8uYWdlbnQudjEuU2VsZWN0ZWRDdXJzb3JD" + + "b21tYW5kEjcKDmRvY3VtZW50YXRpb25zGA0gAygLMh8uYWdlbnQudjEuU2VsZWN0ZWREb2N1bWVu" + + "dGF0aW9uEjAKC3VpX2VsZW1lbnRzGA4gAygLMhsuYWdlbnQudjEuU2VsZWN0ZWRVSUVsZW1lbnQS" + + "MgoMY29uc29sZV9sb2dzGA8gAygLMhwuYWdlbnQudjEuU2VsZWN0ZWRDb25zb2xlTG9nEjAKC2dp" + + "dF9jb21taXRzGBEgAygLMhsuYWdlbnQudjEuU2VsZWN0ZWRHaXRDb21taXQSLgoKcGFzdF9jaGF0" + + "cxgTIAMoCzIaLmFnZW50LnYxLlNlbGVjdGVkUGFzdENoYXQSRAoWZ2l0X3ByX2RpZmZfc2VsZWN0" + + "aW9ucxgUIAMoCzIkLmFnZW50LnYxLlNlbGVjdGVkR2l0UFJEaWZmU2VsZWN0aW9uEj0KFnNlbGVj" + + "dGVkX3B1bGxfcmVxdWVzdHMYFSADKAsyHS5hZ2VudC52MS5TZWxlY3RlZFB1bGxSZXF1ZXN0EjYK" + + "EnNlbGVjdGVkX3N1YmFnZW50cxgWIAMoCzIaLmFnZW50LnYxLlNlbGVjdGVkU3ViYWdlbnRCFQoT" + + "X2ludm9jYXRpb25fY29udGV4dEILCglfZ2l0X2RpZmZCHwodX2dpdF9kaWZmX2Zyb21fYnJhbmNo" + + "X3RvX21haW4i5QEKEUludm9jYXRpb25Db250ZXh0Ej8KDHNsYWNrX3RocmVhZBgBIAEoCzInLmFn" + + "ZW50LnYxLkludm9jYXRpb25Db250ZXh0X1NsYWNrVGhyZWFkSAASOQoJZ2l0aHViX3ByGAIgASgL" + + "MiQuYWdlbnQudjEuSW52b2NhdGlvbkNvbnRleHRfR2l0aHViUFJIABI5CglpZGVfc3RhdGUYAyAB" + + "KAsyJC5hZ2VudC52MS5JbnZvY2F0aW9uQ29udGV4dF9JZGVTdGF0ZUgAEhEKB2Jsb2JfaWQYCiAB" + + "KAxIAEIGCgRkYXRhIrsBCh1JbnZvY2F0aW9uQ29udGV4dF9TbGFja1RocmVhZBIOCgZ0aHJlYWQY" + + "ASABKAkSGQoMY2hhbm5lbF9uYW1lGAIgASgJSACIAQESHAoPY2hhbm5lbF9wdXJwb3NlGAMgASgJ" + + "SAGIAQESGgoNY2hhbm5lbF90b3BpYxgEIAEoCUgCiAEBQg8KDV9jaGFubmVsX25hbWVCEgoQX2No" + + "YW5uZWxfcHVycG9zZUIQCg5fY2hhbm5lbF90b3BpYyJ8ChpJbnZvY2F0aW9uQ29udGV4dF9HaXRo" + + "dWJQUhINCgV0aXRsZRgBIAEoCRITCgtkZXNjcmlwdGlvbhgCIAEoCRIQCghjb21tZW50cxgDIAEo" + + "CRIYCgtjaV9mYWlsdXJlcxgEIAEoCUgAiAEBQg4KDF9jaV9mYWlsdXJlcyL+AQoaSW52b2NhdGlv" + + "bkNvbnRleHRfSWRlU3RhdGUSQAoNdmlzaWJsZV9maWxlcxgBIAMoCzIpLmFnZW50LnYxLkludm9j" + + "YXRpb25Db250ZXh0X0lkZVN0YXRlX0ZpbGUSSAoVcmVjZW50bHlfdmlld2VkX2ZpbGVzGAIgAygL" + + "MikuYWdlbnQudjEuSW52b2NhdGlvbkNvbnRleHRfSWRlU3RhdGVfRmlsZRJUChRjdXJyZW50bHlf" + + "dmlld2VkX3BycxgDIAMoCzI2LmFnZW50LnYxLkludm9jYXRpb25Db250ZXh0X0lkZVN0YXRlX1Zp" + + "ZXdlZFB1bGxSZXF1ZXN0Io4CCh9JbnZvY2F0aW9uQ29udGV4dF9JZGVTdGF0ZV9GaWxlEgwKBHBh" + + "dGgYASABKAkSGgoNcmVsYXRpdmVfcGF0aBgCIAEoCUgAiAEBElYKD2N1cnNvcl9wb3NpdGlvbhgD" + + "IAEoCzI4LmFnZW50LnYxLkludm9jYXRpb25Db250ZXh0X0lkZVN0YXRlX0ZpbGVfQ3Vyc29yUG9z" + + "aXRpb25IAYgBARITCgt0b3RhbF9saW5lcxgEIAEoBRIbCg5hY3RpdmVfY29tbWFuZBgFIAEoCUgC" + + "iAEBQhAKDl9yZWxhdGl2ZV9wYXRoQhIKEF9jdXJzb3JfcG9zaXRpb25CEQoPX2FjdGl2ZV9jb21t" + + "YW5kIkwKLkludm9jYXRpb25Db250ZXh0X0lkZVN0YXRlX0ZpbGVfQ3Vyc29yUG9zaXRpb24SDAoE" + + "bGluZRgBIAEoBRIMCgR0ZXh0GAIgASgJIukBCixJbnZvY2F0aW9uQ29udGV4dF9JZGVTdGF0ZV9W" + + "aWV3ZWRQdWxsUmVxdWVzdBIOCgZudW1iZXIYASABKAUSCwoDdXJsGAIgASgJEhIKBXRpdGxlGAMg" + + "ASgJSACIAQESGAoLZm9sZGVyX3BhdGgYBCABKAlIAYgBARIZCgxzdW1tYXJ5X2pzb24YBSABKAlI" + + "AogBARIYCgtkZXNjcmlwdGlvbhgGIAEoCUgDiAEBQggKBl90aXRsZUIOCgxfZm9sZGVyX3BhdGhC" + + "DwoNX3N1bW1hcnlfanNvbkIOCgxfZGVzY3JpcHRpb24iSAoWU2V0dXBWbUVudmlyb25tZW50QXJn" + + "cxIXCg9pbnN0YWxsX2NvbW1hbmQYAiABKAkSFQoNc3RhcnRfY29tbWFuZBgDIAEoCSJcChhTZXR1" + + "cFZtRW52aXJvbm1lbnRSZXN1bHQSNgoHc3VjY2VzcxgBIAEoCzIjLmFnZW50LnYxLlNldHVwVm1F" + + "bnZpcm9ubWVudFN1Y2Nlc3NIAEIICgZyZXN1bHQiGwoZU2V0dXBWbUVudmlyb25tZW50U3VjY2Vz" + + "cyKAAQoaU2V0dXBWbUVudmlyb25tZW50VG9vbENhbGwSLgoEYXJncxgBIAEoCzIgLmFnZW50LnYx" + + "LlNldHVwVm1FbnZpcm9ubWVudEFyZ3MSMgoGcmVzdWx0GAIgASgLMiIuYWdlbnQudjEuU2V0dXBW" + + "bUVudmlyb25tZW50UmVzdWx0IsABChlTaGVsbENvbW1hbmRQYXJzaW5nUmVzdWx0EhYKDnBhcnNp" + + "bmdfZmFpbGVkGAEgASgIElIKE2V4ZWN1dGFibGVfY29tbWFuZHMYAiADKAsyNS5hZ2VudC52MS5T" + + "aGVsbENvbW1hbmRQYXJzaW5nUmVzdWx0X0V4ZWN1dGFibGVDb21tYW5kEhUKDWhhc19yZWRpcmVj" + + "dHMYAyABKAgSIAoYaGFzX2NvbW1hbmRfc3Vic3RpdHV0aW9uGAQgASgIIk0KLlNoZWxsQ29tbWFu" + + "ZFBhcnNpbmdSZXN1bHRfRXhlY3V0YWJsZUNvbW1hbmRBcmcSDAoEdHlwZRgBIAEoCRINCgV2YWx1" + + "ZRgCIAEoCSKWAQorU2hlbGxDb21tYW5kUGFyc2luZ1Jlc3VsdF9FeGVjdXRhYmxlQ29tbWFuZBIM" + + "CgRuYW1lGAEgASgJEkYKBGFyZ3MYAiADKAsyOC5hZ2VudC52MS5TaGVsbENvbW1hbmRQYXJzaW5n" + + "UmVzdWx0X0V4ZWN1dGFibGVDb21tYW5kQXJnEhEKCWZ1bGxfdGV4dBgDIAEoCSKIBAoJU2hlbGxB" + + "cmdzEg8KB2NvbW1hbmQYASABKAkSGQoRd29ya2luZ19kaXJlY3RvcnkYAiABKAkSDwoHdGltZW91" + + "dBgDIAEoBRIUCgx0b29sX2NhbGxfaWQYBCABKAkSFwoPc2ltcGxlX2NvbW1hbmRzGAUgAygJEhoK" + + "Emhhc19pbnB1dF9yZWRpcmVjdBgGIAEoCBIbChNoYXNfb3V0cHV0X3JlZGlyZWN0GAcgASgIEjsK" + + "DnBhcnNpbmdfcmVzdWx0GAggASgLMiMuYWdlbnQudjEuU2hlbGxDb21tYW5kUGFyc2luZ1Jlc3Vs" + + "dBI+ChhyZXF1ZXN0ZWRfc2FuZGJveF9wb2xpY3kYCSABKAsyFy5hZ2VudC52MS5TYW5kYm94UG9s" + + "aWN5SACIAQESKAobZmlsZV9vdXRwdXRfdGhyZXNob2xkX2J5dGVzGAogASgESAGIAQESFQoNaXNf" + + "YmFja2dyb3VuZBgLIAEoCBIVCg1za2lwX2FwcHJvdmFsGAwgASgIEhgKEHRpbWVvdXRfYmVoYXZp" + + "b3IYDSABKAUSGQoMaGFyZF90aW1lb3V0GA4gASgFSAKIAQFCGwoZX3JlcXVlc3RlZF9zYW5kYm94" + + "X3BvbGljeUIeChxfZmlsZV9vdXRwdXRfdGhyZXNob2xkX2J5dGVzQg8KDV9oYXJkX3RpbWVvdXQi" + + "+gMKC1NoZWxsUmVzdWx0EjQKDnNhbmRib3hfcG9saWN5GGUgASgLMhcuYWdlbnQudjEuU2FuZGJv" + + "eFBvbGljeUgBiAEBEhoKDWlzX2JhY2tncm91bmQYZiABKAhIAogBARIdChB0ZXJtaW5hbHNfZm9s" + + "ZGVyGGcgASgJSAOIAQESEAoDcGlkGGggASgNSASIAQESKQoHc3VjY2VzcxgBIAEoCzIWLmFnZW50" + + "LnYxLlNoZWxsU3VjY2Vzc0gAEikKB2ZhaWx1cmUYAiABKAsyFi5hZ2VudC52MS5TaGVsbEZhaWx1" + + "cmVIABIpCgd0aW1lb3V0GAMgASgLMhYuYWdlbnQudjEuU2hlbGxUaW1lb3V0SAASKwoIcmVqZWN0" + + "ZWQYBCABKAsyFy5hZ2VudC52MS5TaGVsbFJlamVjdGVkSAASMAoLc3Bhd25fZXJyb3IYBSABKAsy" + + "GS5hZ2VudC52MS5TaGVsbFNwYXduRXJyb3JIABI8ChFwZXJtaXNzaW9uX2RlbmllZBgHIAEoCzIf" + + "LmFnZW50LnYxLlNoZWxsUGVybWlzc2lvbkRlbmllZEgAQggKBnJlc3VsdEIRCg9fc2FuZGJveF9w" + + "b2xpY3lCEAoOX2lzX2JhY2tncm91bmRCEwoRX3Rlcm1pbmFsc19mb2xkZXJCBgoEX3BpZCIhChFT" + + "aGVsbFN0cmVhbVN0ZG91dBIMCgRkYXRhGAEgASgJIiEKEVNoZWxsU3RyZWFtU3RkZXJyEgwKBGRh" + + "dGEYASABKAkitQEKD1NoZWxsU3RyZWFtRXhpdBIMCgRjb2RlGAEgASgNEgsKA2N3ZBgCIAEoCRI2" + + "Cg9vdXRwdXRfbG9jYXRpb24YAyABKAsyGC5hZ2VudC52MS5PdXRwdXRMb2NhdGlvbkgAiAEBEg8K" + + "B2Fib3J0ZWQYBCABKAgSGQoMYWJvcnRfcmVhc29uGAUgASgFSAGIAQFCEgoQX291dHB1dF9sb2Nh" + + "dGlvbkIPCg1fYWJvcnRfcmVhc29uIlsKEFNoZWxsU3RyZWFtU3RhcnQSNAoOc2FuZGJveF9wb2xp" + + "Y3kYASABKAsyFy5hZ2VudC52MS5TYW5kYm94UG9saWN5SACIAQFCEQoPX3NhbmRib3hfcG9saWN5" + + "IpkBChdTaGVsbFN0cmVhbUJhY2tncm91bmRlZBIQCghzaGVsbF9pZBgBIAEoDRIPCgdjb21tYW5k" + + "GAIgASgJEhkKEXdvcmtpbmdfZGlyZWN0b3J5GAMgASgJEhAKA3BpZBgEIAEoDUgAiAEBEhcKCm1z" + + "X3RvX3dhaXQYBSABKAVIAYgBAUIGCgRfcGlkQg0KC19tc190b193YWl0IvICCgtTaGVsbFN0cmVh" + + "bRItCgZzdGRvdXQYASABKAsyGy5hZ2VudC52MS5TaGVsbFN0cmVhbVN0ZG91dEgAEi0KBnN0ZGVy" + + "chgCIAEoCzIbLmFnZW50LnYxLlNoZWxsU3RyZWFtU3RkZXJySAASKQoEZXhpdBgDIAEoCzIZLmFn" + + "ZW50LnYxLlNoZWxsU3RyZWFtRXhpdEgAEisKBXN0YXJ0GAQgASgLMhouYWdlbnQudjEuU2hlbGxT" + + "dHJlYW1TdGFydEgAEisKCHJlamVjdGVkGAUgASgLMhcuYWdlbnQudjEuU2hlbGxSZWplY3RlZEgA" + + "EjwKEXBlcm1pc3Npb25fZGVuaWVkGAYgASgLMh8uYWdlbnQudjEuU2hlbGxQZXJtaXNzaW9uRGVu" + + "aWVkSAASOQoMYmFja2dyb3VuZGVkGAcgASgLMiEuYWdlbnQudjEuU2hlbGxTdHJlYW1CYWNrZ3Jv" + + "dW5kZWRIAEIHCgVldmVudCJLCg5PdXRwdXRMb2NhdGlvbhIRCglmaWxlX3BhdGgYASABKAkSEgoK" + + "c2l6ZV9ieXRlcxgCIAEoAxISCgpsaW5lX2NvdW50GAMgASgDIv8CCgxTaGVsbFN1Y2Nlc3MSDwoH" + + "Y29tbWFuZBgBIAEoCRIZChF3b3JraW5nX2RpcmVjdG9yeRgCIAEoCRIRCglleGl0X2NvZGUYAyAB" + + "KAUSDgoGc2lnbmFsGAQgASgJEg4KBnN0ZG91dBgFIAEoCRIOCgZzdGRlcnIYBiABKAkSFgoOZXhl" + + "Y3V0aW9uX3RpbWUYByABKAUSNgoPb3V0cHV0X2xvY2F0aW9uGAggASgLMhguYWdlbnQudjEuT3V0" + + "cHV0TG9jYXRpb25IAIgBARIVCghzaGVsbF9pZBgJIAEoDUgBiAEBEh8KEmludGVybGVhdmVkX291" + + "dHB1dBgKIAEoCUgCiAEBEhAKA3BpZBgLIAEoDUgDiAEBEhcKCm1zX3RvX3dhaXQYDCABKAVIBIgB" + + "AUISChBfb3V0cHV0X2xvY2F0aW9uQgsKCV9zaGVsbF9pZEIVChNfaW50ZXJsZWF2ZWRfb3V0cHV0" + + "QgYKBF9waWRCDQoLX21zX3RvX3dhaXQi1gIKDFNoZWxsRmFpbHVyZRIPCgdjb21tYW5kGAEgASgJ" + + "EhkKEXdvcmtpbmdfZGlyZWN0b3J5GAIgASgJEhEKCWV4aXRfY29kZRgDIAEoBRIOCgZzaWduYWwY" + + "BCABKAkSDgoGc3Rkb3V0GAUgASgJEg4KBnN0ZGVychgGIAEoCRIWCg5leGVjdXRpb25fdGltZRgH" + + "IAEoBRI2Cg9vdXRwdXRfbG9jYXRpb24YCCABKAsyGC5hZ2VudC52MS5PdXRwdXRMb2NhdGlvbkgA" + + "iAEBEh8KEmludGVybGVhdmVkX291dHB1dBgJIAEoCUgBiAEBEhkKDGFib3J0X3JlYXNvbhgKIAEo" + + "BUgCiAEBEg8KB2Fib3J0ZWQYCyABKAhCEgoQX291dHB1dF9sb2NhdGlvbkIVChNfaW50ZXJsZWF2" + + "ZWRfb3V0cHV0Qg8KDV9hYm9ydF9yZWFzb24iTgoMU2hlbGxUaW1lb3V0Eg8KB2NvbW1hbmQYASAB" + + "KAkSGQoRd29ya2luZ19kaXJlY3RvcnkYAiABKAkSEgoKdGltZW91dF9tcxgDIAEoBSJgCg1TaGVs" + + "bFJlamVjdGVkEg8KB2NvbW1hbmQYASABKAkSGQoRd29ya2luZ19kaXJlY3RvcnkYAiABKAkSDgoG" + + "cmVhc29uGAMgASgJEhMKC2lzX3JlYWRvbmx5GAQgASgIImcKFVNoZWxsUGVybWlzc2lvbkRlbmll" + + "ZBIPCgdjb21tYW5kGAEgASgJEhkKEXdvcmtpbmdfZGlyZWN0b3J5GAIgASgJEg0KBWVycm9yGAMg" + + "ASgJEhMKC2lzX3JlYWRvbmx5GAQgASgIIkwKD1NoZWxsU3Bhd25FcnJvchIPCgdjb21tYW5kGAEg" + + "ASgJEhkKEXdvcmtpbmdfZGlyZWN0b3J5GAIgASgJEg0KBWVycm9yGAMgASgJIkAKElNoZWxsUGFy" + + "dGlhbFJlc3VsdBIUCgxzdGRvdXRfZGVsdGEYASABKAkSFAoMc3RkZXJyX2RlbHRhGAIgASgJIlkK" + + "DVNoZWxsVG9vbENhbGwSIQoEYXJncxgBIAEoCzITLmFnZW50LnYxLlNoZWxsQXJncxIlCgZyZXN1" + + "bHQYAiABKAsyFS5hZ2VudC52MS5TaGVsbFJlc3VsdCIrChhTaGVsbFRvb2xDYWxsU3Rkb3V0RGVs" + + "dGESDwoHY29udGVudBgBIAEoCSIrChhTaGVsbFRvb2xDYWxsU3RkZXJyRGVsdGESDwoHY29udGVu" + + "dBgBIAEoCSKJAQoSU2hlbGxUb29sQ2FsbERlbHRhEjQKBnN0ZG91dBgBIAEoCzIiLmFnZW50LnYx" + + "LlNoZWxsVG9vbENhbGxTdGRvdXREZWx0YUgAEjQKBnN0ZGVychgCIAEoCzIiLmFnZW50LnYxLlNo" + + "ZWxsVG9vbENhbGxTdGRlcnJEZWx0YUgAQgcKBWRlbHRhIu0BCgxTdWJhZ2VudFR5cGUSOAoLdW5z" + + "cGVjaWZpZWQYASABKAsyIS5hZ2VudC52MS5TdWJhZ2VudFR5cGVVbnNwZWNpZmllZEgAEjkKDGNv" + + "bXB1dGVyX3VzZRgCIAEoCzIhLmFnZW50LnYxLlN1YmFnZW50VHlwZUNvbXB1dGVyVXNlSAASLgoG" + + "Y3VzdG9tGAMgASgLMhwuYWdlbnQudjEuU3ViYWdlbnRUeXBlQ3VzdG9tSAASMAoHZXhwbG9yZRgE" + + "IAEoCzIdLmFnZW50LnYxLlN1YmFnZW50VHlwZUV4cGxvcmVIAEIGCgR0eXBlIhkKF1N1YmFnZW50" + + "VHlwZVVuc3BlY2lmaWVkIhkKF1N1YmFnZW50VHlwZUNvbXB1dGVyVXNlIhUKE1N1YmFnZW50VHlw" + + "ZUV4cGxvcmUiIgoSU3ViYWdlbnRUeXBlQ3VzdG9tEgwKBG5hbWUYASABKAkijQEKDkN1c3RvbVN1" + + "YmFnZW50EhEKCWZ1bGxfcGF0aBgBIAEoCRIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMg" + + "ASgJEg0KBXRvb2xzGAQgAygJEg0KBW1vZGVsGAUgASgJEg4KBnByb21wdBgGIAEoCRIXCg9wZXJt" + + "aXNzaW9uX21vZGUYByABKAUiaAoOU3dpdGNoTW9kZUFyZ3MSFgoOdGFyZ2V0X21vZGVfaWQYASAB" + + "KAkSGAoLZXhwbGFuYXRpb24YAiABKAlIAIgBARIUCgx0b29sX2NhbGxfaWQYAyABKAlCDgoMX2V4" + + "cGxhbmF0aW9uIqoBChBTd2l0Y2hNb2RlUmVzdWx0Ei4KB3N1Y2Nlc3MYASABKAsyGy5hZ2VudC52" + + "MS5Td2l0Y2hNb2RlU3VjY2Vzc0gAEioKBWVycm9yGAIgASgLMhkuYWdlbnQudjEuU3dpdGNoTW9k" + + "ZUVycm9ySAASMAoIcmVqZWN0ZWQYAyABKAsyHC5hZ2VudC52MS5Td2l0Y2hNb2RlUmVqZWN0ZWRI" + + "AEIICgZyZXN1bHQiPQoRU3dpdGNoTW9kZVN1Y2Nlc3MSFAoMZnJvbV9tb2RlX2lkGAEgASgJEhIK" + + "CnRvX21vZGVfaWQYAiABKAkiIAoPU3dpdGNoTW9kZUVycm9yEg0KBWVycm9yGAEgASgJIiQKElN3" + + "aXRjaE1vZGVSZWplY3RlZBIOCgZyZWFzb24YASABKAkiaAoSU3dpdGNoTW9kZVRvb2xDYWxsEiYK" + + "BGFyZ3MYASABKAsyGC5hZ2VudC52MS5Td2l0Y2hNb2RlQXJncxIqCgZyZXN1bHQYAiABKAsyGi5h" + + "Z2VudC52MS5Td2l0Y2hNb2RlUmVzdWx0IkAKFlN3aXRjaE1vZGVSZXF1ZXN0UXVlcnkSJgoEYXJn" + + "cxgBIAEoCzIYLmFnZW50LnYxLlN3aXRjaE1vZGVBcmdzIqkBChlTd2l0Y2hNb2RlUmVxdWVzdFJl" + + "c3BvbnNlEkAKCGFwcHJvdmVkGAEgASgLMiwuYWdlbnQudjEuU3dpdGNoTW9kZVJlcXVlc3RSZXNw" + + "b25zZV9BcHByb3ZlZEgAEkAKCHJlamVjdGVkGAIgASgLMiwuYWdlbnQudjEuU3dpdGNoTW9kZVJl" + + "cXVlc3RSZXNwb25zZV9SZWplY3RlZEgAQggKBnJlc3VsdCIkCiJTd2l0Y2hNb2RlUmVxdWVzdFJl" + + "c3BvbnNlX0FwcHJvdmVkIjQKIlN3aXRjaE1vZGVSZXF1ZXN0UmVzcG9uc2VfUmVqZWN0ZWQSDgoG" + + "cmVhc29uGAEgASgJInUKCFRvZG9JdGVtEgoKAmlkGAEgASgJEg8KB2NvbnRlbnQYAiABKAkSDgoG" + + "c3RhdHVzGAMgASgFEhIKCmNyZWF0ZWRfYXQYBCABKAMSEgoKdXBkYXRlZF9hdBgFIAEoAxIUCgxk" + + "ZXBlbmRlbmNpZXMYBiADKAkiawoTVXBkYXRlVG9kb3NUb29sQ2FsbBInCgRhcmdzGAEgASgLMhku" + + "YWdlbnQudjEuVXBkYXRlVG9kb3NBcmdzEisKBnJlc3VsdBgCIAEoCzIbLmFnZW50LnYxLlVwZGF0" + + "ZVRvZG9zUmVzdWx0IkMKD1VwZGF0ZVRvZG9zQXJncxIhCgV0b2RvcxgBIAMoCzISLmFnZW50LnYx" + + "LlRvZG9JdGVtEg0KBW1lcmdlGAIgASgIInsKEVVwZGF0ZVRvZG9zUmVzdWx0Ei8KB3N1Y2Nlc3MY" + + "ASABKAsyHC5hZ2VudC52MS5VcGRhdGVUb2Rvc1N1Y2Nlc3NIABIrCgVlcnJvchgCIAEoCzIaLmFn" + + "ZW50LnYxLlVwZGF0ZVRvZG9zRXJyb3JIAEIICgZyZXN1bHQiXwoSVXBkYXRlVG9kb3NTdWNjZXNz" + + "EiEKBXRvZG9zGAEgAygLMhIuYWdlbnQudjEuVG9kb0l0ZW0SEwoLdG90YWxfY291bnQYAiABKAUS" + + "EQoJd2FzX21lcmdlGAMgASgIIiEKEFVwZGF0ZVRvZG9zRXJyb3ISDQoFZXJyb3IYASABKAkiZQoR" + + "UmVhZFRvZG9zVG9vbENhbGwSJQoEYXJncxgBIAEoCzIXLmFnZW50LnYxLlJlYWRUb2Rvc0FyZ3MS" + + "KQoGcmVzdWx0GAIgASgLMhkuYWdlbnQudjEuUmVhZFRvZG9zUmVzdWx0IjkKDVJlYWRUb2Rvc0Fy" + + "Z3MSFQoNc3RhdHVzX2ZpbHRlchgBIAMoBRIRCglpZF9maWx0ZXIYAiADKAkidQoPUmVhZFRvZG9z" + + "UmVzdWx0Ei0KB3N1Y2Nlc3MYASABKAsyGi5hZ2VudC52MS5SZWFkVG9kb3NTdWNjZXNzSAASKQoF" + + "ZXJyb3IYAiABKAsyGC5hZ2VudC52MS5SZWFkVG9kb3NFcnJvckgAQggKBnJlc3VsdCJKChBSZWFk" + + "VG9kb3NTdWNjZXNzEiEKBXRvZG9zGAEgAygLMhIuYWdlbnQudjEuVG9kb0l0ZW0SEwoLdG90YWxf" + + "Y291bnQYAiABKAUiHwoOUmVhZFRvZG9zRXJyb3ISDQoFZXJyb3IYASABKAkiSwoFUmFuZ2USIQoF" + + "c3RhcnQYASABKAsyEi5hZ2VudC52MS5Qb3NpdGlvbhIfCgNlbmQYAiABKAsyEi5hZ2VudC52MS5Q" + + "b3NpdGlvbiIoCghQb3NpdGlvbhIMCgRsaW5lGAEgASgNEg4KBmNvbHVtbhgCIAEoDSIYCgVFcnJv" + + "chIPCgdtZXNzYWdlGAEgASgJIjoKDVdlYlNlYXJjaEFyZ3MSEwoLc2VhcmNoX3Rlcm0YASABKAkS" + + "FAoMdG9vbF9jYWxsX2lkGAIgASgJIqYBCg9XZWJTZWFyY2hSZXN1bHQSLQoHc3VjY2VzcxgBIAEo" + + "CzIaLmFnZW50LnYxLldlYlNlYXJjaFN1Y2Nlc3NIABIpCgVlcnJvchgCIAEoCzIYLmFnZW50LnYx" + + "LldlYlNlYXJjaEVycm9ySAASLwoIcmVqZWN0ZWQYAyABKAsyGy5hZ2VudC52MS5XZWJTZWFyY2hS" + + "ZWplY3RlZEgAQggKBnJlc3VsdCJEChBXZWJTZWFyY2hTdWNjZXNzEjAKCnJlZmVyZW5jZXMYASAD" + + "KAsyHC5hZ2VudC52MS5XZWJTZWFyY2hSZWZlcmVuY2UiHwoOV2ViU2VhcmNoRXJyb3ISDQoFZXJy" + + "b3IYASABKAkiIwoRV2ViU2VhcmNoUmVqZWN0ZWQSDgoGcmVhc29uGAEgASgJIj8KEldlYlNlYXJj" + + "aFJlZmVyZW5jZRINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkSDQoFY2h1bmsYAyABKAkiZQoR" + + "V2ViU2VhcmNoVG9vbENhbGwSJQoEYXJncxgBIAEoCzIXLmFnZW50LnYxLldlYlNlYXJjaEFyZ3MS" + + "KQoGcmVzdWx0GAIgASgLMhkuYWdlbnQudjEuV2ViU2VhcmNoUmVzdWx0Ij4KFVdlYlNlYXJjaFJl" + + "cXVlc3RRdWVyeRIlCgRhcmdzGAEgASgLMhcuYWdlbnQudjEuV2ViU2VhcmNoQXJncyKmAQoYV2Vi" + + "U2VhcmNoUmVxdWVzdFJlc3BvbnNlEj8KCGFwcHJvdmVkGAEgASgLMisuYWdlbnQudjEuV2ViU2Vh" + + "cmNoUmVxdWVzdFJlc3BvbnNlX0FwcHJvdmVkSAASPwoIcmVqZWN0ZWQYAiABKAsyKy5hZ2VudC52" + + "MS5XZWJTZWFyY2hSZXF1ZXN0UmVzcG9uc2VfUmVqZWN0ZWRIAEIICgZyZXN1bHQiIwohV2ViU2Vh" + + "cmNoUmVxdWVzdFJlc3BvbnNlX0FwcHJvdmVkIjMKIVdlYlNlYXJjaFJlcXVlc3RSZXNwb25zZV9S" + + "ZWplY3RlZBIOCgZyZWFzb24YASABKAkifwoJV3JpdGVBcmdzEgwKBHBhdGgYASABKAkSEQoJZmls" + + "ZV90ZXh0GAIgASgJEhQKDHRvb2xfY2FsbF9pZBgDIAEoCRInCh9yZXR1cm5fZmlsZV9jb250ZW50" + + "X2FmdGVyX3dyaXRlGAQgASgIEhIKCmZpbGVfYnl0ZXMYBSABKAwigAIKC1dyaXRlUmVzdWx0EikK" + + "B3N1Y2Nlc3MYASABKAsyFi5hZ2VudC52MS5Xcml0ZVN1Y2Nlc3NIABI8ChFwZXJtaXNzaW9uX2Rl" + + "bmllZBgDIAEoCzIfLmFnZW50LnYxLldyaXRlUGVybWlzc2lvbkRlbmllZEgAEioKCG5vX3NwYWNl" + + "GAQgASgLMhYuYWdlbnQudjEuV3JpdGVOb1NwYWNlSAASJQoFZXJyb3IYBSABKAsyFC5hZ2VudC52" + + "MS5Xcml0ZUVycm9ySAASKwoIcmVqZWN0ZWQYBiABKAsyFy5hZ2VudC52MS5Xcml0ZVJlamVjdGVk" + + "SABCCAoGcmVzdWx0IooBCgxXcml0ZVN1Y2Nlc3MSDAoEcGF0aBgBIAEoCRIVCg1saW5lc19jcmVh" + + "dGVkGAIgASgFEhEKCWZpbGVfc2l6ZRgDIAEoBRIlChhmaWxlX2NvbnRlbnRfYWZ0ZXJfd3JpdGUY" + + "BCABKAlIAIgBAUIbChlfZmlsZV9jb250ZW50X2FmdGVyX3dyaXRlIm8KFVdyaXRlUGVybWlzc2lv" + + "bkRlbmllZBIMCgRwYXRoGAEgASgJEhEKCWRpcmVjdG9yeRgCIAEoCRIRCglvcGVyYXRpb24YAyAB" + + "KAkSDQoFZXJyb3IYBCABKAkSEwoLaXNfcmVhZG9ubHkYBSABKAgiHAoMV3JpdGVOb1NwYWNlEgwK" + + "BHBhdGgYASABKAkiKQoKV3JpdGVFcnJvchIMCgRwYXRoGAEgASgJEg0KBWVycm9yGAIgASgJIi0K" + + "DVdyaXRlUmVqZWN0ZWQSDAoEcGF0aBgBIAEoCRIOCgZyZWFzb24YAiABKAkigwEKF0Jvb3RzdHJh" + + "cFN0YXRzaWdSZXF1ZXN0Eh4KEWlnbm9yZV9kZXZfc3RhdHVzGAEgASgISACIAQESHQoQb3BlcmF0" + + "aW5nX3N5c3RlbRgCIAEoBUgBiAEBQhQKEl9pZ25vcmVfZGV2X3N0YXR1c0ITChFfb3BlcmF0aW5n" + + "X3N5c3RlbSIOCgxQaW5nUmVzcG9uc2UitwEKC0V4ZWNSZXF1ZXN0Eg8KB2NvbW1hbmQYASABKAkS" + + "EAoDY3dkGAIgASgJSACIAQESDAoEYXJncxgDIAMoCRI7CgtlbnZpcm9ubWVudBgEIAMoCzImLmFn" + + "ZW50LnYxLkV4ZWNSZXF1ZXN0LkVudmlyb25tZW50RW50cnkaMgoQRW52aXJvbm1lbnRFbnRyeRIL" + + "CgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBQgYKBF9jd2QioAEKDEV4ZWNSZXNwb25zZRIt" + + "CgxzdGRvdXRfZXZlbnQYASABKAsyFS5hZ2VudC52MS5TdGRvdXRFdmVudEgAEi0KDHN0ZGVycl9l" + + "dmVudBgCIAEoCzIVLmFnZW50LnYxLlN0ZGVyckV2ZW50SAASKQoKZXhpdF9ldmVudBgDIAEoCzIT" + + "LmFnZW50LnYxLkV4aXRFdmVudEgAQgcKBWV2ZW50IhsKC1N0ZG91dEV2ZW50EgwKBGRhdGEYASAB" + + "KAkiGwoLU3RkZXJyRXZlbnQSDAoEZGF0YRgBIAEoCSIeCglFeGl0RXZlbnQSEQoJZXhpdF9jb2Rl" + + "GAEgASgFIiMKE1JlYWRUZXh0RmlsZVJlcXVlc3QSDAoEcGF0aBgBIAEoCSInChRSZWFkVGV4dEZp" + + "bGVSZXNwb25zZRIPCgdjb250ZW50GAEgASgJIjUKFFdyaXRlVGV4dEZpbGVSZXF1ZXN0EgwKBHBh" + + "dGgYASABKAkSDwoHY29udGVudBgCIAEoCSIXChVXcml0ZVRleHRGaWxlUmVzcG9uc2UiJQoVUmVh" + + "ZEJpbmFyeUZpbGVSZXF1ZXN0EgwKBHBhdGgYASABKAkiKQoWUmVhZEJpbmFyeUZpbGVSZXNwb25z" + + "ZRIPCgdjb250ZW50GAEgASgMIjcKFldyaXRlQmluYXJ5RmlsZVJlcXVlc3QSDAoEcGF0aBgBIAEo" + + "CRIPCgdjb250ZW50GAIgASgMIhkKF1dyaXRlQmluYXJ5RmlsZVJlc3BvbnNlIkUKHkdldFdvcmtz" + + "cGFjZUNoYW5nZXNIYXNoUmVxdWVzdBIRCglyb290X3BhdGgYASABKAkSEAoIYmFzZV9yZWYYAiAB" + + "KAkiLwofR2V0V29ya3NwYWNlQ2hhbmdlc0hhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIlAKH1Jl" + + "ZnJlc2hHaXRodWJBY2Nlc3NUb2tlblJlcXVlc3QSGwoTZ2l0aHViX2FjY2Vzc190b2tlbhgBIAEo" + + "CRIQCghob3N0bmFtZRgCIAEoCSIiCiBSZWZyZXNoR2l0aHViQWNjZXNzVG9rZW5SZXNwb25zZSJX" + + "Ch1XYXJtUmVtb3RlQWNjZXNzU2VydmVyUmVxdWVzdBIOCgZjb21taXQYASABKAkSDAoEcG9ydBgC" + + "IAEoBRIYChBjb25uZWN0aW9uX3Rva2VuGAMgASgJIiAKHldhcm1SZW1vdGVBY2Nlc3NTZXJ2ZXJS" + + "ZXNwb25zZSIWChRMaXN0QXJ0aWZhY3RzUmVxdWVzdCKKAgoWQXJ0aWZhY3RVcGxvYWRNZXRhZGF0" + + "YRIVCg1hYnNvbHV0ZV9wYXRoGAEgASgJEhIKCnNpemVfYnl0ZXMYAiABKAQSGgoSdXBkYXRlZF9h" + + "dF91bml4X21zGAMgASgDEg4KBnN0YXR1cxgEIAEoBRIWCg5ieXRlc191cGxvYWRlZBgFIAEoBBIS" + + "CgpsYXN0X2Vycm9yGAYgASgJEhcKD3VwbG9hZF9hdHRlbXB0cxgHIAEoDRIfChdsYXN0X3N0YXJ0" + + "ZWRfYXRfdW5peF9tcxgIIAEoAxIgChhsYXN0X2ZpbmlzaGVkX2F0X3VuaXhfbXMYCSABKAMSEQoJ" + + "dXBsb2FkX2lkGAogASgJIkwKFUxpc3RBcnRpZmFjdHNSZXNwb25zZRIzCglhcnRpZmFjdHMYASAD" + + "KAsyIC5hZ2VudC52MS5BcnRpZmFjdFVwbG9hZE1ldGFkYXRhIk4KFlVwbG9hZEFydGlmYWN0c1Jl" + + "cXVlc3QSNAoHdXBsb2FkcxgBIAMoCzIjLmFnZW50LnYxLkFydGlmYWN0VXBsb2FkSW5zdHJ1Y3Rp" + + "b24i1wIKGUFydGlmYWN0VXBsb2FkSW5zdHJ1Y3Rpb24SFQoNYWJzb2x1dGVfcGF0aBgBIAEoCRIS" + + "Cgp1cGxvYWRfdXJsGAIgASgJEg4KBm1ldGhvZBgDIAEoCRJBCgdoZWFkZXJzGAQgAygLMjAuYWdl" + + "bnQudjEuQXJ0aWZhY3RVcGxvYWRJbnN0cnVjdGlvbi5IZWFkZXJzRW50cnkSGQoMY29udGVudF90" + + "eXBlGAUgASgJSACIAQESHQoQc2xhY2tfdXBsb2FkX3VybBgGIAEoCUgBiAEBEhoKDXNsYWNrX2Zp" + + "bGVfaWQYByABKAlIAogBARouCgxIZWFkZXJzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIg" + + "ASgJOgI4AUIPCg1fY29udGVudF90eXBlQhMKEV9zbGFja191cGxvYWRfdXJsQhAKDl9zbGFja19m" + + "aWxlX2lkIoQBChxBcnRpZmFjdFVwbG9hZERpc3BhdGNoUmVzdWx0EhUKDWFic29sdXRlX3BhdGgY" + + "ASABKAkSDgoGc3RhdHVzGAIgASgFEg8KB21lc3NhZ2UYAyABKAkSGgoNc2xhY2tfZmlsZV9pZBgE" + + "IAEoCUgAiAEBQhAKDl9zbGFja19maWxlX2lkIlIKF1VwbG9hZEFydGlmYWN0c1Jlc3BvbnNlEjcK" + + "B3Jlc3VsdHMYASADKAsyJi5hZ2VudC52MS5BcnRpZmFjdFVwbG9hZERpc3BhdGNoUmVzdWx0IhwK" + + "GkdldE1jcFJlZnJlc2hUb2tlbnNSZXF1ZXN0IqUBChtHZXRNY3BSZWZyZXNoVG9rZW5zUmVzcG9u" + + "c2USUAoOcmVmcmVzaF90b2tlbnMYASADKAsyOC5hZ2VudC52MS5HZXRNY3BSZWZyZXNoVG9rZW5z" + + "UmVzcG9uc2UuUmVmcmVzaFRva2Vuc0VudHJ5GjQKElJlZnJlc2hUb2tlbnNFbnRyeRILCgNrZXkY" + + "ASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIqMBCiFVcGRhdGVFbnZpcm9ubWVudFZhcmlhYmxlc1Jl" + + "cXVlc3QSQQoDZW52GAEgAygLMjQuYWdlbnQudjEuVXBkYXRlRW52aXJvbm1lbnRWYXJpYWJsZXNS" + + "ZXF1ZXN0LkVudkVudHJ5Eg8KB3JlcGxhY2UYAiABKAgaKgoIRW52RW50cnkSCwoDa2V5GAEgASgJ" + + "Eg0KBXZhbHVlGAIgASgJOgI4ASJGCiJVcGRhdGVFbnZpcm9ubWVudFZhcmlhYmxlc1Jlc3BvbnNl" + + "Eg8KB2FwcGxpZWQYASABKA0SDwoHcmVtb3ZlZBgCIAEoDSKDAQoSTWNwT0F1dGhTdG9yZWREYXRh" + + "EhUKDXJlZnJlc2hfdG9rZW4YASABKAkSEQoJY2xpZW50X2lkGAIgASgJEhoKDWNsaWVudF9zZWNy" + + "ZXQYAyABKAlIAIgBARIVCg1yZWRpcmVjdF91cmlzGAQgAygJQhAKDl9jbGllbnRfc2VjcmV0Ik4K" + + "BUZyYW1lEgoKAmlkGAEgASgJEg4KBm1ldGhvZBgCIAEoCRIMCgRkYXRhGAMgASgMEgwKBGtpbmQY" + + "BCABKAUSDQoFZXJyb3IYBSABKAkiBwoFRW1wdHkiIwoNQmlkaVJlcXVlc3RJZBISCgpyZXF1ZXN0" + + "X2lkGAEgASgJKogBCh1BcHBsaWVkQWdlbnRDaGFuZ2VfQ2hhbmdlVHlwZRIbChdDSEFOR0VfVFlQ" + + "RV9VTlNQRUNJRklFRBAAEhcKE0NIQU5HRV9UWVBFX0NSRUFURUQQARIYChRDSEFOR0VfVFlQRV9N" + + "T0RJRklFRBACEhcKE0NIQU5HRV9UWVBFX0RFTEVURUQQAyqkAQoLTW91c2VCdXR0b24SHAoYTU9V" + + "U0VfQlVUVE9OX1VOU1BFQ0lGSUVEEAASFQoRTU9VU0VfQlVUVE9OX0xFRlQQARIWChJNT1VTRV9C" + + "VVRUT05fUklHSFQQAhIXChNNT1VTRV9CVVRUT05fTUlERExFEAMSFQoRTU9VU0VfQlVUVE9OX0JB" + + "Q0sQBBIYChRNT1VTRV9CVVRUT05fRk9SV0FSRBAFKp4BCg9TY3JvbGxEaXJlY3Rpb24SIAocU0NS" + + "T0xMX0RJUkVDVElPTl9VTlNQRUNJRklFRBAAEhcKE1NDUk9MTF9ESVJFQ1RJT05fVVAQARIZChVT" + + "Q1JPTExfRElSRUNUSU9OX0RPV04QAhIZChVTQ1JPTExfRElSRUNUSU9OX0xFRlQQAxIaChZTQ1JP" + + "TExfRElSRUNUSU9OX1JJR0hUEAQqcAoQQ3Vyc29yUnVsZVNvdXJjZRIiCh5DVVJTT1JfUlVMRV9T" + + "T1VSQ0VfVU5TUEVDSUZJRUQQABIbChdDVVJTT1JfUlVMRV9TT1VSQ0VfVEVBTRABEhsKF0NVUlNP" + + "Ul9SVUxFX1NPVVJDRV9VU0VSEAIqvAEKEkRpYWdub3N0aWNTZXZlcml0eRIjCh9ESUFHTk9TVElD" + + "X1NFVkVSSVRZX1VOU1BFQ0lGSUVEEAASHQoZRElBR05PU1RJQ19TRVZFUklUWV9FUlJPUhABEh8K" + + "G0RJQUdOT1NUSUNfU0VWRVJJVFlfV0FSTklORxACEiMKH0RJQUdOT1NUSUNfU0VWRVJJVFlfSU5G" + + "T1JNQVRJT04QAxIcChhESUFHTk9TVElDX1NFVkVSSVRZX0hJTlQQBCqcAQoNUmVjb3JkaW5nTW9k" + + "ZRIeChpSRUNPUkRJTkdfTU9ERV9VTlNQRUNJRklFRBAAEiIKHlJFQ09SRElOR19NT0RFX1NUQVJU" + + "X1JFQ09SRElORxABEiEKHVJFQ09SRElOR19NT0RFX1NBVkVfUkVDT1JESU5HEAISJAogUkVDT1JE" + + "SU5HX01PREVfRElTQ0FSRF9SRUNPUkRJTkcQAyqTAQofUmVxdWVzdGVkRmlsZVBhdGhSZWplY3Rl" + + "ZFJlYXNvbhIzCi9SRVFVRVNURURfRklMRV9QQVRIX1JFSkVDVEVEX1JFQVNPTl9VTlNQRUNJRklF" + + "RBAAEjsKN1JFUVVFU1RFRF9GSUxFX1BBVEhfUkVKRUNURURfUkVBU09OX1NMQVNIRVNfTk9UX0FM" + + "TE9XRUQQASqtAQoLUGFja2FnZVR5cGUSHAoYUEFDS0FHRV9UWVBFX1VOU1BFQ0lGSUVEEAASHwob" + + "UEFDS0FHRV9UWVBFX0NVUlNPUl9QUk9KRUNUEAESIAocUEFDS0FHRV9UWVBFX0NVUlNPUl9QRVJT" + + "T05BTBACEh0KGVBBQ0tBR0VfVFlQRV9DTEFVREVfU0tJTEwQAxIeChpQQUNLQUdFX1RZUEVfQ0xB" + + "VURFX1BMVUdJThAEKn0KElNhbmRib3hQb2xpY3lfVHlwZRIUChBUWVBFX1VOU1BFQ0lGSUVEEAAS" + + "FgoSVFlQRV9JTlNFQ1VSRV9OT05FEAESHAoYVFlQRV9XT1JLU1BBQ0VfUkVBRFdSSVRFEAISGwoX" + + "VFlQRV9XT1JLU1BBQ0VfUkVBRE9OTFkQAypxCg9UaW1lb3V0QmVoYXZpb3ISIAocVElNRU9VVF9C" + + "RUhBVklPUl9VTlNQRUNJRklFRBAAEhsKF1RJTUVPVVRfQkVIQVZJT1JfQ0FOQ0VMEAESHwobVElN" + + "RU9VVF9CRUhBVklPUl9CQUNLR1JPVU5EEAIqeQoQU2hlbGxBYm9ydFJlYXNvbhIiCh5TSEVMTF9B" + + "Qk9SVF9SRUFTT05fVU5TUEVDSUZJRUQQABIhCh1TSEVMTF9BQk9SVF9SRUFTT05fVVNFUl9BQk9S" + + "VBABEh4KGlNIRUxMX0FCT1JUX1JFQVNPTl9USU1FT1VUEAIqqgEKHEN1c3RvbVN1YmFnZW50UGVy" + + "bWlzc2lvbk1vZGUSLworQ1VTVE9NX1NVQkFHRU5UX1BFUk1JU1NJT05fTU9ERV9VTlNQRUNJRklF" + + "RBAAEisKJ0NVU1RPTV9TVUJBR0VOVF9QRVJNSVNTSU9OX01PREVfREVGQVVMVBABEiwKKENVU1RP" + + "TV9TVUJBR0VOVF9QRVJNSVNTSU9OX01PREVfUkVBRE9OTFkQAiqVAQoKVG9kb1N0YXR1cxIbChdU" + + "T0RPX1NUQVRVU19VTlNQRUNJRklFRBAAEhcKE1RPRE9fU1RBVFVTX1BFTkRJTkcQARIbChdUT0RP" + + "X1NUQVRVU19JTl9QUk9HUkVTUxACEhkKFVRPRE9fU1RBVFVTX0NPTVBMRVRFRBADEhkKFVRPRE9f" + + "U1RBVFVTX0NBTkNFTExFRBAEKmYKCENsaWVudE9TEhkKFUNMSUVOVF9PU19VTlNQRUNJRklFRBAA" + + "EhUKEUNMSUVOVF9PU19XSU5ET1dTEAESEwoPQ0xJRU5UX09TX01BQ09TEAISEwoPQ0xJRU5UX09T" + + "X0xJTlVYEAMq7AEKHEFydGlmYWN0VXBsb2FkRGlzcGF0Y2hTdGF0dXMSLworQVJUSUZBQ1RfVVBM" + + "T0FEX0RJU1BBVENIX1NUQVRVU19VTlNQRUNJRklFRBAAEiwKKEFSVElGQUNUX1VQTE9BRF9ESVNQ" + + "QVRDSF9TVEFUVVNfQUNDRVBURUQQARIsCihBUlRJRkFDVF9VUExPQURfRElTUEFUQ0hfU1RBVFVT" + + "X1JFSkVDVEVEEAISPwo7QVJUSUZBQ1RfVVBMT0FEX0RJU1BBVENIX1NUQVRVU19TS0lQUEVEX0FM" + + "UkVBRFlfSU5fUFJPR1JFU1MQAypXCgpGcmFtZV9LaW5kEhQKEEtJTkRfVU5TUEVDSUZJRUQQABIQ" + + "CgxLSU5EX1JFUVVFU1QQARIRCg1LSU5EX1JFU1BPTlNFEAISDgoKS0lORF9FUlJPUhADKrACChdC" + + "dWdib3REZWVwbGlua0V2ZW50S2luZBIqCiZCVUdCT1RfREVFUExJTktfRVZFTlRfS0lORF9VTlNQ" + + "RUNJRklFRBAAEiYKIkJVR0JPVF9ERUVQTElOS19FVkVOVF9LSU5EX0NMSUNLRUQQARIzCi9CVUdC" + + "T1RfREVFUExJTktfRVZFTlRfS0lORF9IQU5ETEVEX0RJQUxPR19TSE9XThACEjMKL0JVR0JPVF9E" + + "RUVQTElOS19FVkVOVF9LSU5EX0hBTkRMRURfQ0hBVF9DUkVBVEVEEAMSJAogQlVHQk9UX0RFRVBM" + + "SU5LX0VWRU5UX0tJTkRfRVJST1IQBBIxCi1CVUdCT1RfREVFUExJTktfRVZFTlRfS0lORF9IQU5E" + + "TEVEX0ZJWF9JTl9XRUIQBTKHBAoMQWdlbnRTZXJ2aWNlEkEKA1J1bhIcLmFnZW50LnYxLkFnZW50" + + "Q2xpZW50TWVzc2FnZRocLmFnZW50LnYxLkFnZW50U2VydmVyTWVzc2FnZRI/CgZSdW5TU0USFy5h" + + "Z2VudC52MS5CaWRpUmVxdWVzdElkGhwuYWdlbnQudjEuQWdlbnRTZXJ2ZXJNZXNzYWdlEkQKCU5h" + + "bWVBZ2VudBIaLmFnZW50LnYxLk5hbWVBZ2VudFJlcXVlc3QaGy5hZ2VudC52MS5OYW1lQWdlbnRS" + + "ZXNwb25zZRJWCg9HZXRVc2FibGVNb2RlbHMSIC5hZ2VudC52MS5HZXRVc2FibGVNb2RlbHNSZXF1" + + "ZXN0GiEuYWdlbnQudjEuR2V0VXNhYmxlTW9kZWxzUmVzcG9uc2USaAoVR2V0RGVmYXVsdE1vZGVs" + + "Rm9yQ2xpEiYuYWdlbnQudjEuR2V0RGVmYXVsdE1vZGVsRm9yQ2xpUmVxdWVzdBonLmFnZW50LnYx" + + "LkdldERlZmF1bHRNb2RlbEZvckNsaVJlc3BvbnNlEmsKFkdldEFsbG93ZWRNb2RlbEludGVudHMS" + + "Jy5hZ2VudC52MS5HZXRBbGxvd2VkTW9kZWxJbnRlbnRzUmVxdWVzdBooLmFnZW50LnYxLkdldEFs" + + "bG93ZWRNb2RlbEludGVudHNSZXNwb25zZTK1CAoOQ29udHJvbFNlcnZpY2USTQoMUmVhZFRleHRG" + + "aWxlEh0uYWdlbnQudjEuUmVhZFRleHRGaWxlUmVxdWVzdBoeLmFnZW50LnYxLlJlYWRUZXh0Rmls" + + "ZVJlc3BvbnNlElAKDVdyaXRlVGV4dEZpbGUSHi5hZ2VudC52MS5Xcml0ZVRleHRGaWxlUmVxdWVz" + + "dBofLmFnZW50LnYxLldyaXRlVGV4dEZpbGVSZXNwb25zZRJTCg5SZWFkQmluYXJ5RmlsZRIfLmFn" + + "ZW50LnYxLlJlYWRCaW5hcnlGaWxlUmVxdWVzdBogLmFnZW50LnYxLlJlYWRCaW5hcnlGaWxlUmVz" + + "cG9uc2USVgoPV3JpdGVCaW5hcnlGaWxlEiAuYWdlbnQudjEuV3JpdGVCaW5hcnlGaWxlUmVxdWVz" + + "dBohLmFnZW50LnYxLldyaXRlQmluYXJ5RmlsZVJlc3BvbnNlEm4KF0dldFdvcmtzcGFjZUNoYW5n" + + "ZXNIYXNoEiguYWdlbnQudjEuR2V0V29ya3NwYWNlQ2hhbmdlc0hhc2hSZXF1ZXN0GikuYWdlbnQu" + + "djEuR2V0V29ya3NwYWNlQ2hhbmdlc0hhc2hSZXNwb25zZRJxChhSZWZyZXNoR2l0aHViQWNjZXNz" + + "VG9rZW4SKS5hZ2VudC52MS5SZWZyZXNoR2l0aHViQWNjZXNzVG9rZW5SZXF1ZXN0GiouYWdlbnQu" + + "djEuUmVmcmVzaEdpdGh1YkFjY2Vzc1Rva2VuUmVzcG9uc2USawoWV2FybVJlbW90ZUFjY2Vzc1Nl" + + "cnZlchInLmFnZW50LnYxLldhcm1SZW1vdGVBY2Nlc3NTZXJ2ZXJSZXF1ZXN0GiguYWdlbnQudjEu" + + "V2FybVJlbW90ZUFjY2Vzc1NlcnZlclJlc3BvbnNlElAKDUxpc3RBcnRpZmFjdHMSHi5hZ2VudC52" + + "MS5MaXN0QXJ0aWZhY3RzUmVxdWVzdBofLmFnZW50LnYxLkxpc3RBcnRpZmFjdHNSZXNwb25zZRJW" + + "Cg9VcGxvYWRBcnRpZmFjdHMSIC5hZ2VudC52MS5VcGxvYWRBcnRpZmFjdHNSZXF1ZXN0GiEuYWdl" + + "bnQudjEuVXBsb2FkQXJ0aWZhY3RzUmVzcG9uc2USYgoTR2V0TWNwUmVmcmVzaFRva2VucxIkLmFn" + + "ZW50LnYxLkdldE1jcFJlZnJlc2hUb2tlbnNSZXF1ZXN0GiUuYWdlbnQudjEuR2V0TWNwUmVmcmVz" + + "aFRva2Vuc1Jlc3BvbnNlEncKGlVwZGF0ZUVudmlyb25tZW50VmFyaWFibGVzEisuYWdlbnQudjEu" + + "VXBkYXRlRW52aXJvbm1lbnRWYXJpYWJsZXNSZXF1ZXN0GiwuYWdlbnQudjEuVXBkYXRlRW52aXJv" + + "bm1lbnRWYXJpYWJsZXNSZXNwb25zZTINCgtFeGVjU2VydmljZTJRCiJQcml2YXRlV29ya2VyQnJp" + + "ZGdlRXh0ZXJuYWxTZXJ2aWNlEisKB0Nvbm5lY3QSDy5hZ2VudC52MS5GcmFtZRoPLmFnZW50LnYx" + + "LkZyYW1lMngKEExpZmVjeWNsZVNlcnZpY2USMQoNUmVzZXRJbnN0YW5jZRIPLmFnZW50LnYxLkVt" + + "cHR5Gg8uYWdlbnQudjEuRW1wdHkSMQoNUmVuZXdJbnN0YW5jZRIPLmFnZW50LnYxLkVtcHR5Gg8u" + + "YWdlbnQudjEuRW1wdHliBnByb3RvMw==" + +var ( + fileDescOnce sync.Once + fileDesc protoreflect.FileDescriptor +) + +// AgentFileDescriptor returns the parsed FileDescriptor for agent.proto. +func AgentFileDescriptor() protoreflect.FileDescriptor { + fileDescOnce.Do(func() { + raw, err := base64.StdEncoding.DecodeString(agentDescriptorB64) + if err != nil { + panic("cursor proto: failed to decode descriptor: " + err.Error()) + } + fdp := &descrptorpb.FileDescriptorProto{} + if err := proto.Unmarshal(raw, fdp); err != nil { + panic("cursor proto: failed to unmarshal descriptor: " + err.Error()) + } + fd, err := protodesc.NewFile(fdp, nil) + if err != nil { + panic("cursor proto: failed to create file descriptor: " + err.Error()) + } + fileDesc = fd + }) + return fileDesc +} + +// Msg returns the MessageDescriptor for a top-level message by name. +func Msg(name string) protoreflect.MessageDescriptor { + md := AgentFileDescriptor().Messages().ByName(protoreflect.Name(name)) + if md == nil { + panic("cursor proto: message not found: " + name) + } + return md +} diff --git a/internal/auth/cursor/proto/encode.go b/internal/auth/cursor/proto/encode.go new file mode 100644 index 00000000..8ee2910f --- /dev/null +++ b/internal/auth/cursor/proto/encode.go @@ -0,0 +1,491 @@ +// Package proto provides protobuf encoding for Cursor's gRPC API, +// using dynamicpb with the embedded FileDescriptorProto from agent.proto. +// This mirrors the cursor-auth TS plugin's use of @bufbuild/protobuf create()+toBinary(). +package proto + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/dynamicpb" + "google.golang.org/protobuf/types/known/structpb" +) + +// --- Public types --- + +// RunRequestParams holds all data needed to build an AgentRunRequest. +type RunRequestParams struct { + ModelId string + SystemPrompt string + UserText string + MessageId string + ConversationId string + Images []ImageData + Turns []TurnData + McpTools []McpToolDef + BlobStore map[string][]byte // hex(sha256) -> data, populated during encoding +} + +type ImageData struct { + MimeType string + Data []byte +} + +type TurnData struct { + UserText string + AssistantText string +} + +type McpToolDef struct { + Name string + Description string + InputSchema json.RawMessage +} + +// --- Helper: create a dynamic message and set fields --- + +func newMsg(name string) *dynamicpb.Message { + return dynamicpb.NewMessage(Msg(name)) +} + +func field(msg *dynamicpb.Message, name string) protoreflect.FieldDescriptor { + return msg.Descriptor().Fields().ByName(protoreflect.Name(name)) +} + +func setStr(msg *dynamicpb.Message, name, val string) { + if val != "" { + msg.Set(field(msg, name), protoreflect.ValueOfString(val)) + } +} + +func setBytes(msg *dynamicpb.Message, name string, val []byte) { + if len(val) > 0 { + msg.Set(field(msg, name), protoreflect.ValueOfBytes(val)) + } +} + +func setUint32(msg *dynamicpb.Message, name string, val uint32) { + msg.Set(field(msg, name), protoreflect.ValueOfUint32(val)) +} + +func setBool(msg *dynamicpb.Message, name string, val bool) { + msg.Set(field(msg, name), protoreflect.ValueOfBool(val)) +} + +func setMsg(msg *dynamicpb.Message, name string, sub *dynamicpb.Message) { + msg.Set(field(msg, name), protoreflect.ValueOfMessage(sub.ProtoReflect())) +} + +func marshal(msg *dynamicpb.Message) []byte { + b, err := proto.Marshal(msg) + if err != nil { + panic("cursor proto marshal: " + err.Error()) + } + return b +} + +// --- Encode functions mirroring cursor-fetch.ts --- + +// EncodeHeartbeat returns an encoded AgentClientMessage with clientHeartbeat. +// Mirrors: create(AgentClientMessageSchema, { message: { case: 'clientHeartbeat', value: create(ClientHeartbeatSchema, {}) } }) +func EncodeHeartbeat() []byte { + hb := newMsg("ClientHeartbeat") + acm := newMsg("AgentClientMessage") + setMsg(acm, "client_heartbeat", hb) + return marshal(acm) +} + +// EncodeRunRequest builds a full AgentClientMessage wrapping an AgentRunRequest. +// Mirrors buildCursorRequest() in cursor-fetch.ts. +func EncodeRunRequest(p *RunRequestParams) []byte { + if p.BlobStore == nil { + p.BlobStore = make(map[string][]byte) + } + + // --- Conversation turns --- + // Each turn is serialized as bytes (ConversationTurnStructure → bytes) + var turnBytes [][]byte + for _, turn := range p.Turns { + // UserMessage for this turn + um := newMsg("UserMessage") + setStr(um, "text", turn.UserText) + setStr(um, "message_id", generateId()) + umBytes := marshal(um) + + // Steps (assistant response) + var stepBytes [][]byte + if turn.AssistantText != "" { + am := newMsg("AssistantMessage") + setStr(am, "text", turn.AssistantText) + step := newMsg("ConversationStep") + setMsg(step, "assistant_message", am) + stepBytes = append(stepBytes, marshal(step)) + } + + // AgentConversationTurnStructure (fields are bytes, not submessages) + agentTurn := newMsg("AgentConversationTurnStructure") + setBytes(agentTurn, "user_message", umBytes) + for _, sb := range stepBytes { + stepsField := field(agentTurn, "steps") + list := agentTurn.Mutable(stepsField).List() + list.Append(protoreflect.ValueOfBytes(sb)) + } + + // ConversationTurnStructure (oneof turn → agentConversationTurn) + cts := newMsg("ConversationTurnStructure") + setMsg(cts, "agent_conversation_turn", agentTurn) + turnBytes = append(turnBytes, marshal(cts)) + } + + // --- System prompt blob --- + systemJSON, _ := json.Marshal(map[string]string{"role": "system", "content": p.SystemPrompt}) + blobId := sha256Sum(systemJSON) + p.BlobStore[hex.EncodeToString(blobId)] = systemJSON + + // --- ConversationStateStructure --- + css := newMsg("ConversationStateStructure") + // rootPromptMessagesJson: repeated bytes + rootField := field(css, "root_prompt_messages_json") + rootList := css.Mutable(rootField).List() + rootList.Append(protoreflect.ValueOfBytes(blobId)) + // turns: repeated bytes + turnsField := field(css, "turns") + turnsList := css.Mutable(turnsField).List() + for _, tb := range turnBytes { + turnsList.Append(protoreflect.ValueOfBytes(tb)) + } + + // --- UserMessage (current) --- + userMessage := newMsg("UserMessage") + setStr(userMessage, "text", p.UserText) + setStr(userMessage, "message_id", p.MessageId) + + // Images via SelectedContext + if len(p.Images) > 0 { + sc := newMsg("SelectedContext") + imgsField := field(sc, "selected_images") + imgsList := sc.Mutable(imgsField).List() + for _, img := range p.Images { + si := newMsg("SelectedImage") + setStr(si, "uuid", generateId()) + setStr(si, "mime_type", img.MimeType) + setBytes(si, "data", img.Data) + imgsList.Append(protoreflect.ValueOfMessage(si.ProtoReflect())) + } + setMsg(userMessage, "selected_context", sc) + } + + // --- UserMessageAction --- + uma := newMsg("UserMessageAction") + setMsg(uma, "user_message", userMessage) + + // --- ConversationAction --- + ca := newMsg("ConversationAction") + setMsg(ca, "user_message_action", uma) + + // --- ModelDetails --- + md := newMsg("ModelDetails") + setStr(md, "model_id", p.ModelId) + setStr(md, "display_model_id", p.ModelId) + setStr(md, "display_name", p.ModelId) + + // --- AgentRunRequest --- + arr := newMsg("AgentRunRequest") + setMsg(arr, "conversation_state", css) + setMsg(arr, "action", ca) + setMsg(arr, "model_details", md) + setStr(arr, "conversation_id", p.ConversationId) + + // McpTools + if len(p.McpTools) > 0 { + mcpTools := newMsg("McpTools") + toolsField := field(mcpTools, "mcp_tools") + toolsList := mcpTools.Mutable(toolsField).List() + for _, tool := range p.McpTools { + td := newMsg("McpToolDefinition") + setStr(td, "name", tool.Name) + setStr(td, "description", tool.Description) + if len(tool.InputSchema) > 0 { + setBytes(td, "input_schema", jsonToProtobufValueBytes(tool.InputSchema)) + } + setStr(td, "provider_identifier", "proxy") + setStr(td, "tool_name", tool.Name) + toolsList.Append(protoreflect.ValueOfMessage(td.ProtoReflect())) + } + setMsg(arr, "mcp_tools", mcpTools) + } + + // --- AgentClientMessage --- + acm := newMsg("AgentClientMessage") + setMsg(acm, "run_request", arr) + + return marshal(acm) +} + +// --- KV response encoders --- +// Mirrors handleKvMessage() in cursor-fetch.ts + +// EncodeKvGetBlobResult responds to a getBlobArgs request. +func EncodeKvGetBlobResult(kvId uint32, blobData []byte) []byte { + result := newMsg("GetBlobResult") + if blobData != nil { + setBytes(result, "blob_data", blobData) + } + + kvc := newMsg("KvClientMessage") + setUint32(kvc, "id", kvId) + setMsg(kvc, "get_blob_result", result) + + acm := newMsg("AgentClientMessage") + setMsg(acm, "kv_client_message", kvc) + return marshal(acm) +} + +// EncodeKvSetBlobResult responds to a setBlobArgs request. +func EncodeKvSetBlobResult(kvId uint32) []byte { + result := newMsg("SetBlobResult") + + kvc := newMsg("KvClientMessage") + setUint32(kvc, "id", kvId) + setMsg(kvc, "set_blob_result", result) + + acm := newMsg("AgentClientMessage") + setMsg(acm, "kv_client_message", kvc) + return marshal(acm) +} + +// --- Exec response encoders --- +// Mirrors handleExecMessage() and sendExec() in cursor-fetch.ts + +// EncodeExecRequestContextResult responds to requestContextArgs with tool definitions. +func EncodeExecRequestContextResult(execMsgId uint32, execId string, tools []McpToolDef) []byte { + // RequestContext with tools + rc := newMsg("RequestContext") + if len(tools) > 0 { + toolsField := field(rc, "tools") + toolsList := rc.Mutable(toolsField).List() + for _, tool := range tools { + td := newMsg("McpToolDefinition") + setStr(td, "name", tool.Name) + setStr(td, "description", tool.Description) + if len(tool.InputSchema) > 0 { + setBytes(td, "input_schema", jsonToProtobufValueBytes(tool.InputSchema)) + } + setStr(td, "provider_identifier", "proxy") + setStr(td, "tool_name", tool.Name) + toolsList.Append(protoreflect.ValueOfMessage(td.ProtoReflect())) + } + } + + // RequestContextSuccess + rcs := newMsg("RequestContextSuccess") + setMsg(rcs, "request_context", rc) + + // RequestContextResult (oneof success) + rcr := newMsg("RequestContextResult") + setMsg(rcr, "success", rcs) + + return encodeExecClientMsg(execMsgId, execId, "request_context_result", rcr) +} + +// EncodeExecMcpResult responds with MCP tool result. +func EncodeExecMcpResult(execMsgId uint32, execId string, content string, isError bool) []byte { + textContent := newMsg("McpTextContent") + setStr(textContent, "text", content) + + contentItem := newMsg("McpToolResultContentItem") + setMsg(contentItem, "text", textContent) + + success := newMsg("McpSuccess") + contentField := field(success, "content") + contentList := success.Mutable(contentField).List() + contentList.Append(protoreflect.ValueOfMessage(contentItem.ProtoReflect())) + setBool(success, "is_error", isError) + + result := newMsg("McpResult") + setMsg(result, "success", success) + + return encodeExecClientMsg(execMsgId, execId, "mcp_result", result) +} + +// EncodeExecMcpError responds with MCP error. +func EncodeExecMcpError(execMsgId uint32, execId string, errMsg string) []byte { + mcpErr := newMsg("McpError") + setStr(mcpErr, "error", errMsg) + + result := newMsg("McpResult") + setMsg(result, "error", mcpErr) + + return encodeExecClientMsg(execMsgId, execId, "mcp_result", result) +} + +// --- Rejection encoders (mirror handleExecMessage rejections) --- + +func EncodeExecReadRejected(execMsgId uint32, execId string, path, reason string) []byte { + rej := newMsg("ReadRejected") + setStr(rej, "path", path) + setStr(rej, "reason", reason) + result := newMsg("ReadResult") + setMsg(result, "rejected", rej) + return encodeExecClientMsg(execMsgId, execId, "read_result", result) +} + +func EncodeExecShellRejected(execMsgId uint32, execId string, command, workDir, reason string) []byte { + rej := newMsg("ShellRejected") + setStr(rej, "command", command) + setStr(rej, "working_directory", workDir) + setStr(rej, "reason", reason) + result := newMsg("ShellResult") + setMsg(result, "rejected", rej) + return encodeExecClientMsg(execMsgId, execId, "shell_result", result) +} + +func EncodeExecWriteRejected(execMsgId uint32, execId string, path, reason string) []byte { + rej := newMsg("WriteRejected") + setStr(rej, "path", path) + setStr(rej, "reason", reason) + result := newMsg("WriteResult") + setMsg(result, "rejected", rej) + return encodeExecClientMsg(execMsgId, execId, "write_result", result) +} + +func EncodeExecDeleteRejected(execMsgId uint32, execId string, path, reason string) []byte { + rej := newMsg("DeleteRejected") + setStr(rej, "path", path) + setStr(rej, "reason", reason) + result := newMsg("DeleteResult") + setMsg(result, "rejected", rej) + return encodeExecClientMsg(execMsgId, execId, "delete_result", result) +} + +func EncodeExecLsRejected(execMsgId uint32, execId string, path, reason string) []byte { + rej := newMsg("LsRejected") + setStr(rej, "path", path) + setStr(rej, "reason", reason) + result := newMsg("LsResult") + setMsg(result, "rejected", rej) + return encodeExecClientMsg(execMsgId, execId, "ls_result", result) +} + +func EncodeExecGrepError(execMsgId uint32, execId string, errMsg string) []byte { + grepErr := newMsg("GrepError") + setStr(grepErr, "error", errMsg) + result := newMsg("GrepResult") + setMsg(result, "error", grepErr) + return encodeExecClientMsg(execMsgId, execId, "grep_result", result) +} + +func EncodeExecFetchError(execMsgId uint32, execId string, url, errMsg string) []byte { + fetchErr := newMsg("FetchError") + setStr(fetchErr, "url", url) + setStr(fetchErr, "error", errMsg) + result := newMsg("FetchResult") + setMsg(result, "error", fetchErr) + return encodeExecClientMsg(execMsgId, execId, "fetch_result", result) +} + +func EncodeExecDiagnosticsResult(execMsgId uint32, execId string) []byte { + result := newMsg("DiagnosticsResult") + return encodeExecClientMsg(execMsgId, execId, "diagnostics_result", result) +} + +func EncodeExecBackgroundShellSpawnRejected(execMsgId uint32, execId string, command, workDir, reason string) []byte { + rej := newMsg("ShellRejected") + setStr(rej, "command", command) + setStr(rej, "working_directory", workDir) + setStr(rej, "reason", reason) + result := newMsg("BackgroundShellSpawnResult") + setMsg(result, "rejected", rej) + return encodeExecClientMsg(execMsgId, execId, "background_shell_spawn_result", result) +} + +func EncodeExecWriteShellStdinError(execMsgId uint32, execId string, errMsg string) []byte { + wsErr := newMsg("WriteShellStdinError") + setStr(wsErr, "error", errMsg) + result := newMsg("WriteShellStdinResult") + setMsg(result, "error", wsErr) + return encodeExecClientMsg(execMsgId, execId, "write_shell_stdin_result", result) +} + +// encodeExecClientMsg wraps an exec result in AgentClientMessage. +// Mirrors sendExec() in cursor-fetch.ts. +func encodeExecClientMsg(id uint32, execId string, resultFieldName string, resultMsg *dynamicpb.Message) []byte { + ecm := newMsg("ExecClientMessage") + setUint32(ecm, "id", id) + // Force set exec_id even if empty - Cursor requires this field to be set + ecm.Set(field(ecm, "exec_id"), protoreflect.ValueOfString(execId)) + + // Debug: check if field exists + fd := field(ecm, resultFieldName) + if fd == nil { + panic(fmt.Sprintf("field %q NOT FOUND in ExecClientMessage! Available fields: %v", resultFieldName, listFields(ecm))) + } + + // Debug: log the actual field being set + log.Debugf("encodeExecClientMsg: setting field %q (number=%d, kind=%s)", fd.Name(), fd.Number(), fd.Kind()) + + ecm.Set(fd, protoreflect.ValueOfMessage(resultMsg.ProtoReflect())) + + acm := newMsg("AgentClientMessage") + setMsg(acm, "exec_client_message", ecm) + return marshal(acm) +} + +func listFields(msg *dynamicpb.Message) []string { + var names []string + for i := 0; i < msg.Descriptor().Fields().Len(); i++ { + names = append(names, string(msg.Descriptor().Fields().Get(i).Name())) + } + return names +} + +// --- Utilities --- + +// jsonToProtobufValueBytes converts a JSON schema (json.RawMessage) to protobuf Value binary. +// This mirrors the TS pattern: toBinary(ValueSchema, fromJson(ValueSchema, jsonSchema)) +func jsonToProtobufValueBytes(jsonData json.RawMessage) []byte { + if len(jsonData) == 0 { + return nil + } + var v interface{} + if err := json.Unmarshal(jsonData, &v); err != nil { + return jsonData // fallback to raw JSON if parsing fails + } + pbVal, err := structpb.NewValue(v) + if err != nil { + return jsonData // fallback + } + b, err := proto.Marshal(pbVal) + if err != nil { + return jsonData // fallback + } + return b +} + +// ProtobufValueBytesToJSON converts protobuf Value binary back to JSON. +// This mirrors the TS pattern: toJson(ValueSchema, fromBinary(ValueSchema, value)) +func ProtobufValueBytesToJSON(data []byte) (interface{}, error) { + val := &structpb.Value{} + if err := proto.Unmarshal(data, val); err != nil { + return nil, err + } + return val.AsInterface(), nil +} + +func sha256Sum(data []byte) []byte { + h := sha256.Sum256(data) + return h[:] +} + +var idCounter uint64 + +func generateId() string { + idCounter++ + h := sha256.Sum256([]byte{byte(idCounter), byte(idCounter >> 8), byte(idCounter >> 16)}) + return hex.EncodeToString(h[:16]) +} diff --git a/internal/auth/cursor/proto/fieldnumbers.go b/internal/auth/cursor/proto/fieldnumbers.go new file mode 100644 index 00000000..7ba24109 --- /dev/null +++ b/internal/auth/cursor/proto/fieldnumbers.go @@ -0,0 +1,332 @@ +// Package proto provides hand-rolled protobuf encode/decode for Cursor's gRPC API. +// Field numbers are extracted from the TypeScript generated proto/agent_pb.ts in alma-plugins/cursor-auth. +package proto + +// AgentClientMessage (msg 118) oneof "message" +const ( + ACM_RunRequest = 1 // AgentRunRequest + ACM_ExecClientMessage = 2 // ExecClientMessage + ACM_KvClientMessage = 3 // KvClientMessage + ACM_ConversationAction = 4 // ConversationAction + ACM_ExecClientControlMsg = 5 // ExecClientControlMessage + ACM_InteractionResponse = 6 // InteractionResponse + ACM_ClientHeartbeat = 7 // ClientHeartbeat +) + +// AgentServerMessage (msg 119) oneof "message" +const ( + ASM_InteractionUpdate = 1 // InteractionUpdate + ASM_ExecServerMessage = 2 // ExecServerMessage + ASM_ConversationCheckpoint = 3 // ConversationStateStructure + ASM_KvServerMessage = 4 // KvServerMessage + ASM_ExecServerControlMessage = 5 // ExecServerControlMessage + ASM_InteractionQuery = 7 // InteractionQuery +) + +// AgentRunRequest (msg 91) +const ( + ARR_ConversationState = 1 // ConversationStateStructure + ARR_Action = 2 // ConversationAction + ARR_ModelDetails = 3 // ModelDetails + ARR_McpTools = 4 // McpTools + ARR_ConversationId = 5 // string (optional) +) + +// ConversationStateStructure (msg 83) +const ( + CSS_RootPromptMessagesJson = 1 // repeated bytes + CSS_TurnsOld = 2 // repeated bytes (deprecated) + CSS_Todos = 3 // repeated bytes + CSS_PendingToolCalls = 4 // repeated string + CSS_Turns = 8 // repeated bytes (CURRENT field for turns) + CSS_PreviousWorkspaceUris = 9 // repeated string + CSS_SelfSummaryCount = 17 // uint32 + CSS_ReadPaths = 18 // repeated string +) + +// ConversationAction (msg 54) oneof "action" +const ( + CA_UserMessageAction = 1 // UserMessageAction +) + +// UserMessageAction (msg 55) +const ( + UMA_UserMessage = 1 // UserMessage +) + +// UserMessage (msg 63) +const ( + UM_Text = 1 // string + UM_MessageId = 2 // string + UM_SelectedContext = 3 // SelectedContext (optional) +) + +// SelectedContext +const ( + SC_SelectedImages = 1 // repeated SelectedImage +) + +// SelectedImage +const ( + SI_BlobId = 1 // bytes (oneof dataOrBlobId) + SI_Uuid = 2 // string + SI_Path = 3 // string + SI_MimeType = 7 // string + SI_Data = 8 // bytes (oneof dataOrBlobId) +) + +// ModelDetails (msg 88) +const ( + MD_ModelId = 1 // string + MD_ThinkingDetails = 2 // ThinkingDetails (optional) + MD_DisplayModelId = 3 // string + MD_DisplayName = 4 // string +) + +// McpTools (msg 307) +const ( + MT_McpTools = 1 // repeated McpToolDefinition +) + +// McpToolDefinition (msg 306) +const ( + MTD_Name = 1 // string + MTD_Description = 2 // string + MTD_InputSchema = 3 // bytes + MTD_ProviderIdentifier = 4 // string + MTD_ToolName = 5 // string +) + +// ConversationTurnStructure (msg 70) oneof "turn" +const ( + CTS_AgentConversationTurn = 1 // AgentConversationTurnStructure +) + +// AgentConversationTurnStructure (msg 72) +const ( + ACTS_UserMessage = 1 // bytes (serialized UserMessage) + ACTS_Steps = 2 // repeated bytes (serialized ConversationStep) +) + +// ConversationStep (msg 53) oneof "message" +const ( + CS_AssistantMessage = 1 // AssistantMessage +) + +// AssistantMessage +const ( + AM_Text = 1 // string +) + +// --- Server-side message fields --- + +// InteractionUpdate oneof "message" +const ( + IU_TextDelta = 1 // TextDeltaUpdate + IU_ThinkingDelta = 4 // ThinkingDeltaUpdate + IU_ThinkingCompleted = 5 // ThinkingCompletedUpdate +) + +// TextDeltaUpdate (msg 92) +const ( + TDU_Text = 1 // string +) + +// ThinkingDeltaUpdate (msg 97) +const ( + TKD_Text = 1 // string +) + +// KvServerMessage (msg 271) +const ( + KSM_Id = 1 // uint32 + KSM_GetBlobArgs = 2 // GetBlobArgs + KSM_SetBlobArgs = 3 // SetBlobArgs +) + +// GetBlobArgs (msg 267) +const ( + GBA_BlobId = 1 // bytes +) + +// SetBlobArgs (msg 269) +const ( + SBA_BlobId = 1 // bytes + SBA_BlobData = 2 // bytes +) + +// KvClientMessage (msg 272) +const ( + KCM_Id = 1 // uint32 + KCM_GetBlobResult = 2 // GetBlobResult + KCM_SetBlobResult = 3 // SetBlobResult +) + +// GetBlobResult (msg 268) +const ( + GBR_BlobData = 1 // bytes (optional) +) + +// ExecServerMessage +const ( + ESM_Id = 1 // uint32 + ESM_ExecId = 15 // string + // oneof message: + ESM_ShellArgs = 2 // ShellArgs + ESM_WriteArgs = 3 // WriteArgs + ESM_DeleteArgs = 4 // DeleteArgs + ESM_GrepArgs = 5 // GrepArgs + ESM_ReadArgs = 7 // ReadArgs (NOTE: 6 is skipped) + ESM_LsArgs = 8 // LsArgs + ESM_DiagnosticsArgs = 9 // DiagnosticsArgs + ESM_RequestContextArgs = 10 // RequestContextArgs + ESM_McpArgs = 11 // McpArgs + ESM_ShellStreamArgs = 14 // ShellArgs (stream variant) + ESM_BackgroundShellSpawn = 16 // BackgroundShellSpawnArgs + ESM_FetchArgs = 20 // FetchArgs + ESM_WriteShellStdinArgs = 23 // WriteShellStdinArgs +) + +// ExecClientMessage +const ( + ECM_Id = 1 // uint32 + ECM_ExecId = 15 // string + // oneof message (mirrors server fields): + ECM_ShellResult = 2 + ECM_WriteResult = 3 + ECM_DeleteResult = 4 + ECM_GrepResult = 5 + ECM_ReadResult = 7 + ECM_LsResult = 8 + ECM_DiagnosticsResult = 9 + ECM_RequestContextResult = 10 + ECM_McpResult = 11 + ECM_ShellStream = 14 + ECM_BackgroundShellSpawnRes = 16 + ECM_FetchResult = 20 + ECM_WriteShellStdinResult = 23 +) + +// McpArgs +const ( + MCA_Name = 1 // string + MCA_Args = 2 // map + MCA_ToolCallId = 3 // string + MCA_ProviderIdentifier = 4 // string + MCA_ToolName = 5 // string +) + +// RequestContextResult oneof "result" +const ( + RCR_Success = 1 // RequestContextSuccess + RCR_Error = 2 // RequestContextError +) + +// RequestContextSuccess (msg 337) +const ( + RCS_RequestContext = 1 // RequestContext +) + +// RequestContext +const ( + RC_Rules = 2 // repeated CursorRule + RC_Tools = 7 // repeated McpToolDefinition +) + +// McpResult oneof "result" +const ( + MCR_Success = 1 // McpSuccess + MCR_Error = 2 // McpError + MCR_Rejected = 3 // McpRejected +) + +// McpSuccess (msg 290) +const ( + MCS_Content = 1 // repeated McpToolResultContentItem + MCS_IsError = 2 // bool +) + +// McpToolResultContentItem oneof "content" +const ( + MTRCI_Text = 1 // McpTextContent +) + +// McpTextContent (msg 287) +const ( + MTC_Text = 1 // string +) + +// McpError (msg 291) +const ( + MCE_Error = 1 // string +) + +// --- Rejection messages --- + +// ReadRejected: path=1, reason=2 +// ShellRejected: command=1, workingDirectory=2, reason=3, isReadonly=4 +// WriteRejected: path=1, reason=2 +// DeleteRejected: path=1, reason=2 +// LsRejected: path=1, reason=2 +// GrepError: error=1 +// FetchError: url=1, error=2 +// WriteShellStdinError: error=1 + +// ReadResult oneof: success=1, error=2, rejected=3 +// ShellResult oneof: success=1 (+ various), rejected=? +// The TS code uses specific result field numbers from the oneof: +const ( + RR_Rejected = 3 // ReadResult.rejected + SR_Rejected = 5 // ShellResult.rejected (from TS: ShellResult has success/various/rejected) + WR_Rejected = 5 // WriteResult.rejected + DR_Rejected = 3 // DeleteResult.rejected + LR_Rejected = 3 // LsResult.rejected + GR_Error = 2 // GrepResult.error + FR_Error = 2 // FetchResult.error + BSSR_Rejected = 2 // BackgroundShellSpawnResult.rejected (error field) + WSSR_Error = 2 // WriteShellStdinResult.error +) + +// --- Rejection struct fields --- +const ( + REJ_Path = 1 + REJ_Reason = 2 + SREJ_Command = 1 + SREJ_WorkingDir = 2 + SREJ_Reason = 3 + SREJ_IsReadonly = 4 + GERR_Error = 1 + FERR_Url = 1 + FERR_Error = 2 +) + +// ReadArgs +const ( + RA_Path = 1 // string +) + +// WriteArgs +const ( + WA_Path = 1 // string +) + +// DeleteArgs +const ( + DA_Path = 1 // string +) + +// LsArgs +const ( + LA_Path = 1 // string +) + +// ShellArgs +const ( + SHA_Command = 1 // string + SHA_WorkingDirectory = 2 // string +) + +// FetchArgs +const ( + FA_Url = 1 // string +) diff --git a/internal/auth/cursor/proto/h2stream.go b/internal/auth/cursor/proto/h2stream.go new file mode 100644 index 00000000..d08d099e --- /dev/null +++ b/internal/auth/cursor/proto/h2stream.go @@ -0,0 +1,273 @@ +package proto + +import ( + "crypto/tls" + "fmt" + "io" + "net" + "sync" + "time" + + log "github.com/sirupsen/logrus" + "golang.org/x/net/http2" + "golang.org/x/net/http2/hpack" +) + +// H2Stream provides bidirectional HTTP/2 streaming for the Connect protocol. +// Go's net/http does not support full-duplex HTTP/2, so we use the low-level framer. +type H2Stream struct { + framer *http2.Framer + conn net.Conn + streamID uint32 + mu sync.Mutex + id string // unique identifier for debugging + frameNum int64 // sequential frame counter for debugging + + dataCh chan []byte + doneCh chan struct{} + err error +} + +// ID returns the unique identifier for this stream (for logging). +func (s *H2Stream) ID() string { return s.id } + +// FrameNum returns the current frame number for debugging. +func (s *H2Stream) FrameNum() int64 { + s.mu.Lock() + defer s.mu.Unlock() + return s.frameNum +} + +// DialH2Stream establishes a TLS+HTTP/2 connection and opens a new stream. +func DialH2Stream(host string, headers map[string]string) (*H2Stream, error) { + tlsConn, err := tls.Dial("tcp", host+":443", &tls.Config{ + NextProtos: []string{"h2"}, + }) + if err != nil { + return nil, fmt.Errorf("h2: TLS dial failed: %w", err) + } + if tlsConn.ConnectionState().NegotiatedProtocol != "h2" { + tlsConn.Close() + return nil, fmt.Errorf("h2: server did not negotiate h2") + } + + framer := http2.NewFramer(tlsConn, tlsConn) + + // Client connection preface + if _, err := tlsConn.Write([]byte(http2.ClientPreface)); err != nil { + tlsConn.Close() + return nil, fmt.Errorf("h2: preface write failed: %w", err) + } + + // Send initial SETTINGS (with large initial window) + if err := framer.WriteSettings( + http2.Setting{ID: http2.SettingInitialWindowSize, Val: 4 * 1024 * 1024}, + http2.Setting{ID: http2.SettingMaxConcurrentStreams, Val: 100}, + ); err != nil { + tlsConn.Close() + return nil, fmt.Errorf("h2: settings write failed: %w", err) + } + + // Connection-level window update (default is 65535, bump it up) + if err := framer.WriteWindowUpdate(0, 3*1024*1024); err != nil { + tlsConn.Close() + return nil, fmt.Errorf("h2: window update failed: %w", err) + } + + // Read and handle initial server frames (SETTINGS, WINDOW_UPDATE) + for i := 0; i < 5; i++ { + f, err := framer.ReadFrame() + if err != nil { + tlsConn.Close() + return nil, fmt.Errorf("h2: initial frame read failed: %w", err) + } + switch sf := f.(type) { + case *http2.SettingsFrame: + if !sf.IsAck() { + framer.WriteSettingsAck() + } else { + goto handshakeDone + } + case *http2.WindowUpdateFrame: + // ignore + default: + // unexpected but continue + } + } +handshakeDone: + + // Build HEADERS + streamID := uint32(1) + var hdrBuf []byte + enc := hpack.NewEncoder(&sliceWriter{buf: &hdrBuf}) + enc.WriteField(hpack.HeaderField{Name: ":method", Value: "POST"}) + enc.WriteField(hpack.HeaderField{Name: ":scheme", Value: "https"}) + enc.WriteField(hpack.HeaderField{Name: ":authority", Value: host}) + if p, ok := headers[":path"]; ok { + enc.WriteField(hpack.HeaderField{Name: ":path", Value: p}) + } + for k, v := range headers { + if len(k) > 0 && k[0] == ':' { + continue + } + enc.WriteField(hpack.HeaderField{Name: k, Value: v}) + } + + if err := framer.WriteHeaders(http2.HeadersFrameParam{ + StreamID: streamID, + BlockFragment: hdrBuf, + EndStream: false, + EndHeaders: true, + }); err != nil { + tlsConn.Close() + return nil, fmt.Errorf("h2: headers write failed: %w", err) + } + + s := &H2Stream{ + framer: framer, + conn: tlsConn, + streamID: streamID, + dataCh: make(chan []byte, 256), + doneCh: make(chan struct{}), + id: fmt.Sprintf("%d-%s", streamID, time.Now().Format("150405.000")), + frameNum: 0, + } + go s.readLoop() + return s, nil +} + +// Write sends a DATA frame on the stream. +func (s *H2Stream) Write(data []byte) error { + s.mu.Lock() + defer s.mu.Unlock() + const maxFrame = 16384 + for len(data) > 0 { + chunk := data + if len(chunk) > maxFrame { + chunk = data[:maxFrame] + } + data = data[len(chunk):] + if err := s.framer.WriteData(s.streamID, false, chunk); err != nil { + return err + } + } + // Try to flush the underlying connection if it supports it + if flusher, ok := s.conn.(interface{ Flush() error }); ok { + flusher.Flush() + } + return nil +} + +// Data returns the channel of received data chunks. +func (s *H2Stream) Data() <-chan []byte { return s.dataCh } + +// Done returns a channel closed when the stream ends. +func (s *H2Stream) Done() <-chan struct{} { return s.doneCh } + +// Close tears down the connection. +func (s *H2Stream) Close() { + s.conn.Close() +} + +func (s *H2Stream) readLoop() { + defer close(s.doneCh) + defer close(s.dataCh) + log.Debugf("h2stream[%s]: readLoop started for streamID=%d", s.id, s.streamID) + + for { + f, err := s.framer.ReadFrame() + if err != nil { + if err != io.EOF { + s.err = err + log.Debugf("h2stream[%s]: readLoop error: %v", s.id, err) + } else { + log.Debugf("h2stream[%s]: readLoop EOF", s.id) + } + return + } + + // Increment frame counter for debugging + s.mu.Lock() + s.frameNum++ + frameNum := s.frameNum + s.mu.Unlock() + + switch frame := f.(type) { + case *http2.DataFrame: + log.Debugf("h2stream[%s]: frame#%d received DATA frame streamID=%d, len=%d, endStream=%v", s.id, frameNum, frame.StreamID, len(frame.Data()), frame.StreamEnded()) + if frame.StreamID == s.streamID && len(frame.Data()) > 0 { + cp := make([]byte, len(frame.Data())) + copy(cp, frame.Data()) + // Log first 20 bytes for debugging + previewLen := len(cp) + if previewLen > 20 { + previewLen = 20 + } + log.Debugf("h2stream[%s]: frame#%d sending to dataCh: len=%d, dataCh len=%d/%d, first bytes: %x (%q)", s.id, frameNum, len(cp), len(s.dataCh), cap(s.dataCh), cp[:previewLen], string(cp[:previewLen])) + s.dataCh <- cp + + // Flow control: send WINDOW_UPDATE + s.mu.Lock() + s.framer.WriteWindowUpdate(0, uint32(len(cp))) + s.framer.WriteWindowUpdate(s.streamID, uint32(len(cp))) + s.mu.Unlock() + } + if frame.StreamEnded() { + log.Debugf("h2stream[%s]: frame#%d DATA frame has END_STREAM flag, stream ending", s.id, frameNum) + return + } + + case *http2.HeadersFrame: + // Decode HPACK headers for debugging + decoder := hpack.NewDecoder(4096, func(hf hpack.HeaderField) { + log.Debugf("h2stream[%s]: frame#%d header: %s = %q", s.id, frameNum, hf.Name, hf.Value) + // Check for error status + if hf.Name == "grpc-status" || hf.Name == ":status" && hf.Value != "200" { + log.Warnf("h2stream[%s]: frame#%d received error status header: %s = %q", s.id, frameNum, hf.Name, hf.Value) + } + }) + decoder.Write(frame.HeaderBlockFragment()) + log.Debugf("h2stream[%s]: frame#%d received HEADERS frame streamID=%d, endStream=%v", s.id, frameNum, frame.StreamID, frame.StreamEnded()) + if frame.StreamEnded() { + log.Debugf("h2stream[%s]: frame#%d HEADERS frame has END_STREAM flag, stream ending", s.id, frameNum) + return + } + + case *http2.RSTStreamFrame: + s.err = fmt.Errorf("h2: RST_STREAM code=%d", frame.ErrCode) + log.Debugf("h2stream[%s]: frame#%d received RST_STREAM code=%d", s.id, frameNum, frame.ErrCode) + return + + case *http2.GoAwayFrame: + s.err = fmt.Errorf("h2: GOAWAY code=%d", frame.ErrCode) + log.Debugf("h2stream[%s]: received GOAWAY code=%d", s.id, frame.ErrCode) + return + + case *http2.PingFrame: + log.Debugf("h2stream[%s]: received PING frame, isAck=%v", s.id, frame.IsAck()) + if !frame.IsAck() { + s.mu.Lock() + s.framer.WritePing(true, frame.Data) + s.mu.Unlock() + } + + case *http2.SettingsFrame: + log.Debugf("h2stream[%s]: received SETTINGS frame, isAck=%v, numSettings=%d", s.id, frame.IsAck(), frame.NumSettings()) + if !frame.IsAck() { + s.mu.Lock() + s.framer.WriteSettingsAck() + s.mu.Unlock() + } + + case *http2.WindowUpdateFrame: + log.Debugf("h2stream[%s]: received WINDOW_UPDATE frame", s.id) + } + } +} + +type sliceWriter struct{ buf *[]byte } + +func (w *sliceWriter) Write(p []byte) (int, error) { + *w.buf = append(*w.buf, p...) + return len(p), nil +} diff --git a/internal/cmd/auth_manager.go b/internal/cmd/auth_manager.go index 83f42e0c..df62281e 100644 --- a/internal/cmd/auth_manager.go +++ b/internal/cmd/auth_manager.go @@ -24,6 +24,7 @@ func newAuthManager() *sdkAuth.Manager { sdkAuth.NewKiloAuthenticator(), sdkAuth.NewGitLabAuthenticator(), sdkAuth.NewCodeBuddyAuthenticator(), + sdkAuth.NewCursorAuthenticator(), ) return manager } diff --git a/internal/cmd/cursor_login.go b/internal/cmd/cursor_login.go new file mode 100644 index 00000000..0ffdef1b --- /dev/null +++ b/internal/cmd/cursor_login.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" + log "github.com/sirupsen/logrus" +) + +// DoCursorLogin triggers the OAuth PKCE flow for Cursor and saves tokens. +func DoCursorLogin(cfg *config.Config, options *LoginOptions) { + if options == nil { + options = &LoginOptions{} + } + + manager := newAuthManager() + authOpts := &sdkAuth.LoginOptions{ + NoBrowser: options.NoBrowser, + Metadata: map[string]string{}, + Prompt: options.Prompt, + } + + record, savedPath, err := manager.Login(context.Background(), "cursor", cfg, authOpts) + if err != nil { + log.Errorf("Cursor authentication failed: %v", err) + return + } + + if savedPath != "" { + fmt.Printf("Authentication saved to %s\n", savedPath) + } + if record != nil && record.Label != "" { + fmt.Printf("Authenticated as %s\n", record.Label) + } + fmt.Println("Cursor authentication successful!") +} diff --git a/internal/registry/model_definitions.go b/internal/registry/model_definitions.go index 6764a962..9299ca22 100644 --- a/internal/registry/model_definitions.go +++ b/internal/registry/model_definitions.go @@ -231,11 +231,25 @@ func GetStaticModelDefinitionsByChannel(channel string) []*ModelInfo { return GetAntigravityModels() case "codebuddy": return GetCodeBuddyModels() + case "cursor": + return GetCursorModels() default: return nil } } +// GetCursorModels returns the fallback Cursor model definitions. +func GetCursorModels() []*ModelInfo { + return []*ModelInfo{ + {ID: "composer-2", Object: "model", OwnedBy: "cursor", Type: "cursor", DisplayName: "Composer 2", ContextLength: 200000, MaxCompletionTokens: 64000, Thinking: &ThinkingSupport{Max: 50000, DynamicAllowed: true}}, + {ID: "claude-4-sonnet", Object: "model", OwnedBy: "cursor", Type: "cursor", DisplayName: "Claude 4 Sonnet", ContextLength: 200000, MaxCompletionTokens: 64000, Thinking: &ThinkingSupport{Max: 50000, DynamicAllowed: true}}, + {ID: "claude-3.5-sonnet", Object: "model", OwnedBy: "cursor", Type: "cursor", DisplayName: "Claude 3.5 Sonnet", ContextLength: 200000, MaxCompletionTokens: 8192}, + {ID: "gpt-4o", Object: "model", OwnedBy: "cursor", Type: "cursor", DisplayName: "GPT-4o", ContextLength: 128000, MaxCompletionTokens: 16384}, + {ID: "cursor-small", Object: "model", OwnedBy: "cursor", Type: "cursor", DisplayName: "Cursor Small", ContextLength: 200000, MaxCompletionTokens: 64000}, + {ID: "gemini-2.5-pro", Object: "model", OwnedBy: "cursor", Type: "cursor", DisplayName: "Gemini 2.5 Pro", ContextLength: 1000000, MaxCompletionTokens: 65536, Thinking: &ThinkingSupport{Max: 50000, DynamicAllowed: true}}, + } +} + // LookupStaticModelInfo searches all static model definitions for a model by ID. // Returns nil if no matching model is found. func LookupStaticModelInfo(modelID string) *ModelInfo { @@ -260,6 +274,7 @@ func LookupStaticModelInfo(modelID string) *ModelInfo { GetKiloModels(), GetAmazonQModels(), GetCodeBuddyModels(), + GetCursorModels(), } for _, models := range allModels { for _, m := range models { diff --git a/internal/runtime/executor/cursor_executor.go b/internal/runtime/executor/cursor_executor.go new file mode 100644 index 00000000..5519e92d --- /dev/null +++ b/internal/runtime/executor/cursor_executor.go @@ -0,0 +1,1341 @@ +package executor + +import ( + "bytes" + "context" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "sync" + "time" + + "github.com/google/uuid" + cursorauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/cursor" + cursorproto "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/cursor/proto" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" + "golang.org/x/net/http2" +) + +const ( + cursorAPIURL = "https://api2.cursor.sh" + cursorRunPath = "/agent.v1.AgentService/Run" + cursorModelsPath = "/agent.v1.AgentService/GetUsableModels" + cursorClientVersion = "cli-2026.02.13-41ac335" + cursorAuthType = "cursor" + cursorHeartbeatInterval = 5 * time.Second + cursorSessionTTL = 5 * time.Minute +) + +// CursorExecutor handles requests to the Cursor API via Connect+Protobuf protocol. +type CursorExecutor struct { + cfg *config.Config + mu sync.Mutex + sessions map[string]*cursorSession +} + +type cursorSession struct { + stream *cursorproto.H2Stream + blobStore map[string][]byte + mcpTools []cursorproto.McpToolDef + pending []pendingMcpExec + cancel context.CancelFunc // cancels the session-scoped heartbeat (NOT tied to HTTP request) + createdAt time.Time +} + +type pendingMcpExec struct { + ExecMsgId uint32 + ExecId string + ToolCallId string + ToolName string + Args string // JSON-encoded args +} + +// NewCursorExecutor constructs a new executor instance. +func NewCursorExecutor(cfg *config.Config) *CursorExecutor { + e := &CursorExecutor{ + cfg: cfg, + sessions: make(map[string]*cursorSession), + } + go e.cleanupLoop() + return e +} + +// Identifier implements ProviderExecutor. +func (e *CursorExecutor) Identifier() string { return cursorAuthType } + +// CloseExecutionSession implements ExecutionSessionCloser. +func (e *CursorExecutor) CloseExecutionSession(sessionID string) { + e.mu.Lock() + defer e.mu.Unlock() + if sessionID == cliproxyauth.CloseAllExecutionSessionsID { + for k, s := range e.sessions { + s.cancel() + delete(e.sessions, k) + } + return + } + if s, ok := e.sessions[sessionID]; ok { + s.cancel() + delete(e.sessions, sessionID) + } +} + +func (e *CursorExecutor) cleanupLoop() { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + for range ticker.C { + e.mu.Lock() + for k, s := range e.sessions { + if time.Since(s.createdAt) > cursorSessionTTL { + s.cancel() + delete(e.sessions, k) + } + } + e.mu.Unlock() + } +} + +// PrepareRequest implements ProviderExecutor (for HttpRequest support). +func (e *CursorExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error { + token := cursorAccessToken(auth) + if token == "" { + return fmt.Errorf("cursor: access token not found") + } + req.Header.Set("Authorization", "Bearer "+token) + return nil +} + +// HttpRequest injects credentials and executes the request. +func (e *CursorExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) { + if req == nil { + return nil, fmt.Errorf("cursor: request is nil") + } + if err := e.PrepareRequest(req, auth); err != nil { + return nil, err + } + return http.DefaultClient.Do(req) +} + +// CountTokens estimates token count locally using tiktoken. +func (e *CursorExecutor) CountTokens(_ context.Context, _ *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) { + defer func() { + if err != nil { + log.Warnf("cursor CountTokens error: %v", err) + } else { + log.Debugf("cursor CountTokens: model=%s result=%s", req.Model, string(resp.Payload)) + } + }() + model := gjson.GetBytes(req.Payload, "model").String() + if model == "" { + model = req.Model + } + + enc, err := getTokenizer(model) + if err != nil { + // Fallback: return zero tokens rather than error (avoids 502) + return cliproxyexecutor.Response{Payload: buildOpenAIUsageJSON(0)}, nil + } + + // Detect format: Claude (/v1/messages) vs OpenAI (/v1/chat/completions) + var count int64 + if gjson.GetBytes(req.Payload, "system").Exists() || opts.SourceFormat.String() == "claude" { + count, _ = countClaudeChatTokens(enc, req.Payload) + } else { + count, _ = countOpenAIChatTokens(enc, req.Payload) + } + + return cliproxyexecutor.Response{Payload: buildOpenAIUsageJSON(count)}, nil +} + +// Refresh attempts to refresh the Cursor access token. +func (e *CursorExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { + refreshToken := cursorRefreshToken(auth) + if refreshToken == "" { + return nil, fmt.Errorf("cursor: no refresh token available") + } + + tokens, err := cursorauth.RefreshToken(ctx, refreshToken) + if err != nil { + return nil, err + } + + expiresAt := cursorauth.GetTokenExpiry(tokens.AccessToken) + + newAuth := auth.Clone() + newAuth.Metadata["access_token"] = tokens.AccessToken + newAuth.Metadata["refresh_token"] = tokens.RefreshToken + newAuth.Metadata["expires_at"] = expiresAt.Format(time.RFC3339) + return newAuth, nil +} + +// Execute handles non-streaming requests. +func (e *CursorExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) { + log.Debugf("cursor Execute: model=%s sourceFormat=%s payloadLen=%d", req.Model, opts.SourceFormat, len(req.Payload)) + defer func() { + if r := recover(); r != nil { + log.Errorf("cursor Execute PANIC: %v", r) + err = fmt.Errorf("cursor: internal panic: %v", r) + } + if err != nil { + log.Warnf("cursor Execute error: %v", err) + } + }() + accessToken := cursorAccessToken(auth) + if accessToken == "" { + return resp, fmt.Errorf("cursor: access token not found") + } + + // Translate input to OpenAI format if needed (e.g. Claude /v1/messages format) + from := opts.SourceFormat + to := sdktranslator.FromString("openai") + payload := req.Payload + if from.String() != "" && from.String() != "openai" { + payload = sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(payload), false) + } + + parsed := parseOpenAIRequest(payload) + params := buildRunRequestParams(parsed) + + requestBytes := cursorproto.EncodeRunRequest(params) + framedRequest := cursorproto.FrameConnectMessage(requestBytes, 0) + + stream, err := openCursorH2Stream(accessToken) + if err != nil { + return resp, err + } + defer stream.Close() + + // Send the request frame + if err := stream.Write(framedRequest); err != nil { + return resp, fmt.Errorf("cursor: failed to send request: %w", err) + } + + // Start heartbeat + sessionCtx, sessionCancel := context.WithCancel(ctx) + defer sessionCancel() + go cursorH2Heartbeat(sessionCtx, stream) + + // Collect full text from streaming response + var fullText strings.Builder + processH2SessionFrames(sessionCtx, stream, params.BlobStore, nil, + func(text string, isThinking bool) { + fullText.WriteString(text) + }, + nil, + ) + + id := "chatcmpl-" + uuid.New().String()[:28] + created := time.Now().Unix() + openaiResp := fmt.Sprintf(`{"id":"%s","object":"chat.completion","created":%d,"model":"%s","choices":[{"index":0,"message":{"role":"assistant","content":%s},"finish_reason":"stop"}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`, + id, created, parsed.Model, jsonString(fullText.String())) + + // Translate response back to source format if needed + result := []byte(openaiResp) + if from.String() != "" && from.String() != "openai" { + var param any + result = sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), payload, result, ¶m) + } + resp.Payload = result + return resp, nil +} + +// ExecuteStream handles streaming requests. +// It supports MCP tool call sessions: when Cursor returns an MCP tool call, +// the H2 stream is kept alive. When Claude Code returns the tool result in +// the next request, the result is sent back on the same stream (session resume). +// This mirrors the activeSessions/resumeWithToolResults pattern in cursor-fetch.ts. +func (e *CursorExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) { + log.Debugf("cursor ExecuteStream: model=%s sourceFormat=%s payloadLen=%d", req.Model, opts.SourceFormat, len(req.Payload)) + defer func() { + if r := recover(); r != nil { + log.Errorf("cursor ExecuteStream PANIC: %v", r) + err = fmt.Errorf("cursor: internal panic: %v", r) + } + if err != nil { + log.Warnf("cursor ExecuteStream error: %v", err) + } + }() + accessToken := cursorAccessToken(auth) + if accessToken == "" { + return nil, fmt.Errorf("cursor: access token not found") + } + + // Translate input to OpenAI format if needed + from := opts.SourceFormat + to := sdktranslator.FromString("openai") + payload := req.Payload + originalPayload := bytes.Clone(req.Payload) + if len(opts.OriginalRequest) > 0 { + originalPayload = bytes.Clone(opts.OriginalRequest) + } + if from.String() != "" && from.String() != "openai" { + log.Debugf("cursor: translating request from %s to openai", from) + payload = sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(payload), true) + log.Debugf("cursor: translated payload len=%d", len(payload)) + } + + parsed := parseOpenAIRequest(payload) + log.Debugf("cursor: parsed request: model=%s userText=%d chars, turns=%d, tools=%d, toolResults=%d", + parsed.Model, len(parsed.UserText), len(parsed.Turns), len(parsed.Tools), len(parsed.ToolResults)) + + sessionKey := deriveSessionKey(parsed.Model, parsed.Messages) + needsTranslate := from.String() != "" && from.String() != "openai" + + // Check if we can resume an existing session with tool results + if len(parsed.ToolResults) > 0 { + e.mu.Lock() + session, hasSession := e.sessions[sessionKey] + if hasSession { + delete(e.sessions, sessionKey) + } + e.mu.Unlock() + + if hasSession && session.stream != nil { + log.Debugf("cursor: resuming session %s with %d tool results", sessionKey, len(parsed.ToolResults)) + return e.resumeWithToolResults(ctx, session, parsed, from, to, req, originalPayload, payload, needsTranslate) + } + } + + // Clean up any stale session for this key + e.mu.Lock() + if old, ok := e.sessions[sessionKey]; ok { + old.cancel() + delete(e.sessions, sessionKey) + } + e.mu.Unlock() + + params := buildRunRequestParams(parsed) + requestBytes := cursorproto.EncodeRunRequest(params) + framedRequest := cursorproto.FrameConnectMessage(requestBytes, 0) + + stream, err := openCursorH2Stream(accessToken) + if err != nil { + return nil, err + } + + if err := stream.Write(framedRequest); err != nil { + stream.Close() + return nil, fmt.Errorf("cursor: failed to send request: %w", err) + } + + // Use a session-scoped context for the heartbeat that is NOT tied to the HTTP request. + // This ensures the heartbeat survives across request boundaries during MCP tool execution. + // Mirrors the TS plugin's setInterval-based heartbeat that lives independently of HTTP responses. + sessionCtx, sessionCancel := context.WithCancel(context.Background()) + go cursorH2Heartbeat(sessionCtx, stream) + + chunks := make(chan cliproxyexecutor.StreamChunk, 64) + chatId := "chatcmpl-" + uuid.New().String()[:28] + created := time.Now().Unix() + + // sendChunk builds an OpenAI SSE line and optionally translates to target format + var streamParam any + sendChunk := func(delta string, finishReason string) { + fr := "null" + if finishReason != "" { + fr = finishReason + } + openaiJSON := fmt.Sprintf(`{"id":"%s","object":"chat.completion.chunk","created":%d,"model":"%s","choices":[{"index":0,"delta":%s,"finish_reason":%s}]}`, + chatId, created, parsed.Model, delta, fr) + sseLine := []byte("data: " + openaiJSON + "\n") + + if needsTranslate { + translated := sdktranslator.TranslateStream(ctx, to, from, req.Model, originalPayload, payload, sseLine, &streamParam) + for _, t := range translated { + chunks <- cliproxyexecutor.StreamChunk{Payload: bytes.Clone(t)} + } + } else { + chunks <- cliproxyexecutor.StreamChunk{Payload: []byte(openaiJSON)} + } + } + + sendDone := func() { + if needsTranslate { + done := sdktranslator.TranslateStream(ctx, to, from, req.Model, originalPayload, payload, []byte("data: [DONE]\n"), &streamParam) + for _, d := range done { + chunks <- cliproxyexecutor.StreamChunk{Payload: bytes.Clone(d)} + } + } else { + chunks <- cliproxyexecutor.StreamChunk{Payload: []byte("[DONE]")} + } + } + + go func() { + defer close(chunks) + + thinkingActive := false + toolCallIndex := 0 + mcpExecReceived := false + + processH2SessionFrames(sessionCtx, stream, params.BlobStore, params.McpTools, + func(text string, isThinking bool) { + if isThinking { + if !thinkingActive { + thinkingActive = true + sendChunk(`{"role":"assistant","content":""}`, "") + } + sendChunk(fmt.Sprintf(`{"content":%s}`, jsonString(text)), "") + } else { + if thinkingActive { + thinkingActive = false + sendChunk(`{"content":""}`, "") + } + sendChunk(fmt.Sprintf(`{"content":%s}`, jsonString(text)), "") + } + }, + func(exec pendingMcpExec) { + mcpExecReceived = true + if thinkingActive { + thinkingActive = false + sendChunk(`{"content":""}`, "") + } + toolCallJSON := fmt.Sprintf(`{"tool_calls":[{"index":%d,"id":"%s","type":"function","function":{"name":"%s","arguments":%s}}]}`, + toolCallIndex, exec.ToolCallId, exec.ToolName, jsonString(exec.Args)) + toolCallIndex++ + sendChunk(toolCallJSON, "") + sendChunk(`{}`, `"tool_calls"`) + sendDone() + + // Save session for resume — keep stream alive. + // The heartbeat goroutine continues running (session-scoped context), + // keeping the H2 connection alive while the MCP tool executes. + log.Debugf("cursor: saving session %s for MCP tool resume (tool=%s, streamID=%s)", sessionKey, exec.ToolName, stream.ID()) + e.mu.Lock() + e.sessions[sessionKey] = &cursorSession{ + stream: stream, + blobStore: params.BlobStore, + mcpTools: params.McpTools, + pending: []pendingMcpExec{exec}, + cancel: sessionCancel, + createdAt: time.Now(), + } + e.mu.Unlock() + }, + ) + + if !mcpExecReceived { + if thinkingActive { + sendChunk(`{"content":""}`, "") + } + sendChunk(`{}`, `"stop"`) + sendDone() + sessionCancel() + stream.Close() + } + // If mcpExecReceived, do NOT close stream or cancel — session resume will handle it + }() + + return &cliproxyexecutor.StreamResult{Chunks: chunks}, nil +} + +// resumeWithToolResults sends MCP tool results back on the existing H2 stream, +// then continues reading the stream for the model's response. +// Mirrors resumeWithToolResults() in cursor-fetch.ts. +func (e *CursorExecutor) resumeWithToolResults( + ctx context.Context, + session *cursorSession, + parsed *parsedOpenAIRequest, + from, to sdktranslator.Format, + req cliproxyexecutor.Request, + originalPayload, payload []byte, + needsTranslate bool, +) (*cliproxyexecutor.StreamResult, error) { + stream := session.stream + log.Debugf("cursor: resumeWithToolResults: using stream ID=%s", stream.ID()) + + // Cancel old session-scoped heartbeat before starting a new one + session.cancel() + + // CRITICAL: Process any pending messages from the channel before sending MCP result. + // After the initial processH2SessionFrames returned (upon receiving MCP args), + // the server may have sent more data (KV messages, text deltas) that are now buffered in dataCh. + // We must process KV messages (respond to them) but discard text deltas (stale responses). + drainedCount := 0 + drainedBytes := 0 + kvProcessedCount := 0 + for { + select { + case staleData, ok := <-stream.Data(): + if !ok { + log.Debugf("cursor: resumeWithToolResults: dataCh closed during drain") + break + } + drainedCount++ + drainedBytes += len(staleData) + log.Debugf("cursor: resumeWithToolResults: processing stale data #%d: len=%d, first bytes: %x (%q)", drainedCount, len(staleData), staleData[:min(20, len(staleData))], string(staleData[:min(20, len(staleData))])) + + // Try to decode and handle KV messages (they need responses) + if len(staleData) > 5 { + frameLen := binary.BigEndian.Uint32(staleData[1:5]) + if int(frameLen)+5 <= len(staleData) { + payload := staleData[5 : 5+frameLen] + msg, err := cursorproto.DecodeAgentServerMessage(payload) + if err == nil && msg.Type == cursorproto.ServerMsgKvGetBlob { + // Respond to KV getBlob + blobKey := cursorproto.BlobIdHex(msg.BlobId) + data := session.blobStore[blobKey] + log.Debugf("cursor: resumeWithToolResults: responding to stale KV getBlob kvId=%d blobKey=%s found=%v", msg.KvId, blobKey, len(data) > 0) + resp := cursorproto.EncodeKvGetBlobResult(msg.KvId, data) + stream.Write(cursorproto.FrameConnectMessage(resp, 0)) + kvProcessedCount++ + continue + } else if err == nil && msg.Type == cursorproto.ServerMsgKvSetBlob { + // Respond to KV setBlob + blobKey := cursorproto.BlobIdHex(msg.BlobId) + session.blobStore[blobKey] = append([]byte(nil), msg.BlobData...) + log.Debugf("cursor: resumeWithToolResults: responding to stale KV setBlob kvId=%d blobKey=%s", msg.KvId, blobKey) + resp := cursorproto.EncodeKvSetBlobResult(msg.KvId) + stream.Write(cursorproto.FrameConnectMessage(resp, 0)) + kvProcessedCount++ + continue + } + } + } + log.Debugf("cursor: resumeWithToolResults: discarding non-KV stale data") + default: + // No more data in channel + goto drainDone + } + } +drainDone: + if drainedCount > 0 { + log.Debugf("cursor: resumeWithToolResults: processed %d stale frames (%d bytes total, %d KV responded)", drainedCount, drainedBytes, kvProcessedCount) + } + + // Send MCP results back on the same H2 stream + for _, exec := range session.pending { + var content string + var isError bool + found := false + for _, tr := range parsed.ToolResults { + if tr.ToolCallId == exec.ToolCallId { + content = tr.Content + found = true + break + } + } + if !found { + content = "Tool result not provided" + isError = true + } + log.Debugf("cursor: sending MCP result for tool=%s callId=%s execMsgId=%d execId=%s contentLen=%d isError=%v", + exec.ToolName, exec.ToolCallId, exec.ExecMsgId, exec.ExecId, len(content), isError) + resultBytes := cursorproto.EncodeExecMcpResult(exec.ExecMsgId, exec.ExecId, content, isError) + framedResult := cursorproto.FrameConnectMessage(resultBytes, 0) + // Log the framed result details for debugging + log.Debugf("cursor: MCP result frame size=%d bytes", len(framedResult)) + log.Debugf("cursor: MCP result frame header: flags=%d, len=%d", framedResult[0], binary.BigEndian.Uint32(framedResult[1:5])) + log.Debugf("cursor: MCP result protobuf hex (first 50 bytes): %x", resultBytes[:min(50, len(resultBytes))]) + if err := stream.Write(framedResult); err != nil { + stream.Close() + return nil, fmt.Errorf("cursor: failed to send MCP result: %w", err) + } + log.Debugf("cursor: MCP result sent successfully for tool=%s", exec.ToolName) + } + + // Start new session-scoped heartbeat (independent of HTTP request context) + sessionCtx, sessionCancel := context.WithCancel(context.Background()) + go cursorH2Heartbeat(sessionCtx, stream) + log.Debugf("cursor: started new heartbeat for resumed session, waiting for Cursor response...") + + chunks := make(chan cliproxyexecutor.StreamChunk, 64) + chatId := "chatcmpl-" + uuid.New().String()[:28] + created := time.Now().Unix() + sessionKey := deriveSessionKey(parsed.Model, parsed.Messages) + + var streamParam any + sendChunk := func(delta string, finishReason string) { + fr := "null" + if finishReason != "" { + fr = finishReason + } + openaiJSON := fmt.Sprintf(`{"id":"%s","object":"chat.completion.chunk","created":%d,"model":"%s","choices":[{"index":0,"delta":%s,"finish_reason":%s}]}`, + chatId, created, parsed.Model, delta, fr) + sseLine := []byte("data: " + openaiJSON + "\n") + + if needsTranslate { + translated := sdktranslator.TranslateStream(ctx, to, from, req.Model, originalPayload, payload, sseLine, &streamParam) + for _, t := range translated { + chunks <- cliproxyexecutor.StreamChunk{Payload: bytes.Clone(t)} + } + } else { + chunks <- cliproxyexecutor.StreamChunk{Payload: []byte(openaiJSON)} + } + } + + sendDone := func() { + if needsTranslate { + done := sdktranslator.TranslateStream(ctx, to, from, req.Model, originalPayload, payload, []byte("data: [DONE]\n"), &streamParam) + for _, d := range done { + chunks <- cliproxyexecutor.StreamChunk{Payload: bytes.Clone(d)} + } + } else { + chunks <- cliproxyexecutor.StreamChunk{Payload: []byte("[DONE]")} + } + } + + go func() { + defer func() { + log.Debugf("cursor: resume goroutine exiting, closing chunks channel") + close(chunks) + }() + log.Debugf("cursor: resume goroutine started, entering processH2SessionFrames") + + thinkingActive := false + toolCallIndex := 0 + mcpExecReceived := false + + processH2SessionFrames(sessionCtx, stream, session.blobStore, session.mcpTools, + func(text string, isThinking bool) { + log.Debugf("cursor: resume received text (isThinking=%v, len=%d)", isThinking, len(text)) + if isThinking { + if !thinkingActive { + thinkingActive = true + sendChunk(`{"role":"assistant","content":""}`, "") + } + sendChunk(fmt.Sprintf(`{"content":%s}`, jsonString(text)), "") + } else { + if thinkingActive { + thinkingActive = false + sendChunk(`{"content":""}`, "") + } + sendChunk(fmt.Sprintf(`{"content":%s}`, jsonString(text)), "") + } + }, + func(exec pendingMcpExec) { + mcpExecReceived = true + if thinkingActive { + thinkingActive = false + sendChunk(`{"content":""}`, "") + } + toolCallJSON := fmt.Sprintf(`{"tool_calls":[{"index":%d,"id":"%s","type":"function","function":{"name":"%s","arguments":%s}}]}`, + toolCallIndex, exec.ToolCallId, exec.ToolName, jsonString(exec.Args)) + toolCallIndex++ + sendChunk(toolCallJSON, "") + sendChunk(`{}`, `"tool_calls"`) + sendDone() + + // Save session again for another round of tool calls + log.Debugf("cursor: saving session %s for another MCP tool resume (tool=%s, streamID=%s)", sessionKey, exec.ToolName, stream.ID()) + e.mu.Lock() + e.sessions[sessionKey] = &cursorSession{ + stream: stream, + blobStore: session.blobStore, + mcpTools: session.mcpTools, + pending: []pendingMcpExec{exec}, + cancel: sessionCancel, + createdAt: time.Now(), + } + e.mu.Unlock() + }, + ) + + if !mcpExecReceived { + if thinkingActive { + sendChunk(`{"content":""}`, "") + } + sendChunk(`{}`, `"stop"`) + sendDone() + sessionCancel() + stream.Close() + } + }() + + return &cliproxyexecutor.StreamResult{Chunks: chunks}, nil +} + +// --- H2Stream helpers --- + +func openCursorH2Stream(accessToken string) (*cursorproto.H2Stream, error) { + headers := map[string]string{ + ":path": cursorRunPath, + "content-type": "application/connect+proto", + "connect-protocol-version": "1", + "te": "trailers", + "authorization": "Bearer " + accessToken, + "x-ghost-mode": "true", + "x-cursor-client-version": cursorClientVersion, + "x-cursor-client-type": "cli", + "x-request-id": uuid.New().String(), + } + return cursorproto.DialH2Stream("api2.cursor.sh", headers) +} + +func cursorH2Heartbeat(ctx context.Context, stream *cursorproto.H2Stream) { + ticker := time.NewTicker(cursorHeartbeatInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + hb := cursorproto.EncodeHeartbeat() + frame := cursorproto.FrameConnectMessage(hb, 0) + if err := stream.Write(frame); err != nil { + return + } + } + } +} + +// --- Response processing --- + +func processH2SessionFrames( + ctx context.Context, + stream *cursorproto.H2Stream, + blobStore map[string][]byte, + mcpTools []cursorproto.McpToolDef, + onText func(text string, isThinking bool), + onMcpExec func(exec pendingMcpExec), +) { + var buf bytes.Buffer + rejectReason := "Tool not available in this environment. Use the MCP tools provided instead." + log.Debugf("cursor: processH2SessionFrames started for streamID=%s, waiting for data...", stream.ID()) + for { + select { + case <-ctx.Done(): + log.Debugf("cursor: processH2SessionFrames exiting: context done") + return + case data, ok := <-stream.Data(): + if !ok { + log.Debugf("cursor: processH2SessionFrames[%s]: exiting: stream data channel closed", stream.ID()) + return + } + // Log first 20 bytes of raw data for debugging + previewLen := min(20, len(data)) + log.Debugf("cursor: processH2SessionFrames[%s]: received %d bytes from dataCh, first bytes: %x (%q)", stream.ID(), len(data), data[:previewLen], string(data[:previewLen])) + buf.Write(data) + log.Debugf("cursor: processH2SessionFrames[%s]: buf total=%d", stream.ID(), buf.Len()) + + // Process all complete frames + for { + currentBuf := buf.Bytes() + if len(currentBuf) == 0 { + break + } + flags, payload, consumed, ok := cursorproto.ParseConnectFrame(currentBuf) + if !ok { + // Log detailed info about why parsing failed + previewLen := min(20, len(currentBuf)) + log.Debugf("cursor: incomplete frame in buffer, waiting for more data (buf=%d bytes, first bytes: %x = %q)", len(currentBuf), currentBuf[:previewLen], string(currentBuf[:previewLen])) + break + } + buf.Next(consumed) + log.Debugf("cursor: parsed Connect frame flags=0x%02x payload=%d bytes consumed=%d", flags, len(payload), consumed) + + if flags&cursorproto.ConnectEndStreamFlag != 0 { + if err := cursorproto.ParseConnectEndStream(payload); err != nil { + log.Warnf("cursor: connect end stream error: %v", err) + } + continue + } + + msg, err := cursorproto.DecodeAgentServerMessage(payload) + if err != nil { + log.Debugf("cursor: failed to decode server message: %v", err) + continue + } + + log.Debugf("cursor: decoded server message type=%d", msg.Type) + switch msg.Type { + case cursorproto.ServerMsgTextDelta: + if msg.Text != "" && onText != nil { + onText(msg.Text, false) + } + case cursorproto.ServerMsgThinkingDelta: + if msg.Text != "" && onText != nil { + onText(msg.Text, true) + } + case cursorproto.ServerMsgThinkingCompleted: + // Handled by caller + + case cursorproto.ServerMsgKvGetBlob: + blobKey := cursorproto.BlobIdHex(msg.BlobId) + data := blobStore[blobKey] + resp := cursorproto.EncodeKvGetBlobResult(msg.KvId, data) + stream.Write(cursorproto.FrameConnectMessage(resp, 0)) + + case cursorproto.ServerMsgKvSetBlob: + blobKey := cursorproto.BlobIdHex(msg.BlobId) + blobStore[blobKey] = append([]byte(nil), msg.BlobData...) + resp := cursorproto.EncodeKvSetBlobResult(msg.KvId) + stream.Write(cursorproto.FrameConnectMessage(resp, 0)) + + case cursorproto.ServerMsgExecRequestCtx: + resp := cursorproto.EncodeExecRequestContextResult(msg.ExecMsgId, msg.ExecId, mcpTools) + stream.Write(cursorproto.FrameConnectMessage(resp, 0)) + + case cursorproto.ServerMsgExecMcpArgs: + if onMcpExec != nil { + decodedArgs := decodeMcpArgsToJSON(msg.McpArgs) + toolCallId := msg.McpToolCallId + if toolCallId == "" { + toolCallId = uuid.New().String() + } + // Debug: log the received execId from server + log.Debugf("cursor: received mcpArgs from server: execMsgId=%d execId=%q toolName=%s toolCallId=%s", + msg.ExecMsgId, msg.ExecId, msg.McpToolName, toolCallId) + onMcpExec(pendingMcpExec{ + ExecMsgId: msg.ExecMsgId, + ExecId: msg.ExecId, + ToolCallId: toolCallId, + ToolName: msg.McpToolName, + Args: decodedArgs, + }) + return + } + + case cursorproto.ServerMsgExecReadArgs: + stream.Write(cursorproto.FrameConnectMessage(cursorproto.EncodeExecReadRejected(msg.ExecMsgId, msg.ExecId, msg.Path, rejectReason), 0)) + case cursorproto.ServerMsgExecWriteArgs: + stream.Write(cursorproto.FrameConnectMessage(cursorproto.EncodeExecWriteRejected(msg.ExecMsgId, msg.ExecId, msg.Path, rejectReason), 0)) + case cursorproto.ServerMsgExecDeleteArgs: + stream.Write(cursorproto.FrameConnectMessage(cursorproto.EncodeExecDeleteRejected(msg.ExecMsgId, msg.ExecId, msg.Path, rejectReason), 0)) + case cursorproto.ServerMsgExecLsArgs: + stream.Write(cursorproto.FrameConnectMessage(cursorproto.EncodeExecLsRejected(msg.ExecMsgId, msg.ExecId, msg.Path, rejectReason), 0)) + case cursorproto.ServerMsgExecGrepArgs: + stream.Write(cursorproto.FrameConnectMessage(cursorproto.EncodeExecGrepError(msg.ExecMsgId, msg.ExecId, rejectReason), 0)) + case cursorproto.ServerMsgExecShellArgs, cursorproto.ServerMsgExecShellStream: + stream.Write(cursorproto.FrameConnectMessage(cursorproto.EncodeExecShellRejected(msg.ExecMsgId, msg.ExecId, msg.Command, msg.WorkingDirectory, rejectReason), 0)) + case cursorproto.ServerMsgExecBgShellSpawn: + stream.Write(cursorproto.FrameConnectMessage(cursorproto.EncodeExecBackgroundShellSpawnRejected(msg.ExecMsgId, msg.ExecId, msg.Command, msg.WorkingDirectory, rejectReason), 0)) + case cursorproto.ServerMsgExecFetchArgs: + stream.Write(cursorproto.FrameConnectMessage(cursorproto.EncodeExecFetchError(msg.ExecMsgId, msg.ExecId, msg.Url, rejectReason), 0)) + case cursorproto.ServerMsgExecDiagnostics: + stream.Write(cursorproto.FrameConnectMessage(cursorproto.EncodeExecDiagnosticsResult(msg.ExecMsgId, msg.ExecId), 0)) + case cursorproto.ServerMsgExecWriteShellStdin: + stream.Write(cursorproto.FrameConnectMessage(cursorproto.EncodeExecWriteShellStdinError(msg.ExecMsgId, msg.ExecId, rejectReason), 0)) + } + } + + case <-stream.Done(): + log.Debugf("cursor: processH2SessionFrames exiting: stream done") + return + } + } +} + +// --- OpenAI request parsing --- + +type parsedOpenAIRequest struct { + Model string + Messages []gjson.Result + Tools []gjson.Result + Stream bool + SystemPrompt string + UserText string + Images []cursorproto.ImageData + Turns []cursorproto.TurnData + ToolResults []toolResultInfo +} + +type toolResultInfo struct { + ToolCallId string + Content string +} + +func parseOpenAIRequest(payload []byte) *parsedOpenAIRequest { + p := &parsedOpenAIRequest{ + Model: gjson.GetBytes(payload, "model").String(), + Stream: gjson.GetBytes(payload, "stream").Bool(), + } + + messages := gjson.GetBytes(payload, "messages").Array() + p.Messages = messages + + // Extract system prompt + var systemParts []string + for _, msg := range messages { + if msg.Get("role").String() == "system" { + systemParts = append(systemParts, extractTextContent(msg.Get("content"))) + } + } + if len(systemParts) > 0 { + p.SystemPrompt = strings.Join(systemParts, "\n") + } else { + p.SystemPrompt = "You are a helpful assistant." + } + + // Extract turns, tool results, and last user message + var pendingUser string + for _, msg := range messages { + role := msg.Get("role").String() + switch role { + case "system": + continue + case "tool": + p.ToolResults = append(p.ToolResults, toolResultInfo{ + ToolCallId: msg.Get("tool_call_id").String(), + Content: extractTextContent(msg.Get("content")), + }) + case "user": + if pendingUser != "" { + p.Turns = append(p.Turns, cursorproto.TurnData{UserText: pendingUser}) + } + pendingUser = extractTextContent(msg.Get("content")) + p.Images = extractImages(msg.Get("content")) + case "assistant": + if pendingUser != "" { + p.Turns = append(p.Turns, cursorproto.TurnData{ + UserText: pendingUser, + AssistantText: extractTextContent(msg.Get("content")), + }) + pendingUser = "" + } + } + } + + if pendingUser != "" { + p.UserText = pendingUser + } else if len(p.Turns) > 0 && len(p.ToolResults) == 0 { + last := p.Turns[len(p.Turns)-1] + p.Turns = p.Turns[:len(p.Turns)-1] + p.UserText = last.UserText + } + + // Extract tools + p.Tools = gjson.GetBytes(payload, "tools").Array() + + return p +} + +func extractTextContent(content gjson.Result) string { + if content.Type == gjson.String { + return content.String() + } + if content.IsArray() { + var parts []string + for _, part := range content.Array() { + if part.Get("type").String() == "text" { + parts = append(parts, part.Get("text").String()) + } + } + return strings.Join(parts, "") + } + return content.String() +} + +func extractImages(content gjson.Result) []cursorproto.ImageData { + if !content.IsArray() { + return nil + } + var images []cursorproto.ImageData + for _, part := range content.Array() { + if part.Get("type").String() == "image_url" { + url := part.Get("image_url.url").String() + if strings.HasPrefix(url, "data:") { + img := parseDataURL(url) + if img != nil { + images = append(images, *img) + } + } + } + } + return images +} + +func parseDataURL(url string) *cursorproto.ImageData { + // data:image/png;base64,... + if !strings.HasPrefix(url, "data:") { + return nil + } + parts := strings.SplitN(url[5:], ";", 2) + if len(parts) != 2 { + return nil + } + mimeType := parts[0] + if !strings.HasPrefix(parts[1], "base64,") { + return nil + } + encoded := parts[1][7:] + data, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + // Try RawStdEncoding for unpadded base64 + data, err = base64.RawStdEncoding.DecodeString(encoded) + if err != nil { + return nil + } + } + return &cursorproto.ImageData{ + MimeType: mimeType, + Data: data, + } +} + +func buildRunRequestParams(parsed *parsedOpenAIRequest) *cursorproto.RunRequestParams { + params := &cursorproto.RunRequestParams{ + ModelId: parsed.Model, + SystemPrompt: parsed.SystemPrompt, + UserText: parsed.UserText, + MessageId: uuid.New().String(), + ConversationId: uuid.New().String(), + Images: parsed.Images, + Turns: parsed.Turns, + BlobStore: make(map[string][]byte), + } + + // Convert OpenAI tools to McpToolDefs + for _, tool := range parsed.Tools { + fn := tool.Get("function") + params.McpTools = append(params.McpTools, cursorproto.McpToolDef{ + Name: fn.Get("name").String(), + Description: fn.Get("description").String(), + InputSchema: json.RawMessage(fn.Get("parameters").Raw), + }) + } + + return params +} + +// --- Helpers --- + +func cursorAccessToken(auth *cliproxyauth.Auth) string { + if auth == nil || auth.Metadata == nil { + return "" + } + if v, ok := auth.Metadata["access_token"].(string); ok { + return v + } + return "" +} + +func cursorRefreshToken(auth *cliproxyauth.Auth) string { + if auth == nil || auth.Metadata == nil { + return "" + } + if v, ok := auth.Metadata["refresh_token"].(string); ok { + return v + } + return "" +} + +func applyCursorHeaders(req *http.Request, accessToken string) { + req.Header.Set("Content-Type", "application/connect+proto") + req.Header.Set("Connect-Protocol-Version", "1") + req.Header.Set("Te", "trailers") + req.Header.Set("Authorization", "Bearer "+accessToken) + req.Header.Set("X-Ghost-Mode", "true") + req.Header.Set("X-Cursor-Client-Version", cursorClientVersion) + req.Header.Set("X-Cursor-Client-Type", "cli") + req.Header.Set("X-Request-Id", uuid.New().String()) +} + +func newH2Client() *http.Client { + return &http.Client{ + Transport: &http2.Transport{ + TLSClientConfig: &tls.Config{}, + }, + } +} + +func deriveSessionKey(model string, messages []gjson.Result) string { + var firstUserContent string + for _, msg := range messages { + if msg.Get("role").String() == "user" { + firstUserContent = extractTextContent(msg.Get("content")) + break + } + } + input := model + ":" + firstUserContent + if len(input) > 200 { + input = input[:200] + } + h := sha256.Sum256([]byte(input)) + return hex.EncodeToString(h[:])[:16] +} + +func sseChunk(id string, created int64, model string, delta string, finishReason string) cliproxyexecutor.StreamChunk { + fr := "null" + if finishReason != "" { + fr = finishReason + } + // Note: the framework's WriteChunk adds "data: " prefix and "\n\n" suffix, + // so we only output the raw JSON here. + data := fmt.Sprintf(`{"id":"%s","object":"chat.completion.chunk","created":%d,"model":"%s","choices":[{"index":0,"delta":%s,"finish_reason":%s}]}`, + id, created, model, delta, fr) + return cliproxyexecutor.StreamChunk{ + Payload: []byte(data), + } +} + +func jsonString(s string) string { + b, _ := json.Marshal(s) + return string(b) +} + +func decodeMcpArgsToJSON(args map[string][]byte) string { + if len(args) == 0 { + return "{}" + } + result := make(map[string]interface{}) + for k, v := range args { + // Try protobuf Value decoding first (matches TS: toJson(ValueSchema, fromBinary(ValueSchema, value))) + if decoded, err := cursorproto.ProtobufValueBytesToJSON(v); err == nil { + result[k] = decoded + } else { + // Fallback: try raw JSON + var jsonVal interface{} + if err := json.Unmarshal(v, &jsonVal); err == nil { + result[k] = jsonVal + } else { + result[k] = string(v) + } + } + } + b, _ := json.Marshal(result) + return string(b) +} + +// --- Model Discovery --- + +// FetchCursorModels retrieves available models from Cursor's API. +func FetchCursorModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *config.Config) []*registry.ModelInfo { + accessToken := cursorAccessToken(auth) + if accessToken == "" { + return GetCursorFallbackModels() + } + + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + // GetUsableModels is a unary RPC call (not streaming) + // Send an empty protobuf request + emptyReq := make([]byte, 0) + + h2Req, err := http.NewRequestWithContext(ctx, http.MethodPost, + cursorAPIURL+cursorModelsPath, bytes.NewReader(emptyReq)) + if err != nil { + log.Debugf("cursor: failed to create models request: %v", err) + return GetCursorFallbackModels() + } + + h2Req.Header.Set("Content-Type", "application/proto") + h2Req.Header.Set("Te", "trailers") + h2Req.Header.Set("Authorization", "Bearer "+accessToken) + h2Req.Header.Set("X-Ghost-Mode", "true") + h2Req.Header.Set("X-Cursor-Client-Version", cursorClientVersion) + h2Req.Header.Set("X-Cursor-Client-Type", "cli") + + client := newH2Client() + resp, err := client.Do(h2Req) + if err != nil { + log.Debugf("cursor: models request failed: %v", err) + return GetCursorFallbackModels() + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + log.Debugf("cursor: models request returned status %d", resp.StatusCode) + return GetCursorFallbackModels() + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return GetCursorFallbackModels() + } + + models := parseModelsResponse(body) + if len(models) == 0 { + return GetCursorFallbackModels() + } + return models +} + +func parseModelsResponse(data []byte) []*registry.ModelInfo { + // Try stripping Connect framing first + if len(data) >= cursorproto.ConnectFrameHeaderSize { + _, payload, _, ok := cursorproto.ParseConnectFrame(data) + if ok { + data = payload + } + } + + // The response is a GetUsableModelsResponse protobuf. + // We need to decode it manually - it contains a repeated "models" field. + // Based on the TS code, the response has a `models` field (repeated) containing + // model objects with modelId, displayName, thinkingDetails, etc. + + // For now, we'll try a simple decode approach + var models []*registry.ModelInfo + // Field 1 is likely "models" (repeated submessage) + for len(data) > 0 { + num, typ, n := consumeTag(data) + if n < 0 { + break + } + data = data[n:] + + if typ == 2 { // BytesType (submessage) + val, n := consumeBytes(data) + if n < 0 { + break + } + data = data[n:] + + if num == 1 { // models field + if m := parseModelEntry(val); m != nil { + models = append(models, m) + } + } + } else { + n := consumeFieldValue(num, typ, data) + if n < 0 { + break + } + data = data[n:] + } + } + + return models +} + +func parseModelEntry(data []byte) *registry.ModelInfo { + var modelId, displayName string + var hasThinking bool + + for len(data) > 0 { + num, typ, n := consumeTag(data) + if n < 0 { + break + } + data = data[n:] + + switch typ { + case 2: // BytesType + val, n := consumeBytes(data) + if n < 0 { + return nil + } + data = data[n:] + switch num { + case 1: // modelId + modelId = string(val) + case 2: // thinkingDetails + hasThinking = true + case 3: // displayModelId (use as fallback) + if displayName == "" { + displayName = string(val) + } + case 4: // displayName + displayName = string(val) + case 5: // displayNameShort + if displayName == "" { + displayName = string(val) + } + } + case 0: // VarintType + _, n := consumeVarint(data) + if n < 0 { + return nil + } + data = data[n:] + default: + n := consumeFieldValue(num, typ, data) + if n < 0 { + return nil + } + data = data[n:] + } + } + + if modelId == "" { + return nil + } + if displayName == "" { + displayName = modelId + } + + info := ®istry.ModelInfo{ + ID: modelId, + Object: "model", + Created: time.Now().Unix(), + OwnedBy: "cursor", + Type: cursorAuthType, + DisplayName: displayName, + ContextLength: 200000, + MaxCompletionTokens: 64000, + } + if hasThinking { + info.Thinking = ®istry.ThinkingSupport{ + Max: 50000, + DynamicAllowed: true, + } + } + return info +} + +// GetCursorFallbackModels returns hardcoded fallback models. +func GetCursorFallbackModels() []*registry.ModelInfo { + return []*registry.ModelInfo{ + {ID: "composer-2", Object: "model", OwnedBy: "cursor", Type: cursorAuthType, DisplayName: "Composer 2", ContextLength: 200000, MaxCompletionTokens: 64000, Thinking: ®istry.ThinkingSupport{Max: 50000, DynamicAllowed: true}}, + {ID: "claude-4-sonnet", Object: "model", OwnedBy: "cursor", Type: cursorAuthType, DisplayName: "Claude 4 Sonnet", ContextLength: 200000, MaxCompletionTokens: 64000, Thinking: ®istry.ThinkingSupport{Max: 50000, DynamicAllowed: true}}, + {ID: "claude-3.5-sonnet", Object: "model", OwnedBy: "cursor", Type: cursorAuthType, DisplayName: "Claude 3.5 Sonnet", ContextLength: 200000, MaxCompletionTokens: 8192}, + {ID: "gpt-4o", Object: "model", OwnedBy: "cursor", Type: cursorAuthType, DisplayName: "GPT-4o", ContextLength: 128000, MaxCompletionTokens: 16384}, + {ID: "cursor-small", Object: "model", OwnedBy: "cursor", Type: cursorAuthType, DisplayName: "Cursor Small", ContextLength: 200000, MaxCompletionTokens: 64000}, + {ID: "gemini-2.5-pro", Object: "model", OwnedBy: "cursor", Type: cursorAuthType, DisplayName: "Gemini 2.5 Pro", ContextLength: 1000000, MaxCompletionTokens: 65536, Thinking: ®istry.ThinkingSupport{Max: 50000, DynamicAllowed: true}}, + } +} + +// Low-level protowire helpers (avoid importing protowire in executor) +func consumeTag(b []byte) (num int, typ int, n int) { + v, n := consumeVarint(b) + if n < 0 { + return 0, 0, -1 + } + return int(v >> 3), int(v & 7), n +} + +func consumeVarint(b []byte) (uint64, int) { + var val uint64 + for i := 0; i < len(b) && i < 10; i++ { + val |= uint64(b[i]&0x7f) << (7 * i) + if b[i]&0x80 == 0 { + return val, i + 1 + } + } + return 0, -1 +} + +func consumeBytes(b []byte) ([]byte, int) { + length, n := consumeVarint(b) + if n < 0 || int(length) > len(b)-n { + return nil, -1 + } + return b[n : n+int(length)], n + int(length) +} + +func consumeFieldValue(num, typ int, b []byte) int { + switch typ { + case 0: // Varint + _, n := consumeVarint(b) + return n + case 1: // 64-bit + if len(b) < 8 { + return -1 + } + return 8 + case 2: // Length-delimited + _, n := consumeBytes(b) + return n + case 5: // 32-bit + if len(b) < 4 { + return -1 + } + return 4 + default: + return -1 + } +} diff --git a/internal/util/provider.go b/internal/util/provider.go index 15351354..a9c1de0c 100644 --- a/internal/util/provider.go +++ b/internal/util/provider.go @@ -57,6 +57,12 @@ func GetProviderName(modelName string) []string { return providers } + // Fallback: if cursor provider has registered models, route unknown models to it. + // Cursor acts as a universal proxy supporting multiple model families (Claude, GPT, Gemini, etc.). + if models := registry.GetGlobalRegistry().GetAvailableModelsByProvider("cursor"); len(models) > 0 { + return []string{"cursor"} + } + return providers } diff --git a/sdk/auth/cursor.go b/sdk/auth/cursor.go new file mode 100644 index 00000000..86cad880 --- /dev/null +++ b/sdk/auth/cursor.go @@ -0,0 +1,91 @@ +package auth + +import ( + "context" + "fmt" + "time" + + cursorauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/cursor" + "github.com/router-for-me/CLIProxyAPI/v6/internal/browser" + "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" + log "github.com/sirupsen/logrus" +) + +// CursorAuthenticator implements OAuth PKCE login for Cursor. +type CursorAuthenticator struct{} + +// NewCursorAuthenticator constructs a new Cursor authenticator. +func NewCursorAuthenticator() Authenticator { + return &CursorAuthenticator{} +} + +// Provider returns the provider key for cursor. +func (CursorAuthenticator) Provider() string { + return "cursor" +} + +// RefreshLead returns the time before expiry when a refresh should be attempted. +func (CursorAuthenticator) RefreshLead() *time.Duration { + d := 10 * time.Minute + return &d +} + +// Login initiates the Cursor PKCE authentication flow. +func (a CursorAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) { + if cfg == nil { + return nil, fmt.Errorf("cursor auth: configuration is required") + } + if opts == nil { + opts = &LoginOptions{} + } + + // Generate PKCE auth parameters + authParams, err := cursorauth.GenerateAuthParams() + if err != nil { + return nil, fmt.Errorf("cursor: failed to generate auth params: %w", err) + } + + // Display the login URL + fmt.Println("Starting Cursor authentication...") + fmt.Printf("\nPlease visit this URL to log in:\n%s\n\n", authParams.LoginURL) + + // Try to open the browser automatically + if !opts.NoBrowser { + if browser.IsAvailable() { + if errOpen := browser.OpenURL(authParams.LoginURL); errOpen != nil { + log.Warnf("Failed to open browser automatically: %v", errOpen) + } + } + } + + fmt.Println("Waiting for Cursor authorization...") + + // Poll for the auth result + tokens, err := cursorauth.PollForAuth(ctx, authParams.UUID, authParams.Verifier) + if err != nil { + return nil, fmt.Errorf("cursor: authentication failed: %w", err) + } + + expiresAt := cursorauth.GetTokenExpiry(tokens.AccessToken) + + fmt.Println("\nCursor authentication successful!") + + metadata := map[string]any{ + "type": "cursor", + "access_token": tokens.AccessToken, + "refresh_token": tokens.RefreshToken, + "expires_at": expiresAt.Format(time.RFC3339), + "timestamp": time.Now().UnixMilli(), + } + + fileName := "cursor.json" + + return &coreauth.Auth{ + ID: fileName, + Provider: a.Provider(), + FileName: fileName, + Label: "cursor-user", + Metadata: metadata, + }, nil +} diff --git a/sdk/auth/refresh_registry.go b/sdk/auth/refresh_registry.go index 651ba540..59c58bee 100644 --- a/sdk/auth/refresh_registry.go +++ b/sdk/auth/refresh_registry.go @@ -19,6 +19,7 @@ func init() { registerRefreshLead("github-copilot", func() Authenticator { return NewGitHubCopilotAuthenticator() }) registerRefreshLead("gitlab", func() Authenticator { return NewGitLabAuthenticator() }) registerRefreshLead("codebuddy", func() Authenticator { return NewCodeBuddyAuthenticator() }) + registerRefreshLead("cursor", func() Authenticator { return NewCursorAuthenticator() }) } func registerRefreshLead(provider string, factory func() Authenticator) { diff --git a/sdk/cliproxy/auth/scheduler.go b/sdk/cliproxy/auth/scheduler.go index bfff53bf..a9c51405 100644 --- a/sdk/cliproxy/auth/scheduler.go +++ b/sdk/cliproxy/auth/scheduler.go @@ -545,6 +545,11 @@ func (m *scheduledAuthMeta) supportsModel(modelKey string) bool { if modelKey == "" { return true } + // Cursor acts as a universal proxy supporting multiple model families. + // Allow any model to be routed to cursor auth. + if m.providerKey == "cursor" { + return true + } if len(m.supportedModelSet) == 0 { return false } diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index b2e83592..6b5e2042 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -441,6 +441,8 @@ func (s *Service) ensureExecutorsForAuthWithMode(a *coreauth.Auth, forceReplace s.coreManager.RegisterExecutor(executor.NewKiroExecutor(s.cfg)) case "kilo": s.coreManager.RegisterExecutor(executor.NewKiloExecutor(s.cfg)) + case "cursor": + s.coreManager.RegisterExecutor(executor.NewCursorExecutor(s.cfg)) case "github-copilot": s.coreManager.RegisterExecutor(executor.NewGitHubCopilotExecutor(s.cfg)) case "codebuddy": @@ -942,6 +944,11 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) { case "kimi": models = registry.GetKimiModels() models = applyExcludedModels(models, excluded) + case "cursor": + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + models = executor.FetchCursorModels(ctx, a, s.cfg) + models = applyExcludedModels(models, excluded) case "github-copilot": ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() diff --git a/test_cursor.sh b/test_cursor.sh new file mode 100755 index 00000000..985685fa --- /dev/null +++ b/test_cursor.sh @@ -0,0 +1,309 @@ +#!/bin/bash +# Test script for Cursor proxy integration +# Usage: +# ./test_cursor.sh login - Login to Cursor (opens browser) +# ./test_cursor.sh start - Build and start the server +# ./test_cursor.sh test - Run API tests against running server +# ./test_cursor.sh all - Login + Start + Test (full flow) + +set -e + +export PATH="/opt/homebrew/bin:$PATH" +export GOROOT="/opt/homebrew/Cellar/go/1.26.1/libexec" + +PROJECT_DIR="/Volumes/Personal/cursor-cli-proxy/CLIProxyAPIPlus" +BINARY="$PROJECT_DIR/cliproxy-test" +API_KEY="quotio-local-D6ABC285-3085-44B4-B872-BD269888811F" +BASE_URL="http://127.0.0.1:8317" +CONFIG="$PROJECT_DIR/config-cursor-test.yaml" +PID_FILE="/tmp/cliproxy-test.pid" + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# --- Build --- +build() { + info "Building CLIProxyAPIPlus..." + cd "$PROJECT_DIR" + go build -o "$BINARY" ./cmd/server/ + info "Build successful: $BINARY" +} + +# --- Create test config --- +create_config() { + cat > "$CONFIG" << 'EOF' +host: '127.0.0.1' +port: 8317 +auth-dir: '~/.cli-proxy-api' +api-keys: + - 'quotio-local-D6ABC285-3085-44B4-B872-BD269888811F' +debug: true +EOF + info "Test config created: $CONFIG" +} + +# --- Login --- +do_login() { + build + create_config + info "Starting Cursor login (will open browser)..." + "$BINARY" --config "$CONFIG" --cursor-login +} + +# --- Start server --- +start_server() { + # Kill any existing instance + stop_server 2>/dev/null || true + + build + create_config + + info "Starting server on port 8317..." + "$BINARY" --config "$CONFIG" & + SERVER_PID=$! + echo "$SERVER_PID" > "$PID_FILE" + info "Server started (PID: $SERVER_PID)" + + # Wait for server to be ready + info "Waiting for server to be ready..." + for i in $(seq 1 15); do + if curl -s "$BASE_URL/v1/models" -H "Authorization: Bearer $API_KEY" > /dev/null 2>&1; then + info "Server is ready!" + return 0 + fi + sleep 1 + done + error "Server failed to start within 15 seconds" + return 1 +} + +# --- Stop server --- +stop_server() { + if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if kill -0 "$PID" 2>/dev/null; then + info "Stopping server (PID: $PID)..." + kill "$PID" + rm -f "$PID_FILE" + fi + fi + # Also kill any stale process on port 8317 + lsof -ti:8317 2>/dev/null | xargs kill 2>/dev/null || true +} + +# --- Test: List models --- +test_models() { + info "Testing GET /v1/models (looking for cursor models)..." + RESPONSE=$(curl -s "$BASE_URL/v1/models" \ + -H "Authorization: Bearer $API_KEY") + + CURSOR_MODELS=$(echo "$RESPONSE" | python3 -c " +import json, sys +try: + data = json.load(sys.stdin) + models = [m['id'] for m in data.get('data', []) if m.get('owned_by') == 'cursor' or m.get('type') == 'cursor'] + if models: + print('\n'.join(models)) + else: + print('NONE') +except: + print('ERROR') +" 2>/dev/null || echo "PARSE_ERROR") + + if [ "$CURSOR_MODELS" = "NONE" ] || [ "$CURSOR_MODELS" = "ERROR" ] || [ "$CURSOR_MODELS" = "PARSE_ERROR" ]; then + warn "No cursor models found. Have you run '--cursor-login' first?" + echo " Response preview: $(echo "$RESPONSE" | head -c 200)" + return 1 + else + info "Found cursor models:" + echo "$CURSOR_MODELS" | while read -r model; do + echo " - $model" + done + return 0 + fi +} + +# --- Test: Chat completion (streaming) --- +test_chat_stream() { + local model="${1:-cursor-small}" + info "Testing POST /v1/chat/completions (stream, model=$model)..." + + RESPONSE=$(curl -s --max-time 30 "$BASE_URL/v1/chat/completions" \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"$model\", + \"messages\": [{\"role\": \"user\", \"content\": \"Say hello in exactly 3 words.\"}], + \"stream\": true + }" 2>&1) + + # Check if we got SSE data + if echo "$RESPONSE" | grep -q "data:"; then + # Extract content from SSE chunks + CONTENT=$(echo "$RESPONSE" | grep "^data: " | grep -v "\[DONE\]" | while read -r line; do + echo "${line#data: }" | python3 -c " +import json, sys +try: + chunk = json.load(sys.stdin) + delta = chunk.get('choices', [{}])[0].get('delta', {}) + content = delta.get('content', '') + if content: + sys.stdout.write(content) +except: + pass +" 2>/dev/null + done) + + if [ -n "$CONTENT" ]; then + info "Stream response received:" + echo " Content: $CONTENT" + return 0 + else + warn "Got SSE chunks but no content extracted" + echo " Raw (first 500 chars): $(echo "$RESPONSE" | head -c 500)" + return 1 + fi + else + error "No SSE data received" + echo " Response: $(echo "$RESPONSE" | head -c 300)" + return 1 + fi +} + +# --- Test: Chat completion (non-streaming) --- +test_chat_nonstream() { + local model="${1:-cursor-small}" + info "Testing POST /v1/chat/completions (non-stream, model=$model)..." + + RESPONSE=$(curl -s --max-time 30 "$BASE_URL/v1/chat/completions" \ + -H "Authorization: Bearer $API_KEY" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"$model\", + \"messages\": [{\"role\": \"user\", \"content\": \"What is 2+2? Answer with just the number.\"}], + \"stream\": false + }" 2>&1) + + CONTENT=$(echo "$RESPONSE" | python3 -c " +import json, sys +try: + data = json.load(sys.stdin) + content = data['choices'][0]['message']['content'] + print(content) +except Exception as e: + print(f'ERROR: {e}') +" 2>/dev/null || echo "PARSE_ERROR") + + if echo "$CONTENT" | grep -q "ERROR\|PARSE_ERROR"; then + error "Non-streaming request failed" + echo " Response: $(echo "$RESPONSE" | head -c 300)" + return 1 + else + info "Non-stream response received:" + echo " Content: $CONTENT" + return 0 + fi +} + +# --- Run all tests --- +run_tests() { + local passed=0 + local failed=0 + + echo "" + echo "=========================================" + echo " Cursor Proxy Integration Tests" + echo "=========================================" + echo "" + + # Test 1: Models + if test_models; then + ((passed++)) + else + ((failed++)) + fi + echo "" + + # Test 2: Streaming chat + if test_chat_stream "cursor-small"; then + ((passed++)) + else + ((failed++)) + fi + echo "" + + # Test 3: Non-streaming chat + if test_chat_nonstream "cursor-small"; then + ((passed++)) + else + ((failed++)) + fi + echo "" + + echo "=========================================" + echo " Results: ${passed} passed, ${failed} failed" + echo "=========================================" + + [ "$failed" -eq 0 ] +} + +# --- Cleanup --- +cleanup() { + stop_server + rm -f "$BINARY" "$CONFIG" + info "Cleaned up." +} + +# --- Main --- +case "${1:-help}" in + login) + do_login + ;; + start) + start_server + info "Server running. Use './test_cursor.sh test' to run tests." + info "Use './test_cursor.sh stop' to stop." + ;; + stop) + stop_server + ;; + test) + run_tests + ;; + all) + info "=== Full flow: login -> start -> test ===" + echo "" + info "Step 1: Login to Cursor" + do_login + echo "" + info "Step 2: Start server" + start_server + echo "" + info "Step 3: Run tests" + sleep 2 + run_tests + echo "" + info "Step 4: Cleanup" + stop_server + ;; + clean) + cleanup + ;; + *) + echo "Usage: $0 {login|start|stop|test|all|clean}" + echo "" + echo " login - Authenticate with Cursor (opens browser)" + echo " start - Build and start the proxy server" + echo " stop - Stop the running server" + echo " test - Run API tests against running server" + echo " all - Full flow: login + start + test" + echo " clean - Stop server and remove artifacts" + ;; +esac