Skip to main content

alibi_core/wire/
plugins.rs

1use crate::{AuthApiKey, AuthInvitation, AuthOrganization, AuthPasskey, InvitationStatus};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize, Serializer};
4/// Public organization response shape.
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
6pub struct OrganizationView {
7    #[serde(default, flatten)]
8    pub additional_fields: std::collections::BTreeMap<String, serde_json::Value>,
9    pub id: String,
10    pub name: String,
11    pub slug: String,
12    pub logo: Option<String>,
13    #[serde(
14        serialize_with = "serialize_json_option_as_string",
15        skip_serializing_if = "Option::is_none"
16    )]
17    #[serde(
18        default,
19        deserialize_with = "crate::utils::json::deserialize_optional_value"
20    )]
21    pub metadata: Option<serde_json::Value>,
22    #[serde(rename = "createdAt")]
23    #[serde(serialize_with = "crate::utils::datetime::serialize")]
24    pub created_at: DateTime<Utc>,
25}
26
27impl<T: AuthOrganization> From<&T> for OrganizationView {
28    fn from(org: &T) -> Self {
29        Self {
30            additional_fields: org.additional_fields(),
31            id: org.id().into_owned(),
32            name: org.name().to_owned(),
33            slug: org.slug().to_owned(),
34            logo: org.logo().map(str::to_owned),
35            metadata: org.metadata().cloned(),
36            created_at: org.created_at(),
37        }
38    }
39}
40
41/// Public invitation response shape.
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43pub struct InvitationView {
44    pub id: String,
45    #[serde(rename = "organizationId")]
46    pub organization_id: String,
47    pub email: String,
48    pub role: Option<String>,
49    pub status: InvitationStatus,
50    #[serde(rename = "inviterId")]
51    pub inviter_id: String,
52    #[serde(rename = "expiresAt")]
53    #[serde(serialize_with = "crate::utils::datetime::serialize")]
54    pub expires_at: DateTime<Utc>,
55    #[serde(rename = "createdAt")]
56    #[serde(serialize_with = "crate::utils::datetime::serialize")]
57    pub created_at: DateTime<Utc>,
58    #[serde(rename = "teamId", default, skip_serializing_if = "Option::is_none")]
59    pub team_id: Option<String>,
60    #[serde(
61        flatten,
62        default,
63        deserialize_with = "crate::utils::json::deserialize_btree_map"
64    )]
65    pub extension_fields: std::collections::BTreeMap<String, serde_json::Value>,
66}
67
68impl<T: AuthInvitation> From<&T> for InvitationView {
69    fn from(inv: &T) -> Self {
70        Self {
71            id: inv.id().into_owned(),
72            organization_id: inv.organization_id().into_owned(),
73            email: inv.email().to_owned(),
74            role: inv.optional_role().map(str::to_owned),
75            status: inv.status().clone(),
76            inviter_id: inv.inviter_id().into_owned(),
77            expires_at: inv.expires_at(),
78            created_at: inv.created_at(),
79            team_id: inv.team_id().map(str::to_owned),
80            extension_fields: std::collections::BTreeMap::default(),
81        }
82    }
83}
84
85/// Public passkey response shape.
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87pub struct PasskeyView {
88    pub id: String,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub name: Option<String>,
91    #[serde(rename = "credentialID")]
92    pub credential_id: String,
93    #[serde(rename = "userId")]
94    pub user_id: String,
95    #[serde(rename = "publicKey")]
96    pub public_key: String,
97    pub counter: u64,
98    #[serde(rename = "deviceType")]
99    pub device_type: String,
100    #[serde(rename = "backedUp")]
101    pub backed_up: bool,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub transports: Option<String>,
104    #[serde(rename = "createdAt")]
105    pub created_at: Option<String>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub aaguid: Option<String>,
108}
109
110impl<T: AuthPasskey> From<&T> for PasskeyView {
111    fn from(pk: &T) -> Self {
112        Self {
113            id: pk.id().into_owned(),
114            name: pk.name().map(str::to_owned),
115            credential_id: pk.credential_id().to_owned(),
116            user_id: pk.user_id().into_owned(),
117            public_key: pk.public_key().to_owned(),
118            counter: pk.counter(),
119            device_type: pk.device_type().to_owned(),
120            backed_up: pk.backed_up(),
121            transports: pk.transports().map(str::to_owned),
122            created_at: pk
123                .created_at()
124                .map(|time| time.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)),
125            aaguid: pk.aaguid().map(str::to_owned),
126        }
127    }
128}
129
130/// Public API key response shape.
131///
132/// Intentionally omits `key_hash` — the hashed key value is never returned
133/// over the wire (matches upstream TS behavior).
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135pub struct ApiKeyView {
136    pub id: String,
137    pub name: Option<String>,
138    pub start: Option<String>,
139    pub prefix: Option<String>,
140    #[serde(rename = "referenceId")]
141    pub reference_id: String,
142    #[serde(rename = "configId")]
143    pub config_id: String,
144    #[serde(rename = "refillInterval")]
145    #[serde(serialize_with = "serialize_api_key_number")]
146    pub refill_interval: Option<f64>,
147    #[serde(rename = "refillAmount")]
148    #[serde(serialize_with = "serialize_api_key_number")]
149    pub refill_amount: Option<f64>,
150    #[serde(rename = "lastRefillAt")]
151    pub last_refill_at: Option<String>,
152    pub enabled: bool,
153    #[serde(rename = "rateLimitEnabled")]
154    pub rate_limit_enabled: bool,
155    #[serde(rename = "rateLimitTimeWindow")]
156    #[serde(serialize_with = "serialize_api_key_number")]
157    pub rate_limit_time_window: Option<f64>,
158    #[serde(rename = "rateLimitMax")]
159    #[serde(serialize_with = "serialize_api_key_number")]
160    pub rate_limit_max: Option<f64>,
161    #[serde(rename = "requestCount")]
162    #[serde(serialize_with = "serialize_api_key_number")]
163    pub request_count: Option<f64>,
164    #[serde(serialize_with = "serialize_api_key_number")]
165    pub remaining: Option<f64>,
166    #[serde(rename = "lastRequest")]
167    pub last_request: Option<String>,
168    #[serde(rename = "expiresAt")]
169    pub expires_at: Option<String>,
170    #[serde(rename = "createdAt")]
171    pub created_at: String,
172    #[serde(rename = "updatedAt")]
173    pub updated_at: String,
174    #[serde(
175        default,
176        deserialize_with = "crate::utils::json::deserialize_optional_value"
177    )]
178    pub permissions: Option<serde_json::Value>,
179    #[serde(
180        default,
181        deserialize_with = "crate::utils::json::deserialize_optional_value"
182    )]
183    pub metadata: Option<serde_json::Value>,
184}
185
186impl<T: AuthApiKey> From<&T> for ApiKeyView {
187    fn from(ak: &T) -> Self {
188        Self {
189            id: ak.id().into_owned(),
190            name: ak.name().map(str::to_owned),
191            start: ak.start().map(str::to_owned),
192            prefix: ak.prefix().map(str::to_owned),
193            reference_id: ak.reference_id().into_owned(),
194            config_id: ak.config_id().into_owned(),
195            refill_interval: ak.refill_interval(),
196            refill_amount: ak.refill_amount(),
197            last_refill_at: ak.last_refill_at().map(api_key_wire_date),
198            enabled: ak.enabled(),
199            rate_limit_enabled: ak.rate_limit_enabled(),
200            rate_limit_time_window: ak.rate_limit_time_window(),
201            rate_limit_max: ak.rate_limit_max(),
202            request_count: ak.request_count(),
203            remaining: ak.remaining(),
204            last_request: ak.last_request().map(api_key_wire_date),
205            expires_at: ak.expires_at().map(api_key_wire_date),
206            created_at: api_key_wire_date(ak.created_at()),
207            updated_at: api_key_wire_date(ak.updated_at()),
208            permissions: ak
209                .permissions()
210                .and_then(|s| crate::utils::json::from_slice(s.as_bytes()).ok())
211                .map(|mut value| {
212                    normalize_api_key_permission_dates(&mut value);
213                    value
214                }),
215            metadata: ak.metadata().and_then(|s| {
216                let value = crate::utils::json::parse_value(s).ok()?;
217                if let crate::utils::json::JsValue::String(encoded) = value {
218                    let mut value = crate::utils::json::parse_value(&encoded)
219                        .ok()?
220                        .to_json_value()
221                        .ok()?;
222                    normalize_api_key_permission_dates(&mut value);
223                    Some(value)
224                } else {
225                    value.to_json_value().ok()
226                }
227            }),
228        }
229    }
230}
231
232pub(in crate::wire) fn normalize_api_key_permission_dates(value: &mut serde_json::Value) {
233    match value {
234        serde_json::Value::String(text) => {
235            if let Some(date) = crate::utils::datetime::normalize_json_date(text) {
236                *text = date;
237            }
238        }
239        serde_json::Value::Array(values) => {
240            for value in values {
241                normalize_api_key_permission_dates(value);
242            }
243        }
244        serde_json::Value::Object(values) => {
245            for value in values.values_mut() {
246                normalize_api_key_permission_dates(value);
247            }
248        }
249        _ => {}
250    }
251}
252
253// Plugin entity views
254
255#[expect(
256    clippy::ref_option,
257    reason = "Serde field serializers receive a reference to the declared Option field type"
258)]
259pub(in crate::wire) fn serialize_json_option_as_string<S>(
260    value: &Option<serde_json::Value>,
261    serializer: S,
262) -> Result<S::Ok, S::Error>
263where
264    S: Serializer,
265{
266    match value {
267        Some(inner) => serializer
268            .serialize_some(&serde_json::to_string(inner).map_err(serde::ser::Error::custom)?),
269        None => serializer.serialize_none(),
270    }
271}
272
273// JSON.stringify emits safe integral JavaScript numbers without a decimal suffix.
274#[expect(
275    clippy::as_conversions,
276    clippy::cast_possible_truncation,
277    reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
278)]
279#[expect(
280    clippy::ref_option,
281    reason = "Serde field serializers receive a reference to the declared Option field type"
282)]
283pub(in crate::wire) fn serialize_api_key_number<S: Serializer>(
284    value: &Option<f64>,
285    serializer: S,
286) -> Result<S::Ok, S::Error> {
287    match value {
288        Some(value) if value.fract() == 0.0 && value.abs() <= 9_007_199_254_740_991.0 => {
289            serializer.serialize_i64(*value as i64)
290        }
291        value => value.serialize(serializer),
292    }
293}
294
295// The source adapter exposes dates to JSON through Date.toJSON(). Keep native
296// accessor/storage precision intact while projecting valid RFC3339 wire dates.
297pub(in crate::wire) fn api_key_wire_date(value: &str) -> String {
298    DateTime::parse_from_rfc3339(value).map_or_else(
299        |_| value.to_owned(),
300        |date| {
301            date.with_timezone(&Utc)
302                .to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
303        },
304    )
305}