Skip to main content

better_auth_core/wire/
plugins.rs

1use super::*;
2/// Public organization response shape.
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
4pub struct OrganizationView {
5    #[serde(default, flatten)]
6    pub additional_fields: std::collections::BTreeMap<String, serde_json::Value>,
7    pub id: String,
8    pub name: String,
9    pub slug: String,
10    pub logo: Option<String>,
11    #[serde(
12        serialize_with = "serialize_json_option_as_string",
13        skip_serializing_if = "Option::is_none"
14    )]
15    #[serde(
16        default,
17        deserialize_with = "crate::utils::json::deserialize_optional_value"
18    )]
19    pub metadata: Option<serde_json::Value>,
20    #[serde(rename = "createdAt")]
21    #[serde(serialize_with = "crate::utils::datetime::serialize")]
22    pub created_at: DateTime<Utc>,
23    #[serde(rename = "updatedAt")]
24    #[serde(serialize_with = "crate::utils::datetime::serialize")]
25    pub updated_at: DateTime<Utc>,
26}
27
28impl<T: AuthOrganization> From<&T> for OrganizationView {
29    fn from(org: &T) -> Self {
30        Self {
31            additional_fields: org.additional_fields(),
32            id: org.id().into_owned(),
33            name: org.name().to_owned(),
34            slug: org.slug().to_owned(),
35            logo: org.logo().map(str::to_owned),
36            metadata: org.metadata().cloned(),
37            created_at: org.created_at(),
38            updated_at: org.updated_at(),
39        }
40    }
41}
42
43/// Public invitation response shape.
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub struct InvitationView {
46    pub id: String,
47    #[serde(rename = "organizationId")]
48    pub organization_id: String,
49    pub email: String,
50    pub role: Option<String>,
51    pub status: InvitationStatus,
52    #[serde(rename = "inviterId")]
53    pub inviter_id: String,
54    #[serde(rename = "expiresAt")]
55    #[serde(serialize_with = "crate::utils::datetime::serialize")]
56    pub expires_at: DateTime<Utc>,
57    #[serde(rename = "createdAt")]
58    #[serde(serialize_with = "crate::utils::datetime::serialize")]
59    pub created_at: DateTime<Utc>,
60    #[serde(rename = "teamId", default, skip_serializing_if = "Option::is_none")]
61    pub team_id: Option<String>,
62    #[serde(
63        flatten,
64        default,
65        deserialize_with = "crate::utils::json::deserialize_btree_map"
66    )]
67    pub extension_fields: std::collections::BTreeMap<String, serde_json::Value>,
68}
69
70impl<T: AuthInvitation> From<&T> for InvitationView {
71    fn from(inv: &T) -> Self {
72        Self {
73            id: inv.id().into_owned(),
74            organization_id: inv.organization_id().into_owned(),
75            email: inv.email().to_owned(),
76            role: inv.optional_role().map(str::to_owned),
77            status: inv.status().clone(),
78            inviter_id: inv.inviter_id().into_owned(),
79            expires_at: inv.expires_at(),
80            created_at: inv.created_at(),
81            team_id: inv.team_id().map(str::to_owned),
82            extension_fields: std::collections::BTreeMap::default(),
83        }
84    }
85}
86
87/// Public passkey response shape.
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
89pub struct PasskeyView {
90    pub id: String,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub name: Option<String>,
93    #[serde(rename = "credentialID")]
94    pub credential_id: String,
95    #[serde(rename = "userId")]
96    pub user_id: String,
97    #[serde(rename = "publicKey")]
98    pub public_key: String,
99    pub counter: u64,
100    #[serde(rename = "deviceType")]
101    pub device_type: String,
102    #[serde(rename = "backedUp")]
103    pub backed_up: bool,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub transports: Option<String>,
106    #[serde(rename = "createdAt")]
107    pub created_at: String,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub aaguid: Option<String>,
110}
111
112impl<T: AuthPasskey> From<&T> for PasskeyView {
113    fn from(pk: &T) -> Self {
114        Self {
115            id: pk.id().into_owned(),
116            name: pk.name().map(str::to_owned),
117            credential_id: pk.credential_id().to_owned(),
118            user_id: pk.user_id().into_owned(),
119            public_key: pk.public_key().to_owned(),
120            counter: pk.counter(),
121            device_type: pk.device_type().to_owned(),
122            backed_up: pk.backed_up(),
123            transports: pk.transports().map(str::to_owned),
124            created_at: pk
125                .created_at()
126                .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
127            aaguid: pk.aaguid().map(str::to_owned),
128        }
129    }
130}
131
132/// Public API key response shape.
133///
134/// Intentionally omits `key_hash` — the hashed key value is never returned
135/// over the wire (matches upstream TS behavior).
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137pub struct ApiKeyView {
138    pub id: String,
139    pub name: Option<String>,
140    pub start: Option<String>,
141    pub prefix: Option<String>,
142    #[serde(rename = "referenceId")]
143    pub reference_id: String,
144    #[serde(rename = "configId")]
145    pub config_id: String,
146    #[serde(rename = "refillInterval")]
147    #[serde(serialize_with = "serialize_api_key_number")]
148    pub refill_interval: Option<f64>,
149    #[serde(rename = "refillAmount")]
150    #[serde(serialize_with = "serialize_api_key_number")]
151    pub refill_amount: Option<f64>,
152    #[serde(rename = "lastRefillAt")]
153    pub last_refill_at: Option<String>,
154    pub enabled: bool,
155    #[serde(rename = "rateLimitEnabled")]
156    pub rate_limit_enabled: bool,
157    #[serde(rename = "rateLimitTimeWindow")]
158    #[serde(serialize_with = "serialize_api_key_number")]
159    pub rate_limit_time_window: Option<f64>,
160    #[serde(rename = "rateLimitMax")]
161    #[serde(serialize_with = "serialize_api_key_number")]
162    pub rate_limit_max: Option<f64>,
163    #[serde(rename = "requestCount")]
164    #[serde(serialize_with = "serialize_api_key_number")]
165    pub request_count: Option<f64>,
166    #[serde(serialize_with = "serialize_api_key_number")]
167    pub remaining: Option<f64>,
168    #[serde(rename = "lastRequest")]
169    pub last_request: Option<String>,
170    #[serde(rename = "expiresAt")]
171    pub expires_at: Option<String>,
172    #[serde(rename = "createdAt")]
173    pub created_at: String,
174    #[serde(rename = "updatedAt")]
175    pub updated_at: String,
176    #[serde(
177        default,
178        deserialize_with = "crate::utils::json::deserialize_optional_value"
179    )]
180    pub permissions: Option<serde_json::Value>,
181    #[serde(
182        default,
183        deserialize_with = "crate::utils::json::deserialize_optional_value"
184    )]
185    pub metadata: Option<serde_json::Value>,
186}
187
188impl<T: AuthApiKey> From<&T> for ApiKeyView {
189    fn from(ak: &T) -> Self {
190        Self {
191            id: ak.id().into_owned(),
192            name: ak.name().map(str::to_owned),
193            start: ak.start().map(str::to_owned),
194            prefix: ak.prefix().map(str::to_owned),
195            reference_id: ak.reference_id().into_owned(),
196            config_id: ak.config_id().into_owned(),
197            refill_interval: ak.refill_interval(),
198            refill_amount: ak.refill_amount(),
199            last_refill_at: ak.last_refill_at().map(api_key_wire_date),
200            enabled: ak.enabled(),
201            rate_limit_enabled: ak.rate_limit_enabled(),
202            rate_limit_time_window: ak.rate_limit_time_window(),
203            rate_limit_max: ak.rate_limit_max(),
204            request_count: ak.request_count(),
205            remaining: ak.remaining(),
206            last_request: ak.last_request().map(api_key_wire_date),
207            expires_at: ak.expires_at().map(api_key_wire_date),
208            created_at: api_key_wire_date(ak.created_at()),
209            updated_at: api_key_wire_date(ak.updated_at()),
210            permissions: ak
211                .permissions()
212                .and_then(|s| crate::utils::json::from_slice(s.as_bytes()).ok())
213                .map(|mut value| {
214                    normalize_api_key_permission_dates(&mut value);
215                    value
216                }),
217            metadata: ak.metadata().and_then(|s| {
218                let value = crate::utils::json::parse_value(s).ok()?;
219                if let crate::utils::json::JsValue::String(encoded) = value {
220                    let mut value = crate::utils::json::parse_value(&encoded)
221                        .ok()?
222                        .to_json_value()
223                        .ok()?;
224                    normalize_api_key_permission_dates(&mut value);
225                    Some(value)
226                } else {
227                    value.to_json_value().ok()
228                }
229            }),
230        }
231    }
232}
233
234pub(in crate::wire) fn normalize_api_key_permission_dates(value: &mut serde_json::Value) {
235    match value {
236        serde_json::Value::String(text) => {
237            if let Some(date) = crate::utils::datetime::normalize_json_date(text) {
238                *text = date;
239            }
240        }
241        serde_json::Value::Array(values) => {
242            for value in values {
243                normalize_api_key_permission_dates(value);
244            }
245        }
246        serde_json::Value::Object(values) => {
247            for value in values.values_mut() {
248                normalize_api_key_permission_dates(value);
249            }
250        }
251        _ => {}
252    }
253}
254
255// ---------------------------------------------------------------------------
256// Plugin entity views
257// ---------------------------------------------------------------------------
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}