supabase_auth/
models.rs

1#![cfg(not(doctest))]
2
3use core::fmt;
4use reqwest::{Client, Url};
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::{collections::HashMap, fmt::Display};
8use uuid::Uuid;
9
10/// Supabase Auth Client
11#[derive(Clone)]
12pub struct AuthClient {
13    pub(crate) client: Client,
14    /// REST endpoint for querying and managing your database
15    /// Example: `https://YOUR_PROJECT_ID.supabase.co`
16    pub(crate) project_url: String,
17    /// WARN: The `service role` key has the ability to bypass Row Level Security. Never share it publicly.
18    pub(crate) api_key: String,
19    /// Used to decode your JWTs. You can also use this to mint your own JWTs.
20    pub(crate) jwt_secret: String,
21}
22
23#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
24pub struct Session {
25    /// The oauth provider token. If present, this can be used to make external API requests to the oauth provider used.
26    pub provider_token: Option<String>,
27    /// The oauth provider refresh token. If present, this can be used to refresh the provider_token via the oauth provider's API.
28    ///
29    /// Not all oauth providers return a provider refresh token. If the provider_refresh_token is missing, please refer to the oauth provider's documentation for information on how to obtain the provider refresh token.
30    pub provider_refresh_token: Option<String>,
31    /// The access token jwt. It is recommended to set the JWT_EXPIRY to a shorter expiry value.
32    pub access_token: String,
33    pub token_type: String,
34    /// The number of seconds until the token expires (since it was issued). Returned when a login is confirmed.
35    pub expires_in: i64,
36    /// A timestamp of when the token will expire. Returned when a login is confirmed.
37    pub expires_at: u64,
38    /// A one-time used refresh token that never expires.
39    pub refresh_token: String,
40    pub user: User,
41}
42
43/// User respresents a registered user
44#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
45pub struct User {
46    pub id: Uuid,
47    pub aud: String,
48    pub role: String,
49    pub email: String,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub invited_at: Option<String>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub confirmation_sent_at: Option<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub email_confirmed_at: Option<String>,
56    pub phone: String,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub phone_confirmed_at: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub confirmed_at: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub recovery_sent_at: Option<String>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub last_sign_in_at: Option<String>,
65    pub app_metadata: AppMetadata,
66    pub user_metadata: UserMetadata,
67    pub identities: Vec<Identity>,
68    pub created_at: String,
69    pub updated_at: String,
70    pub is_anonymous: bool,
71}
72
73#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
74pub struct AppMetadata {
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub provider: Option<String>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub providers: Option<Vec<String>>,
79}
80
81#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
82pub struct UserMetadata {
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub name: Option<String>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub full_name: Option<String>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub email: Option<String>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub email_verified: Option<bool>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub phone_verified: Option<bool>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub picture: Option<String>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub avatar_url: Option<String>,
97    #[serde(flatten)]
98    pub custom: HashMap<String, Value>,
99}
100
101#[derive(Debug)]
102pub enum EmailSignUpResult {
103    SessionResult(Session),
104    ConfirmationResult(EmailSignUpConfirmation),
105}
106
107#[derive(Clone, Debug, Deserialize, PartialEq, Default)]
108pub struct EmailSignUpConfirmation {
109    pub id: Uuid,
110    pub aud: String,
111    pub role: String,
112    pub email: Option<String>,
113    pub phone: Option<String>,
114    pub confirmation_sent_at: String,
115    pub created_at: String,
116    pub updated_at: String,
117    pub is_anonymous: bool,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121pub struct IdTokenCredentials {
122    /// Provider name or OIDC `iss` value identifying which provider should be used to verify the provided token.
123    pub provider: Provider,
124    /// OIDC ID token issued by the specified provider. The iss claim in the ID token must match the supplied provider. Some ID tokens contain an at_hash which require that you provide an access_token value to be accepted properly. If the token contains a nonce claim you must supply the nonce used to obtain the ID token.
125    #[serde(rename = "id_token")]
126    pub token: String,
127    /// If the ID token contains an at_hash claim, then the hash of this value is compared to the value in the ID token.
128    pub access_token: Option<String>,
129    /// If the ID token contains a nonce claim, then the hash of this value is compared to the value in the ID token.
130    pub nonce: Option<String>,
131    /// Optional Object which may contain a captcha token
132    pub gotrue_meta_security: Option<GotrueMetaSecurity>,
133}
134
135#[derive(Debug, Deserialize, Serialize, PartialEq, Default)]
136pub struct LoginWithOAuthOptions {
137    pub query_params: Option<HashMap<String, String>>,
138    pub redirect_to: Option<String>,
139    pub scopes: Option<String>,
140    pub skip_brower_redirect: Option<bool>,
141}
142
143#[derive(Debug, PartialEq)]
144pub struct OAuthResponse {
145    pub url: Url,
146    pub provider: Provider,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
150pub struct GotrueMetaSecurity {
151    /// Verification token received when the user completes the captcha on the site.
152    captcha_token: Option<String>,
153}
154
155#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct Identity {
157    pub identity_id: String,
158    pub id: String,
159    pub user_id: String,
160    pub identity_data: IdentityData,
161    pub provider: String,
162    pub last_sign_in_at: String,
163    pub created_at: String,
164    pub updated_at: String,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub email: Option<String>,
167}
168
169#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
170pub struct IdentityData {
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub email: Option<String>,
173    pub email_verified: bool,
174    pub phone_verified: bool,
175    pub sub: String,
176}
177
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub enum LoginOptions {
180    Email(String),
181    Phone(String),
182}
183
184#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub(crate) struct LoginWithEmailAndPasswordPayload<'a> {
186    pub(crate) email: &'a str,
187    pub(crate) password: &'a str,
188}
189
190#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
191pub(crate) struct LoginWithPhoneAndPasswordPayload<'a> {
192    pub(crate) phone: &'a str,
193    pub(crate) password: &'a str,
194}
195
196#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
197pub(crate) struct SignUpWithEmailAndPasswordPayload<'a> {
198    pub(crate) email: &'a str,
199    pub(crate) password: &'a str,
200    #[serde(flatten)]
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub(crate) options: Option<SignUpWithPasswordOptions>,
203}
204
205#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
206pub(crate) struct SignUpWithPhoneAndPasswordPayload<'a> {
207    pub(crate) phone: &'a str,
208    pub(crate) password: &'a str,
209    #[serde(flatten)]
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub(crate) options: Option<SignUpWithPasswordOptions>,
212}
213
214#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
215pub(crate) struct LoginAnonymouslyPayload {
216    #[serde(flatten)]
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub(crate) options: Option<LoginAnonymouslyOptions>,
219}
220
221#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
222pub struct SignUpWithPasswordOptions {
223    /// The redirect url embedded in the email link
224    #[serde(skip)]
225    pub email_redirect_to: Option<String>,
226    /// A custom data object to store the user's metadata. This maps to the `auth.users.raw_user_meta_data` column.
227    ///
228    /// The `data` should be a JSON object that includes user-specific info, such as their first and last name.
229    pub data: Option<Value>,
230    /// Verification token received when the user completes the captcha on the site.
231    pub captcha_token: Option<String>,
232}
233
234#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
235pub struct ResetPasswordOptions {
236    /// The redirect url embedded in the email link
237    #[serde(skip)]
238    pub email_redirect_to: Option<String>,
239
240    /// Verification token received when the user completes the captcha on the site.
241    pub captcha_token: Option<String>,
242}
243
244#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub struct LoginAnonymouslyOptions {
246    /// The `data` should be a JSON object that includes user-specific info, such as their first and last name.
247    pub data: Option<Value>,
248    /// Verification token received when the user completes the captcha on the site.
249    pub captcha_token: Option<String>,
250}
251
252#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
253pub(crate) struct RequestMagicLinkPayload<'a> {
254    pub(crate) email: &'a str,
255}
256
257#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
258pub struct UpdatedUser {
259    pub email: Option<String>,
260    pub password: Option<String>,
261    pub data: Option<serde_json::Value>,
262}
263
264#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
265pub(crate) struct SendSMSOtpPayload<'a> {
266    pub phone: &'a str,
267}
268
269#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
270pub struct OTPResponse {
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub message_id: Option<String>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
276#[serde(untagged)]
277pub enum VerifyOtpParams {
278    Mobile(VerifyMobileOtpParams),
279    Email(VerifyEmailOtpParams),
280    TokenHash(VerifyTokenHashParams),
281}
282
283#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
284pub struct VerifyMobileOtpParams {
285    /// The user's phone number.
286    pub phone: String,
287    /// The otp sent to the user's phone number.
288    pub token: String,
289    /// The user's verification type.
290    #[serde(rename = "type")]
291    pub otp_type: OtpType,
292    /// Optional parameters
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub options: Option<VerifyOtpOptions>,
295}
296
297#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
298pub struct VerifyEmailOtpParams {
299    /// The user's email.
300    pub email: String,
301    /// The otp sent to the user's email.
302    pub token: String,
303    /// The user's verification type.
304    #[serde(rename = "type")]
305    pub otp_type: OtpType,
306    /// Optional parameters
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub options: Option<VerifyOtpOptions>,
309}
310
311#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
312pub struct VerifyTokenHashParams {
313    /// The user's phone number.
314    pub token_hash: String,
315    /// The user's verification type.
316    #[serde(rename = "type")]
317    pub otp_type: OtpType,
318}
319
320#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
321#[serde(rename_all = "snake_case")]
322pub enum OtpType {
323    #[default]
324    Signup,
325    EmailChange,
326    Sms,
327    Email,
328    PhoneChange,
329    Invite,
330    Magiclink,
331    Recovery,
332}
333
334#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
335pub struct VerifyOtpOptions {
336    /// A URL to send the user to after they are confirmed.
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub redirect_to: Option<String>,
339}
340
341#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
342pub(crate) struct LoginWithEmailOtpPayload<'a> {
343    pub email: &'a str,
344    #[serde(flatten)]
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub(crate) options: Option<LoginEmailOtpParams>,
347}
348
349#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
350pub struct LoginEmailOtpParams {
351    /// Verification token received when the user completes the captcha on the site.
352    pub captcha_token: Option<String>,
353    /// A custom data object to store the user's metadata. This maps to the `auth.users.raw_user_meta_data` column.
354    pub data: Option<serde_json::Value>,
355    /// The redirect url embedded in the email link
356    pub email_redirect_to: Option<String>,
357    /// If set to false, this method will not create a new user. Defaults to true.
358    pub should_create_user: Option<bool>,
359}
360
361#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
362pub struct LoginMobileOtpParams {
363    /// Verification token received when the user completes the captcha on the site.
364    pub captcha_token: Option<String>,
365    /// A custom data object to store the user's metadata. This maps to the `auth.users.raw_user_meta_data` column.
366    pub data: Option<serde_json::Value>,
367    /// The redirect url embedded in the email link
368    pub channel: Option<Channel>,
369    /// If set to false, this method will not create a new user. Defaults to true.
370    pub should_create_user: Option<bool>,
371}
372
373#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
374pub(crate) struct RefreshSessionPayload<'a> {
375    pub refresh_token: &'a str,
376}
377
378#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
379pub(crate) struct ResetPasswordForEmailPayload {
380    pub email: String,
381    #[serde(flatten)]
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub(crate) options: Option<ResetPasswordOptions>,
384}
385
386#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
387pub struct ResendParams {
388    #[serde(rename = "type")]
389    pub otp_type: OtpType,
390    pub email: String,
391    #[serde(flatten)]
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub options: Option<DesktopResendOptions>,
394}
395
396#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
397pub struct InviteParams {
398    pub email: String,
399    pub data: Option<Value>,
400}
401
402#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
403pub struct DesktopResendOptions {
404    pub email_redirect_to: Option<String>,
405    pub captcha_token: Option<String>,
406}
407
408#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
409pub struct MobileResendParams {
410    #[serde(rename = "type")]
411    pub otp_type: OtpType,
412    pub phone: String,
413    #[serde(flatten)]
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub options: Option<MobileResendOptions>,
416}
417
418#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
419pub struct MobileResendOptions {
420    captcha_token: Option<String>,
421}
422
423#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
424#[serde(rename_all = "snake_case")]
425pub enum Channel {
426    #[default]
427    Sms,
428    Whatsapp,
429}
430
431impl Display for Channel {
432    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433        match *self {
434            Channel::Sms => write!(f, "sms"),
435            Channel::Whatsapp => write!(f, "whatsapp"),
436        }
437    }
438}
439
440/// Health status of the Auth Server
441#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
442pub struct AuthServerHealth {
443    /// Version of the service
444    pub version: String,
445    /// Name of the service
446    pub name: String,
447    /// Description of the service
448    pub description: String,
449}
450
451/// Settings of the Auth Server
452#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
453pub struct AuthServerSettings {
454    pub external: External,
455    pub disable_signup: bool,
456    pub mailer_autoconfirm: bool,
457    pub phone_autoconfirm: bool,
458    pub sms_provider: String,
459    pub saml_enabled: bool,
460}
461
462#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
463pub struct External {
464    pub anonymous_users: bool,
465    pub apple: bool,
466    pub azure: bool,
467    pub bitbucket: bool,
468    pub discord: bool,
469    pub facebook: bool,
470    pub figma: bool,
471    pub fly: bool,
472    pub github: bool,
473    pub gitlab: bool,
474    pub google: bool,
475    pub keycloak: bool,
476    pub kakao: bool,
477    pub linkedin: bool,
478    pub linkedin_oidc: bool,
479    pub notion: bool,
480    pub spotify: bool,
481    pub slack: bool,
482    pub slack_oidc: bool,
483    pub workos: bool,
484    pub twitch: bool,
485    pub twitter: bool,
486    pub email: bool,
487    pub phone: bool,
488    pub zoom: bool,
489}
490
491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
492#[serde(rename_all = "snake_case")]
493/// Currently enabled OAuth providers.
494///
495/// # Example
496/// ```
497/// let provider = Provider::Github.to_string();
498/// println!("{provider}") // "github"
499/// ```
500pub enum Provider {
501    Apple,
502    Azure,
503    Bitbucket,
504    Discord,
505    Facebook,
506    Figma,
507    Fly,
508    Github,
509    Gitlab,
510    Google,
511    Kakao,
512    Keycloak,
513    Linkedin,
514    LinkedinOidc,
515    Notion,
516    Slack,
517    SlackOidc,
518    Spotify,
519    Twitch,
520    Twitter,
521    Workos,
522    Zoom,
523}
524
525impl Display for Provider {
526    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
527        match *self {
528            Provider::Apple => write!(f, "apple"),
529            Provider::Azure => write!(f, "azure"),
530            Provider::Bitbucket => write!(f, "bitbucket"),
531            Provider::Discord => write!(f, "discord"),
532            Provider::Facebook => write!(f, "facebook"),
533            Provider::Figma => write!(f, "figma"),
534            Provider::Fly => write!(f, "fly"),
535            Provider::Github => write!(f, "github"),
536            Provider::Gitlab => write!(f, "gitlab"),
537            Provider::Google => write!(f, "google"),
538            Provider::Kakao => write!(f, "kakao"),
539            Provider::Keycloak => write!(f, "keycloak"),
540            Provider::Linkedin => write!(f, "linkedin"),
541            Provider::LinkedinOidc => write!(f, "linkedin_oidc"),
542            Provider::Notion => write!(f, "notion"),
543            Provider::Slack => write!(f, "slack"),
544            Provider::SlackOidc => write!(f, "slack_oidc"),
545            Provider::Spotify => write!(f, "spotify"),
546            Provider::Twitch => write!(f, "twitch"),
547            Provider::Twitter => write!(f, "twitter"),
548            Provider::Workos => write!(f, "workos"),
549            Provider::Zoom => write!(f, "zoom"),
550        }
551    }
552}
553
554/// Represents the scope of the logout operation
555#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
556#[serde(rename_all = "snake_case")]
557pub enum LogoutScope {
558    #[default]
559    Global,
560    Local,
561    Others,
562}
563
564#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
565pub struct LoginWithSSO {
566    #[serde(skip_serializing_if = "Option::is_none")]
567    /// UUID of the SSO provider to invoke single-sign on to
568    pub provider_id: Option<String>,
569    #[serde(skip_serializing_if = "Option::is_none")]
570    /// Domain of the SSO provider where users can initiate sign on
571    pub domain: Option<String>,
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub options: Option<SSOLoginOptions>,
574}
575
576#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
577pub struct SSOLoginOptions {
578    #[serde(skip_serializing_if = "Option::is_none")]
579    /// Verification token received when the user completes the captcha on the site.
580    captcha_token: Option<String>,
581    #[serde(skip_serializing_if = "Option::is_none")]
582    /// A URL to send the user to after they have signed-in.
583    redirect_to: Option<String>,
584}
585
586#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
587pub struct SSOSuccess {
588    /// URL to open in a browser which will complete the sign-in flow by
589    /// taking the user to the identity provider's authentication flow.
590    ///
591    /// On browsers you can set the URL to `window.location.href` to take
592    /// the user to the authentication flow.
593    pub url: String,
594    pub status: u16,
595    pub headers: Headers,
596}
597
598#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
599pub struct Headers {
600    pub date: String,
601    #[serde(rename = "content-type")]
602    pub content_type: String,
603    #[serde(rename = "transfer-encoding")]
604    pub transfer_encoding: String,
605    pub connection: String,
606    pub server: String,
607    pub vary: String,
608    #[serde(rename = "x-okta-request-id")]
609    pub x_okta_request_id: String,
610    #[serde(rename = "x-xss-protection")]
611    pub x_xss_protection: String,
612    pub p3p: String,
613    #[serde(rename = "set-cookie")]
614    pub set_cookie: Vec<String>,
615    #[serde(rename = "content-security-policy-report-only")]
616    pub content_security_policy_report_only: String,
617    #[serde(rename = "content-security-policy")]
618    pub content_security_policy: String,
619    #[serde(rename = "x-rate-limit-limit")]
620    pub x_rate_limit_limit: String,
621    #[serde(rename = "x-rate-limit-remaining")]
622    pub x_rate_limit_remaining: String,
623    #[serde(rename = "x-rate-limit-reset")]
624    pub x_rate_limit_reset: String,
625    #[serde(rename = "referrer-policy")]
626    pub referrer_policy: String,
627    #[serde(rename = "accept-ch")]
628    pub accept_ch: String,
629    #[serde(rename = "cache-control")]
630    pub cache_control: String,
631    pub pragma: String,
632    pub expires: String,
633    #[serde(rename = "x-frame-options")]
634    pub x_frame_options: String,
635    #[serde(rename = "x-content-type-options")]
636    pub x_content_type_options: String,
637    #[serde(rename = "x-ua-compatible")]
638    pub x_ua_compatible: String,
639    #[serde(rename = "content-language")]
640    pub content_language: String,
641    #[serde(rename = "strict-transport-security")]
642    pub strict_transport_security: String,
643    #[serde(rename = "x-robots-tag")]
644    pub x_robots_tag: String,
645}
646
647// Implement custom Debug to avoid exposing sensitive information
648impl fmt::Debug for AuthClient {
649    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
650        f.debug_struct("AuthClient")
651            .field("project_url", &self.project_url())
652            .field("api_key", &"[REDACTED]")
653            .field("jwt_secret", &"[REDACTED]")
654            .finish()
655    }
656}
657
658pub const AUTH_V1: &str = "/auth/v1";