1use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::fmt;
18
19fn redact(v: &Option<String>) -> Option<&'static str> {
21 v.as_ref().map(|_| "<redacted>")
22}
23
24#[derive(Debug, Clone, Deserialize)]
32pub struct ClaudeUsageResponse {
33 #[serde(default)]
35 pub five_hour: Option<ClaudeUsageWindow>,
36 #[serde(default)]
38 pub seven_day: Option<ClaudeUsageWindow>,
39 #[serde(default, deserialize_with = "de_lenient_limits")]
42 pub limits: Vec<ClaudeLimit>,
43 #[serde(default)]
45 pub spend: Option<ClaudeSpend>,
46}
47
48#[derive(Debug, Clone, Deserialize)]
50pub struct ClaudeUsageWindow {
51 #[serde(default, deserialize_with = "de_f64_or_zero")]
53 pub utilization: f64,
54 #[serde(default)]
56 pub resets_at: Option<String>,
57}
58
59#[derive(Debug, Clone, Deserialize)]
61pub struct ClaudeLimit {
62 #[serde(default)]
64 pub kind: Option<String>,
65 #[serde(default)]
67 pub percent: f64,
68 #[serde(default)]
70 pub severity: Option<String>,
71 #[serde(default)]
73 pub resets_at: Option<String>,
74 #[serde(default)]
76 pub scope: Option<ClaudeScope>,
77 #[serde(default, deserialize_with = "de_bool_or_false")]
79 pub is_active: bool,
80}
81
82#[derive(Debug, Clone, Deserialize)]
84pub struct ClaudeScope {
85 #[serde(default)]
87 pub model: Option<ClaudeScopeModel>,
88}
89
90#[derive(Debug, Clone, Deserialize)]
92pub struct ClaudeScopeModel {
93 #[serde(default)]
95 pub display_name: Option<String>,
96}
97
98#[derive(Debug, Clone, Deserialize)]
100pub struct ClaudeSpend {
101 #[serde(default)]
103 pub used: Option<ClaudeMoney>,
104 #[serde(default)]
106 pub balance: Option<ClaudeMoney>,
107 #[serde(default)]
109 pub enabled: bool,
110}
111
112#[derive(Debug, Clone, Deserialize)]
114pub struct ClaudeMoney {
115 #[serde(default)]
117 pub amount_minor: i64,
118 #[serde(default)]
120 pub currency: Option<String>,
121 #[serde(default)]
123 pub exponent: i32,
124}
125
126impl ClaudeMoney {
127 pub fn as_display(&self) -> String {
129 let value = self.amount_minor as f64 / 10f64.powi(self.exponent.max(0));
130 match self.currency.as_deref() {
131 Some("USD") | None => format!("${value:.2}"),
132 Some(cur) => format!("{value:.2} {cur}"),
133 }
134 }
135}
136
137#[derive(Clone, Deserialize)]
142pub struct ClaudeCredentials {
143 #[serde(rename = "claudeAiOauth", default)]
145 pub claude_ai_oauth: Option<ClaudeOauth>,
146}
147
148impl fmt::Debug for ClaudeCredentials {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.debug_struct("ClaudeCredentials")
151 .field("claude_ai_oauth", &self.claude_ai_oauth)
152 .finish()
153 }
154}
155
156#[derive(Clone, Deserialize)]
158pub struct ClaudeOauth {
159 #[serde(rename = "accessToken", default)]
161 pub access_token: Option<String>,
162 #[serde(rename = "refreshToken", default)]
164 pub refresh_token: Option<String>,
165 #[serde(rename = "expiresAt", default)]
167 pub expires_at: Option<i64>,
168 #[serde(default)]
170 pub scopes: Vec<String>,
171 #[serde(rename = "rateLimitTier", default)]
174 pub rate_limit_tier: Option<String>,
175 #[serde(rename = "subscriptionType", default)]
177 pub subscription_type: Option<String>,
178}
179
180impl fmt::Debug for ClaudeOauth {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 f.debug_struct("ClaudeOauth")
183 .field("access_token", &redact(&self.access_token))
184 .field("refresh_token", &redact(&self.refresh_token))
185 .field("expires_at", &self.expires_at)
186 .field("scopes", &self.scopes)
187 .field("rate_limit_tier", &self.rate_limit_tier)
188 .field("subscription_type", &self.subscription_type)
189 .finish()
190 }
191}
192
193#[derive(Clone, Deserialize)]
195pub struct ClaudeRefreshResponse {
196 #[serde(default)]
198 pub access_token: Option<String>,
199 #[serde(default)]
201 pub refresh_token: Option<String>,
202 #[serde(default)]
204 pub expires_in: Option<i64>,
205 #[serde(default)]
207 pub scope: Option<String>,
208}
209
210impl fmt::Debug for ClaudeRefreshResponse {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 f.debug_struct("ClaudeRefreshResponse")
213 .field("access_token", &redact(&self.access_token))
214 .field("refresh_token", &redact(&self.refresh_token))
215 .field("expires_in", &self.expires_in)
216 .field("scope", &self.scope)
217 .finish()
218 }
219}
220
221#[derive(Debug, Clone, Default, Serialize, Deserialize)]
223pub struct ClaudeQuotaSnapshot {
224 #[serde(default)]
226 pub source: QuotaSource,
227 pub fetched_at: i64,
229 #[serde(default)]
231 pub plan_type: Option<String>,
232 pub five_hour: Option<QuotaWindow>,
234 pub seven_day: Option<QuotaWindow>,
236 #[serde(default)]
238 pub scoped_weekly: Option<QuotaWindow>,
239 #[serde(default)]
241 pub scoped_label: Option<String>,
242 #[serde(default)]
244 pub balance: Option<String>,
245 #[serde(default)]
247 pub spend_used: Option<String>,
248 #[serde(default)]
250 pub limit_reached: bool,
251 #[serde(default)]
254 pub needs_login: bool,
255}
256
257#[derive(Debug, Clone, Deserialize)]
261pub struct WhamUsageResponse {
262 #[serde(default)]
264 pub plan_type: Option<String>,
265 #[serde(default)]
267 pub rate_limit: Option<WhamRateLimit>,
268 #[serde(default)]
270 pub credits: Option<WhamCredits>,
271 #[serde(default)]
273 pub rate_limit_reset_credits: Option<WhamResetCredits>,
274 #[serde(default)]
276 pub spend_control: Option<WhamSpendControl>,
277}
278
279#[derive(Debug, Clone, Deserialize)]
281pub struct WhamSpendControl {
282 #[serde(default)]
284 pub reached: Option<bool>,
285 #[serde(default)]
287 pub individual_limit: Option<f64>,
288}
289
290#[derive(Debug, Clone, Deserialize)]
292pub struct WhamRateLimit {
293 #[serde(default)]
295 pub limit_reached: Option<bool>,
296 #[serde(default)]
298 pub primary_window: Option<WhamWindow>,
299 #[serde(default)]
301 pub secondary_window: Option<WhamWindow>,
302}
303
304#[derive(Debug, Clone, Deserialize)]
306pub struct WhamWindow {
307 #[serde(default)]
309 pub used_percent: Option<f64>,
310 #[serde(default)]
312 pub limit_window_seconds: Option<i64>,
313 #[serde(default)]
315 pub reset_after_seconds: Option<i64>,
316 #[serde(default)]
318 pub reset_at: Option<i64>,
319}
320
321#[derive(Debug, Clone, Deserialize)]
323pub struct WhamCredits {
324 #[serde(default)]
326 pub has_credits: Option<bool>,
327 #[serde(default)]
329 pub unlimited: Option<bool>,
330 #[serde(default)]
332 pub overage_limit_reached: Option<bool>,
333 #[serde(default, deserialize_with = "de_string_or_number")]
335 pub balance: Option<String>,
336 #[serde(default)]
338 pub approx_local_messages: Option<Vec<i64>>,
339 #[serde(default)]
341 pub approx_cloud_messages: Option<Vec<i64>>,
342}
343
344fn de_string_or_number<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
350where
351 D: serde::Deserializer<'de>,
352{
353 Ok(match Option::<Value>::deserialize(deserializer)? {
354 Some(Value::String(s)) => Some(s),
355 Some(Value::Number(n)) => Some(n.to_string()),
356 _ => None,
357 })
358}
359
360fn de_f64_or_zero<'de, D>(deserializer: D) -> Result<f64, D::Error>
365where
366 D: serde::Deserializer<'de>,
367{
368 Ok(match Option::<Value>::deserialize(deserializer)? {
369 Some(Value::Number(n)) => n.as_f64().unwrap_or(0.0),
370 _ => 0.0,
371 })
372}
373
374fn de_bool_or_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
376where
377 D: serde::Deserializer<'de>,
378{
379 Ok(matches!(
380 Option::<Value>::deserialize(deserializer)?,
381 Some(Value::Bool(true))
382 ))
383}
384
385fn de_lenient_limits<'de, D>(deserializer: D) -> Result<Vec<ClaudeLimit>, D::Error>
390where
391 D: serde::Deserializer<'de>,
392{
393 Ok(match Value::deserialize(deserializer)? {
394 Value::Array(arr) => arr
395 .into_iter()
396 .filter_map(|e| serde_json::from_value(e).ok())
397 .collect(),
398 _ => Vec::new(),
399 })
400}
401
402#[derive(Debug, Clone, Deserialize)]
404pub struct WhamResetCredits {
405 #[serde(default)]
407 pub available_count: Option<i64>,
408}
409
410#[derive(Debug, Clone, Deserialize)]
412pub struct WhamResetCreditsDetails {
413 pub credits: Vec<WhamResetCreditDetails>,
415 pub available_count: i64,
417}
418
419#[derive(Debug, Clone, Deserialize)]
421pub struct WhamResetCreditDetails {
422 pub id: String,
424 pub reset_type: String,
426 pub status: String,
428 pub granted_at: String,
430 pub expires_at: Option<String>,
432}
433
434#[derive(Debug, Clone, Deserialize)]
438pub struct CodexAuthJson {
439 #[serde(default)]
441 pub tokens: Option<CodexAuthTokens>,
442}
443
444#[derive(Clone, Deserialize)]
450pub struct CodexAuthTokens {
451 #[serde(default)]
453 pub id_token: Option<String>,
454 #[serde(default)]
456 pub access_token: Option<String>,
457 #[serde(default)]
459 pub refresh_token: Option<String>,
460 #[serde(default)]
462 pub account_id: Option<String>,
463}
464
465impl fmt::Debug for CodexAuthTokens {
466 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467 f.debug_struct("CodexAuthTokens")
468 .field("id_token", &redact(&self.id_token))
469 .field("access_token", &redact(&self.access_token))
470 .field("refresh_token", &redact(&self.refresh_token))
471 .field("account_id", &redact(&self.account_id))
472 .finish()
473 }
474}
475
476#[derive(Clone, Deserialize)]
478pub struct CodexRefreshResponse {
479 #[serde(default)]
481 pub id_token: Option<String>,
482 #[serde(default)]
484 pub access_token: Option<String>,
485 #[serde(default)]
487 pub refresh_token: Option<String>,
488}
489
490impl fmt::Debug for CodexRefreshResponse {
491 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492 f.debug_struct("CodexRefreshResponse")
493 .field("id_token", &redact(&self.id_token))
494 .field("access_token", &redact(&self.access_token))
495 .field("refresh_token", &redact(&self.refresh_token))
496 .finish()
497 }
498}
499
500#[derive(Debug, Clone, Deserialize)]
504pub struct CodexSessionRateLimits {
505 #[serde(default)]
508 pub limit_id: Option<String>,
509 #[serde(default)]
511 pub plan_type: Option<String>,
512 #[serde(default)]
514 pub primary: Option<CodexSessionWindow>,
515 #[serde(default)]
517 pub secondary: Option<CodexSessionWindow>,
518}
519
520#[derive(Debug, Clone, Deserialize)]
522pub struct CodexSessionWindow {
523 #[serde(default)]
525 pub used_percent: Option<f64>,
526 #[serde(default)]
528 pub window_minutes: Option<i64>,
529 #[serde(default)]
531 pub resets_at: Option<i64>,
532}
533
534#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
538#[serde(rename_all = "snake_case")]
539pub enum QuotaSource {
540 #[default]
542 None,
543 Api,
545 SessionFallback,
547}
548
549#[derive(Debug, Clone, Default, Serialize, Deserialize)]
551pub struct QuotaWindow {
552 pub used_percent: f64,
554 pub resets_at_unix: Option<i64>,
556}
557
558#[derive(Debug, Clone, Default, Serialize, Deserialize)]
561pub struct CodexQuotaSnapshot {
562 pub source: QuotaSource,
564 pub fetched_at: i64,
566 pub plan_type: Option<String>,
568 pub primary: Option<QuotaWindow>,
570 pub secondary: Option<QuotaWindow>,
572 pub credits_balance: Option<String>,
574 pub has_credits: Option<bool>,
576 pub unlimited: Option<bool>,
578 pub reset_credits_available: Option<i64>,
580 #[serde(default)]
584 pub reset_credit_expirations: Option<Vec<Option<i64>>>,
585 #[serde(default)]
587 pub approx_messages: Option<(i64, i64)>,
588 #[serde(default)]
590 pub spend_limit: Option<f64>,
591 pub limit_reached: Option<bool>,
593 #[serde(default)]
596 pub needs_login: bool,
597}
598
599#[derive(Debug, Clone, Deserialize)]
605pub struct CopilotUserResponse {
606 #[serde(default)]
608 pub copilot_plan: Option<String>,
609 #[serde(default)]
611 pub quota_reset_date_utc: Option<String>,
612 #[serde(default)]
614 pub quota_reset_date: Option<String>,
615 #[serde(default)]
617 pub quota_snapshots: Option<CopilotQuotaSnapshots>,
618}
619
620#[derive(Debug, Clone, Deserialize)]
622pub struct CopilotQuotaSnapshots {
623 #[serde(default)]
625 pub premium_interactions: Option<CopilotQuotaEntry>,
626}
627
628#[derive(Debug, Clone, Deserialize)]
630pub struct CopilotQuotaEntry {
631 #[serde(default)]
633 pub percent_remaining: Option<f64>,
634 #[serde(default)]
636 pub remaining: Option<f64>,
637 #[serde(default)]
639 pub entitlement: Option<f64>,
640 #[serde(default)]
642 pub unlimited: Option<bool>,
643}
644
645#[derive(Debug, Clone, Deserialize)]
649#[serde(rename_all = "camelCase")]
650pub struct CursorUsageSummary {
651 #[serde(default)]
653 pub membership_type: Option<String>,
654 #[serde(default)]
656 pub is_unlimited: Option<bool>,
657 #[serde(default)]
659 pub billing_cycle_end: Option<String>,
660 #[serde(default)]
662 pub individual_usage: Option<CursorIndividualUsage>,
663 #[serde(default)]
665 pub team_usage: Option<CursorTeamUsage>,
666}
667
668#[derive(Debug, Clone, Deserialize)]
670#[serde(rename_all = "camelCase")]
671pub struct CursorTeamUsage {
672 #[serde(default)]
674 pub on_demand: Option<CursorOnDemand>,
675}
676
677#[derive(Debug, Clone, Deserialize)]
679#[serde(rename_all = "camelCase")]
680pub struct CursorIndividualUsage {
681 #[serde(default)]
683 pub plan: Option<CursorPlanUsage>,
684 #[serde(default)]
686 pub on_demand: Option<CursorOnDemand>,
687}
688
689#[derive(Debug, Clone, Deserialize)]
691#[serde(rename_all = "camelCase")]
692pub struct CursorPlanUsage {
693 #[serde(default)]
695 pub auto_percent_used: Option<f64>,
696 #[serde(default)]
698 pub api_percent_used: Option<f64>,
699 #[serde(default)]
701 pub total_percent_used: Option<f64>,
702}
703
704#[derive(Debug, Clone, Deserialize)]
706#[serde(rename_all = "camelCase")]
707pub struct CursorOnDemand {
708 #[serde(default)]
710 pub enabled: Option<bool>,
711 #[serde(default)]
713 pub used: Option<f64>,
714}
715
716#[derive(Debug, Clone, Deserialize)]
724pub struct GrokBillingResponse {
725 #[serde(default)]
727 pub config: Option<GrokCreditsConfig>,
728}
729
730#[derive(Debug, Clone, Deserialize)]
739#[serde(rename_all = "camelCase")]
740pub struct GrokCreditsConfig {
741 #[serde(default, deserialize_with = "de_f64_or_zero")]
743 pub credit_usage_percent: f64,
744 #[serde(default)]
746 pub current_period: Option<GrokUsagePeriod>,
747 #[serde(default)]
749 pub on_demand_cap: Option<GrokMoney>,
750 #[serde(default)]
752 pub on_demand_used: Option<GrokMoney>,
753 #[serde(default)]
755 pub prepaid_balance: Option<GrokMoney>,
756 #[serde(default)]
759 pub billing_period_end: Option<String>,
760}
761
762#[derive(Debug, Clone, Deserialize)]
764pub struct GrokUsagePeriod {
765 #[serde(default, rename = "type")]
767 pub period_type: Option<String>,
768 #[serde(default)]
770 pub end: Option<String>,
771}
772
773#[derive(Debug, Clone, Deserialize)]
778pub struct GrokMoney {
779 #[serde(default, deserialize_with = "de_f64_or_zero")]
781 pub val: f64,
782}
783
784impl GrokMoney {
785 pub fn as_dollars(&self) -> f64 {
787 self.val / 100.0
788 }
789}
790
791#[derive(Debug, Clone, Deserialize)]
796pub struct GrokSettingsResponse {
797 #[serde(default)]
799 pub subscription_tier_display: Option<String>,
800}
801
802#[derive(Clone, Default, Deserialize)]
811pub struct GrokAuthEntry {
812 #[serde(default)]
814 pub key: Option<String>,
815 #[serde(default)]
817 pub refresh_token: Option<String>,
818 #[serde(default)]
820 pub expires_at: Option<String>,
821 #[serde(default)]
823 pub oidc_issuer: Option<String>,
824 #[serde(default)]
826 pub oidc_client_id: Option<String>,
827 #[serde(default)]
829 pub user_id: Option<String>,
830 #[serde(default)]
832 pub principal_type: Option<String>,
833 #[serde(default)]
835 pub principal_id: Option<String>,
836}
837
838impl fmt::Debug for GrokAuthEntry {
839 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
840 f.debug_struct("GrokAuthEntry")
841 .field("key", &redact(&self.key))
842 .field("refresh_token", &redact(&self.refresh_token))
843 .field("expires_at", &self.expires_at)
844 .field("oidc_issuer", &self.oidc_issuer)
845 .field("oidc_client_id", &self.oidc_client_id)
846 .field("user_id", &redact(&self.user_id))
847 .field("principal_type", &self.principal_type)
848 .field("principal_id", &redact(&self.principal_id))
849 .finish()
850 }
851}
852
853#[derive(Clone, Deserialize)]
855pub struct GrokRefreshResponse {
856 #[serde(default)]
858 pub access_token: Option<String>,
859 #[serde(default)]
861 pub refresh_token: Option<String>,
862 #[serde(default)]
864 pub expires_in: Option<i64>,
865}
866
867impl fmt::Debug for GrokRefreshResponse {
868 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
869 f.debug_struct("GrokRefreshResponse")
870 .field("access_token", &redact(&self.access_token))
871 .field("refresh_token", &redact(&self.refresh_token))
872 .field("expires_in", &self.expires_in)
873 .finish()
874 }
875}
876
877#[derive(Debug, Clone, Default, Serialize, Deserialize)]
882pub struct CopilotQuotaSnapshot {
883 #[serde(default)]
885 pub source: QuotaSource,
886 pub fetched_at: i64,
888 #[serde(default)]
890 pub plan_type: Option<String>,
891 #[serde(default)]
893 pub premium: Option<QuotaWindow>,
894 #[serde(default)]
896 pub premium_remaining: Option<i64>,
897 #[serde(default)]
899 pub premium_entitlement: Option<i64>,
900 #[serde(default)]
902 pub premium_unlimited: bool,
903 #[serde(default)]
905 pub limit_reached: bool,
906 #[serde(default)]
909 pub needs_login: bool,
910}
911
912#[derive(Debug, Clone, Default, Serialize, Deserialize)]
915pub struct CursorQuotaSnapshot {
916 #[serde(default)]
918 pub source: QuotaSource,
919 pub fetched_at: i64,
921 #[serde(default)]
923 pub plan_type: Option<String>,
924 #[serde(default)]
926 pub total: Option<QuotaWindow>,
927 #[serde(default)]
929 pub auto: Option<QuotaWindow>,
930 #[serde(default)]
932 pub api: Option<QuotaWindow>,
933 #[serde(default)]
935 pub on_demand_dollars: Option<f64>,
936 #[serde(default)]
938 pub limit_reached: bool,
939 #[serde(default)]
942 pub needs_login: bool,
943}
944
945#[derive(Debug, Clone, Default, Serialize, Deserialize)]
948pub struct GrokQuotaSnapshot {
949 #[serde(default)]
951 pub source: QuotaSource,
952 pub fetched_at: i64,
954 #[serde(default)]
956 pub plan_type: Option<String>,
957 #[serde(default)]
959 pub included: Option<QuotaWindow>,
960 #[serde(default)]
962 pub period_label: Option<String>,
963 #[serde(default)]
965 pub on_demand_dollars: Option<f64>,
966 #[serde(default)]
968 pub on_demand_cap_dollars: Option<f64>,
969 #[serde(default)]
971 pub prepaid_balance_dollars: Option<f64>,
972 #[serde(default)]
974 pub limit_reached: bool,
975 #[serde(default)]
978 pub needs_login: bool,
979}
980
981#[cfg(test)]
982mod tests {
983 use super::*;
984
985 #[test]
986 fn auth_tokens_debug_redacts_secrets() {
987 let tokens = CodexAuthTokens {
988 id_token: Some("jwt-header.payload.sig".into()),
989 access_token: Some("sk-super-secret-value".into()),
990 refresh_token: Some("rt-super-secret".into()),
991 account_id: Some("acct-1234567890".into()),
992 };
993 let direct = format!("{tokens:?}");
994 assert!(!direct.contains("sk-super-secret-value"));
995 assert!(!direct.contains("rt-super-secret"));
996 assert!(!direct.contains("acct-1234567890"));
997 assert!(direct.contains("<redacted>"));
998
999 let wrapped = format!(
1001 "{:?}",
1002 CodexAuthJson {
1003 tokens: Some(tokens)
1004 }
1005 );
1006 assert!(!wrapped.contains("sk-super-secret-value"));
1007 assert!(!wrapped.contains("acct-1234567890"));
1008 }
1009
1010 #[test]
1011 fn claude_oauth_debug_redacts_secrets() {
1012 let oauth = ClaudeOauth {
1013 access_token: Some("claude-access-secret".into()),
1014 refresh_token: Some("claude-refresh-secret".into()),
1015 expires_at: Some(1783108188604),
1016 scopes: vec!["user:inference".into()],
1017 rate_limit_tier: Some("default_claude_max_20x".into()),
1018 subscription_type: Some("max".into()),
1019 };
1020 let s = format!("{oauth:?}");
1021 assert!(!s.contains("claude-access-secret"));
1022 assert!(!s.contains("claude-refresh-secret"));
1023 assert!(s.contains("<redacted>"));
1024 assert!(s.contains("1783108188604"));
1026 assert!(s.contains("user:inference"));
1027 }
1028
1029 #[test]
1030 fn refresh_responses_debug_redact_secrets() {
1031 let c = ClaudeRefreshResponse {
1032 access_token: Some("new-access".into()),
1033 refresh_token: Some("new-refresh".into()),
1034 expires_in: Some(28800),
1035 scope: Some("user:inference".into()),
1036 };
1037 let cs = format!("{c:?}");
1038 assert!(!cs.contains("new-access"));
1039 assert!(!cs.contains("new-refresh"));
1040 assert!(cs.contains("28800"));
1041
1042 let x = CodexRefreshResponse {
1043 id_token: Some("id-secret".into()),
1044 access_token: Some("acc-secret".into()),
1045 refresh_token: Some("ref-secret".into()),
1046 };
1047 let xs = format!("{x:?}");
1048 assert!(!xs.contains("id-secret"));
1049 assert!(!xs.contains("acc-secret"));
1050 assert!(!xs.contains("ref-secret"));
1051 }
1052}