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