(** GitHub OAuth URL generation and token exchange helpers. Supports both GitHub Apps (with token expiry and refresh tokens) and traditional OAuth Apps. {2 Standards} - {{:https://datatracker.ietf.org/doc/html/rfc6749} RFC 6749} - OAuth 2.0 - {{:https://datatracker.ietf.org/doc/html/rfc7636} RFC 7636} - PKCE {2 Example} {[ (* Generate authorization URL *) let state = Github_oauth.generate_state () in let url = Github_oauth.authorization_url ~client_id:"your_client_id" ~callback_url:"https://yourapp.com/callback" ~state ~scope:[ "repo" ] in (* After user authorizes, exchange code for token *) let body = Github_oauth.exchange_request_body ~client_id:"your_client_id" ~client_secret:"your_secret" ~code ~redirect_uri:"https://yourapp.com/callback" in (* POST body to Github_oauth.access_token_url with Accept: application/json *) ]} *) (** {1 State Generation} *) val generate_state : unit -> string (** [generate_state ()] generates a cryptographically secure random state for CSRF protection. Returns a 64-character lowercase hex string (32 random bytes). @raise Crypto_rng.Unseeded_generator if RNG not initialized. *) (** {1 Authorization URL} *) val authorization_url : client_id:string -> callback_url:string -> state:string -> scope:string list -> string (** [authorization_url ~client_id ~callback_url ~state ~scope] generates a GitHub OAuth authorization URL. @param client_id Your GitHub App or OAuth App client ID @param callback_url URL GitHub will redirect to after authorization @param state Random state for CSRF protection (from {!generate_state}) @param scope List of requested scopes. Use empty list for GitHub Apps (permissions configured in app settings). Common scopes: ["repo"], ["user"], ["read:org"]. *) (** {1 Token Exchange} *) val access_token_url : string (** [access_token_url] is the GitHub access token exchange endpoint: [https://github.com/login/oauth/access_token]. *) val exchange_request_body : client_id:string -> client_secret:string -> code:string -> redirect_uri:string -> string (** [exchange_request_body ~client_id ~client_secret ~code ~redirect_uri] generates a JSON request body for exchanging an authorization code for an access token. POST this to {!access_token_url} with headers: - [Content-Type: application/json] - [Accept: application/json]. *) (** {1 Token Response} *) type token_response = { access_token : string; (** The access token *) expires_in : int option; (** Seconds until expiry. [Some 28800] for GitHub Apps, [None] for OAuth Apps *) refresh_token : string option; (** Refresh token. [Some _] for GitHub Apps, [None] for OAuth Apps *) refresh_token_expires_in : int option; (** Seconds until refresh token expiry (~6 months for GitHub Apps) *) } (** Token response structure supporting both GitHub Apps and OAuth Apps. *) type parse_token_error = | Invalid_json (** JSON parsing failed *) | Missing_access_token (** Required access_token field missing *) | Invalid_token_format (** Token fields have unexpected format *) val parse_token_response : string -> (token_response, parse_token_error) result (** [parse_token_response body] parses a GitHub token response JSON. Supports both GitHub App responses (with expiry and refresh tokens) and OAuth App responses (access token only). *) val pp_parse_token_error : Format.formatter -> parse_token_error -> unit (** Pretty-printer for parse errors. *) (** {1 Token Refresh} *) val refresh_request_body : client_id:string -> client_secret:string -> refresh_token:string -> string (** [refresh_request_body ~client_id ~client_secret ~refresh_token] generates a JSON request body for refreshing a GitHub App access token. POST this to {!access_token_url} with the same headers as token exchange. *)