tailorclient package - github.com/k1LoW/tailor-client-go - Go Packages

tailorclient

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: MIT Imports: 18 Imported by: 0

README

[!IMPORTANT] This is an unofficial library.

tailor-client-go

Go Reference build Coverage Code to Test Ratio Test Execution Time

tailor-client-go is an unofficial Go client library for the Tailor Platform.

[!IMPORTANT] tailor-client-go implements no login flow of its own. It runs on the access tokens the official Tailor SDK already holds, so npx tailor-sdk login is a prerequisite, not a suggestion. For CI and other unattended callers, use WithClientCredentials instead. See Authentication model.

In short, tailorclient.New(ctx) returns the buf.build generated connect-go OperatorServiceClient already wired with bearer-token auth and auto-refresh.

Features

  • Piggybacks on Tailor SDK authentication. Tokens are sourced from ~/.config/tailor-platform/config.yaml (file and keyring storage both supported)
  • OAuth2 client_credentials grant for platform machine users via WithClientCredentials, for CI and other unattended callers
  • One-call constructor. tailorclient.New(ctx) returns a ready-to-use authenticated client
  • Automatic token refresh on Unauthenticated RPC errors
  • Optional SDK config writeback on token refresh (off by default) so the SDK and other tools see the new tokens
  • Token handling follows the SDK. The config user key is platform-scoped the way the SDK scopes it, the OAuth2 client_id defaults to the SDK's own and honors the same environment variables, and the platform endpoint is taken from the SDK config when you name none
  • Embeds tailorv1connect.OperatorServiceClient, so RPC methods are callable directly on the client

Install

$ go get github.com/k1LoW/tailor-client-go

Authentication model

tailor-client-go does not handle the OAuth2 login flow itself. New picks its credentials from one of three sources, in this order.

Source Option Use it for
OAuth2 client_credentials grant WithClientCredentials(clientID, clientSecret) CI, service accounts, any unattended caller. The SDK config is never touched
Caller-managed tokens WithTokens(access, refresh) You already hold tokens and want to manage their lifecycle yourself
Tailor SDK config (default) Local development, where a human has run npx tailor-sdk login
Machine user (CI)

Create a platform machine user, then pass its credentials. No SDK login and no SDK config are involved.

c, err := tailorclient.New(ctx,
	tailorclient.WithClientCredentials(os.Getenv("TAILOR_CLIENT_ID"), os.Getenv("TAILOR_CLIENT_SECRET")),
)

Machine user grants do not issue a refresh token, so on an Unauthenticated error the client re-fetches with the same credentials. WithClientCredentials cannot be combined with WithTokens or WithTokenPersist.

SDK config (local development)
  1. Log in once via the Tailor SDK.

    $ npx tailor-sdk login
    

    This writes the tokens for the current user to ~/.config/tailor-platform/config.yaml, or into the OS keyring when the user is configured for storage: keyring.

  2. Any Go program using tailor-client-go calls tailorclient.New(ctx) and the library transparently:

    • resolves the current_user's entry in the SDK config,
    • reads the tokens from the config file or the OS keyring,
    • refreshes the access token if token_expires_at is in the past,
    • retries once with a refreshed token if an RPC returns Unauthenticated.

Platform and client_id resolution

The SDK scopes each login to a platform, and tailor-client-go follows the same rules rather than assuming production.

