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}