clientconfig

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package clientconfig manages the local state of the Backyard CLI and MCP server (SPEC §13): the per-user config file with named profiles ($XDG_CONFIG_HOME/backyard/config.json, mode 0600), environment overrides, the per-project .backyard.json file, gateway URL resolution, token expiry awareness and the OAuth device-flow helpers used to obtain and refresh IdP-issued tokens.

The package performs no terminal I/O; callers decide how to prompt.

Index

Constants

View Source
const (
	EnvServer       = "BACKYARD_SERVER"
	EnvGateway      = "BACKYARD_GATEWAY"
	EnvToken        = "BACKYARD_TOKEN"
	EnvProfile      = "BACKYARD_PROFILE"
	EnvDeployMethod = "BACKYARD_DEPLOY_METHOD"
)

Environment variables that override profile settings.

View Source
const (
	LoginToken     = "token"
	LoginBrowser   = "browser"
	LoginDevice    = "device"
	LoginAnonymous = "anonymous"
)

Login methods recorded in Profile.Login.

View Source
const (
	MethodAuto  = "auto"
	MethodAPI   = "api"
	MethodS3    = "s3"
	MethodGCS   = "gcs"
	MethodAzure = "azure"
	MethodRsync = "rsync"
)

Deploy methods (SPEC §13).

View Source
const DefaultProfile = "default"

DefaultProfile is the profile used when none is selected.

View Source
const ExpiryWarning = 24 * time.Hour

ExpiryWarning is how long before a (non-refreshable) stored token expires that sessions start warning about it.

View Source
const GatewayPath = deploy.EmbeddedPath

GatewayPath is where the server embeds the deploy gateway.

View Source
const ProjectFileName = ".backyard.json"

ProjectFileName is the per-project settings file.

View Source
const (

	// RefreshSkew is how long before expiry a refreshable token is renewed.
	RefreshSkew = 2 * time.Minute
)

Variables

View Source
var ErrDeviceDenied = errors.New("the device login was denied")

ErrDeviceDenied is returned when the user denied the device login.

View Source
var ErrDeviceExpired = errors.New("the device code expired before the login was approved")

ErrDeviceExpired is returned when the user did not approve the device code in time.

View Source
var ErrNoServer = errors.New("no Backyard server configured: run `backyard login --server https://backyard.example.com` (or set BACKYARD_SERVER)")

ErrNoServer is returned when no server or gateway is configured.

Methods lists every valid deploy method, "auto" first.

Functions

func DefaultHTTPClient

func DefaultHTTPClient() *http.Client

DefaultHTTPClient returns the HTTP client used for IdP calls when none is supplied: 30 s overall timeout.

func DefaultPath

func DefaultPath() (string, error)

DefaultPath returns the config file location: $XDG_CONFIG_HOME/backyard/config.json, falling back to ~/.config/backyard/config.json (%AppData%\backyard\config.json on Windows).

func GatewayURL

func GatewayURL(server, gateway string) string

GatewayURL returns gateway if set, else server + "/__backyard/api/deploy" ("" when both are empty).

func HubURL

func HubURL(info *deploy.Info, server string) string

HubURL returns the hub URL for a server described by info.

func IsLocalHost

func IsLocalHost(host string) bool

IsLocalHost reports whether host (optionally with a port) is localhost, a *.localhost name or a loopback IP.

func NormalizeURL

func NormalizeURL(raw string) (string, error)

NormalizeURL validates an http(s) base URL, adding a scheme when missing (http:// for localhost and loopback addresses, https:// otherwise) and dropping the trailing slash, query and fragment.

func RefreshProfile

func RefreshProfile(ctx context.Context, hc *http.Client, p *Profile, now time.Time, skew time.Duration) (bool, error)

RefreshProfile renews p's token in place when it is refreshable and expires within skew of now. It reports whether p changed (and so should be saved).

func SanitizeSiteName

func SanitizeSiteName(s string) string

SanitizeSiteName turns an arbitrary string into a DNS-label site name, or "" when nothing usable remains.

func SaveProject

func SaveProject(dir string, p Project) error

SaveProject writes p to dir/.backyard.json (mode 0644).

func SiteNameFromDir

func SiteNameFromDir(dir string) string

SiteNameFromDir derives a valid site name from a directory's base name: lowercased, runs of characters outside [a-z0-9] replaced by "-", trimmed to 63 characters. It returns "" when nothing usable remains.

func SiteURL

func SiteURL(info *deploy.Info, server, site string) string

SiteURL returns the public URL of site for a server described by info. In host mode that is <scheme>://<site>.<base_domain>[:<port>]/ with the scheme and port of the public URL (base_domain may differ from the public URL's host; without one the public URL's host is used), in path mode <public_url>/s/<site>/. server is used when info lacks a public URL.

func TokenExpiry

func TokenExpiry(token string) (time.Time, bool)

TokenExpiry returns the "exp" of a JWT token (ok=false for opaque tokens or JWTs without exp).

func ValidMethod

func ValidMethod(m string) bool

ValidMethod reports whether m is a known deploy method ("" counts as auto).

func ValidProfileName

func ValidProfileName(name string) bool

ValidProfileName reports whether name is usable as a profile name.

Types

type Device

type Device struct {
	Issuer        string   `json:"issuer"`
	ClientID      string   `json:"client_id"`
	Scopes        []string `json:"scopes,omitempty"`
	TokenEndpoint string   `json:"token_endpoint,omitempty"`
}

Device describes the IdP used for a device-flow login.

type DeviceAuthorization

type DeviceAuthorization struct {
	DeviceCode              string `json:"device_code"`
	UserCode                string `json:"user_code"`
	VerificationURI         string `json:"verification_uri"`
	VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
	ExpiresIn               int    `json:"expires_in"`
	Interval                int    `json:"interval,omitempty"`
}

DeviceAuthorization is an RFC 8628 §3.2 device authorization response.

func (*DeviceAuthorization) UnmarshalJSON

func (d *DeviceAuthorization) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts the "verification_url" spelling used by some IdPs.

type DeviceFlow

type DeviceFlow struct {
	// HTTP is the client for IdP calls (default DefaultHTTPClient).
	HTTP *http.Client
	// ClientID is the public client id (info.auth.device.client_id).
	ClientID string
	// Scopes requested (info.auth.device.scopes).
	Scopes []string
	// Discovery holds the IdP endpoints (see Discover).
	Discovery *Discovery
	// PollUnit is the length of one interval second (default time.Second);
	// tests shorten it.
	PollUnit time.Duration
}

DeviceFlow performs the OAuth 2.0 device authorization grant (RFC 8628).

func (*DeviceFlow) Poll

Poll polls the token endpoint until the user approves, denies or the code expires, honouring the interval and slow_down responses.

func (*DeviceFlow) Start

Start requests a device and user code.

type Discovery

type Discovery struct {
	Issuer                      string `json:"issuer"`
	DeviceAuthorizationEndpoint string `json:"device_authorization_endpoint"`
	TokenEndpoint               string `json:"token_endpoint"`
}

Discovery is the subset of OpenID Connect discovery metadata the device flow needs.

func Discover

func Discover(ctx context.Context, hc *http.Client, issuer string) (*Discovery, error)

Discover fetches <issuer>/.well-known/openid-configuration.

type File

type File struct {
	// Current is the active profile name.
	Current string `json:"current"`
	// Profiles by name.
	Profiles map[string]*Profile `json:"profiles"`
}

File is the on-disk CLI configuration document.

func Load

func Load(path string) (*File, error)

Load reads the config file at path. A missing file yields an empty config.

func (*File) ActiveName

func (f *File) ActiveName(flag string) string

ActiveName returns the profile selected by flag, else $BACKYARD_PROFILE, else the file's current profile, else "default".

func (*File) Delete

func (f *File) Delete(name string)

Delete removes the named profile; the current selection is cleared when it pointed at it.

func (*File) Names

func (f *File) Names() []string

Names returns the profile names, sorted.

func (*File) Profile

func (f *File) Profile(name string) *Profile

Profile returns the named profile, or nil.

func (*File) Resolve

func (f *File) Resolve(o Overrides) (*Resolved, error)

Resolve merges flags > environment > profile into the effective settings. URLs are normalized; invalid URLs yield an error naming their source.

func (*File) Save

func (f *File) Save(path string) error

Save writes the config atomically with mode 0600, creating the directory with mode 0700 when needed. Wider permissions on an existing file or directory are tightened.

func (*File) Set

func (f *File) Set(name string, p *Profile)

Set stores p under name.

type JWTClaims

type JWTClaims struct {
	Exp   int64           `json:"exp"`
	Iss   string          `json:"iss"`
	Sub   string          `json:"sub"`
	Email string          `json:"email"`
	Aud   json.RawMessage `json:"aud"`
	Azp   string          `json:"azp"`
	Cid   string          `json:"cid"`
}

JWTClaims is the subset of unverified JWT claims the CLI inspects.

func ParseJWTUnverified

func ParseJWTUnverified(token string) (claims *JWTClaims, ok bool)

ParseJWTUnverified decodes the claims of a compact JWT without verifying it. It is only used for local decisions (expiry display, token choice); servers always verify. ok is false for anything that is not a JWT.

func (*JWTClaims) Audiences

func (c *JWTClaims) Audiences() []string

Audiences returns the "aud" claim as a list.

type OAuthError

type OAuthError struct {
	Status      int
	Code        string
	Description string
}

OAuthError is an error response from an OAuth endpoint.

func (*OAuthError) Error

func (e *OAuthError) Error() string

type Overrides

type Overrides struct {
	Profile string
	Server  string
	Gateway string
	Token   string
	Method  string
}

Overrides are explicit settings (command-line flags) that take precedence over the environment and the stored profile.

type Profile

type Profile struct {
	// Server is the hub URL, e.g. https://backyard.acme.com.
	Server string `json:"server,omitempty"`
	// Gateway is an explicit deploy gateway URL (standalone gateway, or the
	// proxy-exempt path); empty means Server + "/__backyard/api/deploy".
	Gateway string `json:"gateway,omitempty"`
	// Token is the bearer token (Backyard API token, static token or IdP JWT).
	Token string `json:"token,omitempty"`
	// TokenExpiresAt is when Token expires (zero when unknown or never).
	TokenExpiresAt time.Time `json:"token_expires_at,omitzero"`
	// Method is the preferred deploy method (auto|api|s3|gcs|azure|rsync).
	Method string `json:"method,omitempty"`
	// RefreshToken is the IdP refresh token of a device-flow login.
	RefreshToken string `json:"refresh_token,omitempty"`
	// Device holds what is needed to refresh a device-flow token.
	Device *Device `json:"device,omitempty"`
	// Login records how the credentials were obtained (token, browser,
	// device or anonymous).
	Login string `json:"login,omitempty"`
	// User is the identity behind the credentials at login time.
	User *User `json:"user,omitempty"`
	// Info caches the gateway's /info document from the last successful
	// contact so direct-sync deploys work while the gateway is unreachable.
	Info *deploy.Info `json:"info,omitempty"`
}

Profile is one Backyard server the CLI talks to, with its credentials.

func (*Profile) CanRefresh

func (p *Profile) CanRefresh() bool

CanRefresh reports whether the profile holds a device-flow refresh token.

func (*Profile) ClearCredentials

func (p *Profile) ClearCredentials()

ClearCredentials removes every secret and identity from the profile while keeping its server settings.

func (*Profile) Clone

func (p *Profile) Clone() *Profile

Clone returns a deep copy of p (nil-safe).

func (*Profile) GatewayURL

func (p *Profile) GatewayURL() string

GatewayURL returns the profile's effective gateway URL.

func (*Profile) TokenExpired

func (p *Profile) TokenExpired(now time.Time) bool

TokenExpired reports whether the profile's token has a known expiry that is not after now.

func (*Profile) TokenExpiresWithin

func (p *Profile) TokenExpiresWithin(now time.Time, d time.Duration) bool

TokenExpiresWithin reports whether the token has a known expiry within d of now (expired tokens included).

type Project

type Project struct {
	// Site is the site name to deploy to.
	Site string `json:"site,omitempty"`
	// Dir is the folder to deploy, relative to the project file (default ".").
	Dir string `json:"dir,omitempty"`
	// Method is the preferred deploy method (default auto).
	Method string `json:"method,omitempty"`
	// Ignore lists extra gitignore-style patterns excluded from deploys.
	Ignore []string `json:"ignore,omitempty"`
}

Project is the content of .backyard.json.

type ProjectFile

type ProjectFile struct {
	Project
	// Path is the absolute path of the .backyard.json file.
	Path string
}

ProjectFile is a project file found on disk.

func FindProject

func FindProject(start string) (*ProjectFile, error)

FindProject searches start and its parents for .backyard.json. It returns (nil, nil) when none is found. Unreadable directories are skipped.

func LoadProject

func LoadProject(path string) (*ProjectFile, error)

LoadProject reads a .backyard.json file.

func (*ProjectFile) DeployDir

func (p *ProjectFile) DeployDir() (string, error)

DeployDir returns the absolute folder to deploy: Dir resolved against the folder holding the project file (Root). Dir must name a folder inside Root: an absolute path, a ".." that leaves Root or a symlink to a folder outside it is an error naming the file. A .backyard.json that arrived with a cloned repository or a pulled site therefore cannot point a plain `backyard deploy` at an unrelated local folder.

func (*ProjectFile) Root

func (p *ProjectFile) Root() string

Root returns the directory containing the project file.

type Resolved

type Resolved struct {
	// ProfileName is the selected profile (it may not exist yet).
	ProfileName string
	// Profile is a copy of the stored profile (never nil).
	Profile *Profile
	// Exists reports whether the profile is stored in the file.
	Exists bool

	Server  string
	Gateway string // effective gateway URL ("" when no server is known)
	Token   string
	Method  string

	ServerSource Source
	TokenSource  Source
}

Resolved is the effective client configuration for one invocation.

func (*Resolved) CachedInfo

func (r *Resolved) CachedInfo() *deploy.Info

CachedInfo returns the server info cached in the profile, or nil when this invocation targets another server or gateway than the profile's: the cache describes the profile's server only (its storage, URLs and methods), and applying it to another one would, for example, point a direct-sync deploy at the wrong bucket.

func (*Resolved) UsesProfileServer

func (r *Resolved) UsesProfileServer() bool

UsesProfileServer reports whether the effective server and gateway are the stored profile's own, i.e. no flag or environment variable pointed this invocation at another server or gateway.

type Session

type Session struct {
	*Resolved
	// Path is the config file path.
	Path string
	// File is the loaded config file.
	File *File
	// Warnings collects non-fatal problems worth showing the user (expired
	// tokens, failed refreshes).
	Warnings []string
	// contains filtered or unexported fields
}

Session is the effective client state for one command: the config file, the resolved settings and helpers to persist profile changes.

func OpenSession

func OpenSession(ctx context.Context, o SessionOptions) (*Session, error)

OpenSession loads the config, resolves flags/env/profile and, when the stored device-flow token is about to expire, refreshes and persists it.

func (*Session) CacheInfo

func (s *Session) CacheInfo(info *deploy.Info)

CacheInfo stores info in the profile (best effort) when the session uses the stored profile's own server and gateway (see CachedInfo).

func (*Session) Client

func (s *Session) Client(hc *http.Client) *deploy.Client

Client returns a deploy gateway client authenticated with the session's token. hc replaces the client's HTTP client when non-nil.

func (*Session) Now

func (s *Session) Now() time.Time

Now returns the session clock's current time.

func (*Session) RequireGateway

func (s *Session) RequireGateway() error

RequireGateway returns ErrNoServer when no gateway URL is known.

func (*Session) UpdateProfile

func (s *Session) UpdateProfile(mutate func(p *Profile)) error

UpdateProfile re-reads the config file, applies mutate to the selected profile (created when missing) and saves it, so concurrent CLI processes do not clobber unrelated changes. The session's copy is updated too.

type SessionOptions

type SessionOptions struct {
	// ConfigPath is the config file (default DefaultPath()).
	ConfigPath string
	// Overrides are flag values.
	Overrides Overrides
	// HTTP is used for IdP token refreshes (default DefaultHTTPClient()).
	HTTP *http.Client
	// Now returns the current time (default time.Now).
	Now func() time.Time
	// Logger receives debug/warn logs (default: discard).
	Logger *slog.Logger
}

SessionOptions configure OpenSession.

type Source

type Source string

Source names where a resolved setting came from.

const (
	SourceFlag    Source = "flag"
	SourceEnv     Source = "env"
	SourceProfile Source = "profile"
	SourceNone    Source = ""
)

Setting sources, in precedence order.

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	IDToken      string `json:"id_token,omitempty"`
	RefreshToken string `json:"refresh_token,omitempty"`
	TokenType    string `json:"token_type,omitempty"`
	ExpiresIn    int64  `json:"expires_in,omitempty"`
}

TokenResponse is an OAuth 2.0 token endpoint response.

func Refresh

func Refresh(ctx context.Context, hc *http.Client, tokenEndpoint, clientID, refreshToken string) (*TokenResponse, error)

Refresh exchanges a refresh token at tokenEndpoint.

func (*TokenResponse) Credential

func (t *TokenResponse) Credential(clientID string, now time.Time) (string, time.Time)

Credential picks the bearer token to present to Backyard and its expiry. The access token is used when it is a JWT issued for clientID (aud, azp or cid); otherwise the ID token (whose audience is the client) when present; otherwise the access token as is.

type User

type User struct {
	ID    string `json:"id"`
	Email string `json:"email,omitempty"`
	Name  string `json:"name,omitempty"`
	Admin bool   `json:"admin,omitempty"`
}

User is the compact identity remembered for a profile.

func (*User) Is

func (u *User) Is(actor string) bool

Is reports whether actor (a SiteMeta created_by/updated_by value) denotes this user.

Jump to

Keyboard shortcuts

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