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.
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BetterAuth
6
+ module URLHelpers
7
+ module_function
8
+
9
+ def valid_proxy_header?(header, type)
10
+ value = header.to_s
11
+ return false if value.strip.empty?
12
+
13
+ case type.to_sym
14
+ when :proto
15
+ ["http", "https"].include?(value)
16
+ when :host
17
+ return false if value.match?(/\.\.|\0|\s|\A[.]|[<>'"]|javascript:|file:|data:/i)
18
+ return false if value.match?(%r{[/\\]})
19
+
20
+ patterns = [
21
+ /\A[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(:[0-9]{1,5})?\z/,
22
+ /\A(\d{1,3}\.){3}\d{1,3}(:[0-9]{1,5})?\z/,
23
+ /\A\[[0-9a-fA-F:]+\](:[0-9]{1,5})?\z/,
24
+ /\Alocalhost(:[0-9]{1,5})?\z/i
25
+ ]
26
+ patterns.any? { |pattern| value.match?(pattern) } && valid_port?(value)
27
+ else
28
+ false
29
+ end
30
+ end
31
+
32
+ def matches_host_pattern?(host, pattern)
33
+ return false if host.to_s.empty? || pattern.to_s.empty?
34
+
35
+ normalized_host = normalize_host_pattern_value(host)
36
+ normalized_pattern = normalize_host_pattern_value(pattern)
37
+ regex = Regexp.escape(normalized_pattern)
38
+ .gsub("\\*", ".*")
39
+ .gsub("\\?", ".")
40
+ !!normalized_host.match?(/\A#{regex}\z/i)
41
+ end
42
+
43
+ def host_from_source(source, trusted_proxy_headers: false)
44
+ headers = headers_from_source(source)
45
+ if trusted_proxy_headers
46
+ forwarded_host = header_value(headers, "x-forwarded-host")
47
+ return forwarded_host if forwarded_host && valid_proxy_header?(forwarded_host, :host)
48
+ end
49
+
50
+ host = header_value(headers, "host")
51
+ return host if host && valid_proxy_header?(host, :host)
52
+
53
+ uri_host(source_url(source))
54
+ end
55
+
56
+ def protocol_from_source(source, config_protocol: nil, trusted_proxy_headers: false)
57
+ return config_protocol if ["http", "https"].include?(config_protocol)
58
+
59
+ headers = headers_from_source(source)
60
+ if trusted_proxy_headers
61
+ forwarded_proto = header_value(headers, "x-forwarded-proto")
62
+ return forwarded_proto if forwarded_proto && valid_proxy_header?(forwarded_proto, :proto)
63
+ end
64
+
65
+ protocol = uri_scheme(source_url(source))
66
+ return protocol if ["http", "https"].include?(protocol)
67
+
68
+ host = host_from_source(source, trusted_proxy_headers: trusted_proxy_headers)
69
+ return "http" if host && loopback_for_dev_scheme?(host)
70
+
71
+ "https"
72
+ end
73
+
74
+ def resolve_base_url(config, base_path, source = nil, load_env: true, trusted_proxy_headers: false)
75
+ if dynamic_config?(config)
76
+ return resolve_dynamic_base_url(config, source, base_path, trusted_proxy_headers: trusted_proxy_headers) if source
77
+ return with_path(config[:fallback] || config["fallback"], base_path) if config[:fallback] || config["fallback"]
78
+
79
+ return env_base_url(base_path) if load_env
80
+ return nil
81
+ end
82
+
83
+ return with_path(config, base_path) if config.is_a?(String)
84
+ return env_base_url(base_path) if load_env
85
+ return with_path(origin(source_url(source)), base_path) if source
86
+
87
+ nil
88
+ end
89
+
90
+ def resolve_dynamic_base_url(config, source, base_path, trusted_proxy_headers: false)
91
+ host = host_from_source(source, trusted_proxy_headers: trusted_proxy_headers)
92
+ fallback = config[:fallback] || config["fallback"]
93
+ raise Error, "Could not determine host from request headers. Please provide a fallback URL in your baseURL config." unless host || fallback
94
+
95
+ allowed_hosts = config[:allowed_hosts] || config["allowed_hosts"] || config[:allowedHosts] || config["allowedHosts"] || []
96
+ if host && allowed_hosts.any? { |pattern| matches_host_pattern?(host, pattern) }
97
+ protocol = protocol_from_source(source, config_protocol: config[:protocol] || config["protocol"], trusted_proxy_headers: trusted_proxy_headers)
98
+ return with_path("#{protocol}://#{host}", base_path)
99
+ end
100
+
101
+ return with_path(fallback, base_path) if fallback
102
+
103
+ raise Error, "Host \"#{host}\" is not in the allowed hosts list."
104
+ end
105
+
106
+ def with_path(url, path = "/api/auth")
107
+ parsed = URI.parse(url.to_s)
108
+ raise Error, "Invalid base URL: #{url}. URL must include 'http://' or 'https://'" unless ["http", "https"].include?(parsed.scheme)
109
+
110
+ current_path = parsed.path.to_s.gsub(%r{/+\z}, "")
111
+ return url.to_s if !current_path.empty? && current_path != "/"
112
+
113
+ trimmed = url.to_s.gsub(%r{/+\z}, "")
114
+ return trimmed if path.to_s.empty? || path == "/"
115
+
116
+ suffix = path.start_with?("/") ? path : "/#{path}"
117
+ "#{trimmed}#{suffix}"
118
+ rescue URI::InvalidURIError
119
+ raise Error, "Invalid base URL: #{url}. Please provide a valid base URL."
120
+ end
121
+
122
+ def origin(url)
123
+ parsed = URI.parse(url.to_s)
124
+ return nil unless ["http", "https"].include?(parsed.scheme)
125
+
126
+ port = parsed.port
127
+ default_port = (parsed.scheme == "http" && port == 80) || (parsed.scheme == "https" && port == 443)
128
+ default_port ? "#{parsed.scheme}://#{parsed.host}" : "#{parsed.scheme}://#{parsed.host}:#{port}"
129
+ rescue URI::InvalidURIError
130
+ nil
131
+ end
132
+
133
+ def uri_host(url)
134
+ parsed = URI.parse(url.to_s)
135
+ return nil unless parsed.host
136
+
137
+ default_port = (parsed.scheme == "http" && parsed.port == 80) || (parsed.scheme == "https" && parsed.port == 443)
138
+ default_port ? parsed.host : "#{parsed.host}:#{parsed.port}"
139
+ rescue URI::InvalidURIError
140
+ nil
141
+ end
142
+
143
+ def uri_scheme(url)
144
+ URI.parse(url.to_s).scheme
145
+ rescue URI::InvalidURIError
146
+ nil
147
+ end
148
+
149
+ def normalize_host_pattern_value(value)
150
+ value.to_s.sub(%r{\Ahttps?://}i, "").split("/").first.to_s.downcase
151
+ end
152
+
153
+ def headers_from_source(source)
154
+ return {} unless source
155
+ return source.headers if source.respond_to?(:headers)
156
+ return source if source.is_a?(Hash)
157
+
158
+ {}
159
+ end
160
+
161
+ def header_value(headers, key)
162
+ if headers.respond_to?(:get)
163
+ headers.get(key)
164
+ else
165
+ headers[key] || headers[key.to_s] || headers[key.to_s.downcase] || headers[key.to_s.upcase] || headers[key.tr("-", "_").upcase]
166
+ end
167
+ end
168
+
169
+ def source_url(source)
170
+ source.url if source.respond_to?(:url)
171
+ end
172
+
173
+ def dynamic_config?(config)
174
+ config.is_a?(Hash) && (config.key?(:allowed_hosts) || config.key?("allowed_hosts") || config.key?(:allowedHosts) || config.key?("allowedHosts"))
175
+ end
176
+
177
+ def env_base_url(base_path)
178
+ url = ENV["BETTER_AUTH_URL"] || ENV["NEXT_PUBLIC_BETTER_AUTH_URL"] || ENV["PUBLIC_BETTER_AUTH_URL"] || ENV["NUXT_PUBLIC_BETTER_AUTH_URL"] || ENV["NUXT_PUBLIC_AUTH_URL"]
179
+ url ||= ENV["BASE_URL"] if ENV["BASE_URL"] && ENV["BASE_URL"] != "/"
180
+ url ? with_path(url, base_path) : nil
181
+ end
182
+
183
+ def loopback_for_dev_scheme?(host)
184
+ hostname = host.to_s.sub(/:\d+\z/, "").sub(/\A\[/, "").sub(/\]\z/, "").downcase
185
+ hostname == "localhost" || hostname.end_with?(".localhost") || hostname == "::1" || hostname.start_with?("127.")
186
+ end
187
+
188
+ def valid_port?(host)
189
+ port = host[/:(\d{1,5})\z/, 1]
190
+ return true unless port
191
+
192
+ port.to_i.between?(1, 65_535)
193
+ end
194
+ end
195
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BetterAuth
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/better_auth.rb CHANGED
@@ -5,6 +5,14 @@ require_relative "better_auth/core"
5
5
  require_relative "better_auth/error"
6
6
  require_relative "better_auth/api_error"
7
7
  require_relative "better_auth/crypto"
8
+ require_relative "better_auth/host"
9
+ require_relative "better_auth/url_helpers"
10
+ require_relative "better_auth/request_state"
11
+ require_relative "better_auth/async"
12
+ require_relative "better_auth/deprecate"
13
+ require_relative "better_auth/logger"
14
+ require_relative "better_auth/instrumentation"
15
+ require_relative "better_auth/oauth2"
8
16
  require_relative "better_auth/password"
9
17
  require_relative "better_auth/plugin"
10
18
  require_relative "better_auth/configuration"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Sala
@@ -268,6 +268,7 @@ files:
268
268
  - lib/better_auth/adapters/sqlite.rb
269
269
  - lib/better_auth/api.rb
270
270
  - lib/better_auth/api_error.rb
271
+ - lib/better_auth/async.rb
271
272
  - lib/better_auth/auth.rb
272
273
  - lib/better_auth/configuration.rb
273
274
  - lib/better_auth/context.rb
@@ -276,9 +277,14 @@ files:
276
277
  - lib/better_auth/crypto.rb
277
278
  - lib/better_auth/crypto/jwe.rb
278
279
  - lib/better_auth/database_hooks.rb
280
+ - lib/better_auth/deprecate.rb
279
281
  - lib/better_auth/endpoint.rb
280
282
  - lib/better_auth/error.rb
283
+ - lib/better_auth/host.rb
284
+ - lib/better_auth/instrumentation.rb
285
+ - lib/better_auth/logger.rb
281
286
  - lib/better_auth/middleware/origin_check.rb
287
+ - lib/better_auth/oauth2.rb
282
288
  - lib/better_auth/password.rb
283
289
  - lib/better_auth/plugin.rb
284
290
  - lib/better_auth/plugin_context.rb
@@ -322,6 +328,7 @@ files:
322
328
  - lib/better_auth/plugins/username.rb
323
329
  - lib/better_auth/rate_limiter.rb
324
330
  - lib/better_auth/request_ip.rb
331
+ - lib/better_auth/request_state.rb
325
332
  - lib/better_auth/router.rb
326
333
  - lib/better_auth/routes/account.rb
327
334
  - lib/better_auth/routes/email_verification.rb
@@ -375,6 +382,7 @@ files:
375
382
  - lib/better_auth/social_providers/vk.rb
376
383
  - lib/better_auth/social_providers/wechat.rb
377
384
  - lib/better_auth/social_providers/zoom.rb
385
+ - lib/better_auth/url_helpers.rb
378
386
  - lib/better_auth/version.rb
379
387
  homepage: https://github.com/sebasxsala/better-auth
380
388
  licenses: