---
source: tests/golden_go.rs
expression: contents
---
package auth

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"

	"out/internal/core"
)

const credentialKey = "active-credentials"

// CredentialStore is the subset of `core.CredentialStore` the auth manager
// needs — declared locally so this package doesn't import `core` for
// anything beyond `AuthMethod`.
type CredentialStore interface {
	Save(key, secret string) error
	Get(key string) (string, bool, error)
	Delete(key string) error
}

// Manager selects exactly one active auth strategy per deployment, chosen
// via config's `AuthMethod` (REQ-1.2.3) — there is no runtime engine for
// resolving multiple simultaneously-required schemes. Mirrors
// `targets::csharp::templates::Auth::AuthManager.cs.tera`'s "resolve one
// strategy by a config-driven key" design, using a
// `map[core.AuthMethod]Strategy` instead of .NET's keyed DI container
// since Go has no DI container to route the lookup through — this
// target's own idiomatic equivalent of v1's TS object-literal lookup,
// v2's Rust `match`, v3's Python dispatch dict, v4's C# keyed-DI
// registration.
type Manager struct {
	authMethod  core.AuthMethod
	strategy    Strategy
	credentials CredentialStore

	mu                sync.Mutex
	cachedCredentials Credentials
}

func NewManager(authMethod core.AuthMethod, strategies map[core.AuthMethod]Strategy, credentialStore CredentialStore) (*Manager, error) {
	strategy, ok := strategies[authMethod]
	if !ok {
		return nil, fmt.Errorf("no auth strategy registered for method %q", authMethod)
	}
	return &Manager{authMethod: authMethod, strategy: strategy, credentials: credentialStore}, nil
}

// headerLocations maps each discovered auth method to where its
// credential value travels on the wire — used only by the HTTP-transport
// auth-gate/extractor (internal/http) to know which incoming header to
// read; stdio never calls this. Built as individual assignments rather
// than one map literal — gofmt column-aligns every entry in a multi-line
// map literal by the longest key, which a Tera template can't predict
// ahead of rendering since the set of discovered schemes varies per
// generation (the same reason `internal/core/config.go.tera`'s
// per-method consts are ungrouped statements instead of one `const (...)`
// block).
var headerLocations = func() map[core.AuthMethod][2]string {
	locations := map[core.AuthMethod][2]string{}
	locations[core.AuthMethodBasic] = [2]string{"header", "Authorization"}
	locations[core.AuthMethodPat] = [2]string{"header", "Authorization"}
	locations[core.AuthMethodOAuth2] = [2]string{"header", "Authorization"}
	locations[core.AuthMethodOAuth1] = [2]string{"none", ""}
	return locations
}()

// HeaderLocationFor returns (headerLocation, headerName) for authMethod.
func HeaderLocationFor(authMethod core.AuthMethod) (headerLocation, headerName string) {
	location := headerLocations[authMethod]
	return location[0], location[1]
}

func (m *Manager) Login(ctx context.Context, config Credentials) (Credentials, error) {
	credentials, err := m.strategy.Authenticate(ctx, config)
	if err != nil {
		return nil, err
	}
	m.mu.Lock()
	m.cachedCredentials = credentials
	m.mu.Unlock()

	encoded, err := json.Marshal(credentials)
	if err != nil {
		return nil, err
	}
	if err := m.credentials.Save(credentialKey, string(encoded)); err != nil {
		return nil, err
	}
	return credentials, nil
}

// SetCredentials seeds in-memory credentials directly, bypassing
// OS-keychain/file storage entirely — for tests (which must never touch
// the real credential store as a side effect of running) and for callers
// that already hold validated credentials from elsewhere.
func (m *Manager) SetCredentials(credentials Credentials) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.cachedCredentials = credentials
}

