cloud

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

Documentation

Overview

Package cloud is the laptop's HTTP client for the user's clank gateway deployment. Provider-agnostic: the gateway exposes an /auth-config discovery endpoint that returns standard OAuth 2.0 endpoints (authorize, token, client_id, scopes), and clank's OAuth client (oauth.go) runs authorization code + PKCE against them.

Designed for the TUI's Cloud panel (internal/tui/cloudview.go) and the `clank login` subcommand. Every call is one-shot, Context-bounded. No background goroutines, no caching: callers own lifecycle.

Index

Constants

This section is empty.

Variables

View Source
var ErrLoginCancelled = errors.New("cloud: login cancelled")

ErrLoginCancelled is returned when the user aborts the flow (closes the browser, or sends ^C in the parent context).

View Source
var ErrLoginTimeout = errors.New("cloud: login timed out")

ErrLoginTimeout is returned when the OAuth provider doesn't redirect back within the timeout window. Default 5 minutes (passed via ctx; oauth.go itself doesn't set a budget).

View Source
var ErrUnauthorized = errors.New("cloud: unauthorized")

ErrUnauthorized is returned when the bearer is rejected by the gateway (e.g. token expired). Caller should re-prompt sign-in.

Functions

This section is empty.

Types

type AuthCaller

type AuthCaller struct {
	// contains filtered or unexported fields
}

AuthCaller wraps an *http.Client targeting the gateway, with the user's OAuth access token attached to each request. Construct one per modal invocation — it is cheap and stateless.

func NewAuthCaller

func NewAuthCaller(gatewayURL, accessToken string, httpClient *http.Client) *AuthCaller

NewAuthCaller constructs a caller against gatewayURL using the given bearer access token. httpClient may be nil for the default.

func (*AuthCaller) AuthFlowStatus

func (a *AuthCaller) AuthFlowStatus(ctx context.Context, providerID, flowID string) (agent.DeviceFlowStatus, error)

AuthFlowStatus mirrors daemonclient.HostClient.AuthFlowStatus.

func (*AuthCaller) CancelAuthFlow

func (a *AuthCaller) CancelAuthFlow(ctx context.Context, providerID, flowID string) error

CancelAuthFlow mirrors daemonclient.HostClient.CancelAuthFlow.

func (*AuthCaller) ListAuthProviders

func (a *AuthCaller) ListAuthProviders(ctx context.Context, backend agent.BackendType) ([]agent.ProviderAuthInfo, error)

ListAuthProviders mirrors hostclient.HTTP.ListAuthProviders.

func (*AuthCaller) StartAuthDeviceFlow

func (a *AuthCaller) StartAuthDeviceFlow(ctx context.Context, providerID string) (agent.DeviceFlowStart, error)

StartAuthDeviceFlow mirrors daemonclient.HostClient.StartAuthDeviceFlow.

func (*AuthCaller) StartAuthOAuthCodeFlow

func (a *AuthCaller) StartAuthOAuthCodeFlow(ctx context.Context, providerID string) (agent.DeviceFlowStart, error)

StartAuthOAuthCodeFlow mirrors daemonclient.HostClient.StartAuthOAuthCodeFlow.

func (*AuthCaller) SubmitAuthAPIKey

func (a *AuthCaller) SubmitAuthAPIKey(ctx context.Context, providerID, key string, metadata map[string]string) (agent.DeviceFlowStart, error)

SubmitAuthAPIKey mirrors daemonclient.HostClient.SubmitAuthAPIKey.

func (*AuthCaller) SubmitAuthCode

func (a *AuthCaller) SubmitAuthCode(ctx context.Context, providerID, flowID, code string) error

SubmitAuthCode mirrors daemonclient.HostClient.SubmitAuthCode.

type AuthConfig

type AuthConfig struct {
	// AuthorizeEndpoint is the IdP's /authorize URL, e.g.
	// "https://abc.supabase.co/oauth/authorize".
	AuthorizeEndpoint string `json:"authorize_endpoint"`

	// TokenEndpoint is the IdP's /token URL, e.g.
	// "https://abc.supabase.co/oauth/token".
	TokenEndpoint string `json:"token_endpoint"`

	// ClientID is the public OAuth client identifier the laptop
	// presents at both endpoints. PKCE replaces the client secret;
	// nothing secret is shipped to the laptop.
	ClientID string `json:"client_id"`

	// Scopes are the OAuth scopes the laptop should request. Joined
	// with spaces on the authorize URL per RFC 6749 §3.3.
	Scopes []string `json:"scopes,omitempty"`

	// DefaultProvider is an optional IdP hint (e.g. "github") the
	// gateway suggests as the primary sign-in option. Passed as the
	// non-spec `provider` query param when set; ignored by IdPs that
	// don't recognise it.
	DefaultProvider string `json:"default_provider,omitempty"`

	// CallbackPort, when set, tells the laptop to bind its PKCE
	// callback listener to exactly this port (instead of a random
	// kernel-assigned port). Required by IdPs that match
	// redirect_uris strictly — Supabase OAuth Server, for example.
	// The IdP must have the same `http://127.0.0.1:<port>` registered
	// as a redirect_uri for the OAuth client.
	CallbackPort int `json:"callback_port,omitempty"`
}

