hub

package
v0.124.2 Latest Latest
Warning

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

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

README

Workbench GitHub App

The github.com/sneat-dev/wb/hub package is the server side of bench: the Workbench GitHub App. It owns browser and daemon enrollment, GitHub user OAuth verification, installation entitlements, signed webhook translation, and durable repository-event delivery.

The api/githubapp package in this same module owns only the daemon-facing wire models and client behavior. A host such as Sneat Go supplies Firebase, Firestore, and secret configuration adapters and mounts this package's handler on a githubapp.FirestoreBackend.

See docs/architecture.md for the trust boundaries, protocols, authentication, wire format, delivery guarantees, and daemon communication diagrams.

Documentation

Index

Constants

View Source
const (
	WebhookPath = APIPrefix + "/github/webhook"
	// MinimumWebhookSecretBytes is the configuration-readiness floor for a
	// high-entropy GitHub webhook secret.
	MinimumWebhookSecretBytes = 32
)
View Source
const (
	InstallationConnectPath   = APIPrefix + "/github/installations/connect"
	InstallationContinuePath  = APIPrefix + "/github/installations/continue"
	InstallationAuthorizePath = APIPrefix + "/github/installations/authorize"
	InstallationSetupPath     = APIPrefix + "/github/installations/setup"
	InstallationCallbackPath  = APIPrefix + "/github/installations/callback"
)
View Source
const APIPrefix = "/v0/workbench"
View Source
const (
	MachineEnrollmentPath = APIPrefix + "/machines/enroll"
)
View Source
const StatusPath = APIPrefix + "/github/status"

Variables

View Source
var (
	ErrUnauthorized             = errors.New("workbench github app authentication failed")
	ErrUnavailable              = errors.New("workbench github app service is unavailable")
	ErrInvalidInstallationState = errors.New("workbench github app installation state is invalid")
)

Functions

func NewHandler

func NewHandler(options HandlerOptions) http.Handler

func NewInstallationStores

NewInstallationStores wires the provider-owned installation stores to a host's github.com/sneat-dev/wb/api/githubapp FirestoreBackend. The returned binding store also implements RepositoryEntitlementResolver and InstallationLifecycleStore, so the same value can be assigned to every matching field on InstallationConnectionService, RepositoryEventService, and StatusService.

func NewRepositoryEventStore

NewRepositoryEventStore wires the provider-owned repository event store to a host's github.com/sneat-dev/wb/api/githubapp FirestoreBackend. The returned value implements both RepositoryEventStore and RepositoryEventStatusStore.

Types

type AppInstallationVerifier

type AppInstallationVerifier interface {
	VerifyAppInstallation(context.Context, int64) (VerifiedInstallation, error)
}

type EnqueueResult

type EnqueueResult struct {
	Enqueued  int  `json:"enqueued"`
	Duplicate bool `json:"duplicate"`
}

type EnrolledIdentity

type EnrolledIdentity struct {
	ID          string `json:"id"`
	DisplayName string `json:"display_name,omitempty"`
}

type EnrolledMachine

type EnrolledMachine struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

type GitHubAppInstallationVerifier

type GitHubAppInstallationVerifier struct {
	Client        *http.Client
	AppID         int64
	PrivateKeyPEM []byte
	APIBaseURL    string
	Now           func() time.Time
}

func (GitHubAppInstallationVerifier) VerifyAppInstallation

func (verifier GitHubAppInstallationVerifier) VerifyAppInstallation(ctx context.Context, installationID int64) (VerifiedInstallation, error)

type GitHubIdentityVerifier

type GitHubIdentityVerifier interface {
	ExchangeGitHubOAuthCode(context.Context, string) (string, error)
	VerifyGitHubIdentity(context.Context, string) (VerifiedGitHubIdentity, error)
}

type GitHubOAuthVerifier

type GitHubOAuthVerifier struct {
	Client       *http.Client
	ClientID     string
	ClientSecret string
	CallbackURL  string
	OAuthBaseURL string
	APIBaseURL   string
}

func (GitHubOAuthVerifier) ExchangeGitHubOAuthCode

func (verifier GitHubOAuthVerifier) ExchangeGitHubOAuthCode(ctx context.Context, code string) (string, error)

func (GitHubOAuthVerifier) Validate

