diff --git a/pkg/connector/apikey.go b/pkg/connector/apikey.go index fb4db5913..3e4784259 100644 --- a/pkg/connector/apikey.go +++ b/pkg/connector/apikey.go @@ -24,6 +24,16 @@ import ( type APIKeyConnection struct { APIKey string `json:"api_key"` + // Header selects how the API key is presented on outbound requests. + // Empty (the default) sends it as `Authorization: Bearer `, + // which every OAuth-style and standard API-key connector uses. A + // non-empty value (e.g. "x-api-key") sends the raw key in that + // request header instead and omits Authorization entirely — + // required by providers such as Anthropic that reject Bearer auth + // and return 400 when both x-api-key and Authorization are present. + // It is populated from the provider Registration at connector + // creation time. + Header string `json:"header,omitempty"` } var _ Connection = (*APIKeyConnection)(nil) @@ -37,13 +47,43 @@ func (c *APIKeyConnection) Scopes() []string { } func (c *APIKeyConnection) Client(ctx context.Context) (*http.Client, error) { - transport := &oauth2Transport{ - token: c.APIKey, - tokenType: "Bearer", - underlying: httpclient.DefaultPooledTransport(httpclient.WithSSRFProtection()), + underlying := httpclient.DefaultPooledTransport(httpclient.WithSSRFProtection()) + + if c.Header != "" { + return &http.Client{ + Transport: &apiKeyHeaderTransport{ + header: c.Header, + value: c.APIKey, + underlying: underlying, + }, + }, nil } - return &http.Client{Transport: transport}, nil + return &http.Client{ + Transport: &oauth2Transport{ + token: c.APIKey, + tokenType: "Bearer", + underlying: underlying, + }, + }, nil +} + +// apiKeyHeaderTransport injects the API key into a custom request header +// (for example "x-api-key") and, unlike oauth2Transport, never sets +// Authorization. Providers such as Anthropic require the key in their +// own header and reject requests that carry both that header and +// Authorization. +type apiKeyHeaderTransport struct { + header string + value string + underlying http.RoundTripper +} + +func (t *apiKeyHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req2 := req.Clone(req.Context()) + req2.Header.Set(t.header, t.value) + + return t.underlying.RoundTrip(req2) } func (c APIKeyConnection) MarshalJSON() ([]byte, error) { diff --git a/pkg/connector/provider/registry.go b/pkg/connector/provider/registry.go index 2492977df..3a1a75ed8 100644 --- a/pkg/connector/provider/registry.go +++ b/pkg/connector/provider/registry.go @@ -119,6 +119,19 @@ func (r *Registry) ProviderDisplayName(p coredata.ConnectorProvider) string { return string(p) } +// APIKeyHeader returns the request header an API-key connection for the +// given provider must use to present its key. Empty means the default +// `Authorization: Bearer` scheme; a value such as "x-api-key" means the +// raw key is sent in that header instead. Returns empty for unknown +// providers and for providers that do not customise the scheme. +func (r *Registry) APIKeyHeader(p coredata.ConnectorProvider) string { + if reg, ok := r.Get(p); ok { + return reg.APIKeyHeader + } + + return "" +} + // ProviderOAuth2Scopes returns the OAuth2 scopes the access review // driver for the given provider needs to list user accounts. Returns // nil for providers that do not need any scopes (Notion, Intercom) diff --git a/pkg/connector/provider/types.go b/pkg/connector/provider/types.go index 3f9e66ac8..0c02b5d21 100644 --- a/pkg/connector/provider/types.go +++ b/pkg/connector/provider/types.go @@ -56,6 +56,13 @@ type Registration struct { SupportsAPIKey bool SupportsClientCredentials bool ExtraSettings []ExtraSetting + // APIKeyHeader selects how an API-key connection presents its key + // on outbound requests. Empty (the default) uses the standard + // `Authorization: Bearer ` scheme; a value such as "x-api-key" + // sends the raw key in that header instead and omits Authorization + // (Anthropic). It is consumed when the create-connector resolver + // builds the APIKeyConnection. + APIKeyHeader string // Factory closures — wired by Stages 2 and 3. NewDriver func(context.Context, *http.Client, *coredata.Connector, *log.Logger) (drivers.Driver, error) diff --git a/pkg/server/api/console/v1/connector_resolvers.go b/pkg/server/api/console/v1/connector_resolvers.go index 2b51a4241..7a164fcc0 100644 --- a/pkg/server/api/console/v1/connector_resolvers.go +++ b/pkg/server/api/console/v1/connector_resolvers.go @@ -40,7 +40,10 @@ func (r *mutationResolver) CreateAPIKeyConnector(ctx context.Context, input type OrganizationID: input.OrganizationID, Provider: input.Provider, Protocol: coredata.ConnectorProtocolAPIKey, - Connection: &connector.APIKeyConnection{APIKey: input.APIKey}, + Connection: &connector.APIKeyConnection{ + APIKey: input.APIKey, + Header: r.providerRegistry.APIKeyHeader(input.Provider), + }, } raw, err := apiKeyConnectorSettings(input)