Help us shape the future of Go! Take the 2026 Go Developer Survey

auth

package
v0.2.0 Latest Latest
Warning

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

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

Documentation

Overview

Package auth finds the GitHub token, runs the GitHub App device flow, and keeps the login in the OS keychain. A token is never printed, logged, written to a file or put in an error message.

Index

Constants

View Source
const (
	Service = "ghostbranch"
	Account = "github.com"
)

Service and Account name the keychain entry that holds the login.

View Source
const (
	// RefreshWindow: a stored app token this close to expiry is refreshed
	// before it is used.
	RefreshWindow = 5 * time.Minute
	// LockWait is how long a command waits for another one's refresh.
	LockWait = 30 * time.Second
)
View Source
const (
	BotName  = "ghostbranch-cli[bot]"
	BotEmail = "340449680+ghostbranch-cli[bot]@users.noreply.github.com"
)

BotName and BotEmail sign the commits that only ever live in the shadow repo. 340449680 is the app bot user's GitHub ID (GET /users/ghostbranch-cli[bot]); GitHub links <id>+<login>@users.noreply.github.com to that account and shows the app's name and logo. Both change if the app is renamed or recreated, so they live next to ClientID and AppSlug.

Variables

View Source
var (
	ClientID = "Iv23liqDTESZ1JStXO0g"
	AppSlug  = "ghostbranch-cli"
)

ClientID is the Ghostbranch GitHub App's client ID, which is public by design (there is no client secret). AppSlug is the app's URL name, for its install page. Both are empty in builds made before the app was registered; login then offers only --with-token.

Functions

func DeleteLogin

func DeleteLogin(s keychain.Store) error

DeleteLogin removes the stored login, a corrupt one included; nothing stored is not an error. A newer Ghostbranch's login is kept.

func FileLock

func FileLock(path string, now func() time.Time, wait time.Duration) func() (func(), error)

FileLock is a Source.Lock over the OS file lock at path (workspace.Lock, which frees itself when its process dies). It waits up to wait for another command's hold to end, then gives that command's LOCKED.

func GHCLI

func GHCLI() (string, error)

GHCLI asks the GitHub CLI for its token.

func Kind

func Kind(token string) string

Kind names a token's type from its prefix.

func LooksLikeAgent

func LooksLikeAgent(getenv func(string) string, consoleUsable bool) bool

LooksLikeAgent reports whether this run is likely an agent or a script rather than a person at a terminal: GHOSTBRANCH_ACTOR names an agent or the app, a coding agent's marker variable is set, or there is no usable console. GHOSTBRANCH_ACTOR=human:… says it is a person.

func NoGH added in v0.2.0

func NoGH(getenv func(string) string) bool

NoGH reports whether GHOSTBRANCH_NO_GH turns off the `gh auth token` fallback: 1, true or yes, in any case, after trimming spaces.

func Resolve

func Resolve(ctx context.Context, src Source) (string, string, error)

Resolve returns the token and where it came from, in this order: GHOSTBRANCH_TOKEN, the stored login ("login"), GH_TOKEN, GITHUB_TOKEN, `gh auth token` (off when GHOSTBRANCH_NO_GH is set; see NoGH). With GHOSTBRANCH_TOKEN set the keychain is never touched. A stored login that can't be used (corrupt, locked keychain, expired, newer) is an error, never a quiet fall back to a later source; only a machine with no keychain at all goes on.

func SaveLogin

func SaveLogin(s keychain.Store, l Login) error

SaveLogin stores l, replacing the stored login unless a newer Ghostbranch wrote it.

Types

type DeviceCode

type DeviceCode struct {
	DeviceCode, UserCode, VerificationURI string
	ExpiresIn, Interval                   int
}

func (DeviceCode) GoString

func (d DeviceCode) GoString() string

func (DeviceCode) String

func (d DeviceCode) String() string

String keeps the device code, a live secret until it expires, out of any %v, %+v, %#v or %s. The user code and URL are shown to the person anyway.

type DeviceFlow

type DeviceFlow struct {
	ClientID string
	BaseURL  string // "https://github.com"; tests point it at authtest
	HTTP     *http.Client
	Sleep    func(time.Duration)
	Now      func() time.Time
}

DeviceFlow is GitHub's OAuth device flow for the Ghostbranch GitHub App: POST /login/device/code, then poll POST /login/oauth/access_token. Its refresh needs no client secret (device-flow tokens), so there is no server.

func (DeviceFlow) Poll

func (d DeviceFlow) Poll(ctx context.Context, dc DeviceCode) (Login, error)

Poll waits for the person to approve the code. It sleeps the interval before each poll (at least 5 s), adds 5 s on slow_down (or takes GitHub's larger interval), stops when the code's life is over, and returns ctx's error on cancel. A network blip while waiting is retried. Account is left empty: the caller fills it from the API.

func (DeviceFlow) Refresh

func (d DeviceFlow) Refresh(ctx context.Context, refreshToken string) (Login, error)

Refresh trades a refresh token for a new pair. GitHub invalidates the old pair, so callers refresh under Source.Lock. A refused refresh token is LOGIN_EXPIRED.

func (DeviceFlow) Start

func (d DeviceFlow) Start(ctx context.Context) (DeviceCode, error)

Start asks GitHub for a device code and the code the person types.

type Login

type Login struct {
	Token, RefreshToken     string
	Expires, RefreshExpires time.Time
	Account                 string // GitHub login
	Kind                    string // "app" (device flow) | "pat" (pasted)
	SchemaVersion           string
}

Login is the stored GitHub login. Expiries are zero for a pasted token.

func LoadLogin

func LoadLogin(s keychain.Store) (*Login, error)

LoadLogin reads the stored login: (nil, nil) when none is stored.

  • A keychain failure is LOGIN_UNAVAILABLE (no_keychain or keychain_error) wrapping the keychain error, so errors.Is(err, keychain.ErrUnavailable) works.
  • An entry from a newer Ghostbranch is UPGRADE_NEEDED.
  • An unreadable entry is LOGIN_UNAVAILABLE (corrupt).

No message quotes the entry or a decoder error, which could carry its text.

func (Login) GoString

func (l Login) GoString() string

func (Login) MarshalJSON

func (Login) MarshalJSON() ([]byte, error)

MarshalJSON refuses: a Login reaches storage only through SaveLogin, and never output.

func (Login) String

func (l Login) String() string

String keeps both tokens out of any %v, %+v, %#v or %s.

type Source

type Source struct {
	Getenv  func(string) string
	GH      func() (string, error)
	Store   keychain.Store // nil: no stored login is consulted
	Refresh func(ctx context.Context, l Login) (Login, error)
	Lock    func() (unlock func(), err error) // <GHOSTBRANCH_HOME>/locks/auth.lock in the CLI
	Now     func() time.Time
	// OnSaveError hears that a refreshed login couldn't be stored. The
	// error may carry a secret, so it is for tests and logic, never output.
	OnSaveError func(error)
}

Source is everything Resolve reads.

Directories

Path Synopsis
Package authtest is a fake GitHub OAuth server for device-flow and refresh tests.
Package authtest is a fake GitHub OAuth server for device-flow and refresh tests.

Jump to

Keyboard shortcuts

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