Skip to main content

owlauth_types/
runtime.rs

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    /// True only while this Runtime can complete the durable email flow.
101    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    /// Assertion of the immutable method selected by Control for this exact proof slot.
235    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    /// Trusted type of the stored Application that owns this exact interaction. Present only on
428    /// successful completion and used by Hosted UI to validate the final navigation scheme.
429    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/// Generates the complete Runtime-plane `OpenAPI` document.
1053#[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}