Platform endpoint, highest precedence first:

  1. WithPlatformURL(url)
  2. TAILOR_PLATFORM_URL, then PLATFORM_URL
  3. The platform recorded on the SDK config user key
  4. tailorclient.DefaultPlatformURL (https://api.tailor.tech)

Step 3 is what keeps a dev login working. Since SDK config v3, a non-production login is stored under a platform-scoped key while current_user keeps the bare user ID:

version: 3
users:
  https://api.dev.tailor.tech|ac354dd0-...:   # dev, platform-scoped
    storage: keyring
    token_expires_at: '2026-09-02T05:23:39.185Z'
  98f96ebb-...:                               # production, bare key
    storage: keyring
current_user: ac354dd0-...

With no platform named, New resolves current_user to the dev entry above and talks to https://api.dev.tailor.tech, so the dev refresh token is never posted to production. If current_user is registered for several non-production platforms, the lookup is ambiguous and New asks you to pick one with WithPlatformURL.

OAuth2 client_id for the refresh_token grant, highest precedence first:

  1. WithOAuth2ClientID(id)
  2. TAILOR_PLATFORM_OAUTH2_CLIENT_ID, then PLATFORM_OAUTH2_CLIENT_ID
  3. tailorclient.DefaultOAuth2ClientID, which is the SDK's own public client ID

The client_id is deliberately not derived from the platform URL. The SDK ships one client ID for every platform and lets the environment override it, so a self-hosted platform is configured through these variables rather than through a table of known hosts.

Usage

package main

import (
	"context"
	"fmt"
	"log"

	tailorv1 "buf.build/gen/go/tailor-inc/tailor/protocolbuffers/go/tailor/v1"
	"connectrpc.com/connect"

	tailorclient "github.com/k1LoW/tailor-client-go"
)

func main() {
	ctx := context.Background()

	c, err := tailorclient.New(ctx)
	if err != nil {
		log.Fatal(err)
	}

	res, err := c.GetApplication(ctx, connect.NewRequest(&tailorv1.GetApplicationRequest{
		WorkspaceId:     "ws_xxx",
		ApplicationName: "my-app",
	}))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Msg.GetApplication().GetName())
}

Without any options, New reads the current user's tokens from the Tailor SDK config and infers the platform from the config user key. When token_expires_at indicates the access token is stale, it is refreshed proactively before the client is returned.

Explicit tokens

To bypass the SDK config and supply tokens directly:

c, err := tailorclient.New(ctx,
	tailorclient.WithTokens(accessToken, refreshToken),
	tailorclient.WithPlatformURL(tailorclient.DefaultPlatformURL),
)
Persisting refreshed tokens

By default, tokens refreshed during a session are kept in-memory only. Pass WithTokenPersist() to write them back to the SDK config (or keyring, if the user is configured for keyring storage) so other tools stay in sync. Only SDK-config-sourced tokens have an entry to write back to, so this cannot be combined with WithTokens or WithClientCredentials:

c, err := tailorclient.New(ctx, tailorclient.WithTokenPersist())
Available options
Option Description
WithClientCredentials(clientID, clientSecret string) Authenticate as a platform machine user via the OAuth2 client_credentials grant. Mutually exclusive with WithTokens and WithTokenPersist
WithTokens(access, refresh string) Use supplied tokens instead of reading the SDK config. Mutually exclusive with WithTokenPersist
WithPlatformURL(url string) Override the Tailor Platform endpoint. See Platform and client_id resolution for the full precedence
WithOAuth2ClientID(id string) Override the OAuth2 client_id used for the refresh_token grant
WithTokenPersist() Write refreshed tokens back to the SDK config (default: off). Requires SDK-config-sourced tokens, so it cannot be combined with WithTokens or WithClientCredentials
WithHTTPClient(h connect.HTTPClient) Override the underlying HTTP client
WithInterceptors(ics ...connect.Interceptor) Append additional connect interceptors

How it works

  1. Resolves the platform endpoint and the OAuth2 client_id as described in Platform and client_id resolution
  2. Resolves credentials from the machine user grant (WithClientCredentials), the explicit values (WithTokens), or the Tailor SDK config
  3. If an SDK config token is expired, refreshes it proactively against the OAuth2 token endpoint
  4. Builds a connect-go OperatorServiceClient wrapped with an interceptor that:
    • Attaches the Authorization: Bearer <token> header to every request
    • On an Unauthenticated error, obtains a new access token (via the refresh token, or by re-fetching with the machine user credentials) and retries once
  5. When WithTokenPersist() is set, refreshed tokens are written back to the SDK config entry they came from, or to the keyring

License

MIT License

Documentation

Overview

Package tailorclient is an UNOFFICIAL Go client library for the Tailor Platform that runs on the access tokens the official Tailor SDK already holds.

