console

package
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: Jul 29, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package console talks to the Tabstack auth and management host (console.tabstack.ai): the OAuth 2.1 endpoints under /oauth/* and the management API under /cli/*.

It is kept separate from internal/client on purpose. This package sends the user-scoped session token and never sees an org-scoped API key; internal/client sends the API key and never sees the session. Splitting them by host makes it impossible to send the wrong credential to the wrong place.

Index

Constants

View Source
const (
	ErrCodeInvalidGrant       = "invalid_grant"
	ErrCodeInvalidRequest     = "invalid_request"
	ErrCodeInvalidClient      = "invalid_client"
	ErrCodeUnauthorizedClient = "unauthorized_client"
	ErrCodeUnsupportedGrant   = "unsupported_grant_type"
	ErrCodeInvalidScope       = "invalid_scope"
	ErrCodeSessionExpired     = "session_expired"
	ErrCodeInvalidSession     = "invalid_session"
)

Known token endpoint error codes.

View Source
const ClientID = "tabstack-cli"

ClientID identifies the CLI to the authorization server. It is a public client: there is no secret, which is exactly why PKCE is mandatory.

View Source
const CodeChallengeMethodS256 = "S256"

CodeChallengeMethodS256 is the only challenge method we emit or accept. The `plain` method is not implemented anywhere in this package on purpose.

View Source
const DefaultScopes = "cli offline_access"

DefaultScopes is the scope set requested at authorize time.

Provisional value, pending product confirmation of the scope names the console actually registers for this client. It is overridable at runtime with TABSTACK_OAUTH_SCOPES so a wrong default here is a one-variable fix rather than a rebuild, and changing the default is a one-line change.

View Source
const RevealEnabled = true

RevealEnabled gates the API key reveal endpoint and the "use existing key" login option. The auth contract includes both, so it is on; the constant is kept as the single switch that would disable the whole reveal path cleanly if the product ever pulls it. When on, "use existing" is still only offered when the org actually has a key to adopt (see runKeySetup).

Variables

View Source
var ErrInvalidSession = errors.New("session is no longer valid, run: tabstack auth login")

ErrInvalidSession means the server rejected the session as unknown, revoked, or wrong-audience (401 invalid_session). Unlike an aged-out access token a refresh cannot fix this, so we surface it without refreshing.

View Source
var ErrNoSession = errors.New("not signed in, run: tabstack auth login")

ErrNoSession means no session is stored at all.

View Source
var ErrSessionExpired = errors.New("session expired, run: tabstack auth login")

ErrSessionExpired means the session could not be made to work: it was rejected twice, or there is no refresh token left to try. The message carries the fix because this is the one auth error users hit routinely.

Functions

func Challenge

func Challenge(verifier string) string

Challenge derives the S256 code challenge for a verifier: base64url, no padding, of the SHA-256 of the verifier's ASCII bytes.

func IsInvalidGrant

func IsInvalidGrant(err error) bool

IsInvalidGrant reports whether err is an invalid_grant from the token endpoint, which means the code or refresh token is spent, revoked, or wrong. It is unrecoverable without a fresh login.

func NewState

func NewState() (string, error)

NewState returns a fresh OAuth state value, generated the same way as the verifier.

func NewVerifier

func NewVerifier() (string, error)

NewVerifier returns a fresh PKCE code verifier: 32 crypto/rand bytes, base64url encoded without padding.

func Scopes

func Scopes() string

Scopes returns the scope string to request.

Types

type APIError

type APIError struct {
	StatusCode int
	Code       string
	Message    string
}

APIError is a decoded non-2xx management response. The console returns {"error": "..."} bodies, matching the product API.

func (*APIError) Error

func (e *APIError) Error() string

type APIKey

type APIKey struct {
	ID             string     `json:"id"`
	Name           string     `json:"name"`
	OrganizationID string     `json:"organization_id"`
	APIKey         string     `json:"api_key"`
	Preview        string     `json:"preview"`
	LastUsedAt     *time.Time `json:"last_used_at"`
}

APIKey is an org-scoped product credential. APIKey (the plaintext) is only populated on create and reveal; list responses carry Preview instead.

type AuthorizeParams

type AuthorizeParams struct {
	RedirectURI string
	Challenge   string
	State       string
	Scope       string
	// OrgID optionally preselects an organisation on the consent screen. Servers
	// that do not support it ignore the parameter.
	OrgID string
}

AuthorizeParams are the per-login inputs to the authorize URL.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the auth-host client. The zero session manager is valid: the OAuth endpoints (authorize URL, code exchange) need no session, which is what lets `auth login` run before one exists.

func New

func New(authURL string, opts ...Option) *Client

New constructs a Client against an auth host root (no trailing slash).

func (*Client) AttachSession

func (c *Client) AttachSession(store config.CredentialStore, cfg *config.Config) *SessionManager

AttachSession gives the client a session to authenticate management calls with, wiring its own Refresh method in as the refresh transport. It returns the manager so callers can force a refresh or clear the session.

func (*Client) AuthURL

func (c *Client) AuthURL() string

AuthURL returns the configured auth host root.

func (*Client) AuthorizeURL

func (c *Client) AuthorizeURL(p AuthorizeParams) string

AuthorizeURL builds the browser URL that starts the login.

resource carries the auth host, as the authorize contract requires. It is the only configured value that appears in a query string here besides organization_id, and it is not a secret.

func (*Client) CreateAPIKey

func (c *Client) CreateAPIKey(ctx context.Context, orgID, name string) (*APIKey, error)

CreateAPIKey mints a new API key for an organisation. The plaintext comes back exactly once, in this response.

func (*Client) ExchangeCode

func (c *Client) ExchangeCode(ctx context.Context, code, verifier, redirectURI string) (*TokenResponse, error)

ExchangeCode swaps an authorization code for a session. The token endpoint is form encoded, not JSON.

func (*Client) ListAPIKeys

func (c *Client) ListAPIKeys(ctx context.Context, orgID string) ([]APIKey, error)

ListAPIKeys lists an organisation's keys. Previews only, never plaintext.

func (*Client) Logout

func (c *Client) Logout(ctx context.Context) error

Logout revokes the current session only.

func (*Client) Me

func (c *Client) Me(ctx context.Context) (*Me, error)

Me fetches the signed-in user, their session expiry, and their orgs.

func (*Client) Organizations

func (c *Client) Organizations(ctx context.Context) ([]Org, error)

Organizations lists the organisations the user belongs to.

func (*Client) Refresh

func (c *Client) Refresh(ctx context.Context, refreshToken string) (*TokenResponse, error)

Refresh exchanges a refresh token for a new session. The response's refresh_token is a rotation: callers must store it and discard the old value, which the server is assumed to have invalidated.

func (*Client) RevealAPIKey

func (c *Client) RevealAPIKey(ctx context.Context, keyID string) (*APIKey, error)

RevealAPIKey fetches the plaintext of an existing key.

Gated by RevealEnabled: the method stays wired so enabling the feature is a one-constant change, but while the constant is false no command surfaces it and calling it returns an error rather than hitting the endpoint.

func (*Client) RevokeAPIKey

func (c *Client) RevokeAPIKey(ctx context.Context, keyID string) error

RevokeAPIKey revokes a key server-side.

func (*Client) RevokeAllSessions

func (c *Client) RevokeAllSessions(ctx context.Context) error

RevokeAllSessions revokes every session the user has, including this one.

func (*Client) RevokeSession

func (c *Client) RevokeSession(ctx context.Context, id string) error

RevokeSession revokes one session by id.

func (*Client) Session

func (c *Client) Session() *SessionManager

Session returns the attached session manager, or nil.

func (*Client) Sessions

func (c *Client) Sessions(ctx context.Context) ([]SessionInfo, error)

Sessions lists the user's CLI sessions, with the current one marked.

type Me

type Me struct {
	User struct {
		Email string `json:"email"`
	} `json:"user"`
	Session struct {
		ExpiresAt time.Time `json:"expires_at"`
	} `json:"session"`
	DefaultOrg    string `json:"default_org"`
	Organizations []Org  `json:"organizations"`
}

Me is the whoami payload: who the session belongs to, when it expires, and which organisations it can act for.

type OAuthError

type OAuthError struct {
	StatusCode  int
	Code        string
	Description string
}

OAuthError is an RFC 6749 error response from the token endpoint.

func (*OAuthError) Error

func (e *OAuthError) Error() string

type Option

type Option func(*Client)

Option configures a Client.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient swaps the underlying http.Client. Mostly useful for tests.

type Org

type Org struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	Role string `json:"role"`
}

Org is one organisation the signed-in user belongs to. The id is the stable identity; the name is display only and can change.

type RefreshFunc

type RefreshFunc func(ctx context.Context, refreshToken string) (*TokenResponse, error)

RefreshFunc exchanges a refresh token for a new session. Injected rather than called directly so the manager is testable without an HTTP server.

type SessionInfo

type SessionInfo struct {
	ID         string     `json:"id"`
	Label      string     `json:"label"`
	LastUsedAt *time.Time `json:"last_used_at"`
	CreatedAt  *time.Time `json:"created_at"`
	ExpiresAt  *time.Time `json:"expires_at"`
	Current    bool       `json:"current"`
}

SessionInfo is one CLI session belonging to the user.

type SessionManager

type SessionManager struct {
	// contains filtered or unexported fields
}

SessionManager hands out a valid access token, refreshing when it has expired, and persists the rotated refresh token.

Refresh is single-flight: concurrent callers that arrive during a refresh wait on the one in progress instead of each firing their own. That matters because the server rotates the refresh token, so two simultaneous refreshes would leave one of them holding a value the server has already invalidated.

func NewSessionManager

func NewSessionManager(refresh RefreshFunc, store config.CredentialStore, cfg *config.Config) *SessionManager

NewSessionManager builds a manager over an already-loaded config. Mutations are written back through store.

func (*SessionManager) Clear

func (m *SessionManager) Clear() error

Clear drops the session and refresh token and persists the result. API keys are deliberately left alone: signing out is not the same as revoking credentials.

func (*SessionManager) Config

func (m *SessionManager) Config() *config.Config

Config returns the config the manager mutates.

func (*SessionManager) Establish

func (m *SessionManager) Establish(tok *TokenResponse, email string) error

Establish records a brand new session from a token response and persists it. Used by login, where there is nothing to rotate.

func (*SessionManager) ForceRefresh

func (m *SessionManager) ForceRefresh(ctx context.Context) (string, error)

ForceRefresh refreshes regardless of the recorded expiry. It is what a 401 triggers: the server has told us the token is no good, whatever we think its lifetime was.

func (*SessionManager) SetEmail

func (m *SessionManager) SetEmail(email string)

SetEmail records the signed-in user's email on the session without touching the tokens.

func (*SessionManager) Token

func (m *SessionManager) Token(ctx context.Context) (string, error)

Token returns a usable access token, refreshing first if the stored one has expired.

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token"`
	Scope        string `json:"scope"`
}

TokenResponse is a successful /oauth/token response, for both the authorization_code and refresh_token grants.

Jump to

Keyboard shortcuts

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