Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
For releases prior to `0.2.0`, see the auto-generated notes on the
[GitHub Releases](https://github.com/chatbotkit/go-sdk/releases) page.

## [0.4.0] - 2026-06-27

### Added

- Secret token minting and request proxying. `SecretClient.Mint` /
`ContactSecretClient.Mint` mint a usable token from a secret (`oauth`/`jwt`
secrets only; owner-only) and return `{ Token, ExpiresAt }`.
`SecretClient.Proxy` / `ContactSecretClient.Proxy` proxy a request through a
secret — the credential is injected server-side (it never leaves the platform)
and the raw upstream `*http.Response` is returned verbatim. A non-2xx status
(including `409 authorization_required`) is returned, not an error; the caller
closes `resp.Body`. Backed by the new internal `httpclient.DoRaw`.

## [0.3.0] - 2026-06-26

### Added
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.0
0.4.0
60 changes: 60 additions & 0 deletions internal/httpclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,66 @@ func (c *Client) Do(ctx context.Context, opts RequestOptions, result interface{}
return nil
}

// DoRaw performs a request and returns the raw HTTP response without reading or
// decoding the body and without treating a non-2xx status as an error. The
// caller is responsible for closing resp.Body. It is intended for passthrough
// endpoints such as the secret proxy.
func (c *Client) DoRaw(ctx context.Context, opts RequestOptions) (*http.Response, error) {
u, err := url.Parse(c.BaseURL)
if err != nil {
return nil, fmt.Errorf("invalid base URL: %w", err)
}

u.Path = opts.Path
if opts.Query != nil {
u.RawQuery = opts.Query.Encode()
}

var body io.Reader
if opts.Body != nil {
data, err := json.Marshal(opts.Body)
if err != nil {
return nil, fmt.Errorf("failed to encode request body: %w", err)
}
body = bytes.NewReader(data)
}

method := opts.Method
if method == "" {
if opts.Body != nil {
method = http.MethodPost
} else {
method = http.MethodGet
}
}

req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

if opts.Body != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.Secret != "" {
req.Header.Set("Authorization", "Bearer "+c.Secret)
}
if c.RunAsUserID != "" {
req.Header.Set("X-RunAs-User-ID", c.RunAsUserID)
}
if c.Timezone != "" {
req.Header.Set("X-Timezone", c.Timezone)
}
for k, v := range c.Headers {
req.Header.Set(k, v)
}
for k, v := range opts.Headers {
req.Header.Set(k, v)
}

return c.HTTPClient.Do(req)
}

// Get performs a GET request.
func (c *Client) Get(ctx context.Context, path string, query url.Values, result interface{}) error {
return c.Do(ctx, RequestOptions{
Expand Down
25 changes: 25 additions & 0 deletions sdk/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sdk
import (
"context"
"fmt"
"net/http"
"net/url"

"github.com/chatbotkit/go-sdk/internal/httpclient"
Expand Down Expand Up @@ -184,6 +185,30 @@ func (c *ContactSecretClient) Authenticate(ctx context.Context, contactID, secre
return &result, nil
}

// Mint mints a usable token from a contact's secret (owner-only; oauth/jwt only).
func (c *ContactSecretClient) Mint(ctx context.Context, contactID, secretID string) (*types.ContactSecretMintResponse, error) {
path := fmt.Sprintf("/api/v1/contact/%s/secret/%s/mint", contactID, secretID)

var result types.ContactSecretMintResponse
if err := c.httpClient.Post(ctx, path, map[string]interface{}{}, &result); err != nil {
return nil, err
}
return &result, nil
}

// Proxy proxies a request through a contact's secret, injecting it server-side.
// It returns the raw upstream HTTP response; a non-2xx status is returned, not
// an error. The caller must close resp.Body.
func (c *ContactSecretClient) Proxy(ctx context.Context, contactID, secretID string, req types.ContactSecretProxyRequest) (*http.Response, error) {
path := fmt.Sprintf("/api/v1/contact/%s/secret/%s/proxy", contactID, secretID)

return c.httpClient.DoRaw(ctx, httpclient.RequestOptions{
Method: http.MethodPost,
Path: path,
Body: req,
})
}

// ContactSpaceClient provides access to contact space resources.
type ContactSpaceClient struct {
httpClient *httpclient.Client
Expand Down
26 changes: 26 additions & 0 deletions sdk/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sdk
import (
"context"
"fmt"
"net/http"
"net/url"

"github.com/chatbotkit/go-sdk/internal/httpclient"
Expand Down Expand Up @@ -110,3 +111,28 @@ func (c *SecretClient) Authenticate(ctx context.Context, secretID string) (*type
}
return &result, nil
}

// Mint mints a usable token from a secret (owner-only; oauth/jwt only).
func (c *SecretClient) Mint(ctx context.Context, secretID string) (*types.SecretMintResponse, error) {
path := fmt.Sprintf("/api/v1/secret/%s/mint", secretID)

var result types.SecretMintResponse
if err := c.httpClient.Post(ctx, path, struct{}{}, &result); err != nil {
return nil, err
}
return &result, nil
}

// Proxy proxies a request through the secret, injecting it server-side. It
// returns the raw upstream HTTP response; a non-2xx status (including
// 409 authorization_required) is returned, not an error. The caller must close
// resp.Body.
func (c *SecretClient) Proxy(ctx context.Context, secretID string, req types.SecretProxyRequest) (*http.Response, error) {
path := fmt.Sprintf("/api/v1/secret/%s/proxy", secretID)

return c.httpClient.DoRaw(ctx, httpclient.RequestOptions{
Method: http.MethodPost,
Path: path,
Body: req,
})
}
164 changes: 164 additions & 0 deletions types/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading