Files
klinsmayaandClaude Fable 5.1 f23926b2d6 feat(server): add open-connector admin API client
Standalone SDK for a self-hosted open-connector instance: provider
catalog and setup, OAuth client configs, OAuth and API-key connection
flows, connection deletion, and runtime token management. Responses
are unwrapped per path family (/v1 envelope vs bare /api JSON); secrets
in returned structs are redacted from String, GoString and slog output.
Tests replay recorded responses from a real instance.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XqqpKySn2iWtXriK3Gsgak
2026-10-20 17:13:24 +00:00

83 lines
2.1 KiB
Go

package openconnector
import (
"encoding/json"
"errors"
"fmt"
"net/http"
)
// Sentinel errors, matched with errors.Is against an *APIError by HTTP status.
var (
ErrNotFound = errors.New("openconnector: not found")
ErrUnauthorized = errors.New("openconnector: unauthorized")
ErrForbidden = errors.New("openconnector: forbidden")
)
// APIError is returned for any non-2xx response and for 2xx /v1 responses with
// success=false. It deliberately carries neither request nor raw response data.
type APIError struct {
// Status is the HTTP status code.
Status int
// Code is the machine-readable error code (errorCode on /v1, error.code on /api).
Code string
// Message is the human-readable message supplied by the server.
Message string
}
// Error implements error.
func (e *APIError) Error() string {
if e == nil {
return ""
}
msg := e.Message
if msg == "" {
msg = http.StatusText(e.Status)
}
if e.Code != "" {
return fmt.Sprintf("openconnector: %d %s (%s)", e.Status, msg, e.Code)
}
return fmt.Sprintf("openconnector: %d %s", e.Status, msg)
}
// Is makes errors.Is(err, ErrNotFound/ErrUnauthorized/ErrForbidden) work.
func (e *APIError) Is(target error) bool {
if e == nil {
return false
}
switch target {
case ErrNotFound:
return e.Status == http.StatusNotFound
case ErrUnauthorized:
return e.Status == http.StatusUnauthorized
case ErrForbidden:
return e.Status == http.StatusForbidden
}
return false
}
// parseAPIError understands both error shapes regardless of path family.
func parseAPIError(status int, body []byte) *APIError {
out := &APIError{Status: status}
var wire struct {
ErrorCode string `json:"errorCode"`
Message string `json:"message"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if len(body) == 0 || json.Unmarshal(body, &wire) != nil {
return out
}
switch {
case wire.Error != nil:
out.Code, out.Message = wire.Error.Code, wire.Error.Message
default:
out.Code, out.Message = wire.ErrorCode, wire.Message
}
return out
}
func isNotFound(err error) bool { return errors.Is(err, ErrNotFound) }