It implements no login flow of its own. Authentication piggybacks on the Tailor SDK (https://github.com/tailor-platform/sdk): the user logs in once with `npx tailor-sdk login`, and this package reuses the access and refresh tokens the SDK stores in ~/.config/tailor-platform/config.yaml, or in the OS keyring when the user is configured for keyring storage. Running that login is therefore a prerequisite, not a suggestion.

New picks its credentials from one of three sources.

  • WithClientCredentials: an OAuth2 client_credentials grant for a platform machine user. This is the option for CI and other unattended callers, and it never touches the SDK config.
  • WithTokens: tokens the caller manages itself.
  • Otherwise: the current user's tokens from the SDK config.

Whichever source is used, New returns a connect-go OperatorServiceClient wired with bearer-token authentication that refreshes on Unauthenticated errors. Refreshed tokens stay in memory unless WithTokenPersist writes them back to the SDK config.

Token handling follows the SDK rather than reimplementing it. The config user key is platform-scoped the way the SDK scopes it, the OAuth2 client_id defaults to the SDK's own and honors the same environment variables, and the platform endpoint is taken from the SDK config when the caller names none, so a dev or self-hosted login is never refreshed against production.

Example

Example builds an authenticated client from the SDK config and calls an OperatorService RPC. The RPC method is promoted from the embedded tailorv1connect.OperatorServiceClient, so it is callable directly on *Client.

package main

import (
	"context"
	"fmt"
	"log"

	tailorv1 "buf.build/gen/go/tailor-inc/tailor/protocolbuffers/go/tailor/v1"
	"connectrpc.com/connect"

	tailorclient "github.com/k1LoW/tailor-client-go"
)

func main() {
	ctx := context.Background()
	c, err := tailorclient.New(ctx)
	if err != nil {
		log.Fatal(err)
	}

	res, err := c.GetApplication(ctx, connect.NewRequest(&tailorv1.GetApplicationRequest{
		WorkspaceId:     "ws_xxx",
		ApplicationName: "my-app",
	}))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res.Msg.GetApplication().GetName())
}

Index

Examples

Constants

View Source
const DefaultOAuth2ClientID = "cpoc_0Iudir72fqSpqC6GQ58ri1cLAqcq5vJl" //nostyle:repetition

DefaultOAuth2ClientID is the public OAuth2 client_id the Tailor SDK uses for the refresh_token grant. The SDK ships a single client_id for every platform and lets the environment override it, so this package must not invent per-platform IDs of its own.

View Source
const DefaultPlatformURL = "https://api.tailor.tech"

DefaultPlatformURL is the production Tailor Platform endpoint.

View Source
const DefaultUploadChunkSize = 256 * 1024

DefaultUploadChunkSize is the default chunk size used by Client.UploadFileFromReader when UploadFileParams.ChunkSize is not set.

Variables

This section is empty.

Functions

func IsTokenExpired

func IsTokenExpired(expiresAt string) bool

IsTokenExpired checks if a token_expires_at string indicates an expired token.

func ResolveOAuth2ClientID added in v0.4.0

func ResolveOAuth2ClientID(oauth2ClientID string) string

ResolveOAuth2ClientID mirrors the SDK's getOAuth2ClientId: an explicit value wins, then TAILOR_PLATFORM_OAUTH2_CLIENT_ID, then PLATFORM_OAUTH2_CLIENT_ID, then the SDK default. The client_id is not derived from the platform URL, because a self-hosted platform is configured through the same environment variables rather than through a table of known hosts.

func ResolvePlatformURL added in v0.4.0

func ResolvePlatformURL(platformURL string) string

ResolvePlatformURL mirrors the SDK's getPlatformBaseUrl: an explicit value wins, then TAILOR_PLATFORM_URL, then PLATFORM_URL. It returns "" when none is set so callers can apply their own fallback.

func WriteSDKTokens

func WriteSDKTokens(userKey, accessToken, refreshToken, tokenExpiresAt string) error

WriteSDKTokens updates the tokens stored under userKey, which is the value SDKTokens.UserKey carries. Taking the resolved key rather than re-deriving one keeps a writeback on a legacy bare-key entry from silently creating a second, platform-scoped entry beside it.

Tokens go to the keyring or the config file depending on the entry's storage mode.

Types

type Client

type Client struct {
	tailorv1connect.OperatorServiceClient
	// contains filtered or unexported fields
}

Client is an authenticated Tailor Platform client.

It embeds OperatorServiceClient, so RPC methods can be called directly (e.g. c.GetApplication). The auto-refresh interceptor is wired into the embedded client at construction time.

func New

func New(ctx context.Context, opts ...Option) (*Client, error)

New builds an authenticated client.

Authentication source is determined by which options are supplied:

  • WithClientCredentials: fetch an access token via the OAuth2 client_credentials grant using a platform machine user.
  • WithTokens: use the supplied access/refresh tokens directly.
  • Otherwise: read the current user's tokens from the Tailor SDK config, and proactively refresh them if expired. The platform is taken from the config user key unless WithPlatformURL or the environment names one, so a dev or self-hosted login is not refreshed against production.