func (verifier GitHubOAuthVerifier) Validate() error

func (GitHubOAuthVerifier) VerifyGitHubIdentity

func (verifier GitHubOAuthVerifier) VerifyGitHubIdentity(ctx context.Context, token string) (VerifiedGitHubIdentity, error)

type HandlerOptions

type HandlerOptions struct {
	ViewerResolver   ViewerResolver
	MachineBearer    MachineBearerResolver
	Enrollment       *MachineEnrollmentService
	Snapshots        *MachineSnapshotService
	Installations    *InstallationConnectionService
	RepositoryEvents *RepositoryEventService
	Status           *StatusService
	Projection       ProjectionProcessor
	WebhookSecret    []byte
	AllowedOrigin    string
}

type IdentityInstallationBinding

type IdentityInstallationBinding struct {
	IdentityID   string               `firestore:"identity_id"`
	GitHubUserID int64                `firestore:"github_user_id"`
	GitHubLogin  string               `firestore:"github_login"`
	Installation VerifiedInstallation `firestore:"installation"`
	VerifiedAt   time.Time            `firestore:"verified_at"`
}

type InstallationBindingStore

type InstallationBindingStore interface {
	IdentityHasInstallation(context.Context, string, int64) (bool, error)
	// CompleteIdentityInstallationBinding atomically verifies and consumes the
	// pending OAuth state and upserts only the explicitly selected installation.
	// It must reject a different GitHub user when the Workbench identity already
	// has bindings. A replay or state mismatch returns ErrInvalidInstallationState.
	CompleteIdentityInstallationBinding(context.Context, InstallationStateDigest, InstallationState, time.Time, IdentityInstallationBinding) error
	ListIdentityInstallationBindings(context.Context, string) ([]IdentityInstallationBinding, error)
}

type InstallationConnectRequest

type InstallationConnectRequest struct {
	InstallationID int64 `json:"installation_id,omitempty"`
}

type InstallationConnectResponse

type InstallationConnectResponse struct {
	ConnectURL string    `json:"connect_url"`
	ExpiresAt  time.Time `json:"expires_at"`
}

type InstallationConnectionService

type InstallationConnectionService struct {
	States            InstallationStateStore
	AppVerifier       AppInstallationVerifier
	OAuthVerifier     GitHubIdentityVerifier
	Bindings          InstallationBindingStore
	InstallURL        string
	OAuthAuthorizeURL string
	OAuthClientID     string
	OAuthCallbackURL  string
	SuccessURL        string
	StateSecret       []byte
	StateLifetime     time.Duration
	Random            io.Reader
	Now               func() time.Time
}

func (InstallationConnectionService) AuthorizeOpener

func (InstallationConnectionService) Begin

func (InstallationConnectionService) CompleteOAuth

func (service InstallationConnectionService) CompleteOAuth(ctx context.Context, state, code, browserContinuation, oauthCredential string) (InstallationRedirect, error)

func (InstallationConnectionService) CompleteSetup

func (service InstallationConnectionService) CompleteSetup(ctx context.Context, state string, installationID int64, browserContinuation string) (InstallationRedirect, error)

func (InstallationConnectionService) Continue

func (service InstallationConnectionService) Continue(ctx context.Context, state, openerChallenge string) (InstallationContinuation, error)

type InstallationContinuation

type InstallationContinuation struct {
	RedirectURL string
	ExpiresAt   time.Time
	// contains filtered or unexported fields
}

type InstallationLifecycleAction

type InstallationLifecycleAction string
const (
	InstallationSuspended           InstallationLifecycleAction = "suspended"
	InstallationRevoked             InstallationLifecycleAction = "revoked"
	InstallationRepositoriesRemoved InstallationLifecycleAction = "repositories_removed"
	InstallationUserAccessRemoved   InstallationLifecycleAction = "user_access_removed"
	GitHubUserAuthorizationRevoked  InstallationLifecycleAction = "github_user_authorization_revoked"
	RepositoryUserAccessRemoved     InstallationLifecycleAction = "repository_user_access_removed"
)

type InstallationLifecycleEvent

