Ctrl K
ComunidadGuías…

internal/pluginhost/auth_provider.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package pluginhost

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"strings"
	"time"

	"github.com/MoeForge/moego/ojson"
	"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
	coreauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
	"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
)

func (h *Host) hostConfigSummaryLocked() pluginapi.HostConfigSummary {
	if h == nil || h.runtimeConfig == nil {
		return pluginapi.HostConfigSummary{}
	}
	cfg := h.runtimeConfig
	return pluginapi.HostConfigSummary{
		AuthDir:          strings.TrimSpace(cfg.AuthDir),
		ProxyURL:         strings.TrimSpace(cfg.ProxyURL),
		ForceModelPrefix: cfg.ForceModelPrefix,
		OAuthModelAlias:  pluginOAuthModelAliases(cfg.OAuthModelAlias),
		ExcludedModels:   cloneStringSliceMap(cfg.OAuthExcludedModels),
	}
}

func (h *Host) hostConfigSummary() pluginapi.HostConfigSummary {
	if h == nil {
		return pluginapi.HostConfigSummary{}
	}
	h.mu.Lock()
	defer h.mu.Unlock()
	return h.hostConfigSummaryLocked()
}

func pluginOAuthModelAliases(in map[string][]config.OAuthModelAlias) map[string][]pluginapi.ModelAlias {
	if len(in) == 0 {
		return nil
	}
	out := make(map[string][]pluginapi.ModelAlias, len(in))
	for provider, aliases := range in {
		key := normalizeProviderID(provider)
		if key == "" {
			continue
		}
		for _, alias := range aliases {
			name := strings.TrimSpace(alias.Name)
			value := strings.TrimSpace(alias.Alias)
			if name == "" || value == "" {
				continue
			}
			out[key] = append(out[key], pluginapi.ModelAlias{Name: name, Alias: value})
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func cloneStringSliceMap(in map[string][]string) map[string][]string {
	if len(in) == 0 {
		return nil
	}
	out := make(map[string][]string, len(in))
	for key, values := range in {
		cleanKey := normalizeProviderID(key)
		if cleanKey == "" {
			continue
		}
		out[cleanKey] = cloneStringSlice(values)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func normalizeProviderID(provider string) string {
	return strings.ToLower(strings.TrimSpace(provider))
}

func authIDForPath(path, authDir string) string {
	path = strings.TrimSpace(path)
	if path == "" {
		return ""
	}
	id := path
	if authDir = strings.TrimSpace(authDir); authDir != "" {
		cleanDir := filepath.Clean(authDir)
		cleanPath := filepath.Clean(path)
		if rel, errRel := filepath.Rel(cleanDir, cleanPath); isRelativeWithinAuthDir(rel, errRel) {
			id = rel
		} else {
			if !filepath.IsAbs(cleanDir) {
				if absDir, errAbs := filepath.Abs(cleanDir); errAbs == nil {
					cleanDir = absDir
				}
			}
			if !filepath.IsAbs(cleanPath) {
				if absPath, errAbs := filepath.Abs(cleanPath); errAbs == nil {
					cleanPath = absPath
				}
			}
			if rel, errRel := filepath.Rel(cleanDir, cleanPath); isRelativeWithinAuthDir(rel, errRel) {
				id = rel
			}
		}
	}
	id = filepath.ToSlash(filepath.Clean(id))
	if runtime.GOOS == "windows" {
		id = strings.ToLower(id)
	}
	return id
}

func isRelativeWithinAuthDir(rel string, errRel error) bool {
	if errRel != nil || rel == "" || rel == "." || rel == ".." {
		return false
	}
	return !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}

func hasAuthProviderCap(caps pluginapi.Capabilities) bool {
	if caps.AuthProvider != nil {
		return true
	}
	for _, p := range caps.AuthProviders {
		if p != nil {
			return true
		}
	}
	return false
}

func hasRecordAuthProvider(record capabilityRecord) bool {
	return hasAuthProviderCap(record.plugin.Capabilities)
}

func recordAuthProviders(record capabilityRecord) []pluginapi.AuthProvider {
	caps := record.plugin.Capabilities
	out := make([]pluginapi.AuthProvider, 0, 1+len(caps.AuthProviders))
	if caps.AuthProvider != nil {
		out = append(out, caps.AuthProvider)
	}
	for _, p := range caps.AuthProviders {
		if p != nil {
			out = append(out, p)
		}
	}
	return out
}

func normalizeAuthProviderIdentifiers(values ...string) []string {
	var out []string
	seen := make(map[string]struct{}, len(values))
	for _, raw := range values {
		for _, part := range strings.Split(raw, ",") {
			id := normalizeProviderID(part)
			if id == "" {
				continue
			}
			if _, exists := seen[id]; exists {
				continue
			}
			seen[id] = struct{}{}
			out = append(out, id)
		}
	}
	return out
}

func (h *Host) AuthProviderIdentifiers() []string {
	if h == nil {
		return nil
	}
	out := make([]string, 0)
	seen := make(map[string]struct{})
	for _, record := range h.activeRecords() {
		if !hasRecordAuthProvider(record) || h.isPluginFused(record.id) {
			continue
		}
		identifiers, okIdentifiers := h.callRecordAuthProviderIdentifiers(record)
		if !okIdentifiers {
			continue
		}
		for _, identifier := range identifiers {
			if identifier == "" {
				continue
			}
			if _, exists := seen[identifier]; exists {
				continue
			}
			seen[identifier] = struct{}{}
			out = append(out, identifier)
		}
	}
	return out
}

func (h *Host) HasAuthProvider(provider string) bool {
	return h.authProviderRecord(provider) != nil
}

// HasMultipleAuthProviders reports whether the active owning plugin declares multiple OAuth entries.
func (h *Host) HasMultipleAuthProviders(provider string) bool {
	record := h.authProviderRecord(provider)
	return record != nil && h.recordHasMultipleAuthProviders(*record)
}

func (h *Host) recordHasMultipleAuthProviders(record capabilityRecord) bool {
	if h == nil || !h.recordCurrent(record) {
		return false
	}
	identifiers, okIdentifiers := h.callRecordAuthProviderIdentifiers(record)
	return okIdentifiers && len(identifiers) > 1
}

// SameAuthProvider reports whether providerA and providerB belong to the same active plugin auth provider.
func (h *Host) SameAuthProvider(providerA, providerB string) bool {
	providerA = normalizeProviderID(providerA)
	providerB = normalizeProviderID(providerB)
	if h == nil || providerA == "" || providerB == "" {
		return false
	}
	if providerA == providerB {
		return true
	}
	for _, record := range h.activeRecords() {
		if !hasRecordAuthProvider(record) || h.isPluginFused(record.id) {
			continue
		}
		_, okA := h.recordOwnsCredentialProvider(record, providerA)
		_, okB := h.recordOwnsCredentialProvider(record, providerB)
		if okA && okB {
			return true
		}
	}
	return false
}

func (h *Host) authProviderRecord(provider string) *capabilityRecord {
	record, _ := h.authProviderEntry(provider)
	return record
}

func (h *Host) authProviderEntry(provider string) (*capabilityRecord, pluginapi.AuthProvider) {
	provider = normalizeProviderID(provider)
	if h == nil || provider == "" {
		return nil, nil
	}
	records := h.activeRecords()
	for _, record := range records {
		authProv, _, okMatch := h.recordAuthProviderForTarget(record, provider)
		if okMatch && authProv != nil {
			copyRecord := record
			return &copyRecord, authProv
		}
	}
	for _, record := range records {
		authProv, okOwn := h.recordOwnsCredentialProvider(record, provider)
		if okOwn && authProv != nil {
			copyRecord := record
			return &copyRecord, authProv
		}
	}
	return nil, nil
}

func (h *Host) authProviderCredentialEntry(provider string) (*capabilityRecord, pluginapi.AuthProvider) {
	return h.authProviderEntry(provider)
}

func (h *Host) recordOwnsCredentialProvider(record capabilityRecord, provider string) (pluginapi.AuthProvider, bool) {
	provider = normalizeProviderID(provider)
	if h == nil || provider == "" || !hasRecordAuthProvider(record) || h.isPluginFused(record.id) {
		return nil, false
	}
	if authProv, _, okMatch := h.recordAuthProviderForTarget(record, provider); okMatch && authProv != nil {
		return authProv, true
	}
	if portalID, okRecorded := h.recordedCredentialProviderPortal(record.id, provider); okRecorded {
		if portalID != "" {
			if authProv, _, okPortal := h.recordAuthProviderForTarget(record, portalID); okPortal && authProv != nil {
				return authProv, true
			}
		}
		authProv, _, okPrimary := h.recordAuthProviderForTarget(record, "")
		return authProv, okPrimary && authProv != nil
	}
	matched := false
	for _, candidate := range h.modelProvidersForPlugin(record.id) {
		if normalizeProviderID(candidate) == provider {
			matched = true
			break
		}
	}
	if !matched && record.plugin.Capabilities.Executor != nil {
		if candidate, okExec := h.executorProvider(record, record.plugin.Capabilities.Executor); okExec && candidate == provider {
			matched = true
		}
	}
	if matched {
		authProv, _, okPrimary := h.recordAuthProviderForTarget(record, "")
		return authProv, okPrimary && authProv != nil
	}
	for _, prov := range recordAuthProviders(record) {
		credIDs, okCredIDs := h.callAuthProviderCredentialProviders(record.id, prov)
		if !okCredIDs {
			return nil, false
		}
		for _, credID := range credIDs {
			if credID == provider {
				return prov, true
			}
		}
	}
	return nil, false
}

func (h *Host) recordAuthProviderForTarget(record capabilityRecord, target string) (pluginapi.AuthProvider, string, bool) {
	if h == nil || !hasRecordAuthProvider(record) || h.isPluginFused(record.id) {
		return nil, "", false
	}
	providers := recordAuthProviders(record)
	if len(providers) == 0 {
		return nil, "", false
	}
	target = normalizeProviderID(target)
	if target == "" {
		identifiers, okIDs := h.callAuthProviderIdentifiers(record.id, providers[0])
		if !okIDs {
			return nil, "", false
		}
		primary := ""
		if len(identifiers) > 0 {
			primary = identifiers[0]
		}
		return providers[0], primary, true
	}
	for _, prov := range providers {
		identifiers, okIDs := h.callAuthProviderIdentifiers(record.id, prov)
		if !okIDs {
			return nil, "", false
		}
		for _, identifier := range identifiers {
			if identifier == target {
				return prov, identifier, true
			}
		}
	}
	return nil, "", false
}

func (h *Host) callRecordAuthProviderIdentifiers(record capabilityRecord) ([]string, bool) {
	identifiers, _, ok := h.callRecordAuthProviderMetadata(record)
	return identifiers, ok
}

func (h *Host) callRecordAuthProviderMetadata(record capabilityRecord) ([]string, []pluginapi.AuthProviderEntry, bool) {
	if h == nil || !hasRecordAuthProvider(record) || h.isPluginFused(record.id) {
		return nil, nil, false
	}
	var out []string
	seen := make(map[string]struct{})
	byID := make(map[string]pluginapi.AuthProviderEntry)
	hasDeclaredEntries := false
	for _, prov := range recordAuthProviders(record) {
		identifiers, entries, okIDs := h.callAuthProviderMetadata(record.id, prov)
		if !okIDs {
			return nil, nil, false
		}
		for _, identifier := range identifiers {
			if identifier == "" {
				continue
			}
			if _, exists := seen[identifier]; exists {
				continue
			}
			seen[identifier] = struct{}{}
			out = append(out, identifier)
		}
		for _, entry := range entries {
			if entry.Identifier == "" {
				continue
			}
			hasDeclaredEntries = true
			existing, exists := byID[entry.Identifier]
			if !exists {
				byID[entry.Identifier] = entry
				continue
			}
			if existing.Name == "" && entry.Name != "" {
				existing.Name = entry.Name
			}
			if existing.Logo == "" && entry.Logo != "" {
				existing.Logo = entry.Logo
			}
			byID[entry.Identifier] = existing
		}
	}
	if !hasDeclaredEntries || len(out) == 0 {
		return out, nil, true
	}
	defaultName := strings.TrimSpace(record.meta.Name)
	if defaultName == "" {
		defaultName = strings.TrimSpace(record.plugin.Metadata.Name)
	}
	defaultLogo := strings.TrimSpace(record.meta.Logo)
	if defaultLogo == "" {
		defaultLogo = strings.TrimSpace(record.plugin.Metadata.Logo)
	}
	entries := make([]pluginapi.AuthProviderEntry, 0, len(out))
	for _, id := range out {
		entry := byID[id]
		entry.Identifier = id
		if entry.Name == "" {
			entry.Name = defaultName
		}
		if entry.Logo == "" {
			entry.Logo = defaultLogo
		}
		entries = append(entries, entry)
	}
	return out, entries, true
}

func (h *Host) callAuthProviderIdentifiers(pluginID string, provider pluginapi.AuthProvider) (identifiers []string, ok bool) {
	identifiers, _, ok = h.callAuthProviderMetadata(pluginID, provider)
	return identifiers, ok
}

func (h *Host) callAuthProviderMetadata(pluginID string, provider pluginapi.AuthProvider) (identifiers []string, entries []pluginapi.AuthProviderEntry, ok bool) {
	if h == nil || provider == nil || h.isPluginFused(pluginID) {
		return nil, nil, false
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(pluginID, "AuthProvider.Identifier", recovered)
			identifiers = nil
			entries = nil
			ok = false
		}
	}()
	raw := []string{provider.Identifier()}
	if multi, okMulti := provider.(interface{ Identifiers() []string }); okMulti && multi != nil {
		raw = append(raw, multi.Identifiers()...)
	}
	if withEntries, okEntries := provider.(interface {
		AuthEntries() []pluginapi.AuthProviderEntry
	}); okEntries && withEntries != nil {
		for _, entry := range withEntries.AuthEntries() {
			raw = append(raw, entry.Identifier)
			ids := normalizeAuthProviderIdentifiers(entry.Identifier)
			if len(ids) == 0 {
				continue
			}
			name := strings.TrimSpace(entry.Name)
			logo := strings.TrimSpace(entry.Logo)
			for _, id := range ids {
				entries = append(entries, pluginapi.AuthProviderEntry{
					Identifier: id,
					Name:       name,
					Logo:       logo,
				})
			}
		}
	}
	return normalizeAuthProviderIdentifiers(raw...), entries, true
}

func (h *Host) callAuthProviderCredentialProviders(pluginID string, provider pluginapi.AuthProvider) (providers []string, ok bool) {
	if h == nil || provider == nil || h.isPluginFused(pluginID) {
		return nil, false
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(pluginID, "AuthProvider.Identifier", recovered)
			providers = nil
			ok = false
		}
	}()
	cov, okCov := provider.(interface{ CredentialProviders() []string })
	if !okCov || cov == nil {
		return nil, true
	}
	return normalizeAuthProviderIdentifiers(cov.CredentialProviders()...), true
}

func (h *Host) callAuthProviderIdentifier(pluginID string, provider pluginapi.AuthProvider) (identifier string, ok bool) {
	identifiers, ok := h.callAuthProviderIdentifiers(pluginID, provider)
	if !ok {
		return "", false
	}
	if len(identifiers) == 0 {
		return "", true
	}
	return identifiers[0], true
}

func (h *Host) ParseAuth(ctx context.Context, req pluginapi.AuthParseRequest) (*coreauth.Auth, bool, error) {
	auths, handled, errParseAuths := h.ParseAuths(ctx, req)
	if errParseAuths != nil || !handled || len(auths) == 0 {
		return nil, handled, errParseAuths
	}
	return auths[0], true, nil
}

func extractRawJSONOAuthProviderHint(raw []byte) string {
	if !ojson.ValidBytes(raw) {
		return ""
	}
	for _, key := range []string{"oauth_provider", "portal"} {
		if value := ojson.GetBytes(raw, key); value.Type == ojson.String {
			if normalized := normalizeProviderID(value.String()); normalized != "" {
				return normalized
			}
		}
	}
	return ""
}

func (h *Host) ParseAuths(ctx context.Context, req pluginapi.AuthParseRequest) ([]*coreauth.Auth, bool, error) {
	if h == nil {
		return nil, false, nil
	}
	if strings.TrimSpace(req.Provider) != "" {
		if hint := extractRawJSONOAuthProviderHint(req.RawJSON); hint != "" {
			if hintRecord, hintProv := h.authProviderEntry(hint); hintRecord != nil && hintProv != nil {
				if owner, _ := h.authProviderEntry(req.Provider); owner == nil || owner.id == hintRecord.id {
					return h.callSingleProviderParseAuths(ctx, *hintRecord, hintProv, req)
				}
			}
		}
		record, _ := h.authProviderCredentialEntry(req.Provider)
		if record == nil {
			return nil, false, nil
		}
		return h.callParseAuths(ctx, *record, req)
	}
	for _, record := range h.activeRecords() {
		if !hasRecordAuthProvider(record) || h.isPluginFused(record.id) {
			continue
		}
		auths, handled, errParse := h.callParseAuths(ctx, record, req)
		if errParse != nil || handled {
			return auths, handled, errParse
		}
	}
	return nil, false, nil
}

func (h *Host) callParseAuth(ctx context.Context, record capabilityRecord, req pluginapi.AuthParseRequest) (auth *coreauth.Auth, handled bool, err error) {
	auths, handled, errParseAuths := h.callParseAuths(ctx, record, req)
	if errParseAuths != nil || !handled || len(auths) == 0 {
		return nil, handled, errParseAuths
	}
	return auths[0], true, nil
}

func (h *Host) callParseAuths(ctx context.Context, record capabilityRecord, req pluginapi.AuthParseRequest) (auths []*coreauth.Auth, handled bool, err error) {
	if h == nil || !hasRecordAuthProvider(record) || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return nil, false, nil
	}
	var candidates []pluginapi.AuthProvider
	if hint := extractRawJSONOAuthProviderHint(req.RawJSON); hint != "" {
		if matched, _, okMatch := h.recordAuthProviderForTarget(record, hint); okMatch && matched != nil {
			candidates = []pluginapi.AuthProvider{matched}
		}
	}
	if len(candidates) == 0 {
		if target := normalizeProviderID(req.Provider); target != "" {
			if matched, _, okMatch := h.recordAuthProviderForTarget(record, target); okMatch && matched != nil {
				candidates = []pluginapi.AuthProvider{matched}
			} else if owned, okOwn := h.recordOwnsCredentialProvider(record, target); okOwn && owned != nil {
				candidates = []pluginapi.AuthProvider{owned}
			}
		} else {
			candidates = recordAuthProviders(record)
		}
	}
	for _, provider := range candidates {
		auths, handled, err = h.callSingleProviderParseAuths(ctx, record, provider, req)
		if err != nil || handled {
			return auths, handled, err
		}
	}
	return nil, false, nil
}

func (h *Host) callSingleProviderParseAuths(ctx context.Context, record capabilityRecord, provider pluginapi.AuthProvider, req pluginapi.AuthParseRequest) (auths []*coreauth.Auth, handled bool, err error) {
	if h == nil || provider == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return nil, false, nil
	}
	primaryIdentifier, okID := h.callAuthProviderIdentifier(record.id, provider)
	if !okID {
		return nil, false, fmt.Errorf("auth provider identifier panic")
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, "AuthProvider.ParseAuth", recovered)
			auths = nil
			handled = false
			err = fmt.Errorf("auth provider panic: %v", recovered)
		}
	}()
	portalIdentifier := h.resolveParsePortalIdentifier(record.id, provider, req.Provider, req.RawJSON, primaryIdentifier)
	if req.Host.AuthDir == "" {
		req.Host = h.hostConfigSummary()
	}
	req.Provider = normalizeProviderID(req.Provider)
	if req.Provider == "" {
		req.Provider = portalIdentifier
	}
	req.RawJSON = bytes.Clone(req.RawJSON)
	resp, errParse := provider.ParseAuth(ctx, req)
	if errParse != nil {
		return nil, false, errParse
	}
	if !resp.Handled {
		return nil, false, nil
	}
	datas := pluginAuthParseResponseAuths(resp)
	multiEntry := h.recordHasMultipleAuthProviders(record)
	auths = make([]*coreauth.Auth, 0, len(datas))
	for _, data := range datas {
		if strings.TrimSpace(data.Provider) == "" {
			data.Provider = req.Provider
		}
		if strings.TrimSpace(data.Provider) == "" {
			data.Provider = primaryIdentifier
		}
		if normalizeProviderID(data.Provider) == "" {
			return nil, true, fmt.Errorf("auth provider %s returned auth without provider", record.id)
		}
		explicitHint := firstNonEmpty(authDataPortalHint(&data), extractRawJSONOAuthProviderHint(req.RawJSON))
		effectivePortal := firstNonEmpty(explicitHint, portalIdentifier)
		if explicitHint != "" || multiEntry || normalizeProviderID(data.Provider) != effectivePortal {
			stampAuthDataOAuthProvider(&data, effectivePortal)
		}
		parsed := h.AuthDataToCoreAuth(data, req.Path, req.FileName)
		if parsed == nil {
			return nil, true, fmt.Errorf("auth provider %s returned invalid auth data", record.id)
		}
		h.recordCredentialProvider(record.id, req.Provider, effectivePortal)
		h.recordCredentialProvider(record.id, data.Provider, effectivePortal)
		auths = append(auths, parsed)
	}
	return auths, true, nil
}