func (m *Manager) GetCredentials(ctx context.Context) (Credentials, error) {
	m.mu.Lock()
	cached := m.cachedCredentials
	m.mu.Unlock()
	if cached != nil && m.strategy.ValidateCredentials(cached) {
		return cached, nil
	}

	stored, ok, err := m.credentials.Get(credentialKey)
	if err != nil {
		return nil, err
	}
	if ok {
		var parsed Credentials
		if err := json.Unmarshal([]byte(stored), &parsed); err == nil {
			if m.strategy.ValidateCredentials(parsed) {
				m.mu.Lock()
				m.cachedCredentials = parsed
				m.mu.Unlock()
				return parsed, nil
			}

			refreshed, err := m.strategy.RefreshToken(ctx, parsed)
			if err == nil {
				m.mu.Lock()
				m.cachedCredentials = refreshed
				m.mu.Unlock()
				if encoded, err := json.Marshal(refreshed); err == nil {
					_ = m.credentials.Save(credentialKey, string(encoded))
				}
				return refreshed, nil
			}
			// Fall through to NewNoActiveCredentialsError below — a
			// failed refresh means "no valid credentials", not a fatal
			// error, mirroring `targets::python`'s auth_manager.py.
		}
	}

	return nil, NewNoActiveCredentialsError(string(m.authMethod))
}

// ApplyAuthHeaders decorates outgoing request headers with credentials
// for transport: on core.TransportHTTP, requestOverride (extracted
// per-request from the caller's own headers — see internal/http's
// extractor) is the *only* source ever consulted, since HTTP deployments
// must never leak the operator's local config/keychain secrets to a
// caller who didn't supply their own; a nil override is a hard error, not
// a fallback. On core.TransportStdio, requestOverride is ignored and
// behavior is unchanged from before HTTP-transport support existed:
// GetCredentials (config cascade + keychain).
//
// API-key placement (header vs. query, and the header/param name)
// defaults to an `X-Api-Key` header here; a scheme declaring a different
// location is one of Story G6's API-client responsibilities to refine,
// not this manager's — requestOverride's request_header_name/
// request_header_value pair is the one exception, since the HTTP path
// already knows the scheme's declared header name at generation time.
func (m *Manager) ApplyAuthHeaders(
	ctx context.Context,
	headers map[string]string,
	method, url string,
	transport core.Transport,
	requestOverride *RequestCredentials,
) (map[string]string, error) {
	if transport == core.TransportHTTP && m.authMethod == core.AuthMethodOAuth1 {
		return nil, &Error{Message: "OAuth1 is not supported over HTTP transport: its signature must be recomputed per request against the destination URL using the server's consumer key/secret, which a simple incoming-header relay cannot provide. Use stdio transport for OAuth1-authenticated APIs."}
	}

	var credentials Credentials
	if transport == core.TransportHTTP {
		if requestOverride == nil {
			return nil, &Error{Message: "no credentials were supplied on this HTTP request; local config/env/keychain is never used as a fallback over HTTP transport"}
		}
		credentials = requestOverride.ToCredentialsMap()
	} else {
		var err error
		credentials, err = m.GetCredentials(ctx)
		if err != nil {
			return nil, err
		}
	}
	if m.authMethod == core.AuthMethodOAuth1 {
		consumerKey, hasConsumerKey := credentials["consumer_key"]
		privateKey, hasPrivateKey := credentials["private_key"]
		if !hasConsumerKey || !hasPrivateKey {
			return nil, NewMissingCredentialsError("consumer_key, private_key")
		}
		header, err := BuildOAuth1AuthorizationHeader(consumerKey, privateKey, credentials["token"], method, url)
		if err != nil {
			return nil, err
		}
		updated := copyHeaders(headers)
		updated["Authorization"] = header
		return updated, nil
	}

	updated := copyHeaders(headers)
	if headerName, ok := credentials["request_header_name"]; ok {
		updated[headerName] = credentials["request_header_value"]
	} else if authHeader, ok := credentials["authorization_header"]; ok {
		updated["Authorization"] = authHeader
	} else if apiKey, ok := credentials["api_key"]; ok {
		updated["X-Api-Key"] = apiKey
	}
	return updated, nil
}

func copyHeaders(headers map[string]string) map[string]string {
	updated := make(map[string]string, len(headers))
	for k, v := range headers {
		updated[k] = v
	}
	return updated
}
