forge

package
v0.38.0 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package forge is the code host behind a board: the identity provider the server signs people in with, the authority on who may read or write a repository, the directory of names and avatars, and the credential git pushes with. GitHub was the first; GitLab is the second. Everything the server does with a forge goes through the Forge interface, and the server never assembles a forge URL of its own.

Index

Constants

View Source
const UnansweredTTL = 30 * time.Second

UnansweredTTL is how long a source remembers that the forge could not say who a token belongs to — a 403 for a token with no user behind it, a 5xx, a timeout, a DNS failure. Without it the question is asked again on every request, under the lock the source holds while it asks, so one unreachable forge serialises every caller behind a full timeout each.

It matches the ceiling those sources put on the call, so a window holds at most one attempt that runs to the end; and half a minute is short enough that a forge coming back — a VPN reconnecting, a rate limit lifting — names the person again without a restart. A refusal is NOT this: a 401 is an answer, and it is remembered until the token changes.

Variables

View Source
var (
	// ErrBadToken is the forge refusing the token it was given (401).
	ErrBadToken = errors.New("forge: the token was rejected")
	// ErrNotFound is a login the forge does not know.
	ErrNotFound = errors.New("forge: no such person")
	// ErrRateLimited is the forge throttling us. It shares the 403 of "you
	// may not see this" and must never be read as one: a moment of
	// throttling would otherwise be remembered as a visitor's lack of
	// access to their own board.
	ErrRateLimited = errors.New("forge: rate limited")
)

Errors a forge reports.

Functions

func CanPushGit added in v0.27.0

func CanPushGit(ctx context.Context, client *http.Client, f Forge, token, repoURL string) (bool, error)

CanPushGit asks the git transport itself whether a token may push: the receive-pack advertisement (GET /info/refs?service=git-receive-pack) answers 200 exactly when it may, 403 when it may not, 401 when the token is nobody there. The REST permissions block is a different animal — a GitHub App's user token can push a repository the REST probe says nothing useful about, and the transport is the authority the push will face anyway.

func HostOf

func HostOf(repoURL string) string

HostOf is the host of a repository URL — https://host/…, git@host:… — or "" for a bare owner/repo.

func IsAppClientID added in v0.27.0

func IsAppClientID(clientID string) bool

IsAppClientID reports whether a client id belongs to a GitHub App rather than an OAuth App. A GitHub App has no scopes — its permissions come from the installation, and GitHub ignores a scope parameter — so asking for one would leave "scope=repo" in a URL that grants nothing of the sort. GitHub App client ids carry an Iv prefix (Iv1.<hex> historically, Iv23li… now); anything else is taken for an OAuth App, whose sign-in breaks without its scope — so the doubt falls the safe way.

func IsUserToServerToken added in v0.27.0

func IsUserToServerToken(token string) bool

IsUserToServerToken reports whether a token was minted for a person BY a GitHub App: such a token reaches only the repositories the app is installed on, so a refusal about someone's own repository means "not installed there", not "no access". GitHub says which kind a token is in its prefix — ghu_ for user-to-server, gho_ for an OAuth App's, ghp_ for a classic personal token, ghs_ for an installation's own.

Types

type AppGitAuth added in v0.27.0

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

AppGitAuth stamps a fresh installation token onto each git HTTP request.

func (*AppGitAuth) Name added in v0.27.0

func (g *AppGitAuth) Name() string

func (*AppGitAuth) SetAuth added in v0.27.0

func (g *AppGitAuth) SetAuth(r *http.Request)

SetAuth cannot return an error; when the mint fails the request goes out bare and the operation fails with the forge's own refusal, to be retried by the sync — the mint error itself surfaces on the next Token caller.

func (*AppGitAuth) String added in v0.27.0

func (g *AppGitAuth) String() string

type AppNotInstalledError added in v0.27.0

type AppNotInstalledError struct {
	App        string // the app id
	Slug       string // owner/repo
	InstallURL string // where the app is installed
}

AppNotInstalledError is a repository the app has no installation for — the one startup trouble a page with a button can fix, so callers tell it apart (errors.As) and serve that page instead of refusing to run.

func (*AppNotInstalledError) Error added in v0.27.0

func (e *AppNotInstalledError) Error() string

type CLI

type CLI interface {
	// Token is this source's credential. A source with nothing to give
	// may answer either way — an error, or the empty string — and a
	// caller standing for SEVERAL sources owes the difference: it must
	// return an error when none of them answered, never a silent empty
	// string, because the server decides "have we got a credential at
	// all" on that error alone.
	Token(ctx context.Context) (string, error)
	// Login is who the forge says this source's token belongs to, which
	// need not be whoever ran a tool on the machine: a stored bot token
	// belongs to the bot. It must describe the token Token would hand
	// back right now, or the push is made with one account's credential
	// and the work signed with another's name.
	Login(ctx context.Context) (string, error)
}

CLI is a source of the person's credential and identity on a single-user server — the forge's own tool (gh, glab), the environment, or the OS keychain. Its token is what the server reads the forge and pushes with, and its login is who that token belongs to; the two come from ONE source, or the board pushes as one account and signs the work with another's name.

type Credential added in v0.33.0

type Credential interface {
	CLI
	TokenAndLogin(ctx context.Context) (token, login string, err error)
}

