cliauth

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package cliauth stores Oura credentials out-of-band from agent context.

Primary store is the OS keyring (macOS Keychain, Windows Credential Manager, libsecret/Secret Service on Linux) via zalando/go-keyring. Where no keyring is available (headless Linux, CI), it falls back to an AES-256-GCM encrypted file under the user config dir, keyed by a random key stored alongside with 0600 permissions — obfuscation-at-rest, clearly weaker than a real keyring, and reported as such by `oura doctor`.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("no stored credentials")

ErrNotFound is returned when no credentials are stored.

Functions

func ConfigDir

func ConfigDir() string

ConfigDir returns the ouracli config directory (~/.config/ouracli or the platform equivalent), honoring OURA_CONFIG_DIR for tests and containers.

func ResolveToken

func ResolveToken() (token, source string)

ResolveToken returns the access token to use, and where it came from. Precedence: OURA_TOKEN env > stored credentials. Returns source "env", "keyring", "encrypted-file", or "" when nothing is available. OAuth refresh, when needed, is handled by the caller via Load().

func Revoke

func Revoke(ctx context.Context, token string) error

Revoke asks Oura to invalidate a token. The token is passed as a query parameter, matching Oura's revoke endpoint.

Types

type AuthorizeRejectedError

type AuthorizeRejectedError struct {
	RedirectURI string
	ClientID    string
	Status      int
}

AuthorizeRejectedError reports that Oura's authorize endpoint rejected the request outright (HTTP 400) before any user interaction — in practice this means the redirect URI is not registered on the OAuth app. Detected by the preflight probe so callers can fail fast with exact remediation instead of waiting for a browser callback that will never arrive.

func (*AuthorizeRejectedError) Error

func (e *AuthorizeRejectedError) Error() string

type Credentials

type Credentials struct {
	Method       Method    `json:"method"`
	AccessToken  string    `json:"access_token"`
	RefreshToken string    `json:"refresh_token,omitempty"`
	Expiry       time.Time `json:"expiry,omitempty"`
	Scopes       []string  `json:"scopes,omitempty"`
	ClientID     string    `json:"client_id,omitempty"`
	ClientSecret string    `json:"client_secret,omitempty"`
	SavedAt      time.Time `json:"saved_at"`
}

Credentials is the JSON blob stored in the keyring.

func LoginOAuth

func LoginOAuth(ctx context.Context, clientID, clientSecret string, scopes []string, port int, openBrowser func(url string) error) (Credentials, error)

LoginOAuth runs the loopback authorization-code flow. It binds a tiny HTTP server on 127.0.0.1:port, opens the browser to Oura's consent screen (and always prints the URL to stderr so headless users can paste it), waits for the redirect back to /callback, validates the state parameter, and exchanges the returned code for tokens. The granted scope set (which may be a subset of what was requested, since users can untick scopes) and the derived expiry are recorded on the returned Credentials. Saving is left to the caller.

func Refresh

func Refresh(ctx context.Context, store Store, c Credentials) (Credentials, error)

Refresh exchanges the stored refresh token for a new access token. Oura's refresh tokens are single-use and rotate on every exchange, so the rotated credentials are saved to the store BEFORE this returns — a caller that used the new access token without persisting the new refresh token would strand the account on the next refresh.

type Method

type Method string

Method distinguishes how the stored token was obtained.

const (
	MethodPAT   Method = "pat"
	MethodOAuth Method = "oauth"
)

type Store

type Store interface {
	Save(Credentials) error
	Load() (Credentials, error)
	Delete() error
	Backend() string // "keyring" or "encrypted-file"
}

Store abstracts the two backends so doctor can report which is active.

func Open

func Open(configDir string) Store

Open returns the keyring store if the platform keyring works, otherwise the encrypted-file fallback. Setting OURA_KEYRING_BACKEND=file forces the encrypted-file backend even where a keyring exists — useful for CI, headless runs, and tests that must not touch (or prompt) the OS keyring.

Jump to

Keyboard shortcuts

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