- Mark Phase 4 and Phase 5 as complete, indicating successful removal of fixed upstream connections and the introduction of dynamic routing based on request metadata. - Implement `TokenManager` for handling authentication tokens and update gRPC client methods to support new features. - Enhance tests for dynamic routing and token management, achieving significant coverage improvements. - Begin preparations for Phase 6, outlining the structure for OAuth Device Flow and related tasks. This commit finalizes key gRPC features and sets the stage for upcoming authentication enhancements.
147 lines
4.1 KiB
Go
147 lines
4.1 KiB
Go
package grpc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/metadata"
|
|
)
|
|
|
|
// TokenManager reads auth credentials from environment variables and attaches
|
|
// them as gRPC metadata on every call. It also handles automatic token refresh
|
|
// by reading new tokens from response headers.
|
|
type TokenManager struct {
|
|
mu sync.RWMutex
|
|
accessToken string
|
|
refreshToken string
|
|
sandboxID string
|
|
upstream string // only set when YAO_GRPC_TAI=enable
|
|
taiMode bool
|
|
}
|
|
|
|
// NewTokenManagerFromEnv creates a TokenManager from environment variables.
|
|
// Returns an error if required variables are missing.
|
|
func NewTokenManagerFromEnv() (*TokenManager, error) {
|
|
tm := &TokenManager{
|
|
accessToken: os.Getenv("YAO_TOKEN"),
|
|
refreshToken: os.Getenv("YAO_REFRESH_TOKEN"),
|
|
sandboxID: os.Getenv("YAO_SANDBOX_ID"),
|
|
}
|
|
|
|
if os.Getenv("YAO_GRPC_TAI") == "enable" {
|
|
tm.taiMode = true
|
|
tm.upstream = os.Getenv("YAO_GRPC_UPSTREAM")
|
|
if tm.upstream == "" {
|
|
return nil, fmt.Errorf("YAO_GRPC_TAI=enable but YAO_GRPC_UPSTREAM is not set")
|
|
}
|
|
}
|
|
|
|
return tm, nil
|
|
}
|
|
|
|
// NewTokenManager creates a TokenManager with explicit values (for testing).
|
|
func NewTokenManager(accessToken, refreshToken, sandboxID, upstream string) *TokenManager {
|
|
return &TokenManager{
|
|
accessToken: accessToken,
|
|
refreshToken: refreshToken,
|
|
sandboxID: sandboxID,
|
|
upstream: upstream,
|
|
taiMode: upstream != "",
|
|
}
|
|
}
|
|
|
|
// AttachMetadata returns a context with auth credentials in gRPC metadata.
|
|
func (tm *TokenManager) AttachMetadata(ctx context.Context) context.Context {
|
|
tm.mu.RLock()
|
|
defer tm.mu.RUnlock()
|
|
|
|
pairs := []string{}
|
|
if tm.accessToken != "" {
|
|
pairs = append(pairs, "authorization", "Bearer "+tm.accessToken)
|
|
}
|
|
if tm.refreshToken != "" {
|
|
pairs = append(pairs, "x-refresh-token", tm.refreshToken)
|
|
}
|
|
if tm.sandboxID != "" {
|
|
pairs = append(pairs, "x-sandbox-id", tm.sandboxID)
|
|
}
|
|
if tm.taiMode && tm.upstream != "" {
|
|
pairs = append(pairs, "x-grpc-upstream", tm.upstream)
|
|
}
|
|
|
|
if len(pairs) == 0 {
|
|
return ctx
|
|
}
|
|
return metadata.AppendToOutgoingContext(ctx, pairs...)
|
|
}
|
|
|
|
// HandleResponseHeaders reads new tokens from response headers and updates
|
|
// the in-memory credentials. Call after each gRPC response.
|
|
func (tm *TokenManager) HandleResponseHeaders(header metadata.MD) {
|
|
if header == nil {
|
|
return
|
|
}
|
|
|
|
tm.mu.Lock()
|
|
defer tm.mu.Unlock()
|
|
|
|
if vals := header.Get("x-access-token"); len(vals) > 0 && vals[0] != "" {
|
|
tm.accessToken = vals[0]
|
|
}
|
|
if vals := header.Get("x-refresh-token"); len(vals) > 0 && vals[0] != "" {
|
|
tm.refreshToken = vals[0]
|
|
}
|
|
}
|
|
|
|
// UnaryInterceptor returns a gRPC unary client interceptor that attaches
|
|
// auth metadata and handles token refresh from response headers.
|
|
func (tm *TokenManager) UnaryInterceptor() grpc.UnaryClientInterceptor {
|
|
return func(ctx context.Context, method string, req, reply any,
|
|
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
|
|
|
ctx = tm.AttachMetadata(ctx)
|
|
|
|
var header metadata.MD
|
|
opts = append(opts, grpc.Header(&header))
|
|
|
|
err := invoker(ctx, method, req, reply, cc, opts...)
|
|
tm.HandleResponseHeaders(header)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// StreamInterceptor returns a gRPC stream client interceptor that attaches
|
|
// auth metadata. Token refresh from stream headers is handled by the caller
|
|
// via stream.Header().
|
|
func (tm *TokenManager) StreamInterceptor() grpc.StreamClientInterceptor {
|
|
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn,
|
|
method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
|
|
|
ctx = tm.AttachMetadata(ctx)
|
|
stream, err := streamer(ctx, desc, cc, method, opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if header, hErr := stream.Header(); hErr == nil {
|
|
tm.HandleResponseHeaders(header)
|
|
}
|
|
|
|
return stream, nil
|
|
}
|
|
}
|
|
|
|
// AccessToken returns the current access token (for testing/debugging).
|
|
func (tm *TokenManager) AccessToken() string {
|
|
tm.mu.RLock()
|
|
defer tm.mu.RUnlock()
|
|
return tm.accessToken
|
|
}
|
|
|
|
// IsTaiMode returns whether the client is configured for Tai relay mode.
|
|
func (tm *TokenManager) IsTaiMode() bool {
|
|
return tm.taiMode
|
|
}
|