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
impl GithubApp
Sourcepub fn new(
app_id: u64,
private_key_pem: Option<String>,
http: Arc<dyn HttpClient>,
clock: Arc<dyn Clock>,
api_base: impl Into<String>,
) -> Self
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.
Sourcepub fn with_web_base(self, web_base: impl Into<String>) -> Self
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.
Sourcepub fn is_configured(&self) -> bool
pub fn is_configured(&self) -> bool
Whether a usable private key was supplied.
Sourcepub fn app_jwt(&self) -> Result<String, GithubAppError>
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.
Sourcepub async fn installation_token(
&self,
installation_id: u64,
permissions: Option<&BTreeMap<String, String>>,
repositories: Option<&[String]>,
) -> Result<InstallationToken, GithubAppError>
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.
Sourcepub async fn request(
&self,
installation_id: u64,
request: Request<Bytes>,
) -> Result<GithubResponse, GithubAppError>
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.
Sourcepub async fn paginate(
&self,
installation_id: u64,
first_url: impl Into<String>,
max_pages: usize,
) -> Result<Vec<GithubResponse>, GithubAppError>
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.
Sourcepub async fn exchange_user_code(
&self,
client_id: &str,
client_secret: &str,
code: &str,
redirect_uri: Option<&str>,
) -> Result<UserToken, GithubAppError>
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.
Sourcepub async fn user_can_access_installation(
&self,
user_token: &str,
installation_id: u64,
) -> Result<bool, GithubAppError>
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.