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
- Variables
- func Challenge(verifier string) string
- func IsInvalidGrant(err error) bool
- func NewState() (string, error)
- func NewVerifier() (string, error)
- func Scopes() string
- type APIError
- type APIKey
- type AuthorizeParams
- type Client
- func (c *Client) AttachSession(store config.CredentialStore, cfg *config.Config) *SessionManager
- func (c *Client) AuthURL() string
- func (c *Client) AuthorizeURL(p AuthorizeParams) string
- func (c *Client) CreateAPIKey(ctx context.Context, orgID, name string) (*APIKey, error)
- func (c *Client) ExchangeCode(ctx context.Context, code, verifier, redirectURI string) (*TokenResponse, error)
- func (c *Client) ListAPIKeys(ctx context.Context, orgID string) ([]APIKey, error)
- func (c *Client) Logout(ctx context.Context) error
- func (c *Client) Me(ctx context.Context) (*Me, error)
- func (c *Client) Organizations(ctx context.Context) ([]Org, error)
- func (c *Client) Refresh(ctx context.Context, refreshToken string) (*TokenResponse, error)
- func (c *Client) RevealAPIKey(ctx context.Context, keyID string) (*APIKey, error)
- func (c *Client) RevokeAPIKey(ctx context.Context, keyID string) error
- func (c *Client) RevokeAllSessions(ctx context.Context) error
- func (c *Client) RevokeSession(ctx context.Context, id string) error
- func (c *Client) Session() *SessionManager
- func (c *Client) Sessions(ctx context.Context) ([]SessionInfo, error)
- type Me
- type OAuthError
- type Option
- type Org
- type RefreshFunc
- type SessionInfo
- type SessionManager
- func (m *SessionManager) Clear() error
- func (m *SessionManager) Config() *config.Config
- func (m *SessionManager) Establish(tok *TokenResponse, email string) error
- func (m *SessionManager) ForceRefresh(ctx context.Context) (string, error)
- func (m *SessionManager) SetEmail(email string)
- func (m *SessionManager) Token(ctx context.Context) (string, error)
- type TokenResponse
Constants ¶
const ( ErrCodeInvalidGrant = "invalid_grant" ErrCodeInvalidRequest = "invalid_request" ErrCodeInvalidClient = "invalid_client" ErrCodeUnsupportedGrant = "unsupported_grant_type" ErrCodeInvalidScope = "invalid_scope" ErrCodeSessionExpired = "session_expired" ErrCodeInvalidSession = "invalid_session" )
Known token endpoint error codes.
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.
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.
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.
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 ¶
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.
var ErrNoSession = errors.New("not signed in, run: tabstack auth login")
ErrNoSession means no session is stored at all.
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 ¶
Challenge derives the S256 code challenge for a verifier: base64url, no padding, of the SHA-256 of the verifier's ASCII bytes.
func IsInvalidGrant ¶
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 NewVerifier ¶
NewVerifier returns a fresh PKCE code verifier: 32 crypto/rand bytes, base64url encoded without padding.
Types ¶
type APIError ¶
APIError is a decoded non-2xx management response. The console returns {"error": "..."} bodies, matching the product API.
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 (*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) 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 ¶
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 ¶
ListAPIKeys lists an organisation's keys. Previews only, never plaintext.
func (*Client) Organizations ¶
Organizations lists the organisations the user belongs to.
func (*Client) Refresh ¶
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 ¶
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 ¶
RevokeAPIKey revokes a key server-side.
func (*Client) RevokeAllSessions ¶
RevokeAllSessions revokes every session the user has, including this one.
func (*Client) RevokeSession ¶
RevokeSession revokes one session by id.
func (*Client) Session ¶
func (c *Client) Session() *SessionManager
Session returns the attached session manager, or nil.
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 ¶
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 ¶
WithHTTPClient swaps the underlying http.Client. Mostly useful for tests.
type Org ¶
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.
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.