func (h *Host) resolveParsePortalIdentifier(pluginID string, provider pluginapi.AuthProvider, reqProvider string, rawJSON []byte, fallback string) string {
	if identifiers, ok := h.callAuthProviderIdentifiers(pluginID, provider); ok {
		matches := func(candidate string) bool {
			candidate = normalizeProviderID(candidate)
			if candidate == "" {
				return false
			}
			for _, id := range identifiers {
				if id == candidate {
					return true
				}
			}
			return false
		}
		if hint := extractRawJSONOAuthProviderHint(rawJSON); matches(hint) {
			return normalizeProviderID(hint)
		}
		if matches(reqProvider) {
			return normalizeProviderID(reqProvider)
		}
	}
	return normalizeProviderID(fallback)
}

func (h *Host) recordCredentialProvider(pluginID, provider, portalIdentifier string) {
	pluginID = strings.TrimSpace(pluginID)
	provider = normalizeProviderID(provider)
	portalIdentifier = normalizeProviderID(portalIdentifier)
	if h == nil || pluginID == "" || provider == "" {
		return
	}
	h.mu.Lock()
	defer h.mu.Unlock()
	if h.authCredentialProviders == nil {
		h.authCredentialProviders = make(map[string]map[string]string)
	}
	set := h.authCredentialProviders[pluginID]
	if set == nil {
		set = make(map[string]string)
		h.authCredentialProviders[pluginID] = set
	}
	if _, exists := set[provider]; !exists || set[provider] == "" {
		set[provider] = portalIdentifier
	}
}