type InstallationLifecycleEvent struct {
	DeliveryID     string                      `firestore:"delivery_id"`
	Action         InstallationLifecycleAction `firestore:"action"`
	InstallationID int64                       `firestore:"installation_id"`
	RepositoryIDs  []int64                     `firestore:"repository_ids,omitempty"`
	RepositoryID   int64                       `firestore:"repository_id,omitempty"`
	GitHubUserID   int64                       `firestore:"github_user_id,omitempty"`
}

type InstallationLifecycleStore

type InstallationLifecycleStore interface {
	// ApplyInstallationLifecycle is idempotent by DeliveryID and atomically
	// removes or disables every affected entitlement before it returns success.
	// A GitHubUserAuthorizationRevoked event applies to every binding for that
	// GitHub user; RepositoryUserAccessRemoved applies only to its exact
	// installation, repository, and GitHub user tuple.
	ApplyInstallationLifecycle(context.Context, InstallationLifecycleEvent) error
}

type InstallationOpenerAuthorizationRequest

type InstallationOpenerAuthorizationRequest struct {
	State     string `json:"state"`
	Challenge string `json:"challenge"`
}

type InstallationRedirect

type InstallationRedirect struct {
	RedirectURL string
	ExpiresAt   time.Time
	// contains filtered or unexported fields
}

type InstallationState

type InstallationState struct {
	Kind                       installationStateKind `firestore:"kind"`
	IdentityID                 string                `firestore:"identity_id"`
	InstallationID             int64                 `firestore:"installation_id,omitempty"`
	BrowserContinuationDigest  string                `firestore:"browser_continuation_digest"`
	OpenerChallengeDigest      string                `firestore:"opener_challenge_digest,omitempty"`
	OpenerChallengeExpiresAt   time.Time             `firestore:"opener_challenge_expires_at,omitempty"`
	OpenerAuthorizedAt         time.Time             `firestore:"opener_authorized_at,omitempty"`
	OAuthAccessTokenCiphertext string                `firestore:"oauth_access_token_ciphertext,omitempty"`
	IssuedAt                   time.Time             `firestore:"issued_at"`
	ExpiresAt                  time.Time             `firestore:"expires_at"`
}

type InstallationStateDigest

type InstallationStateDigest [sha256.Size]byte

type InstallationStateStore

type InstallationStateStore interface {
	// IssueInstallationState rejects an existing digest so a random-state
	// collision cannot overwrite another pending authorization.
	IssueInstallationState(context.Context, InstallationStateDigest, InstallationState) error
	// ReadInstallationState returns a pending, unexpired state without consuming
	// it. Store availability errors must remain distinguishable from
	// ErrInvalidInstallationState so callers can safely retry transient failures.
	ReadInstallationState(context.Context, InstallationStateDigest, time.Time) (InstallationState, error)
	// TransitionInstallationState atomically verifies current against the stored
	// pending state, consumes it, and issues next. A replay, expired state,
	// mismatch, or next-digest collision returns ErrInvalidInstallationState.
	TransitionInstallationState(context.Context, InstallationStateDigest, InstallationState, time.Time, InstallationStateDigest, InstallationState) error
	// ReplaceInstallationState atomically verifies current against the stored
	// pending state and replaces it under the same digest. It is used to retain
	// an encrypted exchanged OAuth credential before any fallible post-exchange
	// reads. A replay, expiry, or mismatch returns ErrInvalidInstallationState.
	ReplaceInstallationState(context.Context, InstallationStateDigest, InstallationState, time.Time, InstallationState) error
}

type Machine

type Machine struct {
	ID         string
	Name       string
	IdentityID string
	Scopes     []MachineScope
}

type MachineBearerResolver

type MachineBearerResolver interface {
	ResolveMachineBearer(*http.Request) (Machine, error)
}

func NewMachineBearerResolver

func NewMachineBearerResolver(credentials MachineCredentialResolver, pepper []byte) MachineBearerResolver

type MachineCredentialBinding

type MachineCredentialBinding struct {
	MachineID           string         `firestore:"machine_id"`
	MachineName         string         `firestore:"machine_name"`
	IdentityID          string         `firestore:"identity_id"`
	IdentityDisplayName string         `firestore:"identity_display_name,omitempty"`
	IssuedAt            time.Time      `firestore:"issued_at"`
	Scopes              []MachineScope `firestore:"scopes"`
}

