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

proxy

package
v0.0.0-...-5cbd1aa Latest Latest
Warning

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

Go to latest
Published: Oct 24, 2026 License: MIT Imports: 37 Imported by: 0

Documentation

Overview

Package proxy implements the core reverse proxy that sits between coding agents and GitHub. It is responsible for:

  • Extracting ghx_/gha_ tokens from the Authorization header
  • Resolving tokens to their database records and checking expiry/revocation
  • Enforcing repository and permission scopes against the requested API path
  • Swapping the ghp token for the real GitHub credential (decrypted or obtained from the GitHub App installation token provider)
  • Forwarding the request to the real GitHub API and streaming the response
  • Emitting structured JSON audit log entries for API proxy requests and Prometheus metrics for all requests

The proxy handles three distinct traffic patterns through separate handlers:

  • Handler: the API proxy for api.github.com traffic (REST and GraphQL)
  • ScopedPassthroughHandler: the github.com passthrough for git operations
  • CopilotPassthroughHandler: transparent forwarding for *.githubcopilot.com

Each stage of the request pipeline is individually timed and recorded in the ghp_proxy_decision_duration_seconds histogram, enabling operators to identify exactly where overhead is introduced.

Package proxy implements the GitHub API reverse proxy with scope enforcement.

Index

Constants

View Source
const (
	ForwardProxyLayerToken   = "token"
	ForwardProxyLayerApp     = "app"
	ForwardProxyLayerNet     = "net"
	ForwardProxyLayerSystem  = "system"
	ForwardProxyLayerControl = "control"
	ForwardProxyLayerAmbient = "ambient"
	// ForwardProxyLayerDirect marks non-GitHub control calls (release HEAD
	// probes against the operator's mirror) sent with no proxy at all — the
	// default for those calls unless a control rule opts them in via
	// include_non_github.
	ForwardProxyLayerDirect = "direct"
	// ForwardProxyLayerHeader marks requests whose forward proxy was chosen
	// by the client via the X-GitHub-Proxy-Forward-* request headers
	// (requires forward_proxy.allow_request_header). Client choice beats
	// every ruleset layer.
	ForwardProxyLayerHeader = "header"
)

Forward proxy selection layers, from most to least specific. The layer a request matched is exported as the `layer` label on ghp_forward_proxy_select_total. "ambient" means no ruleset matched and the upstream connection falls back to the process environment (HTTPS_PROXY/HTTP_PROXY/NO_PROXY via http.ProxyFromEnvironment).

View Source
const (
	ForwardProxyHeaderHTTP  = "X-GitHub-Proxy-Forward-HTTP"
	ForwardProxyHeaderHTTPS = "X-GitHub-Proxy-Forward-HTTPS"
	ForwardProxyHeaderSOCKS = "X-GitHub-Proxy-Forward-SOCKS"
)

Request headers a client may use to select the upstream forward proxy for a single request, when forward_proxy.allow_request_header is enabled. The HTTP and HTTPS headers are aliases accepting http:// or https:// proxy URLs; the SOCKS header accepts socks5:// or socks5h:// URLs. The headers are always stripped before the request is forwarded upstream.

Variables

This section is empty.

Functions

func EndpointScope

func EndpointScope(method, path string) (permission, level string)

EndpointScope returns the permission and level required for a given method and path. Returns empty strings if the endpoint is not recognized.

func ExtractClientForwardProxy

func ExtractClientForwardProxy(r *http.Request, enabled bool) (*url.URL, error)

ExtractClientForwardProxy reads and strips the X-GitHub-Proxy-Forward-* headers from r. When enabled is false the headers are stripped and ignored (nil, nil). When enabled, at most one proxy may be specified across the three headers (duplicate identical values are tolerated); the value must be a valid proxy URL with a scheme matching its header family, a host, and no query string or fragment. A non-nil error means the request should be rejected with 400.

func ExtractRepoFromPath

func ExtractRepoFromPath(path string) string

ExtractRepoFromPath extracts the owner/repo from a /repos/{owner}/{repo}/... path. Returns empty string if the path doesn't match.

func GetCacheState

func GetCacheState(r *http.Request) string

GetCacheState returns the cache result state from the request's context slot, or "" if no state was set.

func GetUserID

func GetUserID(r *http.Request) string

