1use serde::{Deserialize, Deserializer, Serialize};
2use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityScheme};
3use utoipa::{Modify, OpenApi, ToSchema};
4
5use crate::health::HealthResponse;
6
7fn deserialize_required_nullable<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
8where
9 D: Deserializer<'de>,
10{
11 Option::<String>::deserialize(deserializer)
12}
13
14#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
15#[serde(rename_all = "snake_case")]
16pub enum ProviderKind {
17 Oidc,
18 Google,
19 Github,
20}
21
22#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
23#[serde(rename_all = "snake_case")]
24pub enum IdentityKind {
25 Provider,
26 Email,
27}
28
29#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
30#[serde(rename_all = "snake_case")]
31pub enum IdentityMutationMethodKind {
32 Provider,
33 Email,
34}
35
36#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
37pub enum JwkKeyType {
38 #[serde(rename = "OKP")]
39 Okp,
40}
41
42#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
43pub enum JwkCurve {
44 Ed25519,
45}
46
47#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
48pub enum SigningAlgorithm {
49 #[serde(rename = "EdDSA")]
50 EdDsa,
51}
52
53#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
54pub enum JwkUse {
55 #[serde(rename = "sig")]
56 Signature,
57}
58
59#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
60#[serde(deny_unknown_fields)]
61pub struct PublicJwk {
62 pub kty: JwkKeyType,
63 pub crv: JwkCurve,
64 pub alg: SigningAlgorithm,
65 #[serde(rename = "use")]
66 pub key_use: JwkUse,
67 #[schema(max_length = 128)]
68 pub kid: String,
69 #[schema(max_length = 64)]
70 pub x: String,
71}
72
73#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
74pub struct PublicProvider {
75 #[schema(max_length = 64)]
76 pub key: String,
77 #[schema(max_length = 128)]
78 pub display_name: String,
79 pub kind: ProviderKind,
80}
81
82#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
83#[allow(
84 clippy::struct_excessive_bools,
85 reason = "the public wire contract exposes orthogonal current capability facts"
86)]
87pub struct PublicApplicationConfig {
88 #[schema(max_length = 96)]
89 pub project_public_id: String,
90 #[schema(max_length = 128)]
91 pub project_display_name: String,
92 #[schema(max_length = 96)]
93 pub application_public_id: String,
94 #[schema(max_length = 128)]
95 pub application_display_name: String,
96 #[schema(max_items = 50)]
97 pub publishable_keys: Vec<String>,
98 #[schema(max_items = 50)]
99 pub providers: Vec<PublicProvider>,
100 pub email_available: bool,
102 pub email_otp_enabled: bool,
103 pub email_magic_link_enabled: bool,
104 pub login_available: bool,
105}
106
107#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)]
108pub struct JwksDocument {
109 #[schema(max_items = 100)]
110 pub keys: Vec<PublicJwk>,
111 pub revision: i64,
112 pub signing_epoch: i64,
113}
114
115#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
116#[serde(deny_unknown_fields)]
117pub struct LoginStartRequest {
118 #[schema(max_length = 96)]
119 pub application_id: String,
120 #[schema(max_length = 128)]
121 pub publishable_key: String,
122 #[schema(max_length = 2048)]
123 pub redirect_uri: String,
124 #[schema(min_length = 43, max_length = 43)]
125 pub pkce_challenge: String,
126 #[schema(min_length = 1, max_length = 1024)]
127 pub state: String,
128 #[schema(max_length = 64)]
129 pub presentation_hint: Option<String>,
130}
131
132#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
133pub struct LoginStartResponse {
134 #[schema(max_length = 512)]
135 pub hosted_url: String,
136 #[schema(max_length = 64)]
137 pub expires_at: String,
138}
139
140#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
141#[serde(rename_all = "snake_case")]
142pub enum HostedApplicationType {
143 Web,
144 Native,
145}
146
147#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
148#[serde(rename_all = "snake_case")]
149pub enum HostedInteractionStatus {
150 AwaitingMethodSelection,
151 EmailAddressEntry,
152 EmailChallengePending,
153 ProviderAuthorizationStarted,
154 ProviderExchangeInProgress,
155 Authenticated,
156 HandoffIssued,
157 Completed,
158 Failed,
159 Expired,
160}
161
162#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
163pub struct HostedProvider {
164 #[schema(max_length = 64)]
165 pub key: String,
166 #[schema(max_length = 128)]
167 pub display_name: String,
168 pub kind: ProviderKind,
169}
170
171#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
172pub struct HostedPendingEmailChallenge {
173 #[schema(min_length = 1, max_length = 96)]
174 pub challenge_id: String,
175 #[schema(minimum = 1)]
176 pub generation: i16,
177 #[schema(min_items = 1, max_items = 2)]
178 pub proof_modes: Vec<EmailProofMode>,
179 #[schema(max_length = 64)]
180 pub expires_at: String,
181}
182
183#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
184pub struct HostedInteractionResponse {
185 #[schema(max_length = 96)]
186 pub project_id: String,
187 #[schema(max_length = 128)]
188 pub project_display_name: String,
189 #[schema(max_length = 96)]
190 pub application_id: String,
191 #[schema(max_length = 128)]
192 pub application_display_name: String,
193 pub application_type: HostedApplicationType,
194 pub status: HostedInteractionStatus,
195 pub revision: i64,
196 pub session_reuse_available: bool,
197 #[schema(max_length = 64)]
198 pub presentation_hint: Option<String>,
199 #[schema(max_items = 50)]
200 pub providers: Vec<HostedProvider>,
201 pub email_available: bool,
202 #[schema(max_items = 2)]
203 pub email_proof_modes: Vec<EmailProofMode>,
204 pub pending_email_challenge: Option<HostedPendingEmailChallenge>,
205 #[schema(max_length = 64)]
206 pub csrf: String,
207 #[schema(max_length = 64)]
208 pub expires_at: String,
209}
210
211#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
212#[serde(deny_unknown_fields)]
213pub struct SelectProviderRequest {
214 pub expected_revision: i64,
215 #[schema(max_length = 64)]
216 pub csrf: String,
217 #[schema(max_length = 64)]
218 pub provider_key: String,
219}
220
221#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
222pub struct NavigationResponse {
223 #[schema(max_length = 4096)]
224 pub url: String,
225}
226
227#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
228#[serde(deny_unknown_fields)]
229pub struct SelectIdentityMutationMethodRequest {
230 #[schema(minimum = 1)]
231 pub expected_revision: i64,
232 #[schema(min_length = 1, max_length = 64)]
233 pub csrf: String,
234 pub method_kind: IdentityMutationMethodKind,
236}
237
238#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
239#[serde(rename_all = "snake_case")]
240pub enum IdentityMutationProofState {
241 EmailAddressEntry,
242 EmailChallengePending,
243 ProviderAuthorizationStarted,
244 ProviderExchangeInProgress,
245 Proved,
246 Expired,
247}
248
249#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
250pub struct IdentityMutationProofStateResponse {
251 #[schema(minimum = 1)]
252 pub revision: i64,
253 pub state: IdentityMutationProofState,
254}
255
256#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
257#[serde(tag = "method_kind", content = "result", rename_all = "snake_case")]
258pub enum IdentityMutationMethodResponse {
259 Provider(NavigationResponse),
260 Email(IdentityMutationProofStateResponse),
261}
262
263#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
264#[serde(deny_unknown_fields)]
265pub struct BeginIdentityMutationEmailChallengeRequest {
266 #[schema(minimum = 1)]
267 pub expected_revision: i64,
268 #[schema(min_length = 1, max_length = 64)]
269 pub csrf: String,
270 #[schema(min_length = 3, max_length = 254)]
271 pub email: String,
272}
273
274#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
275pub struct IdentityMutationEmailChallengeResponse {
276 pub accepted: bool,
277 #[schema(minimum = 1)]
278 pub revision: i64,
279 #[schema(min_length = 1, max_length = 96)]
280 pub challenge_id: String,
281 #[schema(minimum = 1)]
282 pub generation: i16,
283 #[schema(min_items = 1, max_items = 2)]
284 pub proof_modes: Vec<EmailProofMode>,
285 #[schema(max_length = 64)]
286 pub expires_at: String,
287}
288
289#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
290#[serde(deny_unknown_fields)]
291pub struct VerifyIdentityMutationEmailOtpRequest {
292 #[schema(minimum = 1)]
293 pub expected_revision: i64,
294 #[schema(min_length = 1, max_length = 64)]
295 pub csrf: String,
296 #[schema(min_length = 1, max_length = 96)]
297 pub challenge_id: String,
298 #[schema(minimum = 1)]
299 pub generation: i16,
300 #[schema(min_length = 6, max_length = 10, pattern = "^[0-9]{6,10}$")]
301 pub otp: String,
302}
303
304#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
305#[serde(deny_unknown_fields)]
306pub struct VerifyIdentityMutationEmailLinkRequest {
307 #[schema(minimum = 1)]
308 pub expected_revision: i64,
309 #[schema(min_length = 1, max_length = 64)]
310 pub csrf: String,
311 #[schema(min_length = 1, max_length = 96)]
312 pub challenge_id: String,
313 #[schema(minimum = 1)]
314 pub generation: i16,
315 #[schema(min_length = 22, max_length = 128, pattern = "^[A-Za-z0-9_-]{22,128}$")]
316 pub token: String,
317}
318
319#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
320#[serde(deny_unknown_fields)]
321pub struct ConfirmHostedIdentityMutationRequest {
322 #[schema(minimum = 1)]
323 pub expected_revision: i64,
324 #[schema(min_length = 1, max_length = 64)]
325 pub csrf: String,
326}
327
328#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
329#[serde(rename_all = "snake_case")]
330pub enum HostedIdentityMutationStatus {
331 PendingProof,
332 Ready,
333 Expired,
334 Cancelled,
335}
336
337#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
338pub struct HostedIdentityMutationResponse {
339 #[schema(minimum = 1)]
340 pub revision: i64,
341 pub status: HostedIdentityMutationStatus,
342}
343
344#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
345#[serde(deny_unknown_fields)]
346pub struct SelectEmailRequest {
347 pub expected_revision: i64,
348 #[schema(max_length = 64)]
349 pub csrf: String,
350}
351
352#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
353pub struct SelectEmailResponse {
354 pub status: HostedInteractionStatus,
355 #[schema(minimum = 1)]
356 pub revision: i64,
357}
358
359#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
360#[serde(deny_unknown_fields)]
361pub struct StartManagedReauthorizationRequest {
362 #[schema(minimum = 1)]
363 pub expected_revision: i64,
364 #[schema(max_length = 64)]
365 pub csrf: String,
366}
367
368#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
369#[serde(deny_unknown_fields)]
370pub struct BeginEmailChallengeRequest {
371 pub expected_revision: i64,
372 #[schema(max_length = 64)]
373 pub csrf: String,
374 #[schema(max_length = 254)]
375 pub email: String,
376}
377
378#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
379#[serde(rename_all = "snake_case")]
380pub enum EmailProofMode {
381 Otp,
382 MagicLink,
383}
384
385#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
386pub struct EmailChallengeAcceptedResponse {
387 pub accepted: bool,
388 pub revision: i64,
389 pub challenge_id: String,
390 pub generation: i16,
391 #[schema(min_items = 1, max_items = 2)]
392 pub proof_modes: Vec<EmailProofMode>,
393 #[schema(max_length = 64)]
394 pub expires_at: String,
395}
396
397#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
398#[serde(deny_unknown_fields)]
399pub struct VerifyEmailOtpRequest {
400 pub expected_revision: i64,
401 #[schema(max_length = 64)]
402 pub csrf: String,
403 pub challenge_id: String,
404 pub generation: i16,
405 #[schema(min_length = 6, max_length = 10, pattern = "^[0-9]{6,10}$")]
406 pub otp: String,
407}
408
409#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
410#[serde(deny_unknown_fields)]
411pub struct ConfirmEmailMagicRequest {
412 pub expected_revision: i64,
413 #[schema(max_length = 64)]
414 pub csrf: String,
415 pub challenge_id: String,
416 pub transaction_id: String,
417 pub generation: i16,
418 #[schema(min_length = 22, max_length = 128, pattern = "^[A-Za-z0-9_-]{22,128}$")]
419 pub proof: String,
420}
421
422#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
423pub struct EmailProofResponse {
424 pub completed: bool,
425 #[schema(max_length = 4096)]
426 pub redirect_url: Option<String>,
427 pub application_type: Option<HostedApplicationType>,
430}
431
432#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
433#[serde(deny_unknown_fields)]
434pub struct ConfirmSessionReuseRequest {
435 pub expected_revision: i64,
436 #[schema(max_length = 64)]
437 pub csrf: String,
438}
439
440#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
441#[serde(deny_unknown_fields)]
442pub struct HandoffExchangeRequest {
443 #[schema(max_length = 96)]
444 pub application_id: String,
445 #[schema(max_length = 128)]
446 pub publishable_key: String,
447 #[schema(max_length = 256)]
448 pub handoff: String,
449 #[schema(min_length = 43, max_length = 128)]
450 pub pkce_verifier: String,
451}
452
453#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
454#[serde(deny_unknown_fields)]
455pub struct RefreshRequest {
456 #[schema(max_length = 96)]
457 pub application_id: String,
458 #[schema(max_length = 128)]
459 pub publishable_key: String,
460 #[schema(max_length = 256)]
461 pub refresh_token: String,
462}
463
464#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
465#[serde(deny_unknown_fields)]
466pub struct UserProjection {
467 #[schema(max_length = 96)]
468 pub user_id: String,
469 pub user_revision: i64,
470 #[schema(max_length = 64)]
471 pub projection_schema: String,
472 pub projection_revision: i64,
473 #[serde(deserialize_with = "deserialize_required_nullable")]
474 #[schema(max_length = 128, required = true)]
475 pub display_name: Option<String>,
476 #[serde(deserialize_with = "deserialize_required_nullable")]
477 #[schema(max_length = 2048, required = true)]
478 pub picture_url: Option<String>,
479 #[serde(deserialize_with = "deserialize_required_nullable")]
480 #[schema(max_length = 35, required = true)]
481 pub locale: Option<String>,
482 #[serde(deserialize_with = "deserialize_required_nullable")]
483 #[schema(max_length = 320, required = true)]
484 pub verified_email: Option<String>,
485 #[schema(max_length = 32)]
486 pub status: String,
487 #[schema(max_length = 64)]
488 pub created_at: String,
489 #[schema(max_length = 64)]
490 pub updated_at: String,
491}
492
493#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)]
494#[serde(deny_unknown_fields)]
495pub struct CredentialPairResponse {
496 #[schema(max_length = 96)]
497 pub project_id: String,
498 #[schema(max_length = 96)]
499 pub application_id: String,
500 #[schema(max_length = 96)]
501 pub user_id: String,
502 #[schema(max_length = 64)]
503 pub session_id: String,
504 pub refresh_generation: i64,
505 #[schema(max_length = 16384)]
506 pub access_token: String,
507 #[schema(max_length = 256)]
508 pub refresh_token: String,
509 pub token_type: String,
510 pub expires_in: i64,
511 pub projection: UserProjection,
512 pub projection_revision: i64,
513 #[schema(max_length = 64)]
514 pub session_expires_at: String,
515}
516
517#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)]
518#[serde(deny_unknown_fields)]
519pub struct CurrentUserResponse {
520 #[schema(max_length = 96)]
521 pub project_id: String,
522 #[schema(max_length = 96)]
523 pub application_id: String,
524 #[schema(max_length = 96)]
525 pub user_id: String,
526 pub projection: UserProjection,
527 pub projection_revision: i64,
528 #[schema(max_length = 64)]
529 pub authenticated_at: String,
530 #[schema(max_length = 64)]
531 pub session_expires_at: String,
532}
533
534#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
535pub struct BrowserLogoutPreparationResponse {
536 #[schema(max_length = 512)]
537 pub hosted_url: String,
538 #[schema(max_length = 64)]
539 pub expires_at: String,
540}
541
542#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
543pub struct BrowserLogoutResponse {
544 #[schema(max_length = 96)]
545 pub project_id: String,
546 pub revision: i64,
547 #[schema(max_length = 64)]
548 pub csrf: String,
549 #[schema(max_length = 64)]
550 pub expires_at: String,
551}
552
553#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
554#[serde(deny_unknown_fields)]
555pub struct ConfirmBrowserLogoutRequest {
556 pub expected_revision: i64,
557 #[schema(max_length = 64)]
558 pub csrf: String,
559}
560
561#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
562pub struct CompletionResponse {
563 pub completed: bool,
564}
565
566#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
567#[serde(deny_unknown_fields)]
568pub struct RuntimeError {
569 #[schema(max_length = 64)]
570 pub code: String,
571 #[schema(max_length = 256)]
572 pub message: String,
573 #[schema(max_length = 128)]
574 pub request_id: String,
575}
576
577#[utoipa::path(
578 get,
579 path = "/v1/projects/{project_public_id}/auth/config",
580 params(("project_public_id" = String, Path), ("application_id" = String, Query)),
581 responses(
582 (status = 200, body = PublicApplicationConfig),
583 (status = 400, body = RuntimeError),
584 (status = 404, body = RuntimeError),
585 (status = 503, body = RuntimeError)
586 )
587)]
588#[doc(hidden)]
589pub fn get_public_application_config() {}
590
591#[utoipa::path(
592 get,
593 path = "/projects/{project_public_id}/.well-known/jwks.json",
594 params(("project_public_id" = String, Path)),
595 responses((status = 200, body = JwksDocument), (status = 404, body = RuntimeError), (status = 503, body = RuntimeError))
596)]
597#[doc(hidden)]
598pub fn get_project_jwks() {}
599
600#[utoipa::path(
601 post,
602 path = "/v1/projects/{project_public_id}/auth/login/start",
603 params(("project_public_id" = String, Path)),
604 request_body = LoginStartRequest,
605 responses((status = 201, body = LoginStartResponse), (status = 400, body = RuntimeError), (status = 404, body = RuntimeError), (status = 503, body = RuntimeError))
606)]
607#[doc(hidden)]
608pub fn start_login() {}
609
610#[utoipa::path(
611 get,
612 path = "/auth/interactions/{interaction}",
613 params(("interaction" = String, Path)),
614 responses((status = 200, description = "Hosted Authentication HTML", body = String, content_type = "text/html"), (status = 404, body = RuntimeError), (status = 409, body = RuntimeError))
615)]
616#[doc(hidden)]
617pub fn get_hosted_interaction() {}
618
619#[utoipa::path(
620 get,
621 path = "/auth/managed-reauthorizations/{interaction}",
622 params(("interaction" = String, Path)),
623 responses((status = 200, description = "Hosted managed-reauthorization HTML", body = String, content_type = "text/html"), (status = 400, body = RuntimeError), (status = 404, body = RuntimeError), (status = 409, body = RuntimeError))
624)]
625#[doc(hidden)]
626pub fn get_hosted_managed_reauthorization() {}
627
628#[utoipa::path(
629 get,
630 path = "/auth/identity-mutations/{intent}",
631 params(("intent" = String, Path)),
632 responses((status = 200, description = "Hosted identity-mutation HTML", body = String, content_type = "text/html"), (status = 400, body = RuntimeError), (status = 404, body = RuntimeError), (status = 409, body = RuntimeError))
633)]
634#[doc(hidden)]
635pub fn get_identity_mutation() {}
636
637#[utoipa::path(
638 get,
639 path = "/auth/identity-mutations/email/confirm/{challenge_id}",
640 params(("challenge_id" = String, Path)),
641 responses((status = 200, description = "Generic fragment-only identity-mutation magic-link confirmation shell", body = String, content_type = "text/html"), (status = 400, body = RuntimeError), (status = 404, body = RuntimeError))
642)]
643#[doc(hidden)]
644pub fn get_identity_mutation_magic_confirmation() {}
645
646#[utoipa::path(
647 post,
648 path = "/v1/projects/{project_public_id}/auth/interactions/{interaction}/method",
649 params(("project_public_id" = String, Path), ("interaction" = String, Path)),
650 request_body = SelectProviderRequest,
651 responses((status = 200, body = NavigationResponse), (status = 400, body = RuntimeError), (status = 403, body = RuntimeError), (status = 409, body = RuntimeError))
652)]
653#[doc(hidden)]
654pub fn select_provider() {}
655
656#[utoipa::path(
657 post,
658 path = "/v1/projects/{project_public_id}/auth/interactions/{interaction}/email/select",
659 params(("project_public_id" = String, Path), ("interaction" = String, Path)),
660 request_body = SelectEmailRequest,
661 responses((status = 200, body = SelectEmailResponse), (status = 403, body = RuntimeError), (status = 409, body = RuntimeError))
662)]
663#[doc(hidden)]
664pub fn select_email() {}
665
666#[utoipa::path(
667 post,
668 path = "/v1/projects/{project_public_id}/auth/interactions/{interaction}/email/challenges",
669 params(("project_public_id" = String, Path), ("interaction" = String, Path)),
670 request_body = BeginEmailChallengeRequest,
671 responses((status = 202, body = EmailChallengeAcceptedResponse), (status = 400, body = RuntimeError), (status = 403, body = RuntimeError), (status = 409, body = RuntimeError))
672)]
673#[doc(hidden)]
674pub fn begin_email_challenge() {}
675
676#[utoipa::path(
677 post,
678 path = "/v1/projects/{project_public_id}/auth/interactions/{interaction}/email/resend",
679 params(("project_public_id" = String, Path), ("interaction" = String, Path)),
680 request_body = BeginEmailChallengeRequest,
681 responses((status = 202, body = EmailChallengeAcceptedResponse), (status = 400, body = RuntimeError), (status = 403, body = RuntimeError), (status = 409, body = RuntimeError))
682)]
683#[doc(hidden)]
684pub fn resend_email_challenge() {}
685
686#[utoipa::path(
687 post,
688 path = "/v1/projects/{project_public_id}/auth/interactions/{interaction}/email/otp/verify",
689 params(("project_public_id" = String, Path), ("interaction" = String, Path)),
690 request_body = VerifyEmailOtpRequest,
691 responses((status = 200, body = EmailProofResponse), (status = 400, body = RuntimeError), (status = 403, body = RuntimeError), (status = 409, body = RuntimeError))
692)]
693#[doc(hidden)]
694pub fn verify_email_otp() {}
695
696#[utoipa::path(
697 get,
698 path = "/auth/email/confirm/{challenge_id}",
699 params(("challenge_id" = String, Path)),
700 responses((status = 200, description = "Generic fragment-only magic-link confirmation shell", body = String, content_type = "text/html"), (status = 404, body = RuntimeError))
701)]
702#[doc(hidden)]
703pub fn get_email_magic_confirmation() {}
704
705#[utoipa::path(
706 post,
707 path = "/v1/projects/{project_public_id}/auth/email/magic/confirm",
708 params(("project_public_id" = String, Path)),
709 request_body = ConfirmEmailMagicRequest,
710 responses((status = 200, body = EmailProofResponse), (status = 400, body = RuntimeError), (status = 403, body = RuntimeError), (status = 409, body = RuntimeError))
711)]
712#[doc(hidden)]
713pub fn confirm_email_magic() {}
714
715#[utoipa::path(
716 post,
717 path = "/v1/projects/{project_public_id}/auth/managed-reauthorizations/{interaction}/start",
718 params(("project_public_id" = String, Path), ("interaction" = String, Path)),
719 request_body = StartManagedReauthorizationRequest,
720 responses((status = 200, body = NavigationResponse), (status = 400, body = RuntimeError), (status = 403, body = RuntimeError), (status = 409, body = RuntimeError))
721)]
722#[doc(hidden)]
723pub fn start_managed_reauthorization() {}
724
725#[utoipa::path(
726 post,
727 path = "/v1/projects/{project_public_id}/auth/identity-mutations/{intent}/proofs/{proof_slot}/method",
728 params(
729 ("project_public_id" = String, Path),
730 ("intent" = String, Path),
731 ("proof_slot" = String, Path)
732 ),
733 request_body = SelectIdentityMutationMethodRequest,
734 responses(
735 (status = 200, body = IdentityMutationMethodResponse),
736 (status = 400, body = RuntimeError),
737 (status = 403, body = RuntimeError),
738 (status = 404, body = RuntimeError),
739 (status = 409, body = RuntimeError),
740
741 )
742)]
743#[doc(hidden)]
744pub fn select_identity_mutation_method() {}
745
746#[utoipa::path(
747 post,
748 path = "/v1/projects/{project_public_id}/auth/identity-mutations/{intent}/proofs/{proof_slot}/email/challenges",
749 params(
750 ("project_public_id" = String, Path),
751 ("intent" = String, Path),
752 ("proof_slot" = String, Path)
753 ),
754 request_body = BeginIdentityMutationEmailChallengeRequest,
755 responses(
756 (status = 202, body = IdentityMutationEmailChallengeResponse),
757 (status = 400, body = RuntimeError),
758 (status = 403, body = RuntimeError),
759 (status = 404, body = RuntimeError),
760 (status = 409, body = RuntimeError),
761
762 )
763)]
764#[doc(hidden)]
765pub fn begin_identity_mutation_email_challenge() {}
766
767#[utoipa::path(
768 post,
769 path = "/v1/projects/{project_public_id}/auth/identity-mutations/{intent}/proofs/{proof_slot}/email/otp/verify",
770 params(
771 ("project_public_id" = String, Path),
772 ("intent" = String, Path),
773 ("proof_slot" = String, Path)
774 ),
775 request_body = VerifyIdentityMutationEmailOtpRequest,
776 responses(
777 (status = 200, body = IdentityMutationProofStateResponse),
778 (status = 400, body = RuntimeError),
779 (status = 403, body = RuntimeError),
780 (status = 404, body = RuntimeError),
781 (status = 409, body = RuntimeError),
782
783 )
784)]
785#[doc(hidden)]
786pub fn verify_identity_mutation_email_otp() {}
787
788#[utoipa::path(
789 post,
790 path = "/v1/projects/{project_public_id}/auth/identity-mutations/{intent}/proofs/{proof_slot}/email/link/verify",
791 params(
792 ("project_public_id" = String, Path),
793 ("intent" = String, Path),
794 ("proof_slot" = String, Path)
795 ),
796 request_body = VerifyIdentityMutationEmailLinkRequest,
797 responses(
798 (status = 200, body = IdentityMutationProofStateResponse),
799 (status = 400, body = RuntimeError),
800 (status = 403, body = RuntimeError),
801 (status = 404, body = RuntimeError),
802 (status = 409, body = RuntimeError),
803
804 )
805)]
806#[doc(hidden)]
807pub fn verify_identity_mutation_email_link() {}
808
809#[utoipa::path(
810 post,
811 path = "/v1/projects/{project_public_id}/auth/identity-mutations/{intent}/confirm",
812 params(
813 ("project_public_id" = String, Path),
814 ("intent" = String, Path)
815 ),
816 request_body = ConfirmHostedIdentityMutationRequest,
817 responses(
818 (status = 200, body = HostedIdentityMutationResponse),
819 (status = 400, body = RuntimeError),
820 (status = 403, body = RuntimeError),
821 (status = 404, body = RuntimeError),
822 (status = 409, body = RuntimeError),
823
824 )
825)]
826#[doc(hidden)]
827pub fn confirm_identity_mutation() {}
828
829#[utoipa::path(
830 post,
831 path = "/v1/projects/{project_public_id}/auth/interactions/{interaction}/session/reuse",
832 params(("project_public_id" = String, Path), ("interaction" = String, Path)),
833 request_body = ConfirmSessionReuseRequest,
834 responses((status = 200, body = NavigationResponse), (status = 403, body = RuntimeError), (status = 409, body = RuntimeError))
835)]
836#[doc(hidden)]
837pub fn confirm_session_reuse() {}
838
839#[utoipa::path(
840 get,
841 path = "/projects/{project_public_id}/auth/callback/{provider_key}",
842 params(
843 ("project_public_id" = String, Path),
844 ("provider_key" = String, Path),
845 ("code" = String, Query),
846 ("state" = String, Query)
847 ),
848 responses(
849 (status = 303, description = "Redirect to the exact stored Application callback"),
850 (status = 400, body = RuntimeError),
851 (status = 404, body = RuntimeError),
852 (status = 409, body = RuntimeError),
853 (status = 503, body = RuntimeError)
854 )
855)]
856#[doc(hidden)]
857pub fn complete_provider_callback() {}
858
859#[utoipa::path(
860 post,
861 path = "/v1/projects/{project_public_id}/auth/handoff/exchange",
862 params(("project_public_id" = String, Path)),
863 request_body = HandoffExchangeRequest,
864 responses((status = 200, body = CredentialPairResponse), (status = 400, body = RuntimeError), (status = 409, body = RuntimeError), (status = 503, body = RuntimeError))
865)]
866#[doc(hidden)]
867pub fn exchange_handoff() {}
868
869#[utoipa::path(
870 post,
871 path = "/v1/projects/{project_public_id}/auth/sessions/refresh",
872 params(("project_public_id" = String, Path)),
873 request_body = RefreshRequest,
874 responses((status = 200, body = CredentialPairResponse), (status = 400, body = RuntimeError), (status = 409, body = RuntimeError), (status = 503, body = RuntimeError))
875)]
876#[doc(hidden)]
877pub fn refresh_session() {}
878
879#[utoipa::path(
880 get,
881 path = "/v1/projects/{project_public_id}/auth/users/me",
882 params(("project_public_id" = String, Path)),
883 responses((status = 200, body = CurrentUserResponse), (status = 401, body = RuntimeError, headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))), (status = 503, body = RuntimeError)),
884 security(("project_bearer" = []))
885)]
886#[doc(hidden)]
887pub fn get_current_user() {}
888
889#[utoipa::path(
890 post,
891 path = "/v1/projects/{project_public_id}/auth/sessions/logout",
892 params(("project_public_id" = String, Path)),
893 responses((status = 200, body = CompletionResponse), (status = 401, body = RuntimeError, headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))), (status = 503, body = RuntimeError)),
894 security(("project_bearer" = []))
895)]
896#[doc(hidden)]
897pub fn logout_application_session() {}
898
899#[utoipa::path(
900 post,
901 path = "/v1/projects/{project_public_id}/auth/browser-logout/prepare",
902 params(("project_public_id" = String, Path)),
903 responses((status = 201, body = BrowserLogoutPreparationResponse), (status = 401, body = RuntimeError, headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))), (status = 503, body = RuntimeError)),
904 security(("project_bearer" = []))
905)]
906#[doc(hidden)]
907pub fn prepare_browser_logout() {}
908
909#[utoipa::path(
910 get,
911 path = "/auth/browser-logout/{preparation}",
912 params(("preparation" = String, Path)),
913 responses((status = 200, description = "Hosted browser-logout confirmation HTML", body = String, content_type = "text/html"), (status = 404, body = RuntimeError), (status = 409, body = RuntimeError))
914)]
915#[doc(hidden)]
916pub fn get_browser_logout() {}
917
918#[utoipa::path(
919 post,
920 path = "/v1/projects/{project_public_id}/auth/browser-logout/{preparation}/confirm",
921 params(("project_public_id" = String, Path), ("preparation" = String, Path)),
922 request_body = ConfirmBrowserLogoutRequest,
923 responses((status = 200, body = CompletionResponse), (status = 403, body = RuntimeError), (status = 409, body = RuntimeError))
924)]
925#[doc(hidden)]
926pub fn confirm_browser_logout() {}
927
928#[derive(OpenApi)]
929#[openapi(
930 info(
931 title = "OwlAuth Runtime API",
932 description = "Project Auth Runtime API"
933 ),
934 paths(
935 crate::health::get_liveness,
936 crate::health::get_readiness,
937 get_public_application_config,
938 get_project_jwks
939 ),
940 components(schemas(
941 HealthResponse,
942 ProviderKind,
943 JwkKeyType,
944 JwkCurve,
945 SigningAlgorithm,
946 JwkUse,
947 PublicJwk,
948 PublicProvider,
949 PublicApplicationConfig,
950 JwksDocument,
951 RuntimeError
952 ))
953)]
954struct RuntimeApiDoc;
955
956#[derive(OpenApi)]
957#[openapi(
958 info(title = "OwlAuth Runtime API", description = "Project Auth Runtime API"),
959 paths(
960 start_login,
961 get_hosted_interaction,
962 get_hosted_managed_reauthorization,
963 get_identity_mutation,
964 get_identity_mutation_magic_confirmation,
965 select_provider,
966 select_email,
967 begin_email_challenge,
968 resend_email_challenge,
969 verify_email_otp,
970 get_email_magic_confirmation,
971 confirm_email_magic,
972 start_managed_reauthorization,
973 select_identity_mutation_method,
974 begin_identity_mutation_email_challenge,
975 verify_identity_mutation_email_otp,
976 verify_identity_mutation_email_link,
977 confirm_identity_mutation,
978 confirm_session_reuse,
979 complete_provider_callback,
980 exchange_handoff,
981 refresh_session,
982 get_current_user,
983 logout_application_session,
984 prepare_browser_logout,
985 get_browser_logout,
986 confirm_browser_logout
987 ),
988 components(
989 schemas(
990 LoginStartRequest,
991 LoginStartResponse,
992 HostedApplicationType,
993 HostedInteractionStatus,
994 HostedProvider,
995 HostedPendingEmailChallenge,
996 HostedInteractionResponse,
997 SelectProviderRequest,
998 StartManagedReauthorizationRequest,
999 NavigationResponse,
1000 IdentityKind,
1001 IdentityMutationMethodKind,
1002 SelectIdentityMutationMethodRequest,
1003 IdentityMutationProofState,
1004 IdentityMutationProofStateResponse,
1005 IdentityMutationMethodResponse,
1006 BeginIdentityMutationEmailChallengeRequest,
1007 IdentityMutationEmailChallengeResponse,
1008 VerifyIdentityMutationEmailOtpRequest,
1009 VerifyIdentityMutationEmailLinkRequest,
1010 ConfirmHostedIdentityMutationRequest,
1011 HostedIdentityMutationStatus,
1012 HostedIdentityMutationResponse,
1013 SelectEmailRequest,
1014 SelectEmailResponse,
1015 BeginEmailChallengeRequest,
1016 EmailProofMode,
1017 EmailChallengeAcceptedResponse,
1018 VerifyEmailOtpRequest,
1019 ConfirmEmailMagicRequest,
1020 EmailProofResponse,
1021 ConfirmSessionReuseRequest,
1022 HandoffExchangeRequest,
1023 RefreshRequest,
1024 UserProjection,
1025 CredentialPairResponse,
1026 CurrentUserResponse,
1027 BrowserLogoutPreparationResponse,
1028 BrowserLogoutResponse,
1029 ConfirmBrowserLogoutRequest,
1030 CompletionResponse,
1031 RuntimeError
1032 )
1033 ),
1034 modifiers(&RuntimeSecurity)
1035)]
1036struct FederatedProjectAuthApiDoc;
1037
1038struct RuntimeSecurity;
1039
1040impl Modify for RuntimeSecurity {
1041 fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
1042 openapi
1043 .components
1044 .get_or_insert_default()
1045 .add_security_scheme(
1046 "project_bearer",
1047 SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
1048 );
1049 }
1050}
1051
1052#[must_use]
1054pub fn openapi() -> utoipa::openapi::OpenApi {
1055 let mut document = RuntimeApiDoc::openapi();
1056 if crate::FEDERATED_PROJECT_AUTH_AVAILABLE {
1057 document.merge(FederatedProjectAuthApiDoc::openapi());
1058 }
1059 crate::add_response_to_operations(&mut document, "408", |path| {
1060 if path.starts_with("/auth/") {
1061 utoipa::openapi::Response::builder()
1062 .description("The request exceeded the Runtime listener time budget")
1063 .content("text/html", utoipa::openapi::Content::default())
1064 .build()
1065 .into()
1066 } else {
1067 crate::json_error_response(
1068 "The request exceeded the Runtime listener time budget",
1069 "RuntimeError",
1070 "application/json",
1071 )
1072 }
1073 });
1074 document
1075}
1076
1077#[cfg(test)]
1078mod identity_mutation_contract_tests {
1079 use serde_json::json;
1080
1081 use super::{SelectIdentityMutationMethodRequest, VerifyIdentityMutationEmailLinkRequest};
1082
1083 #[test]
1084 fn runtime_identity_mutation_routes_and_responses_are_bounded() {
1085 let document =
1086 serde_json::to_value(super::openapi()).expect("Runtime OpenAPI should serialize");
1087 for path in [
1088 "/v1/projects/{project_public_id}/auth/identity-mutations/{intent}/proofs/{proof_slot}/method",
1089 "/v1/projects/{project_public_id}/auth/identity-mutations/{intent}/proofs/{proof_slot}/email/challenges",
1090 "/v1/projects/{project_public_id}/auth/identity-mutations/{intent}/proofs/{proof_slot}/email/otp/verify",
1091 "/v1/projects/{project_public_id}/auth/identity-mutations/{intent}/proofs/{proof_slot}/email/link/verify",
1092 "/v1/projects/{project_public_id}/auth/identity-mutations/{intent}/confirm",
1093 ] {
1094 assert!(document["paths"][path].is_object(), "missing path: {path}");
1095 }
1096 assert!(document["paths"]["/auth/identity-mutations/{intent}"]["get"].is_object());
1097
1098 for schema in [
1099 "IdentityMutationMethodResponse",
1100 "IdentityMutationProofStateResponse",
1101 "IdentityMutationEmailChallengeResponse",
1102 "HostedIdentityMutationResponse",
1103 ] {
1104 let encoded = document["components"]["schemas"][schema].to_string();
1105 for forbidden in ["receipt", "subject", "scope", "callback", "purpose"] {
1106 assert!(
1107 !encoded.contains(forbidden),
1108 "{schema} leaked forbidden field {forbidden}"
1109 );
1110 }
1111 }
1112 assert_eq!(
1113 document["components"]["schemas"]["IdentityMutationMethodResponse"]["oneOf"]
1114 .as_array()
1115 .map(Vec::len),
1116 Some(2)
1117 );
1118 }
1119
1120 #[test]
1121 fn runtime_identity_mutation_commands_reject_authority_overrides() {
1122 let method = json!({
1123 "expected_revision": 2,
1124 "csrf": "csrf",
1125 "method_kind": "provider",
1126 "provider_id": "caller-provider"
1127 });
1128 assert!(serde_json::from_value::<SelectIdentityMutationMethodRequest>(method).is_err());
1129
1130 let magic = json!({
1131 "expected_revision": 3,
1132 "csrf": "csrf",
1133 "challenge_id": "challenge",
1134 "generation": 1,
1135 "token": "abcdefghijklmnopqrstuv",
1136 "application_id": "caller-application"
1137 });
1138 assert!(serde_json::from_value::<VerifyIdentityMutationEmailLinkRequest>(magic).is_err());
1139 }
1140
1141 #[test]
1142 fn user_projection_nullable_fields_are_explicit_and_required() {
1143 let projection = json!({
1144 "user_id": "usr_example",
1145 "user_revision": 1,
1146 "projection_schema": "owlauth.user.v1",
1147 "projection_revision": 1,
1148 "display_name": null,
1149 "picture_url": null,
1150 "locale": null,
1151 "verified_email": null,
1152 "status": "active",
1153 "created_at": "2026-08-02T00:00:00Z",
1154 "updated_at": "2026-08-02T00:00:00Z"
1155 });
1156 assert!(serde_json::from_value::<super::UserProjection>(projection.clone()).is_ok());
1157
1158 let mut missing = projection.clone();
1159 missing.as_object_mut().unwrap().remove("locale");
1160 assert!(serde_json::from_value::<super::UserProjection>(missing).is_err());
1161 let mut unknown = projection;
1162 unknown["unexpected"] = json!(true);
1163 assert!(serde_json::from_value::<super::UserProjection>(unknown).is_err());
1164
1165 let document = serde_json::to_value(super::openapi()).unwrap();
1166 let required = document["components"]["schemas"]["UserProjection"]["required"]
1167 .as_array()
1168 .unwrap();
1169 for field in ["display_name", "picture_url", "locale", "verified_email"] {
1170 assert!(required.iter().any(|required| required == field));
1171 }
1172 }
1173}