Skip to main content

alibi_core/config/
mod.rs

1mod account;
2mod advanced;
3mod client_ip;
4mod id;
5mod identity;
6mod origin;
7mod secrets;
8mod session;
9
10pub use account::{AccountConfig, AccountLinkingConfig, OAuthStateStrategy};
11pub use advanced::{
12    AdvancedConfig, AdvancedDatabaseConfig, CookieAttributes, CookieOverride, CrossSubDomainConfig,
13    IpAddressConfig, SameSite, TwoFactorDatabaseConfig,
14};
15#[doc(hidden)]
16pub use id::serial_id_statements;
17pub use id::{DatabaseIdGenerator, DatabaseIdStrategy};
18pub use identity::{PasswordConfig, UserConfig, VerificationConfig};
19pub use origin::{
20    BaseUrlProtocol, DynamicBaseUrl, TrustedOriginsResolver, TrustedProvidersResolver,
21};
22pub use secrets::ManagedSecrets;
23pub use session::{
24    CookieCacheConfig, CookieCacheStrategy, CookieRefreshCache, JwtConfig, SessionConfig,
25};
26
27/// Well-known core route paths.
28///
29/// These constants are the single source of truth for route paths used by both
30/// the core request dispatcher (`handle_core_request`) and framework-specific
31/// routers (e.g. Axum) so that path strings are never duplicated.
32pub mod core_paths {
33    pub const OK: &str = "/ok";
34    pub const ERROR: &str = "/error";
35    pub const HEALTH: &str = "/health";
36    pub const OPENAPI_SPEC: &str = "/__test/openapi.json";
37    pub const UPDATE_USER: &str = "/update-user";
38    pub const DELETE_USER: &str = "/delete-user";
39    pub const CHANGE_EMAIL: &str = "/change-email";
40    pub const DELETE_USER_CALLBACK: &str = "/delete-user/callback";
41}
42
43use crate::email::EmailProvider;
44use crate::error::AuthError;
45use chrono::Duration;
46use std::collections::HashMap;
47use std::sync::Arc;
48
49/// Response policy for awaited lifecycle notifications. Explicit verification
50/// delivery retains its direct error contract; deferred work logs its errors.
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub enum AwaitedNotificationErrorPolicy {
53    /// Propagate the callback error after retaining any committed auth writes.
54    #[default]
55    Propagate,
56    /// Log delivery failure and continue the authentication response.
57    LogAndContinue,
58}
59
60/// Main configuration for `Alibi`
61#[derive(Clone)]
62pub struct AuthConfig {
63    /// Single-secret input. Managed mode uses `current_secret()` instead.
64    pub secret: String,
65
66    /// Versioned encryption keys. When present, its current key signs new tokens.
67    /// Bare encrypted values require an explicitly configured legacy key.
68    pub managed_secrets: Option<ManagedSecrets>,
69
70    /// Application name, used for cookie prefixes, email templates, etc.
71    ///
72    /// Defaults to `"Better Auth"`.
73    pub app_name: String,
74
75    /// Base URL for the authentication service (e.g. `"http://localhost:3000"`).
76    pub base_url: String,
77
78    /// Request-dependent host allowlist. Static `base_url` remains the default.
79    pub dynamic_base_url: Option<DynamicBaseUrl>,
80
81    /// Application callback resolving additional trusted origins from the real request.
82    pub trusted_origins_resolver: Option<Arc<dyn TrustedOriginsResolver>>,
83
84    /// Base path where the auth routes are mounted.
85    ///
86    /// All routes handled by `Alibi` will be prefixed with this path.
87    /// For example, with the default `"/api/auth"`, the sign-in route becomes
88    /// `"/api/auth/sign-in/email"`.
89    ///
90    /// Defaults to `"/api/auth"`.
91    pub base_path: String,
92
93    /// Origins that are trusted for CSRF and other cross-origin checks.
94    ///
95    /// Supports glob patterns (e.g. `"https://*.example.com"`).
96    /// These are shared across all middleware that needs origin validation
97    /// (CSRF, CORS, etc.).
98    pub trusted_origins: Vec<String>,
99
100    /// Paths that should be disabled (skipped) by the router.
101    ///
102    /// Any request whose path matches an entry in this list will receive
103    /// a 404 response, even if a handler is registered for it.
104    pub disabled_paths: Vec<String>,
105
106    /// Render the built-in HTML page for `GET /error` instead of redirecting to `/`.
107    ///
108    /// Defaults to `false` when `NODE_ENV` is `production`, and `true` otherwise.
109    /// Set this to `true` to enable the renderer in production, corresponding to
110    /// upstream's explicit `onAPIError.customizeDefaultErrorPage` option.
111    pub render_error_page: bool,
112
113    /// Default OAuth error destination, corresponding to `onAPIError.errorURL`.
114    pub api_error_url: Option<String>,
115
116    /// Propagate ordinary endpoint failures to the hosting application.
117    /// Explicit public API errors continue to return HTTP responses.
118    pub throw_api_errors: bool,
119
120    /// Session configuration
121    pub session: SessionConfig,
122
123    /// Application user-field input, adapter and public output policies.
124    pub user: UserConfig,
125
126    /// Verification storage lifecycle settings.
127    pub verification: VerificationConfig,
128
129    /// JWT configuration
130    pub jwt: JwtConfig,
131
132    /// Password configuration
133    pub password: PasswordConfig,
134
135    /// Account configuration (linking, token encryption, etc.)
136    pub account: AccountConfig,
137
138    /// Email provider for sending emails (verification, password reset, etc.)
139    pub email_provider: Option<Arc<dyn EmailProvider>>,
140    /// Observe already running deferred operations. Ignoring their completion
141    /// does not cancel them; applications may retain completions for shutdown.
142    pub background_tasks: Option<Arc<dyn crate::BackgroundTaskHandler>>,
143    /// Error handling when the request awaits a lifecycle notification.
144    pub awaited_notification_errors: AwaitedNotificationErrorPolicy,
145
146    /// Validate fresh identity data before its creation hooks or provider
147    /// account/session writes. Returning non-provider sign-ins are unchanged.
148    pub user_validation: Option<Arc<dyn crate::user_validation::UserInfoValidator>>,
149
150    /// Advanced configuration options
151    pub advanced: AdvancedConfig,
152}
153
154impl Default for AuthConfig {
155    fn default() -> Self {
156        Self {
157            secret: String::new(),
158            managed_secrets: None,
159            app_name: "Better Auth".to_owned(),
160            base_url: "http://localhost:3000".to_owned(),
161            dynamic_base_url: None,
162            trusted_origins_resolver: None,
163            base_path: "/api/auth".to_owned(),
164            trusted_origins: Vec::new(),
165            disabled_paths: Vec::new(),
166            render_error_page: !std::env::var("NODE_ENV").is_ok_and(|value| value == "production"),
167            api_error_url: None,
168            throw_api_errors: false,
169            session: SessionConfig::default(),
170            user: UserConfig::default(),
171            verification: VerificationConfig::default(),
172            jwt: JwtConfig::default(),
173            password: PasswordConfig::default(),
174            account: AccountConfig::default(),
175            email_provider: None,
176            background_tasks: None,
177            awaited_notification_errors: AwaitedNotificationErrorPolicy::default(),
178            user_validation: None,
179            advanced: AdvancedConfig::default(),
180        }
181    }
182}
183
184impl AuthConfig {
185    /// Choose whether awaited lifecycle email delivery failures fail the request.
186    #[must_use]
187    pub const fn awaited_notification_errors(
188        mut self,
189        policy: AwaitedNotificationErrorPolicy,
190    ) -> Self {
191        self.awaited_notification_errors = policy;
192        self
193    }
194
195    /// Integrate deferred task completions with the application executor.
196    #[must_use]
197    pub fn background_tasks(mut self, handler: Arc<dyn crate::BackgroundTaskHandler>) -> Self {
198        self.background_tasks = Some(handler);
199        self
200    }
201
202    #[must_use]
203    pub fn new(secret: impl Into<String>) -> Self {
204        Self {
205            secret: secret.into(),
206            ..Default::default()
207        }
208    }
209
210    /// Set the application name.
211    #[must_use]
212    pub fn app_name(mut self, name: impl Into<String>) -> Self {
213        self.app_name = name.into();
214        self
215    }
216
217    /// Set the base URL (e.g. `"https://myapp.com"`).
218    ///
219    /// Also updates `session.cookie_secure` to match the URL scheme:
220    /// HTTPS URLs set `Secure=true`, HTTP URLs set `Secure=false`.
221    #[must_use]
222    pub fn base_url(mut self, url: impl Into<String>) -> Self {
223        self.base_url = url.into();
224        self.session.cookie_secure = self.base_url.starts_with("https://");
225        self
226    }
227
228    #[must_use]
229    pub fn account(mut self, account: AccountConfig) -> Self {
230        self.account = account;
231        self
232    }
233
234    /// Configure the default OAuth error destination (`onAPIError.errorURL`).
235    /// An empty value uses the endpoint's ordinary fallback.
236    #[must_use]
237    pub fn api_error_url(mut self, url: impl Into<String>) -> Self {
238        self.api_error_url = Some(url.into());
239        self
240    }
241
242    /// Set the base path where auth routes are mounted.
243    #[must_use]
244    pub fn base_path(mut self, path: impl Into<String>) -> Self {
245        self.base_path = path.into();
246        self
247    }
248
249    /// Add a trusted origin. Supports glob patterns (e.g. `"https://*.example.com"`).
250    #[must_use]
251    pub fn trusted_origin(mut self, origin: impl Into<String>) -> Self {
252        self.trusted_origins.push(origin.into());
253        self
254    }
255
256    /// Set all trusted origins at once.
257    #[must_use]
258    pub fn trusted_origins(mut self, origins: Vec<String>) -> Self {
259        self.trusted_origins = origins;
260        self
261    }
262
263    /// Add a path to the disabled paths list.
264    #[must_use]
265    pub fn disabled_path(mut self, path: impl Into<String>) -> Self {
266        self.disabled_paths.push(path.into());
267        self
268    }
269
270    /// Set all disabled paths at once.
271    #[must_use]
272    pub fn disabled_paths(mut self, paths: Vec<String>) -> Self {
273        self.disabled_paths = paths;
274        self
275    }
276
277    /// Propagate ordinary endpoint exceptions through the public request result.
278    #[must_use]
279    pub const fn throw_api_errors(mut self, enabled: bool) -> Self {
280        self.throw_api_errors = enabled;
281        self
282    }
283
284    /// Set the session expiration duration.
285    #[must_use]
286    pub const fn session_expires_in(mut self, duration: Duration) -> Self {
287        self.session.expires_in = duration;
288        self
289    }
290
291    #[must_use]
292    pub const fn session_update_age(mut self, duration: Duration) -> Self {
293        self.session.update_age = Some(duration);
294        self
295    }
296
297    #[must_use]
298    pub const fn disable_session_refresh(mut self, disabled: bool) -> Self {
299        self.session.disable_session_refresh = disabled;
300        self
301    }
302
303    #[must_use]
304    pub const fn session_fresh_age(mut self, duration: Duration) -> Self {
305        self.session.fresh_age = Some(duration);
306        self
307    }
308
309    /// Set the cookie cache configuration for sessions.
310    #[must_use]
311    pub fn session_cookie_cache(mut self, config: CookieCacheConfig) -> Self {
312        self.session.cookie_cache = Some(config);
313        self
314    }
315
316    /// Set the JWT expiration duration.
317    #[must_use]
318    pub const fn jwt_expires_in(mut self, duration: Duration) -> Self {
319        self.jwt.expires_in = duration;
320        self
321    }
322
323    #[must_use]
324    pub fn advanced(mut self, advanced: AdvancedConfig) -> Self {
325        self.advanced = advanced;
326        self
327    }
328
329    #[must_use]
330    pub fn cookie_prefix(mut self, prefix: impl Into<String>) -> Self {
331        self.advanced.cookie_prefix = Some(prefix.into());
332        self
333    }
334
335    #[must_use]
336    pub const fn disable_csrf_check(mut self, disabled: bool) -> Self {
337        self.advanced.disable_csrf_check = Some(disabled);
338        self
339    }
340
341    /// Current key for signing tokens and cookies. Retained keys are encryption
342    /// readers; signed cookies and signed JWTs deliberately use only this key.
343    #[must_use]
344    pub fn current_secret(&self) -> &str {
345        self.managed_secrets
346            .as_ref()
347            .map_or(self.secret.as_str(), ManagedSecrets::current_secret)
348    }
349
350    /// Encryption readers in configured order, followed by a distinct legacy key.
351    pub fn verification_secrets(&self) -> impl Iterator<Item = &str> {
352        let keys = self
353            .managed_secrets
354            .as_ref()
355            .map(secrets::ManagedSecrets::verification_secrets);
356        keys.into_iter().flatten().chain(
357            self.managed_secrets
358                .is_none()
359                .then_some(self.secret.as_str()),
360        )
361    }
362
363    /// Enable managed encryption with an explicit current version and readers.
364    #[must_use]
365    pub fn managed_secrets(mut self, secrets: ManagedSecrets) -> Self {
366        self.managed_secrets = Some(secrets);
367        self
368    }
369
370    /// Determine whether this request path opts out of origin validation.
371    #[must_use]
372    pub fn origin_check_disabled_for(&self, path: &str) -> bool {
373        if self.advanced.disable_origin_check {
374            return true;
375        }
376        let path = path
377            .strip_prefix(self.base_path.trim_end_matches('/'))
378            .filter(|suffix| suffix.starts_with('/'))
379            .unwrap_or(path)
380            .trim_end_matches('/');
381        self.advanced.disable_origin_check_paths.iter().any(|skip| {
382            let skip = skip.trim_end_matches('/');
383            path == skip
384                || path
385                    .strip_prefix(skip)
386                    .is_some_and(|suffix| suffix.starts_with('/'))
387        })
388    }
389
390    /// Apply the configured origin policy at an initialized request callback.
391    #[must_use]
392    pub fn current_origin_check_disabled(&self) -> bool {
393        crate::hooks::current_request_hook_context()
394            .map_or(self.advanced.disable_origin_check, |request| {
395                self.origin_check_disabled_for(&request.path)
396            })
397    }
398
399    #[must_use]
400    pub const fn disable_origin_check(mut self, disabled: bool) -> Self {
401        self.advanced.disable_origin_check = disabled;
402        self
403    }
404
405    #[must_use]
406    pub fn cross_sub_domain_cookies(mut self, domain: impl Into<String>) -> Self {
407        self.advanced.cross_sub_domain_cookies = Some(CrossSubDomainConfig {
408            domain: domain.into(),
409        });
410        self
411    }
412
413    /// Share cookies using the configured or request-resolved base URL's hostname.
414    /// This follows the published hostname directly; it does not infer a parent
415    /// or registrable domain. Use [`cross_sub_domain_cookies`](Self::cross_sub_domain_cookies)
416    /// when a deployment needs an explicit parent domain.
417    #[must_use]
418    pub fn cross_sub_domain_cookies_from_base_url(mut self) -> Self {
419        self.advanced.cross_sub_domain_cookies = Some(CrossSubDomainConfig::default());
420        self
421    }
422
423    /// Check the resolved base origin or a configured trusted pattern.
424    ///
425    /// Explicit patterns follow Better Auth 1.7.6: `*` and `?` match URL origins
426    /// (or hosts for patterns without a scheme), while custom schemes retain
427    /// authority and normalized path-prefix constraints. Resolve dynamic policies
428    /// with [`resolve_request`](Self::resolve_request) before evaluating them.
429    #[must_use]
430    pub fn is_origin_trusted(&self, origin: &str) -> bool {
431        if self.dynamic_base_url.is_none()
432            && let Some(base_origin) = extract_origin(&self.base_url)
433            && (origin == base_origin
434                || (self.base_url.split_once(':').is_some_and(|(scheme, _)| {
435                    scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
436                }) && extract_origin(origin).as_deref() == Some(base_origin.as_str())))
437        {
438            return true;
439        }
440        self.trusted_origins
441            .iter()
442            .any(|pattern| origin::matches_origin(origin, pattern))
443    }
444
445    /// Check whether a URL is a safe redirect target.
446    ///
447    /// A URL is safe if it is a relative path (starts with `/`, no
448    /// traversal tricks) or its origin matches [`base_url`](Self::base_url)
449    /// or [`trusted_origins`](Self::trusted_origins).
450    ///
451    /// This is used by both the CSRF middleware (for POST body/query
452    /// targets) and per-endpoint origin checks (e.g. verify-email GET).
453    #[must_use]
454    pub fn is_redirect_target_trusted(&self, url: &str) -> bool {
455        is_safe_relative_path(url) || self.is_origin_trusted(url)
456    }
457
458    /// Check whether a given path is disabled.
459    #[must_use]
460    pub fn is_path_disabled(&self, path: &str) -> bool {
461        self.disabled_paths.iter().any(|disabled| disabled == path)
462    }
463
464    /// # Errors
465    ///
466    /// Returns a configuration error if the signing secret is empty or shorter than 32 bytes.
467    pub fn validate(&self) -> Result<(), AuthError> {
468        if self.session.stateless
469            && (self.session.secondary_storage.is_some()
470                || self.session.store_in_database
471                || self.session.preserve_in_database)
472        {
473            return Err(AuthError::config(
474                "Stateless sessions cannot use server session storage",
475            ));
476        }
477        if self
478            .dynamic_base_url
479            .as_ref()
480            .is_some_and(|policy| policy.allowed_hosts.is_empty())
481        {
482            return Err(AuthError::config(
483                "Dynamic base URL allowed hosts cannot be empty",
484            ));
485        }
486        if let Some(secrets) = &self.managed_secrets {
487            return secrets.validate();
488        }
489        if self.secret.is_empty() {
490            return Err(AuthError::config("Secret key cannot be empty"));
491        }
492
493        if self.secret.len() < 32 {
494            return Err(AuthError::config(
495                "Secret key must be at least 32 characters",
496            ));
497        }
498
499        Ok(())
500    }
501}
502
503impl std::fmt::Debug for AuthConfig {
504    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
505        f.debug_struct("AuthConfig").finish_non_exhaustive()
506    }
507}
508
509/// Check whether a URL is a safe relative path.
510///
511/// A relative path is safe if it starts with a single `/` and has no
512/// traversal or scheme-escape tricks (`//`, `\`, `%2f`, `%5c`).
513///
514/// This is used by [`AuthConfig::is_redirect_target_trusted`] and the
515/// CSRF middleware.
516#[must_use]
517pub fn is_safe_relative_path(value: &str) -> bool {
518    if !value.starts_with('/')
519        || value.starts_with("//")
520        || value.contains('\\')
521        || value
522            .chars()
523            .any(|character| matches!(character, '\u{0000}'..='\u{001f}' | '\u{007f}'..='\u{009f}'))
524    {
525        return false;
526    }
527
528    let path = value.split(['?', '#']).next().unwrap_or(value);
529    let lower = path.to_ascii_lowercase();
530    !lower.contains("%2f")
531        && !lower.contains("%5c")
532        && url::Url::parse("https://better-auth.invalid")
533            .and_then(|base| base.join(value))
534            .is_ok_and(|url| url.origin().ascii_serialization() == "https://better-auth.invalid")
535}
536
537/// Extract the origin (scheme + host + port) from a URL string.
538///
539/// For example, `"https://example.com/path"` → `"https://example.com"`.
540///
541/// This is used by [`AuthConfig::is_origin_trusted`] and the CSRF middleware
542/// so that origin comparison is centralised in one place.
543#[must_use]
544pub fn extract_origin(url: &str) -> Option<String> {
545    if let Ok(parsed) = url::Url::parse(url)
546        && matches!(parsed.scheme(), "http" | "https")
547    {
548        return match parsed.origin() {
549            url::Origin::Tuple(..) => Some(parsed.origin().ascii_serialization()),
550            url::Origin::Opaque(_) => None,
551        };
552    }
553    // Custom schemes retain the existing Rust contract. Their upstream
554    // authority/path matcher is a separate capability.
555    legacy_pattern_origin(url)
556}
557
558// Configured glob/custom-scheme matching keeps its existing contract; avoid
559// normalizing an explicit pattern into a newly trusted authority.
560fn legacy_pattern_origin(url: &str) -> Option<String> {
561    let scheme_end = url.find("://")?;
562    let rest = url.get(scheme_end + 3..)?;
563    let host_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
564    let origin = format!("{}{}", url.get(..scheme_end + 3)?, rest.get(..host_end)?);
565    Some(origin)
566}
567
568// LCOV_EXCL_START
569#[cfg(test)]
570mod tests {
571    use super::*;
572
573    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
574    #[test]
575    fn extract_origin_with_path() {
576        assert_eq!(
577            extract_origin("https://example.com/path"),
578            Some("https://example.com".to_owned())
579        );
580    }
581
582    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
583    #[test]
584    fn extract_origin_without_path() {
585        assert_eq!(
586            extract_origin("https://example.com"),
587            Some("https://example.com".to_owned())
588        );
589    }
590
591    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
592    #[test]
593    fn extract_origin_with_port() {
594        assert_eq!(
595            extract_origin("http://localhost:3000/api"),
596            Some("http://localhost:3000".to_owned())
597        );
598    }
599
600    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
601    #[test]
602    fn extract_origin_with_query() {
603        assert_eq!(
604            extract_origin("https://example.com?foo=bar"),
605            Some("https://example.com".to_owned())
606        );
607    }
608
609    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
610    #[test]
611    fn extract_origin_with_fragment() {
612        assert_eq!(
613            extract_origin("https://example.com#fragment"),
614            Some("https://example.com".to_owned())
615        );
616    }
617
618    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
619    #[test]
620    fn extract_origin_no_scheme() {
621        assert_eq!(extract_origin("example.com"), None);
622    }
623
624    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
625    #[test]
626    fn new_config_sets_secret() {
627        let cfg = AuthConfig::new("a]secret-that-is-at-least-32-characters-long");
628        assert_eq!(cfg.secret, "a]secret-that-is-at-least-32-characters-long");
629    }
630
631    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
632    #[test]
633    fn new_config_uses_defaults() {
634        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567");
635        assert_eq!(cfg.app_name, "Better Auth");
636        assert_eq!(cfg.base_url, "http://localhost:3000");
637        assert_eq!(cfg.base_path, "/api/auth");
638        assert_eq!(cfg.trusted_origins, Vec::<String>::new());
639    }
640
641    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
642    #[test]
643    fn base_url_sets_cookie_secure_for_https() {
644        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
645        assert!(cfg.session.cookie_secure);
646    }
647
648    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
649    #[test]
650    fn base_url_clears_cookie_secure_for_http() {
651        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
652            .base_url("https://myapp.com")
653            .base_url("http://localhost:3000");
654        assert!(!cfg.session.cookie_secure);
655    }
656
657    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
658    #[test]
659    fn builder_chaining() {
660        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
661            .app_name("MyApp")
662            .base_path("/auth")
663            .disable_csrf_check(true)
664            .disable_origin_check(true)
665            .cookie_prefix("myapp");
666
667        assert_eq!(cfg.app_name, "MyApp");
668        assert_eq!(cfg.base_path, "/auth");
669        assert_eq!(cfg.advanced.disable_csrf_check, Some(true));
670        assert!(cfg.advanced.disable_origin_check);
671        assert_eq!(cfg.advanced.cookie_prefix, Some("myapp".to_owned()));
672    }
673
674    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
675    #[test]
676    fn trusted_origin_appends() {
677        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
678            .trusted_origin("https://a.com")
679            .trusted_origin("https://b.com");
680        assert_eq!(cfg.trusted_origins.len(), 2);
681    }
682
683    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
684    #[test]
685    fn trusted_origins_replaces() {
686        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
687            .trusted_origin("https://old.com")
688            .trusted_origins(vec!["https://new.com".to_owned()]);
689        assert_eq!(cfg.trusted_origins, vec!["https://new.com"]);
690    }
691
692    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
693    #[test]
694    fn disabled_path_appends() {
695        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
696            .disabled_path("/admin")
697            .disabled_path("/debug");
698        assert_eq!(cfg.disabled_paths.len(), 2);
699    }
700
701    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
702    #[test]
703    fn disabled_paths_replaces() {
704        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
705            .disabled_path("/old")
706            .disabled_paths(vec!["/new".to_owned()]);
707        assert_eq!(cfg.disabled_paths, vec!["/new"]);
708    }
709
710    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
711    #[test]
712    fn is_origin_trusted_matches_base_url() {
713        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
714        assert!(cfg.is_origin_trusted("https://myapp.com"));
715        let native_custom =
716            AuthConfig::new("test-secret-min-32-chars-1234567").base_url("myapp://auth");
717        assert!(native_custom.is_origin_trusted("myapp://auth"));
718        assert!(!native_custom.is_origin_trusted("myapp://auth/callback"));
719    }
720
721    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
722    #[test]
723    fn is_origin_trusted_rejects_unknown() {
724        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
725        assert!(!cfg.is_origin_trusted("https://evil.com"));
726    }
727
728    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
729    #[test]
730    fn is_origin_trusted_glob_pattern() {
731        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
732            .trusted_origin("https://*.example.com");
733        assert!(cfg.is_origin_trusted("https://sub.example.com"));
734        assert!(!cfg.is_origin_trusted("https://other.com"));
735    }
736
737    // Upstream reference: packages/better-auth/src/api/middlewares/origin-check.ts :: originCheck validates callbackURL against trustedOrigins.
738    #[test]
739    fn redirect_target_allows_relative_path() {
740        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
741        assert!(cfg.is_redirect_target_trusted("/dashboard"));
742        assert!(cfg.is_redirect_target_trusted("/callback?foo=bar"));
743    }
744
745    // Upstream reference: packages/better-auth/src/api/middlewares/origin-check.ts :: originCheck validates callbackURL against trustedOrigins.
746    #[test]
747    fn redirect_target_allows_same_origin() {
748        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
749        assert!(cfg.is_redirect_target_trusted("https://myapp.com/verified"));
750    }
751
752    // Upstream reference: packages/better-auth/src/api/middlewares/origin-check.ts :: originCheck validates callbackURL against trustedOrigins.
753    #[test]
754    fn redirect_target_allows_trusted_origin() {
755        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
756            .base_url("https://myapp.com")
757            .trusted_origin("https://trusted.com");
758        assert!(cfg.is_redirect_target_trusted("https://trusted.com/path"));
759    }
760
761    // Upstream reference: packages/better-auth/src/api/middlewares/origin-check.ts :: originCheck validates callbackURL against trustedOrigins.
762    #[test]
763    fn redirect_target_rejects_untrusted_origin() {
764        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
765        assert!(!cfg.is_redirect_target_trusted("https://evil.com/phish"));
766    }
767
768    // Upstream reference: packages/better-auth/src/api/middlewares/origin-check.ts :: originCheck validates callbackURL against trustedOrigins.
769    #[test]
770    fn redirect_target_rejects_protocol_relative() {
771        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").base_url("https://myapp.com");
772        assert!(!cfg.is_redirect_target_trusted("//evil.com"));
773    }
774
775    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
776    #[test]
777    fn is_path_disabled_matches() {
778        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").disabled_path("/admin");
779        assert!(cfg.is_path_disabled("/admin"));
780        assert!(!cfg.is_path_disabled("/user"));
781    }
782
783    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
784    #[test]
785    fn validate_rejects_empty_secret() {
786        let cfg = AuthConfig::default();
787        assert!(cfg.validate().is_err());
788    }
789
790    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
791    #[test]
792    fn validate_rejects_short_secret() {
793        let cfg = AuthConfig::new("short");
794        assert!(cfg.validate().is_err());
795    }
796
797    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
798    #[test]
799    fn validate_accepts_valid_secret() {
800        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567");
801        assert!(cfg.validate().is_ok());
802    }
803
804    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
805    #[test]
806    fn session_config_defaults() {
807        let s = SessionConfig::default();
808        assert_eq!(s.expires_in, Duration::hours(24 * 7));
809        assert_eq!(s.update_age, Some(Duration::hours(24)));
810        assert!(!s.disable_session_refresh);
811        assert_eq!(s.cookie_name, "better-auth.session_token");
812        assert!(s.cookie_http_only);
813        assert_eq!(s.cookie_same_site, SameSite::Lax);
814    }
815
816    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
817    #[test]
818    fn jwt_config_defaults() {
819        let j = JwtConfig::default();
820        assert_eq!(j.expires_in, Duration::hours(24));
821        assert_eq!(j.algorithm, "HS256");
822    }
823
824    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
825    #[test]
826    fn password_config_defaults() {
827        let p = PasswordConfig::default();
828        assert!(!p.require_uppercase);
829    }
830
831    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
832    #[test]
833    fn same_site_display() {
834        assert_eq!(SameSite::Strict.to_string(), "Strict");
835        assert_eq!(SameSite::Lax.to_string(), "Lax");
836        assert_eq!(SameSite::None.to_string(), "None");
837    }
838
839    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
840    #[test]
841    fn cookie_cache_config_defaults() {
842        let c = CookieCacheConfig::default();
843        assert!(!c.enabled);
844        assert!((c.max_age - 300.0).abs() < f64::EPSILON);
845        assert_eq!(c.strategy, CookieCacheStrategy::Compact);
846    }
847
848    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
849    #[test]
850    fn account_config_defaults() {
851        let a = AccountConfig::default();
852        assert!(a.update_account_on_sign_in);
853        assert!(!a.encrypt_oauth_tokens);
854        assert!(a.account_linking.enabled);
855    }
856
857    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
858    #[test]
859    fn core_paths_error_page() {
860        let html = crate::error::page::error_page_html("TEST_ERROR");
861        assert!(html.contains("TEST_ERROR"));
862        assert!(html.contains("Ask AI"));
863        assert!(html.contains("<title>Error</title>"));
864    }
865
866    // Upstream reference: packages/better-auth/src/api/routes/error.ts :: sanitize function and /^[A-Za-z0-9_'-]+$/ whitelist.
867    #[test]
868    fn error_page_sanitizes_script_tag() {
869        let html = crate::error::page::error_page_html("<script>alert(1)</script>");
870        assert!(html.contains("UNKNOWN"));
871        assert!(!html.contains("<script>"));
872    }
873
874    // Upstream reference: packages/better-auth/src/api/routes/error.ts :: sanitize function and /^[A-Za-z0-9_'-]+$/ whitelist.
875    #[test]
876    fn error_page_allows_valid_codes() {
877        assert!(crate::error::page::error_page_html("SOME_ERROR-CODE").contains("SOME_ERROR-CODE"));
878        assert!(crate::error::page::error_page_html("it's").contains("it's"));
879    }
880
881    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
882    #[test]
883    fn session_builder_methods() {
884        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
885            .session_expires_in(Duration::hours(1))
886            .session_update_age(Duration::minutes(30))
887            .disable_session_refresh(true)
888            .session_fresh_age(Duration::minutes(5));
889
890        assert_eq!(cfg.session.expires_in, Duration::hours(1));
891        assert_eq!(cfg.session.update_age, Some(Duration::minutes(30)));
892        assert!(cfg.session.disable_session_refresh);
893        assert_eq!(cfg.session.fresh_age, Some(Duration::minutes(5)));
894    }
895
896    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
897    #[test]
898    fn session_cookie_cache_builder() {
899        let cache = CookieCacheConfig {
900            enabled: true,
901            max_age: 600.0,
902            strategy: CookieCacheStrategy::Jwt,
903            ..CookieCacheConfig::default()
904        };
905        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567").session_cookie_cache(cache);
906
907        let cc = cfg.session.cookie_cache.as_ref();
908        assert!(cc.is_some());
909        let cc = cc.unwrap();
910        assert!(cc.enabled);
911        assert_eq!(cc.strategy, CookieCacheStrategy::Jwt);
912    }
913
914    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
915    #[test]
916    fn cross_sub_domain_cookies_builder() {
917        let cfg = AuthConfig::new("test-secret-min-32-chars-1234567")
918            .cross_sub_domain_cookies(".example.com");
919        let csd = cfg.advanced.cross_sub_domain_cookies.as_ref();
920        assert!(csd.is_some());
921        assert_eq!(csd.unwrap().domain, ".example.com");
922    }
923
924    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
925    #[test]
926    fn advanced_database_defaults() {
927        let d = AdvancedDatabaseConfig::default();
928        assert_eq!(d.default_find_many_limit, 100);
929        assert!(!d.use_number_id);
930    }
931
932    // Rust-specific surface: `AuthConfig`, related configuration builders, and `core_paths` are public Rust APIs with no direct TS analogue.
933    #[test]
934    fn ip_address_config_defaults() {
935        let ip = IpAddressConfig::default();
936        assert_eq!(ip.headers, vec!["x-forwarded-for"]);
937        assert!(!ip.disable_ip_tracking);
938    }
939}
940// LCOV_EXCL_STOP