Skip to main content

owlauth_types/
control_resources.rs

1use serde::{Deserialize, Deserializer, Serialize};
2use utoipa::ToSchema;
3
4pub use crate::runtime::{IdentityKind, IdentityMutationMethodKind};
5use crate::runtime::{ProviderKind, PublicJwk, SigningAlgorithm};
6
7fn deserialize_required_nullable_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
8where
9    D: Deserializer<'de>,
10{
11    Option::<String>::deserialize(deserializer)
12}
13
14fn deserialize_required_nullable_project_server_key<'de, D>(
15    deserializer: D,
16) -> Result<Option<ProjectServerKey>, D::Error>
17where
18    D: Deserializer<'de>,
19{
20    Option::<ProjectServerKey>::deserialize(deserializer)
21}
22
23fn deserialize_required_nullable_project_user<'de, D>(
24    deserializer: D,
25) -> Result<Option<ProjectUser>, D::Error>
26where
27    D: Deserializer<'de>,
28{
29    Option::<ProjectUser>::deserialize(deserializer)
30}
31
32#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
33#[serde(rename_all = "snake_case")]
34pub enum ProjectStatus {
35    Active,
36    Disabled,
37    Deleting,
38}
39
40#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
41#[serde(rename_all = "snake_case")]
42pub enum ApplicationType {
43    Web,
44    Native,
45}
46
47#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
48#[serde(rename_all = "snake_case")]
49pub enum ApplicationStatus {
50    Active,
51    Disabled,
52}
53
54#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
55#[serde(rename_all = "snake_case")]
56pub enum SigningKeyState {
57    Provisioning,
58    Published,
59    Active,
60    Retiring,
61    Retired,
62    Revoked,
63    Abandoned,
64}
65
66#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
67#[serde(rename_all = "snake_case")]
68pub enum ProviderStatus {
69    Provisioning,
70    Active,
71    Disabled,
72}
73
74#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
75pub struct ProblemDetails {
76    #[serde(rename = "type")]
77    pub type_uri: String,
78    pub code: String,
79    pub title: String,
80    pub status: u16,
81    pub detail: String,
82    pub request_id: String,
83}
84
85#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
86pub struct Project {
87    pub id: String,
88    pub public_id: String,
89    #[schema(max_length = 128)]
90    pub display_name: String,
91    #[schema(max_length = 256)]
92    pub belongs_to: Option<String>,
93    pub status: ProjectStatus,
94    pub metadata_revision: i64,
95    pub security_revision: i64,
96}
97
98#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
99pub struct ProjectList {
100    #[schema(max_items = 100)]
101    pub items: Vec<Project>,
102}
103
104#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
105#[serde(deny_unknown_fields)]
106pub struct ProjectOverviewSummary {
107    pub project_id: String,
108    pub applications: ProjectOverviewApplicationCounts,
109    pub providers: ProjectOverviewProviderCounts,
110    pub users: ProjectOverviewUserCounts,
111    pub project_server_keys: ProjectOverviewServerKeyCounts,
112}
113
114#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
115#[serde(deny_unknown_fields)]
116pub struct ProjectOverviewApplicationCounts {
117    #[schema(minimum = 0)]
118    pub total: u64,
119    #[schema(minimum = 0)]
120    pub active: u64,
121    #[schema(minimum = 0)]
122    pub configured: u64,
123}
124
125#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
126#[serde(deny_unknown_fields)]
127pub struct ProjectOverviewProviderCounts {
128    #[schema(minimum = 0)]
129    pub total: u64,
130    #[schema(minimum = 0)]
131    pub active: u64,
132    #[schema(minimum = 0)]
133    pub active_assignments: u64,
134}
135
136#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
137#[serde(deny_unknown_fields)]
138pub struct ProjectOverviewUserCounts {
139    #[schema(minimum = 0)]
140    pub total: u64,
141    #[schema(minimum = 0)]
142    pub active: u64,
143    #[schema(minimum = 0)]
144    pub disabled: u64,
145    #[schema(minimum = 0)]
146    pub merged: u64,
147}
148
149#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
150#[serde(deny_unknown_fields)]
151pub struct ProjectOverviewServerKeyCounts {
152    #[schema(minimum = 0)]
153    pub total: u64,
154    #[schema(minimum = 0)]
155    pub active: u64,
156    #[schema(minimum = 0)]
157    pub revoked: u64,
158}
159
160#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
161#[serde(deny_unknown_fields)]
162pub struct CreateProjectRequest {
163    #[schema(min_length = 1, max_length = 128)]
164    pub display_name: String,
165    #[schema(max_length = 256)]
166    pub belongs_to: Option<String>,
167}
168
169#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
170#[serde(deny_unknown_fields)]
171pub struct UpdateProjectRequest {
172    #[schema(min_length = 1, max_length = 128)]
173    pub display_name: String,
174    #[schema(max_length = 256)]
175    pub belongs_to: Option<String>,
176    #[schema(minimum = 1)]
177    pub expected_metadata_revision: i64,
178}
179
180#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
181pub struct ProjectPolicy {
182    pub project_id: String,
183    #[schema(minimum = 60, maximum = 3600)]
184    pub access_token_lifetime_seconds: i32,
185    pub browser_session_reuse: bool,
186    #[schema(minimum = 1)]
187    pub claims_revision: i64,
188    #[schema(minimum = 1)]
189    pub session_revision: i64,
190}
191
192#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
193#[serde(deny_unknown_fields)]
194pub struct UpdateProjectPolicyRequest {
195    #[schema(minimum = 60, maximum = 3600)]
196    pub access_token_lifetime_seconds: i32,
197    pub browser_session_reuse: bool,
198    #[schema(minimum = 1)]
199    pub expected_claims_revision: i64,
200    #[schema(minimum = 1)]
201    pub expected_session_revision: i64,
202}
203
204#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
205#[serde(deny_unknown_fields)]
206pub struct ExpectedSecurityRevision {
207    #[schema(minimum = 1)]
208    pub expected_security_revision: i64,
209}
210
211#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
212pub struct ApplicationConfiguration {
213    #[schema(max_items = 50)]
214    pub redirect_uris: Vec<String>,
215    #[schema(max_items = 50)]
216    pub allowed_origins: Vec<String>,
217    #[schema(max_items = 50)]
218    pub publishable_keys: Vec<String>,
219}
220
221#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
222pub struct Application {
223    pub id: String,
224    pub project_id: String,
225    pub public_id: String,
226    #[schema(max_length = 128)]
227    pub display_name: String,
228    pub application_type: ApplicationType,
229    pub status: ApplicationStatus,
230    pub metadata_revision: i64,
231    pub security_revision: i64,
232    pub configuration: ApplicationConfiguration,
233}
234
235#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
236pub struct ApplicationList {
237    #[schema(max_items = 100)]
238    pub items: Vec<Application>,
239}
240
241#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
242#[serde(deny_unknown_fields)]
243pub struct CreateApplicationRequest {
244    #[schema(min_length = 1, max_length = 128)]
245    pub display_name: String,
246    pub application_type: ApplicationType,
247}
248
249#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
250#[serde(deny_unknown_fields)]
251pub struct UpdateApplicationRequest {
252    #[schema(min_length = 1, max_length = 128)]
253    pub display_name: String,
254    #[schema(minimum = 1)]
255    pub expected_metadata_revision: i64,
256}
257
258#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
259#[serde(deny_unknown_fields)]
260pub struct ReplaceApplicationConfigurationRequest {
261    #[schema(max_items = 50)]
262    pub redirect_uris: Vec<String>,
263    #[schema(max_items = 50)]
264    pub allowed_origins: Vec<String>,
265    #[schema(minimum = 1)]
266    pub expected_security_revision: i64,
267}
268
269#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
270#[serde(rename_all = "snake_case")]
271pub enum WebhookEndpointStatus {
272    Pending,
273    Active,
274    Disabled,
275}
276
277#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
278pub enum ApplicationUserEventType {
279    #[serde(rename = "user.projection.created")]
280    Created,
281    #[serde(rename = "user.projection.updated")]
282    Updated,
283    #[serde(rename = "user.projection.disabled")]
284    Disabled,
285}
286
287#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
288#[serde(rename_all = "snake_case")]
289pub enum WebhookDeliveryState {
290    Pending,
291    Leased,
292    Delivered,
293    Terminal,
294    Cancelled,
295}
296
297#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
298#[serde(rename_all = "snake_case")]
299pub enum WebhookDeliveryOutcomeClass {
300    Accepted,
301    Transient,
302    Ambiguous,
303    Permanent,
304}
305
306#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
307pub struct WebhookEndpoint {
308    pub id: String,
309    pub public_id: String,
310    pub project_id: String,
311    pub application_id: String,
312    pub url: String,
313    #[schema(max_items = 3)]
314    pub subscribed_event_types: Vec<ApplicationUserEventType>,
315    pub status: WebhookEndpointStatus,
316    #[schema(minimum = 1)]
317    pub revision: i64,
318    pub current_secret_generation: Option<i32>,
319    pub overlap_secret_generation: Option<i32>,
320    pub overlap_expires_at: Option<String>,
321    pub consecutive_failure_count: i32,
322    pub last_delivery_at: Option<String>,
323    pub last_success_at: Option<String>,
324    pub last_failure_class: Option<String>,
325    pub last_tested_at: Option<String>,
326    pub last_test_succeeded_at: Option<String>,
327    pub created_at: String,
328    pub updated_at: String,
329}
330
331#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
332pub struct WebhookEndpointList {
333    #[schema(max_items = 100)]
334    pub items: Vec<WebhookEndpoint>,
335}
336
337#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
338#[serde(deny_unknown_fields)]
339pub struct CreateWebhookEndpointRequest {
340    pub url: String,
341    #[schema(min_items = 1, max_items = 3)]
342    pub subscribed_event_types: Vec<ApplicationUserEventType>,
343    #[schema(min_length = 32, max_length = 128, write_only)]
344    pub secret: String,
345}
346
347#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
348#[serde(deny_unknown_fields)]
349pub struct UpdateWebhookEndpointRequest {
350    #[schema(min_items = 1, max_items = 3)]
351    pub subscribed_event_types: Vec<ApplicationUserEventType>,
352    #[schema(minimum = 1)]
353    pub expected_revision: i64,
354}
355
356#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
357#[serde(deny_unknown_fields)]
358pub struct ExpectedWebhookEndpointRevision {
359    #[schema(minimum = 1)]
360    pub expected_revision: i64,
361}
362
363#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
364#[serde(deny_unknown_fields)]
365pub struct PrepareWebhookSecretRotationRequest {
366    #[schema(min_length = 32, max_length = 128, write_only)]
367    pub secret: String,
368    #[schema(minimum = 1)]
369    pub expected_revision: i64,
370}
371
372#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
373#[serde(rename_all = "snake_case")]
374pub enum WebhookSecretPreparationStatus {
375    Pending,
376    Provisioned,
377    Terminal,
378}
379
380#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
381pub struct PreparedWebhookSecretRotation {
382    pub endpoint: WebhookEndpoint,
383    #[schema(minimum = 1)]
384    pub generation: i32,
385    pub preparation_status: WebhookSecretPreparationStatus,
386    pub already_active: bool,
387}
388
389#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
390#[serde(deny_unknown_fields)]
391pub struct ActivateWebhookSecretRotationRequest {
392    #[schema(minimum = 1)]
393    pub expected_revision: i64,
394    #[schema(minimum = 300, maximum = 86400)]
395    pub overlap_seconds: i64,
396}
397
398#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)]
399pub struct ApplicationUserEvent {
400    pub event_id: String,
401    pub project_id: String,
402    pub application_id: String,
403    pub user_id: String,
404    pub event_type: ApplicationUserEventType,
405    pub user_revision: i64,
406    pub projection_revision: i64,
407    pub projection_schema: String,
408    pub safe_body: serde_json::Value,
409    pub occurred_at: String,
410}
411
412#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)]
413pub struct ApplicationUserEventList {
414    #[schema(max_items = 100)]
415    pub items: Vec<ApplicationUserEvent>,
416    pub next_cursor: Option<String>,
417}
418
419#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
420pub struct WebhookDelivery {
421    pub id: String,
422    pub endpoint_id: String,
423    pub event_id: String,
424    pub replay_sequence: i32,
425    pub replay_of_delivery_id: Option<String>,
426    pub state: WebhookDeliveryState,
427    pub attempt_count: i32,
428    pub next_attempt_at: String,
429    pub last_outcome_class: Option<WebhookDeliveryOutcomeClass>,
430    pub last_http_status: Option<i32>,
431    pub delivered_at: Option<String>,
432    pub terminal_at: Option<String>,
433    pub created_at: String,
434}
435
436#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
437pub struct WebhookDeliveryList {
438    #[schema(max_items = 100)]
439    pub items: Vec<WebhookDelivery>,
440    pub next_cursor: Option<String>,
441}
442
443#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
444#[serde(deny_unknown_fields)]
445pub struct ReplayWebhookDeliveryRequest {
446    pub confirm: bool,
447}
448
449#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)]
450pub struct SigningKey {
451    pub id: String,
452    pub project_id: String,
453    pub kid: String,
454    pub algorithm: SigningAlgorithm,
455    pub state: SigningKeyState,
456    pub ring_revision: i64,
457    pub signing_epoch: i64,
458    pub sign_not_before: Option<String>,
459    pub verify_not_after: Option<String>,
460    /// Absent until the external signer material has been reconciled.
461    pub public_jwk: Option<PublicJwk>,
462}
463
464#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)]
465pub struct SigningKeyList {
466    #[schema(max_items = 100)]
467    pub items: Vec<SigningKey>,
468}
469
470#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
471#[serde(rename_all = "snake_case")]
472pub enum ProjectServerKeyStatus {
473    Active,
474    Revoked,
475}
476
477/// Safe Control inventory metadata. No credential digest or secret component is exposed.
478#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
479#[serde(deny_unknown_fields)]
480pub struct ProjectServerKey {
481    #[schema(max_length = 36)]
482    pub id: String,
483    #[schema(max_length = 36)]
484    pub project_id: String,
485    #[schema(min_length = 22, max_length = 22, pattern = "^[A-Za-z0-9_-]{22}$")]
486    pub public_key_id: String,
487    #[schema(min_length = 1, max_length = 64)]
488    pub label: String,
489    pub status: ProjectServerKeyStatus,
490    #[schema(minimum = 1)]
491    pub digest_key_version: i32,
492    #[schema(
493        min_length = 36,
494        max_length = 36,
495        pattern = "^owl_server_v1\\.[A-Za-z0-9_-]{22}$"
496    )]
497    pub display_prefix: String,
498    #[schema(minimum = 1)]
499    pub revision: i64,
500    #[schema(max_length = 64)]
501    pub created_at: String,
502    /// Set only after an operator explicitly confirms durable secret-manager storage.
503    #[serde(deserialize_with = "deserialize_required_nullable_string")]
504    #[schema(max_length = 64, required = true)]
505    pub credential_acknowledged_at: Option<String>,
506    #[schema(max_length = 64)]
507    pub last_used_at: Option<String>,
508    #[schema(max_length = 64)]
509    pub revoked_at: Option<String>,
510}
511
512#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
513#[serde(deny_unknown_fields)]
514pub struct ProjectServerKeyList {
515    #[schema(max_items = 100)]
516    pub items: Vec<ProjectServerKey>,
517    #[serde(default)]
518    #[schema(max_length = 64)]
519    pub next_cursor: Option<String>,
520    /// Bounded, secret-free creation gate authority independent of paginated history size.
521    #[serde(deserialize_with = "deserialize_required_nullable_project_server_key")]
522    #[schema(required = true)]
523    pub active_unacknowledged_key: Option<ProjectServerKey>,
524}
525
526#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
527#[serde(deny_unknown_fields)]
528pub struct CreateProjectServerKeyRequest {
529    #[schema(min_length = 1, max_length = 64)]
530    pub label: String,
531}
532
533/// Original successful create response. The credential is never durable and is redacted in Debug.
534#[derive(Clone, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
535#[serde(deny_unknown_fields)]
536pub struct CreateProjectServerKeyResponse {
537    pub key: ProjectServerKey,
538    #[schema(
539        min_length = 80,
540        max_length = 80,
541        pattern = "^owl_server_v1\\.[A-Za-z0-9_-]{22}\\.[A-Za-z0-9_-]{43}$",
542        read_only
543    )]
544    pub credential: String,
545}
546
547impl std::fmt::Debug for CreateProjectServerKeyResponse {
548    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549        formatter
550            .debug_struct("CreateProjectServerKeyResponse")
551            .field("key", &self.key)
552            .field("credential", &"[REDACTED]")
553            .finish()
554    }
555}
556
557impl Drop for CreateProjectServerKeyResponse {
558    fn drop(&mut self) {
559        zeroize::Zeroize::zeroize(&mut self.credential);
560    }
561}
562
563#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
564#[serde(deny_unknown_fields)]
565pub struct AcknowledgeProjectServerKeyDeliveryRequest {
566    #[schema(minimum = 1)]
567    pub expected_revision: i64,
568    /// Explicit assertion that the one-time credential is stored outside `OwlAuth`.
569    pub confirm_stored: bool,
570}
571
572#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
573#[serde(deny_unknown_fields)]
574pub struct RevokeProjectServerKeyRequest {
575    #[schema(minimum = 1)]
576    pub expected_revision: i64,
577    /// Explicit acknowledgement that revocation is immediate and irreversible.
578    pub confirm: bool,
579}
580
581#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
582#[serde(deny_unknown_fields)]
583pub struct RotateSigningKeyRequest {
584    #[schema(minimum = 1)]
585    pub expected_project_revision: i64,
586}
587
588#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
589#[serde(deny_unknown_fields)]
590pub struct KeyTransitionRequest {
591    #[schema(minimum = 1)]
592    pub expected_ring_revision: i64,
593}
594
595#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
596#[allow(
597    clippy::struct_excessive_bools,
598    reason = "generated wire capability flags are independently meaningful and additive"
599)]
600pub struct ProviderManagedProfileCapability {
601    pub supported: bool,
602    pub enabled: bool,
603    #[schema(max_items = 16)]
604    pub exact_scopes: Vec<String>,
605    pub profile_schema: String,
606    pub read_retry_safe: bool,
607    pub renewal_idempotent_replay: bool,
608    pub supports_revocation: bool,
609}
610
611#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
612pub struct Provider {
613    pub id: String,
614    pub project_id: String,
615    #[schema(max_length = 64)]
616    pub provider_key: String,
617    pub kind: ProviderKind,
618    #[schema(max_length = 128)]
619    pub display_name: String,
620    pub issuer: String,
621    pub client_id: String,
622    pub callback_url: String,
623    pub status: ProviderStatus,
624    pub revision: i64,
625    /// Whether this adapter can be selected for ordinary Runtime login.
626    pub login_supported: bool,
627    /// Whether this adapter can serve as an identity-mutation proof authority.
628    pub identity_proof_supported: bool,
629    pub managed_profile: ProviderManagedProfileCapability,
630    /// Whether a durable protected-secret replacement awaits reconciliation or abandonment.
631    pub secret_replacement_pending: bool,
632    #[schema(max_items = 100)]
633    pub assigned_application_ids: Vec<String>,
634}
635
636#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
637pub struct ProviderList {
638    #[schema(max_items = 100)]
639    pub items: Vec<Provider>,
640}
641
642#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
643#[serde(rename_all = "snake_case")]
644pub enum ProviderEgressMode {
645    AllowAll,
646    ExactOrigins,
647}
648
649#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
650pub struct ProviderEgressPolicy {
651    pub project_id: String,
652    pub mode: ProviderEgressMode,
653    #[schema(max_items = 1024)]
654    pub exact_origins: Vec<String>,
655    #[schema(minimum = 1)]
656    pub revision: i64,
657}
658
659#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
660#[serde(deny_unknown_fields)]
661pub struct UpdateProviderEgressPolicyRequest {
662    pub mode: ProviderEgressMode,
663    #[schema(max_items = 1024)]
664    pub exact_origins: Vec<String>,
665    #[schema(minimum = 1)]
666    pub expected_revision: i64,
667}
668
669#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
670#[serde(deny_unknown_fields)]
671pub struct OidcPreflightRequest {
672    #[schema(min_length = 1, max_length = 64)]
673    pub provider_key: String,
674    #[schema(min_length = 8, max_length = 2048)]
675    pub issuer: String,
676}
677
678#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
679#[allow(
680    clippy::struct_excessive_bools,
681    reason = "the public diagnostic reports four independent reviewed OIDC capabilities"
682)]
683pub struct OidcPreflightResult {
684    pub canonical_issuer: String,
685    pub callback_url: String,
686    pub callback_guidance: ProviderCallbackGuidance,
687    #[schema(max_items = 8)]
688    pub admitted_endpoint_origins: Vec<String>,
689    #[schema(max_items = 8)]
690    pub exact_scopes: Vec<String>,
691    pub authorization_code_supported: bool,
692    pub pkce_s256_supported: bool,
693    pub rs256_id_tokens_supported: bool,
694    pub managed_profile_supported: bool,
695    pub policy_mode: ProviderEgressMode,
696    #[schema(minimum = 1)]
697    pub policy_revision: i64,
698}
699
700#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
701#[serde(deny_unknown_fields)]
702pub struct NamedProviderPreflightRequest {
703    /// Named server-owned adapter profile. Custom OIDC is rejected.
704    pub kind: ProviderKind,
705    #[schema(min_length = 1, max_length = 64)]
706    pub provider_key: String,
707}
708
709#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
710#[serde(rename_all = "snake_case")]
711pub enum ProviderCallbackGuidance {
712    RegisterExactRedirectUri,
713}
714
715#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
716#[serde(rename_all = "snake_case")]
717pub enum ProviderConsentBehavior {
718    Standard,
719    ExplicitOfflineConsent,
720}
721
722#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
723pub struct FixedProviderAuthorizationPolicy {
724    #[schema(max_items = 8)]
725    pub exact_scopes: Vec<String>,
726    pub consent_behavior: ProviderConsentBehavior,
727}
728
729#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
730pub struct NamedProviderPreflightResult {
731    pub kind: ProviderKind,
732    pub issuer: String,
733    pub callback_url: String,
734    pub callback_guidance: ProviderCallbackGuidance,
735    pub login: FixedProviderAuthorizationPolicy,
736    pub managed_profile: Option<FixedProviderAuthorizationPolicy>,
737}
738
739#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
740#[serde(deny_unknown_fields)]
741pub struct CreateProviderRequest {
742    /// Closed server-owned adapter profile.
743    pub kind: ProviderKind,
744    #[schema(min_length = 1, max_length = 64)]
745    pub provider_key: String,
746    #[schema(min_length = 1, max_length = 128)]
747    pub display_name: String,
748    /// Required for Custom OIDC and forbidden for named profiles.
749    #[schema(min_length = 8, max_length = 2048)]
750    pub issuer: Option<String>,
751    #[schema(min_length = 1, max_length = 512)]
752    pub client_id: String,
753    #[schema(write_only, min_length = 1, max_length = 4096)]
754    pub client_secret: String,
755    /// Enables only adapter-declared fixed least scopes; callers cannot supply scopes.
756    #[serde(default)]
757    pub managed_profile_enabled: bool,
758    #[schema(minimum = 1)]
759    pub expected_project_revision: i64,
760}
761
762#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
763#[serde(deny_unknown_fields)]
764pub struct UpdateProviderRequest {
765    #[schema(min_length = 1, max_length = 128)]
766    pub display_name: String,
767    #[schema(min_length = 1, max_length = 512)]
768    pub client_id: String,
769    #[schema(minimum = 1)]
770    pub expected_provider_revision: i64,
771}
772
773#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
774#[serde(deny_unknown_fields)]
775pub struct ReplaceProviderSecretRequest {
776    #[schema(min_length = 1, max_length = 128)]
777    pub display_name: String,
778    #[schema(min_length = 1, max_length = 512)]
779    pub client_id: String,
780    #[schema(write_only, min_length = 1, max_length = 4096)]
781    pub client_secret: String,
782    #[schema(minimum = 1)]
783    pub expected_provider_revision: i64,
784}
785
786#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
787#[serde(deny_unknown_fields)]
788pub struct ReconcileProviderRequest {
789    #[schema(write_only, min_length = 1, max_length = 4096)]
790    pub client_secret: String,
791    #[schema(minimum = 1)]
792    pub expected_project_revision: i64,
793}
794
795#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
796#[serde(deny_unknown_fields)]
797pub struct ReconcileProviderSecretReplacementRequest {
798    #[schema(write_only, min_length = 1, max_length = 4096)]
799    pub client_secret: String,
800    #[schema(minimum = 1)]
801    pub expected_provider_revision: i64,
802}
803
804#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
805#[serde(deny_unknown_fields)]
806pub struct ProviderRevisionRequest {
807    #[schema(minimum = 1)]
808    pub expected_provider_revision: i64,
809}
810
811#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
812#[serde(deny_unknown_fields)]
813pub struct ProviderAssignmentRequest {
814    #[schema(minimum = 1)]
815    pub expected_application_revision: i64,
816}
817
818#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
819#[serde(rename_all = "snake_case")]
820pub enum SmtpTlsMode {
821    ImplicitTls,
822    StarttlsRequired,
823}
824
825#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
826#[serde(rename_all = "snake_case")]
827pub enum SmtpGenerationStatus {
828    Reconciled,
829    Pending,
830    Active,
831    Retained,
832    Disabled,
833    Compromised,
834    Retired,
835}
836
837#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
838#[allow(
839    clippy::struct_excessive_bools,
840    reason = "generated Control policy DTO preserves explicit independent switches"
841)]
842pub struct EmailMethodPolicy {
843    pub project_id: String,
844    pub enabled: bool,
845    pub policy_revision: i64,
846    pub security_revision: i64,
847    pub otp_enabled: bool,
848    pub magic_link_enabled: bool,
849    #[schema(minimum = 6, maximum = 10)]
850    pub otp_digits: i16,
851    #[schema(minimum = 30, maximum = 600)]
852    pub otp_validity_seconds: i32,
853    #[schema(minimum = 1, maximum = 5)]
854    pub otp_max_attempts: i16,
855    #[schema(minimum = 30, maximum = 600)]
856    pub resend_after_seconds: i32,
857    #[schema(minimum = 1, maximum = 5)]
858    pub max_generations: i16,
859    #[schema(minimum = 30, maximum = 600)]
860    pub magic_validity_seconds: i32,
861    pub signup_enabled: bool,
862    pub transferred_magic_link_enabled: bool,
863    pub allow_deployment_default: bool,
864}
865
866#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
867#[serde(deny_unknown_fields)]
868#[allow(
869    clippy::struct_excessive_bools,
870    reason = "generated Control policy DTO preserves explicit independent switches"
871)]
872pub struct UpdateEmailMethodPolicyRequest {
873    pub enabled: bool,
874    pub otp_enabled: bool,
875    pub magic_link_enabled: bool,
876    pub otp_digits: i16,
877    pub otp_validity_seconds: i32,
878    pub otp_max_attempts: i16,
879    pub resend_after_seconds: i32,
880    pub max_generations: i16,
881    pub magic_validity_seconds: i32,
882    pub signup_enabled: bool,
883    pub transferred_magic_link_enabled: bool,
884    pub allow_deployment_default: bool,
885    pub expected_policy_revision: i64,
886    pub expected_security_revision: i64,
887}
888
889#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
890#[serde(deny_unknown_fields)]
891pub struct EmailAssignmentRequest {
892    pub enabled: bool,
893    pub expected_application_security_revision: i64,
894}
895
896#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
897pub struct EmailAssignment {
898    pub project_id: String,
899    pub application_id: String,
900    pub enabled: bool,
901    #[schema(minimum = 1)]
902    pub security_revision: i64,
903}
904
905#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
906pub struct EmailAssignmentList {
907    #[schema(max_items = 100)]
908    pub items: Vec<EmailAssignment>,
909}
910
911#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
912pub struct SmtpConfiguration {
913    pub id: String,
914    pub project_id: String,
915    pub generation: i32,
916    pub revision: i64,
917    pub security_eligibility_revision: i64,
918    pub status: SmtpGenerationStatus,
919    #[schema(max_length = 253)]
920    pub host: String,
921    pub port: u16,
922    pub tls_mode: SmtpTlsMode,
923    #[schema(max_length = 254)]
924    pub sender_address: String,
925    #[schema(max_length = 128)]
926    pub sender_name: Option<String>,
927    #[schema(max_length = 254)]
928    pub reply_to: Option<String>,
929    #[schema(max_length = 64)]
930    pub retained_until: Option<String>,
931    #[schema(max_length = 64)]
932    pub safe_fingerprint: Option<String>,
933}
934
935#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
936pub struct DeploymentSmtpGeneration {
937    pub generation: i32,
938    pub revision: i64,
939    pub security_eligibility_revision: i64,
940    pub status: SmtpGenerationStatus,
941    #[schema(max_length = 253)]
942    pub host: String,
943    pub port: u16,
944    pub tls_mode: SmtpTlsMode,
945    #[schema(max_length = 254)]
946    pub sender_address: String,
947    #[schema(max_length = 64)]
948    pub retained_until: Option<String>,
949    #[schema(max_length = 64)]
950    pub safe_fingerprint: String,
951    #[schema(max_items = 16)]
952    pub explicitly_allowed_private_ips: Vec<String>,
953}
954
955#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
956pub struct DeploymentSmtpGenerationList {
957    #[schema(max_items = 32)]
958    pub items: Vec<DeploymentSmtpGeneration>,
959}
960
961#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
962pub struct SmtpConfigurationList {
963    #[schema(max_items = 32)]
964    pub items: Vec<SmtpConfiguration>,
965}
966
967#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
968#[serde(deny_unknown_fields)]
969pub struct ReconcileDeploymentSmtpRequest {
970    #[schema(write_only, min_length = 2, max_length = 4096)]
971    pub credential: String,
972}
973
974#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
975#[serde(deny_unknown_fields)]
976pub struct CreateSmtpConfigurationRequest {
977    #[schema(max_length = 253)]
978    pub host: String,
979    pub port: u16,
980    pub tls_mode: SmtpTlsMode,
981    #[schema(max_length = 254)]
982    pub sender_address: String,
983    #[schema(max_length = 128)]
984    pub sender_name: Option<String>,
985    #[schema(max_length = 254)]
986    pub reply_to: Option<String>,
987    #[schema(write_only, min_length = 2, max_length = 4096)]
988    pub credential: String,
989    pub expected_project_security_revision: i64,
990}
991
992#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
993#[serde(deny_unknown_fields)]
994pub struct SmtpRevisionRequest {
995    pub expected_revision: i64,
996}
997
998#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
999#[serde(deny_unknown_fields)]
1000pub struct TestSmtpConfigurationRequest {
1001    #[schema(max_length = 254)]
1002    pub recipient: String,
1003    pub expected_revision: i64,
1004}
1005
1006#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1007pub struct SmtpTestOperation {
1008    pub id: String,
1009    pub project_id: String,
1010    pub smtp_configuration_id: String,
1011    pub status: String,
1012    pub outcome: Option<String>,
1013    pub created_at: String,
1014    pub completed_at: Option<String>,
1015}
1016
1017#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1018#[serde(rename_all = "snake_case")]
1019pub enum ManagedProviderConnectionState {
1020    Active,
1021    ReauthRequired,
1022    Revoked,
1023    Disconnected,
1024}
1025
1026#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1027pub struct ManagedProviderConnection {
1028    pub id: String,
1029    pub project_id: String,
1030    pub provider_id: String,
1031    pub identity_id: String,
1032    pub user_id: String,
1033    pub state: ManagedProviderConnectionState,
1034    pub revision: i64,
1035    pub generation: i64,
1036    pub credential_generation: i64,
1037    pub capability_key: String,
1038    #[schema(max_items = 16)]
1039    pub required_scopes: Vec<String>,
1040    pub source_schema: String,
1041    pub supports_revocation: bool,
1042    #[schema(max_items = 100)]
1043    pub reauthorization_application_ids: Vec<String>,
1044    pub last_safe_outcome: String,
1045    pub last_synchronized_at: Option<String>,
1046    pub next_synchronize_at: Option<String>,
1047    pub next_renewal_at: Option<String>,
1048    pub consecutive_failures: i32,
1049}
1050
1051#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1052pub struct ManagedProviderConnectionList {
1053    #[schema(max_items = 100)]
1054    pub items: Vec<ManagedProviderConnection>,
1055}
1056
1057#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1058#[serde(deny_unknown_fields)]
1059pub struct ManagedProviderConnectionActionRequest {
1060    #[schema(minimum = 1)]
1061    pub expected_revision: i64,
1062    #[schema(minimum = 1)]
1063    pub expected_generation: i64,
1064    /// Required for destructive disconnect/revoke actions.
1065    #[serde(default)]
1066    pub confirm: bool,
1067}
1068
1069#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1070#[serde(rename_all = "snake_case")]
1071pub enum ManagedReauthorizationStatus {
1072    AwaitingBrowserBinding,
1073    AwaitingProviderStart,
1074    ProviderAuthorizationStarted,
1075    ProviderExchangeInProgress,
1076    Completed,
1077    ProviderExchangeFailed,
1078    Expired,
1079    Cancelled,
1080}
1081
1082#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1083pub struct ManagedReauthorization {
1084    pub id: String,
1085    pub project_id: String,
1086    pub user_id: String,
1087    pub connection_id: String,
1088    pub provider_key: String,
1089    pub application_id: String,
1090    pub status: ManagedReauthorizationStatus,
1091    #[schema(minimum = 1)]
1092    pub revision: i64,
1093    pub expires_at: String,
1094}
1095
1096#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1097#[serde(deny_unknown_fields)]
1098pub struct CreateManagedReauthorizationRequest {
1099    pub application_id: String,
1100    #[schema(minimum = 1)]
1101    pub expected_connection_revision: i64,
1102    #[schema(minimum = 1)]
1103    pub expected_connection_generation: i64,
1104    #[schema(minimum = 1)]
1105    pub expected_credential_generation: i64,
1106}
1107
1108#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1109pub struct CreateManagedReauthorizationResponse {
1110    #[serde(flatten)]
1111    pub interaction: ManagedReauthorization,
1112    /// Present only on create or identical idempotency replay through expiry.
1113    pub hosted_target: Option<String>,
1114}
1115
1116#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1117#[serde(deny_unknown_fields)]
1118pub struct CancelManagedReauthorizationRequest {
1119    #[schema(minimum = 1)]
1120    pub expected_revision: i64,
1121}
1122
1123#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1124#[serde(deny_unknown_fields)]
1125pub struct IdentityMutationUserTarget {
1126    pub user_id: String,
1127    #[schema(minimum = 1)]
1128    pub expected_user_revision: i64,
1129    #[schema(minimum = 1)]
1130    pub expected_user_security_revision: i64,
1131}
1132
1133#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1134#[serde(tag = "identity_kind", rename_all = "snake_case", deny_unknown_fields)]
1135pub enum ExistingIdentityReference {
1136    Provider {
1137        identity_id: String,
1138        #[schema(minimum = 1)]
1139        expected_identity_revision: i64,
1140    },
1141    Email {
1142        identity_id: String,
1143        #[schema(minimum = 1)]
1144        expected_identity_revision: i64,
1145    },
1146}
1147
1148#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1149#[serde(tag = "method_kind", rename_all = "snake_case", deny_unknown_fields)]
1150pub enum IdentityMutationProofAuthority {
1151    Provider {
1152        application_id: String,
1153        provider_id: String,
1154    },
1155    Email {
1156        application_id: String,
1157    },
1158}
1159
1160#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1161#[serde(tag = "disposition", rename_all = "snake_case", deny_unknown_fields)]
1162pub enum UnlinkPrimarySourceDisposition {
1163    Preserve,
1164    Clear,
1165    Provider {
1166        identity_id: String,
1167        #[schema(minimum = 1)]
1168        expected_identity_revision: i64,
1169    },
1170    Email {
1171        identity_id: String,
1172        #[schema(minimum = 1)]
1173        expected_identity_revision: i64,
1174    },
1175}
1176
1177#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1178#[serde(tag = "identity_kind", rename_all = "snake_case", deny_unknown_fields)]
1179pub enum MergePrimarySource {
1180    Provider {
1181        identity_id: String,
1182        #[schema(minimum = 1)]
1183        expected_identity_revision: i64,
1184    },
1185    Email {
1186        identity_id: String,
1187        #[schema(minimum = 1)]
1188        expected_identity_revision: i64,
1189    },
1190}
1191
1192#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1193#[serde(rename_all = "snake_case")]
1194pub enum MergeSessionsDisposition {
1195    LoserRevoked,
1196}
1197
1198#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1199#[serde(rename_all = "snake_case")]
1200pub enum MergeBindingsDisposition {
1201    WinnerPreferred,
1202}
1203
1204/// A typed identity mutation plan. Mandatory proof slots and all authority revisions are derived
1205/// by the server; callers can neither provide slots nor override their purposes.
1206#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1207#[serde(tag = "operation_kind", rename_all = "snake_case", deny_unknown_fields)]
1208pub enum CreateIdentityMutationIntentRequest {
1209    Link {
1210        destination: IdentityMutationUserTarget,
1211        destination_identity: ExistingIdentityReference,
1212        candidate_identity_kind: IdentityKind,
1213        destination_proof_authority: IdentityMutationProofAuthority,
1214        candidate_proof_authority: IdentityMutationProofAuthority,
1215    },
1216    Unlink {
1217        owner: IdentityMutationUserTarget,
1218        identity: ExistingIdentityReference,
1219        proof_authority: IdentityMutationProofAuthority,
1220        primary_source_disposition: UnlinkPrimarySourceDisposition,
1221    },
1222    Merge {
1223        winner: IdentityMutationUserTarget,
1224        winner_identity: ExistingIdentityReference,
1225        winner_proof_authority: IdentityMutationProofAuthority,
1226        loser: IdentityMutationUserTarget,
1227        loser_identity: ExistingIdentityReference,
1228        loser_proof_authority: IdentityMutationProofAuthority,
1229        primary_source: MergePrimarySource,
1230        sessions_disposition: MergeSessionsDisposition,
1231        bindings_disposition: MergeBindingsDisposition,
1232    },
1233}
1234
1235#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1236#[serde(rename_all = "snake_case")]
1237pub enum IdentityMutationOperationKind {
1238    Link,
1239    Unlink,
1240    Merge,
1241}
1242
1243#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1244#[serde(rename_all = "snake_case")]
1245pub enum IdentityMutationIntentStatus {
1246    PendingProof,
1247    Ready,
1248    Completed,
1249    Expired,
1250    Cancelled,
1251}
1252
1253#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1254#[serde(rename_all = "snake_case")]
1255pub enum IdentityMutationProofRole {
1256    DestinationOwner,
1257    CandidateIdentity,
1258    IdentityOwner,
1259    WinnerOwner,
1260    LoserOwner,
1261}
1262
1263#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1264pub struct IdentityMutationProofSlot {
1265    pub id: String,
1266    pub role: IdentityMutationProofRole,
1267    pub identity_kind: IdentityKind,
1268    pub method_kind: IdentityMutationMethodKind,
1269    pub proved: bool,
1270}
1271
1272#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1273pub struct IdentityMutationIntent {
1274    pub id: String,
1275    pub project_id: String,
1276    pub operation_kind: IdentityMutationOperationKind,
1277    pub status: IdentityMutationIntentStatus,
1278    #[schema(minimum = 1)]
1279    pub revision: i64,
1280    #[schema(max_length = 64)]
1281    pub effective_expires_at: String,
1282    #[schema(min_items = 1, max_items = 2)]
1283    pub slots: Vec<IdentityMutationProofSlot>,
1284}
1285
1286#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1287pub struct CreateIdentityMutationIntentResponse {
1288    #[serde(flatten)]
1289    pub intent: IdentityMutationIntent,
1290    /// Present only on create or identical idempotency replay through effective expiry.
1291    #[schema(max_length = 512)]
1292    pub hosted_target: Option<String>,
1293}
1294
1295#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1296#[serde(deny_unknown_fields)]
1297pub struct CancelIdentityMutationIntentRequest {
1298    #[schema(minimum = 1)]
1299    pub expected_revision: i64,
1300}
1301
1302#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1303#[serde(rename_all = "snake_case")]
1304pub enum LinkIdentityMutationConfirmation {
1305    LinkIdentity,
1306}
1307
1308#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1309#[serde(rename_all = "snake_case")]
1310pub enum UnlinkIdentityMutationConfirmation {
1311    UnlinkIdentity,
1312}
1313
1314#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1315#[serde(rename_all = "snake_case")]
1316pub enum MergeIdentityMutationConfirmation {
1317    MergeUsers,
1318}
1319
1320#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1321#[serde(tag = "operation_kind", rename_all = "snake_case", deny_unknown_fields)]
1322pub enum ConfirmIdentityMutationIntentRequest {
1323    Link {
1324        #[schema(minimum = 1)]
1325        expected_revision: i64,
1326        confirmation: LinkIdentityMutationConfirmation,
1327    },
1328    Unlink {
1329        #[schema(minimum = 1)]
1330        expected_revision: i64,
1331        confirmation: UnlinkIdentityMutationConfirmation,
1332    },
1333    Merge {
1334        #[schema(minimum = 1)]
1335        expected_revision: i64,
1336        confirmation: MergeIdentityMutationConfirmation,
1337    },
1338}
1339
1340#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1341#[serde(rename_all = "snake_case")]
1342pub enum ProjectUserStatus {
1343    Active,
1344    Disabled,
1345    Merged,
1346}
1347
1348#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1349#[serde(rename_all = "snake_case")]
1350pub enum ProjectUserSort {
1351    CreatedNewest,
1352    CreatedOldest,
1353}
1354
1355#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1356#[serde(rename_all = "snake_case")]
1357pub enum ProjectUserIdentityFilter {
1358    Provider,
1359    Email,
1360}
1361
1362#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1363#[serde(rename_all = "snake_case")]
1364pub enum ManagedSessionStatus {
1365    Active,
1366    Revoked,
1367    Expired,
1368}
1369
1370#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1371pub struct ProjectUser {
1372    pub id: String,
1373    pub project_id: String,
1374    #[schema(max_length = 96)]
1375    pub public_id: String,
1376    pub status: ProjectUserStatus,
1377    #[schema(minimum = 1)]
1378    pub user_revision: i64,
1379    #[schema(minimum = 1)]
1380    pub security_revision: i64,
1381    #[schema(max_length = 256)]
1382    pub display_name: Option<String>,
1383    #[schema(max_length = 2048)]
1384    pub picture_url: Option<String>,
1385    #[schema(max_length = 64)]
1386    pub created_at: String,
1387    #[schema(max_length = 64)]
1388    pub updated_at: String,
1389}
1390
1391#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1392pub struct ProjectUserList {
1393    #[schema(max_items = 100)]
1394    pub items: Vec<ProjectUser>,
1395    #[schema(max_length = 36)]
1396    pub next_cursor: Option<String>,
1397}
1398
1399#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1400#[serde(deny_unknown_fields)]
1401pub struct ProjectUserEmailLookupRequest {
1402    #[schema(min_length = 3, max_length = 320)]
1403    pub email: String,
1404}
1405
1406#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1407#[serde(deny_unknown_fields)]
1408pub struct ProjectUserLookup {
1409    #[serde(deserialize_with = "deserialize_required_nullable_project_user")]
1410    #[schema(required = true)]
1411    pub user: Option<ProjectUser>,
1412}
1413
1414#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1415#[serde(rename_all = "snake_case")]
1416pub enum ProjectUserIdentityStatus {
1417    Active,
1418    Disabled,
1419}
1420
1421#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1422#[serde(rename_all = "snake_case")]
1423pub enum RedactedEmailMarker {
1424    Redacted,
1425}
1426
1427/// Safe presentation only. `provider_key` is immutable creation provenance, not current provider
1428/// authority. Email presentation is a fixed marker and never an address or reversible material.
1429#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1430#[serde(tag = "identity_kind", rename_all = "snake_case")]
1431pub enum ProjectUserIdentityPresentation {
1432    Provider {
1433        #[schema(max_length = 64)]
1434        provider_key: String,
1435    },
1436    Email {
1437        address: RedactedEmailMarker,
1438    },
1439}
1440
1441#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1442pub struct ProjectUserIdentity {
1443    pub id: String,
1444    pub project_id: String,
1445    pub user_id: String,
1446    pub status: ProjectUserIdentityStatus,
1447    #[schema(minimum = 1)]
1448    pub identity_revision: i64,
1449    pub is_primary_source: bool,
1450    #[serde(flatten)]
1451    pub presentation: ProjectUserIdentityPresentation,
1452    #[schema(max_length = 64)]
1453    pub verified_or_observed_at: String,
1454    #[schema(max_length = 64)]
1455    pub created_at: String,
1456    #[schema(max_length = 64)]
1457    pub updated_at: String,
1458}
1459
1460#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1461pub struct ProjectUserIdentityList {
1462    #[schema(max_items = 100)]
1463    pub items: Vec<ProjectUserIdentity>,
1464}
1465
1466#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1467pub struct ApplicationSession {
1468    pub id: String,
1469    pub project_id: String,
1470    pub user_id: String,
1471    pub application_id: String,
1472    #[schema(max_length = 96)]
1473    pub application_public_id: String,
1474    #[schema(max_length = 128)]
1475    pub application_display_name: String,
1476    pub browser_session_id: Option<String>,
1477    pub status: ManagedSessionStatus,
1478    #[schema(minimum = 1)]
1479    pub session_revision: i64,
1480    #[schema(max_length = 64)]
1481    pub authenticated_at: String,
1482    #[schema(max_length = 64)]
1483    pub absolute_expires_at: String,
1484    #[schema(max_length = 64)]
1485    pub revoked_at: Option<String>,
1486    #[schema(max_length = 64)]
1487    pub created_at: String,
1488    #[schema(max_length = 64)]
1489    pub updated_at: String,
1490}
1491
1492#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1493pub struct BrowserSession {
1494    pub id: String,
1495    pub project_id: String,
1496    pub user_id: String,
1497    pub status: ManagedSessionStatus,
1498    #[schema(minimum = 1)]
1499    pub session_revision: i64,
1500    #[schema(max_length = 64)]
1501    pub authenticated_at: String,
1502    #[schema(max_length = 64)]
1503    pub last_activity_at: String,
1504    #[schema(max_length = 64)]
1505    pub idle_expires_at: String,
1506    #[schema(max_length = 64)]
1507    pub absolute_expires_at: String,
1508    #[schema(max_length = 64)]
1509    pub terminated_at: Option<String>,
1510    #[schema(max_length = 64)]
1511    pub created_at: String,
1512    #[schema(max_length = 64)]
1513    pub updated_at: String,
1514}
1515
1516#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1517pub struct ProjectUserSessions {
1518    #[schema(max_items = 100)]
1519    pub application_sessions: Vec<ApplicationSession>,
1520    #[schema(max_items = 100)]
1521    pub browser_sessions: Vec<BrowserSession>,
1522}
1523
1524#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
1525#[serde(deny_unknown_fields)]
1526pub struct ExpectedSessionRevision {
1527    /// Exact current revision. A terminal session with this revision is returned unchanged;
1528    /// a stale revision conflicts.
1529    #[schema(minimum = 1)]
1530    pub expected_session_revision: i64,
1531}
1532
1533macro_rules! control_path {
1534    ($name:ident, $method:ident, $path:literal, $response:ty, $summary:literal $(, body = $body:ty)? $(, params($($params:tt)*))?) => {
1535        #[utoipa::path(
1536            $method,
1537            path = $path,
1538            $(request_body = $body,)?
1539            responses(
1540                (status = 200, description = $summary, body = $response),
1541                (status = 400, description = "Invalid request", body = ProblemDetails, content_type = "application/problem+json"),
1542                (status = 401, description = "Missing or invalid operator API key", body = ProblemDetails, content_type = "application/problem+json", headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))),
1543                (status = 404, description = "Resource not found", body = ProblemDetails, content_type = "application/problem+json"),
1544                (status = 409, description = "Revision, state, idempotency, or capacity conflict", body = ProblemDetails, content_type = "application/problem+json"),
1545                (status = 500, description = "Stored authority data violated an invariant", body = ProblemDetails, content_type = "application/problem+json"),
1546                (status = 503, description = "Required authority unavailable", body = ProblemDetails, content_type = "application/problem+json")
1547            ),
1548            $(params($($params)*),)?
1549            security(("operator_api_key" = []))
1550        )]
1551        #[doc(hidden)]
1552        pub fn $name() {}
1553    };
1554}
1555
1556macro_rules! control_preflight_path {
1557    ($name:ident, $path:literal, $response:ty, $summary:literal, $body:ty) => {
1558        #[utoipa::path(
1559            post,
1560            path = $path,
1561            request_body = $body,
1562            responses(
1563                (status = 200, description = $summary, body = $response),
1564                (status = 400, description = "Invalid request", body = ProblemDetails, content_type = "application/problem+json"),
1565                (status = 401, description = "Missing or invalid operator API key", body = ProblemDetails, content_type = "application/problem+json", headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))),
1566                (status = 404, description = "Resource not found", body = ProblemDetails, content_type = "application/problem+json"),
1567                (status = 409, description = "Revision, state, idempotency, or capacity conflict", body = ProblemDetails, content_type = "application/problem+json"),
1568                (status = 422, description = "Provider metadata or policy rejected", body = ProblemDetails, content_type = "application/problem+json"),
1569                (status = 500, description = "Stored authority data violated an invariant", body = ProblemDetails, content_type = "application/problem+json"),
1570                (status = 503, description = "Required authority unavailable", body = ProblemDetails, content_type = "application/problem+json")
1571            ),
1572            params(("project_id" = String, Path)),
1573            security(("operator_api_key" = []))
1574        )]
1575        #[doc(hidden)]
1576        pub fn $name() {}
1577    };
1578}
1579
1580#[utoipa::path(
1581    post,
1582    path = "/v1/projects/{project_id}/smtp-configurations/{smtp_id}/test",
1583    request_body = TestSmtpConfigurationRequest,
1584    responses(
1585        (
1586            status = 202,
1587            description = "Enqueued bounded SMTP test",
1588            body = SmtpTestOperation,
1589            headers(("Location" = String, description = "Exact Control path for the SMTP test operation"))
1590        ),
1591        (status = 400, description = "Invalid request", body = ProblemDetails, content_type = "application/problem+json"),
1592        (status = 401, description = "Missing or invalid operator API key", body = ProblemDetails, content_type = "application/problem+json", headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))),
1593        (status = 404, description = "Resource not found", body = ProblemDetails, content_type = "application/problem+json"),
1594        (status = 409, description = "Revision, state, idempotency, or capacity conflict", body = ProblemDetails, content_type = "application/problem+json"),
1595        (status = 500, description = "Stored authority data violated an invariant", body = ProblemDetails, content_type = "application/problem+json"),
1596        (status = 503, description = "Required authority unavailable", body = ProblemDetails, content_type = "application/problem+json")
1597    ),
1598    params(
1599        ("project_id" = String, Path),
1600        ("smtp_id" = String, Path),
1601        ("Idempotency-Key" = String, Header)
1602    ),
1603    security(("operator_api_key" = []))
1604)]
1605#[doc(hidden)]
1606pub fn test_smtp_configuration() {}
1607
1608control_path!(
1609    list_projects,
1610    get,
1611    "/v1/projects",
1612    ProjectList,
1613    "Projects",
1614    params(("belongs_to" = Option<String>, Query))
1615);
1616control_path!(
1617    create_project,
1618    post,
1619    "/v1/projects",
1620    Project,
1621    "Created or authoritatively replayed project",
1622    body = CreateProjectRequest,
1623    params(("Idempotency-Key" = String, Header))
1624);
1625control_path!(
1626    get_project,
1627    get,
1628    "/v1/projects/{project_id}",
1629    Project,
1630    "Project",
1631    params(("project_id" = String, Path))
1632);
1633control_path!(
1634    update_project,
1635    patch,
1636    "/v1/projects/{project_id}",
1637    Project,
1638    "Updated project",
1639    body = UpdateProjectRequest,
1640    params(("project_id" = String, Path))
1641);
1642control_path!(
1643    get_project_overview,
1644    get,
1645    "/v1/projects/{project_id}/overview",
1646    ProjectOverviewSummary,
1647    "Project resource overview",
1648    params(("project_id" = String, Path))
1649);
1650control_path!(
1651    get_project_policy,
1652    get,
1653    "/v1/projects/{project_id}/policy",
1654    ProjectPolicy,
1655    "Project policy",
1656    params(("project_id" = String, Path))
1657);
1658control_path!(
1659    update_project_policy,
1660    put,
1661    "/v1/projects/{project_id}/policy",
1662    ProjectPolicy,
1663    "Updated Project policy",
1664    body = UpdateProjectPolicyRequest,
1665    params(("project_id" = String, Path))
1666);
1667control_path!(
1668    disable_project,
1669    post,
1670    "/v1/projects/{project_id}/disable",
1671    Project,
1672    "Disabled project",
1673    body = ExpectedSecurityRevision,
1674    params(("project_id" = String, Path))
1675);
1676control_path!(
1677    enable_project,
1678    post,
1679    "/v1/projects/{project_id}/enable",
1680    Project,
1681    "Enabled project",
1682    body = ExpectedSecurityRevision,
1683    params(("project_id" = String, Path))
1684);
1685#[utoipa::path(
1686    delete,
1687    path = "/v1/projects/{project_id}",
1688    request_body = ExpectedSecurityRevision,
1689    responses(
1690        (status = 202, description = "Project deletion accepted and immediately fenced", body = Project),
1691        (status = 400, description = "Invalid request", body = ProblemDetails, content_type = "application/problem+json"),
1692        (status = 401, description = "Missing or invalid operator API key", body = ProblemDetails, content_type = "application/problem+json", headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))),
1693        (status = 404, description = "Resource not found", body = ProblemDetails, content_type = "application/problem+json"),
1694        (status = 409, description = "Revision or state conflict", body = ProblemDetails, content_type = "application/problem+json"),
1695        (status = 500, description = "Stored authority data violated an invariant", body = ProblemDetails, content_type = "application/problem+json"),
1696        (status = 503, description = "Required authority unavailable", body = ProblemDetails, content_type = "application/problem+json")
1697    ),
1698    params(("project_id" = String, Path)),
1699    security(("operator_api_key" = []))
1700)]
1701#[doc(hidden)]
1702pub fn delete_project() {}
1703control_path!(
1704    list_project_server_keys,
1705    get,
1706    "/v1/projects/{project_id}/server-keys",
1707    ProjectServerKeyList,
1708    "Safe Project server-key metadata",
1709    params(
1710        ("project_id" = String, Path),
1711        ("cursor" = Option<String>, Query),
1712        ("limit" = Option<usize>, Query, minimum = 1, maximum = 100)
1713    )
1714);
1715control_path!(
1716    get_project_server_key,
1717    get,
1718    "/v1/projects/{project_id}/server-keys/{key_id}",
1719    ProjectServerKey,
1720    "Safe metadata for one Project server key",
1721    params(("project_id" = String, Path), ("key_id" = String, Path))
1722);
1723
1724#[utoipa::path(
1725    post,
1726    path = "/v1/projects/{project_id}/server-keys",
1727    request_body = CreateProjectServerKeyRequest,
1728    responses(
1729        (status = 201, description = "Created Project server key with one-time credential reveal", body = CreateProjectServerKeyResponse, headers(("Location" = String, description = "Exact Control path for the created Project server key"))),
1730        (status = 400, description = "Invalid request", body = ProblemDetails, content_type = "application/problem+json"),
1731        (status = 401, description = "Missing or invalid operator API key", body = ProblemDetails, content_type = "application/problem+json", headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))),
1732        (status = 404, description = "Resource not found", body = ProblemDetails, content_type = "application/problem+json"),
1733        (status = 409, description = "Unacknowledged delivery, key limit, idempotency conflict, or secret unavailable on replay", body = ProblemDetails),
1734        (status = 500, description = "Stored authority data violated an invariant", body = ProblemDetails, content_type = "application/problem+json"),
1735        (status = 503, description = "Required Server verifier fleet is not ready", body = ProblemDetails)
1736    ),
1737    params(
1738        ("project_id" = String, Path),
1739        ("Idempotency-Key" = String, Header)
1740    ),
1741    security(("operator_api_key" = []))
1742)]
1743#[doc(hidden)]
1744pub fn create_project_server_key() {}
1745
1746control_path!(
1747    acknowledge_project_server_key_delivery,
1748    post,
1749    "/v1/projects/{project_id}/server-keys/{key_id}/acknowledge",
1750    ProjectServerKey,
1751    "Project server-key delivery acknowledged",
1752    body = AcknowledgeProjectServerKeyDeliveryRequest,
1753    params(
1754        ("project_id" = String, Path),
1755        ("key_id" = String, Path),
1756        ("Idempotency-Key" = String, Header)
1757    )
1758);
1759control_path!(
1760    revoke_project_server_key,
1761    post,
1762    "/v1/projects/{project_id}/server-keys/{key_id}/revoke",
1763    ProjectServerKey,
1764    "Revoked Project server key",
1765    body = RevokeProjectServerKeyRequest,
1766    params(
1767        ("project_id" = String, Path),
1768        ("key_id" = String, Path),
1769        ("Idempotency-Key" = String, Header)
1770    )
1771);
1772control_path!(
1773    list_applications,
1774    get,
1775    "/v1/projects/{project_id}/applications",
1776    ApplicationList,
1777    "Applications",
1778    params(("project_id" = String, Path))
1779);
1780control_path!(
1781    create_application,
1782    post,
1783    "/v1/projects/{project_id}/applications",
1784    Application,
1785    "Created or authoritatively replayed application",
1786    body = CreateApplicationRequest,
1787    params(
1788        ("project_id" = String, Path),
1789        ("Idempotency-Key" = String, Header)
1790    )
1791);
1792control_path!(
1793    get_application,
1794    get,
1795    "/v1/projects/{project_id}/applications/{application_id}",
1796    Application,
1797    "Application",
1798    params(
1799        ("project_id" = String, Path),
1800        ("application_id" = String, Path)
1801    )
1802);
1803control_path!(
1804    update_application,
1805    patch,
1806    "/v1/projects/{project_id}/applications/{application_id}",
1807    Application,
1808    "Updated application",
1809    body = UpdateApplicationRequest,
1810    params(
1811        ("project_id" = String, Path),
1812        ("application_id" = String, Path)
1813    )
1814);
1815control_path!(
1816    replace_application_configuration,
1817    put,
1818    "/v1/projects/{project_id}/applications/{application_id}/configuration",
1819    Application,
1820    "Replaced exact application configuration",
1821    body = ReplaceApplicationConfigurationRequest,
1822    params(
1823        ("project_id" = String, Path),
1824        ("application_id" = String, Path)
1825    )
1826);
1827control_path!(
1828    disable_application,
1829    post,
1830    "/v1/projects/{project_id}/applications/{application_id}/disable",
1831    Application,
1832    "Disabled application",
1833    body = ExpectedSecurityRevision,
1834    params(
1835        ("project_id" = String, Path),
1836        ("application_id" = String, Path)
1837    )
1838);
1839control_path!(
1840    list_webhook_endpoints,
1841    get,
1842    "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints",
1843    WebhookEndpointList,
1844    "Webhook endpoints",
1845    params(
1846        ("project_id" = String, Path),
1847        ("application_id" = String, Path)
1848    )
1849);
1850control_path!(
1851    create_webhook_endpoint,
1852    post,
1853    "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints",
1854    WebhookEndpoint,
1855    "Created or authoritatively replayed pending webhook endpoint",
1856    body = CreateWebhookEndpointRequest,
1857    params(
1858        ("project_id" = String, Path),
1859        ("application_id" = String, Path),
1860        ("Idempotency-Key" = String, Header)
1861    )
1862);
1863control_path!(
1864    get_webhook_endpoint,
1865    get,
1866    "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}",
1867    WebhookEndpoint,
1868    "Webhook endpoint",
1869    params(
1870        ("project_id" = String, Path),
1871        ("application_id" = String, Path),
1872        ("endpoint_id" = String, Path)
1873    )
1874);
1875control_path!(
1876    update_webhook_endpoint,
1877    put,
1878    "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}",
1879    WebhookEndpoint,
1880    "Updated webhook endpoint subscriptions",
1881    body = UpdateWebhookEndpointRequest,
1882    params(
1883        ("project_id" = String, Path),
1884        ("application_id" = String, Path),
1885        ("endpoint_id" = String, Path)
1886    )
1887);
1888control_path!(
1889    test_webhook_endpoint,
1890    post,
1891    "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}/test",
1892    WebhookEndpoint,
1893    "Validated webhook endpoint DNS and destination policy",
1894    body = ExpectedWebhookEndpointRevision,
1895    params(
1896        ("project_id" = String, Path),
1897        ("application_id" = String, Path),
1898        ("endpoint_id" = String, Path)
1899    )
1900);
1901control_path!(
1902    activate_webhook_endpoint,
1903    post,
1904    "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}/activate",
1905    WebhookEndpoint,
1906    "Activated a tested webhook endpoint",
1907    body = ExpectedWebhookEndpointRevision,
1908    params(
1909        ("project_id" = String, Path),
1910        ("application_id" = String, Path),
1911        ("endpoint_id" = String, Path)
1912    )
1913);
1914control_path!(
1915    disable_webhook_endpoint,
1916    post,
1917    "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}/disable",
1918    WebhookEndpoint,
1919    "Disabled webhook endpoint",
1920    body = ExpectedWebhookEndpointRevision,
1921    params(
1922        ("project_id" = String, Path),
1923        ("application_id" = String, Path),
1924        ("endpoint_id" = String, Path)
1925    )
1926);
1927control_path!(
1928    prepare_webhook_secret_rotation,
1929    post,
1930    "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}/secret-rotations",
1931    PreparedWebhookSecretRotation,
1932    "Prepared a write-only webhook secret generation",
1933    body = PrepareWebhookSecretRotationRequest,
1934    params(
1935        ("project_id" = String, Path),
1936        ("application_id" = String, Path),
1937        ("endpoint_id" = String, Path),
1938        ("Idempotency-Key" = String, Header)
1939    )
1940);
1941control_path!(
1942    activate_webhook_secret_rotation,
1943    post,
1944    "/v1/projects/{project_id}/applications/{application_id}/webhook-endpoints/{endpoint_id}/secret-rotations/{generation}/activate",
1945    WebhookEndpoint,
1946    "Activated a prepared webhook secret generation",
1947    body = ActivateWebhookSecretRotationRequest,
1948    params(
1949        ("project_id" = String, Path),
1950        ("application_id" = String, Path),
1951        ("endpoint_id" = String, Path),
1952        ("generation" = i32, Path)
1953    )
1954);
1955control_path!(
1956    list_application_user_events,
1957    get,
1958    "/v1/projects/{project_id}/applications/{application_id}/user-events",
1959    ApplicationUserEventList,
1960    "Immutable Application user events",
1961    params(
1962        ("project_id" = String, Path),
1963        ("application_id" = String, Path),
1964        ("cursor" = Option<String>, Query),
1965        ("limit" = Option<usize>, Query, minimum = 1, maximum = 100)
1966    )
1967);
1968control_path!(
1969    list_webhook_deliveries,
1970    get,
1971    "/v1/projects/{project_id}/applications/{application_id}/webhook-deliveries",
1972    WebhookDeliveryList,
1973    "Webhook delivery history",
1974    params(
1975        ("project_id" = String, Path),
1976        ("application_id" = String, Path),
1977        ("endpoint_id" = Option<String>, Query),
1978        ("cursor" = Option<String>, Query),
1979        ("limit" = Option<usize>, Query, minimum = 1, maximum = 100)
1980    )
1981);
1982control_path!(
1983    get_webhook_delivery,
1984    get,
1985    "/v1/projects/{project_id}/applications/{application_id}/webhook-deliveries/{delivery_id}",
1986    WebhookDelivery,
1987    "One retained webhook delivery",
1988    params(
1989        ("project_id" = String, Path),
1990        ("application_id" = String, Path),
1991        ("delivery_id" = String, Path)
1992    )
1993);
1994#[utoipa::path(
1995    post,
1996    path = "/v1/projects/{project_id}/applications/{application_id}/webhook-deliveries/{delivery_id}/replay",
1997    request_body = ReplayWebhookDeliveryRequest,
1998    responses(
1999        (status = 201, description = "Created a new delivery for the same immutable event and endpoint", body = WebhookDelivery, headers(("Location" = String, description = "Exact Control path for the created webhook delivery"))),
2000        (status = 400, description = "Invalid request", body = ProblemDetails, content_type = "application/problem+json"),
2001        (status = 401, description = "Missing or invalid operator API key", body = ProblemDetails, content_type = "application/problem+json", headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))),
2002        (status = 404, description = "Resource not found", body = ProblemDetails, content_type = "application/problem+json"),
2003        (status = 409, description = "Revision, state, idempotency, or capacity conflict", body = ProblemDetails, content_type = "application/problem+json"),
2004        (status = 500, description = "Stored authority data violated an invariant", body = ProblemDetails, content_type = "application/problem+json"),
2005        (status = 503, description = "Required authority unavailable", body = ProblemDetails, content_type = "application/problem+json")
2006    ),
2007    params(
2008        ("project_id" = String, Path),
2009        ("application_id" = String, Path),
2010        ("delivery_id" = String, Path)
2011    ),
2012    security(("operator_api_key" = []))
2013)]
2014#[doc(hidden)]
2015pub fn replay_webhook_delivery() {}
2016control_path!(
2017    list_signing_keys,
2018    get,
2019    "/v1/projects/{project_id}/signing-keys",
2020    SigningKeyList,
2021    "Signing keys",
2022    params(("project_id" = String, Path))
2023);
2024control_path!(
2025    rotate_signing_key,
2026    post,
2027    "/v1/projects/{project_id}/signing-keys/rotate",
2028    SigningKey,
2029    "Accepted a durable signing key rotation",
2030    body = RotateSigningKeyRequest,
2031    params(
2032        ("project_id" = String, Path),
2033        ("Idempotency-Key" = String, Header)
2034    )
2035);
2036control_path!(
2037    revoke_signing_key,
2038    post,
2039    "/v1/projects/{project_id}/signing-keys/{key_id}/revoke",
2040    SigningKey,
2041    "Emergency-revoked signing key",
2042    body = KeyTransitionRequest,
2043    params(("project_id" = String, Path), ("key_id" = String, Path))
2044);
2045control_path!(
2046    get_provider_egress_policy,
2047    get,
2048    "/v1/projects/{project_id}/provider-egress-policy",
2049    ProviderEgressPolicy,
2050    "Project Custom OIDC egress policy",
2051    params(("project_id" = String, Path))
2052);
2053control_path!(
2054    update_provider_egress_policy,
2055    put,
2056    "/v1/projects/{project_id}/provider-egress-policy",
2057    ProviderEgressPolicy,
2058    "Updated Project Custom OIDC egress policy",
2059    body = UpdateProviderEgressPolicyRequest,
2060    params(("project_id" = String, Path))
2061);
2062control_preflight_path!(
2063    preflight_oidc_provider,
2064    "/v1/projects/{project_id}/providers/oidc/preflight",
2065    OidcPreflightResult,
2066    "Advisory Custom OIDC discovery preflight",
2067    OidcPreflightRequest
2068);
2069control_preflight_path!(
2070    preflight_named_provider,
2071    "/v1/projects/{project_id}/providers/named/preflight",
2072    NamedProviderPreflightResult,
2073    "Advisory named-provider registration preflight",
2074    NamedProviderPreflightRequest
2075);
2076control_path!(
2077    list_providers,
2078    get,
2079    "/v1/projects/{project_id}/providers",
2080    ProviderList,
2081    "Providers",
2082    params(("project_id" = String, Path))
2083);
2084control_path!(
2085    create_provider,
2086    post,
2087    "/v1/projects/{project_id}/providers",
2088    Provider,
2089    "Configured or authoritatively replayed provider",
2090    body = CreateProviderRequest,
2091    params(
2092        ("project_id" = String, Path),
2093        ("Idempotency-Key" = String, Header)
2094    )
2095);
2096control_path!(
2097    update_provider,
2098    patch,
2099    "/v1/projects/{project_id}/providers/{provider_id}",
2100    Provider,
2101    "Updated provider metadata",
2102    body = UpdateProviderRequest,
2103    params(
2104        ("project_id" = String, Path),
2105        ("provider_id" = String, Path)
2106    )
2107);
2108control_path!(
2109    replace_provider_secret,
2110    post,
2111    "/v1/projects/{project_id}/providers/{provider_id}/replace-secret",
2112    Provider,
2113    "Replaced provider protected client secret",
2114    body = ReplaceProviderSecretRequest,
2115    params(
2116        ("project_id" = String, Path),
2117        ("provider_id" = String, Path),
2118        ("Idempotency-Key" = String, Header)
2119    )
2120);
2121control_path!(
2122    reconcile_provider_secret_replacement,
2123    post,
2124    "/v1/projects/{project_id}/providers/{provider_id}/replace-secret/reconcile",
2125    Provider,
2126    "Reconciled a pending provider protected-secret replacement",
2127    body = ReconcileProviderSecretReplacementRequest,
2128    params(
2129        ("project_id" = String, Path),
2130        ("provider_id" = String, Path)
2131    )
2132);
2133control_path!(
2134    abandon_provider_secret_replacement,
2135    post,
2136    "/v1/projects/{project_id}/providers/{provider_id}/replace-secret/abandon",
2137    Provider,
2138    "Abandoned a pending provider protected-secret replacement",
2139    body = ProviderRevisionRequest,
2140    params(
2141        ("project_id" = String, Path),
2142        ("provider_id" = String, Path)
2143    )
2144);
2145control_path!(
2146    reconcile_provider,
2147    post,
2148    "/v1/projects/{project_id}/providers/{provider_id}/reconcile",
2149    Provider,
2150    "Reconciled provider secret provisioning",
2151    body = ReconcileProviderRequest,
2152    params(
2153        ("project_id" = String, Path),
2154        ("provider_id" = String, Path)
2155    )
2156);
2157control_path!(
2158    disable_provider,
2159    post,
2160    "/v1/projects/{project_id}/providers/{provider_id}/disable",
2161    Provider,
2162    "Disabled provider",
2163    body = ProviderRevisionRequest,
2164    params(
2165        ("project_id" = String, Path),
2166        ("provider_id" = String, Path)
2167    )
2168);
2169control_path!(
2170    assign_provider,
2171    put,
2172    "/v1/projects/{project_id}/providers/{provider_id}/assignments/{application_id}",
2173    Provider,
2174    "Assigned provider",
2175    body = ProviderAssignmentRequest,
2176    params(
2177        ("project_id" = String, Path),
2178        ("provider_id" = String, Path),
2179        ("application_id" = String, Path)
2180    )
2181);
2182control_path!(
2183    unassign_provider,
2184    post,
2185    "/v1/projects/{project_id}/providers/{provider_id}/assignments/{application_id}/unassign",
2186    Provider,
2187    "Unassigned provider",
2188    body = ProviderAssignmentRequest,
2189    params(
2190        ("project_id" = String, Path),
2191        ("provider_id" = String, Path),
2192        ("application_id" = String, Path)
2193    )
2194);
2195
2196control_path!(
2197    get_email_method_policy,
2198    get,
2199    "/v1/projects/{project_id}/email-method",
2200    EmailMethodPolicy,
2201    "Passwordless email policy",
2202    params(("project_id" = String, Path))
2203);
2204control_path!(
2205    update_email_method_policy,
2206    put,
2207    "/v1/projects/{project_id}/email-method",
2208    EmailMethodPolicy,
2209    "Updated passwordless email policy",
2210    body = UpdateEmailMethodPolicyRequest,
2211    params(("project_id" = String, Path))
2212);
2213control_path!(
2214    list_email_assignments,
2215    get,
2216    "/v1/projects/{project_id}/email-method/assignments",
2217    EmailAssignmentList,
2218    "Application email assignments",
2219    params(("project_id" = String, Path))
2220);
2221control_path!(
2222    assign_email_method,
2223    put,
2224    "/v1/projects/{project_id}/applications/{application_id}/email-method",
2225    EmailMethodPolicy,
2226    "Updated Application email assignment",
2227    body = EmailAssignmentRequest,
2228    params(
2229        ("project_id" = String, Path),
2230        ("application_id" = String, Path)
2231    )
2232);
2233control_path!(
2234    list_deployment_smtp_generations,
2235    get,
2236    "/v1/system/smtp-default-generations",
2237    DeploymentSmtpGenerationList,
2238    "Deployment SMTP generations"
2239);
2240control_path!(
2241    reconcile_deployment_smtp_generation,
2242    post,
2243    "/v1/system/smtp-default-generations",
2244    DeploymentSmtpGeneration,
2245    "Reconciled deployment SMTP generation",
2246    body = ReconcileDeploymentSmtpRequest,
2247    params(("Idempotency-Key" = String, Header))
2248);
2249control_path!(
2250    disable_deployment_smtp_generation,
2251    post,
2252    "/v1/system/smtp-default-generations/{generation}/disable",
2253    DeploymentSmtpGeneration,
2254    "Disabled deployment SMTP generation",
2255    body = SmtpRevisionRequest,
2256    params(("generation" = i32, Path))
2257);
2258control_path!(
2259    compromise_deployment_smtp_generation,
2260    post,
2261    "/v1/system/smtp-default-generations/{generation}/compromise",
2262    DeploymentSmtpGeneration,
2263    "Compromised deployment SMTP generation",
2264    body = SmtpRevisionRequest,
2265    params(("generation" = i32, Path))
2266);
2267control_path!(
2268    list_smtp_configurations,
2269    get,
2270    "/v1/projects/{project_id}/smtp-configurations",
2271    SmtpConfigurationList,
2272    "SMTP generations",
2273    params(("project_id" = String, Path))
2274);
2275control_path!(
2276    create_smtp_configuration,
2277    post,
2278    "/v1/projects/{project_id}/smtp-configurations",
2279    SmtpConfiguration,
2280    "Created or authoritatively replayed pending SMTP generation",
2281    body = CreateSmtpConfigurationRequest,
2282    params(
2283        ("project_id" = String, Path),
2284        ("Idempotency-Key" = String, Header)
2285    )
2286);
2287control_path!(
2288    get_smtp_test_operation,
2289    get,
2290    "/v1/projects/{project_id}/smtp-configurations/{smtp_id}/tests/{operation_id}",
2291    SmtpTestOperation,
2292    "SMTP test operation status",
2293    params(
2294        ("project_id" = String, Path),
2295        ("smtp_id" = String, Path),
2296        ("operation_id" = String, Path)
2297    )
2298);
2299control_path!(
2300    activate_smtp_configuration,
2301    post,
2302    "/v1/projects/{project_id}/smtp-configurations/{smtp_id}/activate",
2303    SmtpConfiguration,
2304    "Activated SMTP generation",
2305    body = SmtpRevisionRequest,
2306    params(("project_id" = String, Path), ("smtp_id" = String, Path))
2307);
2308control_path!(
2309    disable_smtp_configuration,
2310    post,
2311    "/v1/projects/{project_id}/smtp-configurations/{smtp_id}/disable",
2312    SmtpConfiguration,
2313    "Disabled SMTP generation",
2314    body = SmtpRevisionRequest,
2315    params(("project_id" = String, Path), ("smtp_id" = String, Path))
2316);
2317control_path!(
2318    compromise_smtp_configuration,
2319    post,
2320    "/v1/projects/{project_id}/smtp-configurations/{smtp_id}/compromise",
2321    SmtpConfiguration,
2322    "Marked SMTP generation compromised",
2323    body = SmtpRevisionRequest,
2324    params(("project_id" = String, Path), ("smtp_id" = String, Path))
2325);
2326
2327control_path!(
2328    list_project_users,
2329    get,
2330    "/v1/projects/{project_id}/users",
2331    ProjectUserList,
2332    "Project users",
2333    params(
2334        ("project_id" = String, Path),
2335        ("status" = Option<ProjectUserStatus>, Query),
2336        ("search" = Option<String>, Query, min_length = 1, max_length = 128),
2337        ("identity_kind" = Option<ProjectUserIdentityFilter>, Query),
2338        ("provider_key" = Option<String>, Query, min_length = 1, max_length = 64),
2339        ("sort" = Option<ProjectUserSort>, Query),
2340        ("cursor" = Option<String>, Query, max_length = 36, description = "Cursor returned by a previous page with the same status, search, identity, provider, and sort criteria"),
2341        ("limit" = Option<usize>, Query, minimum = 1, maximum = 100)
2342    )
2343);
2344control_path!(
2345    lookup_project_user_by_email,
2346    post,
2347    "/v1/projects/{project_id}/users/lookup",
2348    ProjectUserLookup,
2349    "Exact canonical email Project user lookup",
2350    body = ProjectUserEmailLookupRequest,
2351    params(("project_id" = String, Path))
2352);
2353control_path!(
2354    get_project_user,
2355    get,
2356    "/v1/projects/{project_id}/users/{user_id}",
2357    ProjectUser,
2358    "Project user",
2359    params(("project_id" = String, Path), ("user_id" = String, Path))
2360);
2361control_path!(
2362    list_project_user_identities,
2363    get,
2364    "/v1/projects/{project_id}/users/{user_id}/identities",
2365    ProjectUserIdentityList,
2366    "Bounded safe mixed provider and email identity inventory",
2367    params(("project_id" = String, Path), ("user_id" = String, Path))
2368);
2369control_path!(
2370    disable_project_user,
2371    post,
2372    "/v1/projects/{project_id}/users/{user_id}/disable",
2373    ProjectUser,
2374    "Disabled Project user",
2375    body = ExpectedSecurityRevision,
2376    params(("project_id" = String, Path), ("user_id" = String, Path))
2377);
2378control_path!(
2379    enable_project_user,
2380    post,
2381    "/v1/projects/{project_id}/users/{user_id}/enable",
2382    ProjectUser,
2383    "Enabled Project user",
2384    body = ExpectedSecurityRevision,
2385    params(("project_id" = String, Path), ("user_id" = String, Path))
2386);
2387control_path!(
2388    list_project_user_sessions,
2389    get,
2390    "/v1/projects/{project_id}/users/{user_id}/sessions",
2391    ProjectUserSessions,
2392    "Project user sessions",
2393    params(("project_id" = String, Path), ("user_id" = String, Path))
2394);
2395control_path!(
2396    revoke_application_session,
2397    post,
2398    "/v1/projects/{project_id}/users/{user_id}/application-sessions/{session_id}/revoke",
2399    ApplicationSession,
2400    "Revoked Application session",
2401    body = ExpectedSessionRevision,
2402    params(
2403        ("project_id" = String, Path),
2404        ("user_id" = String, Path),
2405        ("session_id" = String, Path)
2406    )
2407);
2408control_path!(
2409    revoke_browser_session,
2410    post,
2411    "/v1/projects/{project_id}/users/{user_id}/browser-sessions/{session_id}/revoke",
2412    BrowserSession,
2413    "Revoked Project browser session",
2414    body = ExpectedSessionRevision,
2415    params(
2416        ("project_id" = String, Path),
2417        ("user_id" = String, Path),
2418        ("session_id" = String, Path)
2419    )
2420);
2421
2422#[cfg(test)]
2423mod tests {
2424    use serde_json::json;
2425
2426    use super::{
2427        CreateIdentityMutationIntentRequest, IdentityMutationProofAuthority,
2428        NamedProviderPreflightRequest, OidcPreflightRequest,
2429    };
2430
2431    #[test]
2432    fn provider_preflight_requests_reject_secret_and_callback_authority() {
2433        assert!(
2434            serde_json::from_value::<OidcPreflightRequest>(json!({
2435                "provider_key": "custom-main",
2436                "issuer": "https://identity.example",
2437            }))
2438            .is_ok()
2439        );
2440        for forbidden in ["client_secret", "callback_url", "project_public_id"] {
2441            let mut request = json!({
2442                "provider_key": "custom-main",
2443                "issuer": "https://identity.example",
2444            });
2445            request[forbidden] = json!("caller-owned");
2446            assert!(serde_json::from_value::<OidcPreflightRequest>(request).is_err());
2447        }
2448        for forbidden in ["client_secret", "callback_url", "issuer"] {
2449            let mut request = json!({
2450                "kind": "google",
2451                "provider_key": "google-main",
2452            });
2453            request[forbidden] = json!("caller-owned");
2454            assert!(serde_json::from_value::<NamedProviderPreflightRequest>(request).is_err());
2455        }
2456    }
2457
2458    #[test]
2459    fn user_and_session_lifecycle_contract_is_bounded_and_control_only() {
2460        let document = serde_json::to_value(crate::control::openapi())
2461            .expect("Control OpenAPI should serialize");
2462        for path in [
2463            "/v1/projects/{project_id}/users",
2464            "/v1/projects/{project_id}/users/{user_id}",
2465            "/v1/projects/{project_id}/users/{user_id}/disable",
2466            "/v1/projects/{project_id}/users/{user_id}/sessions",
2467            "/v1/projects/{project_id}/users/{user_id}/application-sessions/{session_id}/revoke",
2468            "/v1/projects/{project_id}/users/{user_id}/browser-sessions/{session_id}/revoke",
2469        ] {
2470            assert!(document["paths"][path].is_object(), "missing path: {path}");
2471        }
2472        let user = &document["components"]["schemas"]["ProjectUser"]["properties"];
2473        assert!(user.get("provider_credentials").is_none());
2474        assert!(user.get("source_payload").is_none());
2475        assert_eq!(
2476            document["components"]["schemas"]["ProjectUserList"]["properties"]["items"]["maxItems"],
2477            100
2478        );
2479    }
2480
2481    #[test]
2482    fn identity_mutation_control_contract_is_typed_and_safe() {
2483        let document = serde_json::to_value(crate::control::openapi())
2484            .expect("Control OpenAPI should serialize");
2485        let collection = "/v1/projects/{project_id}/identity-mutation-intents";
2486        for path in [
2487            collection,
2488            "/v1/projects/{project_id}/identity-mutation-intents/{intent_id}",
2489            "/v1/projects/{project_id}/identity-mutation-intents/{intent_id}/cancel",
2490            "/v1/projects/{project_id}/identity-mutation-intents/{intent_id}/confirm",
2491        ] {
2492            assert!(document["paths"][path].is_object(), "missing path: {path}");
2493        }
2494        assert!(
2495            document["paths"][collection]["post"]["parameters"]
2496                .as_array()
2497                .expect("create parameters")
2498                .iter()
2499                .any(|parameter| parameter["name"] == "Idempotency-Key")
2500        );
2501
2502        let intent = &document["components"]["schemas"]["IdentityMutationIntent"];
2503        assert_eq!(intent["properties"]["slots"]["maxItems"], 2);
2504        for forbidden in [
2505            "raw_state",
2506            "receipt",
2507            "evidence",
2508            "subject",
2509            "email",
2510            "address",
2511            "provider_secret",
2512        ] {
2513            assert!(intent["properties"].get(forbidden).is_none());
2514        }
2515        let create_schema =
2516            &document["components"]["schemas"]["CreateIdentityMutationIntentRequest"];
2517        let variants = create_schema["oneOf"]
2518            .as_array()
2519            .expect("typed create variants");
2520        assert_eq!(variants.len(), 3);
2521        assert!(
2522            document["paths"]["/v1/projects/{project_id}/identity-mutation-intents"]
2523                ["post"]["responses"]
2524                .get("201")
2525                .is_some()
2526        );
2527        assert!(
2528            document["paths"]["/v1/projects/{project_id}/identity-mutation-intents"]
2529                ["post"]["responses"]
2530                .get("200")
2531                .is_some()
2532        );
2533        assert!(
2534            variants
2535                .iter()
2536                .all(|variant| variant["properties"].get("slots").is_none())
2537        );
2538    }
2539
2540    #[test]
2541    fn identity_mutation_commands_reject_caller_derived_authority() {
2542        let link = json!({
2543            "operation_kind": "link",
2544            "destination": {
2545                "user_id": "user-1",
2546                "expected_user_revision": 3,
2547                "expected_user_security_revision": 4
2548            },
2549            "destination_identity": {
2550                "identity_kind": "provider",
2551                "identity_id": "identity-1",
2552                "expected_identity_revision": 5
2553            },
2554            "candidate_identity_kind": "email",
2555            "destination_proof_authority": {
2556                "method_kind": "provider",
2557                "application_id": "application-1",
2558                "provider_id": "provider-1"
2559            },
2560            "candidate_proof_authority": {
2561                "method_kind": "email",
2562                "application_id": "application-1"
2563            },
2564            "slots": []
2565        });
2566        assert!(serde_json::from_value::<CreateIdentityMutationIntentRequest>(link).is_err());
2567
2568        let authority = json!({
2569            "method_kind": "provider",
2570            "application_id": "application-1",
2571            "provider_id": "provider-1",
2572            "scopes": ["openid"],
2573            "callback": "https://attacker.example/callback"
2574        });
2575        assert!(serde_json::from_value::<IdentityMutationProofAuthority>(authority).is_err());
2576    }
2577}
2578
2579control_path!(
2580    list_managed_provider_connections,
2581    get,
2582    "/v1/projects/{project_id}/users/{user_id}/managed-provider-connections",
2583    ManagedProviderConnectionList,
2584    "Safe managed provider connection metadata",
2585    params(("project_id" = String, Path), ("user_id" = String, Path))
2586);
2587control_path!(
2588    synchronize_managed_provider_connection,
2589    post,
2590    "/v1/projects/{project_id}/users/{user_id}/managed-provider-connections/{connection_id}/synchronize",
2591    ManagedProviderConnection,
2592    "Scheduled guarded profile synchronization",
2593    body = ManagedProviderConnectionActionRequest,
2594    params(
2595        ("project_id" = String, Path),
2596        ("user_id" = String, Path),
2597        ("connection_id" = String, Path)
2598    )
2599);
2600#[utoipa::path(
2601    post,
2602    path = "/v1/projects/{project_id}/users/{user_id}/managed-provider-connections/{connection_id}/reauthorizations",
2603    request_body = CreateManagedReauthorizationRequest,
2604    responses(
2605        (status = 201, description = "Created one exact managed reauthorization interaction", body = CreateManagedReauthorizationResponse, headers(("Location" = String, description = "Exact Control path for the created managed reauthorization"))),
2606        (status = 200, description = "Authoritative idempotent replay of the managed reauthorization", body = CreateManagedReauthorizationResponse),
2607        (status = 400, description = "Invalid request", body = ProblemDetails, content_type = "application/problem+json"),
2608        (status = 401, description = "Missing or invalid operator API key", body = ProblemDetails, content_type = "application/problem+json", headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))),
2609        (status = 404, description = "Resource not found", body = ProblemDetails, content_type = "application/problem+json"),
2610        (status = 409, description = "Revision, state, idempotency, or capacity conflict", body = ProblemDetails, content_type = "application/problem+json"),
2611        (status = 500, description = "Stored authority data violated an invariant", body = ProblemDetails, content_type = "application/problem+json"),
2612        (status = 503, description = "Required authority unavailable", body = ProblemDetails, content_type = "application/problem+json")
2613    ),
2614    params(
2615        ("project_id" = String, Path),
2616        ("user_id" = String, Path),
2617        ("connection_id" = String, Path),
2618        ("Idempotency-Key" = String, Header)
2619    ),
2620    security(("operator_api_key" = []))
2621)]
2622#[doc(hidden)]
2623pub fn create_managed_reauthorization() {}
2624control_path!(
2625    get_managed_reauthorization,
2626    get,
2627    "/v1/projects/{project_id}/users/{user_id}/managed-provider-connections/{connection_id}/reauthorizations/{interaction_id}",
2628    ManagedReauthorization,
2629    "Read bounded managed reauthorization status without Hosted target",
2630    params(
2631        ("project_id" = String, Path),
2632        ("user_id" = String, Path),
2633        ("connection_id" = String, Path),
2634        ("interaction_id" = String, Path)
2635    )
2636);
2637control_path!(
2638    cancel_managed_reauthorization,
2639    post,
2640    "/v1/projects/{project_id}/users/{user_id}/managed-provider-connections/{connection_id}/reauthorizations/{interaction_id}/cancel",
2641    ManagedReauthorization,
2642    "Cancelled one current managed reauthorization interaction",
2643    body = CancelManagedReauthorizationRequest,
2644    params(
2645        ("project_id" = String, Path),
2646        ("user_id" = String, Path),
2647        ("connection_id" = String, Path),
2648        ("interaction_id" = String, Path)
2649    )
2650);
2651control_path!(
2652    revoke_managed_provider_connection,
2653    post,
2654    "/v1/projects/{project_id}/users/{user_id}/managed-provider-connections/{connection_id}/revoke",
2655    ManagedProviderConnection,
2656    "Provider revocation when the adapter can prove it",
2657    body = ManagedProviderConnectionActionRequest,
2658    params(
2659        ("project_id" = String, Path),
2660        ("user_id" = String, Path),
2661        ("connection_id" = String, Path)
2662    )
2663);
2664control_path!(
2665    disconnect_managed_provider_connection,
2666    post,
2667    "/v1/projects/{project_id}/users/{user_id}/managed-provider-connections/{connection_id}/disconnect",
2668    ManagedProviderConnection,
2669    "Locally disconnected and destroyed renewable credential",
2670    body = ManagedProviderConnectionActionRequest,
2671    params(
2672        ("project_id" = String, Path),
2673        ("user_id" = String, Path),
2674        ("connection_id" = String, Path)
2675    )
2676);
2677
2678#[utoipa::path(
2679    post,
2680    path = "/v1/projects/{project_id}/identity-mutation-intents",
2681    request_body = CreateIdentityMutationIntentRequest,
2682    responses(
2683        (status = 201, description = "Created one typed identity mutation intent", body = CreateIdentityMutationIntentResponse, headers(("Location" = String, description = "Exact Control path for the created identity mutation intent"))),
2684        (status = 200, description = "Authoritative idempotent replay of the identity mutation intent", body = CreateIdentityMutationIntentResponse),
2685        (status = 400, description = "Invalid request", body = ProblemDetails, content_type = "application/problem+json"),
2686        (status = 401, description = "Missing or invalid operator API key", body = ProblemDetails, content_type = "application/problem+json", headers(("WWW-Authenticate" = String, description = "Bearer authentication challenge"))),
2687        (status = 404, description = "Resource not found", body = ProblemDetails, content_type = "application/problem+json"),
2688        (status = 409, description = "Revision, state, idempotency, or capacity conflict", body = ProblemDetails, content_type = "application/problem+json"),
2689        (status = 500, description = "Stored authority data violated an invariant", body = ProblemDetails, content_type = "application/problem+json"),
2690        (status = 503, description = "Required authority unavailable", body = ProblemDetails, content_type = "application/problem+json")
2691    ),
2692    params(
2693        ("project_id" = String, Path),
2694        ("Idempotency-Key" = String, Header)
2695    ),
2696    security(("operator_api_key" = []))
2697)]
2698#[doc(hidden)]
2699pub fn create_identity_mutation_intent() {}
2700control_path!(
2701    get_identity_mutation_intent,
2702    get,
2703    "/v1/projects/{project_id}/identity-mutation-intents/{intent_id}",
2704    IdentityMutationIntent,
2705    "Read safe identity mutation intent readiness",
2706    params(("project_id" = String, Path), ("intent_id" = String, Path))
2707);
2708control_path!(
2709    cancel_identity_mutation_intent,
2710    post,
2711    "/v1/projects/{project_id}/identity-mutation-intents/{intent_id}/cancel",
2712    IdentityMutationIntent,
2713    "Cancelled one current identity mutation intent",
2714    body = CancelIdentityMutationIntentRequest,
2715    params(("project_id" = String, Path), ("intent_id" = String, Path))
2716);
2717control_path!(
2718    confirm_identity_mutation_intent,
2719    post,
2720    "/v1/projects/{project_id}/identity-mutation-intents/{intent_id}/confirm",
2721    IdentityMutationIntent,
2722    "Confirmed one ready identity mutation intent",
2723    body = ConfirmIdentityMutationIntentRequest,
2724    params(("project_id" = String, Path), ("intent_id" = String, Path))
2725);
2726
2727#[cfg(test)]
2728mod project_server_key_contract_tests {
2729    use super::*;
2730
2731    fn metadata() -> ProjectServerKey {
2732        ProjectServerKey {
2733            id: "00000000-0000-0000-0000-000000000001".to_owned(),
2734            project_id: "00000000-0000-0000-0000-000000000002".to_owned(),
2735            public_key_id: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
2736            label: "production backend".to_owned(),
2737            status: ProjectServerKeyStatus::Active,
2738            digest_key_version: 1,
2739            display_prefix: "owl_server_v1.AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
2740            revision: 1,
2741            created_at: "2026-08-05T00:00:00Z".to_owned(),
2742            credential_acknowledged_at: None,
2743            last_used_at: None,
2744            revoked_at: None,
2745        }
2746    }
2747
2748    #[test]
2749    fn one_time_credential_is_redacted_from_debug() {
2750        let response = CreateProjectServerKeyResponse {
2751            key: metadata(),
2752            credential: format!("owl_server_v1.AAAAAAAAAAAAAAAAAAAAAA.{}", "B".repeat(43)),
2753        };
2754        let debug = format!("{response:?}");
2755        assert!(debug.contains("[REDACTED]"));
2756        assert!(!debug.contains(&"B".repeat(43)));
2757    }
2758
2759    #[test]
2760    fn acknowledgement_status_is_explicit_and_required_in_inventory() {
2761        let mut encoded = serde_json::to_value(metadata()).expect("server-key metadata");
2762        encoded
2763            .as_object_mut()
2764            .expect("server-key metadata object")
2765            .remove("credential_acknowledged_at");
2766        assert!(serde_json::from_value::<ProjectServerKey>(encoded).is_err());
2767
2768        assert!(
2769            serde_json::from_value::<ProjectServerKeyList>(serde_json::json!({
2770                "items": [],
2771                "next_cursor": null
2772            }))
2773            .is_err()
2774        );
2775        assert!(
2776            serde_json::from_value::<ProjectServerKeyList>(serde_json::json!({
2777                "items": [],
2778                "next_cursor": null,
2779                "active_unacknowledged_key": null
2780            }))
2781            .is_ok()
2782        );
2783    }
2784
2785    #[test]
2786    fn lifecycle_commands_reject_unknown_authority_fields() {
2787        assert!(
2788            serde_json::from_value::<CreateProjectServerKeyRequest>(serde_json::json!({
2789                "label": "backend",
2790                "scopes": ["users:read"]
2791            }))
2792            .is_err()
2793        );
2794        assert!(
2795            serde_json::from_value::<RevokeProjectServerKeyRequest>(serde_json::json!({
2796                "expected_revision": 1,
2797                "confirm": true,
2798                "enable": false
2799            }))
2800            .is_err()
2801        );
2802        assert!(
2803            serde_json::from_value::<AcknowledgeProjectServerKeyDeliveryRequest>(
2804                serde_json::json!({
2805                    "expected_revision": 1,
2806                    "confirm_stored": true,
2807                    "credential": "must-never-be-accepted"
2808                })
2809            )
2810            .is_err()
2811        );
2812    }
2813}