Token refresh on unauthenticated unary RPCs is always enabled (using the refresh_token for SDK-config / WithTokens flows, or re-fetching with the stored client_credentials for machine-user flows). SDK config writeback is opt-in via WithTokenPersist and applies only to tokens sourced from that config, since the other two flows have no config entry to write back to.

Example (ClientCredentials)

ExampleNew_clientCredentials authenticates as a platform machine user via the OAuth2 client_credentials grant. This is the option for CI and other unattended callers: it needs no `npx tailor-sdk login` and never touches the SDK config.

package main

import (
	"context"
	"log"
	"os"

	tailorclient "github.com/k1LoW/tailor-client-go"
)

func main() { //nostyle:repetition
	ctx := context.Background()
	_, err := tailorclient.New(ctx,
		tailorclient.WithClientCredentials(os.Getenv("TAILOR_CLIENT_ID"), os.Getenv("TAILOR_CLIENT_SECRET")),
	)
	if err != nil {
		log.Fatal(err)
	}
}
Example (ExplicitTokens)

ExampleNew_explicitTokens passes tokens explicitly instead of reading the SDK config.

package main

import (
	"context"
	"log"

	tailorclient "github.com/k1LoW/tailor-client-go"
)

func main() {
	ctx := context.Background()
	_, err := tailorclient.New(ctx,
		tailorclient.WithTokens("access-token", "refresh-token"),
		tailorclient.WithPlatformURL(tailorclient.DefaultPlatformURL),
	)
	if err != nil {
		log.Fatal(err)
	}
}
Example (PersistTokens)

ExampleNew_persistTokens enables SDK config writeback on token refresh. Disabled by default.

package main

import (
	"context"
	"log"

	tailorclient "github.com/k1LoW/tailor-client-go"
)

func main() {
	ctx := context.Background()
	_, err := tailorclient.New(ctx, tailorclient.WithTokenPersist())
	if err != nil {
		log.Fatal(err)
	}
}

func (*Client) HTTPClient

func (c *Client) HTTPClient() connect.HTTPClient

HTTPClient returns the underlying HTTP client.

func (*Client) PlatformURL

func (c *Client) PlatformURL() string

PlatformURL returns the configured Tailor Platform endpoint.

func (*Client) UploadFileFromReader added in v0.2.0

func (c *Client) UploadFileFromReader(ctx context.Context, params UploadFileParams, r io.Reader) error

UploadFileFromReader streams r to the Tailor Platform as a single file. It sends one InitialMetadata message followed by ChunkData messages until r returns io.EOF, then closes the stream.

This wraps the generated streaming UploadFile RPC so callers do not have to manage the metadata/chunk oneof, the chunk loop, or stream close themselves. The raw streaming RPC is still reachable as c.OperatorServiceClient.UploadFile.

type Option

type Option func(*options)

Option configures New.

func WithClientCredentials added in v0.3.0

func WithClientCredentials(clientID, clientSecret string) Option

WithClientCredentials configures OAuth2 client_credentials grant authentication using a platform machine user. New() will fetch an access token at construction time, and the interceptor re-fetches with the same credentials whenever the token is rejected as Unauthenticated (machine user grants do not issue refresh tokens, so the helper holds on to the client_id/client_secret rather than a refresh_token).

Mutually exclusive with WithTokens and WithTokenPersist.

func WithHTTPClient

func WithHTTPClient(h connect.HTTPClient) Option

WithHTTPClient overrides the underlying HTTP client.

func WithInterceptors

func WithInterceptors(ics ...connect.Interceptor) Option

WithInterceptors appends additional connect interceptors. The auto-refresh interceptor is always installed first.

func WithOAuth2ClientID added in v0.4.0

func WithOAuth2ClientID(id string) Option

WithOAuth2ClientID overrides the OAuth2 client_id used for the refresh_token grant. Defaults to TAILOR_PLATFORM_OAUTH2_CLIENT_ID, PLATFORM_OAUTH2_CLIENT_ID, then DefaultOAuth2ClientID.

func WithPlatformURL

func WithPlatformURL(u string) Option

WithPlatformURL overrides the Tailor Platform endpoint. Without it the endpoint comes from TAILOR_PLATFORM_URL, PLATFORM_URL, the platform recorded on the SDK config user key, and finally DefaultPlatformURL.

func WithTokenPersist

func WithTokenPersist() Option

