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 ©Record, authProv } } for _, record := range records { authProv, okOwn := h.recordOwnsCredentialProvider(record, provider) if okOwn && authProv != nil { copyRecord := record return ©Record, 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 ©Record, 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 "" }