func (h *Host) recordedCredentialProviderPortal(pluginID, provider string) (string, bool) {
	pluginID = strings.TrimSpace(pluginID)
	provider = normalizeProviderID(provider)
	if h == nil || pluginID == "" || provider == "" {
		return "", false
	}
	h.mu.Lock()
	defer h.mu.Unlock()
	set := h.authCredentialProviders[pluginID]
	portal, ok := set[provider]
	return portal, ok
}

func pluginAuthParseResponseAuths(resp pluginapi.AuthParseResponse) []pluginapi.AuthData {
	if len(resp.Auths) > 0 {
		return append([]pluginapi.AuthData(nil), resp.Auths...)
	}
	return []pluginapi.AuthData{resp.Auth}
}

type oauthProxyURLContextKey struct{}

// WithOAuthProxyURL attaches a session-scoped proxy URL to ctx for plugin OAuth login calls.
func WithOAuthProxyURL(ctx context.Context, proxyURL string) context.Context {
	if ctx == nil {
		ctx = context.Background()
	}
	proxyURL = strings.TrimSpace(proxyURL)
	if proxyURL == "" {
		return ctx
	}
	return context.WithValue(ctx, oauthProxyURLContextKey{}, proxyURL)
}

func oauthProxyURLFromContext(ctx context.Context) string {
	if ctx == nil {
		return ""
	}
	value, _ := ctx.Value(oauthProxyURLContextKey{}).(string)
	return strings.TrimSpace(value)
}

func (h *Host) StartLogin(ctx context.Context, provider string, baseURL string, metadata ...map[string]any) (pluginapi.AuthLoginStartResponse, bool, error) {
	record := h.authProviderRecord(provider)
	if record == nil {
		return pluginapi.AuthLoginStartResponse{}, false, nil
	}
	var startMetadata map[string]any
	if len(metadata) > 0 {
		startMetadata = metadata[0]
	}
	return h.callStartLogin(ctx, *record, provider, baseURL, startMetadata)
}

func (h *Host) callStartLogin(ctx context.Context, record capabilityRecord, provider string, baseURL string, metadata map[string]any) (resp pluginapi.AuthLoginStartResponse, handled bool, err error) {
	authProvider, _, okProvider := h.recordAuthProviderForTarget(record, provider)
	if !okProvider || authProvider == nil {
		authProvider, okProvider = h.recordOwnsCredentialProvider(record, provider)
	}
	if h == nil || !okProvider || authProvider == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return pluginapi.AuthLoginStartResponse{}, false, nil
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, "AuthProvider.StartLogin", recovered)
			resp = pluginapi.AuthLoginStartResponse{}
			handled = false
			err = fmt.Errorf("auth provider start login panic: %v", recovered)
		}
	}()
	hostSummary := h.hostConfigSummary()
	sessionProxyURL := oauthProxyURLFromContext(ctx)
	if sessionProxyURL != "" {
		hostSummary.ProxyURL = sessionProxyURL
	}
	req := pluginapi.AuthLoginStartRequest{
		Provider:   normalizeProviderID(provider),
		BaseURL:    strings.TrimSpace(baseURL),
		Host:       hostSummary,
		HTTPClient: h.newHTTPClientWithProxy(nil, sessionProxyURL, provider),
		Metadata:   cloneAnyMap(metadata),
	}
	resp, errStart := authProvider.StartLogin(ctx, req)
	if errStart != nil {
		return pluginapi.AuthLoginStartResponse{}, true, errStart
	}
	return resp, true, nil
}

