Module: BetterAuth::Telemetry::Detectors::AuthConfig

Defined in:
lib/better_auth/telemetry/detectors/auth_config.rb

Overview

AuthConfig detector / redactor. Produces the redacted ‘payload.config` hash emitted by the init event, mirroring upstream `getTelemetryAuthConfig`.

The whole AuthConfig.call entry point is wrapped in ‘rescue StandardError; nil` so any failure during redaction degrades the entire `config` payload to `nil` rather than escaping out of the init payload composition in BetterAuth::Telemetry.create.

Constant Summary collapse

TOP_LEVEL_KEYS =

Top-level keys emitted in the redacted config payload, in the order produced by upstream ‘getTelemetryAuthConfig`.

%i[
  database
  adapter
  emailVerification
  emailAndPassword
  socialProviders
  plugins
  user
  verification
  session
  account
  hooks
  secondaryStorage
  advanced
  trustedOrigins
  rateLimit
  onAPIError
  logger
  databaseHooks
].freeze
DATABASE_HOOK_MODELS =

Models covered by the ‘databaseHooks` redaction map. The order is fixed to mirror the upstream shape produced by `getTelemetryAuthConfig` so the wire-format key order is stable across runs.

%i[user session account verification].freeze
DATABASE_HOOK_OPERATIONS =

Database operations covered for each model.

%i[create update].freeze
DATABASE_HOOK_PHASES =

Phases covered for each (model, operation) pair. The order is ‘after` then `before` to match upstream.

%i[after before].freeze

Class Method Summary collapse

Class Method Details

.bool(value) ⇒ Boolean

Boolean redaction: collapse any value into a strict ‘true`/`false`. Used for callable/secret leaves where the actual value must never reach the wire (Requirement 13.3).

Parameters:

  • value (Object)

Returns:

  • (Boolean)


112
113
114
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 112

def bool(value)
  !!value
end

.bool_present(value) ⇒ Boolean

Presence-aware boolean redaction. Returns ‘true` only when the value is non-`nil`, not `false`, and not the empty string. Mirrors upstream’s ‘!!value && value !== “”` idiom for fields like `advanced.cookiePrefix` where a missing/empty value is meaningfully different from a set one.

Parameters:

  • value (Object)

Returns:

  • (Boolean)


135
136
137
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 135

def bool_present(value)
  !value.nil? && value != "" && value != false
end

.call(options, context) ⇒ Hash{Symbol => Object}?

Build the redacted ‘payload.config` hash for the init event.

Parameters:

  • options (BetterAuth::Configuration, Hash, nil)

    the options passed to BetterAuth::Telemetry.create. May be a Configuration (production path), the raw options hash that Auth.new would consume, or ‘nil`. Both shapes are descended via fetch_path, so the same redaction pipeline produces deep-equal payloads for matching inputs (Requirement 13.1).

  • context (BetterAuth::Telemetry::NormalizedContext, Hash, nil)

    the normalized context. Only ‘:database` and `:adapter` overrides are surfaced into the payload as raw pass-through values (Requirement 13.9). The accessor tolerates either a NormalizedContext (production path), a raw hash with snake_case or camelCase / symbol or string keys (test seams), or `nil` (top-level keys collapse to `nil`).

Returns:

  • (Hash{Symbol => Object}, nil)

    the redacted config hash with the upstream top-level key set, or ‘nil` if anything in the redaction pipeline raises.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 77

def call(options, context)
  {
    database: context_value(context, :database),
    adapter: context_value(context, :adapter),
    emailVerification: redact_email_verification(options),
    emailAndPassword: redact_email_and_password(options),
    socialProviders: redact_social_providers(options),
    plugins: redact_plugins(options),
    user: redact_user(options),
    verification: redact_verification(options),
    session: redact_session(options),
    account: (options),
    hooks: redact_hooks(options),
    secondaryStorage: redact_secondary_storage(options),
    advanced: redact_advanced(options),
    trustedOrigins: redact_trusted_origins(options),
    rateLimit: redact_rate_limit(options),
    onAPIError: redact_on_api_error(options),
    logger: redact_logger(options),
    databaseHooks: redact_database_hooks(options)
  }
rescue
  nil
end

.context_value(context, key) ⇒ Object?

