From 6155aed6fa292b2e815f087f530682cfdf4e0d6e Mon Sep 17 00:00:00 2001 From: pdparchitect Date: Sat, 27 Jun 2026 13:51:45 +0000 Subject: [PATCH] - feat: add secret token minting and request proxying functionality --- CHANGELOG.md | 13 +++ VERSION | 2 +- internal/httpclient/client.go | 60 +++++++++++++ sdk/contact.go | 25 ++++++ sdk/secret.go | 26 ++++++ types/types.go | 164 ++++++++++++++++++++++++++++++++++ 6 files changed, 289 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adcc793..a8e90ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index 0d91a54..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 +0.4.0 diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go index c3f9f68..e666d75 100644 --- a/internal/httpclient/client.go +++ b/internal/httpclient/client.go @@ -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{ diff --git a/sdk/contact.go b/sdk/contact.go index 3510b2e..56754ff 100644 --- a/sdk/contact.go +++ b/sdk/contact.go @@ -3,6 +3,7 @@ package sdk import ( "context" "fmt" + "net/http" "net/url" "github.com/chatbotkit/go-sdk/internal/httpclient" @@ -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 diff --git a/sdk/secret.go b/sdk/secret.go index 62b2154..f4f67c7 100644 --- a/sdk/secret.go +++ b/sdk/secret.go @@ -3,6 +3,7 @@ package sdk import ( "context" "fmt" + "net/http" "net/url" "github.com/chatbotkit/go-sdk/internal/httpclient" @@ -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, + }) +} diff --git a/types/types.go b/types/types.go index 529a2a9..d52c38c 100644 --- a/types/types.go +++ b/types/types.go @@ -364,6 +364,18 @@ // contactSecretAuthenticateResponse, err := UnmarshalContactSecretAuthenticateResponse(bytes) // bytes, err = contactSecretAuthenticateResponse.Marshal() // +// contactSecretMintParams, err := UnmarshalContactSecretMintParams(bytes) +// bytes, err = contactSecretMintParams.Marshal() +// +// contactSecretMintResponse, err := UnmarshalContactSecretMintResponse(bytes) +// bytes, err = contactSecretMintResponse.Marshal() +// +// contactSecretProxyParams, err := UnmarshalContactSecretProxyParams(bytes) +// bytes, err = contactSecretProxyParams.Marshal() +// +// contactSecretProxyRequest, err := UnmarshalContactSecretProxyRequest(bytes) +// bytes, err = contactSecretProxyRequest.Marshal() +// // contactSecretRevokeParams, err := UnmarshalContactSecretRevokeParams(bytes) // bytes, err = contactSecretRevokeParams.Marshal() // @@ -2326,6 +2338,18 @@ // secretFetchResponse, err := UnmarshalSecretFetchResponse(bytes) // bytes, err = secretFetchResponse.Marshal() // +// secretMintParams, err := UnmarshalSecretMintParams(bytes) +// bytes, err = secretMintParams.Marshal() +// +// secretMintResponse, err := UnmarshalSecretMintResponse(bytes) +// bytes, err = secretMintResponse.Marshal() +// +// secretProxyParams, err := UnmarshalSecretProxyParams(bytes) +// bytes, err = secretProxyParams.Marshal() +// +// secretProxyRequest, err := UnmarshalSecretProxyRequest(bytes) +// bytes, err = secretProxyRequest.Marshal() +// // secretRevokeParams, err := UnmarshalSecretRevokeParams(bytes) // bytes, err = secretRevokeParams.Marshal() // @@ -4074,6 +4098,46 @@ func (r *ContactSecretAuthenticateResponse) Marshal() ([]byte, error) { return json.Marshal(r) } +func UnmarshalContactSecretMintParams(data []byte) (ContactSecretMintParams, error) { + var r ContactSecretMintParams + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *ContactSecretMintParams) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +func UnmarshalContactSecretMintResponse(data []byte) (ContactSecretMintResponse, error) { + var r ContactSecretMintResponse + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *ContactSecretMintResponse) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +func UnmarshalContactSecretProxyParams(data []byte) (ContactSecretProxyParams, error) { + var r ContactSecretProxyParams + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *ContactSecretProxyParams) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +func UnmarshalContactSecretProxyRequest(data []byte) (ContactSecretProxyRequest, error) { + var r ContactSecretProxyRequest + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *ContactSecretProxyRequest) Marshal() ([]byte, error) { + return json.Marshal(r) +} + func UnmarshalContactSecretRevokeParams(data []byte) (ContactSecretRevokeParams, error) { var r ContactSecretRevokeParams err := json.Unmarshal(data, &r) @@ -10722,6 +10786,46 @@ func (r *SecretFetchResponse) Marshal() ([]byte, error) { return json.Marshal(r) } +func UnmarshalSecretMintParams(data []byte) (SecretMintParams, error) { + var r SecretMintParams + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *SecretMintParams) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +func UnmarshalSecretMintResponse(data []byte) (SecretMintResponse, error) { + var r SecretMintResponse + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *SecretMintResponse) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +func UnmarshalSecretProxyParams(data []byte) (SecretProxyParams, error) { + var r SecretProxyParams + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *SecretProxyParams) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +func UnmarshalSecretProxyRequest(data []byte) (SecretProxyRequest, error) { + var r SecretProxyRequest + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *SecretProxyRequest) Marshal() ([]byte, error) { + return json.Marshal(r) +} + func UnmarshalSecretRevokeParams(data []byte) (SecretRevokeParams, error) { var r SecretRevokeParams err := json.Unmarshal(data, &r) @@ -13706,6 +13810,38 @@ type ContactSecretAuthenticateResponse struct { URL string `json:"url"` } +type ContactSecretMintParams struct { + // The ID of the contact the secret belongs to + ContactID string `json:"contactId"` + // The ID of the secret to mint + SecretID string `json:"secretId"` +} + +type ContactSecretMintResponse struct { + // Token expiry as a unix timestamp in ms, or null + ExpiresAt *float64 `json:"expiresAt,omitempty"` + // The usable token to send to the provider + Token string `json:"token"` +} + +type ContactSecretProxyParams struct { + // The ID of the contact the secret belongs to + ContactID string `json:"contactId"` + // The ID of the secret to inject + SecretID string `json:"secretId"` +} + +type ContactSecretProxyRequest struct { + // The request body + Body *string `json:"body,omitempty"` + // The request headers (may reference the secret) + Headers map[string]string `json:"headers,omitempty"` + // The HTTP method + Method *string `json:"method,omitempty"` + // The destination URL + URL string `json:"url"` +} + type ContactSecretRevokeParams struct { // The ID of the contact the secret belongs to ContactID string `json:"contactId"` @@ -24382,6 +24518,34 @@ type SecretFetchResponse struct { Visibility *SecretFetchResponseVisibility `json:"visibility,omitempty"` } +type SecretMintParams struct { + // The ID of the secret to mint + SecretID string `json:"secretId"` +} + +type SecretMintResponse struct { + // Token expiry as a unix timestamp in ms, or null + ExpiresAt *float64 `json:"expiresAt,omitempty"` + // The usable token to send to the provider + Token string `json:"token"` +} + +type SecretProxyParams struct { + // The ID of the secret to inject + SecretID string `json:"secretId"` +} + +type SecretProxyRequest struct { + // The request body + Body *string `json:"body,omitempty"` + // The request headers (may reference the secret) + Headers map[string]string `json:"headers,omitempty"` + // The HTTP method + Method *string `json:"method,omitempty"` + // The destination URL + URL string `json:"url"` +} + type SecretRevokeParams struct { SecretID string `json:"secretId"` }