func (h *Host) PollLogin(ctx context.Context, provider, state string, metadata ...map[string]any) (pluginapi.AuthLoginPollResponse, bool, error) {
	record := h.authProviderRecord(provider)
	if record == nil {
		return pluginapi.AuthLoginPollResponse{}, false, nil
	}
	var pollMetadata map[string]any
	if len(metadata) > 0 {
		pollMetadata = metadata[0]
	}
	return h.callPollLogin(ctx, *record, provider, state, pollMetadata)
}

func (h *Host) callPollLogin(ctx context.Context, record capabilityRecord, provider, state string, metadata map[string]any) (resp pluginapi.AuthLoginPollResponse, handled bool, err error) {
	authProvider, matchedPortal, okProvider := h.recordAuthProviderForTarget(record, provider)
	if !okProvider || authProvider == nil {
		authProvider, okProvider = h.recordOwnsCredentialProvider(record, provider)
	}
	if h == nil || !okProvider || authProvider == nil || h.isPluginFused(record.id) || !h.recordCurrent(record) {
		return pluginapi.AuthLoginPollResponse{}, false, nil
	}
	if matchedPortal == "" {
		matchedPortal, _ = h.callAuthProviderIdentifier(record.id, authProvider)
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, "AuthProvider.PollLogin", recovered)
			resp = pluginapi.AuthLoginPollResponse{}
			handled = false
			err = fmt.Errorf("auth provider poll login panic: %v", recovered)
		}
	}()
	hostSummary := h.hostConfigSummary()
	sessionProxyURL := oauthProxyURLFromContext(ctx)
	if sessionProxyURL != "" {
		hostSummary.ProxyURL = sessionProxyURL
	}
	req := pluginapi.AuthLoginPollRequest{
		Provider:   normalizeProviderID(provider),
		State:      strings.TrimSpace(state),
		Host:       hostSummary,
		HTTPClient: h.newHTTPClientWithProxy(nil, sessionProxyURL, provider),
		Metadata:   cloneAnyMap(metadata),
	}
	resp, errPoll := authProvider.PollLogin(ctx, req)
	if errPoll != nil {
		return pluginapi.AuthLoginPollResponse{}, true, errPoll
	}
	if resp.Status == pluginapi.AuthLoginStatusSuccess {
		multiEntry := h.recordHasMultipleAuthProviders(record)
		for index := range resp.Auths {
			explicitHint := authDataPortalHint(&resp.Auths[index])
			effectivePortal := firstNonEmpty(explicitHint, matchedPortal)
			if multiEntry || explicitHint != "" || normalizeProviderID(resp.Auths[index].Provider) != effectivePortal {
				stampAuthDataOAuthProvider(&resp.Auths[index], effectivePortal)
			}
			h.recordCredentialProvider(record.id, resp.Auths[index].Provider, effectivePortal)
		}
		explicitHint := authDataPortalHint(&resp.Auth)
		effectivePortal := firstNonEmpty(explicitHint, matchedPortal)
		if multiEntry || explicitHint != "" || normalizeProviderID(resp.Auth.Provider) != effectivePortal {
			stampAuthDataOAuthProvider(&resp.Auth, effectivePortal)
		}
		h.recordCredentialProvider(record.id, resp.Auth.Provider, effectivePortal)
	}
	return resp, true, nil
}