type MachineCredentialResolver

type MachineCredentialResolver interface {
	ResolveMachineCredential(context.Context, MachineTokenDigest) (MachineCredentialBinding, error)
}

type MachineCredentialStore

type MachineCredentialStore interface {
	RotateMachineCredential(context.Context, MachineCredentialBinding, MachineTokenDigest) (MachineCredentialBinding, error)
}

type MachineEnrollmentRequest

type MachineEnrollmentRequest struct {
	Name string `json:"name"`
}

type MachineEnrollmentResponse

type MachineEnrollmentResponse struct {
	Machine    EnrolledMachine  `json:"machine"`
	Identity   EnrolledIdentity `json:"identity"`
	Token      string           `json:"token"`
	EnrolledAt time.Time        `json:"enrolled_at"`
}

type MachineEnrollmentService

type MachineEnrollmentService struct {
	Store  MachineCredentialStore
	Pepper []byte
	Random io.Reader
	Now    func() time.Time
}

func (MachineEnrollmentService) Enroll

type MachineScope

type MachineScope string
const (
	ScopeSnapshotPublish MachineScope = "machine_snapshot:publish"
	ScopeSnapshotRead    MachineScope = "machine_snapshot:read"
	ScopeEventsPoll      MachineScope = "repository_events:poll"
	ScopeEventsAck       MachineScope = "repository_events:ack"
)

type MachineSnapshotService

type MachineSnapshotService struct {
	Store MachineSnapshotStore
	Now   func() time.Time
}

func (MachineSnapshotService) List

func (MachineSnapshotService) Publish

type MachineSnapshotStore

type MachineSnapshotStore interface {
	StoreLatest(context.Context, StoredMachineSnapshot) (MachineSnapshotStoreResult, error)
	ListLatest(context.Context) ([]StoredMachineSnapshot, error)
}

type MachineSnapshotStoreResult

type MachineSnapshotStoreResult struct {
	Current StoredMachineSnapshot
	Updated bool
}

func ResolveLatestMachineSnapshot

func ResolveLatestMachineSnapshot(current *StoredMachineSnapshot, candidate StoredMachineSnapshot) (MachineSnapshotStoreResult, error)

type MachineTokenDigest

type MachineTokenDigest [sha256.Size]byte

func DigestMachineToken

func DigestMachineToken(token string, pepper []byte) (MachineTokenDigest, error)

type PendingRefresh

type PendingRefresh struct {
	ID             string    `json:"id"`
	Repository     string    `json:"repository"`
	Event          string    `json:"event,omitempty"`
	QueuedAt       time.Time `json:"queued_at"`
	InstallationID string    `json:"installation_id,omitempty"`
	MachineID      string    `json:"machine_id,omitempty"`
}

type ProjectionProcessor

type ProjectionProcessor interface {
	ProcessProjection(context.Context, WebhookDelivery, string) error
}

ProjectionProcessor lets an existing dashboard projection subsystem consume the same already authenticated delivery while that subsystem is migrated.

type RepositoryEntitlementResolver

type RepositoryEntitlementResolver interface {
	IdentityHasRepositoryEntitlement(context.Context, string, int64, int64) (bool, error)
}

type RepositoryEventService

type RepositoryEventService struct {
	Snapshots    MachineSnapshotStore
	Entitlements RepositoryEntitlementResolver
	Lifecycle    InstallationLifecycleStore
	Store        RepositoryEventStore
	PollInterval time.Duration
	Sleep        func(context.Context, time.Duration) error
	Now          func() time.Time
}

func (RepositoryEventService) Acknowledge

func (RepositoryEventService) EnqueueWebhook

func (service RepositoryEventService) EnqueueWebhook(ctx context.Context, delivery WebhookDelivery) (EnqueueResult, error)

func (RepositoryEventService) Poll

func (service RepositoryEventService) Poll(ctx context.Context, machine Machine, cursor string, limit int, wait time.Duration) (repositoryevent.PollResponse, error)

type RepositoryEventStatusStore

type RepositoryEventStatusStore interface {
	IdentityRepositoryEventStatus(context.Context, string) (*StatusDelivery, []PendingRefresh, []StatusError, error)
}

type StatusConnection

