uarp_sdk/generated/api/auth.rs
1// Code generated by @uarp/codegen from spec/openapi.json. DO NOT EDIT.
2//!
3//! User registration and OTP authentication
4
5#![allow(unused_imports, clippy::too_many_arguments)]
6
7use reqwest::Method;
8use serde::{Deserialize, Serialize};
9
10use crate::client::{Client, Request, NO_BODY, NO_QUERY};
11use crate::error::Result;
12use crate::generated::models;
13use crate::multipart::{field_text, FilePart};
14use crate::util::encode_path;
15
16/// Query and header parameters for `completeOAuthLogin`.
17#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
18pub struct CompleteOAuthLoginParams {
19 pub code: String,
20 pub state: String,
21 /// Provider-side error (e.g. `access_denied`). When present, server returns 400.
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub error: Option<String>,
24}
25
26/// Query and header parameters for `startOAuthLogin`.
27#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
28pub struct StartOAuthLoginParams {
29 /// Where to redirect after callback. Same-origin paths, Universal-Link hosts (default
30 /// `snaga.ai/auth/callback`), and `snaga://` allowed. Anything else is silently dropped.
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub return_to: Option<String>,
33 /// Human-readable device name surfaced on the minted api_key.
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub device_label: Option<String>,
36}
37
38/// User registration and OTP authentication
39#[derive(Debug, Clone)]
40pub struct AuthApi {
41 pub(crate) client: Client,
42}
43
44impl Client {
45 /// User registration and OTP authentication
46 pub fn auth(&self) -> AuthApi {
47 AuthApi { client: self.clone() }
48 }
49}
50
51impl AuthApi {
52 /// Sign in with Apple — native iOS handshake
53 ///
54 /// iOS drives the Apple authorization sheet via `ASAuthorizationController` and POSTs the
55 /// resulting `identity_token` (Apple-signed JWT, RS256) here. Server fetches Apple JWKS (cached
56 /// 24h), verifies signature/iss/aud/exp, extracts `sub` + `email`, then mints a session. The
57 /// `email` is only present on first login — subsequent logins reuse the stored
58 /// `apple_sub_to_email` mapping. `is_private_email` (`@privaterelay.appleid.com`) is honoured:
59 /// relay address is stored as the canonical email.
60 ///
61 /// `POST /api/v1/auth/oauth/apple/native`
62 pub async fn apple_native_auth(&self, body: &models::AppleNativeAuthRequest) -> Result<models::AppleNativeAuthResponse> {
63 self.client
64 .request_json(Request {
65 method: Method::POST,
66 path: "/api/v1/auth/oauth/apple/native".to_string(),
67 query: NO_QUERY,
68 body: Some(body),
69 headers: Vec::new(),
70 idempotent: true,
71 })
72 .await
73 }
74
75 /// OAuth callback — exchange code, mint session
76 ///
77 /// Provider redirects here. Server validates state (one-shot, 10 min TTL, provider-bound),
78 /// exchanges `code` for tokens, fetches verified email + IdP `sub`, then mints a session via
79 /// the shared post-verify path. On success: 302 to `${return_to}#api_key=...&email=...` if a
80 /// return_to was stored, else JSON `{api_key, email}`. Refuses to mint for super-admin email
81 /// (Guideline-style separation: super-admin must use OTP).
82 ///
83 /// `GET /api/v1/auth/oauth/{provider}/callback`
84 pub async fn complete_o_auth_login(&self, provider: &models::OAuthLoginProviderConfigStatusProvider, params: &CompleteOAuthLoginParams) -> Result<models::CompleteOAuthLoginResponse> {
85 self.client
86 .request_json(Request {
87 method: Method::GET,
88 path: format!("/api/v1/auth/oauth/{}/callback", encode_path(&provider.to_string())),
89 query: Some(params),
90 body: NO_BODY,
91 headers: Vec::new(),
92 idempotent: false,
93 })
94 .await
95 }
96
97 /// Remove MFA enrolment for the calling user
98 ///
99 /// `POST /api/v1/auth/mfa/disable`
100 pub async fn disable_mfa(&self) -> Result<models::DisableMfaResponse> {
101 self.client
102 .request_json(Request {
103 method: Method::POST,
104 path: "/api/v1/auth/mfa/disable".to_string(),
105 query: NO_QUERY,
106 body: NO_BODY,
107 headers: Vec::new(),
108 idempotent: true,
109 })
110 .await
111 }
112
113 /// Begin MFA enrolment
114 ///
115 /// Returns the TOTP secret (otpauth URL + base32 secret) and one-time recovery codes. Recovery
116 /// codes are shown ONCE and never re-readable; user must store them. Optional body: `{ label?,
117 /// issuer?, algorithm? }`.
118 ///
119 /// `POST /api/v1/auth/mfa/enrol`
120 pub async fn enrol_mfa(&self, body: &models::EnrolMfaRequest) -> Result<models::MfaEnrolment> {
121 self.client
122 .request_json(Request {
123 method: Method::POST,
124 path: "/api/v1/auth/mfa/enrol".to_string(),
125 query: NO_QUERY,
126 body: Some(body),
127 headers: Vec::new(),
128 idempotent: true,
129 })
130 .await
131 }
132
133 /// Trade a mobile hand-off code for the session
134 ///
135 /// Second half of the mobile sign-in hand-off. When `start` is called with
136 /// `app_code_challenge`, the callback redirects to the app with `#code=` instead of
137 /// `#api_key=`, and the key is only released here, to a caller that presents the matching
138 /// verifier. Single-use and short-lived: the code is consumed on the first attempt, successful
139 /// or not.
140 ///
141 /// `POST /api/v1/auth/oauth/exchange`
142 pub async fn exchange_o_auth_app_code(&self, body: &models::OAuthAppExchangeRequest) -> Result<models::OAuthAppExchangeResponse> {
143 self.client
144 .request_json(Request {
145 method: Method::POST,
146 path: "/api/v1/auth/oauth/exchange".to_string(),
147 query: NO_QUERY,
148 body: Some(body),
149 headers: Vec::new(),
150 idempotent: true,
151 })
152 .await
153 }
154
155 /// Get the calling user + tenant context
156 ///
157 /// Single source of truth for the browser to render account state, role badges, and the tenant
158 /// header. Resolves the user via api-key.user_id, then JWT `sub` claim, then
159 /// tenant_primary_email lookup.
160 ///
161 /// **Read `role` from the top level, never from `user.role`.** The top-level field is required
162 /// and always present; it resolves as `user?.role ?? extractRole(auth)`, so it answers for
163 /// every caller. `user` is nullable and NOT required — it is null whenever the caller
164 /// authenticated with an api key rather than a signed-in session, and the nested `role` exists
165 /// only in the other case.
166 ///
167 /// Spelled out because the schema alone did not prevent it: two mobile clients independently
168 /// modelled the response as `{user, tenant}`, read `user.role`, and so could not tell an owner
169 /// from a viewer for any key-authed caller — one of them had gated no settings on role at all
170 /// as a result. A client tested only against a JWT session sees `user.role` work perfectly and
171 /// ships. `scopes` and `auth_method` are required for the same reason and answer for every
172 /// caller too.
173 ///
174 /// `GET /api/v1/me`
175 pub async fn get_me(&self) -> Result<models::GetMeResponse> {
176 self.client
177 .request_json(Request {
178 method: Method::GET,
179 path: "/api/v1/me".to_string(),
180 query: NO_QUERY,
181 body: NO_BODY,
182 headers: Vec::new(),
183 idempotent: false,
184 })
185 .await
186 }
187
188 /// MFA enrolment status for the calling user
189 ///
190 /// `GET /api/v1/auth/mfa/status`
191 pub async fn get_mfa_status(&self) -> Result<models::GetMfaStatusResponse> {
192 self.client
193 .request_json(Request {
194 method: Method::GET,
195 path: "/api/v1/auth/mfa/status".to_string(),
196 query: NO_QUERY,
197 body: NO_BODY,
198 headers: Vec::new(),
199 idempotent: false,
200 })
201 .await
202 }
203
204 /// Sign in with Google — One Tap (FedCM) credential exchange
205 ///
206 /// The browser-side Google Identity Services (GSI) script renders a FedCM One Tap prompt on
207 /// `/login` and POSTs the resulting `credential` (a Google-signed JWT, RS256) here. Server
208 /// fetches Google JWKS (cached 24h), verifies signature/iss/aud/exp against the configured
209 /// Google OAuth client_id from admin KV, extracts `sub` + `email`, then mints a session via the
210 /// shared post-verify path. Same session shape the `/oauth/google/callback` redirect flow
211 /// returns.
212 ///
213 /// `POST /api/v1/auth/oauth/google/onetap`
214 pub async fn google_one_tap_auth(&self, body: &models::GoogleOneTapAuthRequest) -> Result<models::GoogleOneTapAuthResponse> {
215 self.client
216 .request_json(Request {
217 method: Method::POST,
218 path: "/api/v1/auth/oauth/google/onetap".to_string(),
219 query: NO_QUERY,
220 body: Some(body),
221 headers: Vec::new(),
222 idempotent: true,
223 })
224 .await
225 }
226
227 /// List linked OAuth identity providers + OTP fallback
228 ///
229 /// Returns one entry per supported login method (`otp`, `github`, `google`, `apple`) with
230 /// `linked: true|false`. Used by the Settings UI to render link/unlink buttons.
231 ///
232 /// `GET /api/v1/me/auth-providers`
233 pub async fn list_auth_providers(&self) -> Result<models::ListAuthProvidersResponse> {
234 self.client
235 .request_json(Request {
236 method: Method::GET,
237 path: "/api/v1/me/auth-providers".to_string(),
238 query: NO_QUERY,
239 body: NO_BODY,
240 headers: Vec::new(),
241 idempotent: false,
242 })
243 .await
244 }
245
246 /// List active api_keys (devices) for the calling user
247 ///
248 /// Returns the user's active api_keys across all devices (browser, iOS, CLI). `is_current`
249 /// flags the key making this call so UI can highlight 'this device'. Sorted by `last_used_at`
250 /// desc.
251 ///
252 /// `GET /api/v1/me/sessions`
253 pub async fn list_me_sessions(&self) -> Result<models::ListMeSessionsResponse> {
254 self.client
255 .request_json(Request {
256 method: Method::GET,
257 path: "/api/v1/me/sessions".to_string(),
258 query: NO_QUERY,
259 body: NO_BODY,
260 headers: Vec::new(),
261 idempotent: false,
262 })
263 .await
264 }
265
266 /// List OAuth identity providers (for login UI)
267 ///
268 /// Public, IP-rate-limited (60/min). Returns the list of configured OAuth login providers with
269 /// their enabled status — UI uses this to render only the buttons that will work. `apple` is
270 /// always advertised as enabled (App Store Guideline 4.8 placement requirement);
271 /// `github`/`google` enabled depends on whether super-admin has provisioned
272 /// client_id+client_secret.
273 ///
274 /// `GET /api/v1/auth/oauth/providers`
275 pub async fn list_o_auth_login_providers(&self) -> Result<models::OAuthLoginProvidersList> {
276 self.client
277 .request_json(Request {
278 method: Method::GET,
279 path: "/api/v1/auth/oauth/providers".to_string(),
280 query: NO_QUERY,
281 body: NO_BODY,
282 headers: Vec::new(),
283 idempotent: false,
284 })
285 .await
286 }
287
288 /// Revoke the calling api_key
289 ///
290 /// Revokes the api_key used to make this call. Idempotent — re-call returns 200 with
291 /// `already_revoked: true`. Cookie/JWT-only sessions return `{ok: true, key_id: null}` since
292 /// there's no server-side key to revoke.
293 ///
294 /// `POST /api/v1/auth/logout`
295 pub async fn logout(&self) -> Result<models::LogoutResponse> {
296 self.client
297 .request_json(Request {
298 method: Method::POST,
299 path: "/api/v1/auth/logout".to_string(),
300 query: NO_QUERY,
301 body: NO_BODY,
302 headers: Vec::new(),
303 idempotent: true,
304 })
305 .await
306 }
307
308 /// Mint a 60-second SSE token scoped to events:read
309 ///
310 /// Mints a short-lived (60 s) API key carrying only `events:read` scope, for use as the
311 /// `?token=` query param on browser SSE/WebSocket subscriptions which cannot set Authorization
312 /// headers. Token does not appear in the tenant key dashboard and auto-purges from KV.
313 ///
314 /// `POST /api/v1/auth/sse-tokens`
315 pub async fn mint_sse_token(&self) -> Result<models::MintSSETokenResponse> {
316 self.client
317 .request_json(Request {
318 method: Method::POST,
319 path: "/api/v1/auth/sse-tokens".to_string(),
320 query: NO_QUERY,
321 body: NO_BODY,
322 headers: Vec::new(),
323 idempotent: true,
324 })
325 .await
326 }
327
328 /// Register new user
329 ///
330 /// `POST /api/v1/register`
331 pub async fn register(&self, body: &serde_json::Map<String, serde_json::Value>) -> Result<serde_json::Map<String, serde_json::Value>> {
332 self.client
333 .request_json(Request {
334 method: Method::POST,
335 path: "/api/v1/register".to_string(),
336 query: NO_QUERY,
337 body: Some(body),
338 headers: Vec::new(),
339 idempotent: true,
340 })
341 .await
342 }
343
344 /// Request OTP code
345 ///
346 /// `POST /api/v1/auth/request-code`
347 pub async fn request_otp_code(&self, body: &serde_json::Map<String, serde_json::Value>) -> Result<serde_json::Map<String, serde_json::Value>> {
348 self.client
349 .request_json(Request {
350 method: Method::POST,
351 path: "/api/v1/auth/request-code".to_string(),
352 query: NO_QUERY,
353 body: Some(body),
354 headers: Vec::new(),
355 idempotent: true,
356 })
357 .await
358 }
359
360 /// Revoke a specific device session
361 ///
362 /// Revokes one of the user's api_keys. Refuses to revoke another user's key (403). Idempotent —
363 /// re-revoking a revoked key returns `{ok: true, already_revoked: true}`.
364 ///
365 /// `DELETE /api/v1/me/sessions/{keyId}`
366 pub async fn revoke_me_session(&self, key_id: &str) -> Result<models::RevokeMeSessionResponse> {
367 self.client
368 .request_json(Request {
369 method: Method::DELETE,
370 path: format!("/api/v1/me/sessions/{}", encode_path(key_id)),
371 query: NO_QUERY,
372 body: NO_BODY,
373 headers: Vec::new(),
374 idempotent: true,
375 })
376 .await
377 }
378
379 /// Begin browser OAuth login (302 to provider)
380 ///
381 /// Redirects the browser to GitHub/Google authorize URL. Server stores a one-shot state record
382 /// (10 min TTL) keyed by the `state` query the provider will echo back. PKCE codeVerifier is
383 /// generated server-side. `return_to` defaults to nothing (response is JSON); when set, must
384 /// satisfy the allow-list (same-origin paths, configured Universal-Link hosts, or `snaga://`).
385 /// `device_label` is captured here and surfaces on the minted `api_key` for `/me/sessions`.
386 ///
387 /// `GET /api/v1/auth/oauth/{provider}/start`
388 pub async fn start_o_auth_login(&self, provider: &models::OAuthLoginProviderConfigStatusProvider, params: &StartOAuthLoginParams) -> Result<serde_json::Value> {
389 self.client
390 .request_json(Request {
391 method: Method::GET,
392 path: format!("/api/v1/auth/oauth/{}/start", encode_path(&provider.to_string())),
393 query: Some(params),
394 body: NO_BODY,
395 headers: Vec::new(),
396 idempotent: false,
397 })
398 .await
399 }
400
401 /// Unlink an OAuth provider from the user
402 ///
403 /// Refuses to drop the user's last sign-in factor (counts other linked providers + OTP recovery
404 /// email). Idempotent — unlinking an already-unlinked provider returns 200 with
405 /// `already_unlinked: true`.
406 ///
407 /// `DELETE /api/v1/me/auth-providers/{provider}`
408 pub async fn unlink_auth_provider(&self, provider: &models::OAuthLoginProviderItemId) -> Result<models::UnlinkAuthProviderResponse> {
409 self.client
410 .request_json(Request {
411 method: Method::DELETE,
412 path: format!("/api/v1/me/auth-providers/{}", encode_path(&provider.to_string())),
413 query: NO_QUERY,
414 body: NO_BODY,
415 headers: Vec::new(),
416 idempotent: true,
417 })
418 .await
419 }
420
421 /// Verify email link
422 ///
423 /// `GET /api/v1/verify-email`
424 pub async fn verify_email(&self) -> Result<serde_json::Map<String, serde_json::Value>> {
425 self.client
426 .request_json(Request {
427 method: Method::GET,
428 path: "/api/v1/verify-email".to_string(),
429 query: NO_QUERY,
430 body: NO_BODY,
431 headers: Vec::new(),
432 idempotent: false,
433 })
434 .await
435 }
436
437 /// Verify a TOTP code
438 ///
439 /// Used both during enrolment confirmation and on subsequent logins. Burns no recovery code;
440 /// for recovery use `/auth/mfa/recovery`.
441 ///
442 /// `POST /api/v1/auth/mfa/verify`
443 pub async fn verify_mfa(&self, body: &models::VerifyMfaRequest) -> Result<models::VerifyMfaResponse> {
444 self.client
445 .request_json(Request {
446 method: Method::POST,
447 path: "/api/v1/auth/mfa/verify".to_string(),
448 query: NO_QUERY,
449 body: Some(body),
450 headers: Vec::new(),
451 idempotent: true,
452 })
453 .await
454 }
455
456 /// Verify a one-time recovery code (burns it on success)
457 ///
458 /// `POST /api/v1/auth/mfa/recovery`
459 pub async fn verify_mfa_recovery(&self, body: &models::VerifyMfaRecoveryRequest) -> Result<models::VerifyMfaRecoveryResponse> {
460 self.client
461 .request_json(Request {
462 method: Method::POST,
463 path: "/api/v1/auth/mfa/recovery".to_string(),
464 query: NO_QUERY,
465 body: Some(body),
466 headers: Vec::new(),
467 idempotent: true,
468 })
469 .await
470 }
471
472 /// Verify OTP code
473 ///
474 /// Expects body with email and code only (no request_id). Returns api_key to use as Bearer
475 /// token.
476 ///
477 /// `POST /api/v1/auth/verify-code`
478 pub async fn verify_otp_code(&self, body: &models::AuthVerifyCodeRequest) -> Result<models::AuthVerifyCodeResponse> {
479 self.client
480 .request_json(Request {
481 method: Method::POST,
482 path: "/api/v1/auth/verify-code".to_string(),
483 query: NO_QUERY,
484 body: Some(body),
485 headers: Vec::new(),
486 idempotent: true,
487 })
488 .await
489 }
490}