func authDataPortalHint(data *pluginapi.AuthData) string {
	if data == nil {
		return ""
	}
	for _, key := range []string{"oauth_provider", "portal"} {
		if data.Attributes != nil {
			if v := normalizeProviderID(data.Attributes[key]); v != "" {
				return v
			}
		}
		if data.Metadata != nil {
			if raw, ok := data.Metadata[key].(string); ok {
				if v := normalizeProviderID(raw); v != "" {
					return v
				}
			}
		}
	}
	return extractRawJSONOAuthProviderHint(data.StorageJSON)
}

func stampAuthDataOAuthProvider(data *pluginapi.AuthData, portalIdentifier string) {
	if data == nil {
		return
	}
	portalIdentifier = firstNonEmpty(authDataPortalHint(data), normalizeProviderID(portalIdentifier))
	if portalIdentifier == "" {
		return
	}
	if normalizeProviderID(data.Provider) == "" {
		return
	}
	if data.Metadata == nil {
		data.Metadata = make(map[string]any)
	}
	data.Metadata["oauth_provider"] = portalIdentifier
	if data.Attributes == nil {
		data.Attributes = make(map[string]string)
	}
	data.Attributes["oauth_provider"] = portalIdentifier
}

func authPortalHints(auth *coreauth.Auth) []string {
	if auth == nil {
		return nil
	}
	out := make([]string, 0, 2)
	add := func(v string) {
		v = normalizeProviderID(v)
		if v == "" {
			return
		}
		for _, existing := range out {
			if existing == v {
				return
			}
		}
		out = append(out, v)
	}
	for _, key := range []string{"oauth_provider", "portal"} {
		if auth.Attributes != nil {
			add(auth.Attributes[key])
		}
		if auth.Metadata != nil {
			if raw, ok := auth.Metadata[key].(string); ok {
				add(raw)
			}
		}
	}
	return out
}

func (h *Host) authProviderCredentialEntryForAuth(auth *coreauth.Auth) (*capabilityRecord, pluginapi.AuthProvider) {
	if h == nil || auth == nil {
		return nil, nil
	}
	credProv := normalizeProviderID(authProvider(auth))
	for _, hint := range authPortalHints(auth) {
		for _, record := range h.activeRecords() {
			portalAuthProv, _, okPortal := h.recordAuthProviderForTarget(record, hint)
			if !okPortal || portalAuthProv == nil {
				continue
			}
			if credProv != "" && credProv != hint {
				if explicitOwner, _ := h.authProviderEntry(credProv); explicitOwner != nil && explicitOwner.id != record.id {
					continue
				}
				h.recordCredentialProvider(record.id, credProv, hint)
			}
			copyRecord := record
			return &copyRecord, portalAuthProv
		}
	}
	if credProv != "" {
		return h.authProviderCredentialEntry(credProv)
	}
	return nil, nil
}