Credential is a CLI that can answer both questions at once. A caller that needs the token AND the person it belongs to must not ask twice: a CLI standing for several sources re-decides between the calls when one of them empties, and the caller is then holding one source's token beside another's name — which is the split the pairing exists to prevent. A CLI that is a single source has nothing to add and need not implement it; ask separately when the assertion fails.

type Forge

type Forge interface {
	// Kind names the forge; Label is its display name for UI copy.
	Kind() Kind
	Label() string
	// Host is the instance this forge is: github.com, or the host of a
	// GitLab's base URL. It is what a credential belongs to, so a caller
	// keeping one token per forge keys it by this rather than parsing a
	// repository URL of its own.
	Host() string

	// AuthorizeURL and TokenURL are the OAuth endpoints the sign-in flow
	// uses; DefaultScopes the scope string a board needs when the operator
	// sets none.
	AuthorizeURL() string
	TokenURL() string
	DefaultScopes() string
	// ExchangeForm and RefreshForm are the token endpoint's request bodies
	// in the forge's dialect (GitLab names the grant type and wants the
	// redirect URI on a refresh too).
	ExchangeForm(clientID, secret, code, redirectURI string) url.Values
	RefreshForm(clientID, secret, refresh, redirectURI string) url.Values

	// User is who a token belongs to. A rejected token is ErrBadToken.
	User(ctx context.Context, client *http.Client, token string) (User, error)

	// RepoRef is a repository URL as the forge's API names the repository —
	// and the check that the URL names one at all.
	RepoRef(repoURL string) (string, error)
	// Access asks, as the visitor, what they may do with the repository:
	// read it, write it. A repository they cannot see is (false, false, nil);
	// a rejected token is ErrBadToken.
	Access(ctx context.Context, client *http.Client, token, repoURL string) (read, write bool, err error)
	// Readers is the subset of logins that may read the repository, asked
	// with the server's credential, keyed by login — with whatever the forge
	// said about each on the way (a member list carries names and avatars).
	Readers(ctx context.Context, client *http.Client, serverToken, repoURL string, logins []string) (map[string]Person, error)
	// Lookup finds a person by login. It may or may not call the forge.
	Lookup(ctx context.Context, client *http.Client, token, login string) (Person, error)

	// GitAuth is the HTTPS credential git operations use with a token.
	GitAuth(token string) *githttp.BasicAuth
}

Forge is one code host.

func Detect

func Detect(repoURL string, explicit Kind, gitlabBase string) (Forge, error)

Detect names the forge for a repository: the explicit kind when given, else by the URL's host — github.com is GitHub, a host that says gitlab is that GitLab instance — else GitLab when a base URL for one is configured, else GitHub, the forge this code base grew up on.

func NewGitHub

func NewGitHub() Forge

NewGitHub is github.com.

func NewGitHubAt

func NewGitHubAt(api string) Forge

NewGitHubAt is GitHub with its REST base elsewhere — tests.

func NewGitLab

func NewGitLab(base string) Forge

NewGitLab is the GitLab instance at base.

type GitHubApp added in v0.27.0

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

GitHubApp is the server credential minted instead of kept: the server signs a short-lived JWT with the app's private key, asks which installation a repository belongs to, and trades the JWT for an installation token — scoped to the repositories the app is installed on, expiring within the hour, renewed here before it runs out. One key covers every organisation the app is installed in; nothing is issued by hand and nothing quietly expires in a .env file.

func NewGitHubApp added in v0.27.0

func NewGitHubApp(id string, pemKey []byte) (*GitHubApp, error)

NewGitHubApp is a GitHub App credential for github.com. id is the app id (the JWT issuer), pemKey the private key GitHub generated for the app. The key is validated here — a broken PEM stops the server at startup, not the first push an hour in.

func NewGitHubAppAt added in v0.27.0

func NewGitHubAppAt(api string, client *http.Client, id string, pemKey []byte) (*GitHubApp, error)

NewGitHubAppAt is NewGitHubApp with the REST base elsewhere — tests.

func (*GitHubApp) GitAuthFor added in v0.27.0

func (a *GitHubApp) GitAuthFor(repoURL string) *AppGitAuth

GitAuthFor is the git transport credential for one repository: the token is asked for on every request, so one renewed between two pushes is picked up without re-wiring the remote.

func (*GitHubApp) InstallURL added in v0.27.0

func (a *GitHubApp) InstallURL(ctx context.Context) string

InstallURL is where the app is installed on an account: the app's own page plus /installations/new, which lets the person pick the account and the repositories. Asked from the forge once; a lookup that fails answers the app's settings path, which always exists.

func (*GitHubApp) Token added in v0.27.0

func (a *GitHubApp) Token(ctx context.Context, repoURL string) (string, error)

Token is the installation token for the repository, minted or renewed as needed. A repository the app is not installed on is a plain error — not a mystery 401 three layers later.

type Kind

type Kind string

Kind names a forge.

const (
	GitHub Kind = "github"
	GitLab Kind = "gitlab"
)

The forges aeman knows.

type Person

type Person struct {
	Login     string
	Name      string
	AvatarURL string
}

Person is what the forge knows about a login: the name and the avatar. A forge that cannot say (GitHub's permission check names nobody) leaves Name empty and builds the avatar from the login.

type User

type User struct {
	Login     string
	Name      string
	AvatarURL string
}

User is the signed-in person as the forge describes them.

Jump to

Keyboard shortcuts

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