package pluginhost import ( "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "runtime" "sort" "strconv" "strings" "time" "github.com/router-for-me/CLIProxyAPI/v8/internal/registry" coreauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth" "github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi" ) type rpcHostAuthGetRequest struct { AuthIndex string `json:"auth_index"` } type rpcHostAuthListResponse struct { Files []pluginapi.HostAuthFileEntry `json:"files"` } type rpcHostAuthGetResponse struct { AuthIndex string `json:"auth_index"` Name string `json:"name,omitempty"` Path string `json:"path,omitempty"` JSON json.RawMessage `json:"json"` } func (h *Host) SetAuthManager(manager *coreauth.Manager) { if h == nil { return } h.mu.Lock() h.authManager = manager h.mu.Unlock() } func (h *Host) currentAuthManager() *coreauth.Manager { if h == nil { return nil } h.mu.Lock() manager := h.authManager h.mu.Unlock() return manager } func (h *Host) callHostAuthList(ctx context.Context, request []byte) ([]byte, error) { _ = ctx if len(bytesTrimSpace(request)) > 0 { var req map[string]any if errUnmarshal := json.Unmarshal(request, &req); errUnmarshal != nil { return nil, fmt.Errorf("decode host auth list request: %w", errUnmarshal) } } entries, errList := h.listAuthFiles() if errList != nil { return nil, errList } return marshalRPCResult(rpcHostAuthListResponse{Files: entries}) } func (h *Host) callHostAuthGet(ctx context.Context, request []byte) ([]byte, error) { _ = ctx var req rpcHostAuthGetRequest if errUnmarshal := json.Unmarshal(request, &req); errUnmarshal != nil { return nil, fmt.Errorf("decode host auth get request: %w", errUnmarshal) } authIndex := strings.TrimSpace(req.AuthIndex) if authIndex == "" { return nil, fmt.Errorf("auth_index is required") } auth, rawJSON, errGet := h.authPhysicalJSONByIndex(authIndex) if errGet != nil { return nil, errGet } name := strings.TrimSpace(auth.FileName) if name == "" { name = strings.TrimSpace(auth.ID) } path := strings.TrimSpace(authAttribute(auth, "path")) return marshalRPCResult(rpcHostAuthGetResponse{ AuthIndex: authIndex, Name: name, Path: path, JSON: json.RawMessage(rawJSON), }) } func (h *Host) callHostAuthGetRuntime(ctx context.Context, request []byte) ([]byte, error) { _ = ctx var req rpcHostAuthGetRequest if errUnmarshal := json.Unmarshal(request, &req); errUnmarshal != nil { return nil, fmt.Errorf("decode host auth get runtime request: %w", errUnmarshal) } authIndex := strings.TrimSpace(req.AuthIndex) if authIndex == "" { return nil, fmt.Errorf("auth_index is required") } auth, errGet := h.authByIndex(authIndex) if errGet != nil { return nil, errGet } entry := h.buildHostAuthFileEntry(auth) if entry == nil { return nil, fmt.Errorf("auth runtime info not found for auth_index %s", authIndex) } return marshalRPCResult(pluginapi.HostAuthGetRuntimeResponse{Auth: *entry}) } func (h *Host) callHostAuthSave(ctx context.Context, request []byte) ([]byte, error) { var req pluginapi.HostAuthSaveRequest if errUnmarshal := json.Unmarshal(request, &req); errUnmarshal != nil { return nil, fmt.Errorf("decode host auth save request: %w", errUnmarshal) } name, rawJSON, errValidate := validateHostAuthSaveRequest(req) if errValidate != nil { return nil, errValidate } path, errSave := h.saveAuthFile(ctx, name, rawJSON) if errSave != nil { return nil, errSave } return marshalRPCResult(pluginapi.HostAuthSaveResponse{ Name: name, Path: path, }) } func (h *Host) callHostAuthModels(ctx context.Context, request []byte) ([]byte, error) { _ = ctx var req pluginapi.HostAuthModelsRequest if len(bytesTrimSpace(request)) > 0 { if errUnmarshal := json.Unmarshal(request, &req); errUnmarshal != nil { return nil, fmt.Errorf("decode host auth models request: %w", errUnmarshal) } } reg := registry.GetGlobalRegistry() authIndex := strings.TrimSpace(req.AuthIndex) if authIndex != "" { auth, errGet := h.authByIndex(authIndex) if errGet != nil { return nil, errGet } return marshalRPCResult(pluginapi.HostAuthModelsResponse{ Credentials: []pluginapi.HostAuthModels{buildHostAuthModels(auth, reg)}, }) } manager := h.currentAuthManager() if manager == nil { return marshalRPCResult(pluginapi.HostAuthModelsResponse{ Credentials: []pluginapi.HostAuthModels{}, }) } auths := manager.List() credentials := make([]pluginapi.HostAuthModels, 0, len(auths)) for _, auth := range auths { if auth == nil { continue } credentials = append(credentials, buildHostAuthModels(auth, reg)) } sort.Slice(credentials, func(i, j int) bool { leftID := strings.ToLower(credentials[i].ID) rightID := strings.ToLower(credentials[j].ID) if leftID != rightID { return leftID < rightID } if credentials[i].ID != credentials[j].ID { return credentials[i].ID < credentials[j].ID } return credentials[i].AuthIndex < credentials[j].AuthIndex }) return marshalRPCResult(pluginapi.HostAuthModelsResponse{ Credentials: credentials, }) } func buildHostAuthModels(auth *coreauth.Auth, reg *registry.ModelRegistry) pluginapi.HostAuthModels { auth.EnsureIndex() var regModels []*registry.ModelInfo if reg != nil && strings.TrimSpace(auth.ID) != "" { regModels = reg.GetModelsForClient(auth.ID) } models := make([]string, 0, len(regModels)) for _, m := range regModels { if m == nil { continue } if id := strings.TrimSpace(m.ID); id != "" { models = append(models, id) } } return pluginapi.HostAuthModels{ ID: auth.ID, AuthIndex: auth.Index, Provider: strings.TrimSpace(auth.Provider), Models: models, } } func (h *Host) listAuthFiles() ([]pluginapi.HostAuthFileEntry, error) { manager := h.currentAuthManager() if manager != nil { auths := manager.List() entries := make([]pluginapi.HostAuthFileEntry, 0, len(auths)) for _, auth := range auths { if entry := h.buildHostAuthFileEntry(auth); entry != nil { entries = append(entries, *entry) } } sort.Slice(entries, func(i, j int) bool { return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name) }) return entries, nil } return h.listAuthFilesFromDisk() } func (h *Host) listAuthFilesFromDisk() ([]pluginapi.HostAuthFileEntry, error) { authDir := h.resolvedAuthDir() if authDir == "" { return nil, fmt.Errorf("auth directory is unavailable") } entries, errReadDir := os.ReadDir(authDir) if errReadDir != nil { return nil, fmt.Errorf("failed to read auth dir: %w", errReadDir) } files := make([]pluginapi.HostAuthFileEntry, 0) for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() if !strings.HasSuffix(strings.ToLower(name), ".json") { continue } full := filepath.Join(authDir, name) fileEntry := pluginapi.HostAuthFileEntry{ Name: name, Source: "file", Path: full, } if info, errInfo := entry.Info(); errInfo == nil { fileEntry.Size = info.Size() fileEntry.ModTime = info.ModTime() } if data, errRead := os.ReadFile(full); errRead == nil { var metadata map[string]any if errUnmarshal := json.Unmarshal(data, &metadata); errUnmarshal == nil { if provider, ok := metadata["type"].(string); ok { fileEntry.Type = strings.TrimSpace(provider) fileEntry.Provider = fileEntry.Type } if email, ok := metadata["email"].(string); ok { fileEntry.Email = strings.TrimSpace(email) } if projectID, ok := metadata["project_id"].(string); ok { fileEntry.ProjectID = strings.TrimSpace(projectID) } if rawPriority, ok := metadata["priority"]; ok { if priority, okPriority := parsePriorityValue(rawPriority); okPriority { fileEntry.Priority = priority } } if note, ok := metadata["note"].(string); ok { fileEntry.Note = strings.TrimSpace(note) } if baseURL, ok := metadata["base_url"].(string); ok { fileEntry.BaseURL = strings.TrimSpace(baseURL) } if websockets, okWebsockets := parseWebsocketsValue(metadata["websockets"]); okWebsockets { fileEntry.Websockets = websockets } if disabled, okDisabled := parseBoolValue(metadata["disabled"]); okDisabled && disabled { fileEntry.Disabled = true fileEntry.Status = string(coreauth.StatusDisabled) } else { fileEntry.Status = string(coreauth.StatusActive) } } } files = append(files, fileEntry) } sort.Slice(files, func(i, j int) bool { return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name) }) return files, nil } func (h *Host) authByIndex(authIndex string) (*coreauth.Auth, error) { authIndex = strings.TrimSpace(authIndex) if authIndex == "" { return nil, fmt.Errorf("auth_index is required") } manager := h.currentAuthManager() if manager == nil { return nil, fmt.Errorf("core auth manager unavailable") } for _, auth := range manager.List() { if auth == nil { continue } auth.EnsureIndex() if auth.Index == authIndex { return auth, nil } } return nil, fmt.Errorf("auth not found for auth_index %s", authIndex) } func (h *Host) authPhysicalJSONByIndex(authIndex string) (*coreauth.Auth, []byte, error) { auth, errGet := h.authByIndex(authIndex) if errGet != nil { return nil, nil, errGet } path := strings.TrimSpace(authAttribute(auth, "path")) if path == "" { return nil, nil, fmt.Errorf("auth file path not found for auth_index %s", authIndex) } data, errRead := os.ReadFile(path) if errRead != nil { if os.IsNotExist(errRead) { return nil, nil, fmt.Errorf("auth file not found for auth_index %s", authIndex) } return nil, nil, fmt.Errorf("failed to read auth file: %w", errRead) } if len(bytesTrimSpace(data)) == 0 { return nil, nil, fmt.Errorf("auth file is empty for auth_index %s", authIndex) } var metadata map[string]any if errUnmarshal := json.Unmarshal(data, &metadata); errUnmarshal != nil { return nil, nil, fmt.Errorf("invalid auth file for auth_index %s: %w", authIndex, errUnmarshal) } return auth, data, nil } func validateHostAuthSaveRequest(req pluginapi.HostAuthSaveRequest) (string, []byte, error) { name := strings.TrimSpace(req.Name) if isUnsafeAuthFileName(name) { return "", nil, fmt.Errorf("invalid auth file name") } if !strings.HasSuffix(strings.ToLower(name), ".json") { return "", nil, fmt.Errorf("auth file name must end with .json") } rawJSON := bytesTrimSpace(req.JSON) if len(rawJSON) == 0 { return "", nil, fmt.Errorf("json is required") } var metadata map[string]any if errUnmarshal := json.Unmarshal(rawJSON, &metadata); errUnmarshal != nil { return "", nil, fmt.Errorf("invalid auth json: %w", errUnmarshal) } return filepath.Base(name), rawJSON, nil } func (h *Host) saveAuthFile(ctx context.Context, name string, data []byte) (string, error) { authDir := h.resolvedAuthDir() if authDir == "" { return "", fmt.Errorf("auth directory is unavailable") } dst := filepath.Join(authDir, filepath.Base(name)) if !filepath.IsAbs(dst) { if abs, errAbs := filepath.Abs(dst); errAbs == nil { dst = abs } } auths, errBuild := h.buildAuthsFromFileData(ctx, dst, data) if errBuild != nil { return "", errBuild } if errWrite := os.WriteFile(dst, data, 0o600); errWrite != nil { return "", fmt.Errorf("failed to write auth file: %w", errWrite) } if errUpsert := h.upsertAuthRecordsForPath(ctx, dst, auths); errUpsert != nil { return "", errUpsert } return dst, nil } func (h *Host) buildAuthsFromFileData(ctx context.Context, path string, data []byte) ([]*coreauth.Auth, error) { if strings.TrimSpace(path) == "" { return nil, fmt.Errorf("auth path is empty") } if data == nil { var errRead error data, errRead = os.ReadFile(path) if errRead != nil { return nil, fmt.Errorf("failed to read auth file: %w", errRead) } } metadata := make(map[string]any) if errUnmarshal := json.Unmarshal(data, &metadata); errUnmarshal != nil { return nil, fmt.Errorf("invalid auth file: %w", errUnmarshal) } coreauth.NormalizeCredentialMetadata(metadata) if errWeight := coreauth.ValidateAuthWeight(&coreauth.Auth{Metadata: metadata}); errWeight != nil { return nil, fmt.Errorf("invalid auth weight: %w", errWeight) } provider, _ := metadata["type"].(string) provider = strings.TrimSpace(provider) fileName := filepath.Base(path) defaultAuthID := h.authIDForPath(path) if defaultAuthID == "" { defaultAuthID = path } disabled, okDisabled := parseBoolValue(metadata["disabled"]) now := time.Now().UTC() parsedAuths, handled, errParse := h.ParseAuths(ctx, pluginapi.AuthParseRequest{ Provider: provider, Path: path, FileName: defaultAuthID, RawJSON: data, }) if errParse != nil { return nil, fmt.Errorf("invalid auth file: %w", errParse) } if handled { out := make([]*coreauth.Auth, 0, len(parsedAuths)) for _, auth := range parsedAuths { if auth == nil { continue } if errWeight := coreauth.ValidateAuthWeight(auth); errWeight != nil { return nil, fmt.Errorf("invalid auth weight: %w", errWeight) } out = append(out, auth) } for index, auth := range out { coreauth.NormalizeCredentialMetadata(auth.Metadata) if len(out) == 1 { if auth.Metadata == nil { auth.Metadata = make(map[string]any, len(metadata)) } for key, value := range metadata { if _, exists := auth.Metadata[key]; !exists { auth.Metadata[key] = value } } if pts, ok := auth.Storage.(*pluginTokenStorage); ok && pts != nil && len(bytes.TrimSpace(pts.rawJSON)) == 0 { pts.rawJSON = bytes.Clone(data) } } if len(out) > 1 { coreauth.MarkPluginVirtualAuth(auth, path, index) } if strings.TrimSpace(auth.FileName) == "" { auth.FileName = fileName } if len(out) == 1 && (strings.TrimSpace(auth.ID) == "" || auth.ID == defaultAuthID) { if existing := h.findExistingAuthForSave(auth.ID, path, true); existing != nil && strings.TrimSpace(existing.ID) != "" { auth.ID = strings.TrimSpace(existing.ID) } } if strings.TrimSpace(auth.ID) == "" { auth.ID = defaultAuthID } auth.CreatedAt = now auth.UpdatedAt = now if auth.Attributes == nil { auth.Attributes = make(map[string]string) } auth.Attributes[coreauth.AttributePath] = path auth.Attributes[coreauth.AttributeSource] = path auth.Attributes[coreauth.AttributeSourceBackend] = coreauth.AuthSourceFile if okDisabled && disabled { auth.Disabled = true auth.Status = coreauth.StatusDisabled if auth.Metadata == nil { auth.Metadata = make(map[string]any) } auth.Metadata["disabled"] = true } if p, ok := metadata["proxy_url"].(string); ok && auth.ProxyURL == "" { auth.ProxyURL = strings.TrimSpace(p) } if pref, ok := metadata["prefix"].(string); ok && auth.Prefix == "" { auth.Prefix = strings.Trim(strings.TrimSpace(pref), "/") } if errWeight := coreauth.ApplyAuthWeightMetadata(auth, metadata); errWeight != nil { return nil, fmt.Errorf("invalid auth weight: %w", errWeight) } coreauth.ApplyAuthPriorityMetadata(auth, metadata) if _, inherited := auth.Attributes[coreauth.AttributeFilePriority]; inherited { if setter, ok := auth.Storage.(interface{ SetMetadata(map[string]any) }); ok { setter.SetMetadata(auth.Metadata) } } coreauth.ApplyCustomHeadersFromMetadata(auth) if existing := h.findExistingAuthForSave(auth.ID, path, len(out) == 1); existing != nil { preserveExistingAuthState(auth, existing) } } return out, nil } if provider == "" { provider = "unknown" } label := provider if email, ok := metadata["email"].(string); ok && strings.TrimSpace(email) != "" { label = strings.TrimSpace(email) } authID := defaultAuthID existing := h.findExistingAuthForSave(authID, path, true) if existing != nil && strings.TrimSpace(existing.ID) != "" { authID = strings.TrimSpace(existing.ID) } status := coreauth.StatusActive if okDisabled && disabled { status = coreauth.StatusDisabled } else { disabled = false } auth := &coreauth.Auth{ ID: authID, Provider: provider, FileName: fileName, Label: label, Status: status, Disabled: disabled, Attributes: map[string]string{ coreauth.AttributePath: path, coreauth.AttributeSource: path, coreauth.AttributeSourceBackend: coreauth.AuthSourceFile, }, Metadata: metadata, CreatedAt: now, UpdatedAt: now, } if existing != nil { preserveExistingAuthState(auth, existing) } if errWeight := coreauth.ApplyAuthWeightMetadata(auth, metadata); errWeight != nil { return nil, fmt.Errorf("invalid auth weight: %w", errWeight) } coreauth.ApplyAuthPriorityMetadata(auth, metadata) coreauth.ApplyCustomHeadersFromMetadata(auth) return []*coreauth.Auth{auth}, nil } func preserveExistingAuthState(dst, existing *coreauth.Auth) { if dst == nil || existing == nil { return } if !existing.CreatedAt.IsZero() { dst.CreatedAt = existing.CreatedAt } dst.LastRefreshedAt = existing.LastRefreshedAt dst.NextRetryAfter = existing.NextRetryAfter dst.NextRefreshAfter = existing.NextRefreshAfter dst.Runtime = existing.Runtime if !existing.Disabled && existing.Status != coreauth.StatusDisabled && !dst.Disabled && dst.Status != coreauth.StatusDisabled { if len(dst.ModelStates) == 0 && len(existing.ModelStates) > 0 { dst.ModelStates = existing.ModelStates } } } func (h *Host) findExistingAuthForSave(authID, path string, allowPathFallback bool) *coreauth.Auth { manager := h.currentAuthManager() if manager == nil { return nil } authID = strings.TrimSpace(authID) defaultID := h.authIDForPath(path) if authID != "" && (!allowPathFallback || authID != defaultID) { if existing, ok := manager.GetByID(authID); ok && existing != nil { return existing } } if !allowPathFallback { return nil } var matched *coreauth.Auth for _, candidate := range manager.List() { if candidate == nil || isRuntimeOnlyAuth(candidate) || coreauth.IsPluginVirtualAuth(candidate) { continue } if !authMatchesPhysicalFile(candidate, path) { continue } if matched == nil || (matched.ID == defaultID && candidate.ID != defaultID) { matched = candidate } } if matched != nil { return matched } if authID != "" { if existing, ok := manager.GetByID(authID); ok && existing != nil { return existing } } return nil } func authMatchesPhysicalFile(auth *coreauth.Auth, path string) bool { if auth == nil || strings.TrimSpace(path) == "" { return false } if sameHostAuthFilePath(authAttribute(auth, coreauth.AttributePath), path) || sameHostAuthFilePath(authAttribute(auth, coreauth.AttributeSource), path) || sameHostAuthFilePath(authAttribute(auth, coreauth.AttributeVirtualSource), path) { return true } if strings.TrimSpace(authAttribute(auth, coreauth.AttributePath)) == "" { return strings.EqualFold(strings.TrimSpace(auth.FileName), filepath.Base(path)) } return false } func sameHostAuthFilePath(left, right string) bool { left = strings.TrimSpace(left) right = strings.TrimSpace(right) if left == "" || right == "" { return false } if absLeft, errLeft := filepath.Abs(left); errLeft == nil { left = absLeft } if absRight, errRight := filepath.Abs(right); errRight == nil { right = absRight } left = filepath.Clean(left) right = filepath.Clean(right) if runtime.GOOS == "windows" { return strings.EqualFold(left, right) } return left == right } func (h *Host) upsertAuthRecordsForPath(ctx context.Context, path string, auths []*coreauth.Auth) error { manager := h.currentAuthManager() if manager == nil { return nil } keepIDs := make(map[string]struct{}, len(auths)) for _, auth := range auths { if auth == nil || strings.TrimSpace(auth.ID) == "" { continue } keepIDs[auth.ID] = struct{}{} if existing, ok := manager.GetByID(auth.ID); ok && existing != nil { if !existing.CreatedAt.IsZero() { auth.CreatedAt = existing.CreatedAt } if _, errUpdate := manager.Update(ctx, auth); errUpdate != nil { return errUpdate } continue } if _, errRegister := manager.Register(ctx, auth); errRegister != nil { return errRegister } } skipPersistCtx := coreauth.WithSkipPersist(ctx) for _, existing := range manager.List() { if existing == nil || isRuntimeOnlyAuth(existing) { continue } if _, keep := keepIDs[existing.ID]; keep { continue } if authMatchesPhysicalFile(existing, path) { manager.Remove(skipPersistCtx, existing.ID) registry.GetGlobalRegistry().UnregisterClient(existing.ID) } } return nil } func isUnsafeAuthFileName(name string) bool { if strings.TrimSpace(name) == "" { return true } if strings.ContainsAny(name, "/\\") { return true } if filepath.VolumeName(name) != "" { return true } return false } func (h *Host) buildHostAuthFileEntry(auth *coreauth.Auth) *pluginapi.HostAuthFileEntry { if auth == nil { return nil } auth.EnsureIndex() runtimeOnly := isRuntimeOnlyAuth(auth) if runtimeOnly && (auth.Disabled || auth.Status == coreauth.StatusDisabled) { return nil } path := strings.TrimSpace(authAttribute(auth, "path")) if path == "" && !runtimeOnly { return nil } name := strings.TrimSpace(auth.FileName) if name == "" { name = auth.ID } entry := &pluginapi.HostAuthFileEntry{ ID: auth.ID, AuthIndex: auth.Index, Name: name, Type: strings.TrimSpace(auth.Provider), Provider: strings.TrimSpace(auth.Provider), Label: auth.Label, Status: string(auth.Status), StatusMessage: auth.StatusMessage, Disabled: auth.Disabled, Unavailable: auth.Unavailable, RuntimeOnly: runtimeOnly, Source: "memory", Success: auth.Success, Failed: auth.Failed, RecentRequests: hostRecentRequests(auth), } if email := authEmail(auth); email != "" { entry.Email = email } if projectID := authProjectID(auth); projectID != "" { entry.ProjectID = projectID } if accountType, account := auth.AccountInfo(); accountType != "" || account != "" { entry.AccountType = accountType entry.Account = account } if !auth.CreatedAt.IsZero() { entry.CreatedAt = auth.CreatedAt } if !auth.UpdatedAt.IsZero() { entry.ModTime = auth.UpdatedAt entry.UpdatedAt = auth.UpdatedAt } if !auth.LastRefreshedAt.IsZero() { entry.LastRefresh = auth.LastRefreshedAt } if !auth.NextRetryAfter.IsZero() { entry.NextRetryAfter = auth.NextRetryAfter } if path != "" { entry.Path = path entry.Source = "file" if info, err := os.Stat(path); err == nil { entry.Size = info.Size() entry.ModTime = info.ModTime() } else if os.IsNotExist(err) { if !runtimeOnly && (auth.Disabled || auth.Status == coreauth.StatusDisabled || strings.EqualFold(strings.TrimSpace(auth.StatusMessage), "removed via management api")) { return nil } entry.Source = "memory" } } if p := strings.TrimSpace(authAttribute(auth, "priority")); p != "" { if parsed, err := strconv.Atoi(p); err == nil { entry.Priority = parsed } } else if auth.Metadata != nil { if rawPriority, ok := auth.Metadata["priority"]; ok { if priority, okPriority := parsePriorityValue(rawPriority); okPriority { entry.Priority = priority } } } if note := strings.TrimSpace(authAttribute(auth, "note")); note != "" { entry.Note = note } else if auth.Metadata != nil { if rawNote, ok := auth.Metadata["note"].(string); ok { entry.Note = strings.TrimSpace(rawNote) } } if baseURL := strings.TrimSpace(authAttribute(auth, "base_url")); baseURL != "" { entry.BaseURL = baseURL } else if auth.Metadata != nil { if rawBaseURL, ok := auth.Metadata["base_url"].(string); ok { entry.BaseURL = strings.TrimSpace(rawBaseURL) } } if websockets, ok := authWebsocketsValue(auth); ok { entry.Websockets = websockets } return entry } func (h *Host) resolvedAuthDir() string { if h == nil { return "" } h.mu.Lock() authDir := "" if h.runtimeConfig != nil { authDir = strings.TrimSpace(h.runtimeConfig.AuthDir) } h.mu.Unlock() if authDir == "" { return "" } authDir = filepath.Clean(authDir) if !filepath.IsAbs(authDir) { if abs, errAbs := filepath.Abs(authDir); errAbs == nil { authDir = abs } } return authDir } func (h *Host) authIDForPath(path string) string { return authIDForPath(path, h.resolvedAuthDir()) } func authEmail(auth *coreauth.Auth) string { if auth == nil { return "" } if auth.Metadata != nil { if v, ok := auth.Metadata["email"].(string); ok { return strings.TrimSpace(v) } } if auth.Attributes != nil { if v := strings.TrimSpace(auth.Attributes["email"]); v != "" { return v } if v := strings.TrimSpace(auth.Attributes["account_email"]); v != "" { return v } } return "" } func authProjectID(auth *coreauth.Auth) string { if auth == nil { return "" } if auth.Metadata != nil { if v, ok := auth.Metadata["project_id"].(string); ok { if projectID := strings.TrimSpace(v); projectID != "" { return projectID } } } if auth.Attributes != nil { if projectID := strings.TrimSpace(auth.Attributes["project_id"]); projectID != "" { return projectID } } return "" } func authAttribute(auth *coreauth.Auth, key string) string { if auth == nil || len(auth.Attributes) == 0 { return "" } return auth.Attributes[key] } func isRuntimeOnlyAuth(auth *coreauth.Auth) bool { if auth == nil || len(auth.Attributes) == 0 { return false } return strings.EqualFold(strings.TrimSpace(auth.Attributes["runtime_only"]), "true") } func authWebsocketsValue(auth *coreauth.Auth) (bool, bool) { if auth == nil { return false, false } if auth.Attributes != nil { if raw := strings.TrimSpace(auth.Attributes["websockets"]); raw != "" { parsed, errParse := strconv.ParseBool(raw) if errParse == nil { return parsed, true } } } if auth.Metadata == nil { return false, false } return parseWebsocketsValue(auth.Metadata["websockets"]) } func parsePriorityValue(raw any) (int, bool) { switch v := raw.(type) { case int: return v, true case int32: return int(v), true case int64: return int(v), true case float64: return int(v), true case string: parsed, err := strconv.Atoi(strings.TrimSpace(v)) if err == nil { return parsed, true } } return 0, false } func parseBoolValue(raw any) (bool, bool) { switch v := raw.(type) { case bool: return v, true case string: parsed, errParse := strconv.ParseBool(strings.TrimSpace(v)) if errParse == nil { return parsed, true } } return false, false } func parseWebsocketsValue(raw any) (bool, bool) { return parseBoolValue(raw) } func bytesTrimSpace(raw []byte) []byte { return []byte(strings.TrimSpace(string(raw))) } func hostRecentRequests(auth *coreauth.Auth) []pluginapi.HostRecentRequestEntry { if auth == nil { return nil } snapshot := auth.RecentRequestsSnapshot(time.Now()) if len(snapshot) == 0 { return nil } out := make([]pluginapi.HostRecentRequestEntry, 0, len(snapshot)) for _, entry := range snapshot { out = append(out, pluginapi.HostRecentRequestEntry{ Time: entry.Time, Success: entry.Success, Failed: entry.Failed, }) } return out }