func (h *Host) RefreshAuth(ctx context.Context, auth *coreauth.Auth) (refreshed *coreauth.Auth, handled bool, err error) {
	if h == nil || auth == nil {
		return nil, false, nil
	}
	record, authProv := h.authProviderCredentialEntryForAuth(auth)
	if record == nil || authProv == nil {
		return nil, false, nil
	}
	if !h.recordCurrent(*record) {
		return nil, false, nil
	}
	defer func() {
		if recovered := recover(); recovered != nil {
			h.fusePlugin(record.id, "AuthProvider.RefreshAuth", recovered)
			refreshed = nil
			handled = true
			err = fmt.Errorf("auth provider refresh panic: %v", recovered)
		}
	}()

	pluginResp, errRefresh := authProv.RefreshAuth(ctx, pluginapi.AuthRefreshRequest{
		AuthID:       authID(auth),
		AuthProvider: authProvider(auth),
		StorageJSON:  storageJSONFromAuth(auth),
		Metadata:     cloneAnyMap(authMetadata(auth)),
		Attributes:   authAttributes(auth),
		Host:         h.hostConfigSummary(),
		HTTPClient:   h.newHTTPClient(auth),
	})
	if errRefresh != nil {
		return nil, true, errRefresh
	}
	data := pluginResp.Auth
	explicitPortalHint := authDataPortalHint(&data)
	if strings.TrimSpace(data.Provider) == "" {
		data.Provider = authProvider(auth)
	}
	if strings.TrimSpace(data.ID) == "" {
		data.ID = authID(auth)
	}
	if strings.TrimSpace(data.FileName) == "" {
		data.FileName = auth.FileName
	}
	if strings.TrimSpace(data.Label) == "" {
		data.Label = auth.Label
	}
	if strings.TrimSpace(data.Prefix) == "" {
		data.Prefix = auth.Prefix
	}
	if strings.TrimSpace(data.ProxyURL) == "" {
		data.ProxyURL = auth.ProxyURL
	}
	if len(data.Metadata) == 0 {
		data.Metadata = cloneAnyMap(auth.Metadata)
	}
	if len(data.Attributes) == 0 {
		if auth != nil {
			data.Attributes = cloneStringMap(auth.Attributes)
		}
	} else if auth != nil {
		attributes := cloneStringMap(data.Attributes)
		for key, value := range auth.Attributes {
			if _, exists := attributes[key]; !exists {
				attributes[key] = value
			}
		}
		data.Attributes = attributes
	}
	preserveAuthOAuthProviderMetadata(&data, auth, explicitPortalHint)
	preserveFileAuthPriority(&data, auth)
	preserveAuthDisabledState(&data, auth)
	if len(data.StorageJSON) == 0 {
		data.StorageJSON = storageJSONFromAuth(auth)
	}
	if pluginResp.NextRefreshAfter.IsZero() {
		data.NextRefreshAfter = auth.NextRefreshAfter
	} else {
		data.NextRefreshAfter = pluginResp.NextRefreshAfter
	}
	path := ""
	if auth != nil && auth.Attributes != nil {
		path = auth.Attributes[coreauth.AttributePath]
	}
	next := h.AuthDataToCoreAuth(data, path, data.FileName)
	if next == nil {
		return nil, true, fmt.Errorf("auth provider refresh returned invalid auth data")
	}
	next.Index = auth.Index
	next.CreatedAt = auth.CreatedAt
	next.UpdatedAt = auth.UpdatedAt
	return next, true, nil
}

// explicitPortalHint must be resolved before merging existing auth fields.
func preserveAuthOAuthProviderMetadata(data *pluginapi.AuthData, auth *coreauth.Auth, explicitPortalHint string) {
	if data == nil || auth == nil {
		return
	}
	hint := explicitPortalHint
	if hint == "" {
		hint = authDataPortalHint(&pluginapi.AuthData{
			Attributes:  auth.Attributes,
			Metadata:    auth.Metadata,
			StorageJSON: storageJSONFromAuth(auth),
		})
	}
	for _, key := range []string{"oauth_provider", "portal"} {
		raw, ok := auth.Metadata[key].(string)
		if !ok || strings.TrimSpace(raw) == "" {
			continue
		}
		if data.Metadata == nil {
			data.Metadata = make(map[string]any)
		}
		if _, exists := data.Metadata[key]; !exists {
			data.Metadata[key] = raw
		}
	}
	if hint == "" {
		return
	}
	if data.Metadata == nil {
		data.Metadata = make(map[string]any)
	}
	data.Metadata["oauth_provider"] = hint
	if data.Attributes == nil {
		data.Attributes = make(map[string]string)
	}
	data.Attributes["oauth_provider"] = hint
}

func preserveAuthDisabledState(data *pluginapi.AuthData, auth *coreauth.Auth) {
	if data == nil || auth == nil {
		return
	}
	if auth.Disabled || auth.Status == coreauth.StatusDisabled {
		data.Disabled = true
	}
}

func preserveFileAuthPriority(data *pluginapi.AuthData, auth *coreauth.Auth) {
	if data == nil || auth == nil || auth.Attributes[coreauth.AttributeSourceBackend] != coreauth.AuthSourceFile {
		return
	}
	if data.Attributes == nil {
		data.Attributes = make(map[string]string)
	}
	for _, key := range []string{coreauth.AttributePath, coreauth.AttributeSource, coreauth.AttributeSourceBackend, coreauth.AttributeFilePriority} {
		if value, ok := auth.Attributes[key]; ok {
			data.Attributes[key] = value
		}
	}
	if auth.Attributes[coreauth.AttributeFilePriority] != "true" {
		delete(data.Attributes, coreauth.AttributeFilePriority)
		return
	}
	if priority, ok := auth.Attributes["priority"]; ok {
		data.Attributes["priority"] = priority
	} else {
		delete(data.Attributes, "priority")
	}
	if data.Metadata == nil {
		data.Metadata = make(map[string]any)
	}
	if priority, ok := auth.Metadata["priority"]; ok {
		data.Metadata["priority"] = priority
	} else {
		delete(data.Metadata, "priority")
	}
}

func (h *Host) AuthDataToCoreAuth(data pluginapi.AuthData, path, fileName string) *coreauth.Auth {
	authDir := ""
	if h != nil {
		authDir = h.hostConfigSummary().AuthDir
	}
	return pluginAuthDataToCoreAuth(data, path, fileName, authDir)
}

type pluginTokenStorage struct {
	provider string
	rawJSON  []byte
	meta     map[string]any
}

func (s *pluginTokenStorage) SetMetadata(meta map[string]any) {
	if s == nil {
		return
	}
	if _, hadPriority := s.meta["priority"]; hadPriority {
		if _, hasPriority := meta["priority"]; !hasPriority {
			var raw map[string]any
			if errUnmarshal := json.Unmarshal(s.rawJSON, &raw); errUnmarshal == nil {
				delete(raw, "priority")
				if cleaned, errMarshal := json.Marshal(raw); errMarshal == nil {
					s.rawJSON = cleaned
				}
			}
		}
	}
	s.meta = cloneAnyMap(meta)
}

func (s *pluginTokenStorage) RawJSON() []byte {
	if s == nil {
		return nil
	}
	payload, errPayload := mergedStorageJSON(s.rawJSON, s.meta, s.provider)
	if errPayload != nil {
		return nil
	}
	return payload
}