type StatusConnection struct {
	State      string `json:"state"`
	AppName    string `json:"app_name,omitempty"`
	Account    string `json:"account,omitempty"`
	ConnectURL string `json:"connect_url,omitempty"`
}

type StatusDelivery

type StatusDelivery struct {
	LastReceived     *StatusDeliveryMarker `json:"last_received,omitempty"`
	LastAcknowledged *StatusDeliveryMarker `json:"last_acknowledged,omitempty"`
}

type StatusDeliveryMarker

type StatusDeliveryMarker struct {
	DeliveryID string    `json:"delivery_id,omitempty"`
	Event      string    `json:"event,omitempty"`
	OccurredAt time.Time `json:"occurred_at"`
}

type StatusError

type StatusError struct {
	Code           string `json:"code"`
	Message        string `json:"message"`
	Action         string `json:"action,omitempty"`
	ActionURL      string `json:"action_url,omitempty"`
	InstallationID string `json:"installation_id,omitempty"`
}

type StatusInstallation

type StatusInstallation struct {
	ID                  string `json:"id"`
	Account             string `json:"account"`
	AccountType         string `json:"account_type,omitempty"`
	State               string `json:"state"`
	RepositorySelection string `json:"repository_selection,omitempty"`
	Repositories        int    `json:"repositories,omitempty"`
	ManageURL           string `json:"manage_url,omitempty"`
}

type StatusMachine

type StatusMachine struct {
	ID         string     `json:"id"`
	Name       string     `json:"name"`
	State      string     `json:"state,omitempty"`
	LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
}

type StatusResponse

type StatusResponse struct {
	GeneratedAt      time.Time            `json:"generated_at"`
	Connection       StatusConnection     `json:"connection"`
	Installations    []StatusInstallation `json:"installations"`
	Machines         []StatusMachine      `json:"machines"`
	Delivery         *StatusDelivery      `json:"delivery,omitempty"`
	PendingRefreshes []PendingRefresh     `json:"pending_refreshes"`
	Errors           []StatusError        `json:"errors"`
}

type StatusService

type StatusService struct {
	Bindings  InstallationBindingStore
	Snapshots MachineSnapshotStore
	Events    RepositoryEventStatusStore
	AppName   string
	Now       func() time.Time
}

func (StatusService) Read

func (service StatusService) Read(ctx context.Context, viewer Viewer) (StatusResponse, error)

type StoredMachineSnapshot

type StoredMachineSnapshot struct {
	IdentityID string                   `firestore:"identity_id"`
	MachineID  string                   `firestore:"machine_id"`
	Snapshot   machinesnapshot.Snapshot `firestore:"snapshot"`
	ReceivedAt time.Time                `firestore:"received_at"`
	Digest     string                   `firestore:"digest"`
}

type VerifiedGitHubIdentity

type VerifiedGitHubIdentity struct {
	UserID        int64                  `json:"user_id"`
	Login         string                 `json:"login"`
	Installations []VerifiedInstallation `json:"installations"`
}

func (VerifiedGitHubIdentity) Validate

func (identity VerifiedGitHubIdentity) Validate() error

type VerifiedInstallation

type VerifiedInstallation struct {
	ID                  int64                `json:"id" firestore:"id"`
	Account             string               `json:"account" firestore:"account"`
	AccountType         string               `json:"account_type,omitempty" firestore:"account_type,omitempty"`
	RepositorySelection string               `json:"repository_selection" firestore:"repository_selection"`
	Repositories        []VerifiedRepository `json:"repositories" firestore:"repositories"`
	State               string               `json:"state,omitempty" firestore:"state,omitempty"`
	ManageURL           string               `json:"manage_url,omitempty" firestore:"manage_url,omitempty"`
}

type VerifiedRepository

type VerifiedRepository struct {
	ID         int64  `json:"id" firestore:"id"`
	Repository string `json:"repository" firestore:"repository"`
}

type Viewer

type Viewer struct {
	Authenticated bool
	IdentityID    string
	DisplayName   string
}

type ViewerResolver

type ViewerResolver interface {
	Viewer(*http.Request) (Viewer, error)
}

type WebhookDelivery

type WebhookDelivery struct {
	ID         string
	Event      string
	Repository string
	Payload    []byte
}

Jump to

Keyboard shortcuts

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