Skip to main content

better_auth_core/
field_policy.rs

1//! Application and plugin field policies at the session input boundary.
2use crate::utils::json::JsValue;
3use indexmap::{IndexMap, IndexSet};
4use serde_json::Value;
5use std::{fmt, future::Future, pin::Pin, sync::Arc};
6
7/// Values retain JavaScript numbers until the actual database binding.
8#[derive(Clone, Default)]
9pub struct FieldValues {
10    values: IndexMap<String, JsValue>,
11    adapter_fields: Option<Arc<FieldConfigs>>,
12    undefined_input_keys: IndexSet<String>,
13    transform_omitted: bool,
14    binding_names: IndexMap<String, String>,
15}
16
17impl FieldValues {
18    #[must_use]
19    pub fn new() -> Self {
20        Self::default()
21    }
22    /// Whether parsing supplied any keys, including a transform returning undefined.
23    /// Unlike `is_empty`, this reflects JavaScript Object.keys before adapter omission.
24    #[must_use]
25    pub fn has_input_fields(&self) -> bool {
26        !self.values.is_empty() || !self.undefined_input_keys.is_empty()
27    }
28    /// Resolve an immutable configured physical column at the binding boundary.
29    #[must_use]
30    pub fn binding_name<'a>(&'a self, name: &'a str) -> &'a str {
31        self.binding_names.get(name).map_or(name, String::as_str)
32    }
33    /// Consume pending adapter callbacks once using current values after database hooks.
34    /// Custom stores must call this immediately before binding configured session fields.
35    ///
36    /// # Errors
37    ///
38    /// Propagates errors from configured adapter field transforms.
39    pub fn apply_adapter_transforms(&mut self) -> crate::AuthResult<()> {
40        if self.adapter_fields.as_ref().is_some_and(|fields| {
41            fields
42                .values()
43                .any(|field| field.adapter_input_transform.is_some())
44        }) {
45            return Err(crate::AuthError::config(
46                "Asynchronous adapter transforms require apply_adapter_transforms_async",
47            ));
48        }
49        if let Some(fields) = self.adapter_fields.take() {
50            for (name, field) in &*fields {
51                if self.prepare_adapter_value(name, field)
52                    && let Some(transform) = &field.transform
53                {
54                    let value = transform(self.values.get(name))?;
55                    self.set_adapter_value(name, value);
56                }
57            }
58        }
59        self.finish_adapter_transforms();
60        Ok(())
61    }
62
63    /// Await genuine storage-stage callbacks after database before hooks.
64    /// Synchronous endpoint parsing is a separate phase.
65    ///
66    /// # Errors
67    /// Propagates configured callback errors.
68    pub async fn apply_adapter_transforms_async(&mut self) -> crate::AuthResult<()> {
69        if let Some(fields) = self.adapter_fields.take() {
70            for (name, field) in &*fields {
71                if !self.prepare_adapter_value(name, field) {
72                    continue;
73                }
74                let value = if let Some(transform) = &field.adapter_input_transform {
75                    transform(self.values.get(name).cloned()).await.map_err(
76                        |error| match error {
77                            crate::AuthError::Internal(_) => {
78                                crate::AuthError::CallbackFailure(Box::new(error))
79                            }
80                            error => error,
81                        },
82                    )?
83                } else if let Some(transform) = &field.transform {
84                    transform(self.values.get(name))?
85                } else {
86                    continue;
87                };
88                self.set_adapter_value(name, value);
89            }
90        }
91        self.finish_adapter_transforms();
92        Ok(())
93    }
94
95    fn prepare_adapter_value(&mut self, name: &str, field: &FieldConfig) -> bool {
96        if !self.transform_omitted && !self.values.contains_key(name) && field.on_update.is_none() {
97            return false;
98        }
99        if self.transform_omitted
100            && (!self.values.contains_key(name)
101                || (field.required && self.values.get(name).is_some_and(JsValue::is_null)))
102            && let Some(default) = &field.default
103        {
104            drop(self.values.insert(name.to_owned(), default.value()));
105        }
106        if !self.transform_omitted
107            && !self.values.contains_key(name)
108            && let Some(update) = &field.on_update
109        {
110            drop(self.values.insert(name.to_owned(), update()));
111        }
112        true
113    }
114    fn set_adapter_value(&mut self, name: &str, value: Option<JsValue>) {
115        match value {
116            Some(value) => {
117                drop(self.values.insert(name.to_owned(), value));
118            }
119            None => {
120                drop(self.values.shift_remove(name));
121            }
122        }
123    }
124    fn finish_adapter_transforms(&mut self) {
125        self.undefined_input_keys.clear();
126        self.transform_omitted = false;
127    }
128    /// Preserve an existing trusted creation value before adapter defaults are applied.
129    /// Database hooks may already have supplied a mapped replacement, which wins.
130    pub fn preserve_creation_value(&mut self, name: &str, value: JsValue) {
131        if self
132            .adapter_fields
133            .as_ref()
134            .is_some_and(|fields| fields.contains_key(name))
135        {
136            let _ignored_or_insert = self.values.entry(name.to_owned()).or_insert(value);
137        }
138    }
139}
140
141impl fmt::Debug for FieldValues {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        self.values.fmt(f)
144    }
145}
146
147impl std::ops::Deref for FieldValues {
148    type Target = IndexMap<String, JsValue>;
149    fn deref(&self) -> &Self::Target {
150        &self.values
151    }
152}
153
154impl std::ops::DerefMut for FieldValues {
155    fn deref_mut(&mut self) -> &mut Self::Target {
156        &mut self.values
157    }
158}
159
160impl From<IndexMap<String, JsValue>> for FieldValues {
161    fn from(values: IndexMap<String, JsValue>) -> Self {
162        Self {
163            values,
164            ..Self::default()
165        }
166    }
167}
168
169impl FromIterator<(String, JsValue)> for FieldValues {
170    fn from_iter<T: IntoIterator<Item = (String, JsValue)>>(iter: T) -> Self {
171        IndexMap::from_iter(iter).into()
172    }
173}
174
175impl IntoIterator for FieldValues {
176    type Item = (String, JsValue);
177    type IntoIter = indexmap::map::IntoIter<String, JsValue>;
178    fn into_iter(self) -> Self::IntoIter {
179        self.values.into_iter()
180    }
181}
182
183impl<'a> IntoIterator for &'a FieldValues {
184    type Item = (&'a String, &'a JsValue);
185    type IntoIter = indexmap::map::Iter<'a, String, JsValue>;
186    fn into_iter(self) -> Self::IntoIter {
187        self.values.iter()
188    }
189}
190
191impl<'a> IntoIterator for &'a mut FieldValues {
192    type Item = (&'a String, &'a mut JsValue);
193    type IntoIter = indexmap::map::IterMut<'a, String, JsValue>;
194    fn into_iter(self) -> Self::IntoIter {
195        self.values.iter_mut()
196    }
197}
198
199impl serde::Serialize for FieldValues {
200    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
201        self.values.serialize(serializer)
202    }
203}
204
205impl<'de> serde::Deserialize<'de> for FieldValues {
206    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
207        IndexMap::deserialize(deserializer).map(Self::from)
208    }
209}
210
211pub type FieldOutput = serde_json::Map<String, Value>;
212
213pub type FieldConfigs = IndexMap<String, FieldConfig>;
214
215pub type FieldValidator = Arc<dyn Fn(&JsValue) -> Result<JsValue, String> + Send + Sync>;
216pub type AsyncFieldValidator = Arc<
217    dyn Fn(JsValue) -> Pin<Box<dyn Future<Output = Result<JsValue, String>> + Send>> + Send + Sync,
218>;
219
220/// `None` is JavaScript undefined: absent input or omitted adapter output.
221pub type FieldTransform =
222    Arc<dyn Fn(Option<&JsValue>) -> crate::AuthResult<Option<JsValue>> + Send + Sync>;
223
224/// Adapter output callbacks receive an owned actual storage value and are awaited.
225pub type FieldOutputTransform = Arc<
226    dyn Fn(
227            Option<JsValue>,
228        ) -> Pin<Box<dyn Future<Output = crate::AuthResult<Option<JsValue>>> + Send>>
229        + Send
230        + Sync,
231>;
232
233#[derive(Clone)]
234pub enum FieldDefault {
235    Value(JsValue),
236    Callback(Arc<dyn Fn() -> JsValue + Send + Sync>),
237}
238
239impl fmt::Debug for FieldDefault {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        match self {
242            Self::Value(value) => f.debug_tuple("Value").field(value).finish(),
243            Self::Callback(_) => f.write_str("Callback(..)"),
244        }
245    }
246}
247
248impl FieldDefault {
249    #[must_use]
250    pub fn value(&self) -> JsValue {
251        match self {
252            Self::Value(value) => value.clone(),
253            Self::Callback(callback) => callback(),
254        }
255    }
256}
257
258/// Type/schema metadata does not implicitly validate input values.
259#[derive(Clone)]
260pub struct FieldConfig {
261    pub schema: Value,
262    pub required: bool,
263    pub input: bool,
264    pub returned: bool,
265    pub default: Option<FieldDefault>,
266    pub validator: Option<FieldValidator>,
267    pub async_validator: Option<AsyncFieldValidator>,
268    /// Source declares output validators but does not invoke them at this stage.
269    pub output_validator: Option<FieldValidator>,
270    pub adapter_input_transform: Option<FieldOutputTransform>,
271    pub transform: Option<FieldTransform>,
272    pub output_transform: Option<FieldOutputTransform>,
273    pub on_update: Option<Arc<dyn Fn() -> JsValue + Send + Sync>>,
274    /// Physical column binding. Logical input/output names remain registry keys.
275    pub field_name: Option<String>,
276}
277
278impl fmt::Debug for FieldConfig {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        f.debug_struct("FieldConfig")
281            .field("schema", &self.schema)
282            .field("required", &self.required)
283            .field("input", &self.input)
284            .field("returned", &self.returned)
285            .field("default", &self.default)
286            .field("validator", &self.validator.as_ref().map(|_| "callback"))
287            .field(
288                "async_validator",
289                &self.async_validator.as_ref().map(|_| "callback"),
290            )
291            .field(
292                "output_validator",
293                &self.output_validator.as_ref().map(|_| "callback"),
294            )
295            .field(
296                "adapter_input_transform",
297                &self.adapter_input_transform.as_ref().map(|_| "callback"),
298            )
299            .field("transform", &self.transform.as_ref().map(|_| "callback"))
300            .field(
301                "output_transform",
302                &self.output_transform.as_ref().map(|_| "callback"),
303            )
304            .field("on_update", &self.on_update.as_ref().map(|_| "callback"))
305            .field("field_name", &self.field_name)
306            .finish()
307    }
308}
309
310impl FieldConfig {
311    #[must_use]
312    pub fn new(schema: Value) -> Self {
313        Self {
314            schema,
315            required: false,
316            input: true,
317            returned: true,
318            default: None,
319            validator: None,
320            async_validator: None,
321            output_validator: None,
322            adapter_input_transform: None,
323            transform: None,
324            output_transform: None,
325            on_update: None,
326            field_name: None,
327        }
328    }
329    #[must_use]
330    pub fn default_value(mut self, value: impl Into<JsValue>) -> Self {
331        self.default = Some(FieldDefault::Value(value.into()));
332        self
333    }
334    #[must_use]
335    pub fn default_callback(
336        mut self,
337        callback: impl Fn() -> JsValue + Send + Sync + 'static,
338    ) -> Self {
339        self.default = Some(FieldDefault::Callback(Arc::new(callback)));
340        self
341    }
342    #[must_use]
343    pub const fn read_only(mut self) -> Self {
344        self.input = false;
345        self
346    }
347    #[must_use]
348    pub const fn hidden(mut self) -> Self {
349        self.returned = false;
350        self
351    }
352    #[must_use]
353    pub fn validate(
354        mut self,
355        validator: impl Fn(&JsValue) -> Result<JsValue, String> + Send + Sync + 'static,
356    ) -> Self {
357        self.validator = Some(Arc::new(validator));
358        self.async_validator = None;
359        self
360    }
361    #[must_use]
362    pub fn transform(
363        mut self,
364        transform: impl Fn(Option<&JsValue>) -> crate::AuthResult<Option<JsValue>>
365        + Send
366        + Sync
367        + 'static,
368    ) -> Self {
369        self.transform = Some(Arc::new(transform));
370        self
371    }
372
373    /// A Promise-style validator is invoked but rejected by endpoint parsing,
374    /// matching the published ASYNC_VALIDATION_NOT_SUPPORTED contract.
375    #[must_use]
376    pub fn validate_async<F, Fut>(mut self, validate: F) -> Self
377    where
378        F: Fn(JsValue) -> Fut + Send + Sync + 'static,
379        Fut: Future<Output = Result<JsValue, String>> + Send + 'static,
380    {
381        self.async_validator = Some(Arc::new(move |value| Box::pin(validate(value))));
382        self.validator = None;
383        self
384    }
385
386    #[must_use]
387    pub fn validate_output(
388        mut self,
389        validate: impl Fn(&JsValue) -> Result<JsValue, String> + Send + Sync + 'static,
390    ) -> Self {
391        self.output_validator = Some(Arc::new(validate));
392        self
393    }
394
395    /// Await this callback only at the actual adapter binding boundary. For a
396    /// Source input transform shared with endpoint parsing, use a synchronous
397    /// validator to select its parsed value before the awaited storage phase.
398    #[must_use]
399    pub fn transform_adapter_input<F, Fut>(mut self, transform: F) -> Self
400    where
401        F: Fn(Option<JsValue>) -> Fut + Send + Sync + 'static,
402        Fut: Future<Output = crate::AuthResult<Option<JsValue>>> + Send + 'static,
403    {
404        self.adapter_input_transform = Some(Arc::new(move |value| Box::pin(transform(value))));
405        self
406    }
407
408    #[must_use]
409    pub fn transform_output<F, Fut>(mut self, transform: F) -> Self
410    where
411        F: Fn(Option<JsValue>) -> Fut + Send + Sync + 'static,
412        Fut: Future<Output = crate::AuthResult<Option<JsValue>>> + Send + 'static,
413    {
414        self.output_transform = Some(Arc::new(move |value| Box::pin(transform(value))));
415        self
416    }
417
418    #[must_use]
419    pub fn on_update(mut self, callback: impl Fn() -> JsValue + Send + Sync + 'static) -> Self {
420        self.on_update = Some(Arc::new(callback));
421        self
422    }
423
424    #[must_use]
425    pub fn field_name(mut self, name: impl Into<String>) -> Self {
426        self.field_name = Some(name.into());
427        self
428    }
429}
430
431#[derive(Debug)]
432pub enum FieldInputError {
433    Validation { code: &'static str, message: String },
434    Transform(crate::AuthError),
435}
436
437/// Immutable instance policies: configuration first, registered plugins last.
438#[derive(Debug, Clone, Default)]
439pub struct SessionFields(pub IndexMap<String, FieldConfig>);
440
441impl SessionFields {
442    pub fn defaults(&self, values: &mut FieldValues) {
443        for (name, field) in &self.0 {
444            if let Some(default) = &field.default {
445                let _ignored_value = values
446                    .entry(name.clone())
447                    .or_insert_with(|| default.value());
448            }
449        }
450    }
451    ///
452    /// # Errors
453    ///
454    /// Returns a validation error for disallowed fields or rejected field validators.
455    pub fn parse_update(
456        &self,
457        input: &IndexMap<String, JsValue>,
458    ) -> Result<FieldValues, FieldInputError> {
459        self.parse(input, false)
460    }
461
462    /// Parse application input before database hooks. Creation defaults and
463    /// required fields apply here; adapter binding remains a separate phase.
464    pub fn parse_create(
465        &self,
466        input: &IndexMap<String, JsValue>,
467    ) -> Result<FieldValues, FieldInputError> {
468        self.parse(input, true)
469    }
470
471    fn parse(
472        &self,
473        input: &IndexMap<String, JsValue>,
474        creation: bool,
475    ) -> Result<FieldValues, FieldInputError> {
476        let mut parsed = FieldValues::new();
477        for (name, field) in &self.0 {
478            let Some(value) = input.get(name) else {
479                if creation {
480                    if let Some(default) = &field.default {
481                        drop(parsed.insert(name.clone(), default.value()));
482                    } else if field.required {
483                        return Err(FieldInputError::Validation {
484                            code: "MISSING_FIELD",
485                            message: format!("{name} is required"),
486                        });
487                    }
488                }
489                continue;
490            };
491            if !field.input {
492                if creation && let Some(default) = &field.default {
493                    drop(parsed.insert(name.clone(), default.value()));
494                    continue;
495                }
496                if truthy(value) {
497                    return Err(FieldInputError::Validation {
498                        code: "FIELD_NOT_ALLOWED",
499                        message: format!("{name} is not allowed to be set"),
500                    });
501                }
502                continue;
503            }
504            if let Some(validator) = &field.async_validator {
505                drop(validator(value.clone()));
506                return Err(FieldInputError::Transform(crate::AuthError::Upstream {
507                    status: 500,
508                    code: "ASYNC_VALIDATION_NOT_SUPPORTED",
509                    message: "Async validation is not supported",
510                }));
511            }
512            let value = if let Some(validator) = &field.validator {
513                Some(
514                    validator(value).map_err(|message| FieldInputError::Validation {
515                        code: "VALIDATION_ERROR",
516                        message: if message.is_empty() {
517                            "Validation Error".into()
518                        } else {
519                            message
520                        },
521                    })?,
522                )
523            } else if let Some(transform) = &field.transform {
524                transform(Some(value)).map_err(FieldInputError::Transform)?
525            } else {
526                Some(value.clone())
527            };
528            match value {
529                Some(value) => {
530                    drop(parsed.insert(name.clone(), value));
531                }
532                None => {
533                    let _ignored_clone = parsed.undefined_input_keys.insert(name.clone());
534                }
535            }
536        }
537        Ok(parsed)
538    }
539}
540
541/// Immutable policies for application/plugin user fields.
542#[derive(Debug, Clone, Default)]
543pub struct UserFields(pub SessionFields);
544
545/// Immutable policies for application/plugin account fields.
546#[derive(Debug, Clone, Default)]
547pub struct AccountFields(pub SessionFields);
548
549/// Adapter precedence remains distinct from endpoint/public projection policies.
550#[derive(Debug, Clone, Default)]
551pub struct AdapterFieldPolicies {
552    pub user: SessionAdapterFields,
553    pub account: SessionAdapterFields,
554}
555
556/// Immutable adapter schema policies: plugins first, application configuration last.
557/// This differs from input/output `SessionFields`, matching getAuthTables precedence.
558#[derive(Debug, Clone, Default)]
559pub struct SessionAdapterFields(pub Arc<FieldConfigs>);
560
561impl SessionAdapterFields {
562    pub(crate) fn attach(&self, values: &mut FieldValues, creation: bool) {
563        values.transform_omitted = creation;
564        values.adapter_fields = Some(Arc::clone(&self.0));
565        values.binding_names = self
566            .0
567            .iter()
568            .filter_map(|(name, field)| {
569                field
570                    .field_name
571                    .as_ref()
572                    .map(|physical| (name.clone(), physical.clone()))
573            })
574            .collect();
575    }
576
577    /// Resolve declared fields from physical canonical getters and custom columns
578    /// before applying their output policies. The base projection alone controls
579    /// undeclared fields, so unrelated physical columns never become observable.
580    pub(crate) async fn record_output(
581        &self,
582        canonical: Value,
583        mut additional: FieldOutput,
584        base: Value,
585    ) -> crate::AuthResult<crate::AdapterOutput> {
586        let (Value::Object(canonical), Value::Object(mut base)) = (canonical, base) else {
587            return Err(crate::AuthError::internal(
588                "Adapter output must be an object",
589            ));
590        };
591        additional.extend(canonical);
592        for name in self.0.keys() {
593            // Typed DTOs omit optional core fields during serialization. The
594            // actual initialized projection still distinguishes a stored null
595            // from an absent property; retain it before applying output policy.
596            if !additional.contains_key(name)
597                && let Some(value) = base.get(name)
598            {
599                drop(additional.insert(name.clone(), value.clone()));
600            }
601            drop(base.remove(name));
602        }
603        let mut output = crate::AdapterOutput::from_values(base);
604        output.extend(self.output(additional).await?);
605        Ok(output)
606    }
607
608    /// Transform only declared additional fields, retaining their omission.
609    pub(crate) async fn output(
610        &self,
611        values: FieldOutput,
612    ) -> crate::AuthResult<crate::AdapterOutput> {
613        let mut output = crate::AdapterOutput::default();
614        for (name, field) in &*self.0 {
615            let value = values
616                .get(name)
617                .or_else(|| {
618                    field
619                        .field_name
620                        .as_ref()
621                        .and_then(|physical| values.get(physical))
622                })
623                .cloned()
624                .map(JsValue::from);
625            let value = if let Some(transform) = &field.output_transform {
626                transform(value).await.map_err(|error| match error {
627                    crate::AuthError::Api { .. } | crate::AuthError::Upstream { .. } => error,
628                    error => crate::AuthError::CallbackFailure(Box::new(error)),
629                })?
630            } else {
631                value
632            };
633            output.insert(
634                name.clone(),
635                value.map(|value| value.to_json_value()).transpose()?,
636            );
637        }
638        Ok(output)
639    }
640}
641
642fn truthy(value: &JsValue) -> bool {
643    match value {
644        JsValue::Null => false,
645        JsValue::Bool(value) => *value,
646        JsValue::Number(value) => *value != 0.0 && !value.is_nan(),
647        JsValue::String(value) => !value.is_empty(),
648        JsValue::Array(_) | JsValue::Object(_) => true,
649    }
650}