Documentation
¶
Overview ¶
OpenAI Codex OAuth device-authorization-grant primitives (RFC 8628) for the explicit interactive fallback when browser/loopback login cannot be used. These are primitives only: the auth login command owns the decision to invoke them and never starts them from non-interactive code. Storage is untouched.
OpenAI ChatGPT (Codex) OAuth2 + PKCE login primitives: PKCE generation, authorize-URL construction, fixed-loopback callback capture, and token exchange, with typed cancellation/timeout/unsupported/upstream outcomes and a storage-deferred success payload. Storage is owned downstream; this package never writes credential state.
OpenAI ChatGPT token refresh policy: a proactive refresh before a near-expiry turn and a one-shot reactive refresh after a model 401/403. Successful updates are persisted atomically through the shared settings path; any failure leaves the prior credential state byte-for-byte unchanged and yields a credential-free, actionable error. Refresh traffic targets only the pinned OpenAI auth origin.
Index ¶
- Variables
- func GenerateState() (string, error)
- func ShouldRefreshAfterStatus(status int) bool
- type DeviceAuth
- type Error
- type FailureKind
- type Flow
- func (f *Flow) AuthorizeURL(state string, p PKCE) string
- func (f *Flow) EnsureFreshSession(ctx context.Context, settingsPath string) (authstate.Session, error)
- func (f *Flow) ExchangeCode(ctx context.Context, code, verifier string) (TokenResult, error)
- func (f *Flow) PollDeviceToken(ctx context.Context, da DeviceAuth) (TokenResult, error)
- func (f *Flow) RefreshNow(ctx context.Context, settingsPath string) (authstate.Session, error)
- func (f *Flow) RequestDeviceCode(ctx context.Context) (DeviceAuth, error)
- func (f *Flow) Revoke(ctx context.Context, refreshToken string) error
- func (f *Flow) StartLoopback(expectedState string) (*Loopback, error)
- type Loopback
- type PKCE
- type RefreshFailure
- type TokenResult
Constants ¶
This section is empty.
Variables ¶
var ErrNoSession = errors.New("no usable OpenAI ChatGPT session; run `codemeld auth login openai-chatgpt`")
ErrNoSession is returned by the refresh entrypoints when no usable ChatGPT session is stored (absent, unreadable, or corrupted) and so there is nothing to refresh. Turn dispatch surfaces it as "not logged in" and points the user at login. It carries no credential-derived value.
Functions ¶
func GenerateState ¶
GenerateState returns a random opaque CSRF state value for the authorize request, base64url-encoded from 32 random bytes.
func ShouldRefreshAfterStatus ¶
ShouldRefreshAfterStatus reports whether a model-request HTTP status warrants a single reactive refresh-and-retry: a 401 or 403. It is a pure predicate over the status value so the turn runner can decide whether to invoke RefreshNow without coupling this package to the turn-runner types; the one-shot retry itself is owned by the turn runner.
Types ¶
type DeviceAuth ¶
type DeviceAuth struct {
DeviceCode string
UserCode string
VerificationURI string
VerificationURIComplete string
Interval int64
ExpiresIn int64
}
DeviceAuth is the device-authorization response. UserCode and the verification URIs are meant to be shown to the user; DeviceCode is the polling credential and must never be logged or printed. Interval is the requested poll cadence in seconds and ExpiresIn the lifetime of the device/user code pair.
type Error ¶
type Error struct {
Kind FailureKind
Status int
Detail string
// contains filtered or unexported fields
}
Error is the single typed failure value every login primitive returns. Detail is a credential-free message safe to print; it never contains an access token, refresh token, authorization code, PKCE verifier, or account id. Status carries an upstream HTTP status when one applies (0 otherwise).
type FailureKind ¶
type FailureKind string
FailureKind classifies a login-flow failure without exposing any credential-derived value. It lets the wiring command map each outcome to an exit/UX path while leaving storage untouched.
const ( // KindCanceled means the caller's context was canceled (user interrupt). KindCanceled FailureKind = "canceled" // KindTimeout means the caller's context deadline elapsed, or an upstream // device-code/session expired before the user completed login. KindTimeout FailureKind = "timeout" // KindUnsupported means the host cannot run this flow — typically the fixed // loopback port could not be bound — so the caller should fall back to the // device-code flow or the API-key provider rather than switch redirect URIs. KindUnsupported FailureKind = "unsupported_platform" // KindUpstream means OpenAI rejected or failed the login (bad/expired code, // denied authorization, a non-2xx token response, or an unreachable endpoint). KindUpstream FailureKind = "upstream_failure" )
type Flow ¶
type Flow struct {
// contains filtered or unexported fields
}
Flow holds the login configuration. Production callers use NewFlow, which pins the auth origin and Codex client id; the httpClient, clock, sleep, base URL, and loopback address are unexported so only same-package tests can substitute them.
func NewFlow ¶
func NewFlow() *Flow
NewFlow returns a Flow pinned to the production OpenAI auth origin and Codex client id. There is no exported way to change the origin.
func (*Flow) AuthorizeURL ¶
AuthorizeURL builds the deterministic authorize URL for the browser step: given the same state and PKCE pair it always returns the same URL. The redirect_uri is the fixed loopback callback and the origin is the pinned auth origin.
func (*Flow) EnsureFreshSession ¶
func (f *Flow) EnsureFreshSession(ctx context.Context, settingsPath string) (authstate.Session, error)
EnsureFreshSession is the proactive refresh entrypoint, called before a ChatGPT request. It loads the stored ChatGPT session and, if the access token is already expired or within refreshSkew of expiry, refreshes it against the pinned auth origin and persists the result, returning the session to use for the turn. A still-fresh session is returned unchanged with no network call. It returns ErrNoSession when no usable session is stored and a *RefreshFailure when the refresh or its persistence fails (prior state left unchanged in that case).
func (*Flow) ExchangeCode ¶
ExchangeCode swaps an authorization code for tokens at the pinned token endpoint, completing the PKCE flow with verifier. On success it returns the storage-deferred TokenResult; on failure a typed Error and a zero TokenResult, so no partial credential state escapes. It never writes storage.
func (*Flow) PollDeviceToken ¶
func (f *Flow) PollDeviceToken(ctx context.Context, da DeviceAuth) (TokenResult, error)
PollDeviceToken polls the token endpoint until the user authorizes the device, returning the storage-deferred TokenResult on success. It honors the server-requested interval (and slow_down back-off), the context (cancellation / deadline), and the device-code lifetime: a denied authorization is a typed KindUpstream failure and an expired or timed-out flow is KindTimeout. It writes no storage. The first poll happens only after one interval wait, per RFC 8628.
func (*Flow) RefreshNow ¶
RefreshNow is the reactive refresh entrypoint, called at most once per turn after a model 401/403 (see ShouldRefreshAfterStatus). It re-loads the stored session and forces a refresh regardless of the skew window — a 401/403 can mean the access token was revoked server-side while still appearing locally unexpired — persists the result, and returns the refreshed session. The turn runner owns the single retry of the model request on success and surfaces the error on failure. It returns ErrNoSession when no usable session is stored and a *RefreshFailure on refresh/persist failure (prior state unchanged).
func (*Flow) RequestDeviceCode ¶
func (f *Flow) RequestDeviceCode(ctx context.Context) (DeviceAuth, error)
RequestDeviceCode starts the device flow by requesting a device/user code pair from the pinned device endpoint. On success it returns the DeviceAuth to display to the user and poll with; on failure a typed Error. It writes no storage.
func (*Flow) Revoke ¶
Revoke makes a best-effort RFC 7009 revocation of the ChatGPT session's refresh token at the pinned auth origin so logout can invalidate the grant server-side before deleting local state. Revoking the refresh token revokes the whole grant. It is best-effort: callers proceed with local deletion regardless of the result. A non-2xx response (including a 404 when the origin does not support revocation) returns a typed KindUpstream Error; a context cancellation/deadline returns KindCanceled/KindTimeout; any other transport failure returns a credential-free KindUpstream Error. The refresh token is sent only in the form body to the pinned origin and is never logged or embedded in the returned error.
func (*Flow) StartLoopback ¶
StartLoopback binds the fixed loopback listener and starts serving the OAuth callback. It binds before the authorize URL is opened so a port conflict is reported up front as a typed KindUnsupported failure rather than a silent redirect-URI switch. The handler accepts only a callback whose state matches expectedState and which carries a non-empty code; wrong-state or missing-code callbacks get a local 400 and never yield a code for exchange.
type Loopback ¶
type Loopback struct {
// contains filtered or unexported fields
}
Loopback is the bound OAuth callback listener. The caller must Close it on every path (success, error, cancel, timeout) to release the fixed port and avoid leaking the server goroutine.
func (*Loopback) Close ¶
Close shuts the listener and server down. It is safe to call multiple times.
type PKCE ¶
PKCE is an RFC 7636 verifier/challenge pair. Method is always "S256".
func GeneratePKCE ¶
GeneratePKCE returns a fresh RFC 7636 S256 verifier/challenge pair. The verifier is 32 random bytes base64url-encoded (43 chars, within the 43-128 range); the challenge is the base64url-encoded SHA-256 of the verifier. Neither value is logged.
type RefreshFailure ¶
type RefreshFailure struct {
Status int
// contains filtered or unexported fields
}
RefreshFailure is the typed, credential-free error returned when a proactive or reactive ChatGPT token refresh cannot complete — an upstream non-2xx, a malformed or incomplete token response, a transport error, a context cancellation/deadline, or a failure to persist the refreshed state. Its message is always refreshFailureMessage, so it never contains a token, refresh token, account id, header, cookie, or any other credential-derived value. Status carries the upstream HTTP status when the refresh reached OpenAI (0 otherwise); cause is exposed only for errors.Is and is only ever a context sentinel (context.Canceled / context.DeadlineExceeded).
func (*RefreshFailure) Error ¶
func (e *RefreshFailure) Error() string
func (*RefreshFailure) Unwrap ¶
func (e *RefreshFailure) Unwrap() error
Unwrap exposes only the safe context cause so errors.Is(err, context.Canceled) and errors.Is(err, context.DeadlineExceeded) work for callers.
type TokenResult ¶
type TokenResult struct {
AccessToken string
RefreshToken string
// Expiry is an RFC3339 timestamp matching authstate.Session.ExpiresAt.
Expiry string
AccountID string
}
TokenResult is the storage-deferred success payload of a completed login. Its fields map one-to-one onto authstate.Session (access_token, refresh_token, expires_at, account_id) so the auth login command can persist them under auth.openai-chatgpt; this package returns the payload only and never writes settings. The token/account values must never be logged, printed, or embedded in errors.