Skip to main content

auths_core/ports/
platform.rs

1//! Platform claim port traits for OAuth device flow, proof publishing, and registry submission.
2
3use std::future::Future;
4use std::time::Duration;
5
6use thiserror::Error;
7
8use crate::ports::network::NetworkError;
9
10/// Errors from platform identity claim operations (OAuth, proof publishing, registry submission).
11///
12/// Usage:
13/// ```ignore
14/// match result {
15///     Err(PlatformError::AccessDenied) => { /* user denied */ }
16///     Err(PlatformError::ExpiredToken) => { /* device code expired, restart flow */ }
17///     Err(PlatformError::Network(e)) => { /* retry */ }
18///     Err(e) => return Err(e.into()),
19///     Ok(response) => { /* proceed */ }
20/// }
21/// ```
22#[derive(Debug, Error)]
23#[non_exhaustive]
24pub enum PlatformError {
25    /// OAuth authorization is pending — the user has not yet approved.
26    #[error("OAuth authorization pending")]
27    AuthorizationPending,
28    /// OAuth server requested slower polling.
29    #[error("OAuth slow down")]
30    SlowDown,
31    /// The user explicitly denied the OAuth authorization request.
32    #[error("OAuth access denied")]
33    AccessDenied,
34    /// The device code has expired; the flow must be restarted.
35    #[error("device code expired")]
36    ExpiredToken,
37    /// A network-level error occurred.
38    #[error("network error: {0}")]
39    Network(#[source] NetworkError),
40    /// A platform-specific error with a human-readable message.
41    #[error("platform error: {message}")]
42    Platform {
43        /// Human-readable error detail from the platform API.
44        message: String,
45    },
46}
47
48impl auths_crypto::AuthsErrorInfo for PlatformError {
49    fn error_code(&self) -> &'static str {
50        match self {
51            Self::AuthorizationPending => "AUTHS-E3801",
52            Self::SlowDown => "AUTHS-E3802",
53            Self::AccessDenied => "AUTHS-E3803",
54            Self::ExpiredToken => "AUTHS-E3804",
55            Self::Network(_) => "AUTHS-E3805",
56            Self::Platform { .. } => "AUTHS-E3806",
57        }
58    }
59
60    fn suggestion(&self) -> Option<&'static str> {
61        match self {
62            Self::AccessDenied => Some("Re-run the command and approve the authorization request"),
63            Self::ExpiredToken => Some("The device code expired — restart the flow"),
64            Self::Network(_) => Some("Check your internet connection"),
65            Self::AuthorizationPending => Some(
66                "Complete the authorization on the linked device, then the CLI will continue automatically",
67            ),
68            Self::SlowDown => {
69                Some("The authorization server is rate-limiting; the CLI will retry automatically")
70            }
71            Self::Platform { .. } => {
72                Some("A platform-specific error occurred; run `auths doctor` to diagnose")
73            }
74        }
75    }
76}
77
78/// OAuth 2.0 device authorization grant response (RFC 8628 §3.2).
79///
80/// Returned by [`OAuthDeviceFlowProvider::request_device_code`].
81/// The CLI displays `user_code` + `verification_uri` to the user,
82/// then polls with `device_code`.
83pub struct DeviceCodeResponse {
84    /// Opaque device verification code used to poll for the token.
85    pub device_code: String,
86    /// Short user-facing code to enter at `verification_uri`.
87    pub user_code: String,
88    /// URL where the user enters `user_code`.
89    pub verification_uri: String,
90    /// Duration in seconds until expiration (per RFC 6749).
91    pub expires_in: u64,
92    /// Minimum polling interval in seconds.
93    pub interval: u64,
94}
95
96/// Authenticated platform user profile returned after token exchange.
97///
98/// Returned by [`OAuthDeviceFlowProvider::fetch_user_profile`].
99pub struct PlatformUserProfile {
100    /// Platform login / username.
101    pub login: String,
102    /// Display name (optional).
103    pub name: Option<String>,
104}
105
106/// Response from the registry after submitting a platform claim.
107///
108/// Returned by [`RegistryClaimClient::submit_claim`].
109pub struct ClaimResponse {
110    /// Human-readable confirmation message from the registry.
111    pub message: String,
112}
113
114/// Two-phase OAuth 2.0 device authorization flow (RFC 8628).
115///
116/// The CLI calls [`request_device_code`](Self::request_device_code), displays the
117/// `user_code` and `verification_uri`, optionally opens a browser, then calls
118/// [`poll_for_token`](Self::poll_for_token). All presentation logic stays in the CLI.
119///
120/// Usage:
121/// ```ignore
122/// let resp = provider.request_device_code(CLIENT_ID, "read:user gist").await?;
123/// // CLI: display resp.user_code, open resp.verification_uri
124/// let token = provider.poll_for_token(CLIENT_ID, &resp.device_code, Duration::from_secs(resp.interval), expires_at).await?;
125/// let profile = provider.fetch_user_profile(&token).await?;
126/// ```
127pub trait OAuthDeviceFlowProvider: Send + Sync {
128    /// Request a device code to begin the device authorization flow.
129    ///
130    /// Args:
131    /// * `client_id`: OAuth application client ID.
132    /// * `scopes`: Space-separated OAuth scopes to request.
133    fn request_device_code(
134        &self,
135        client_id: &str,
136        scopes: &str,
137    ) -> impl Future<Output = Result<DeviceCodeResponse, PlatformError>> + Send;
138
139    /// Poll for the access token until granted, denied, or the lifetime elapses.
140    ///
141    /// Args:
142    /// * `client_id`: OAuth application client ID.
143    /// * `device_code`: Device code from [`request_device_code`](Self::request_device_code).
144    /// * `interval`: Minimum time between poll attempts.
145    /// * `expires_in`: Total lifetime of the device code (stop polling after this elapses).
146    fn poll_for_token(
147        &self,
148        client_id: &str,
149        device_code: &str,
150        interval: Duration,
151        expires_in: Duration,
152    ) -> impl Future<Output = Result<String, PlatformError>> + Send;
153
154    /// Fetch the authenticated user's profile using the access token.
155    ///
156    /// Args:
157    /// * `access_token`: OAuth access token from [`poll_for_token`](Self::poll_for_token).
158    fn fetch_user_profile(
159        &self,
160        access_token: &str,
161    ) -> impl Future<Output = Result<PlatformUserProfile, PlatformError>> + Send;
162}
163
164/// Publish a signed platform claim as a publicly readable proof artifact.
165///
166/// For GitHub: publishes a Gist. Returns the URL of the published proof.
167///
168/// Usage:
169/// ```ignore
170/// let proof_url = publisher.publish_proof(&access_token, &claim_json).await?;
171/// registry_client.submit_claim(registry, &did, &proof_url).await?;
172/// ```
173pub trait PlatformProofPublisher: Send + Sync {
174    /// Publish the claim JSON as a proof artifact and return its public URL.
175    ///
176    /// Args:
177    /// * `access_token`: OAuth access token with write permission to publish.
178    /// * `claim_json`: Canonicalized, signed JSON claim to publish.
179    fn publish_proof(
180        &self,
181        access_token: &str,
182        claim_json: &str,
183    ) -> impl Future<Output = Result<String, PlatformError>> + Send;
184}
185
186/// Submit a published platform claim to the auths registry for verification.
187///
188/// Usage:
189/// ```ignore
190/// let response = registry_client.submit_claim(registry_url, &did, &proof_url).await?;
191/// println!("{}", response.message);
192/// ```
193pub trait RegistryClaimClient: Send + Sync {
194    /// Submit a platform identity claim to the registry.
195    ///
196    /// Args:
197    /// * `registry_url`: Base URL of the auths registry.
198    /// * `did`: DID of the identity making the claim.
199    /// * `proof_url`: Public URL of the published proof artifact.
200    fn submit_claim(
201        &self,
202        registry_url: &str,
203        did: &str,
204        proof_url: &str,
205    ) -> impl Future<Output = Result<ClaimResponse, PlatformError>> + Send;
206}