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