Read a single override key from the NormalizedContext surface, accepting either a NormalizedContext instance (the production path), a raw hash with snake_case or camelCase keys in symbol or string form (test seams), or ‘nil`. Returns the raw value when present, `nil` otherwise. Used to inject `payload` and `payload` from the call-site context override without going through the redaction map (Requirement 13.9 — context overrides are pass-through).

Parameters:

  • context (Object, nil)
  • key (Symbol)

    one of ‘:database` or `:adapter`.

Returns:

  • (Object, nil)


608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 608

def context_value(context, key)
  return nil if context.nil?
  return context.public_send(key) if context.respond_to?(key)
  return nil unless context.is_a?(Hash)

  symbol_key = key.is_a?(Symbol) ? key : key.to_s.to_sym
  return context[symbol_key] if context.key?(symbol_key)

  string_key = key.to_s
  return context[string_key] if context.key?(string_key)

  nil
rescue
  nil
end

.count(array) ⇒ Integer

Length helper. Returns the integer length of any ‘Array`-coercible input, with `nil` and non-array values treated as the empty list. Used for `trustedOrigins`, which is emitted as an integer count (never the contents).

Parameters:

  • array (Array, nil, Object)

Returns:

  • (Integer)


146
147
148
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 146

def count(array)
  Array(array).length
end

.fetch_path(opts, path) ⇒ Object?

Read a nested value from either a Configuration instance or a raw options hash, using the same snake_case path. Symbol/string key shapes in nested hashes are both accepted.

The path’s first segment is treated as the Configuration reader name (snake_case). When the source is a Configuration, the first segment is sent via ‘public_send`; the remainder of the path is descended into the returned value as if it were a hash. When the source is a Hash, every segment is looked up as a hash key, trying both symbol and string forms at each level.

Any failure (a missing reader, a missing key, an intermediate non-hash value) returns ‘nil` so the redaction map can short-circuit cleanly without rescuing per-leaf.

Examples:

Configuration source

cfg = BetterAuth::Configuration.new(
  secret: "0"*40,
  email_verification: { expires_in: 3600 }
)
AuthConfig.fetch_path(cfg, [:email_verification, :expires_in])
# => 3600

Raw hash source with mixed symbol/string keys

opts = { "email_verification" => { send_verification_email: ->{} } }
AuthConfig.fetch_path(opts, [:email_verification, :send_verification_email])
# => #<Proc:...>

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)
  • path (Array<Symbol>)

    non-empty snake_case path. Each segment is matched against either a ‘Configuration` reader (first segment only) or a hash key (subsequent segments).

Returns:

  • (Object, nil)

    the value at ‘path`, or `nil` when any segment is missing or the source is `nil`.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 191

def fetch_path(opts, path)
  return nil if opts.nil?
  return nil if path.nil? || path.empty?

  head, *tail = path
  current = read_root(opts, head)
  return current if tail.empty?

  tail.reduce(current) do |value, key|
    break nil unless value.is_a?(Hash)

    hash_lookup(value, key)
  end
rescue
  nil
end

.hash_lookup(hash, key) ⇒ Object?

Look up a key in a hash trying both symbol and string forms. Returns ‘nil` when neither shape contains the key.

Parameters:

  • hash (Hash)
  • key (Symbol, String)

Returns:

  • (Object, nil)


650
651
652
653
654
655
656
657
658
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 650

def hash_lookup(hash, key)
  symbol_key = key.is_a?(Symbol) ? key : key.to_s.to_sym
  return hash[symbol_key] if hash.key?(symbol_key)

  string_key = key.to_s
  return hash[string_key] if hash.key?(string_key)

  nil
end

.raw(value) ⇒ Object

Pass-through helper: emit the value as-is. Used for raw scalars that are safe to ship verbatim (timeouts, lengths, field maps, …).

Parameters:

  • value (Object)

Returns:

  • (Object)


122
123
124
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 122

def raw(value)
  value
end

.read_root(opts, key) ⇒ Object?

Read the root (first segment) of a ‘fetch_path` lookup. For a Configuration we call the snake_case reader; for a Hash we look up the key under both symbol and string forms; for any other object we return `nil`.

Parameters:

  • opts (BetterAuth::Configuration, Hash, Object)
  • key (Symbol)

Returns:

  • (Object, nil)


632
633
634
635
636
637
638
639
640
641
642
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 632

def read_root(opts, key)
  if defined?(::BetterAuth::Configuration) && opts.is_a?(::BetterAuth::Configuration)
    return opts.public_send(key) if opts.respond_to?(key)

    return nil
  end

  return hash_lookup(opts, key) if opts.is_a?(Hash)

  nil
end

.redact_account(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.account` hash. Every documented leaf is a raw pass-through; nested `accountLinking.*` keys are emitted as their own sub-hash mirroring upstream.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 393

def (opts)
  {
    modelName: raw(fetch_path(opts, [:account, :model_name])),
    fields: raw(fetch_path(opts, [:account, :fields])),
    encryptOAuthTokens: raw(fetch_path(opts, [:account, :encrypt_oauth_tokens])),
    updateAccountOnSignIn: raw(fetch_path(opts, [:account, :update_account_on_sign_in])),
    accountLinking: {
      enabled: raw(fetch_path(opts, [:account, :account_linking, :enabled])),
      trustedProviders: raw(fetch_path(opts, [:account, :account_linking, :trusted_providers])),
      updateUserInfoOnLink: raw(fetch_path(opts, [:account, :account_linking, :update_user_info_on_link])),
      allowUnlinkingAll: raw(fetch_path(opts, [:account, :account_linking, :allow_unlinking_all]))
    }
  }
end

