better_auth-telemetry 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE.md +20 -0
- data/README.md +202 -0
- data/lib/better_auth/plugins/telemetry.rb +11 -0
- data/lib/better_auth/telemetry/create.rb +293 -0
- data/lib/better_auth/telemetry/detectors/auth_config.rb +662 -0
- data/lib/better_auth/telemetry/detectors/database.rb +194 -0
- data/lib/better_auth/telemetry/detectors/environment.rb +86 -0
- data/lib/better_auth/telemetry/detectors/framework.rb +80 -0
- data/lib/better_auth/telemetry/detectors/project_info.rb +84 -0
- data/lib/better_auth/telemetry/detectors/runtime.rb +45 -0
- data/lib/better_auth/telemetry/detectors/system_info.rb +320 -0
- data/lib/better_auth/telemetry/env.rb +77 -0
- data/lib/better_auth/telemetry/http_client.rb +99 -0
- data/lib/better_auth/telemetry/logger_adapter.rb +118 -0
- data/lib/better_auth/telemetry/noop_publisher.rb +33 -0
- data/lib/better_auth/telemetry/options.rb +240 -0
- data/lib/better_auth/telemetry/project_id.rb +234 -0
- data/lib/better_auth/telemetry/publisher.rb +111 -0
- data/lib/better_auth/telemetry/version.rb +7 -0
- data/lib/better_auth/telemetry.rb +68 -0
- metadata +137 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Telemetry
|
|
5
|
+
module Detectors
|
|
6
|
+
# AuthConfig detector / redactor. Produces the redacted
|
|
7
|
+
# `payload.config` hash emitted by the init event, mirroring
|
|
8
|
+
# upstream `getTelemetryAuthConfig`.
|
|
9
|
+
#
|
|
10
|
+
# The whole {call} entry point is wrapped in `rescue
|
|
11
|
+
# StandardError; nil` so any failure during redaction degrades
|
|
12
|
+
# the entire `config` payload to `nil` rather than escaping out
|
|
13
|
+
# of the init payload composition in {BetterAuth::Telemetry.create}.
|
|
14
|
+
module AuthConfig
|
|
15
|
+
# Top-level keys emitted in the redacted config payload, in
|
|
16
|
+
# the order produced by upstream `getTelemetryAuthConfig`.
|
|
17
|
+
TOP_LEVEL_KEYS = %i[
|
|
18
|
+
database
|
|
19
|
+
adapter
|
|
20
|
+
emailVerification
|
|
21
|
+
emailAndPassword
|
|
22
|
+
socialProviders
|
|
23
|
+
plugins
|
|
24
|
+
user
|
|
25
|
+
verification
|
|
26
|
+
session
|
|
27
|
+
account
|
|
28
|
+
hooks
|
|
29
|
+
secondaryStorage
|
|
30
|
+
advanced
|
|
31
|
+
trustedOrigins
|
|
32
|
+
rateLimit
|
|
33
|
+
onAPIError
|
|
34
|
+
logger
|
|
35
|
+
databaseHooks
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
# Models covered by the `databaseHooks` redaction map. The
|
|
39
|
+
# order is fixed to mirror the upstream shape produced by
|
|
40
|
+
# `getTelemetryAuthConfig` so the wire-format key order is
|
|
41
|
+
# stable across runs.
|
|
42
|
+
DATABASE_HOOK_MODELS = %i[user session account verification].freeze
|
|
43
|
+
|
|
44
|
+
# Database operations covered for each model.
|
|
45
|
+
DATABASE_HOOK_OPERATIONS = %i[create update].freeze
|
|
46
|
+
|
|
47
|
+
# Phases covered for each (model, operation) pair. The
|
|
48
|
+
# order is `after` then `before` to match upstream.
|
|
49
|
+
DATABASE_HOOK_PHASES = %i[after before].freeze
|
|
50
|
+
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------------
|
|
54
|
+
# Public entry point
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
# Build the redacted `payload.config` hash for the init event.
|
|
58
|
+
#
|
|
59
|
+
# @param options [BetterAuth::Configuration, Hash, nil] the
|
|
60
|
+
# options passed to {BetterAuth::Telemetry.create}. May be
|
|
61
|
+
# a {BetterAuth::Configuration} (production path), the raw
|
|
62
|
+
# options hash that {BetterAuth::Auth.new} would consume,
|
|
63
|
+
# or `nil`. Both shapes are descended via {fetch_path}, so
|
|
64
|
+
# the same redaction pipeline produces deep-equal payloads
|
|
65
|
+
# for matching inputs (Requirement 13.1).
|
|
66
|
+
# @param context [BetterAuth::Telemetry::NormalizedContext, Hash, nil]
|
|
67
|
+
# the normalized context. Only `:database` and `:adapter`
|
|
68
|
+
# overrides are surfaced into the payload as raw
|
|
69
|
+
# pass-through values (Requirement 13.9). The accessor
|
|
70
|
+
# tolerates either a {NormalizedContext} (production path),
|
|
71
|
+
# a raw hash with snake_case or camelCase / symbol or
|
|
72
|
+
# string keys (test seams), or `nil` (top-level keys
|
|
73
|
+
# collapse to `nil`).
|
|
74
|
+
# @return [Hash{Symbol => Object}, nil] the redacted config
|
|
75
|
+
# hash with the upstream top-level key set, or `nil` if
|
|
76
|
+
# anything in the redaction pipeline raises.
|
|
77
|
+
def call(options, context)
|
|
78
|
+
{
|
|
79
|
+
database: context_value(context, :database),
|
|
80
|
+
adapter: context_value(context, :adapter),
|
|
81
|
+
emailVerification: redact_email_verification(options),
|
|
82
|
+
emailAndPassword: redact_email_and_password(options),
|
|
83
|
+
socialProviders: redact_social_providers(options),
|
|
84
|
+
plugins: redact_plugins(options),
|
|
85
|
+
user: redact_user(options),
|
|
86
|
+
verification: redact_verification(options),
|
|
87
|
+
session: redact_session(options),
|
|
88
|
+
account: redact_account(options),
|
|
89
|
+
hooks: redact_hooks(options),
|
|
90
|
+
secondaryStorage: redact_secondary_storage(options),
|
|
91
|
+
advanced: redact_advanced(options),
|
|
92
|
+
trustedOrigins: redact_trusted_origins(options),
|
|
93
|
+
rateLimit: redact_rate_limit(options),
|
|
94
|
+
onAPIError: redact_on_api_error(options),
|
|
95
|
+
logger: redact_logger(options),
|
|
96
|
+
databaseHooks: redact_database_hooks(options)
|
|
97
|
+
}
|
|
98
|
+
rescue
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
# Redaction primitives
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
# Boolean redaction: collapse any value into a strict
|
|
107
|
+
# `true`/`false`. Used for callable/secret leaves where the
|
|
108
|
+
# actual value must never reach the wire (Requirement 13.3).
|
|
109
|
+
#
|
|
110
|
+
# @param value [Object]
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def bool(value)
|
|
113
|
+
!!value
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Pass-through helper: emit the value as-is. Used for raw
|
|
117
|
+
# scalars that are safe to ship verbatim (timeouts, lengths,
|
|
118
|
+
# field maps, …).
|
|
119
|
+
#
|
|
120
|
+
# @param value [Object]
|
|
121
|
+
# @return [Object]
|
|
122
|
+
def raw(value)
|
|
123
|
+
value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Presence-aware boolean redaction. Returns `true` only when
|
|
127
|
+
# the value is non-`nil`, not `false`, and not the empty
|
|
128
|
+
# string. Mirrors upstream's `!!value && value !== ""`
|
|
129
|
+
# idiom for fields like `advanced.cookiePrefix` where a
|
|
130
|
+
# missing/empty value is meaningfully different from a set
|
|
131
|
+
# one.
|
|
132
|
+
#
|
|
133
|
+
# @param value [Object]
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
def bool_present(value)
|
|
136
|
+
!value.nil? && value != "" && value != false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Length helper. Returns the integer length of any
|
|
140
|
+
# `Array`-coercible input, with `nil` and non-array values
|
|
141
|
+
# treated as the empty list. Used for `trustedOrigins`,
|
|
142
|
+
# which is emitted as an integer count (never the contents).
|
|
143
|
+
#
|
|
144
|
+
# @param array [Array, nil, Object]
|
|
145
|
+
# @return [Integer]
|
|
146
|
+
def count(array)
|
|
147
|
+
Array(array).length
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
# Unified accessor
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
# Read a nested value from either a {BetterAuth::Configuration}
|
|
155
|
+
# instance or a raw options hash, using the same snake_case
|
|
156
|
+
# path. Symbol/string key shapes in nested hashes are both
|
|
157
|
+
# accepted.
|
|
158
|
+
#
|
|
159
|
+
# The path's first segment is treated as the
|
|
160
|
+
# {BetterAuth::Configuration} reader name (snake_case). When
|
|
161
|
+
# the source is a Configuration, the first segment is sent
|
|
162
|
+
# via `public_send`; the remainder of the path is descended
|
|
163
|
+
# into the returned value as if it were a hash. When the
|
|
164
|
+
# source is a Hash, every segment is looked up as a hash
|
|
165
|
+
# key, trying both symbol and string forms at each level.
|
|
166
|
+
#
|
|
167
|
+
# Any failure (a missing reader, a missing key, an
|
|
168
|
+
# intermediate non-hash value) returns `nil` so the redaction
|
|
169
|
+
# map can short-circuit cleanly without rescuing per-leaf.
|
|
170
|
+
#
|
|
171
|
+
# @example Configuration source
|
|
172
|
+
# cfg = BetterAuth::Configuration.new(
|
|
173
|
+
# secret: "0"*40,
|
|
174
|
+
# email_verification: { expires_in: 3600 }
|
|
175
|
+
# )
|
|
176
|
+
# AuthConfig.fetch_path(cfg, [:email_verification, :expires_in])
|
|
177
|
+
# # => 3600
|
|
178
|
+
#
|
|
179
|
+
# @example Raw hash source with mixed symbol/string keys
|
|
180
|
+
# opts = { "email_verification" => { send_verification_email: ->{} } }
|
|
181
|
+
# AuthConfig.fetch_path(opts, [:email_verification, :send_verification_email])
|
|
182
|
+
# # => #<Proc:...>
|
|
183
|
+
#
|
|
184
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
185
|
+
# @param path [Array<Symbol>] non-empty snake_case path. Each
|
|
186
|
+
# segment is matched against either a `Configuration`
|
|
187
|
+
# reader (first segment only) or a hash key (subsequent
|
|
188
|
+
# segments).
|
|
189
|
+
# @return [Object, nil] the value at `path`, or `nil` when
|
|
190
|
+
# any segment is missing or the source is `nil`.
|
|
191
|
+
def fetch_path(opts, path)
|
|
192
|
+
return nil if opts.nil?
|
|
193
|
+
return nil if path.nil? || path.empty?
|
|
194
|
+
|
|
195
|
+
head, *tail = path
|
|
196
|
+
current = read_root(opts, head)
|
|
197
|
+
return current if tail.empty?
|
|
198
|
+
|
|
199
|
+
tail.reduce(current) do |value, key|
|
|
200
|
+
break nil unless value.is_a?(Hash)
|
|
201
|
+
|
|
202
|
+
hash_lookup(value, key)
|
|
203
|
+
end
|
|
204
|
+
rescue
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# ------------------------------------------------------------------
|
|
209
|
+
# Per-section stubs (filled by 4.8 / 4.9 / 4.10 / 4.11)
|
|
210
|
+
# ------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
# Build the redacted `payload.config.emailVerification` hash.
|
|
213
|
+
#
|
|
214
|
+
# Every callable leaf is `bool`-redacted (Requirement 13.3) so
|
|
215
|
+
# the actual proc/lambda/object never reaches the wire. The
|
|
216
|
+
# only raw scalar in this section is `expiresIn`.
|
|
217
|
+
#
|
|
218
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
219
|
+
# @return [Hash{Symbol => Object}]
|
|
220
|
+
def redact_email_verification(opts)
|
|
221
|
+
{
|
|
222
|
+
sendVerificationEmail: bool(fetch_path(opts, [:email_verification, :send_verification_email])),
|
|
223
|
+
sendOnSignUp: bool(fetch_path(opts, [:email_verification, :send_on_sign_up])),
|
|
224
|
+
sendOnSignIn: bool(fetch_path(opts, [:email_verification, :send_on_sign_in])),
|
|
225
|
+
autoSignInAfterVerification: bool(fetch_path(opts, [:email_verification, :auto_sign_in_after_verification])),
|
|
226
|
+
expiresIn: raw(fetch_path(opts, [:email_verification, :expires_in])),
|
|
227
|
+
beforeEmailVerification: bool(fetch_path(opts, [:email_verification, :before_email_verification])),
|
|
228
|
+
afterEmailVerification: bool(fetch_path(opts, [:email_verification, :after_email_verification]))
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Build the redacted `payload.config.emailAndPassword` hash.
|
|
233
|
+
#
|
|
234
|
+
# All callables (`sendResetPassword`, `onPasswordReset`,
|
|
235
|
+
# `password.hash`, `password.verify`, …) are `bool`-redacted
|
|
236
|
+
# per Requirement 13.4. Numeric configuration scalars
|
|
237
|
+
# (`maxPasswordLength`, `minPasswordLength`,
|
|
238
|
+
# `resetPasswordTokenExpiresIn`) are emitted raw.
|
|
239
|
+
#
|
|
240
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
241
|
+
# @return [Hash{Symbol => Object}]
|
|
242
|
+
def redact_email_and_password(opts)
|
|
243
|
+
{
|
|
244
|
+
enabled: bool(fetch_path(opts, [:email_and_password, :enabled])),
|
|
245
|
+
disableSignUp: bool(fetch_path(opts, [:email_and_password, :disable_sign_up])),
|
|
246
|
+
requireEmailVerification: bool(fetch_path(opts, [:email_and_password, :require_email_verification])),
|
|
247
|
+
maxPasswordLength: raw(fetch_path(opts, [:email_and_password, :max_password_length])),
|
|
248
|
+
minPasswordLength: raw(fetch_path(opts, [:email_and_password, :min_password_length])),
|
|
249
|
+
sendResetPassword: bool(fetch_path(opts, [:email_and_password, :send_reset_password])),
|
|
250
|
+
resetPasswordTokenExpiresIn: raw(fetch_path(opts, [:email_and_password, :reset_password_token_expires_in])),
|
|
251
|
+
onPasswordReset: bool(fetch_path(opts, [:email_and_password, :on_password_reset])),
|
|
252
|
+
password: {
|
|
253
|
+
hash: bool(fetch_path(opts, [:email_and_password, :password, :hash])),
|
|
254
|
+
verify: bool(fetch_path(opts, [:email_and_password, :password, :verify]))
|
|
255
|
+
},
|
|
256
|
+
autoSignIn: bool(fetch_path(opts, [:email_and_password, :auto_sign_in])),
|
|
257
|
+
revokeSessionsOnPasswordReset: bool(fetch_path(opts, [:email_and_password, :revoke_sessions_on_password_reset]))
|
|
258
|
+
}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Build the redacted `payload.config.socialProviders` array.
|
|
262
|
+
#
|
|
263
|
+
# The Ruby port stores `social_providers` as a `Hash` keyed
|
|
264
|
+
# by provider id (`:github`, `:google`, …) where each value
|
|
265
|
+
# is the per-provider options hash. Upstream emits an
|
|
266
|
+
# `Array` of redacted-provider hashes, so we walk the source
|
|
267
|
+
# hash and rebuild the wire shape one entry at a time.
|
|
268
|
+
#
|
|
269
|
+
# Mapping of keys (Ruby snake_case → upstream camelCase):
|
|
270
|
+
# bool leaves (callable / presence indicators):
|
|
271
|
+
# map_profile_to_user → mapProfileToUser
|
|
272
|
+
# disable_default_scope → disableDefaultScope
|
|
273
|
+
# disable_id_token_sign_in → disableIdTokenSignIn
|
|
274
|
+
# get_user_info → getUserInfo
|
|
275
|
+
# override_user_info_on_sign_in → overrideUserInfoOnSignIn
|
|
276
|
+
# verify_id_token → verifyIdToken
|
|
277
|
+
# refresh_access_token → refreshAccessToken
|
|
278
|
+
# raw pass-through scalars:
|
|
279
|
+
# disable_implicit_sign_up → disableImplicitSignUp
|
|
280
|
+
# disable_sign_up → disableSignUp
|
|
281
|
+
# prompt → prompt
|
|
282
|
+
# scope → scope
|
|
283
|
+
#
|
|
284
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
285
|
+
# @return [Array<Hash{Symbol => Object}>]
|
|
286
|
+
def redact_social_providers(opts)
|
|
287
|
+
providers = fetch_path(opts, [:social_providers])
|
|
288
|
+
return [] unless providers.is_a?(Hash)
|
|
289
|
+
|
|
290
|
+
providers.map do |provider_id, raw_provider|
|
|
291
|
+
provider = raw_provider.is_a?(Hash) ? raw_provider : {}
|
|
292
|
+
{
|
|
293
|
+
id: provider_id.to_s,
|
|
294
|
+
mapProfileToUser: bool(provider[:map_profile_to_user]),
|
|
295
|
+
disableDefaultScope: bool(provider[:disable_default_scope]),
|
|
296
|
+
disableIdTokenSignIn: bool(provider[:disable_id_token_sign_in]),
|
|
297
|
+
disableImplicitSignUp: provider[:disable_implicit_sign_up],
|
|
298
|
+
disableSignUp: provider[:disable_sign_up],
|
|
299
|
+
getUserInfo: bool(provider[:get_user_info]),
|
|
300
|
+
overrideUserInfoOnSignIn: bool(provider[:override_user_info_on_sign_in]),
|
|
301
|
+
prompt: provider[:prompt],
|
|
302
|
+
verifyIdToken: bool(provider[:verify_id_token]),
|
|
303
|
+
scope: provider[:scope],
|
|
304
|
+
refreshAccessToken: bool(provider[:refresh_access_token])
|
|
305
|
+
}
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Build the redacted `payload.config.plugins` value.
|
|
310
|
+
#
|
|
311
|
+
# Upstream emits an array of plugin id strings, or `null`
|
|
312
|
+
# (Ruby `nil`) when no plugins are configured. We mirror
|
|
313
|
+
# that exactly: each configured plugin is asked for its
|
|
314
|
+
# `id`, the result is stringified, blanks (nil / empty) are
|
|
315
|
+
# dropped, and the empty-list case collapses to `nil`.
|
|
316
|
+
#
|
|
317
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
318
|
+
# @return [Array<String>, nil]
|
|
319
|
+
def redact_plugins(opts)
|
|
320
|
+
plugins = fetch_path(opts, [:plugins])
|
|
321
|
+
ids = Array(plugins).map { |plugin| plugin.respond_to?(:id) ? plugin.id.to_s : nil }
|
|
322
|
+
ids = ids.reject { |id| id.nil? || id.empty? }
|
|
323
|
+
ids.empty? ? nil : ids
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Build the redacted `payload.config.user` hash.
|
|
327
|
+
#
|
|
328
|
+
# Every leaf except `changeEmail.sendChangeEmailConfirmation`
|
|
329
|
+
# is a raw pass-through. The send-change-email confirmation
|
|
330
|
+
# callback is `bool`-redacted per Requirement 13.4 so the
|
|
331
|
+
# callable never reaches the wire.
|
|
332
|
+
#
|
|
333
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
334
|
+
# @return [Hash{Symbol => Object}]
|
|
335
|
+
def redact_user(opts)
|
|
336
|
+
{
|
|
337
|
+
modelName: raw(fetch_path(opts, [:user, :model_name])),
|
|
338
|
+
fields: raw(fetch_path(opts, [:user, :fields])),
|
|
339
|
+
additionalFields: raw(fetch_path(opts, [:user, :additional_fields])),
|
|
340
|
+
changeEmail: {
|
|
341
|
+
enabled: raw(fetch_path(opts, [:user, :change_email, :enabled])),
|
|
342
|
+
sendChangeEmailConfirmation: bool(fetch_path(opts, [:user, :change_email, :send_change_email_confirmation]))
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Build the redacted `payload.config.verification` hash. All
|
|
348
|
+
# leaves are raw pass-throughs (no callables in this section).
|
|
349
|
+
#
|
|
350
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
351
|
+
# @return [Hash{Symbol => Object}]
|
|
352
|
+
def redact_verification(opts)
|
|
353
|
+
{
|
|
354
|
+
modelName: raw(fetch_path(opts, [:verification, :model_name])),
|
|
355
|
+
disableCleanup: raw(fetch_path(opts, [:verification, :disable_cleanup])),
|
|
356
|
+
fields: raw(fetch_path(opts, [:verification, :fields]))
|
|
357
|
+
}
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Build the redacted `payload.config.session` hash. Every
|
|
361
|
+
# documented leaf is a raw pass-through; nested
|
|
362
|
+
# `cookieCache.*` keys are emitted as their own sub-hash
|
|
363
|
+
# mirroring upstream.
|
|
364
|
+
#
|
|
365
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
366
|
+
# @return [Hash{Symbol => Object}]
|
|
367
|
+
def redact_session(opts)
|
|
368
|
+
{
|
|
369
|
+
modelName: raw(fetch_path(opts, [:session, :model_name])),
|
|
370
|
+
additionalFields: raw(fetch_path(opts, [:session, :additional_fields])),
|
|
371
|
+
cookieCache: {
|
|
372
|
+
enabled: raw(fetch_path(opts, [:session, :cookie_cache, :enabled])),
|
|
373
|
+
maxAge: raw(fetch_path(opts, [:session, :cookie_cache, :max_age])),
|
|
374
|
+
strategy: raw(fetch_path(opts, [:session, :cookie_cache, :strategy]))
|
|
375
|
+
},
|
|
376
|
+
disableSessionRefresh: raw(fetch_path(opts, [:session, :disable_session_refresh])),
|
|
377
|
+
expiresIn: raw(fetch_path(opts, [:session, :expires_in])),
|
|
378
|
+
fields: raw(fetch_path(opts, [:session, :fields])),
|
|
379
|
+
freshAge: raw(fetch_path(opts, [:session, :fresh_age])),
|
|
380
|
+
preserveSessionInDatabase: raw(fetch_path(opts, [:session, :preserve_session_in_database])),
|
|
381
|
+
storeSessionInDatabase: raw(fetch_path(opts, [:session, :store_session_in_database])),
|
|
382
|
+
updateAge: raw(fetch_path(opts, [:session, :update_age]))
|
|
383
|
+
}
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Build the redacted `payload.config.account` hash. Every
|
|
387
|
+
# documented leaf is a raw pass-through; nested
|
|
388
|
+
# `accountLinking.*` keys are emitted as their own sub-hash
|
|
389
|
+
# mirroring upstream.
|
|
390
|
+
#
|
|
391
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
392
|
+
# @return [Hash{Symbol => Object}]
|
|
393
|
+
def redact_account(opts)
|
|
394
|
+
{
|
|
395
|
+
modelName: raw(fetch_path(opts, [:account, :model_name])),
|
|
396
|
+
fields: raw(fetch_path(opts, [:account, :fields])),
|
|
397
|
+
encryptOAuthTokens: raw(fetch_path(opts, [:account, :encrypt_oauth_tokens])),
|
|
398
|
+
updateAccountOnSignIn: raw(fetch_path(opts, [:account, :update_account_on_sign_in])),
|
|
399
|
+
accountLinking: {
|
|
400
|
+
enabled: raw(fetch_path(opts, [:account, :account_linking, :enabled])),
|
|
401
|
+
trustedProviders: raw(fetch_path(opts, [:account, :account_linking, :trusted_providers])),
|
|
402
|
+
updateUserInfoOnLink: raw(fetch_path(opts, [:account, :account_linking, :update_user_info_on_link])),
|
|
403
|
+
allowUnlinkingAll: raw(fetch_path(opts, [:account, :account_linking, :allow_unlinking_all]))
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Build the redacted `payload.config.hooks` hash.
|
|
409
|
+
#
|
|
410
|
+
# Both `before` and `after` may be a single proc, an array
|
|
411
|
+
# of procs, or `nil`. The redaction collapses any non-nil/
|
|
412
|
+
# non-false value into `true`, so callable references never
|
|
413
|
+
# leak (Requirement 13.4).
|
|
414
|
+
#
|
|
415
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
416
|
+
# @return [Hash{Symbol => Object}]
|
|
417
|
+
def redact_hooks(opts)
|
|
418
|
+
{
|
|
419
|
+
after: bool(fetch_path(opts, [:hooks, :after])),
|
|
420
|
+
before: bool(fetch_path(opts, [:hooks, :before]))
|
|
421
|
+
}
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Build the redacted `payload.config.secondaryStorage` value.
|
|
425
|
+
#
|
|
426
|
+
# Upstream emits `!!options.secondaryStorage`: a strict
|
|
427
|
+
# boolean indicating whether a secondary storage backend has
|
|
428
|
+
# been wired up, never the storage object itself
|
|
429
|
+
# (Requirement 13.4 — callable / object references must not
|
|
430
|
+
# reach the wire).
|
|
431
|
+
#
|
|
432
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
433
|
+
# @return [Boolean]
|
|
434
|
+
def redact_secondary_storage(opts)
|
|
435
|
+
bool(fetch_path(opts, [:secondary_storage]))
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Build the redacted `payload.config.advanced` hash.
|
|
439
|
+
#
|
|
440
|
+
# The shape mirrors upstream `getTelemetryAuthConfig`'s
|
|
441
|
+
# `advanced` block, including the rename from the Ruby
|
|
442
|
+
# source key `default_cookie_attributes` to the upstream
|
|
443
|
+
# wire key `cookieAttributes` (Requirement 13.7).
|
|
444
|
+
#
|
|
445
|
+
# The four boolean-redacted leaves protect host-identifying
|
|
446
|
+
# values from leaking onto the wire (Requirement 13.3 /
|
|
447
|
+
# 13.4):
|
|
448
|
+
#
|
|
449
|
+
# * `cookiePrefix` — the literal cookie name prefix.
|
|
450
|
+
# * `cookies` — the per-cookie configuration hash.
|
|
451
|
+
# * `crossSubDomainCookies.domain` — host-identifying
|
|
452
|
+
# domain string for cross-subdomain cookies.
|
|
453
|
+
# * `cookieAttributes.domain` — host-identifying domain
|
|
454
|
+
# string for the default cookie attributes.
|
|
455
|
+
#
|
|
456
|
+
# Every other leaf is a raw pass-through scalar.
|
|
457
|
+
#
|
|
458
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
459
|
+
# @return [Hash{Symbol => Object}]
|
|
460
|
+
def redact_advanced(opts)
|
|
461
|
+
{
|
|
462
|
+
cookiePrefix: bool(fetch_path(opts, [:advanced, :cookie_prefix])),
|
|
463
|
+
cookies: bool(fetch_path(opts, [:advanced, :cookies])),
|
|
464
|
+
crossSubDomainCookies: {
|
|
465
|
+
domain: bool(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :domain])),
|
|
466
|
+
enabled: raw(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :enabled])),
|
|
467
|
+
additionalCookies: raw(fetch_path(opts, [:advanced, :cross_sub_domain_cookies, :additional_cookies]))
|
|
468
|
+
},
|
|
469
|
+
database: {
|
|
470
|
+
generateId: raw(fetch_path(opts, [:advanced, :database, :generate_id])),
|
|
471
|
+
defaultFindManyLimit: raw(fetch_path(opts, [:advanced, :database, :default_find_many_limit]))
|
|
472
|
+
},
|
|
473
|
+
useSecureCookies: raw(fetch_path(opts, [:advanced, :use_secure_cookies])),
|
|
474
|
+
ipAddress: {
|
|
475
|
+
disableIpTracking: raw(fetch_path(opts, [:advanced, :ip_address, :disable_ip_tracking])),
|
|
476
|
+
ipAddressHeaders: raw(fetch_path(opts, [:advanced, :ip_address, :ip_address_headers]))
|
|
477
|
+
},
|
|
478
|
+
disableCSRFCheck: raw(fetch_path(opts, [:advanced, :disable_csrf_check])),
|
|
479
|
+
cookieAttributes: {
|
|
480
|
+
expires: raw(fetch_path(opts, [:advanced, :default_cookie_attributes, :expires])),
|
|
481
|
+
secure: raw(fetch_path(opts, [:advanced, :default_cookie_attributes, :secure])),
|
|
482
|
+
sameSite: raw(fetch_path(opts, [:advanced, :default_cookie_attributes, :same_site])),
|
|
483
|
+
domain: bool(fetch_path(opts, [:advanced, :default_cookie_attributes, :domain])),
|
|
484
|
+
path: raw(fetch_path(opts, [:advanced, :default_cookie_attributes, :path])),
|
|
485
|
+
httpOnly: raw(fetch_path(opts, [:advanced, :default_cookie_attributes, :http_only]))
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Build the redacted `payload.config.trustedOrigins` value.
|
|
491
|
+
#
|
|
492
|
+
# Upstream emits `options.trustedOrigins?.length`: an integer
|
|
493
|
+
# count of configured origins, or `nil` when the key is
|
|
494
|
+
# absent. We never emit the origin strings themselves, since
|
|
495
|
+
# they identify customer hosts (Requirement 13.7).
|
|
496
|
+
#
|
|
497
|
+
# The Ruby `Configuration#trusted_origins` reader normalizes
|
|
498
|
+
# the input into an array (folding in `base_url`,
|
|
499
|
+
# dynamic-base-url hosts, and the
|
|
500
|
+
# `BETTER_AUTH_TRUSTED_ORIGINS` env list); the count we emit
|
|
501
|
+
# matches whatever that normalization produced. When the
|
|
502
|
+
# source is a raw hash, we count the literal value at
|
|
503
|
+
# `:trusted_origins`.
|
|
504
|
+
#
|
|
505
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
506
|
+
# @return [Integer, nil]
|
|
507
|
+
def redact_trusted_origins(opts)
|
|
508
|
+
value = fetch_path(opts, [:trusted_origins])
|
|
509
|
+
return nil if value.nil?
|
|
510
|
+
|
|
511
|
+
count(value)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# Build the redacted `payload.config.rateLimit` hash.
|
|
515
|
+
#
|
|
516
|
+
# `customStorage` is callable in the upstream type and is
|
|
517
|
+
# therefore boolean-redacted (Requirement 13.4); every other
|
|
518
|
+
# leaf is a raw pass-through scalar.
|
|
519
|
+
#
|
|
520
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
521
|
+
# @return [Hash{Symbol => Object}]
|
|
522
|
+
def redact_rate_limit(opts)
|
|
523
|
+
{
|
|
524
|
+
storage: raw(fetch_path(opts, [:rate_limit, :storage])),
|
|
525
|
+
modelName: raw(fetch_path(opts, [:rate_limit, :model_name])),
|
|
526
|
+
window: raw(fetch_path(opts, [:rate_limit, :window])),
|
|
527
|
+
customStorage: bool(fetch_path(opts, [:rate_limit, :custom_storage])),
|
|
528
|
+
enabled: raw(fetch_path(opts, [:rate_limit, :enabled])),
|
|
529
|
+
max: raw(fetch_path(opts, [:rate_limit, :max]))
|
|
530
|
+
}
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Build the redacted `payload.config.onAPIError` hash.
|
|
534
|
+
#
|
|
535
|
+
# `onError` is callable in the upstream type and is
|
|
536
|
+
# therefore boolean-redacted (Requirement 13.4). `errorURL`
|
|
537
|
+
# and `throw` are raw pass-through scalars.
|
|
538
|
+
#
|
|
539
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
540
|
+
# @return [Hash{Symbol => Object}]
|
|
541
|
+
def redact_on_api_error(opts)
|
|
542
|
+
{
|
|
543
|
+
errorURL: raw(fetch_path(opts, [:on_api_error, :error_url])),
|
|
544
|
+
onError: bool(fetch_path(opts, [:on_api_error, :on_error])),
|
|
545
|
+
throw: raw(fetch_path(opts, [:on_api_error, :throw]))
|
|
546
|
+
}
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Build the redacted `payload.config.logger` hash.
|
|
550
|
+
#
|
|
551
|
+
# `log` is callable in the upstream type and is therefore
|
|
552
|
+
# boolean-redacted (Requirement 13.4). `disabled` and
|
|
553
|
+
# `level` are raw pass-through scalars.
|
|
554
|
+
#
|
|
555
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
556
|
+
# @return [Hash{Symbol => Object}]
|
|
557
|
+
def redact_logger(opts)
|
|
558
|
+
{
|
|
559
|
+
disabled: raw(fetch_path(opts, [:logger, :disabled])),
|
|
560
|
+
level: raw(fetch_path(opts, [:logger, :level])),
|
|
561
|
+
log: bool(fetch_path(opts, [:logger, :log]))
|
|
562
|
+
}
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Build the redacted `payload.config.databaseHooks` tree.
|
|
566
|
+
#
|
|
567
|
+
# The full upstream shape is a 4 × 2 × 2 nested tree:
|
|
568
|
+
#
|
|
569
|
+
# { user, session, account, verification } ×
|
|
570
|
+
# { create, update } ×
|
|
571
|
+
# { before, after }
|
|
572
|
+
#
|
|
573
|
+
# giving sixteen leaves total. Every leaf is a callable in
|
|
574
|
+
# the upstream type, so every leaf is boolean-redacted
|
|
575
|
+
# (Requirement 13.8). The full tree is always emitted with
|
|
576
|
+
# the same shape so downstream consumers can rely on the
|
|
577
|
+
# key set being stable; missing leaves collapse to `false`.
|
|
578
|
+
#
|
|
579
|
+
# @param opts [BetterAuth::Configuration, Hash, nil]
|
|
580
|
+
# @return [Hash{Symbol => Hash}]
|
|
581
|
+
def redact_database_hooks(opts)
|
|
582
|
+
DATABASE_HOOK_MODELS.each_with_object({}) do |model, result|
|
|
583
|
+
result[model] = DATABASE_HOOK_OPERATIONS.each_with_object({}) do |operation, ops|
|
|
584
|
+
ops[operation] = DATABASE_HOOK_PHASES.each_with_object({}) do |phase, phases|
|
|
585
|
+
phases[phase] = bool(fetch_path(opts, [:database_hooks, model, operation, phase]))
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
# ------------------------------------------------------------------
|
|
592
|
+
# Internal helpers
|
|
593
|
+
# ------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
# Read a single override key from the {NormalizedContext}
|
|
596
|
+
# surface, accepting either a {NormalizedContext} instance
|
|
597
|
+
# (the production path), a raw hash with snake_case or
|
|
598
|
+
# camelCase keys in symbol or string form (test seams), or
|
|
599
|
+
# `nil`. Returns the raw value when present, `nil`
|
|
600
|
+
# otherwise. Used to inject `payload[:database]` and
|
|
601
|
+
# `payload[:adapter]` from the call-site context override
|
|
602
|
+
# without going through the redaction map (Requirement
|
|
603
|
+
# 13.9 — context overrides are pass-through).
|
|
604
|
+
#
|
|
605
|
+
# @param context [Object, nil]
|
|
606
|
+
# @param key [Symbol] one of `:database` or `:adapter`.
|
|
607
|
+
# @return [Object, nil]
|
|
608
|
+
def context_value(context, key)
|
|
609
|
+
return nil if context.nil?
|
|
610
|
+
return context.public_send(key) if context.respond_to?(key)
|
|
611
|
+
return nil unless context.is_a?(Hash)
|
|
612
|
+
|
|
613
|
+
symbol_key = key.is_a?(Symbol) ? key : key.to_s.to_sym
|
|
614
|
+
return context[symbol_key] if context.key?(symbol_key)
|
|
615
|
+
|
|
616
|
+
string_key = key.to_s
|
|
617
|
+
return context[string_key] if context.key?(string_key)
|
|
618
|
+
|
|
619
|
+
nil
|
|
620
|
+
rescue
|
|
621
|
+
nil
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Read the root (first segment) of a `fetch_path` lookup.
|
|
625
|
+
# For a {BetterAuth::Configuration} we call the snake_case
|
|
626
|
+
# reader; for a Hash we look up the key under both symbol
|
|
627
|
+
# and string forms; for any other object we return `nil`.
|
|
628
|
+
#
|
|
629
|
+
# @param opts [BetterAuth::Configuration, Hash, Object]
|
|
630
|
+
# @param key [Symbol]
|
|
631
|
+
# @return [Object, nil]
|
|
632
|
+
def read_root(opts, key)
|
|
633
|
+
if defined?(::BetterAuth::Configuration) && opts.is_a?(::BetterAuth::Configuration)
|
|
634
|
+
return opts.public_send(key) if opts.respond_to?(key)
|
|
635
|
+
|
|
636
|
+
return nil
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
return hash_lookup(opts, key) if opts.is_a?(Hash)
|
|
640
|
+
|
|
641
|
+
nil
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Look up a key in a hash trying both symbol and string
|
|
645
|
+
# forms. Returns `nil` when neither shape contains the key.
|
|
646
|
+
#
|
|
647
|
+
# @param hash [Hash]
|
|
648
|
+
# @param key [Symbol, String]
|
|
649
|
+
# @return [Object, nil]
|
|
650
|
+
def hash_lookup(hash, key)
|
|
651
|
+
symbol_key = key.is_a?(Symbol) ? key : key.to_s.to_sym
|
|
652
|
+
return hash[symbol_key] if hash.key?(symbol_key)
|
|
653
|
+
|
|
654
|
+
string_key = key.to_s
|
|
655
|
+
return hash[string_key] if hash.key?(string_key)
|
|
656
|
+
|
|
657
|
+
nil
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
end
|