Documentation
¶
Index ¶
- Constants
- Variables
- func BuildAppJWT(appID int64, privateKeyPEM []byte, now func() time.Time) (string, error)
- func Collections() []string
- func MachineID(identityID, machineName string) string
- func NewHandler(options HandlerOptions) http.Handler
- func NewInstallationStores(backend githubapp.DocumentStore) (InstallationStateStore, InstallationBindingStore, ...)
- func NewMachineStores(backend githubapp.DocumentStore) (MachineCredentialStore, MachineCredentialResolver, MachineSnapshotStore)
- func NewPeerStores(backend githubapp.DocumentStore) (PeerTrustStore, PeerStatsStore)
- func NewRepositoryEventStore(backend githubapp.DocumentStore) (RepositoryEventStore, RepositoryEventStatusStore)
- func RetryAfter(header http.Header) (time.Duration, bool)
- type AppInstallationVerifier
- type EnqueueResult
- type EnrolledIdentity
- type EnrolledMachine
- type GitHubAppInstallationVerifier
- type GitHubIdentityVerifier
- type GitHubOAuthVerifier
- type HandlerOptions
- type IdentityInstallationBinding
- type InstallationBindingStore
- type InstallationConnectRequest
- type InstallationConnectResponse
- type InstallationConnectionService
- func (service InstallationConnectionService) AuthorizeOpener(ctx context.Context, viewer Viewer, ...) error
- func (service InstallationConnectionService) Begin(ctx context.Context, viewer Viewer, request InstallationConnectRequest) (InstallationConnectResponse, error)
- func (service InstallationConnectionService) CompleteOAuth(ctx context.Context, state, code, browserContinuation, oauthCredential string) (InstallationRedirect, error)
- func (service InstallationConnectionService) CompleteSetup(ctx context.Context, state string, installationID int64, ...) (InstallationRedirect, error)
- func (service InstallationConnectionService) Continue(ctx context.Context, state, openerChallenge string) (InstallationContinuation, error)
- type InstallationContinuation
- type InstallationLifecycleAction
- type InstallationLifecycleEvent
- type InstallationLifecycleStore
- type InstallationOpenerAuthorizationRequest
- type InstallationRedirect
- type InstallationState
- type InstallationStateDigest
- type InstallationStateStore
- type Machine
- type MachineBearerResolver
- type MachineCredentialBinding
- type MachineCredentialResolver
- type MachineCredentialStore
- type MachineEnrollmentRequest
- type MachineEnrollmentResponse
- type MachineEnrollmentService
- type MachineIndex
- type MachineScope
- type MachineSnapshotService
- type MachineSnapshotStore
- type MachineSnapshotStoreResult
- type MachineTokenDigest
- type PeerAdminService
- func (service *PeerAdminService) Block(ctx context.Context, nameOrID string) (PeerRecord, error)
- func (service *PeerAdminService) Disconnect(ctx context.Context, nameOrID string) (PeerDisconnectResult, error)
- func (service *PeerAdminService) Invite(ctx context.Context, name string, rotate bool) (PeerInviteResult, error)
- func (service *PeerAdminService) RefuseIfPeerNameCollision(ctx context.Context, name string) error
- func (service *PeerAdminService) Unblock(ctx context.Context, nameOrID string) (PeerRecord, error)
- type PeerDisconnectResult
- type PeerInviteResult
- type PeerRecord
- type PeerStats
- type PeerStatsStore
- type PeerTrust
- type PeerTrustResolver
- type PeerTrustStore
- type PendingRefresh
- type PollObservation
- type PollObservationStore
- type ProjectionProcessor
- type QueuedWorkStore
- type RateLimit
- type RepositoryEntitlementResolver
- type RepositoryEventService
- func (service RepositoryEventService) Acknowledge(ctx context.Context, machine Machine, request repositoryevent.AckRequest) (repositoryevent.AckResponse, error)
- func (service RepositoryEventService) EnqueueWebhook(ctx context.Context, delivery WebhookDelivery) (EnqueueResult, error)
- func (service RepositoryEventService) Poll(ctx context.Context, machine Machine, cursor string, limit int, ...) (repositoryevent.PollResponse, error)
- type RepositoryEventStatusStore
- type RepositoryEventStore
- type StatusConnection
- type StatusDelivery
- type StatusDeliveryMarker
- type StatusError
- type StatusInstallation
- type StatusMachine
- type StatusResponse
- type StatusService
- type StoredMachineSnapshot
- type VerifiedGitHubIdentity
- type VerifiedInstallation
- type VerifiedRepository
- type Viewer
- type ViewerResolver
- type WebhookDelivery
- type WebhookRedeliveryRecord
- type WebhookRedeliveryStore
Constants ¶
const ( WebhookPath = APIPrefix + "/github/webhook" // MinimumWebhookSecretBytes is the configuration-readiness floor for a // high-entropy GitHub webhook secret. MinimumWebhookSecretBytes = 32 )
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" )
const APIPrefix = "/v0/workbench"
const (
MachineEnrollmentPath = APIPrefix + "/machines/enroll"
)
const PeersConnectPath = APIPrefix + "/peers/connect"
PeersConnectPath is the route peer-connectivity#req:invite-and-join adds ahead of Task 2's WebSocket upgrade: "The verification route doesn't exist yet. Add GET /v0/workbench/peers/connect to the hub handler: a request without a WebSocket upgrade and with a valid peer bearer returns 200 {schema_version, peer_id, name}." Task 2 adds the upgrade on the same path; this probe stays as a cheap liveness/verification check. This is an addition to the spec text, which only names the path as the session route — see the PR description.
const StatusPath = APIPrefix + "/github/status"
Variables ¶
var ( ErrInvalidInstallationState = errors.New("workbench github app installation state is invalid") )
var ( // ErrPeerExists is returned by CreatePeer when a trust document already // exists for the given MachineID. ErrPeerExists = errors.New("peer record already exists") // ErrPeerNotFound is returned by the read and mutate operations below // when no trust document exists for the given MachineID or name. A // credential with no such record is not a peer at all, which is a // distinct, non-error outcome reported through the bool return instead. ErrPeerNotFound = errors.New("peer record not found") )
Functions ¶
func BuildAppJWT ¶ added in v0.147.0
BuildAppJWT mints the short-lived JWT the GitHub App API authenticates with — the same signing GitHubAppInstallationVerifier does for installation verification, exported so another authenticated caller (the missed-webhook redelivery sweep in hub/redeliver) reuses it rather than duplicating the signing code. now is optional; nil means time.Now.
func Collections ¶ added in v0.124.5
func Collections() []string
Collections lists every collection path shape the hub-owned stores write to, in declaration order: a root collection always precedes the subcollections nested beneath it.
A schemaless engine (Firestore, OpenVaultDB, dalgo2memory) never needs this — a collection springs into existence on first write. A schema-first engine does: inGitDB refuses a write to a collection that has no definition on disk, and a nested path must be declared as a subcollection of its root. The dynamic segments in a real path (a machine id, an identity digest, a generation) are document ids, not collection names, so the shapes here are the complete and finite set.
Keep this in step with the collection constants and path helpers in installation_store.go, repository_event_store.go, machine_credential_store.go, machine_snapshot_store.go, poll_observation_store.go and peer_store.go; the store tests assert every one of them is listed.
func MachineID ¶ added in v0.124.5
MachineID derives the stable machine document id from the identity and the machine name. A self-hosting daemon needs it to recognise the machine it already enrolled without keeping a second record of the id.
func NewHandler ¶
func NewHandler(options HandlerOptions) http.Handler
func NewInstallationStores ¶
func NewInstallationStores(backend githubapp.DocumentStore) (InstallationStateStore, InstallationBindingStore, RepositoryEntitlementResolver, InstallationLifecycleStore)
NewInstallationStores wires the provider-owned installation stores to a host's github.com/sneat-dev/wb/api/githubapp DocumentStore. 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 NewMachineStores ¶ added in v0.124.5
func NewMachineStores(backend githubapp.DocumentStore) (MachineCredentialStore, MachineCredentialResolver, MachineSnapshotStore)
NewMachineStores wires the hub-owned machine stores to a host's github.com/sneat-dev/wb/api/githubapp DocumentStore. One value satisfies both MachineCredentialStore and MachineCredentialResolver, so the same backing records answer enrollment and bearer resolution.
The returned values are what MachineEnrollmentService.Store, NewMachineBearerResolver and MachineSnapshotService.Store expect, which is every persistence a loopback hub needs before an App is involved.
func NewPeerStores ¶ added in v0.150.1
func NewPeerStores(backend githubapp.DocumentStore) (PeerTrustStore, PeerStatsStore)
NewPeerStores wires PeerTrustStore and PeerStatsStore to a host's api/githubapp DocumentStore, the same port every other hub store uses.
func NewRepositoryEventStore ¶
func NewRepositoryEventStore(backend githubapp.DocumentStore) (RepositoryEventStore, RepositoryEventStatusStore)
NewRepositoryEventStore wires the provider-owned repository event store to a host's github.com/sneat-dev/wb/api/githubapp DocumentStore. The returned value implements both RepositoryEventStore and RepositoryEventStatusStore.
func RetryAfter ¶ added in v0.147.0
RetryAfter parses GitHub's Retry-After response header, in seconds. GitHub sends it on some rate-limited and abuse-detection responses, sometimes instead of the X-RateLimit-* headers above and sometimes alongside them; a caller that wants to honor both prefers this one when present.
Types ¶
type AppInstallationVerifier ¶
type AppInstallationVerifier interface {
VerifyAppInstallation(context.Context, int64) (VerifiedInstallation, error)
}
type EnqueueResult ¶
type EnrolledIdentity ¶
type EnrolledMachine ¶
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 GitHubOAuthVerifier ¶
type GitHubOAuthVerifier struct {
Client *http.Client
ClientID string
ClientSecret string
CallbackURL string
OAuthBaseURL string
APIBaseURL string
}
func (GitHubOAuthVerifier) ExchangeGitHubOAuthCode ¶
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
// Narrate receives one line for every delivery the handler itself
// rejects, written before the response to GitHub is sent. Deliveries the
// handler accepts are narrated by RepositoryEventService instead, so each
// delivery produces exactly one line. Optional.
Narrate func(narrate.Line)
// DisableSelfHostedEnrollment unmounts POST MachineEnrollmentPath
// (peer-connectivity#req:admin-requires-owner-credential): a self-hosted
// hub's loopback listener is reachable through a tunnel or proxy, which is
// not proof of the local operator, so self-hosted enrollment moves to the
// daemon's owner-token RPC service instead. The zero value (false) mounts
// the route exactly as before, which is what the hosted instance's own
// OAuth-viewer-gated deployment keeps doing without changing a line here.
DisableSelfHostedEnrollment bool
}
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 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 (service InstallationConnectionService) AuthorizeOpener(ctx context.Context, viewer Viewer, request InstallationOpenerAuthorizationRequest) error
func (InstallationConnectionService) Begin ¶
func (service InstallationConnectionService) Begin(ctx context.Context, viewer Viewer, request InstallationConnectRequest) (InstallationConnectResponse, error)
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 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 InstallationRedirect ¶
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 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 ¶
func NewMachineBearerResolver ¶
func NewMachineBearerResolver(credentials MachineCredentialResolver, pepper []byte) MachineBearerResolver
func NewPeerAwareBearerResolver ¶ added in v0.150.1
func NewPeerAwareBearerResolver(inner MachineBearerResolver, trust PeerTrustResolver) MachineBearerResolver
NewPeerAwareBearerResolver returns a MachineBearerResolver that refuses a blocked peer's credential after delegating ordinary resolution to inner. A nil trust resolver makes this a pass-through, so a caller that has not wired peer storage yet (or a hosted deployment that never will) keeps exactly today's behaviour.
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 ¶
func (service MachineEnrollmentService) Enroll(ctx context.Context, viewer Viewer, request MachineEnrollmentRequest) (MachineEnrollmentResponse, error)
type MachineIndex ¶ added in v0.150.1
MachineIndex answers whether any credential — peer or not — already exists for a MachineID, without exposing the credential body. Invite uses it to refuse a name already held by a non-peer credential (peer-connectivity#req:invite-and-join): the peer trust store alone cannot tell "no peer" from "an enrolled machine that keeps its name".
func NewMachineIndex ¶ added in v0.150.1
func NewMachineIndex(backend githubapp.DocumentStore) MachineIndex
NewMachineIndex wires MachineIndex to the same enrollment collection machineCredentialStore writes, without exposing RotateMachineCredential or ResolveMachineCredential to a caller that only needs "does this name already have a credential".
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" // ScopePeerSession is the sole scope a peer credential carries // (peer-connectivity#req:invite-and-join). A peer token is deliberately // refused by every route gated on the enrollment scopes above, so the // session it opens can never become a second consumer of its own queue. ScopePeerSession MachineScope = "peer:session" )
type MachineSnapshotService ¶
type MachineSnapshotService struct {
Store MachineSnapshotStore
Now func() time.Time
}
func (MachineSnapshotService) List ¶
func (service MachineSnapshotService) List(ctx context.Context, machine Machine) (machinesnapshot.ListResponse, error)
func (MachineSnapshotService) Publish ¶
func (service MachineSnapshotService) Publish(ctx context.Context, machine Machine, snapshot machinesnapshot.Snapshot) (machinesnapshot.Receipt, error)
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 ¶
func DigestMachineToken ¶
func DigestMachineToken(token string, pepper []byte) (MachineTokenDigest, error)
type PeerAdminService ¶ added in v0.150.1
type PeerAdminService struct {
// Credentials, Index, Trust and Stats are the seams Block, Unblock,
// Disconnect, resolvePeer and RefuseIfPeerNameCollision use. Invite does
// not use Credentials: see Backend.
Credentials MachineCredentialStore
Index MachineIndex
Trust PeerTrustStore
Stats PeerStatsStore
// Backend is the raw document store Invite writes the credential, trust
// and statistics documents against inside one UpdateAtomic transaction,
// so a partial invite (a credential with no matching trust document, or
// the reverse) can never be observed. DocumentStore's contract promises
// serializable transaction semantics, so two concurrent invites of the
// same name always resolve to exactly one winner with a working token;
// the loser's writes (including its own randomly minted token) are
// discarded by the retried transaction rather than partially applied,
// and it observes ErrPeerExists-shaped refusal instead.
Backend githubapp.DocumentStore
Pepper []byte
Random io.Reader
Now func() time.Time
// LocalIdentityID is the fixed self-hosted identity every peer credential
// is minted under (hub.MachineID's first argument), matching
// ensureLocalEnrollment's "local".
LocalIdentityID string
// HubMachineName is the hub's own machine name. invite-and-join refuses
// it outright: the hub is never its own peer.
HubMachineName string
// MemoryEngine is true when the hub's store engine is "memory". Invite
// refuses admission in that case, per invite-and-join: a peer credential
// minted against a store that discards on exit could never be reasoned
// about as durable state.
MemoryEngine bool
}
PeerAdminService implements every admission and trust change peer-connectivity#req:admin-requires-owner-credential requires: invite, rotate, block, unblock and disconnect. It is deliberately not reachable from any HTTP route; cmd/wb mounts it only on the daemon's owner-token unix-socket RPC.
func (*PeerAdminService) Block ¶ added in v0.150.1
func (service *PeerAdminService) Block(ctx context.Context, nameOrID string) (PeerRecord, error)
Block closes the live session (a no-op until Task 2) and refuses future sessions and HTTP calls with that credential. The record, cursor and lifetime counters are unchanged.
func (*PeerAdminService) Disconnect ¶ added in v0.150.1
func (service *PeerAdminService) Disconnect(ctx context.Context, nameOrID string) (PeerDisconnectResult, error)
Disconnect closes the live session only; the peer stays trusted and reconnects by itself. Task 2 adds the session registry this needs; until then it answers "no live session" for any known peer, and an error for an unknown one.
func (*PeerAdminService) Invite ¶ added in v0.150.1
func (service *PeerAdminService) Invite(ctx context.Context, name string, rotate bool) (PeerInviteResult, error)
Invite mints a peer credential and its trust/statistics records. name must not already belong to the hub's own machine, an existing non-peer credential, or an existing peer unless rotate is true. The credential, trust and statistics documents are written inside one atomic transaction: see Backend's doc comment for why.
func (*PeerAdminService) RefuseIfPeerNameCollision ¶ added in v0.150.1
func (service *PeerAdminService) RefuseIfPeerNameCollision(ctx context.Context, name string) error
RefuseIfPeerNameCollision reports whether name is off-limits to the owner RPC's plain (non-peer) "enroll" route: the hub's own machine name, or a name already held by a peer trust document. It does not apply to ensureLocalEnrollment's own self-bootstrap, which enrolls the hub's own machine name under the "local" identity by design and must keep doing so.
func (*PeerAdminService) Unblock ¶ added in v0.150.1
func (service *PeerAdminService) Unblock(ctx context.Context, nameOrID string) (PeerRecord, error)
Unblock allows sessions again and sets reset_pending, so the peer's next redial reconciles from the hub's heads instead of resuming a cursor that may have gone stale while it was refused.
type PeerDisconnectResult ¶ added in v0.150.1
type PeerDisconnectResult struct {
Peer PeerRecord
Disconnected bool
Message string
}
PeerDisconnectResult is what `wb peers disconnect` reports. Until Task 2 adds the live-session registry, there is never a live session to close, so Disconnected is always false and Message says so plainly.
type PeerInviteResult ¶ added in v0.150.1
type PeerInviteResult struct {
PeerID string
Name string
Token string
CreatedAt time.Time
Rotated bool
}
PeerInviteResult is what `wb peers invite` and the owner RPC return: the token is present exactly once, here, and never retrievable again.
type PeerRecord ¶ added in v0.150.1
type PeerRecord struct {
MachineID string `firestore:"machine_id"`
Name string `firestore:"name"`
IdentityID string `firestore:"identity_id"`
NodeID string `firestore:"node_id,omitempty"`
Trust PeerTrust `firestore:"trust"`
CreatedAt time.Time `firestore:"created_at"`
TrustChangedAt time.Time `firestore:"trust_changed_at"`
ResetPending bool `firestore:"reset_pending"`
}
PeerRecord is the trust document: display name and owning identity, the bound node ID (empty until Task 2 binds it), trust state, created and trust-changed times, and reset_pending. Only admin operations (hub.PeerAdminService) write it, as transactional field merges.
type PeerStats ¶ added in v0.150.1
type PeerStats struct {
MachineID string `firestore:"machine_id"`
LastSeenAt time.Time `firestore:"last_seen_at,omitempty"`
LastConnectedAt time.Time `firestore:"last_connected_at,omitempty"`
WBVersion string `firestore:"wb_version,omitempty"`
OS string `firestore:"os,omitempty"`
Arch string `firestore:"arch,omitempty"`
Protocol int `firestore:"protocol,omitempty"`
RXPayloadBytes uint64 `firestore:"rx_payload_bytes"`
TXPayloadBytes uint64 `firestore:"tx_payload_bytes"`
RXMessages uint64 `firestore:"rx_messages"`
TXMessages uint64 `firestore:"tx_messages"`
RXEvents uint64 `firestore:"rx_events"`
TXEvents uint64 `firestore:"tx_events"`
Connections uint64 `firestore:"connections"`
ConnectedSeconds uint64 `firestore:"connected_seconds"`
}
PeerStats is the statistics document: reported version/platform/protocol and lifetime counters. Every counter is zero until Task 6 fills it; this task only creates the document at invite time, so later writers have somewhere to merge into.
type PeerStatsStore ¶ added in v0.150.1
type PeerStatsStore interface {
// CreateStats writes a new, all-zero statistics document for machineID.
// It is idempotent: an existing document is left untouched rather than
// reset, so a later writer's progress survives a repeated invite.
CreateStats(context.Context, string) error
// GetStats reads the statistics document. found is false when none
// exists yet.
GetStats(context.Context, string) (stats PeerStats, found bool, err error)
}
PeerStatsStore is the statistics document's persistence seam.
type PeerTrust ¶ added in v0.150.1
type PeerTrust string
PeerTrust is the one bit of trust state a peer record carries: active peers are admitted, blocked peers are refused everywhere a credential is resolved.
type PeerTrustResolver ¶ added in v0.150.1
type PeerTrustResolver interface {
GetPeer(context.Context, string) (record PeerRecord, found bool, err error)
}
PeerTrustResolver is the read the peer-aware bearer resolver needs: does a peer record exist for this MachineID, and is it blocked. It is the narrow slice of PeerTrustStore that authentication depends on.
type PeerTrustStore ¶ added in v0.150.1
type PeerTrustStore interface {
// CreatePeer writes a new trust document. It fails with ErrPeerExists if
// one already exists for record.MachineID.
CreatePeer(context.Context, PeerRecord) error
// GetPeer reads the trust document by MachineID. found is false, with a
// nil error, when no peer record exists — the credential is not a peer.
GetPeer(context.Context, string) (record PeerRecord, found bool, err error)
// FindPeerByName reads the trust document by display name, for `<peer>`
// arguments and invite's existing-name refusal.
FindPeerByName(context.Context, string) (record PeerRecord, found bool, err error)
// ListPeers returns every peer trust document, for `wb peers list`.
ListPeers(context.Context) ([]PeerRecord, error)
// UpdateTrust reads the current record inside a transaction, applies
// mutate, and writes the result back — the "transactional field merge"
// every admin write is. mutate must not change MachineID, Name,
// IdentityID, or CreatedAt; it returns ErrPeerNotFound when machineID has
// no trust document.
UpdateTrust(ctx context.Context, machineID string, mutate func(*PeerRecord)) (PeerRecord, error)
}
PeerTrustStore is the trust document's persistence seam, backed by the same DocumentStore port every other hub store uses.
type PendingRefresh ¶
type PollObservation ¶ added in v0.124.6
type PollObservation struct {
// Repository is the canonical `github.com/owner/repository` this
// observation is keyed by, lowercased the way machine snapshots are.
Repository string `firestore:"repository"`
// FullName is `owner/repository` exactly as GitHub reported it, so a
// rename is detected against what the API said rather than against a
// normalisation of it.
FullName string `firestore:"full_name"`
// DefaultBranch is the branch name GitHub reported, without a ref prefix.
DefaultBranch string `firestore:"default_branch"`
// SHA is the object ID at the head of DefaultBranch.
SHA string `firestore:"sha"`
// ObservedAt is when the poller read those values.
ObservedAt time.Time `firestore:"observed_at"`
}
PollObservation is the last state the polling ingester saw for one repository. It is the poller's entire memory: without it every restart would re-enqueue the current head as though it had just been pushed.
Only the three fields the poller compares are kept. Nothing here is a secret: a repository's name, its default branch, and the object ID at that branch's head are all already part of the repository-event contract.
type PollObservationStore ¶ added in v0.124.6
type PollObservationStore interface {
// LoadPollObservation returns the stored observation for a repository.
// The boolean is false when the poller has never seen it, which is the
// signal to record without enqueueing.
LoadPollObservation(context.Context, string) (PollObservation, bool, error)
// SavePollObservation replaces the observation for observation.Repository.
SavePollObservation(context.Context, PollObservation) error
// DeletePollObservation drops the observation a rename re-keyed away
// from. Deleting one that is not there is not an error.
DeletePollObservation(context.Context, string) error
}
PollObservationStore persists what the poller last saw, keyed by the canonical repository name.
func NewPollObservationStore ¶ added in v0.124.6
func NewPollObservationStore(backend githubapp.DocumentStore) PollObservationStore
NewPollObservationStore binds the poller's memory to a host's githubapp.DocumentStore, the same seam every other hub-owned store writes through.
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 QueuedWorkStore ¶ added in v0.150.1
type QueuedWorkStore interface {
// QueuedWork reads workbench_repository_event_queues/<machineID>'s
// queued_work field. found is false when the machine has no queue-state
// document yet — LAG then shows "-", not zero, since "no document" and
// "a document reporting zero" are different facts once Task 3 starts
// writing it.
QueuedWork(ctx context.Context, machineID string) (count int64, found bool, err error)
}
QueuedWorkStore is the narrow read the peers read model needs: how much queued work a machine's queue holds, for `wb peers list`'s LAG column (peer-connectivity#req:peer-is-persistent-state, #req:peers-api).
func NewQueuedWorkStore ¶ added in v0.150.1
func NewQueuedWorkStore(backend githubapp.DocumentStore) QueuedWorkStore
NewQueuedWorkStore wires QueuedWorkStore to the same backend document store repositoryEventStore itself uses, reading the exact per-machine queue-state document Acknowledge above reads and writes.
type RateLimit ¶ added in v0.147.0
RateLimit is what GitHub's X-RateLimit-* response headers said about the remaining budget and its next reset. Known is false when neither header was present or parseable, which every caller treats as "nothing to act on" rather than a zero budget.
This is the one place that parsing exists: hub/poller and hub/redeliver both read GitHub REST responses and both need it, and a second copy would be exactly the kind of drift a shared seam is for.
func ReadRateLimit ¶ added in v0.147.0
ReadRateLimit parses the X-RateLimit-Remaining and X-RateLimit-Reset response headers GitHub's REST API sends on almost every response.
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
// Narrate receives one line for every delivery the service decides about:
// queued, ignored, dropped as a duplicate, or applied as a lifecycle
// change. A delivery the service returns an error for is narrated by the
// HTTP handler instead, so every delivery produces exactly one line.
// Optional; the hosted instance leaves it unset.
Narrate func(narrate.Line)
}
func (RepositoryEventService) Acknowledge ¶
func (service RepositoryEventService) Acknowledge(ctx context.Context, machine Machine, request repositoryevent.AckRequest) (repositoryevent.AckResponse, error)
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 RepositoryEventStore ¶
type RepositoryEventStore interface {
EnqueueForMachines(context.Context, repositoryevent.Event, []Machine) (EnqueueResult, error)
Poll(context.Context, Machine, string, int) (repositoryevent.PollResponse, error)
Acknowledge(context.Context, Machine, repositoryevent.AckRequest) (repositoryevent.AckResponse, error)
}
type StatusConnection ¶
type StatusDelivery ¶
type StatusDelivery struct {
LastReceived *StatusDeliveryMarker `json:"last_received,omitempty"`
LastAcknowledged *StatusDeliveryMarker `json:"last_acknowledged,omitempty"`
}
type StatusDeliveryMarker ¶
type StatusError ¶
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 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 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 WebhookDelivery ¶
type WebhookRedeliveryRecord ¶ added in v0.147.0
type WebhookRedeliveryRecord struct {
// GUID is the delivery GUID GitHub keeps stable across every attempt and
// every redelivery of one logical delivery.
GUID string `firestore:"guid"`
// Attempts is how many times this hub has asked GitHub to redeliver this
// GUID. It never exceeds MaxAttempts in the redeliver package.
Attempts int `firestore:"attempts"`
// Abandoned is set once Attempts has been exhausted with the delivery
// still failing. An abandoned GUID is never retried again.
Abandoned bool `firestore:"abandoned"`
// LastAttemptAt is when this record was last written, whether by a
// redelivery or by being marked abandoned. It is the retention clock.
LastAttemptAt time.Time `firestore:"last_attempt_at"`
// LastAttemptDeliveryID is the GitHub delivery attempt id that was this
// GUID's latest listed attempt the last time this hub acted on it. A
// later sweep counts a redelivery attempt against this GUID only when
// its now-latest listed attempt was answered and carries a different id
// than this one: an id comparison, not a timestamp comparison, because
// GitHub's delivered_at has whole-second resolution on GitHub's own
// clock while this hub's own attempt time is local and sub-second, so a
// fast redeliver round trip routinely lands in the same GitHub-clock
// second as the request that caused it.
LastAttemptDeliveryID int64 `firestore:"last_attempt_delivery_id"`
// FirstDeliveredAt is the delivered_at of this GUID's earliest attempt
// this hub has seen, set once when the record is first created and never
// overwritten after. Because every uncounted redelivery creates a new
// attempt with a fresh delivered_at, the GUID's own latest attempt never
// ages out of the App API's listing on its own; the 72-hour bound in
// spec/features/peer-connectivity/README.md is measured from this field,
// not from LastAttemptAt, so an unreachable endpoint cannot keep a GUID
// alive forever.
FirstDeliveredAt time.Time `firestore:"first_delivered_at"`
}
WebhookRedeliveryRecord is what the hub remembers about one App webhook delivery GUID across missed-webhook recovery sweeps: how many times this hub has already asked GitHub to redeliver it, and whether it has given up. Nothing here is a secret or a payload: a GUID, a small counter and a timestamp are all a redelivery decision needs.
type WebhookRedeliveryStore ¶ added in v0.147.0
type WebhookRedeliveryStore interface {
// LoadWebhookRedelivery returns the stored record for a GUID. The boolean
// is false when the sweep has never acted on it, which is the signal to
// treat it as zero prior attempts.
LoadWebhookRedelivery(ctx context.Context, guid string) (WebhookRedeliveryRecord, bool, error)
// SaveWebhookRedelivery replaces the record for record.GUID.
SaveWebhookRedelivery(ctx context.Context, record WebhookRedeliveryRecord) error
// ListWebhookRedeliveries returns every stored record, for the retention
// sweep to filter by age.
ListWebhookRedeliveries(ctx context.Context) ([]WebhookRedeliveryRecord, error)
// DeleteWebhookRedeliveries removes the named GUIDs' records. Deleting a
// GUID that is not there is not an error. Callers must keep each call
// within the store's transaction write bound.
DeleteWebhookRedeliveries(ctx context.Context, guids []string) error
}
WebhookRedeliveryStore persists the missed-webhook recovery sweep's memory, keyed by GitHub's delivery GUID, with the 7-day retention the feature specifies.
func NewWebhookRedeliveryStore ¶ added in v0.147.0
func NewWebhookRedeliveryStore(backend githubapp.DocumentStore) WebhookRedeliveryStore
NewWebhookRedeliveryStore binds the missed-webhook recovery sweep's memory to a host's githubapp.DocumentStore, the same seam every other hub-owned store writes through.
Source Files
¶
- collections.go
- github_app.go
- github_oauth.go
- github_rate_limit.go
- http.go
- installation.go
- installation_store.go
- machine_credential_store.go
- machine_enrollment.go
- machine_snapshot.go
- machine_snapshot_store.go
- model.go
- peer_admin.go
- peer_store.go
- poll_observation_store.go
- repository_event_store.go
- repository_events.go
- status.go
- webhook_redelivery_store.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package narrate renders the one console line the hub writes for every event it handles, whether the event arrived by webhook or was produced by the poller.
|
Package narrate renders the one console line the hub writes for every event it handles, whether the event arrived by webhook or was produced by the poller. |
|
Package poller is the ingester a self-hosted bench needs by default.
|
Package poller is the ingester a self-hosted bench needs by default. |
|
Package redeliver recovers GitHub App webhook deliveries the hub's own downtime lost.
|
Package redeliver recovers GitHub App webhook deliveries the hub's own downtime lost. |
|
Package web embeds the built bench dashboard so a self-hosted hub serves the same pages the hosted instance does, with no Node runtime and no configuration.
|
Package web embeds the built bench dashboard so a self-hosted hub serves the same pages the hosted instance does, with no Node runtime and no configuration. |