Skip to main content

rustauth_core/options/
root.rs

1use std::fmt;
2use std::sync::Arc;
3
4#[cfg(feature = "oauth")]
5use rustauth_oauth::oauth2::SocialOAuthProvider;
6
7use super::storage::SecondaryStorage;
8use crate::crypto::SecretEntry;
9use crate::plugin::AuthPlugin;
10
11use super::account::AccountOptions;
12use super::advanced::AdvancedOptions;
13use super::api_error::OnApiErrorOptions;
14use super::email_password::EmailPasswordOptions;
15use super::email_verification::EmailVerificationOptions;
16use super::hooks::GlobalHooksOptions;
17use super::init_database_hooks::InitDatabaseHooksOptions;
18use super::origins::TrustedOriginOptions;
19use super::password::PasswordOptions;
20use super::rate_limit::RateLimitOptions;
21use super::session::SessionOptions;
22use super::user::UserOptions;
23use super::verification::VerificationOptions;
24use crate::env::{is_production, logger::LoggerOptions};
25use crate::plugin::PluginDatabaseHook;
26
27/// Runtime deployment posture for security-sensitive defaults.
28///
29/// # Precedence
30///
31/// | `mode` | `RUST_ENV` | Development defaults allowed |
32/// |--------|------------|------------------------------|
33/// | [`Production`](Self::Production) | any | no |
34/// | [`Development`](Self::Development) | `production` | no |
35/// | [`Development`](Self::Development) | otherwise | yes |
36/// | [`Auto`](Self::Auto) (default) | `production` | no |
37/// | [`Auto`](Self::Auto) | `development`, `test`, or test runtime | yes |
38/// | [`Auto`](Self::Auto) | unset (non-test) | no (fail closed) |
39///
40/// [`RustAuthOptions::production`] and [`RustAuthOptions::development`] map to
41/// [`Production`] and [`Development`] respectively. Setting either convenience
42/// method to `false` restores [`Auto`]. When both are chained, the last call wins.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum DeploymentMode {
45    /// Honor `RUST_ENV` and test-runtime detection together (see table above).
46    #[default]
47    Auto,
48    /// Force production posture regardless of environment.
49    Production,
50    /// Allow development-oriented security defaults regardless of environment.
51    Development,
52}
53
54/// Telemetry collection settings (parity with Better Auth `telemetry` init option).
55#[derive(Debug, Clone, PartialEq, Eq, Default)]
56pub struct TelemetryOptions {
57    /// When `None`, option-side telemetry is off unless overridden by environment.
58    pub enabled: Option<bool>,
59    pub debug: bool,
60}
61
62impl TelemetryOptions {
63    pub fn new() -> Self {
64        Self::default()
65    }
66
67    #[must_use]
68    pub fn enabled(mut self, enabled: bool) -> Self {
69        self.enabled = Some(enabled);
70        self
71    }
72
73    #[must_use]
74    pub fn debug(mut self, debug: bool) -> Self {
75        self.debug = debug;
76        self
77    }
78}
79
80/// Experimental feature flags.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct ExperimentalOptions {
83    pub joins: bool,
84}
85
86impl Default for ExperimentalOptions {
87    fn default() -> Self {
88        Self { joins: true }
89    }
90}
91
92impl ExperimentalOptions {
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    #[must_use]
98    pub fn joins(mut self, enabled: bool) -> Self {
99        self.joins = enabled;
100        self
101    }
102}
103
104/// Top-level RustAuth configuration.
105///
106/// Database hooks can be registered in two ways:
107///
108/// - [`Self::init_database_hooks`] — structured, init-time hooks for core models
109///   (`user`, `session`, `account`, `verification`) via [`InitDatabaseHooksOptions`].
110///   Prefer this for parity with Better Auth `databaseHooks` and typed create/update
111///   callbacks on built-in models.
112/// - [`Self::database_hook`] — append a low-level [`PluginDatabaseHook`] directly.
113///   Use this for custom models, plugin-owned tables, or hooks that do not fit the
114///   init-time schema.
115///
116/// Both paths are merged at runtime; they are not mutually exclusive.
117///
118/// # Deployment mode
119///
120/// Use [`Self::deployment_mode`] or the convenience setters [`Self::production`] /
121/// [`Self::development`] to control whether development-oriented security defaults
122/// are allowed. See [`DeploymentMode`] for the full precedence matrix.
123#[derive(Clone, Default)]
124pub struct RustAuthOptions {
125    pub app_name: Option<String>,
126    pub base_url: Option<String>,
127    pub base_path: Option<String>,
128    pub secret: Option<String>,
129    pub secrets: Vec<SecretEntry>,
130    pub trusted_origins: TrustedOriginOptions,
131    pub disabled_paths: Vec<String>,
132    pub session: SessionOptions,
133    pub user: UserOptions,
134    pub email_password: EmailPasswordOptions,
135    pub email_verification: EmailVerificationOptions,
136    pub password: PasswordOptions,
137    pub account: AccountOptions,
138    pub verification: VerificationOptions,
139    pub hooks: GlobalHooksOptions,
140    pub on_api_error: OnApiErrorOptions,
141    pub init_database_hooks: InitDatabaseHooksOptions,
142    pub database_hooks: Vec<PluginDatabaseHook>,
143    pub logger: LoggerOptions,
144    pub advanced: AdvancedOptions,
145    pub rate_limit: RateLimitOptions,
146    pub secondary_storage: Option<Arc<dyn SecondaryStorage>>,
147    pub plugins: Vec<AuthPlugin>,
148    #[cfg(feature = "oauth")]
149    pub social_providers: Vec<Arc<dyn SocialOAuthProvider>>,
150    pub mode: DeploymentMode,
151    pub telemetry: TelemetryOptions,
152    pub experimental: ExperimentalOptions,
153}
154
155impl RustAuthOptions {
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    #[must_use]
161    pub fn app_name(mut self, app_name: impl Into<String>) -> Self {
162        self.app_name = Some(app_name.into());
163        self
164    }
165
166    #[must_use]
167    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
168        self.base_url = Some(base_url.into());
169        self
170    }
171
172    #[must_use]
173    pub fn base_path(mut self, base_path: impl Into<String>) -> Self {
174        self.base_path = Some(base_path.into());
175        self
176    }
177
178    #[must_use]
179    pub fn secret(mut self, secret: impl Into<String>) -> Self {
180        self.secret = Some(secret.into());
181        self
182    }
183
184    #[must_use]
185    pub fn secrets(mut self, secrets: Vec<SecretEntry>) -> Self {
186        self.secrets = secrets;
187        self
188    }
189
190    #[must_use]
191    pub fn trusted_origins(mut self, trusted_origins: TrustedOriginOptions) -> Self {
192        self.trusted_origins = trusted_origins;
193        self
194    }
195
196    #[must_use]
197    /// Append one disabled route path.
198    pub fn disabled_path(mut self, path: impl Into<String>) -> Self {
199        self.disabled_paths.push(path.into());
200        self
201    }
202
203    #[must_use]
204    /// Append one disabled route path (alias for [`Self::disabled_path`]).
205    pub fn push_disabled_path(self, path: impl Into<String>) -> Self {
206        self.disabled_path(path)
207    }
208
209    #[must_use]
210    /// Replace the full disabled-path list.
211    pub fn disabled_paths<I, S>(mut self, paths: I) -> Self
212    where
213        I: IntoIterator<Item = S>,
214        S: Into<String>,
215    {
216        self.disabled_paths = paths.into_iter().map(Into::into).collect();
217        self
218    }
219
220    #[must_use]
221    /// Replace the full disabled-path list (alias for [`Self::disabled_paths`]).
222    pub fn set_disabled_paths<I, S>(self, paths: I) -> Self
223    where
224        I: IntoIterator<Item = S>,
225        S: Into<String>,
226    {
227        self.disabled_paths(paths)
228    }
229
230    #[must_use]
231    pub fn session(mut self, session: SessionOptions) -> Self {
232        self.session = session;
233        self
234    }
235
236    #[must_use]
237    pub fn user(mut self, user: UserOptions) -> Self {
238        self.user = user;
239        self
240    }
241
242    #[must_use]
243    pub fn email_password(mut self, email_password: EmailPasswordOptions) -> Self {
244        self.email_password = email_password;
245        self
246    }
247
248    #[must_use]
249    pub fn email_verification(mut self, email_verification: EmailVerificationOptions) -> Self {
250        self.email_verification = email_verification;
251        self
252    }
253
254    #[must_use]
255    pub fn password(mut self, password: PasswordOptions) -> Self {
256        self.password = password;
257        self
258    }
259
260    #[must_use]
261    pub fn account(mut self, account: AccountOptions) -> Self {
262        self.account = account;
263        self
264    }
265
266    #[must_use]
267    pub fn verification(mut self, verification: VerificationOptions) -> Self {
268        self.verification = verification;
269        self
270    }
271
272    #[must_use]
273    pub fn advanced(mut self, advanced: AdvancedOptions) -> Self {
274        self.advanced = advanced;
275        self
276    }
277
278    #[must_use]
279    pub fn rate_limit(mut self, rate_limit: RateLimitOptions) -> Self {
280        self.rate_limit = rate_limit;
281        self
282    }
283
284    #[must_use]
285    pub fn hooks(mut self, hooks: GlobalHooksOptions) -> Self {
286        self.hooks = hooks;
287        self
288    }
289
290    #[must_use]
291    pub fn on_api_error(mut self, on_api_error: OnApiErrorOptions) -> Self {
292        self.on_api_error = on_api_error;
293        self
294    }
295
296    /// Structured init-time hooks for core models. See [`RustAuthOptions`] for when
297    /// to prefer this over [`Self::database_hook`].
298    #[must_use]
299    pub fn init_database_hooks(mut self, hooks: InitDatabaseHooksOptions) -> Self {
300        self.init_database_hooks = hooks;
301        self
302    }
303
304    /// Append a low-level database hook (custom models or plugin tables). See
305    /// [`RustAuthOptions`] for when to prefer [`Self::init_database_hooks`].
306    #[must_use]
307    pub fn database_hook(mut self, hook: PluginDatabaseHook) -> Self {
308        self.database_hooks.push(hook);
309        self
310    }
311
312    #[must_use]
313    pub fn logger(mut self, logger: LoggerOptions) -> Self {
314        self.logger = logger;
315        self
316    }
317
318    #[must_use]
319    /// Attach secondary storage. The value is already wrapped in [`Arc`].
320    pub fn secondary_storage(mut self, storage: Arc<dyn SecondaryStorage>) -> Self {
321        self.secondary_storage = Some(storage);
322        self
323    }
324
325    #[must_use]
326    /// Attach secondary storage (alias for [`Self::secondary_storage`]).
327    pub fn secondary_storage_arc(self, storage: Arc<dyn SecondaryStorage>) -> Self {
328        self.secondary_storage(storage)
329    }
330
331    #[must_use]
332    /// Append one plugin to the options list.
333    pub fn plugin(mut self, plugin: AuthPlugin) -> Self {
334        self.plugins.push(plugin);
335        self
336    }
337
338    #[must_use]
339    /// Append one plugin (alias for [`Self::plugin`]).
340    pub fn push_plugin(self, plugin: AuthPlugin) -> Self {
341        self.plugin(plugin)
342    }
343
344    #[must_use]
345    /// Append multiple plugins to the options list.
346    ///
347    /// Like chaining [`.plugin`](Self::plugin) repeatedly. To replace the full
348    /// list, use [`.set_plugins`](Self::set_plugins).
349    pub fn plugins(mut self, plugins: Vec<AuthPlugin>) -> Self {
350        self.plugins.extend(plugins);
351        self
352    }
353
354    #[must_use]
355    /// Append multiple plugins (alias for [`Self::plugins`]).
356    pub fn extend_plugins(self, plugins: Vec<AuthPlugin>) -> Self {
357        self.plugins(plugins)
358    }
359
360    #[must_use]
361    /// Replace the full plugin list.
362    pub fn set_plugins(mut self, plugins: Vec<AuthPlugin>) -> Self {
363        self.plugins = plugins;
364        self
365    }
366
367    #[cfg(feature = "oauth")]
368    #[must_use]
369    pub fn social_provider<P>(mut self, provider: P) -> Self
370    where
371        P: SocialOAuthProvider,
372    {
373        self.social_providers.push(Arc::new(provider));
374        self
375    }
376
377    #[cfg(feature = "oauth")]
378    #[must_use]
379    pub fn social_provider_arc(mut self, provider: Arc<dyn SocialOAuthProvider>) -> Self {
380        self.social_providers.push(provider);
381        self
382    }
383
384    #[cfg(feature = "oauth")]
385    #[must_use]
386    /// Append multiple social OAuth providers.
387    pub fn social_providers<I, P>(mut self, providers: I) -> Self
388    where
389        I: IntoIterator<Item = P>,
390        P: SocialOAuthProvider + 'static,
391    {
392        self.social_providers.extend(
393            providers
394                .into_iter()
395                .map(|provider| Arc::new(provider) as Arc<dyn SocialOAuthProvider>),
396        );
397        self
398    }
399
400    #[cfg(feature = "oauth")]
401    /// Append social OAuth providers built from fallible constructors.
402    pub fn try_social_providers<I, P, E>(mut self, iter: I) -> Result<Self, E>
403    where
404        I: IntoIterator<Item = Result<P, E>>,
405        P: SocialOAuthProvider + 'static,
406        E: std::error::Error,
407    {
408        for provider in iter {
409            self.social_providers
410                .push(Arc::new(provider?) as Arc<dyn SocialOAuthProvider>);
411        }
412        Ok(self)
413    }
414
415    #[must_use]
416    /// Set deployment posture explicitly.
417    pub fn deployment_mode(mut self, mode: DeploymentMode) -> Self {
418        self.mode = mode;
419        self
420    }
421
422    #[must_use]
423    /// Enable or disable explicit production posture.
424    pub fn production(mut self, production: bool) -> Self {
425        self.mode = if production {
426            DeploymentMode::Production
427        } else if self.mode == DeploymentMode::Production {
428            DeploymentMode::Auto
429        } else {
430            self.mode
431        };
432        self
433    }
434
435    #[must_use]
436    /// Enable or disable explicit development posture.
437    pub fn development(mut self, development: bool) -> Self {
438        self.mode = if development {
439            DeploymentMode::Development
440        } else if self.mode == DeploymentMode::Development {
441            DeploymentMode::Auto
442        } else {
443            self.mode
444        };
445        self
446    }
447
448    /// Returns true when options request explicit production API error sanitization.
449    pub fn explicit_production(&self) -> bool {
450        matches!(self.mode, DeploymentMode::Production)
451    }
452
453    /// Returns true when options request explicit production API error sanitization,
454    /// or when [`DeploymentMode::Auto`] and `RUST_ENV=production`.
455    pub fn production_error_posture(&self) -> bool {
456        match self.mode {
457            DeploymentMode::Production => true,
458            DeploymentMode::Development => false,
459            DeploymentMode::Auto => is_production(),
460        }
461    }
462
463    #[must_use]
464    pub fn telemetry(mut self, telemetry: TelemetryOptions) -> Self {
465        self.telemetry = telemetry;
466        self
467    }
468
469    #[must_use]
470    pub fn experimental(mut self, experimental: ExperimentalOptions) -> Self {
471        self.experimental = experimental;
472        self
473    }
474}
475
476impl fmt::Debug for RustAuthOptions {
477    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
478        formatter
479            .debug_struct("RustAuthOptions")
480            .field("app_name", &self.app_name)
481            .field("base_url", &self.base_url)
482            .field("base_path", &self.base_path)
483            .field("secret", &self.secret.as_ref().map(|_| "<redacted>"))
484            .field(
485                "secrets",
486                &format_args!("{} secret(s) redacted", self.secrets.len()),
487            )
488            .field("trusted_origins", &self.trusted_origins)
489            .field("disabled_paths", &self.disabled_paths)
490            .field("session", &self.session)
491            .field("user", &self.user)
492            .field("email_password", &self.email_password)
493            .field("email_verification", &self.email_verification)
494            .field("password", &self.password)
495            .field("account", &self.account)
496            .field("verification", &self.verification)
497            .field("hooks", &self.hooks)
498            .field("on_api_error", &self.on_api_error)
499            .field("init_database_hooks", &self.init_database_hooks)
500            .field("database_hooks", &self.database_hooks)
501            .field("logger", &"<logger-options>")
502            .field("advanced", &self.advanced)
503            .field("rate_limit", &self.rate_limit)
504            .field(
505                "secondary_storage",
506                &self
507                    .secondary_storage
508                    .as_ref()
509                    .map(|_| "<secondary-storage>"),
510            )
511            .field("plugins", &self.plugins)
512            .field("social_providers", &debug_social_providers(self))
513            .field("mode", &self.mode)
514            .field("telemetry", &self.telemetry)
515            .field("experimental", &self.experimental)
516            .finish()
517    }
518}
519
520#[cfg(feature = "oauth")]
521fn debug_social_providers(options: &RustAuthOptions) -> Vec<&str> {
522    options
523        .social_providers
524        .iter()
525        .map(|provider| provider.id())
526        .collect()
527}
528
529#[cfg(not(feature = "oauth"))]
530fn debug_social_providers(_options: &RustAuthOptions) -> Vec<&'static str> {
531    Vec::new()
532}