AuthConfig is the gateway's reply to GET /auth-config. It tells clank which OAuth 2.0 IdP to talk to. The endpoint is public (no auth) because clank has no token yet at the point it calls it.

All fields are standard OAuth 2.0 — Supabase OAuth Server, Auth0, Okta, Keycloak, etc. all populate the same shape.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client wraps an *http.Client targeting the gateway base URL. It covers the small bootstrap surface the laptop needs *before* it has a session — currently just /auth-config. Authenticated calls go through dedicated clients elsewhere wrapping the same gateway URL.

func New

func New(gatewayURL string, httpClient *http.Client) *Client

New constructs a Client targeting the gateway base URL (e.g. "https://gateway.example.com"). httpClient may be nil for the default.

func (*Client) FetchAuthConfig

func (c *Client) FetchAuthConfig(ctx context.Context) (*AuthConfig, error)

FetchAuthConfig calls GET <gateway>/auth-config to discover the IdP details. Public endpoint — no Authorization header.

func (*Client) GatewayURL

func (c *Client) GatewayURL() string

GatewayURL returns the configured gateway base URL. Useful for callers that need to construct sub-paths (e.g. sync clients).

type OAuthClient

type OAuthClient struct {
	// AuthorizeEndpoint is the IdP's authorize URL, e.g.
	// "https://abc.supabase.co/oauth/authorize" or
	// "https://auth.example.com/oauth/authorize".
	AuthorizeEndpoint string

	// TokenEndpoint is the IdP's token URL, e.g.
	// "https://abc.supabase.co/oauth/token".
	TokenEndpoint string

	// ClientID is the public OAuth client identifier registered with
	// the IdP. PKCE replaces the client secret, so no secret here.
	ClientID string

	// Scopes are the OAuth scopes requested at /authorize. Joined
	// with spaces per RFC 6749 §3.3. May be empty.
	Scopes []string

	// Provider is an optional IdP hint passed as the non-standard
	// `provider` query parameter on /authorize. Used by Supabase Auth
	// (and similar) to route the user straight to GitHub / Google /
	// etc. Ignored by IdPs that don't recognise it.
	Provider string

	// OpenBrowser is the function called to launch the user's
	// browser. Optional; defaults to the platform's standard
	// open command. Tests inject a no-op.
	OpenBrowser func(target string) error

	// CallbackHosts is the set of bind hosts the localhost listener
	// will try in order. Default ["127.0.0.1", "localhost"]; most
	// IdPs allow either as a wildcard in their redirect-URI config.
	CallbackHosts []string

	// CallbackPort, when non-zero, pins the localhost listener to
	// exactly this port. Default 0 = kernel-assigned random port,
	// which is what RFC 8252 §7.3 recommends for native apps.
	//
	// Set this when the IdP rejects redirect_uris with arbitrary
	// loopback ports (Supabase OAuth Server, for example, requires
	// an exact match against the registered redirect_uris). The
	// gateway communicates the required port via AuthConfig.
	// If the port is already in use, Login returns an error.
	CallbackPort int

	// HTTPClient is used for the token exchange. Optional;
	// defaults to a 30s-timeout client.
	HTTPClient *http.Client

	// Prompt receives the manual-paste URL when OpenBrowser fails so
	// the user can still complete the flow by opening the link in any
	// browser. nil → os.Stderr (visible by default for CLI callers).
	// Set to io.Discard from TUI contexts where stderr writes would
	// corrupt the display.
	Prompt io.Writer
}

OAuthClient runs the PKCE dance against a single IdP. Construct once per login attempt; not reused after Login returns.

func (*OAuthClient) Login

func (c *OAuthClient) Login(ctx context.Context) (*Session, error)

Login runs the full PKCE flow:

  1. Generate code_verifier + code_challenge (S256).
  2. Bind a localhost listener on a random free port.
  3. Open the user's browser to AuthorizeEndpoint with PKCE params and a redirect_uri pointing at the localhost listener.
  4. Wait for the IdP's redirect (?code=... or ?error=...).
  5. Exchange the code at TokenEndpoint (form-encoded).
  6. Return the resulting Session (with sub/email decoded from the JWT payload when the access token is a JWT).

The context's deadline bounds the wait — caller is expected to pass ctx with a reasonable timeout (e.g. 5 minutes). The browser always opens; if OpenBrowser fails, the URL is returned in the error so the user can paste it manually.

func (*OAuthClient) Refresh

func (c *OAuthClient) Refresh(ctx context.Context, refreshToken string) (*Session, error)

Refresh exchanges a refresh_token for a fresh access_token. Used when the cached access_token has expired but the refresh token is still valid.

type Session

type Session struct {
	AccessToken  string
	RefreshToken string
	UserID       string
	UserEmail    string
	ExpiresAt    int64
}

Session is the credential set returned after a successful OAuth grant. ExpiresAt is unix-seconds; treat AccessToken as invalid once time.Now().Unix() > ExpiresAt.

Jump to

Keyboard shortcuts

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