GetUserID returns the internal user ID stored in the request context, or "" if none has been set.

func GetUsername

func GetUsername(r *http.Request) string

GetUsername returns the GitHub username stored in the request context, or "" if none has been set.

func GitSmartHTTPScope

func GitSmartHTTPScope(method, path, query string) (repo, permission, level string)

GitSmartHTTPScope extracts the repository, permission, and level from a git smart HTTP request path. Returns empty strings for non-git paths.

Git smart HTTP paths:

  • GET /{owner}/{repo}.git/info/refs?service=git-upload-pack → contents:read
  • POST /{owner}/{repo}.git/git-upload-pack → contents:read
  • GET /{owner}/{repo}.git/info/refs?service=git-receive-pack → contents:write
  • POST /{owner}/{repo}.git/git-receive-pack → contents:write

func NewCodeloadHandler

func NewCodeloadHandler(cfg *config.Config, logger *slog.Logger, transport http.RoundTripper) http.Handler

NewCodeloadHandler returns an http.Handler for codeload.github.com requests.

When cfg.Codeload.RedirectTo is set to an absolute URL, archive download requests matching /{owner}/{repo}/(tar.gz|zip|legacy.tar.gz|legacy.zip)/{ref} are answered with a 302 to RedirectTo + the original path (and query string). Requests for orgs or org/repo pairs in cfg.Codeload.Allow bypass the redirect and are forwarded to the upstream codeload service. Non-archive paths are always forwarded. When RedirectTo is empty, every archive request is still counted as result="passthrough" before forwarding so operators can see total archive volume even before a mirror is configured.

cfg is read on every request so SIGUSR1 hot-reload of codeload.redirect_to and codeload.allow takes effect without a server restart. transport is optional; when nil the default RoundTripper is used. It exists to allow tests to intercept upstream requests without making real network calls.

func NewCopilotPassthroughHandler

func NewCopilotPassthroughHandler(upstream string, enterpriseSlug string, logger *slog.Logger, transport http.RoundTripper) http.Handler

NewCopilotPassthroughHandler creates a transparent reverse proxy for *.githubcopilot.com traffic. The original Host header is preserved so the correct subdomain reaches the real Copilot service. No token interception or scope enforcement is performed; credentials in the Authorization header are forwarded verbatim. Caddy-compatible access logging and Prometheus metrics are applied consistently by the server layer (accessLogHandler), so this handler only concerns itself with proxying. The upstream parameter sets the network destination (scheme + host:port). The transport parameter allows callers to supply a custom RoundTripper; pass nil to use http.DefaultTransport.

func NewForwardProxyControlTransport

func NewForwardProxyControlTransport(fr *ForwardProxyRouter) http.RoundTripper

NewForwardProxyControlTransport returns a transport that routes every request as control-plane traffic (control rule → system rule → ambient). It is installed on ghp's internal clients so operators can pin the proxy's own GitHub traffic to a dedicated egress path.

func NewForwardProxyNonGitHubControlTransport

func NewForwardProxyNonGitHubControlTransport(fr *ForwardProxyRouter) http.RoundTripper

NewForwardProxyNonGitHubControlTransport returns a transport for ghp control calls whose destination is not GitHub (release redirect HEAD probes). Requests are sent direct unless a control rule sets include_non_github, in which case they follow the control ruleset.

func NewForwardProxyTransport

func NewForwardProxyTransport(fr *ForwardProxyRouter) http.RoundTripper

NewForwardProxyTransport returns a RoundTripper that consults the router for every outbound request and dispatches through a transport dedicated to the selected proxy. All outbound GitHub backends (API proxy, github.com passthrough, codeload, Copilot) share one instance so connection pools are reused per selected egress path.

func NewPassthroughHandler

func NewPassthroughHandler(upstream string, resolver TokenResolver, logger *slog.Logger, transport http.RoundTripper) http.Handler

NewPassthroughHandler creates a transparent reverse proxy to the given upstream URL. If a client token (ghx_/gha_) is found in the Authorization header, it is resolved and replaced with the real GitHub credential. Enterprise access restriction enforcement lives in NewScopedPassthroughHandler (the production wrapper for this handler) so that its evaluation is charged to pre-forward decision time rather than the upstream roundtrip, and so the managed client token type is still known when the policy runs. The transport parameter allows callers to supply a custom RoundTripper (e.g. for test TLS); pass nil to use http.DefaultTransport.