func (s *pluginTokenStorage) SaveTokenToFile(path string) error {
	if s == nil {
		return fmt.Errorf("plugin token storage is nil")
	}
	payload, errPayload := mergedStorageJSON(s.rawJSON, s.meta, s.provider)
	if errPayload != nil {
		return errPayload
	}
	if len(bytes.TrimSpace(payload)) == 0 {
		return fmt.Errorf("plugin token storage payload is empty")
	}
	if pluginTokenStorageFileCurrent(path, payload) {
		return nil
	}
	return atomicWriteFile(path, payload)
}

func pluginTokenStorageFileCurrent(path string, payload []byte) bool {
	if strings.TrimSpace(path) == "" || len(bytes.TrimSpace(payload)) == 0 {
		return false
	}
	current, errRead := os.ReadFile(path)
	if errRead != nil {
		return false
	}
	return jsonPayloadEqual(current, payload)
}

func jsonPayloadEqual(left, right []byte) bool {
	var leftValue any
	if errUnmarshalLeft := json.Unmarshal(left, &leftValue); errUnmarshalLeft != nil {
		return false
	}
	var rightValue any
	if errUnmarshalRight := json.Unmarshal(right, &rightValue); errUnmarshalRight != nil {
		return false
	}
	return reflect.DeepEqual(leftValue, rightValue)
}

func mergedStorageJSON(raw []byte, metadata map[string]any, provider string) ([]byte, error) {
	out := make(map[string]any)
	if len(bytes.TrimSpace(raw)) > 0 {
		if errUnmarshal := json.Unmarshal(raw, &out); errUnmarshal != nil {
			return nil, fmt.Errorf("decode plugin token storage: %w", errUnmarshal)
		}
		if out == nil {
			out = make(map[string]any)
		}
	}
	for key, value := range metadata {
		out[key] = value
	}
	provider = normalizeProviderID(provider)
	if provider != "" {
		out["type"] = provider
	}
	coreauth.NormalizeCredentialMetadata(out)
	if len(out) == 0 {
		return nil, fmt.Errorf("plugin token storage payload is empty")
	}
	payload, errMarshal := json.Marshal(out)
	if errMarshal != nil {
		return nil, fmt.Errorf("encode plugin token storage: %w", errMarshal)
	}
	return payload, nil
}

func atomicWriteFile(path string, data []byte) error {
	path = strings.TrimSpace(path)
	if path == "" {
		return fmt.Errorf("path is empty")
	}
	dir := filepath.Dir(path)
	if errMkdir := os.MkdirAll(dir, 0o700); errMkdir != nil {
		return fmt.Errorf("create auth directory: %w", errMkdir)
	}
	tmp, errCreate := os.CreateTemp(dir, ".plugin-auth-*.tmp")
	if errCreate != nil {
		return fmt.Errorf("create temp auth file: %w", errCreate)
	}
	tmpPath := tmp.Name()
	defer func() {
		_ = os.Remove(tmpPath)
	}()
	if _, errWrite := tmp.Write(data); errWrite != nil {
		if errClose := tmp.Close(); errClose != nil {
			errWrite = fmt.Errorf("%w; close temp auth file: %v", errWrite, errClose)
		}
		return fmt.Errorf("write temp auth file: %w", errWrite)
	}
	if errClose := tmp.Close(); errClose != nil {
		return fmt.Errorf("close temp auth file: %w", errClose)
	}
	if errRename := os.Rename(tmpPath, path); errRename != nil {
		return fmt.Errorf("rename temp auth file: %w", errRename)
	}
	return nil
}

func pluginAuthDataToCoreAuth(data pluginapi.AuthData, path, fileName string, authDir string) *coreauth.Auth {
	provider := normalizeProviderID(data.Provider)
	if provider == "" {
		return nil
	}
	metadata := cloneAnyMap(data.Metadata)
	if metadata == nil {
		metadata = make(map[string]any)
	}
	if provider != "" {
		metadata["type"] = provider
	}
	attributes := cloneStringMap(data.Attributes)
	if attributes == nil {
		attributes = make(map[string]string)
	}
	if hint := authDataPortalHint(&data); hint != "" {
		metadata["oauth_provider"] = hint
		attributes["oauth_provider"] = hint
	}
	path = strings.TrimSpace(path)
	if path != "" {
		if attributes[coreauth.AttributePath] == "" {
			attributes[coreauth.AttributePath] = path
		}
		if attributes[coreauth.AttributeSource] == "" {
			attributes[coreauth.AttributeSource] = path
		}
		if attributes[coreauth.AttributeSourceBackend] == "" {
			attributes[coreauth.AttributeSourceBackend] = coreauth.AuthSourceFile
		}
	}
	fileName = strings.TrimSpace(firstNonEmpty(data.FileName, fileName))
	if fileName != "" && attributes[coreauth.AttributeSource] == "" {
		attributes[coreauth.AttributeSource] = fileName
	}
	id := strings.TrimSpace(data.ID)
	if id == "" {
		id = authIDForPath(firstNonEmpty(path, fileName), authDir)
	}
	status := coreauth.StatusActive
	if data.Disabled {
		status = coreauth.StatusDisabled
	}
	now := time.Now().UTC()
	auth := &coreauth.Auth{
		Provider:         provider,
		ID:               id,
		FileName:         fileName,
		Label:            strings.TrimSpace(data.Label),
		Prefix:           strings.TrimSpace(data.Prefix),
		ProxyURL:         strings.TrimSpace(data.ProxyURL),
		Disabled:         data.Disabled,
		Status:           status,
		Storage:          &pluginTokenStorage{provider: provider, rawJSON: bytes.Clone(data.StorageJSON), meta: metadata},
		Metadata:         metadata,
		Attributes:       attributes,
		CreatedAt:        now,
		UpdatedAt:        now,
		NextRefreshAfter: data.NextRefreshAfter,
	}
	return auth
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}