better_auth 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +15 -0
- data/lib/better_auth/adapters/internal_adapter.rb +5 -2
- data/lib/better_auth/adapters/memory.rb +57 -11
- data/lib/better_auth/adapters/sql.rb +27 -2
- data/lib/better_auth/api.rb +6 -1
- data/lib/better_auth/async.rb +70 -0
- data/lib/better_auth/database_hooks.rb +3 -3
- data/lib/better_auth/deprecate.rb +28 -0
- data/lib/better_auth/endpoint.rb +5 -2
- data/lib/better_auth/host.rb +166 -0
- data/lib/better_auth/instrumentation.rb +74 -0
- data/lib/better_auth/logger.rb +31 -0
- data/lib/better_auth/middleware/origin_check.rb +2 -2
- data/lib/better_auth/oauth2.rb +94 -0
- data/lib/better_auth/plugins/email_otp.rb +16 -5
- data/lib/better_auth/plugins/generic_oauth.rb +14 -28
- data/lib/better_auth/plugins/oauth_protocol.rb +171 -28
- data/lib/better_auth/plugins/organization/schema.rb +6 -0
- data/lib/better_auth/plugins/organization.rb +51 -20
- data/lib/better_auth/plugins/two_factor.rb +53 -18
- data/lib/better_auth/rate_limiter.rb +18 -0
- data/lib/better_auth/request_state.rb +44 -0
- data/lib/better_auth/routes/account.rb +16 -4
- data/lib/better_auth/routes/password.rb +2 -1
- data/lib/better_auth/routes/sign_in.rb +2 -0
- data/lib/better_auth/routes/sign_up.rb +8 -0
- data/lib/better_auth/routes/social.rb +30 -0
- data/lib/better_auth/routes/user.rb +9 -3
- data/lib/better_auth/session.rb +12 -1
- data/lib/better_auth/url_helpers.rb +195 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +8 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 38179f5800613263ca30525a3d878b91c2a5f9057f104b1f24cecbf72a0cc030
|
|
4
|
+
data.tar.gz: 9883d339ce2f1ab8f5a618da3708f4ab5bdde69a09fe98b48889b6069351c7bb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b0bdd97ab9d677df601b0eed5d387a09543743aee41d0243f7ed3981935c3391f3ae10dfc110fc41312a5a4b188f29581563f2c99154a8808ed3158cb47663ad
|
|
7
|
+
data.tar.gz: 6b9b76c6969e601e01506a2cd91a28abf13eeccf0446023fad7c58e8c95476f6110b7ac6f3979fc18705687589cff74748f839c685a8ccb791392d0d2e729c15
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-04-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added upstream-parity helpers for async execution, host resolution, instrumentation, request state, URL handling, OAuth2, deprecation warnings, and expanded route behavior.
|
|
15
|
+
- Added two-factor, OAuth protocol, social route, organization, admin, adapter, schema, and session parity coverage.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Aligned core auth, email OTP, generic OAuth, organization, two-factor, OAuth protocol, adapter, router, rate-limiter, logger, and middleware behavior more closely with upstream Better Auth.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed upstream parity gaps in organization handling, generic OAuth user info, email OTP sign-up, database schema behavior, and route/session edge cases.
|
|
24
|
+
|
|
10
25
|
## [0.3.0] - 2026-04-29
|
|
11
26
|
|
|
12
27
|
### Added
|
|
@@ -59,9 +59,12 @@ module BetterAuth
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def delete_user(user_id)
|
|
62
|
-
|
|
62
|
+
deleted = hooks.delete([{field: "id", value: user_id}], "user")
|
|
63
|
+
return false if deleted == false
|
|
64
|
+
|
|
63
65
|
hooks.delete_many([{field: "userId", value: user_id}], "account")
|
|
64
|
-
|
|
66
|
+
delete_sessions(user_id) if !secondary_storage || options.session[:store_session_in_database]
|
|
67
|
+
deleted
|
|
65
68
|
end
|
|
66
69
|
|
|
67
70
|
def create_session(user_id, dont_remember_me = false, override = nil, override_all = false, context = nil)
|
|
@@ -156,33 +156,79 @@ module BetterAuth
|
|
|
156
156
|
value = fetch_key(clause, :value)
|
|
157
157
|
operator = (fetch_key(clause, :operator) || "eq").to_s
|
|
158
158
|
current = record[field]
|
|
159
|
+
comparable = coerce_where_value(record, field, value, operator)
|
|
159
160
|
|
|
160
161
|
case operator
|
|
161
162
|
when "in"
|
|
162
|
-
Array(
|
|
163
|
+
Array(comparable).include?(current)
|
|
163
164
|
when "not_in"
|
|
164
|
-
!Array(
|
|
165
|
+
!Array(comparable).include?(current)
|
|
165
166
|
when "contains"
|
|
166
|
-
current.to_s.include?(
|
|
167
|
+
current.to_s.include?(comparable.to_s)
|
|
167
168
|
when "starts_with"
|
|
168
|
-
current.to_s.start_with?(
|
|
169
|
+
current.to_s.start_with?(comparable.to_s)
|
|
169
170
|
when "ends_with"
|
|
170
|
-
current.to_s.end_with?(
|
|
171
|
+
current.to_s.end_with?(comparable.to_s)
|
|
171
172
|
when "ne"
|
|
172
|
-
current !=
|
|
173
|
+
current != comparable
|
|
173
174
|
when "gt"
|
|
174
|
-
!
|
|
175
|
+
!comparable.nil? && current > comparable
|
|
175
176
|
when "gte"
|
|
176
|
-
!
|
|
177
|
+
!comparable.nil? && current >= comparable
|
|
177
178
|
when "lt"
|
|
178
|
-
!
|
|
179
|
+
!comparable.nil? && current < comparable
|
|
179
180
|
when "lte"
|
|
180
|
-
!
|
|
181
|
+
!comparable.nil? && current <= comparable
|
|
181
182
|
else
|
|
182
|
-
current ==
|
|
183
|
+
current == comparable
|
|
183
184
|
end
|
|
184
185
|
end
|
|
185
186
|
|
|
187
|
+
def coerce_where_value(record, field, value, operator)
|
|
188
|
+
attributes = schema_for_record_field(record, field)
|
|
189
|
+
return value unless attributes
|
|
190
|
+
return Array(value).map { |entry| coerce_scalar_where_value(entry, attributes) } if %w[in not_in].include?(operator)
|
|
191
|
+
|
|
192
|
+
coerce_scalar_where_value(value, attributes)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def schema_for_record_field(record, field)
|
|
196
|
+
db.each_key do |model|
|
|
197
|
+
fields = Schema.auth_tables(options)[model]&.fetch(:fields, nil)
|
|
198
|
+
next unless fields&.key?(field)
|
|
199
|
+
return fields[field] if table_for(model).include?(record)
|
|
200
|
+
end
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def coerce_scalar_where_value(value, attributes)
|
|
205
|
+
return value if value.nil?
|
|
206
|
+
|
|
207
|
+
case attributes[:type]
|
|
208
|
+
when "boolean"
|
|
209
|
+
return false if value == false || value == 0 || value.to_s.downcase == "false" || value.to_s == "0"
|
|
210
|
+
return true if value == true || value == 1 || value.to_s.downcase == "true" || value.to_s == "1"
|
|
211
|
+
when "number"
|
|
212
|
+
return coerce_number(value)
|
|
213
|
+
when "date"
|
|
214
|
+
return Time.parse(value) if value.is_a?(String)
|
|
215
|
+
when "number[]"
|
|
216
|
+
return Array(value).map { |entry| coerce_number(entry) }
|
|
217
|
+
when "string[]"
|
|
218
|
+
return Array(value).map(&:to_s)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
value
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def coerce_number(value)
|
|
225
|
+
return value unless value.is_a?(String)
|
|
226
|
+
return value.to_i if /\A-?\d+\z/.match?(value)
|
|
227
|
+
return value.to_f if /\A-?\d+\.\d+\z/.match?(value)
|
|
228
|
+
|
|
229
|
+
value
|
|
230
|
+
end
|
|
231
|
+
|
|
186
232
|
def sort_records(model, records, sort_by)
|
|
187
233
|
field = Schema.storage_key(fetch_key(sort_by, :field))
|
|
188
234
|
direction = fetch_key(sort_by, :direction).to_s
|
|
@@ -204,10 +204,11 @@ module BetterAuth
|
|
|
204
204
|
column = "#{quote(table_for(model))}.#{quote(storage_field(model, field))}"
|
|
205
205
|
operator = (fetch_key(clause, :operator) || "eq").to_s
|
|
206
206
|
value = fetch_key(clause, :value)
|
|
207
|
+
attributes = schema_for(model).fetch(:fields).fetch(field)
|
|
207
208
|
|
|
208
209
|
expression = case operator
|
|
209
210
|
when "in", "not_in"
|
|
210
|
-
values = Array(value)
|
|
211
|
+
values = Array(value).map { |entry| coerce_where_value(entry, attributes) }
|
|
211
212
|
placeholders = values.map do |entry|
|
|
212
213
|
params << entry
|
|
213
214
|
placeholder(params.length)
|
|
@@ -223,7 +224,7 @@ module BetterAuth
|
|
|
223
224
|
params << pattern
|
|
224
225
|
"#{column} LIKE #{placeholder(params.length)}"
|
|
225
226
|
else
|
|
226
|
-
params << value
|
|
227
|
+
params << coerce_where_value(value, attributes)
|
|
227
228
|
"#{column} #{sql_operator(operator)} #{placeholder(params.length)}"
|
|
228
229
|
end
|
|
229
230
|
|
|
@@ -385,6 +386,22 @@ module BetterAuth
|
|
|
385
386
|
value
|
|
386
387
|
end
|
|
387
388
|
|
|
389
|
+
def coerce_where_value(value, attributes)
|
|
390
|
+
return value if value.nil?
|
|
391
|
+
|
|
392
|
+
case attributes[:type]
|
|
393
|
+
when "boolean"
|
|
394
|
+
return coerce_value(false, attributes) if value == false || value == 0 || value.to_s.downcase == "false" || value.to_s == "0"
|
|
395
|
+
return coerce_value(true, attributes) if value == true || value == 1 || value.to_s.downcase == "true" || value.to_s == "1"
|
|
396
|
+
when "number"
|
|
397
|
+
return coerce_number(value)
|
|
398
|
+
when "date"
|
|
399
|
+
return Time.parse(value) if value.is_a?(String)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
coerce_value(value, attributes)
|
|
403
|
+
end
|
|
404
|
+
|
|
388
405
|
def coerce_output_value(value, attributes)
|
|
389
406
|
return value if value.nil?
|
|
390
407
|
return coerce_boolean(value) if attributes[:type] == "boolean"
|
|
@@ -412,6 +429,14 @@ module BetterAuth
|
|
|
412
429
|
value
|
|
413
430
|
end
|
|
414
431
|
|
|
432
|
+
def coerce_number(value)
|
|
433
|
+
return value unless value.is_a?(String)
|
|
434
|
+
return value.to_i if /\A-?\d+\z/.match?(value)
|
|
435
|
+
return value.to_f if /\A-?\d+\.\d+\z/.match?(value)
|
|
436
|
+
|
|
437
|
+
value
|
|
438
|
+
end
|
|
439
|
+
|
|
415
440
|
def stringify_keys(data)
|
|
416
441
|
data.each_with_object({}) do |(key, value), result|
|
|
417
442
|
result[storage_key(key)] = value
|
data/lib/better_auth/api.rb
CHANGED
|
@@ -211,7 +211,12 @@ module BetterAuth
|
|
|
211
211
|
return value unless value.is_a?(Hash)
|
|
212
212
|
|
|
213
213
|
value.each_with_object({}) do |(key, object_value), result|
|
|
214
|
-
|
|
214
|
+
normalized_key = normalize_key(key)
|
|
215
|
+
result[normalized_key] = if normalized_key == :metadata
|
|
216
|
+
object_value
|
|
217
|
+
else
|
|
218
|
+
object_value.is_a?(Hash) ? symbolize_keys(object_value) : object_value
|
|
219
|
+
end
|
|
215
220
|
end
|
|
216
221
|
end
|
|
217
222
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Async
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def map_concurrent(items, concurrency:, &mapper)
|
|
8
|
+
list = items.to_a
|
|
9
|
+
return [] if list.empty?
|
|
10
|
+
|
|
11
|
+
width = normalized_concurrency(concurrency, list.length)
|
|
12
|
+
results = Array.new(list.length)
|
|
13
|
+
next_index = 0
|
|
14
|
+
first_error = nil
|
|
15
|
+
mutex = Mutex.new
|
|
16
|
+
status = Queue.new
|
|
17
|
+
|
|
18
|
+
workers = Array.new(width) do
|
|
19
|
+
Thread.new do
|
|
20
|
+
loop do
|
|
21
|
+
index = mutex.synchronize do
|
|
22
|
+
break if first_error || next_index >= list.length
|
|
23
|
+
|
|
24
|
+
current = next_index
|
|
25
|
+
next_index += 1
|
|
26
|
+
current
|
|
27
|
+
end
|
|
28
|
+
break unless index
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
results[index] = mapper.call(list[index], index)
|
|
32
|
+
rescue => error
|
|
33
|
+
mutex.synchronize { first_error ||= error }
|
|
34
|
+
status << [:error, error]
|
|
35
|
+
break
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
ensure
|
|
39
|
+
status << [:done, nil]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
done = 0
|
|
44
|
+
while done < workers.length
|
|
45
|
+
type, error = status.pop
|
|
46
|
+
if type == :error
|
|
47
|
+
workers.each { |worker| worker.kill if worker.alive? }
|
|
48
|
+
workers.each(&:join)
|
|
49
|
+
raise error
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
done += 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
raise first_error if first_error
|
|
56
|
+
|
|
57
|
+
results
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def normalized_concurrency(concurrency, item_count)
|
|
61
|
+
raw = begin
|
|
62
|
+
Float(concurrency).floor
|
|
63
|
+
rescue ArgumentError, TypeError
|
|
64
|
+
1
|
|
65
|
+
end
|
|
66
|
+
raw = 1 if raw < 1
|
|
67
|
+
[raw, item_count].min
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -35,9 +35,9 @@ module BetterAuth
|
|
|
35
35
|
|
|
36
36
|
def delete(where, model, custom: nil, context: nil)
|
|
37
37
|
entity = adapter.find_one(model: model, where: where)
|
|
38
|
-
return
|
|
38
|
+
return nil unless entity
|
|
39
39
|
|
|
40
|
-
return
|
|
40
|
+
return false if before_hooks(model, :delete).any? { |hook| hook.call(entity, context) == false }
|
|
41
41
|
|
|
42
42
|
deleted = custom ? custom.call(where) : adapter.delete(model: model, where: where)
|
|
43
43
|
after_hooks(model, :delete).each { |hook| hook.call(entity, context) }
|
|
@@ -47,7 +47,7 @@ module BetterAuth
|
|
|
47
47
|
def delete_many(where, model, custom: nil, context: nil)
|
|
48
48
|
entities = adapter.find_many(model: model, where: where)
|
|
49
49
|
entities.each do |entity|
|
|
50
|
-
return
|
|
50
|
+
return false if before_hooks(model, :delete).any? { |hook| hook.call(entity, context) == false }
|
|
51
51
|
end
|
|
52
52
|
deleted = custom ? custom.call(where) : adapter.delete_many(model: model, where: where)
|
|
53
53
|
entities.each { |entity| after_hooks(model, :delete).each { |hook| hook.call(entity, context) } }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Deprecate
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def wrap(message, logger: nil, &block)
|
|
8
|
+
warned = false
|
|
9
|
+
proc do |*args, **kwargs|
|
|
10
|
+
unless warned
|
|
11
|
+
warn_once("[Deprecation] #{message}", logger)
|
|
12
|
+
warned = true
|
|
13
|
+
end
|
|
14
|
+
kwargs.empty? ? block.call(*args) : block.call(*args, **kwargs)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def warn_once(message, logger)
|
|
19
|
+
if logger.respond_to?(:call)
|
|
20
|
+
logger.call(message)
|
|
21
|
+
elsif logger.respond_to?(:warn)
|
|
22
|
+
logger.warn(message)
|
|
23
|
+
else
|
|
24
|
+
warn(message)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/better_auth/endpoint.rb
CHANGED
|
@@ -7,16 +7,18 @@ module BetterAuth
|
|
|
7
7
|
attr_reader :path,
|
|
8
8
|
:body_schema,
|
|
9
9
|
:query_schema,
|
|
10
|
+
:params_schema,
|
|
10
11
|
:headers_schema,
|
|
11
12
|
:metadata,
|
|
12
13
|
:use,
|
|
13
14
|
:handler
|
|
14
15
|
|
|
15
|
-
def initialize(path: nil, method: nil, body_schema: nil, query_schema: nil, headers_schema: nil, metadata: {}, use: [], &handler)
|
|
16
|
+
def initialize(path: nil, method: nil, body_schema: nil, query_schema: nil, params_schema: nil, headers_schema: nil, metadata: {}, use: [], &handler)
|
|
16
17
|
@path = path
|
|
17
18
|
@methods = Array(method || "*").map { |value| value.to_s.upcase }
|
|
18
19
|
@body_schema = body_schema
|
|
19
20
|
@query_schema = query_schema
|
|
21
|
+
@params_schema = params_schema
|
|
20
22
|
@headers_schema = headers_schema
|
|
21
23
|
@metadata = metadata || {}
|
|
22
24
|
@use = Array(use)
|
|
@@ -47,6 +49,7 @@ module BetterAuth
|
|
|
47
49
|
def apply_schemas!(context)
|
|
48
50
|
context.body = validate_schema(:body, body_schema, context.body)
|
|
49
51
|
context.query = validate_schema(:query, query_schema, context.query)
|
|
52
|
+
context.params = validate_schema(:params, params_schema, context.params)
|
|
50
53
|
context.headers = context.send(:normalize_headers, validate_schema(:headers, headers_schema, context.headers))
|
|
51
54
|
end
|
|
52
55
|
|
|
@@ -136,7 +139,7 @@ module BetterAuth
|
|
|
136
139
|
return @raw_response if raw_response?
|
|
137
140
|
|
|
138
141
|
body = if response.nil?
|
|
139
|
-
[
|
|
142
|
+
[JSON.generate(nil)]
|
|
140
143
|
elsif response.is_a?(String)
|
|
141
144
|
[response]
|
|
142
145
|
else
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
|
|
5
|
+
module BetterAuth
|
|
6
|
+
module Host
|
|
7
|
+
CLOUD_METADATA_HOSTS = [
|
|
8
|
+
"metadata.google.internal",
|
|
9
|
+
"metadata.goog",
|
|
10
|
+
"metadata",
|
|
11
|
+
"instance-data",
|
|
12
|
+
"instance-data.ec2.internal"
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def classify_host(host)
|
|
18
|
+
canonical_input = normalize_input(host)
|
|
19
|
+
lowered = canonical_input.downcase
|
|
20
|
+
return {kind: :reserved, literal: :fqdn, canonical: ""} if lowered.empty?
|
|
21
|
+
|
|
22
|
+
address = parse_ip(lowered)
|
|
23
|
+
unless address
|
|
24
|
+
return {kind: :localhost, literal: :fqdn, canonical: lowered} if lowered == "localhost" || lowered.end_with?(".localhost")
|
|
25
|
+
return {kind: :cloud_metadata, literal: :fqdn, canonical: lowered} if CLOUD_METADATA_HOSTS.include?(lowered)
|
|
26
|
+
|
|
27
|
+
return {kind: :public, literal: :fqdn, canonical: lowered}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
native = address.respond_to?(:native) ? address.native : address
|
|
31
|
+
if native.ipv4?
|
|
32
|
+
canonical = native.to_s
|
|
33
|
+
return {kind: classify_ipv4(canonical), literal: :ipv4, canonical: canonical}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
canonical = expanded_ipv6(native)
|
|
37
|
+
{kind: classify_ipv6(canonical), literal: :ipv6, canonical: canonical}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def loopback_ip?(host)
|
|
41
|
+
classify_host(host)[:kind] == :loopback
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def loopback_host?(host)
|
|
45
|
+
[:loopback, :localhost].include?(classify_host(host)[:kind])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def public_routable_host?(host)
|
|
49
|
+
classify_host(host)[:kind] == :public
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalize_input(host)
|
|
53
|
+
value = host.to_s.strip
|
|
54
|
+
value = strip_port(value)
|
|
55
|
+
value = value[1...-1] if value.start_with?("[") && value.end_with?("]")
|
|
56
|
+
value = value.split("%", 2).first || ""
|
|
57
|
+
value.gsub(/\.+\z/, "")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def strip_port(host)
|
|
61
|
+
if host.start_with?("[")
|
|
62
|
+
closing = host.index("]")
|
|
63
|
+
return host unless closing
|
|
64
|
+
|
|
65
|
+
return host[0..closing] if host[(closing + 1)..]&.match?(/\A:\d+\z/)
|
|
66
|
+
return host
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
first_colon = host.index(":")
|
|
70
|
+
return host unless first_colon
|
|
71
|
+
return host if host.index(":", first_colon + 1)
|
|
72
|
+
|
|
73
|
+
host[0...first_colon]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_ip(host)
|
|
77
|
+
IPAddr.new(host)
|
|
78
|
+
rescue ArgumentError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def classify_ipv4(ip)
|
|
83
|
+
return :unspecified if ip == "0.0.0.0"
|
|
84
|
+
return :broadcast if ip == "255.255.255.255"
|
|
85
|
+
|
|
86
|
+
value = ipv4_to_i(ip)
|
|
87
|
+
return :loopback if ipv4_range?(value, "127.0.0.0", 8)
|
|
88
|
+
return :private if ipv4_range?(value, "10.0.0.0", 8)
|
|
89
|
+
return :private if ipv4_range?(value, "172.16.0.0", 12)
|
|
90
|
+
return :private if ipv4_range?(value, "192.168.0.0", 16)
|
|
91
|
+
return :link_local if ipv4_range?(value, "169.254.0.0", 16)
|
|
92
|
+
return :shared_address_space if ipv4_range?(value, "100.64.0.0", 10)
|
|
93
|
+
return :documentation if ipv4_range?(value, "192.0.2.0", 24)
|
|
94
|
+
return :documentation if ipv4_range?(value, "198.51.100.0", 24)
|
|
95
|
+
return :documentation if ipv4_range?(value, "203.0.113.0", 24)
|
|
96
|
+
return :benchmarking if ipv4_range?(value, "198.18.0.0", 15)
|
|
97
|
+
return :multicast if ipv4_range?(value, "224.0.0.0", 4)
|
|
98
|
+
return :reserved if ipv4_range?(value, "0.0.0.0", 8)
|
|
99
|
+
return :reserved if ipv4_range?(value, "192.0.0.0", 24)
|
|
100
|
+
return :reserved if ipv4_range?(value, "240.0.0.0", 4)
|
|
101
|
+
|
|
102
|
+
:public
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ipv4_to_i(ip)
|
|
106
|
+
ip.split(".").map(&:to_i).reduce(0) { |sum, part| (sum << 8) + part }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def ipv4_range?(value, prefix, length)
|
|
110
|
+
mask = (length == 32) ? 0xffffffff : ((0xffffffff << (32 - length)) & 0xffffffff)
|
|
111
|
+
(value & mask) == (ipv4_to_i(prefix) & mask)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def classify_ipv6(expanded)
|
|
115
|
+
return :unspecified if expanded == "0000:0000:0000:0000:0000:0000:0000:0000"
|
|
116
|
+
return :loopback if expanded == "0000:0000:0000:0000:0000:0000:0000:0001"
|
|
117
|
+
|
|
118
|
+
first_byte = expanded[0, 2].to_i(16)
|
|
119
|
+
second_byte = expanded[2, 2].to_i(16)
|
|
120
|
+
|
|
121
|
+
return :multicast if first_byte == 0xff
|
|
122
|
+
return :link_local if first_byte == 0xfe && (second_byte & 0xc0) == 0x80
|
|
123
|
+
return :private if (first_byte & 0xfe) == 0xfc
|
|
124
|
+
return :documentation if expanded.start_with?("2001:0db8:")
|
|
125
|
+
|
|
126
|
+
if expanded.start_with?("2002:")
|
|
127
|
+
embedded = embedded_ipv4(expanded, 1)
|
|
128
|
+
return (classify_ipv4(embedded) == :public) ? :public : :reserved if embedded
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if expanded.start_with?("0064:ff9b:0000:0000:0000:0000:")
|
|
132
|
+
embedded = embedded_ipv4(expanded, 6)
|
|
133
|
+
return :reserved if embedded
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if expanded.start_with?("2001:0000:")
|
|
137
|
+
embedded = embedded_ipv4(expanded, 6, xor: true)
|
|
138
|
+
return :reserved if embedded
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
return :reserved if expanded.start_with?("0100:0000:0000:0000:")
|
|
142
|
+
|
|
143
|
+
:public
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def embedded_ipv4(expanded, start_group, xor: false)
|
|
147
|
+
groups = expanded.split(":")
|
|
148
|
+
combined = (groups.fetch(start_group).to_i(16) << 16) | groups.fetch(start_group + 1).to_i(16)
|
|
149
|
+
combined ^= 0xffffffff if xor
|
|
150
|
+
[
|
|
151
|
+
(combined >> 24) & 0xff,
|
|
152
|
+
(combined >> 16) & 0xff,
|
|
153
|
+
(combined >> 8) & 0xff,
|
|
154
|
+
combined & 0xff
|
|
155
|
+
].join(".")
|
|
156
|
+
rescue IndexError
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def expanded_ipv6(address)
|
|
161
|
+
address.hton.bytes.each_slice(2).map do |high, low|
|
|
162
|
+
((high << 8) + low).to_s(16).rjust(4, "0")
|
|
163
|
+
end.join(":")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Instrumentation
|
|
5
|
+
module SpanStatusCode
|
|
6
|
+
UNSET = 0
|
|
7
|
+
OK = 1
|
|
8
|
+
ERROR = 2
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class NoopSpan
|
|
12
|
+
def set_attribute(_key, _value)
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def set_attributes(_attributes)
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def record_exception(_error)
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def set_status(_status)
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add_event(_name, _attributes = nil)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def end
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class NoopTracer
|
|
38
|
+
def start_active_span(_name, attributes: {}, &block)
|
|
39
|
+
span = NoopSpan.new
|
|
40
|
+
return span unless block
|
|
41
|
+
|
|
42
|
+
block.call(span)
|
|
43
|
+
ensure
|
|
44
|
+
span&.end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Trace
|
|
49
|
+
def get_tracer(_name = "better-auth")
|
|
50
|
+
NoopTracer.new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_active_span
|
|
54
|
+
NoopSpan.new
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
module_function
|
|
59
|
+
|
|
60
|
+
def trace
|
|
61
|
+
@trace ||= Trace.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def with_span(name, attributes: {}, &block)
|
|
65
|
+
trace.get_tracer("better-auth").start_active_span(name, attributes: attributes) do |span|
|
|
66
|
+
block.call(span)
|
|
67
|
+
rescue => error
|
|
68
|
+
span.record_exception(error)
|
|
69
|
+
span.set_status(SpanStatusCode::ERROR)
|
|
70
|
+
raise
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Logger
|
|
5
|
+
LEVELS = [:debug, :info, :success, :warn, :error].freeze
|
|
6
|
+
|
|
7
|
+
Internal = Struct.new(:level, :disabled, :handler, keyword_init: true) do
|
|
8
|
+
LEVELS.each do |log_level|
|
|
9
|
+
define_method(log_level) do |message, *args|
|
|
10
|
+
return if disabled || !Logger.should_publish?(level, log_level)
|
|
11
|
+
|
|
12
|
+
if handler
|
|
13
|
+
handler.call((log_level == :success) ? :info : log_level, message, *args)
|
|
14
|
+
else
|
|
15
|
+
Kernel.warn("#{log_level.upcase} [Better Auth]: #{message}")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def should_publish?(current_log_level, log_level)
|
|
24
|
+
LEVELS.index(log_level.to_sym).to_i >= LEVELS.index(current_log_level.to_sym).to_i
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create(level: :warn, disabled: false, log: nil, **)
|
|
28
|
+
Internal.new(level: level.to_sym, disabled: disabled, handler: log)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -14,7 +14,7 @@ module BetterAuth
|
|
|
14
14
|
|
|
15
15
|
validate_origin(endpoint_context)
|
|
16
16
|
validate_fetch_metadata(endpoint_context)
|
|
17
|
-
return if skip_origin_check?(endpoint_context)
|
|
17
|
+
return if skip_origin_check?(endpoint_context) || skip_origin_path?(endpoint_context)
|
|
18
18
|
|
|
19
19
|
validate_callback_urls(endpoint_context)
|
|
20
20
|
nil
|
|
@@ -87,7 +87,7 @@ module BetterAuth
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
def skip_origin_check?(endpoint_context)
|
|
90
|
-
|
|
90
|
+
endpoint_context.context.options.advanced[:disable_origin_check] == true
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def skip_csrf_for_backward_compat?(endpoint_context)
|