func NewReleasesHandler

func NewReleasesHandler(inner http.Handler, cfg *config.Config, logger *slog.Logger, transport ...http.RoundTripper) http.Handler

NewReleasesHandler wraps inner with a policy handler for github.com release download requests. Paths matching /{org}/{repo}/releases/download/** are intercepted when cfg.Releases.Mode is non-empty:

  • "block": Returns 403 unless the org or org/repo is in the allow list.
  • "redirect": Returns a 302 redirect to cfg.Releases.RedirectTo + the original path (and query string) unless the org or org/repo is in the allow list. When cfg.Releases.RedirectHeadCheck is true, a HEAD request is made to the target URL first; if the upstream returns 404, a friendly HTML page is returned instead of a redirect.

When cfg.Releases.RedirectHeadCheckNetrc is set, credentials from the specified netrc file are sent as Basic auth on HEAD availability probes over HTTPS only (plain http:// requests are never authenticated to avoid sending credentials in cleartext). Existing Authorization headers on a request are preserved and never overwritten.

Any other mode value or an empty mode passes all requests through to inner unchanged. The allow list check is always performed before applying the policy, so explicitly listed orgs and repos are never affected. The optional transport parameter overrides the HEAD-check probe client's transport (the server passes the non-GitHub control transport, which sends probes direct unless a control rule sets include_non_github); pass none to use the default transport.

func NewReleasesHandlerWithClient

func NewReleasesHandlerWithClient(inner http.Handler, cfg *config.Config, logger *slog.Logger, client HTTPHeadDoer, creds *netrcCreds) http.Handler

NewReleasesHandlerWithClient is like NewReleasesHandler but accepts a custom HTTP client for HEAD requests and optional pre-parsed netrc credentials, enabling testing without real network calls.

func NewScopedPassthroughHandler

func NewScopedPassthroughHandler(inner http.Handler, enforcer ScopeEnforcer, resolver TokenResolver, ur *UsernameResolver, enterprise *EnterprisePolicy, logger *slog.Logger, cfg ...*config.Config) http.Handler

NewScopedPassthroughHandler wraps a passthrough reverse proxy with git smart HTTP scope enforcement. For requests carrying a client token (ghx_/gha_) that match a git smart HTTP path, the token's repository and permission scopes are verified before the request is forwarded. Non-git paths and non-client tokens pass through unchanged.

When cfg is non-nil, the token type border policy (cfg.Block) is enforced: requests bearing a raw GitHub token whose type prefix is blocked are rejected with 403 before they reach the upstream.

The optional usernameResolver is used to resolve GitHub usernames for both client tokens (via the database) and raw GitHub tokens (via the GitHub API) so that they appear in metrics and access logs.

When enterprise is non-nil, the enterprise access restriction policy is applied to the request (header injection or exception-driven omission, optionally substituting a managed identity) after token resolution and scope enforcement, immediately before the upstream forward — so its evaluation is charged to pre-forward decision time and labeled with the managed client token type.

func PrepareForwardProxyInfo

func PrepareForwardProxyInfo(r *http.Request, clientIP string) *http.Request

PrepareForwardProxyInfo returns a request whose context carries a mutable forward proxy route-info slot seeded with the client IP.

func PrepareUsernameSlot deprecated

func PrepareUsernameSlot(r *http.Request) (*http.Request, *string)

PrepareUsernameSlot returns a new request whose context carries a mutable string slot. Inner handlers call SetUsername to populate it; outer handlers call GetUsername to read the result. This pattern allows the access-log middleware to learn the username that a downstream handler resolved.

Deprecated: use PrepareAccessLogSlots for new code.

func SetCacheRepo

func SetCacheRepo(r *http.Request, repo string)

SetCacheRepo stores the cached repository identifier (owner/repo) in the request's context slot. It is a no-op if no slot was prepared.

func SetCacheState

func SetCacheState(r *http.Request, state string)

SetCacheState stores the cache result state in the request's context slot. Valid values: "hit", "miss", "rejected", "error". It is a no-op if no slot was prepared.

func SetForwardProxyClientChoice

func SetForwardProxyClientChoice(r *http.Request, u *url.URL)

SetForwardProxyClientChoice records a client-selected forward proxy in the request's route-info slot. It is a no-op if no slot was prepared.

func SetForwardProxyIdentity

func SetForwardProxyIdentity(r *http.Request, tokenID, appID, tokenType string)

SetForwardProxyIdentity records the resolved token/app identity in the request's route-info slot. It is a no-op if no slot was prepared.

func SetTokenType

func SetTokenType(r *http.Request, tokenType string)

func SetUserID

func SetUserID(r *http.Request, userID string)

SetUserID stores the internal user ID in the request's context slot. It is a no-op if no slot was prepared.

func SetUsername

func SetUsername(r *http.Request, username string)

SetUsername stores the resolved GitHub username in the request's context slot. It is a no-op if no slot was prepared.

func WithForwardProxyControl

func WithForwardProxyControl(ctx context.Context) context.Context

WithForwardProxyControl returns a context whose route-info slot marks the traffic as ghp-originated control traffic (control rule → system rule → ambient). It shadows any request-derived route info already on the context, so internal calls made with a proxied request's context (e.g. OAuth token refresh) are classified as control rather than inheriting the client's token/app/net routing.

func WithGraphQLURL

func WithGraphQLURL(url string) func(*UsernameResolver)

WithGraphQLURL returns an option that overrides the GitHub GraphQL endpoint URL. This is primarily intended for testing with mock servers.

func WithTeamAPIBaseURL

func WithTeamAPIBaseURL(url string) func(*EnterprisePolicy)

WithTeamAPIBaseURL overrides the GitHub API base URL used for team membership lookups. Intended for tests.

Types

type AccessLogSlots

type AccessLogSlots struct {
	Username   *string
	UserID     *string
	CacheState *string // "hit", "miss", "rejected", "error", or "" for non-cached
	CacheRepo  *string // "owner/repo" if request hit a cached repository
	TokenType  *string // "proxy", "agent", native prefix (e.g. "gho"), or "" if unresolved
}

AccessLogSlots holds the mutable string slots that downstream handlers populate so the access-log middleware can read them after the request.

func PrepareAccessLogSlots

func PrepareAccessLogSlots(r *http.Request) (*http.Request, *AccessLogSlots)

PrepareAccessLogSlots returns a new request whose context carries mutable string slots for both the GitHub username and the internal user ID. Inner handlers call SetUsername / SetUserID to populate them; the access-log middleware reads the results after the request completes.

type AppTokenProvider

type AppTokenProvider interface {
	GetInstallationToken(ctx context.Context, installationID int64, repos []string, permissions map[string]string) (string, error)
}

AppTokenProvider generates installation tokens for agent (gha_) tokens.

type AuditLogEntry

type AuditLogEntry struct {
	Action     string
	UserID     string
	Username   string
	TokenID    string
	TokenType  string
	SessionID  string
	Method     string
	Path       string
	Repository string
	StatusCode int
	DurationMS int
}

AuditLogEntry holds the fields for a structured JSON audit event.

type AuditLogWriter

type AuditLogWriter interface {
	WriteAuditEntry(entry AuditLogEntry)
}

AuditLogWriter is the interface used by the proxy handler to write structured JSON audit log entries.

type EnterprisePolicy

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

EnterprisePolicy decides, per outbound request, whether the enterprise access restriction header is injected and whether a managed identity is substituted for the caller's credential. A nil *EnterprisePolicy is valid and means the feature is disabled (no header, no substitution).

func NewEnterprisePolicy

func NewEnterprisePolicy(gh config.GitHubConfig, identity ExceptionIdentitySource, logger *slog.Logger, opts ...func(*EnterprisePolicy)) *EnterprisePolicy

NewEnterprisePolicy compiles the enterprise restriction configuration into a policy. Returns nil when gh.EnterpriseSlug is empty (feature disabled). identity provides installation tokens for identity substitution and team membership checks; pass nil to disable both (matching exceptions then only omit the header, and team-gated exceptions fail closed and never apply). Malformed match/team entries are logged and skipped.

func (*EnterprisePolicy) Apply

func (p *EnterprisePolicy) Apply(ctx context.Context, hdr http.Header, owner, repo string, username UsernameFunc, tokenType string) string

Apply evaluates the policy for a request targeting owner (and optionally repo) and sets or removes the enterprise restriction header on hdr accordingly. It returns a managed installation token when the matching exception configures identity substitution, or "" when the caller's own credential should be forwarded. Callers must replace the outbound Authorization header when a non-empty token is returned.

tokenType labels the stage-duration metric and follows the decision pipeline vocabulary: "proxy" (ghx_), "agent" (gha_), or "" for raw and anonymous credentials (normalised to "unknown").

A nil policy leaves hdr untouched and returns "".

func (*EnterprisePolicy) SetTransport

func (p *EnterprisePolicy) SetTransport(rt http.RoundTripper)

SetTransport sets the transport used for team membership lookups. The server installs the forward proxy control transport here so membership checks — ghp control traffic — follow the control-layer egress routing instead of dialing GitHub directly. Safe on a nil policy (NewEnterprisePolicy returns nil when the feature is disabled) and a no-op when team checking is disabled (no identity source).

type ExceptionIdentitySource

type ExceptionIdentitySource interface {
	InstallationTokenForOwner(ctx context.Context, appRecordID, owner string, repos []string, permissions map[string]string) (string, error)
}

ExceptionIdentitySource mints a GitHub App installation token for an app's installation on a target account. Implemented by *github.AppRegistry. An empty appRecordID selects the default app.

type ForwardProxyRouteInfo

type ForwardProxyRouteInfo struct {
	ClientIP  string
	TokenID   string // proxy token record UUID, "" when no ghx_/gha_ token resolved
	AppID     string // GitHub App record UUID (agent tokens only), "" otherwise
	TokenType string // "proxy", "agent", or "" — used for metrics labeling only
	Control   bool   // ghp-originated control traffic: route via the control layer only
	// ClientProxy is a per-request forward proxy the client selected via
	// the X-GitHub-Proxy-Forward-* headers (validated and feature-gated at
	// the edge). Non-nil beats every ruleset layer.
	ClientProxy *url.URL
}

ForwardProxyRouteInfo carries the request identity the forward proxy router selects on. The struct is mutable: the server middleware seeds the client IP before dispatch, and the token-resolving handlers fill in the token/app identity once known. Outbound requests built from the inbound request context inherit the same pointer, so http.Transport.Proxy sees the final values at roundtrip time.

func ForwardProxyInfoFromContext

func ForwardProxyInfoFromContext(ctx context.Context) *ForwardProxyRouteInfo

ForwardProxyInfoFromContext returns the route-info slot from ctx, or nil when none was prepared.

type ForwardProxyRouter

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

ForwardProxyRouter selects which upstream forward proxy (if any) an outbound GitHub request should egress through. Rulesets are runtime configuration loaded from the database; Reload rebuilds the in-memory route table. Selection precedence is most-specific first: token rule → app rule → net (CIDR) rule → system rule → ambient environment (http.ProxyFromEnvironment).

func NewForwardProxyRouter

func NewForwardProxyRouter(store database.Store, logger *slog.Logger) *ForwardProxyRouter

NewForwardProxyRouter creates a router with an empty route table. Call Reload to populate it from the store.

func (*ForwardProxyRouter) ControlProxyFunc

func (fr *ForwardProxyRouter) ControlProxyFunc() func(*http.Request) (*url.URL, error)

ControlProxyFunc returns a proxy selection function that always routes as control-plane traffic, regardless of request context. Use it for internal clients whose destination is GitHub (App installation token minting, username resolution, OAuth login flows).

func (*ForwardProxyRouter) ProxyFunc

func (fr *ForwardProxyRouter) ProxyFunc() func(*http.Request) (*url.URL, error)

ProxyFunc returns a per-request proxy selection function suitable for http.Transport.Proxy. It reads the request identity (client IP, token ID, app record ID) from the outbound request's context — populated by the server middleware and the token-resolving handlers on the inbound request, and inherited by outbound requests created with the inbound context — and falls back to http.ProxyFromEnvironment when no ruleset matches or the request carries no route info (e.g. internal OAuth refresh calls).

func (*ForwardProxyRouter) Reload

func (fr *ForwardProxyRouter) Reload(ctx context.Context) error

Reload rebuilds the route table from the store. Disabled rulesets, rulesets with no valid proxy target, and malformed rules are skipped with a warning — a bad ruleset must never take down egress for the rest of the system. When multiple rulesets bind the same token, app, or the system layer, the first by name wins deterministically and a warning is logged.

func (*ForwardProxyRouter) Select

func (fr *ForwardProxyRouter) Select(clientIP, tokenID, appID string) (*url.URL, string, string)

Select returns the forward proxy URL for a request, together with the name of the matched ruleset and the layer that matched. A nil URL with layer "ambient" means no ruleset applies and the caller should fall back to the process environment.

func (*ForwardProxyRouter) SelectControl

func (fr *ForwardProxyRouter) SelectControl() (*url.URL, string, string)

SelectControl returns the forward proxy URL for ghp's own control-plane traffic (OAuth flows and token refresh, App installation token minting, username resolution, release redirect HEAD probes). Precedence: control rule → system rule → ambient. Token, app, and net layers never apply — control traffic is not attributable to a client.

func (*ForwardProxyRouter) StartRefresh

func (fr *ForwardProxyRouter) StartRefresh(ctx context.Context, interval time.Duration)

StartRefresh reloads the route table on the given interval until ctx is cancelled. Admin API mutations reload immediately on the instance that served them; the periodic refresh is what propagates changes to other instances in HA deployments.

type GitHubTokenResolver

type GitHubTokenResolver interface {
	ResolveProxyTokenToGitHub(ctx context.Context, pt *database.ProxyToken) (string, error)
}

GitHubTokenResolver resolves a proxy token database record to the underlying plaintext GitHub credential. This interface is satisfied by ProxyTokenResolver and enables cache warming without depending on the full proxy handler.

type HTTPHeadDoer

type HTTPHeadDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

HTTPHeadDoer is the interface used by the releases handler to perform HEAD requests. This allows injecting a test client.

type Handler

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

Handler is the reverse proxy HTTP handler.

func NewHandler

func NewHandler(cfg *config.Config, ts *token.Service, store database.Store, enc *crypto.Encryptor, atp AppTokenProvider, ur *UsernameResolver, logger *slog.Logger) *Handler

NewHandler creates a new reverse proxy handler.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP handles proxied requests.

func (*Handler) SetAuditLogWriter

func (h *Handler) SetAuditLogWriter(w AuditLogWriter)

SetAuditLogWriter sets the audit log writer for the handler.

func (*Handler) SetEnterprisePolicy

func (h *Handler) SetEnterprisePolicy(p *EnterprisePolicy)

SetEnterprisePolicy replaces the handler's enterprise restriction policy. NewHandler installs a baseline policy compiled from the config (header injection + exception matching, but no identity substitution or team gating); the server calls this after the app registry is available to enable the full feature set.

func (*Handler) SetTransport

func (h *Handler) SetTransport(rt http.RoundTripper)

SetTransport sets the transport used for upstream GitHub requests (and the OAuth token refresh endpoint). The server installs the forward proxy router's transport here so ruleset-based egress selection applies to API proxy traffic.

type MultiAppTokenProvider

type MultiAppTokenProvider interface {
	AppTokenProvider
	// GetInstallationTokenForApp returns a token using the provider for the specified app.
	// If appID is empty, the default provider is used.
	GetInstallationTokenForApp(ctx context.Context, appID string, installationID int64, repos []string, permissions map[string]string) (string, error)
}

MultiAppTokenProvider generates installation tokens with app-specific dispatch.

type ProxyTokenResolver

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

ProxyTokenResolver resolves client tokens (ghx_/gha_) to real GitHub access tokens.

func NewProxyTokenResolver

func NewProxyTokenResolver(ts *token.Service, store database.Store, enc *crypto.Encryptor, atp AppTokenProvider) *ProxyTokenResolver

NewProxyTokenResolver creates a new resolver.

func (*ProxyTokenResolver) ResolveProxyTokenToGitHub

func (r *ProxyTokenResolver) ResolveProxyTokenToGitHub(ctx context.Context, pt *database.ProxyToken) (string, error)

ResolveProxyTokenToGitHub resolves a ProxyToken database record to the underlying plaintext GitHub credential. This is used for cache warming where the caller already has the database record and does not need the full Resolve-from-plaintext-token flow.

func (*ProxyTokenResolver) ResolveToGitHubToken

func (r *ProxyTokenResolver) ResolveToGitHubToken(ctx context.Context, clientToken string) (string, error)

ResolveToGitHubToken resolves a client token to a plaintext GitHub access token.

type ScopeEnforcer

type ScopeEnforcer interface {
	Resolve(ctx context.Context, clientToken string) (*database.ProxyToken, error)
}

ScopeEnforcer resolves a client token string to the full ProxyToken record so that repository and permission scopes can be checked.

type TokenResolver

type TokenResolver interface {
	ResolveToGitHubToken(ctx context.Context, clientToken string) (string, error)
}

TokenResolver resolves a client token (ghx_/gha_) to a real GitHub access token.

type UsernameFunc

type UsernameFunc func(ctx context.Context) string

UsernameFunc lazily resolves the requesting user's GitHub username. It is only invoked when a team-gated exception matches the request target, so callers may back it with a blocking lookup.

type UsernameResolver

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

UsernameResolver resolves GitHub usernames from proxy token user IDs (via the database) and from raw GitHub tokens (via the GitHub GraphQL API). Results are kept in a long-lived in-memory cache keyed by a one-way SHA-256 hash of the token so the actual credential is never stored.

func NewUsernameResolver

func NewUsernameResolver(store database.Store, logger *slog.Logger, opts ...func(*UsernameResolver)) *UsernameResolver

NewUsernameResolver creates a resolver backed by store for database lookups and an LRU cache for GitHub GraphQL API lookups. Optional functional options (e.g. WithGraphQLURL) may be applied for customisation.

func (*UsernameResolver) CheckCache

func (u *UsernameResolver) CheckCache(rawToken string) string

CheckCache returns the cached GitHub username for the given raw token without triggering a background lookup. Returns "" if the token is not yet cached. Use this after an upstream roundtrip to pick up usernames that an in-flight async lookup (started earlier in the same request) may have resolved by then.

func (*UsernameResolver) ResolveFromGitHubToken

func (u *UsernameResolver) ResolveFromGitHubToken(ctx context.Context, rawToken string) string

ResolveFromGitHubToken determines the GitHub username that owns the given raw GitHub token (e.g. gho_, ghp_, ghu_, ghs_ prefixed). The result is cached with a SHA-256 hash of the token as key. On a cache miss the lookup is performed asynchronously so GitHub API latency does not block the caller; empty string is returned for that first request. Only one in-flight lookup is allowed per token to prevent goroutine storms under load. On any error the empty string is returned silently so callers can treat this as best-effort.

func (*UsernameResolver) ResolveFromGitHubTokenSync

func (u *UsernameResolver) ResolveFromGitHubTokenSync(ctx context.Context, rawToken string) string

ResolveFromGitHubTokenSync is the blocking variant of ResolveFromGitHubToken: on a cache miss the GraphQL viewer lookup is performed synchronously (bounded by the resolver's 5s per-lookup timeout) instead of in a background goroutine. It is intended for decisions that cannot proceed without an identity — e.g. team-gated enterprise restriction exceptions — where returning "" on first sight of a token would silently change the outcome. Returns "" on any error.

func (*UsernameResolver) ResolveFromUserID

func (u *UsernameResolver) ResolveFromUserID(ctx context.Context, userID string) string

ResolveFromUserID looks up the GitHub username for an internal user ID via the database. Returns "" if the user cannot be found.

func (*UsernameResolver) SetTransport

func (u *UsernameResolver) SetTransport(rt http.RoundTripper)

SetTransport sets the transport used for GraphQL viewer lookups. The server installs the forward proxy control transport here so username resolution — ghp control traffic — follows the control-layer egress routing.

func (*UsernameResolver) WarmCache

func (u *UsernameResolver) WarmCache(ctx context.Context, resolver GitHubTokenResolver)

WarmCache loads all unexpired, non-revoked proxy tokens from the database, resolves each to its underlying GitHub credential, and triggers an async GraphQL viewer lookup to populate the username cache. This runs in a background goroutine so server startup is not blocked. It is safe to call with a nil resolver or on a resolver with no store — in those cases the warm is silently skipped. ctx is threaded into the warm goroutine so it is cancelled when the caller (e.g. Server.Run) shuts down or returns early due to a startup failure.

Jump to

Keyboard shortcuts

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