WithTokenPersist enables writing refreshed tokens back to the SDK config. Disabled by default.

Only tokens sourced from the SDK config can be written back, so this is mutually exclusive with WithTokens and WithClientCredentials.

func WithTokens

func WithTokens(accessToken, refreshToken string) Option

WithTokens uses the supplied tokens instead of reading the SDK config. Mutually exclusive with WithTokenPersist, which has no config entry to write back to in this flow.

type SDKConfig

type SDKConfig struct {
	Version             int                       `yaml:"version"`
	MinSDKVersion       string                    `yaml:"min_sdk_version,omitempty"`
	LatestVersion       *int                      `yaml:"latest_version,omitempty"`
	LatestMinSDKVersion string                    `yaml:"latest_min_sdk_version,omitempty"`
	Users               map[string]*SDKUserTokens `yaml:"users"`
	Profiles            yaml.MapSlice             `yaml:"profiles,omitempty"`
	CurrentUser         *string                   `yaml:"current_user"`
}

SDKConfig represents the Tailor SDK config.yaml (v1, v2 and v3 formats).

type SDKTokens added in v0.4.0

type SDKTokens struct {
	AccessToken    string
	RefreshToken   string
	TokenExpiresAt string
	// PlatformURL is the platform the tokens belong to, either the one the
	// caller asked for or the one recovered from the config user key.
	PlatformURL string
	// UserKey is the key the entry lives under in the config users map. It is
	// also the OS keyring account name, and the handle WriteSDKTokens takes.
	UserKey string
}

SDKTokens holds the credentials resolved from the Tailor SDK config for one user, together with the platform they were issued for.

func ReadSDKTokens

func ReadSDKTokens(platformURL string) (*SDKTokens, error)

ReadSDKTokens reads the current_user's credentials from the SDK config.

platformURL selects which of the current_user's platform entries to read; pass "" to let the platform be inferred from the config. Both file-based (v1) and keyring-based (v2, v3) storage are supported.

type SDKUserTokens

type SDKUserTokens struct {
	AccessToken    string  `yaml:"access_token,omitempty"`
	RefreshToken   string  `yaml:"refresh_token,omitempty"`
	TokenExpiresAt string  `yaml:"token_expires_at"`
	Storage        *string `yaml:"storage,omitempty"`
	// Email is carried so that a writeback does not strip the field the v3
	// SDK relies on to match a user across platforms.
	Email string `yaml:"email,omitempty"`
}

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	ExpiresIn    int    `json:"expires_in"`
	Error        string `json:"error,omitempty"`
}

TokenResponse is the response from the OAuth2 token endpoint.

func FetchClientCredentialsToken added in v0.3.0

func FetchClientCredentialsToken(ctx context.Context, httpClient connect.HTTPClient, platformURL, clientID, clientSecret string) (*TokenResponse, error)

FetchClientCredentialsToken obtains an access token via the OAuth2 client_credentials grant using a platform machine user's clientID and clientSecret.

ctx is propagated to the underlying HTTP request so callers can apply deadlines and cancellation to the token round-trip. httpClient overrides the HTTP transport (e.g. for custom CAs or proxies on self-hosted platforms); pass nil to use http.DefaultClient.

Unlike RefreshAccessToken, the response does not carry a refresh_token — machine user tokens are short-lived and re-fetched with the same credentials when they expire or are rejected.

func RefreshAccessToken

func RefreshAccessToken(ctx context.Context, httpClient connect.HTTPClient, platformURL, oauth2ClientID, refreshToken string) (*TokenResponse, error)

RefreshAccessToken exchanges a refresh_token for a new access_token.

oauth2ClientID selects the OAuth2 client the grant is made against; pass "" to fall back to the environment and then to DefaultOAuth2ClientID, the same way the SDK resolves it.

ctx is propagated to the underlying HTTP request so callers can apply deadlines and cancellation to the token round-trip. httpClient overrides the HTTP transport (e.g. for custom CAs or proxies on self-hosted platforms); pass nil to use http.DefaultClient.

type UploadFileParams added in v0.2.0

type UploadFileParams struct {
	WorkspaceID  string
	DeploymentID string
	FilePath     string
	ContentType  string
	// ChunkSize controls the size in bytes of each ChunkData message. When
	// zero or negative, DefaultUploadChunkSize is used.
	ChunkSize int
}

UploadFileParams configures Client.UploadFileFromReader.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL