├── .gitignore ├── PKGBUILD ├── README.md ├── go.mod ├── go.sum ├── internal └── auth │ ├── apikey.go │ └── auth.go ├── main.go └── transport └── transport.go /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE/Tools 2 | .idea 3 | /.vscode 4 | # vim 5 | .*.sw? 6 | 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | vendor/ 22 | 23 | # Build Output 24 | target/ 25 | 26 | # Protobuf dependencies 27 | .proto 28 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Di Mei 2 | # Maintainer: Kainoa Kanter 3 | 4 | _pkgname=cdpcurl 5 | pkgname="$_pkgname-git" 6 | pkgver=r12.g8dc4b89 7 | pkgrel=2 8 | pkgdesc='CLI for the Coinbase Developer Platform (CDP)' 9 | url='https://github.com/coinbase/cdpcurl' 10 | arch=('aarch64' 'i686' 'x86_64') 11 | license=('custom:none') 12 | depends=('glibc') 13 | makedepends=('git' 'go') 14 | provides=("$_pkgname") 15 | source=("$_pkgname::git+$url") 16 | sha256sums=('SKIP') 17 | 18 | pkgver() { 19 | cd "${srcdir}/${_pkgname}" || exit 20 | printf "r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" 21 | } 22 | 23 | prepare() { 24 | cd "$_pkgname" 25 | go mod download 26 | } 27 | 28 | build() { 29 | export CGO_CPPFLAGS="${CPPFLAGS}" 30 | export CGO_CFLAGS="${CFLAGS}" 31 | export CGO_CXXFLAGS="${CXXFLAGS}" 32 | export CGO_LDFLAGS="${LDFLAGS}" 33 | export GOFLAGS="-buildmode=pie -trimpath -ldflags=-linkmode=external -mod=readonly -modcacherw" 34 | cd "$_pkgname" 35 | go build 36 | } 37 | 38 | check() { 39 | export CGO_CPPFLAGS="${CPPFLAGS}" 40 | export CGO_CFLAGS="${CFLAGS}" 41 | export CGO_CXXFLAGS="${CXXFLAGS}" 42 | export CGO_LDFLAGS="${LDFLAGS}" 43 | export GOFLAGS="-buildmode=pie -trimpath -ldflags=-linkmode=external -mod=readonly -modcacherw" 44 | cd "$_pkgname" 45 | go test ./... 46 | } 47 | 48 | package() { 49 | cd "$_pkgname" 50 | install -Dv cdpcurl -t "$pkgdir/usr/bin/" 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cdpcurl 2 | 3 | `cdpcurl` is a tool that allows you to make HTTP requests to the Coinbase API with your CDP (Coinbase Developer Platform) API key. It is a wrapper around curl that automatically adds the necessary headers to authenticate your requests. 4 | 5 | To use with Ed25519 keys (Edwards keys), please upgrade to the latest version. 6 | 7 | ## Installation 8 | 9 | ### Homebrew 10 | 11 | ```bash 12 | brew tap coinbase/cdpcurl 13 | brew install cdpcurl 14 | ``` 15 | 16 | ### AUR (_Thanks for the [contribution](https://github.com/coinbase/cdpcurl/pull/27) from @[ThatOneCalculator](https://github.com/ThatOneCalculator)!_) 17 | 18 | ```bash 19 | yay -S cdpcurl-git 20 | ``` 21 | 22 | ### Go 23 | 24 | ```bash 25 | go install github.com/coinbase/cdpcurl@latest 26 | ``` 27 | 28 | ## Example Usage 29 | 30 | ### Get account balance of BTC with Sign In With Coinbase API 31 | ```bash 32 | cdpcurl -k ~/Downloads/cdp_api_key.json 'https://api.coinbase.com/v2/accounts/BTC' 33 | ``` 34 | 35 | ### Get the latest price of BTC with Advanced Trading API 36 | ```bash 37 | cdpcurl -k ~/Downloads/cdp_api_key.json 'https://api.coinbase.com/api/v3/brokerage/products/BTC-USDC' 38 | ``` 39 | 40 | ### Create a wallet on Base Sepolia with CDP API using key ID and secret 41 | 42 | ```bash 43 | cdpcurl -i '{KEY_ID}' -s '{SECRET}' -X POST -d '{"wallet": {"network_id": "base-sepolia"}}' 'https://api.developer.coinbase.com/platform/v1/wallets' 44 | ``` 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coinbase/cdpcurl 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/spf13/cobra v1.8.0 7 | gopkg.in/square/go-jose.v2 v2.6.0 8 | ) 9 | 10 | require ( 11 | github.com/google/go-cmp v0.5.9 // indirect 12 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 13 | github.com/spf13/pflag v1.0.5 // indirect 14 | github.com/stretchr/testify v1.8.4 // indirect 15 | golang.org/x/crypto v0.31.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 4 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 5 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 6 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 9 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 10 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 11 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 12 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 13 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 14 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 15 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 16 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= 19 | gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 21 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /internal/auth/apikey.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path" 8 | ) 9 | 10 | type APIKey struct { 11 | Name string 12 | PrivateKey string 13 | } 14 | 15 | // UnmarshalJSON allows APIKey to support both the old and new JSON field names. 16 | func (a *APIKey) UnmarshalJSON(data []byte) error { 17 | // Define a temporary structure with both possible field names. 18 | var aux struct { 19 | Name string `json:"name"` 20 | ID string `json:"id"` 21 | PrivateKey string `json:"privateKey"` 22 | Secret string `json:"secret"` 23 | } 24 | 25 | if err := json.Unmarshal(data, &aux); err != nil { 26 | return err 27 | } 28 | 29 | // Use the old or new field for the API key name. 30 | if aux.Name != "" { 31 | a.Name = aux.Name 32 | } else { 33 | a.Name = aux.ID 34 | } 35 | 36 | // Use the old or new field for the API key secret. 37 | if aux.PrivateKey != "" { 38 | a.PrivateKey = aux.PrivateKey 39 | } else { 40 | a.PrivateKey = aux.Secret 41 | } 42 | 43 | return nil 44 | } 45 | 46 | type apiKeyLoaderConfig struct { 47 | filename string 48 | path string 49 | envvars map[string]string 50 | envOnly bool 51 | fileOnly bool 52 | directID string 53 | directSecret string 54 | useDirectValues bool 55 | } 56 | 57 | const ( 58 | nameEnvVar = "COINBASE_CLOUD_API_KEY_NAME" 59 | privateKeyEnvVar = "COINBASE_CLOUD_API_PRIVATE_KEY" 60 | defaultFilename = ".coinbase_cloud_api_key.json" 61 | ) 62 | 63 | type LoadAPIKeyOption func(*apiKeyLoaderConfig) 64 | 65 | func WithPath(path string) LoadAPIKeyOption { 66 | return func(c *apiKeyLoaderConfig) { 67 | c.path = path 68 | } 69 | } 70 | 71 | func WithFileName(filename string) LoadAPIKeyOption { 72 | return func(c *apiKeyLoaderConfig) { 73 | c.filename = filename 74 | } 75 | } 76 | 77 | func WithENVVariableNames(name, privateKey string) LoadAPIKeyOption { 78 | return func(c *apiKeyLoaderConfig) { 79 | c.envvars = map[string]string{ 80 | nameEnvVar: name, 81 | privateKeyEnvVar: privateKey, 82 | } 83 | } 84 | } 85 | 86 | func WithENVOnly() LoadAPIKeyOption { 87 | return func(c *apiKeyLoaderConfig) { 88 | c.fileOnly = false 89 | c.envOnly = true 90 | } 91 | } 92 | 93 | func WithFileOnly() LoadAPIKeyOption { 94 | return func(c *apiKeyLoaderConfig) { 95 | c.fileOnly = true 96 | c.envOnly = false 97 | } 98 | } 99 | 100 | func WithDirectIDAndSecret(id, secret string) LoadAPIKeyOption { 101 | return func(c *apiKeyLoaderConfig) { 102 | c.directID = id 103 | c.directSecret = secret 104 | c.useDirectValues = true 105 | } 106 | } 107 | 108 | func LoadAPIKey(options ...LoadAPIKeyOption) (*APIKey, error) { 109 | c := &apiKeyLoaderConfig{ 110 | filename: defaultFilename, 111 | envvars: map[string]string{ 112 | nameEnvVar: nameEnvVar, 113 | privateKeyEnvVar: privateKeyEnvVar, 114 | }, 115 | } 116 | for _, o := range options { 117 | o(c) 118 | } 119 | 120 | apiKey := &APIKey{} 121 | 122 | if !c.envOnly { 123 | if err := c.loadApiKeyFromFile(apiKey); err != nil { 124 | return nil, fmt.Errorf("api key loader: %w", err) 125 | } 126 | } 127 | 128 | if !c.fileOnly { 129 | c.loadApiKeyFromEnv(apiKey) 130 | } 131 | 132 | if apiKey.Name == "" || apiKey.PrivateKey == "" { 133 | return nil, fmt.Errorf("api key loader: could not load api key") 134 | } 135 | 136 | return apiKey, nil 137 | } 138 | 139 | func (c *apiKeyLoaderConfig) loadApiKeyFromFile(a *APIKey) error { 140 | if c.path != "" { 141 | f, err := os.Open(c.path) 142 | if err != nil { 143 | return fmt.Errorf("file load: %w", err) 144 | } 145 | defer f.Close() 146 | 147 | dec := json.NewDecoder(f) 148 | if err := dec.Decode(a); err != nil { 149 | return fmt.Errorf("file load: %w", err) 150 | } 151 | 152 | return nil 153 | } 154 | 155 | wd, err := os.Getwd() 156 | if err != nil { 157 | return fmt.Errorf("file load: %w", err) 158 | } 159 | for wd != "" && wd != "/" { 160 | keyFilepath := path.Join(wd, c.filename) 161 | f, err := os.Open(keyFilepath) 162 | if err != nil { 163 | // Skip if file not accessible. 164 | wd = path.Dir(wd) 165 | continue 166 | } 167 | defer f.Close() 168 | 169 | dec := json.NewDecoder(f) 170 | if err := dec.Decode(a); err != nil { 171 | return fmt.Errorf("file load: %w", err) 172 | } 173 | 174 | return nil 175 | } 176 | 177 | return nil 178 | } 179 | 180 | func (c *apiKeyLoaderConfig) loadApiKeyFromEnv(a *APIKey) { 181 | if c.useDirectValues { 182 | if a.Name == "" { 183 | a.Name = c.directID 184 | } 185 | if a.PrivateKey == "" { 186 | a.PrivateKey = c.directSecret 187 | } 188 | return 189 | } 190 | 191 | if a.Name == "" { 192 | a.Name = os.Getenv(c.envvars[nameEnvVar]) 193 | } 194 | if a.PrivateKey == "" { 195 | a.PrivateKey = os.Getenv(c.envvars[privateKeyEnvVar]) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /internal/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "encoding/pem" 9 | "fmt" 10 | "math" 11 | "math/big" 12 | "strings" 13 | "time" 14 | 15 | "gopkg.in/square/go-jose.v2" 16 | "gopkg.in/square/go-jose.v2/jwt" 17 | ) 18 | 19 | type APIKeyClaims struct { 20 | *jwt.Claims 21 | URIs []string `json:"uris"` 22 | } 23 | 24 | type Authenticator struct { 25 | apiKey APIKey 26 | } 27 | 28 | func New(apiKeyOpts ...LoadAPIKeyOption) (*Authenticator, error) { 29 | apiKey, err := LoadAPIKey(apiKeyOpts...) 30 | if err != nil { 31 | return nil, fmt.Errorf("auth: %w", err) 32 | } 33 | return &Authenticator{ 34 | apiKey: *apiKey, 35 | }, nil 36 | } 37 | 38 | func NewFromConfig(apiKey APIKey) *Authenticator { 39 | return &Authenticator{ 40 | apiKey: apiKey, 41 | } 42 | } 43 | 44 | func (a *Authenticator) BuildJWT(service string, uris []string) (string, error) { 45 | keyStr := a.apiKey.PrivateKey 46 | var ( 47 | key interface{} 48 | alg jose.SignatureAlgorithm 49 | ) 50 | 51 | // If the key starts with a PEM header, parse it as an ECDSA key. 52 | if strings.HasPrefix(keyStr, "-----BEGIN") { 53 | block, _ := pem.Decode([]byte(keyStr)) 54 | if block == nil { 55 | return "", fmt.Errorf("jwt: could not decode PEM private key") 56 | } 57 | ecdsaKey, err := x509.ParseECPrivateKey(block.Bytes) 58 | if err != nil { 59 | return "", fmt.Errorf("jwt: error parsing EC private key: %w", err) 60 | } 61 | key = ecdsaKey 62 | alg = jose.ES256 63 | } else { 64 | // Otherwise, assume the key is a Base64 encoded Ed25519 private key. 65 | decodedKey, err := base64.StdEncoding.DecodeString(keyStr) 66 | if err != nil { 67 | return "", fmt.Errorf("jwt: error base64 decoding key: %w", err) 68 | } 69 | if len(decodedKey) != ed25519.PrivateKeySize { 70 | return "", fmt.Errorf("jwt: invalid Ed25519 private key length: got %d, expected %d", len(decodedKey), ed25519.PrivateKeySize) 71 | } 72 | key = ed25519.PrivateKey(decodedKey) 73 | alg = jose.EdDSA 74 | } 75 | 76 | // Create the JOSE signer with the appropriate algorithm. 77 | sig, err := jose.NewSigner( 78 | jose.SigningKey{Algorithm: alg, Key: key}, 79 | (&jose.SignerOptions{NonceSource: nonceSource{}}). 80 | WithType("JWT"). 81 | WithHeader("kid", a.apiKey.Name), 82 | ) 83 | if err != nil { 84 | return "", fmt.Errorf("jwt: error creating signer: %w", err) 85 | } 86 | 87 | // Build the JWT claims. 88 | claims := &APIKeyClaims{ 89 | Claims: &jwt.Claims{ 90 | Subject: a.apiKey.Name, 91 | Issuer: "coinbase-cloud", 92 | NotBefore: jwt.NewNumericDate(time.Now()), 93 | Expiry: jwt.NewNumericDate(time.Now().Add(1 * time.Minute)), 94 | Audience: jwt.Audience{service}, 95 | }, 96 | URIs: uris, 97 | } 98 | 99 | // Serialize the JWT. 100 | jwtString, err := jwt.Signed(sig).Claims(claims).CompactSerialize() 101 | if err != nil { 102 | return "", fmt.Errorf("jwt: error serializing token: %w", err) 103 | } 104 | return jwtString, nil 105 | } 106 | 107 | var max = big.NewInt(math.MaxInt64) 108 | 109 | type nonceSource struct{} 110 | 111 | func (n nonceSource) Nonce() (string, error) { 112 | r, err := rand.Int(rand.Reader, max) 113 | if err != nil { 114 | return "", err 115 | } 116 | return r.String(), nil 117 | } 118 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/coinbase/cdpcurl/internal/auth" 12 | "github.com/coinbase/cdpcurl/transport" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var ( 17 | version = "v0.0.6" 18 | ) 19 | 20 | var versionCmd = &cobra.Command{ 21 | Use: "version", 22 | Aliases: []string{"v"}, 23 | Short: "Print the version number of cdpcurl", 24 | Long: `All software has versions. This is cdpcurl's`, 25 | Run: func(cmd *cobra.Command, args []string) { 26 | fmt.Println(version) 27 | }, 28 | } 29 | 30 | func main() { 31 | var data, method, apiKeyPath, header string 32 | var versionFlag bool 33 | var id, secret string 34 | 35 | cmd := &cobra.Command{ 36 | Use: "cdpcurl [flags] [URL]", 37 | Args: cobra.MinimumNArgs(0), // Allow zero arguments to handle -v 38 | RunE: func(cmd *cobra.Command, args []string) error { 39 | if versionFlag { 40 | fmt.Println(version) 41 | return nil 42 | } 43 | 44 | if len(args) == 0 { 45 | return fmt.Errorf("URL is required unless using -v") 46 | } 47 | 48 | opts := []transport.Option{} 49 | if apiKeyPath != "" { 50 | opts = append(opts, transport.WithAPIKeyLoaderOption(transport.WithPath(apiKeyPath))) 51 | } 52 | 53 | // Add options for id and secret if they are provided 54 | if id != "" && secret != "" { 55 | opts = append(opts, transport.WithAPIKeyLoaderOption(auth.WithDirectIDAndSecret(id, secret))) 56 | } 57 | 58 | authTransport, err := transport.New("", http.DefaultTransport, opts...) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | req, err := http.NewRequest(method, args[0], bytes.NewBufferString(data)) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | if method == http.MethodPost && header == "" { 69 | req.Header.Set("Content-Type", "application/json") 70 | } 71 | 72 | if header != "" { 73 | var headers map[string]string 74 | if err := json.Unmarshal([]byte(header), &headers); err != nil { 75 | return err 76 | } 77 | 78 | for key, value := range headers { 79 | req.Header.Set(key, value) 80 | } 81 | } 82 | 83 | client := http.Client{Transport: authTransport} 84 | 85 | resp, err := client.Do(req) 86 | if err != nil { 87 | return err 88 | } 89 | defer resp.Body.Close() 90 | 91 | // Read response body 92 | body, err := io.ReadAll(resp.Body) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | // Print HTTP status code and response body 98 | fmt.Println(resp.Status) 99 | fmt.Println(string(body)) 100 | return nil 101 | }, 102 | } 103 | 104 | cmd.Flags().StringVarP(&data, "data", "d", "", "HTTP Body") 105 | cmd.Flags().StringVarP(&apiKeyPath, "api-key-path", "k", "", "API Key Path") 106 | cmd.Flags().StringVarP(&method, "method", "X", "GET", "HTTP Method") 107 | cmd.Flags().StringVarP(&header, "header", "H", "", "HTTP Header") 108 | cmd.Flags().BoolVarP(&versionFlag, "version", "v", false, "Print the version number and exit") 109 | cmd.Flags().StringVarP(&id, "id", "i", "", "API Key ID (only works with Ed25519 keys)") 110 | cmd.Flags().StringVarP(&secret, "secret", "s", "", "API Key Secret (only works with Ed25519 keys)") 111 | 112 | cmd.AddCommand(versionCmd) 113 | 114 | // Custom usage template 115 | customUsage := `Usage: 116 | {{.UseLine}} 117 | 118 | Flags: 119 | {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}} 120 | 121 | Use "{{.CommandPath}} [command] --help" for more information about a command. 122 | ` 123 | 124 | cmd.SetUsageTemplate(customUsage) 125 | 126 | if err := cmd.Execute(); err != nil { 127 | log.Fatal(err) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/coinbase/cdpcurl/internal/auth" 8 | ) 9 | 10 | type APIKey = auth.APIKey 11 | 12 | var ( 13 | WithENVVariableNames = auth.WithENVVariableNames 14 | WithENVOnly = auth.WithENVOnly 15 | WithFileName = auth.WithFileName 16 | WithFileOnly = auth.WithFileOnly 17 | WithPath = auth.WithPath 18 | ) 19 | 20 | type transport struct { 21 | originalTransport http.RoundTripper 22 | authenticator *auth.Authenticator 23 | serviceName string 24 | } 25 | 26 | type Option func(o *options) 27 | 28 | type options struct { 29 | apiKey *auth.APIKey 30 | 31 | apiKeyOptions []auth.LoadAPIKeyOption 32 | } 33 | 34 | func WithAPIKeyLoaderOption(opt auth.LoadAPIKeyOption) Option { 35 | return func(o *options) { 36 | o.apiKeyOptions = append(o.apiKeyOptions, opt) 37 | } 38 | } 39 | 40 | func WithAPIKey(apiKey *auth.APIKey) Option { 41 | return func(o *options) { 42 | o.apiKey = apiKey 43 | } 44 | } 45 | 46 | func New(service string, originalTransport http.RoundTripper, opts ...Option) (http.RoundTripper, error) { 47 | o := &options{ 48 | apiKeyOptions: make([]auth.LoadAPIKeyOption, 0), 49 | } 50 | for _, opt := range opts { 51 | opt(o) 52 | } 53 | var authenticator *auth.Authenticator 54 | if o.apiKey != nil { 55 | authenticator = auth.NewFromConfig(*o.apiKey) 56 | } else { 57 | var err error 58 | authenticator, err = auth.New(o.apiKeyOptions...) 59 | if err != nil { 60 | return nil, err 61 | } 62 | } 63 | 64 | return &transport{ 65 | originalTransport: originalTransport, 66 | authenticator: authenticator, 67 | serviceName: service, 68 | }, nil 69 | } 70 | 71 | func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { 72 | jwt, err := t.authenticator.BuildJWT( 73 | t.serviceName, 74 | []string{fmt.Sprintf("%s %s%s", req.Method, req.URL.Host, req.URL.Path)}, 75 | ) 76 | if err != nil { 77 | return nil, err 78 | } 79 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt)) 80 | return t.originalTransport.RoundTrip(req) 81 | } 82 | --------------------------------------------------------------------------------