feat: Phase 2 Workstream 3 — Go SDK (github.com/sentryagent/idp-sdk-go)
Single-package agentidp SDK in sdk-go/: - AgentIdPClient composing AgentRegistryClient, CredentialClient, TokenServiceClient, AuditClient — all 14 endpoints covered - Goroutine-safe TokenManager (sync.Mutex) with 60s refresh buffer - AgentIdPError implementing error interface with Code/HTTPStatus/Details - Context-aware: all service methods take context.Context as first arg - doRequest shared helper; token endpoints use form-encoded POST directly - go vet: 0 warnings | staticcheck: 0 warnings - go test ./...: 37/37 passed | coverage: 81.0% (>80% gate) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
103
sdk-go/token_service.go
Normal file
103
sdk-go/token_service.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package agentidp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TokenServiceClient provides token introspection and revocation.
|
||||
// Token acquisition is handled separately by TokenManager.
|
||||
type TokenServiceClient struct {
|
||||
baseURL string
|
||||
getToken func(ctx context.Context) (string, error)
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func newTokenServiceClient(baseURL string, getToken func(ctx context.Context) (string, error), httpClient *http.Client) *TokenServiceClient {
|
||||
return &TokenServiceClient{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
getToken: getToken,
|
||||
httpClient: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
// IntrospectToken introspects an access token per RFC 7662.
|
||||
// POST /api/v1/token/introspect (form-encoded) → 200 IntrospectResponse
|
||||
func (c *TokenServiceClient) IntrospectToken(ctx context.Context, accessToken string) (*IntrospectResponse, error) {
|
||||
bearerToken, err := c.getToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("token", accessToken)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/v1/token/introspect", bytes.NewBufferString(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, &AgentIdPError{Code: "REQUEST_BUILD_ERROR", Message: "failed to build introspect request: " + err.Error()}
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+bearerToken)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, newNetworkError(err)
|
||||
}
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, newNetworkError(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, parseAPIError(respBody, resp.StatusCode)
|
||||
}
|
||||
|
||||
var result IntrospectResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, &AgentIdPError{Code: "PARSE_ERROR", Message: "failed to parse introspect response: " + err.Error(), HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// RevokeToken revokes an access token.
|
||||
// POST /api/v1/token/revoke (form-encoded) → 200
|
||||
func (c *TokenServiceClient) RevokeToken(ctx context.Context, accessToken string) error {
|
||||
bearerToken, err := c.getToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("token", accessToken)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/v1/token/revoke", bytes.NewBufferString(form.Encode()))
|
||||
if err != nil {
|
||||
return &AgentIdPError{Code: "REQUEST_BUILD_ERROR", Message: "failed to build revoke request: " + err.Error()}
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Authorization", "Bearer "+bearerToken)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return newNetworkError(err)
|
||||
}
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return newNetworkError(err)
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return parseAPIError(respBody, resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user