.redact_advanced(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.advanced` hash.

The shape mirrors upstream ‘getTelemetryAuthConfig`’s ‘advanced` block, including the rename from the Ruby source key `default_cookie_attributes` to the upstream wire key `cookieAttributes` (Requirement 13.7).

The four boolean-redacted leaves protect host-identifying values from leaking onto the wire (Requirement 13.3 / 13.4):

* `cookiePrefix` — the literal cookie name prefix.
* `cookies` — the per-cookie configuration hash.
* `crossSubDomainCookies.domain` — host-identifying
  domain string for cross-subdomain cookies.
* `cookieAttributes.domain` — host-identifying domain
  string for the default cookie attributes.

Every other leaf is a raw pass-through scalar.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 460

def redact_advanced(opts)
  {
    cookiePrefix: bool(fetch_path(opts, [:advanced, :cookie_prefix])),
    cookies: bool(fetch_path(opts, [:advanced, :cookies])),
    crossSubDomainCookies: {
      domain: bool(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :domain])),
      enabled: raw(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :enabled])),
      additionalCookies: raw(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :additional_cookies]))
    },
    database: {
      generateId: raw(fetch_path(opts, [:advanced, :database, :generate_id])),
      defaultFindManyLimit: raw(fetch_path(opts, [:advanced, :database, :default_find_many_limit]))
    },
    useSecureCookies: raw(fetch_path(opts, [:advanced, :use_secure_cookies])),
    ipAddress: {
      disableIpTracking: raw(fetch_path(opts, [:advanced, :ip_address, :disable_ip_tracking])),
      ipAddressHeaders: raw(fetch_path(opts, [:advanced, :ip_address, :ip_address_headers]))
    },
    disableCSRFCheck: raw(fetch_path(opts, [:advanced, :disable_csrf_check])),
    cookieAttributes: {
      expires: raw(fetch_path(opts, [:advanced, :default_cookie_attributes, :expires])),
      secure: raw(fetch_path(opts, [:advanced, :default_cookie_attributes, :secure])),
      sameSite: raw(fetch_path(opts, [:advanced, :default_cookie_attributes, :same_site])),
      domain: bool(fetch_path(opts, [:advanced, :default_cookie_attributes, :domain])),
      path: raw(fetch_path(opts, [:advanced, :default_cookie_attributes, :path])),
      httpOnly: raw(fetch_path(opts, [:advanced, :default_cookie_attributes, :http_only]))
    }
  }
end

.redact_database_hooks(opts) ⇒ Hash{Symbol => Hash}

Build the redacted ‘payload.config.databaseHooks` tree.

The full upstream shape is a 4 × 2 × 2 nested tree:

{ user, session, account, verification } ×
  { create, update } ×
    { before, after }

giving sixteen leaves total. Every leaf is a callable in the upstream type, so every leaf is boolean-redacted (Requirement 13.8). The full tree is always emitted with the same shape so downstream consumers can rely on the key set being stable; missing leaves collapse to ‘false`.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Hash})


581
582
583
584
585
586
587
588
589
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 581

def redact_database_hooks(opts)
  DATABASE_HOOK_MODELS.each_with_object({}) do |model, result|
    result[model] = DATABASE_HOOK_OPERATIONS.each_with_object({}) do |operation, ops|
      ops[operation] = DATABASE_HOOK_PHASES.each_with_object({}) do |phase, phases|
        phases[phase] = bool(fetch_path(opts, [:database_hooks, model, operation, phase]))
      end
    end
  end
end

.redact_email_and_password(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.emailAndPassword` hash.

All callables (‘sendResetPassword`, `onPasswordReset`, `password.hash`, `password.verify`, …) are `bool`-redacted per Requirement 13.4. Numeric configuration scalars (`maxPasswordLength`, `minPasswordLength`, `resetPasswordTokenExpiresIn`) are emitted raw.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 242

def redact_email_and_password(opts)
  {
    enabled: bool(fetch_path(opts, [:email_and_password, :enabled])),
    disableSignUp: bool(fetch_path(opts, [:email_and_password, :disable_sign_up])),
    requireEmailVerification: bool(fetch_path(opts, [:email_and_password, :require_email_verification])),
    maxPasswordLength: raw(fetch_path(opts, [:email_and_password, :max_password_length])),
    minPasswordLength: raw(fetch_path(opts, [:email_and_password, :min_password_length])),
    sendResetPassword: bool(fetch_path(opts, [:email_and_password, :send_reset_password])),
    resetPasswordTokenExpiresIn: raw(fetch_path(opts, [:email_and_password, :reset_password_token_expires_in])),
    onPasswordReset: bool(fetch_path(opts, [:email_and_password, :on_password_reset])),
    password: {
      hash: bool(fetch_path(opts, [:email_and_password, :password, :hash])),
      verify: bool(fetch_path(opts, [:email_and_password, :password, :verify]))
    },
    autoSignIn: bool(fetch_path(opts, [:email_and_password, :auto_sign_in])),
    revokeSessionsOnPasswordReset: bool(fetch_path(opts, [:email_and_password, :revoke_sessions_on_password_reset]))
  }
end

.redact_email_verification(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.emailVerification` hash.

Every callable leaf is ‘bool`-redacted (Requirement 13.3) so the actual proc/lambda/object never reaches the wire. The only raw scalar in this section is `expiresIn`.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


220
221
222
223
224
225
226
227
228
229
230
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 220

def redact_email_verification(opts)
  {
    sendVerificationEmail: bool(fetch_path(opts, [:email_verification, :send_verification_email])),
    sendOnSignUp: bool(fetch_path(opts, [:email_verification, :send_on_sign_up])),
    sendOnSignIn: bool(fetch_path(opts, [:email_verification, :send_on_sign_in])),
    autoSignInAfterVerification: bool(fetch_path(opts, [:email_verification, :auto_sign_in_after_verification])),
    expiresIn: raw(fetch_path(opts, [:email_verification, :expires_in])),
    beforeEmailVerification: bool(fetch_path(opts, [:email_verification, :before_email_verification])),
    afterEmailVerification: bool(fetch_path(opts, [:email_verification, :after_email_verification]))
  }
end

.redact_hooks(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.hooks` hash.

Both ‘before` and `after` may be a single proc, an array of procs, or `nil`. The redaction collapses any non-nil/ non-false value into `true`, so callable references never leak (Requirement 13.4).

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


417
418
419
420
421
422
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 417

def redact_hooks(opts)
  {
    after: bool(fetch_path(opts, [:hooks, :after])),
    before: bool(fetch_path(opts, [:hooks, :before]))
  }
end

.redact_logger(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.logger` hash.

‘log` is callable in the upstream type and is therefore boolean-redacted (Requirement 13.4). `disabled` and `level` are raw pass-through scalars.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


557
558
559
560
561
562
563
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 557

def redact_logger(opts)
  {
    disabled: raw(fetch_path(opts, [:logger, :disabled])),
    level: raw(fetch_path(opts, [:logger, :level])),
    log: bool(fetch_path(opts, [:logger, :log]))
  }
end

.redact_on_api_error(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.onAPIError` hash.

‘onError` is callable in the upstream type and is therefore boolean-redacted (Requirement 13.4). `errorURL` and `throw` are raw pass-through scalars.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


541
542
543
544
545
546
547
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 541

def redact_on_api_error(opts)
  {
    errorURL: raw(fetch_path(opts, [:on_api_error, :error_url])),
    onError: bool(fetch_path(opts, [:on_api_error, :on_error])),
    throw: raw(fetch_path(opts, [:on_api_error, :throw]))
  }
end

.redact_plugins(opts) ⇒ Array<String>?

Build the redacted ‘payload.config.plugins` value.

Upstream emits an array of plugin id strings, or ‘null` (Ruby `nil`) when no plugins are configured. We mirror that exactly: each configured plugin is asked for its `id`, the result is stringified, blanks (nil / empty) are dropped, and the empty-list case collapses to `nil`.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Array<String>, nil)


319
320
321
322
323
324
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 319

def redact_plugins(opts)
  plugins = fetch_path(opts, [:plugins])
  ids = Array(plugins).map { |plugin| plugin.respond_to?(:id) ? plugin.id.to_s : nil }
  ids = ids.reject { |id| id.nil? || id.empty? }
  ids.empty? ? nil : ids
end

.redact_rate_limit(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.rateLimit` hash.

‘customStorage` is callable in the upstream type and is therefore boolean-redacted (Requirement 13.4); every other leaf is a raw pass-through scalar.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


522
523
524
525
526
527
528
529
530
531
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 522

def redact_rate_limit(opts)
  {
    storage: raw(fetch_path(opts, [:rate_limit, :storage])),
    modelName: raw(fetch_path(opts, [:rate_limit, :model_name])),
    window: raw(fetch_path(opts, [:rate_limit, :window])),
    customStorage: bool(fetch_path(opts, [:rate_limit, :custom_storage])),
    enabled: raw(fetch_path(opts, [:rate_limit, :enabled])),
    max: raw(fetch_path(opts, [:rate_limit, :max]))
  }
end

.redact_secondary_storage(opts) ⇒ Boolean

Build the redacted ‘payload.config.secondaryStorage` value.

Upstream emits ‘!!options.secondaryStorage`: a strict boolean indicating whether a secondary storage backend has been wired up, never the storage object itself (Requirement 13.4 — callable / object references must not reach the wire).

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Boolean)


434
435
436
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 434

def redact_secondary_storage(opts)
  bool(fetch_path(opts, [:secondary_storage]))
end

.redact_session(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.session` hash. Every documented leaf is a raw pass-through; nested `cookieCache.*` keys are emitted as their own sub-hash mirroring upstream.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 367

def redact_session(opts)
  {
    modelName: raw(fetch_path(opts, [:session, :model_name])),
    additionalFields: raw(fetch_path(opts, [:session, :additional_fields])),
    cookieCache: {
      enabled: raw(fetch_path(opts, [:session, :cookie_cache, :enabled])),
      maxAge: raw(fetch_path(opts, [:session, :cookie_cache, :max_age])),
      strategy: raw(fetch_path(opts, [:session, :cookie_cache, :strategy]))
    },
    disableSessionRefresh: raw(fetch_path(opts, [:session, :disable_session_refresh])),
    expiresIn: raw(fetch_path(opts, [:session, :expires_in])),
    fields: raw(fetch_path(opts, [:session, :fields])),
    freshAge: raw(fetch_path(opts, [:session, :fresh_age])),
    preserveSessionInDatabase: raw(fetch_path(opts, [:session, :preserve_session_in_database])),
    storeSessionInDatabase: raw(fetch_path(opts, [:session, :store_session_in_database])),
    updateAge: raw(fetch_path(opts, [:session, :update_age]))
  }
end

.redact_social_providers(opts) ⇒ Array<Hash{Symbol => Object}>

Build the redacted ‘payload.config.socialProviders` array.

The Ruby port stores ‘social_providers` as a `Hash` keyed by provider id (`:github`, `:google`, …) where each value is the per-provider options hash. Upstream emits an `Array` of redacted-provider hashes, so we walk the source hash and rebuild the wire shape one entry at a time.

Mapping of keys (Ruby snake_case → upstream camelCase):

bool leaves (callable / presence indicators):
  map_profile_to_user        → mapProfileToUser
  disable_default_scope      → disableDefaultScope
  disable_id_token_sign_in   → disableIdTokenSignIn
  get_user_info              → getUserInfo
  override_user_info_on_sign_in → overrideUserInfoOnSignIn
  verify_id_token            → verifyIdToken
  refresh_access_token       → refreshAccessToken
raw pass-through scalars:
  disable_implicit_sign_up   → disableImplicitSignUp
  disable_sign_up            → disableSignUp
  prompt                     → prompt
  scope                      → scope

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Array<Hash{Symbol => Object}>)


286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 286

def redact_social_providers(opts)
  providers = fetch_path(opts, [:social_providers])
  return [] unless providers.is_a?(Hash)

  providers.map do |provider_id, raw_provider|
    provider = raw_provider.is_a?(Hash) ? raw_provider : {}
    {
      id: provider_id.to_s,
      mapProfileToUser: bool(provider[:map_profile_to_user]),
      disableDefaultScope: bool(provider[:disable_default_scope]),
      disableIdTokenSignIn: bool(provider[:disable_id_token_sign_in]),
      disableImplicitSignUp: provider[:disable_implicit_sign_up],
      disableSignUp: provider[:disable_sign_up],
      getUserInfo: bool(provider[:get_user_info]),
      overrideUserInfoOnSignIn: bool(provider[:override_user_info_on_sign_in]),
      prompt: provider[:prompt],
      verifyIdToken: bool(provider[:verify_id_token]),
      scope: provider[:scope],
      refreshAccessToken: bool(provider[:refresh_access_token])
    }
  end
end

.redact_trusted_origins(opts) ⇒ Integer?

Build the redacted ‘payload.config.trustedOrigins` value.

Upstream emits ‘options.trustedOrigins?.length`: an integer count of configured origins, or `nil` when the key is absent. We never emit the origin strings themselves, since they identify customer hosts (Requirement 13.7).

The Ruby ‘Configuration#trusted_origins` reader normalizes the input into an array (folding in `base_url`, dynamic-base-url hosts, and the `BETTER_AUTH_TRUSTED_ORIGINS` env list); the count we emit matches whatever that normalization produced. When the source is a raw hash, we count the literal value at `:trusted_origins`.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Integer, nil)


507
508
509
510
511
512
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 507

def redact_trusted_origins(opts)
  value = fetch_path(opts, [:trusted_origins])
  return nil if value.nil?

  count(value)
end

.redact_user(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.user` hash.

Every leaf except ‘changeEmail.sendChangeEmailConfirmation` is a raw pass-through. The send-change-email confirmation callback is `bool`-redacted per Requirement 13.4 so the callable never reaches the wire.

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


335
336
337
338
339
340
341
342
343
344
345
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 335

def redact_user(opts)
  {
    modelName: raw(fetch_path(opts, [:user, :model_name])),
    fields: raw(fetch_path(opts, [:user, :fields])),
    additionalFields: raw(fetch_path(opts, [:user, :additional_fields])),
    changeEmail: {
      enabled: raw(fetch_path(opts, [:user, :change_email, :enabled])),
      sendChangeEmailConfirmation: bool(fetch_path(opts, [:user, :change_email, :send_change_email_confirmation]))
    }
  }
end

.redact_verification(opts) ⇒ Hash{Symbol => Object}

Build the redacted ‘payload.config.verification` hash. All leaves are raw pass-throughs (no callables in this section).

Parameters:

  • opts (BetterAuth::Configuration, Hash, nil)

Returns:

  • (Hash{Symbol => Object})


352
353
354
355
356
357
358
# File 'lib/better_auth/telemetry/detectors/auth_config.rb', line 352

def redact_verification(opts)
  {
    modelName: raw(fetch_path(opts, [:verification, :model_name])),
    disableCleanup: raw(fetch_path(opts, [:verification, :disable_cleanup])),
    fields: raw(fetch_path(opts, [:verification, :fields]))
  }
end