Skip to main content

GithubApp

Struct GithubApp 

Source
pub struct GithubApp { /* private fields */ }
Expand description

GitHub App auth over the HttpClient and Clock ports.

The private_key_pem is optional on purpose: a deployment without a key (a preview environment, a test) constructs the client and every operation that needs the key — GithubApp::app_jwt, GithubApp::installation_token, GithubApp::request, GithubApp::paginate — answers GithubAppError::NotConfigured without touching the network, so a venture can wire it unconditionally and let the absence decide. The user-to-server pair, GithubApp::exchange_user_code and GithubApp::user_can_access_installation, carries the caller’s own credential and runs without one.

Implementations§

Source§

impl GithubApp

Source

pub fn new( app_id: u64, private_key_pem: Option<String>, http: Arc<dyn HttpClient>, clock: Arc<dyn Clock>, api_base: impl Into<String>, ) -> Self

A client for app_id, signing with private_key_pem (PKCS#1 or PKCS#8 PEM; None or empty means “not configured”). api_base is the REST root — pass DEFAULT_API_BASE unless targeting GitHub Enterprise Server or a fake in tests. The OAuth endpoint’s host defaults to DEFAULT_WEB_BASE; override it with GithubApp::with_web_base.

Source

pub fn with_web_base(self, web_base: impl Into<String>) -> Self

Points the user-to-server OAuth exchange at another web host — GitHub Enterprise Server, or a fake in tests — instead of https://github.com.

Source

pub fn app_id(&self) -> u64

The App ID this client signs for.

Source

pub fn is_configured(&self) -> bool

Whether a usable private key was supplied.

Source

pub fn app_jwt(&self) -> Result<String, GithubAppError>

A signed app JWT: header {"alg":"RS256","typ":"JWT"}, claims iat = now − 60s, exp = now + 540s (nine minutes, inside GitHub’s ten-minute cap), and iss = the App ID as a string.

§Errors

GithubAppError::NotConfigured when no key was supplied, GithubAppError::InvalidKey when the key did not parse. Neither message contains key material.

Source

pub async fn installation_token( &self, installation_id: u64, permissions: Option<&BTreeMap<String, String>>, repositories: Option<&[String]>, ) -> Result<InstallationToken, GithubAppError>

An installation token, minted once and cached per installation and scope.

permissions and repositories narrow the token the way GitHub’s POST /app/installations/{id}/access_tokens does; both are omitted from the request body when None, and the pair forms the cache key, so a token scoped to one repository is never handed to a call scoped to another. Some of an empty map or list is not None: GitHub reads the first as “grant nothing” and the second as “grant everything”, and the two get separate cache entries.

Concurrent calls for the same installation and scope make one exchange for the whole client: one app-wide async mutex is held across check-cache, exchange and store, so a second caller arriving mid-flight waits and then reads the token the first one stored. A token minted already inside the five-minute safety margin (GitHub’s answer minus five minutes is not in the future) is returned but not cached — there is nothing worth storing.

§Errors

GithubAppError::NotConfigured / GithubAppError::InvalidKey before any network call; GithubAppError::Http on a transport failure; GithubAppError::Status when GitHub refuses the exchange (carrying the status and the rate-limit headers); and GithubAppError::Decode when a 2xx body cannot be read. No message or logged field contains the token or the key.

Source

pub async fn request( &self, installation_id: u64, request: Request<Bytes>, ) -> Result<GithubResponse, GithubAppError>

Sends an API request authenticated with an installation token.

The request’s URI may be absolute (https://api.github.com/repos/...) or relative (/repos/...); a relative one is resolved against the client’s API base. An absolute URI on another origin than the API base is refused with GithubAppError::ForeignOrigin — before any token is minted — whenever this method would attach the installation token: the token never leaves the API’s origin, whichever path the URL came by (a caller-built one, GithubApp::paginate’s first page, a URL read out of a payload). Authorization, Accept, X-GitHub-Api-Version and User-Agent are set unless the caller already set them — so a caller can pass their own credential and this method will not overwrite it (and will not mint an installation token at all).

A 401 on a request this method authenticated means the installation token went stale: the cached one is dropped, a fresh one is minted, and the request is replayed exactly once. A second 401 (or any other non-2xx, 304 included) is returned as an ordinary GithubResponse for the caller to read — this method does not classify those.

§Errors

As GithubApp::installation_token, plus GithubAppError::InvalidHeader if a token cannot be put on the wire as a header value, and GithubAppError::ForeignOrigin for an off-origin URI this method would have authenticated.

Source

pub async fn paginate( &self, installation_id: u64, first_url: impl Into<String>, max_pages: usize, ) -> Result<Vec<GithubResponse>, GithubAppError>

Follows Link: …; rel="next" from first_url, returning at most max_pages responses. The walk stops when a page carries no next link or the cap is reached; every page after the first is requested through GithubApp::request, so it carries the same installation token and the same 401 replay.

A next link is followed only when it is on api_base’s origin (scheme, host and port). A cross-origin link ends the walk instead: the next page would be fetched with the installation token in Authorization, and a Link header is the one part of a response an upstream gets to choose.

§Errors

As GithubApp::request — so a first_url on another origin is GithubAppError::ForeignOrigin.

Source

pub async fn exchange_user_code( &self, client_id: &str, client_secret: &str, code: &str, redirect_uri: Option<&str>, ) -> Result<UserToken, GithubAppError>

Exchanges a user-to-server authorization code for a user access token (ghu_…).

GitHub answers 200 with {"error": …} for both success and failure, so the body decides: an error field becomes GithubAppError::OAuth carrying only GitHub’s error code, never the body — which would echo the client secret back. A non-success status without an error field is a plain GithubAppError::Status, whether or not the body is JSON (an HTML error page must not be mistaken for a decode failure), and a successful response whose body is not JSON is GithubAppError::Decode.

This flow authenticates with the client id and secret, not the app’s private key, so it works on a client constructed without a key.

§Errors

GithubAppError::Http on transport failure, GithubAppError::OAuth on a GitHub-reported error, GithubAppError::Status on a non-2xx without one, and GithubAppError::Decode when a 2xx body cannot be read.

Source

pub async fn user_can_access_installation( &self, user_token: &str, installation_id: u64, ) -> Result<bool, GithubAppError>

Whether user_token can reach installation_id, by walking GET {api_base}/user/installations?per_page=100 and looking for the id (following Link pages, up to a fixed page cap).

This is the check a venture runs before linking an installation to an account: a user token can only see the installations that user authorised.

A next link is followed only when it is on api_base’s origin. A cross-origin one ends the walk and the answer is false — the call exists to grant access, so an unverifiable walk fails closed rather than granting it.

This flow presents the caller’s own user token, not the app’s private key, so it works on a client constructed without a key.

§Errors

GithubAppError::Http on transport failure, and GithubAppError::Status / GithubAppError::Decode on a bad page.

Trait Implementations§

Source§

impl Debug for GithubApp

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Never prints the key or a token. The http, clock, cache and lock fields are omitted wholesale rather than summarised.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more