Documentation
¶
Overview ¶
ABOUTME: Decodes ChatGPT identity (account ID, plan type) from an id_token's claims and reads ABOUTME: an access_token's exp claim — no signature check, matching Codex (spec §1 "Identity").
ABOUTME: AuthClient, the shared HTTP client for ChatGPT device-code sign-in and token refresh ABOUTME: against auth.openai.com — construction, defaults, and the request shapes.
ABOUTME: Device-code sign-in (design §1, research §B): request a user code, poll until the ABOUTME: user finishes in the browser, then exchange the server-issued PKCE pair for tokens.
ABOUTME: Cross-process refresh lock on Unix: an exclusive flock on chatgpt-auth.lock beside the ABOUTME: token file, polled without blocking so that a canceled context ends the wait.
ABOUTME: Parses the TRACKER_OPENAI_AUTH switch that picks how Tracker authenticates calls to ABOUTME: the openai provider: the default API key, or a signed-in ChatGPT subscription (design §2).
ABOUTME: Refresh-token exchange at auth.openai.com (research §C), and RefreshError, which sorts a ABOUTME: failure into permanent (sign in again) or transient from the token endpoint's JSON error.
ABOUTME: TokenSource hands out the current ChatGPT access token, refreshing it near expiry or after ABOUTME: a 401, one refresh at a time across goroutines (a mutex) and processes (a file lock).
ABOUTME: Derives a point-in-time ChatGPT sign-in summary (plan, refresh and expiry times) from ABOUTME: the token file, folding a missing file into a non-error "not signed in" result.
ABOUTME: The ChatGPT token file: default path, atomic save, and a Load that refuses a ABOUTME: missing, incomplete, symlinked or over-permissive file before touching its contents.
ABOUTME: Unix safety check and no-follow open for the ChatGPT token file: refuses a symlink or ABOUTME: a group/other-accessible mode (mirrors cmd/tracker/envfiles_unix.go's O_NOFOLLOW + Lstat pattern).
ABOUTME: Builds the "tracker/<version> (<GOOS>; <GOARCH>)" User-Agent string every chatgptauth ABOUTME: request sends, deriving <version> from the running binary's embedded build info.
Index ¶
- Constants
- Variables
- func AccessTokenExpiry(accessToken string) (time.Time, bool)
- func DefaultPath() (string, error)
- func Save(path string, t Tokens) error
- func SaveSignIn(ctx context.Context, path string, t Tokens) error
- func UserAgent() string
- type AuthClient
- type DeviceCode
- type Identity
- type Mode
- type RefreshError
- type Status
- type TokenSource
- type Tokens
- type UnsafeFileError
Constants ¶
const ClientID = "app_EMoamEEZ73f0CkXaXp7hrann"
ClientID is Codex's own OAuth client ID, shared by tracker's sign-in and refresh requests (research §A, §B, §C).
const EnvVar = "TRACKER_OPENAI_AUTH"
EnvVar is the environment variable that selects openai's auth mode. Registered in internal/envpolicy as NetworkDestination / ConfigEnv: the shell or the operator's ~/.config/tracker/.env may set it, never a project <workdir>/.env (#659).
const FileName = "chatgpt-auth.json"
FileName is the token file's name under its directory (see DefaultPath).
const Issuer = "https://auth.openai.com"
Issuer is the ChatGPT OAuth issuer: the default base URL for every sign-in and refresh request (research §A "Issuer / base URL"). Tests override AuthClient.Issuer to point at an httptest server instead.
const LockFileName = "chatgpt-auth.lock"
LockFileName is the cross-process refresh lock's name, kept next to the token file; lockFile (lock_unix.go) takes an exclusive flock on it for the length of each refresh (design §1 "Refresh") and of each sign-in save (SaveSignIn).
Variables ¶
var ErrDeviceCodeDisabled = errors.New("device-code sign-in is not enabled for this account (OpenAI returned 404)")
ErrDeviceCodeDisabled is returned by StartDeviceLogin when the server answers the usercode request with 404: a server-side feature flag, not a per-account setting (research §B).
var ErrDeviceCodeExpired = errors.New("the sign-in code expired after 15 minutes; start again")
ErrDeviceCodeExpired is returned by WaitForDeviceLogin once DeviceCode's deadline passes without the user finishing sign-in.
var ErrNotSignedIn = errors.New("not signed in to ChatGPT; run `tracker setup` and choose OpenAI → ChatGPT subscription")
ErrNotSignedIn is returned by Load, and folded into ReadStatus, when the token file does not exist.
Functions ¶
func AccessTokenExpiry ¶
AccessTokenExpiry decodes accessToken's claims and returns its exp claim as a time, or false when the token can't be decoded or carries no exp claim.
func DefaultPath ¶
DefaultPath returns the token file's default location: $XDG_CONFIG_HOME/tracker/chatgpt-auth.json, falling back to $HOME/.config/tracker/... when XDG_CONFIG_HOME is unset or relative — see internal/xdgconfig.ConfigHome.
func Save ¶
Save writes t to path atomically: a temp file at mode 0600 in the same directory, fsynced, then renamed over path. MkdirAll creates a missing directory at 0700; it leaves an already-existing directory's mode exactly as found (MkdirAll is a no-op on an existing directory — it never chmods).
func SaveSignIn ¶
SaveSignIn saves the tokens from a new sign-in while holding the refresh lock (LockFileName beside path). A running tracker process that loaded the token file before the sign-in may be refreshing it right now; it holds that lock until its own Save finishes. Taking the lock makes the sign-in save wait for that refresh and land after it. Without the lock, the refresh could save second and put the old sign-in back. A missing directory is created at 0700 first, because the lock file lives in it. On Windows lockFile is a no-op, so there the save takes no lock.
Types ¶
type AuthClient ¶
type AuthClient struct {
// Issuer is the base URL every request is sent against. Default: Issuer.
Issuer string
// HTTPClient sends every request. Default: a 30-second-timeout client
// that never follows a redirect (refuseRedirects). A client set here
// is used exactly as given, its redirect policy included: the guard
// belongs to the default client only.
HTTPClient *http.Client
// DeviceTimeout bounds how long WaitForDeviceLogin polls before
// returning ErrDeviceCodeExpired. Default: 15 minutes (research §B).
DeviceTimeout time.Duration
// UserAgent is sent as every request's User-Agent header. Default:
// UserAgent().
UserAgent string
}
AuthClient signs a user in to a ChatGPT subscription via the device-code flow. Construct one with NewAuthClient, which fills in every field's documented default; the zero value is not meant to be used directly.
func NewAuthClient ¶
func NewAuthClient() *AuthClient
NewAuthClient returns an AuthClient with its documented defaults.
func (*AuthClient) Refresh ¶
Refresh spends refreshToken at {issuer}/oauth/token for a new token set (research §C). Unlike the form-encoded authorization-code exchange, the refresh grant is a JSON body. The result holds only the fields the server returned: a response without a refresh_token means the server did not rotate it, and the caller keeps its old one. Every failure is a *RefreshError.
func (*AuthClient) StartDeviceLogin ¶
func (c *AuthClient) StartDeviceLogin(ctx context.Context) (DeviceCode, error)
StartDeviceLogin requests a user code from the server (research §B step 1). ErrDeviceCodeDisabled reports a 404 — device-code sign-in not enabled for this server.
func (*AuthClient) WaitForDeviceLogin ¶
func (c *AuthClient) WaitForDeviceLogin(ctx context.Context, dc DeviceCode) (Tokens, error)
WaitForDeviceLogin polls until the user finishes sign-in in the browser, then exchanges the server-issued code for tokens (research §B steps 2-3). It returns ErrDeviceCodeExpired once dc.ExpiresAt passes, or ctx.Err() promptly on cancellation.
type DeviceCode ¶
type DeviceCode struct {
// VerificationURL is where the user enters UserCode: Issuer + "/codex/device".
VerificationURL string
// UserCode is the one-time code to show the user.
UserCode string
// Interval is how long to wait between polls. A caller that needs a
// faster poll (tests) may lower it after StartDeviceLogin returns.
Interval time.Duration
// ExpiresAt is when the code stops being valid: start + DeviceTimeout.
ExpiresAt time.Time
// contains filtered or unexported fields
}
DeviceCode is a pending device-code sign-in: the code and link to show the user, and the state WaitForDeviceLogin needs to poll for completion.
type Identity ¶
type Identity struct{ AccountID, PlanType string }
Identity is the ChatGPT account identity carried in an id_token's namespaced auth claim.
func ParseIDToken ¶
ParseIDToken decodes idToken's claims and reads the account ID and plan type from its "https://api.openai.com/auth" claim. It does not check the token's signature, matching Codex's own id_token handling.
type Mode ¶
type Mode string
Mode selects how Tracker authenticates calls to the openai provider.
func ModeFromEnv ¶
ModeFromEnv reads and parses EnvVar from the process environment.
type RefreshError ¶
type RefreshError struct {
// Code is the server's error code when it is one this package
// recognizes (refresh_token_expired, refresh_token_reused,
// refresh_token_invalidated or invalid_grant), lowercased; "" for none
// or any other. Unrecognized codes are dropped so that no text the
// server chose ever reaches an error string.
Code string
// Status is the HTTP status, or 0 when no response arrived.
Status int
// Permanent is true for any 401, for one of the three refresh-token
// codes, and for a 400 with invalid_grant.
Permanent bool
// contains filtered or unexported fields
}
RefreshError reports a failed refresh-token exchange. Permanent means the sign-in itself is dead — the refresh token expired, was already used, or was revoked — and only signing in again fixes it; anything else (network failures included) may succeed on a retry.
func (*RefreshError) Error ¶
func (e *RefreshError) Error() string
Error sends the user to `tracker setup` for a permanent failure, naming the code (or the status when there is none); a transient one names the status or the underlying failure.
func (*RefreshError) Temporary ¶
func (e *RefreshError) Temporary() bool
Temporary reports whether a retry may succeed: true unless Permanent.
func (*RefreshError) Unwrap ¶
func (e *RefreshError) Unwrap() error
Unwrap returns the transport or decode failure behind the error, if any, so errors.Is still finds a context cancellation.
type Status ¶
Status is a point-in-time summary of the ChatGPT sign-in, for `tracker setup`, `tracker doctor` and `tracker version` to display.
func ReadStatus ¶
ReadStatus loads the token file at path and summarizes it. A missing file (ErrNotSignedIn) is not an error here — it is the ordinary not-signed-in state — and reports as a zero Status. Any other Load failure (an unsafe file, an incomplete sign-in) is returned as an error so the caller surfaces the real problem instead of silently reporting "not signed in".
type TokenSource ¶
type TokenSource struct {
// contains filtered or unexported fields
}
TokenSource hands out the access token and account ID from a token file, refreshing first when the token is due. ChatGPT refresh tokens are single-use and rotate, so exactly one caller may spend one: mu serializes callers inside this process, and on Unix a flock on LockFileName beside the token file serializes processes (lockFile is a no-op on Windows). Whoever takes both locks re-reads the file first and adopts a token another holder already refreshed instead of spending the same refresh token again. After a permanent failure (*RefreshError with Permanent set), every refresh returns that same error without a network call until the token file's refresh token changes — the user signed in again.
func NewTokenSource ¶
func NewTokenSource(path string, client *AuthClient) *TokenSource
NewTokenSource returns a TokenSource for the token file at path whose refreshes go through client. It reads nothing until the first call.
func (*TokenSource) ForceRefresh ¶
func (s *TokenSource) ForceRefresh(ctx context.Context, rejected string) error
ForceRefresh replaces rejected, an access token the backend refused, with a new one. If the token file already holds a different, fresh token — another caller or process refreshed first — it adopts that token without a network call.
func (*TokenSource) Token ¶
func (s *TokenSource) Token(ctx context.Context) (accessToken, accountID string, err error)
Token returns the current access token and account ID, refreshing first when the token is due — so a token that expired while the machine slept is refreshed before its first use, not after a 401.
type Tokens ¶
type Tokens struct {
IDToken string `json:"id_token"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
AccountID string `json:"account_id"`
LastRefresh time.Time `json:"last_refresh"`
}
Tokens is the ChatGPT OAuth token set persisted to the token file.
func Load ¶
Load reads and validates the token file at path.
A missing file is ErrNotSignedIn. A symlink, or (on Unix) a mode with any group or other bit set, is a *UnsafeFileError — checkFileSafe (store_unix.go / store_windows.go) decides this before the content is ever read. A file that parses but is missing its access token, refresh token or account ID is an incomplete sign-in: the error names the problem but never repeats any token value.
type UnsafeFileError ¶
type UnsafeFileError struct{ Path, Problem string }
UnsafeFileError reports that Load refused to read the token file because its symlink status or permission bits could expose it to another account on the same machine.
func (*UnsafeFileError) Error ¶
func (e *UnsafeFileError) Error() string
Error picks its fix hint by the kind of Problem: a symlink is fixed by replacing it with a regular file, loose mode bits by chmod.