aris 1.3.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +342 -0
  4. data/lib/aris/adapters/base.rb +25 -0
  5. data/lib/aris/adapters/joys_integration.rb +94 -0
  6. data/lib/aris/adapters/mock/adapter.rb +141 -0
  7. data/lib/aris/adapters/mock/request.rb +81 -0
  8. data/lib/aris/adapters/mock/response.rb +17 -0
  9. data/lib/aris/adapters/rack/adapter.rb +117 -0
  10. data/lib/aris/adapters/rack/request.rb +66 -0
  11. data/lib/aris/adapters/rack/response.rb +16 -0
  12. data/lib/aris/core.rb +931 -0
  13. data/lib/aris/discovery.rb +312 -0
  14. data/lib/aris/locale_injector.rb +39 -0
  15. data/lib/aris/pipeline_runner.rb +100 -0
  16. data/lib/aris/plugins/api_key_auth.rb +61 -0
  17. data/lib/aris/plugins/basic_auth.rb +68 -0
  18. data/lib/aris/plugins/bearer_auth.rb +64 -0
  19. data/lib/aris/plugins/cache.rb +120 -0
  20. data/lib/aris/plugins/compression.rb +96 -0
  21. data/lib/aris/plugins/cookies.rb +46 -0
  22. data/lib/aris/plugins/cors.rb +81 -0
  23. data/lib/aris/plugins/csrf.rb +48 -0
  24. data/lib/aris/plugins/etag.rb +90 -0
  25. data/lib/aris/plugins/flash.rb +124 -0
  26. data/lib/aris/plugins/form_parser.rb +46 -0
  27. data/lib/aris/plugins/health_check.rb +62 -0
  28. data/lib/aris/plugins/json.rb +32 -0
  29. data/lib/aris/plugins/multipart.rb +160 -0
  30. data/lib/aris/plugins/rate_limiter.rb +60 -0
  31. data/lib/aris/plugins/request_id.rb +38 -0
  32. data/lib/aris/plugins/request_logger.rb +43 -0
  33. data/lib/aris/plugins/security_headers.rb +99 -0
  34. data/lib/aris/plugins/session.rb +175 -0
  35. data/lib/aris/plugins.rb +23 -0
  36. data/lib/aris/response_helpers.rb +156 -0
  37. data/lib/aris/route_helpers.rb +141 -0
  38. data/lib/aris/utils/redirects.rb +44 -0
  39. data/lib/aris/utils/sitemap.rb +84 -0
  40. data/lib/aris/version.rb +3 -0
  41. data/lib/aris.rb +35 -0
  42. metadata +151 -0
@@ -0,0 +1,312 @@
1
+ # lib/aris/discovery.rb
2
+ require 'pathname'
3
+
4
+ module Aris
5
+ module Discovery
6
+ extend self
7
+
8
+ HTTP_METHODS = Set[:get, :post, :put, :patch, :delete, :options].freeze
9
+
10
+ # Scans a directory and generates an Aris routes hash with locale support
11
+ # Handlers are LOADED during discovery, not at request time
12
+ def discover(routes_dir, namespace_handlers: true)
13
+ base_path = Pathname.new(routes_dir).realpath
14
+ routes = {}
15
+ domain_configs = {}
16
+
17
+ # First pass: discover domain configs
18
+ Dir.glob("#{routes_dir}/*/_config.rb").sort.each do |config_file|
19
+ domain_name = File.basename(File.dirname(config_file))
20
+ domain_key = (domain_name == '_') ? '*' : domain_name
21
+
22
+ config = discover_domain_config(config_file)
23
+ domain_configs[domain_key] = config if config
24
+ end
25
+
26
+ # Second pass: discover route files
27
+ Dir.glob("#{routes_dir}/**/*.rb").sort.each do |file_path|
28
+ next if file_path.end_with?('_config.rb')
29
+
30
+ route_info = parse_route_file(file_path, base_path)
31
+ next unless route_info
32
+
33
+ # Load the handler NOW (at boot time, not request time)
34
+ handler = load_handler(file_path, route_info, namespace_handlers)
35
+ next unless handler
36
+
37
+ # Check for localized declaration
38
+ localized_paths = discover_handler_locales(handler)
39
+
40
+ # If handler has localized declaration, validate against domain config
41
+ if localized_paths && !localized_paths.empty?
42
+ domain_config = domain_configs[route_info[:domain]]
43
+
44
+ if domain_config
45
+ validate_localized_handler(
46
+ handler_path: File.dirname(file_path),
47
+ localized_paths: localized_paths,
48
+ domain_config: domain_config,
49
+ route_info: route_info
50
+ )
51
+ else
52
+ warn "Warning: Handler at #{file_path} declares localized paths but domain has no _config.rb"
53
+ end
54
+ end
55
+
56
+ # Build the nested route structure
57
+ add_route_to_hash(routes, route_info, handler, localized_paths)
58
+ end
59
+
60
+ # Merge domain configs into routes
61
+ domain_configs.each do |domain, config|
62
+ routes[domain] ||= {}
63
+ routes[domain][:locales] = config[:locales]
64
+ routes[domain][:default_locale] = config[:default_locale]
65
+ routes[domain][:root_locale_redirect] = config[:root_locale_redirect] if config.key?(:root_locale_redirect)
66
+ end
67
+
68
+ routes
69
+ end
70
+
71
+ private
72
+
73
+ # Discover domain configuration from _config.rb file
74
+ def discover_domain_config(config_file)
75
+ load config_file
76
+
77
+ # Look for DomainConfig module
78
+ if defined?(DomainConfig)
79
+ config = {
80
+ locales: DomainConfig::LOCALES,
81
+ default_locale: DomainConfig::DEFAULT_LOCALE
82
+ }
83
+
84
+ # Optional root redirect configuration
85
+ if DomainConfig.const_defined?(:ROOT_LOCALE_REDIRECT)
86
+ config[:root_locale_redirect] = DomainConfig::ROOT_LOCALE_REDIRECT
87
+ end
88
+
89
+ # Clean up to avoid conflicts with next domain
90
+ Object.send(:remove_const, :DomainConfig)
91
+
92
+ return config
93
+ end
94
+
95
+ nil
96
+ rescue => e
97
+ warn "Error loading domain config from #{config_file}: #{e.message}"
98
+ nil
99
+ end
100
+
101
+ # Parse file path into route information
102
+ def parse_route_file(file_path, base_path)
103
+ absolute_path = Pathname.new(file_path).realpath
104
+ relative_path = absolute_path.relative_path_from(base_path)
105
+ parts = relative_path.to_s.split('/')
106
+
107
+ return nil if parts.size < 2 # Need at least domain/method.rb
108
+
109
+ # Extract method from filename
110
+ method_file = parts.pop
111
+ method_name = File.basename(method_file, '.rb').downcase.to_sym
112
+ return nil unless HTTP_METHODS.include?(method_name)
113
+
114
+ # Extract domain
115
+ domain_name = parts.shift
116
+ domain_key = (domain_name == '_') ? '*' : domain_name
117
+
118
+ # Build path segments, converting _param to :param
119
+ path_parts = parts.reject { |p| p == 'index' }
120
+ .map { |p| p.start_with?('_') ? ":#{p[1..]}" : "/#{p}" }
121
+
122
+ {
123
+ domain: domain_key,
124
+ path_parts: path_parts,
125
+ method: method_name,
126
+ file_path: absolute_path.to_s,
127
+ namespace: build_namespace(domain_key, path_parts, method_name)
128
+ }
129
+ end
130
+
131
+ # Build a namespace for the handler to avoid conflicts
132
+ # e.g., "example.com/users/:id", :get -> ExampleCom::Users::Id::Get
133
+ def build_namespace(domain, path_parts, method)
134
+ # Convert * to Wildcard for valid module name
135
+ domain_part = domain == '*' ? 'Wildcard' : domain.gsub(/[^a-zA-Z0-9]/, '_')
136
+ parts = [domain_part]
137
+ parts += path_parts.map do |p|
138
+ p.sub('/', '').sub(':', '').gsub(/[^a-zA-Z0-9]/, '_')
139
+ end
140
+ parts << method.to_s # Add method to ensure unique namespace
141
+ parts.map(&:capitalize).join('::')
142
+ end
143
+
144
+ # Load handler from file and namespace it to avoid conflicts
145
+ def load_handler(file_path, route_info, namespace_handlers)
146
+ if namespace_handlers
147
+ # Create a module namespace for this specific route
148
+ namespace_module = create_namespace_module(route_info[:namespace])
149
+
150
+ # Evaluate the file within that namespace
151
+ code = File.read(file_path)
152
+ namespace_module.module_eval(code, file_path)
153
+
154
+ # Look for Handler constant in the namespace
155
+ if namespace_module.const_defined?(:Handler, false)
156
+ handler = namespace_module.const_get(:Handler)
157
+ validate_handler!(handler, file_path)
158
+ return handler
159
+ else
160
+ warn "Warning: #{file_path} does not define a Handler constant"
161
+ return nil
162
+ end
163
+ else
164
+ # Load file and grab top-level Handler (simpler but risks conflicts)
165
+ load file_path
166
+
167
+ if Object.const_defined?(:Handler, false)
168
+ handler = Object.const_get(:Handler)
169
+ Object.send(:remove_const, :Handler) # Clean up to avoid next file's conflict
170
+ validate_handler!(handler, file_path)
171
+ return handler
172
+ else
173
+ warn "Warning: #{file_path} does not define a Handler constant"
174
+ return nil
175
+ end
176
+ end
177
+ rescue SyntaxError => e
178
+ warn "Syntax error in #{file_path}: #{e.message}"
179
+ nil
180
+ rescue => e
181
+ warn "Error loading handler from #{file_path}: #{e.message}"
182
+ nil
183
+ end
184
+
185
+ # Create nested module namespace
186
+ def create_namespace_module(namespace_string)
187
+ parts = namespace_string.split('::')
188
+ parts.reduce(Object) do |parent, part|
189
+ if parent.const_defined?(part, false)
190
+ parent.const_get(part)
191
+ else
192
+ parent.const_set(part, Module.new)
193
+ end
194
+ end
195
+ end
196
+
197
+ # Validate handler has required interface
198
+ def validate_handler!(handler, file_path)
199
+ unless handler.respond_to?(:call)
200
+ raise ArgumentError,
201
+ "Handler in #{file_path} must respond to .call(request, params)"
202
+ end
203
+ end
204
+
205
+ # Check if handler declares localized paths
206
+ def discover_handler_locales(handler)
207
+ if handler.respond_to?(:localized_paths)
208
+ handler.localized_paths
209
+ else
210
+ nil
211
+ end
212
+ end
213
+
214
+ # Validate localized handler against domain config
215
+ def validate_localized_handler(handler_path:, localized_paths:, domain_config:, route_info:)
216
+ domain_locales = domain_config[:locales]
217
+ handler_locales = localized_paths.keys
218
+
219
+ # Error: handler uses locale not in domain config
220
+ invalid_locales = handler_locales - domain_locales
221
+ if invalid_locales.any?
222
+ raise Aris::Router::LocaleError,
223
+ "Handler at #{handler_path} uses locales #{invalid_locales.inspect} " +
224
+ "but domain only declares #{domain_locales.inspect}"
225
+ end
226
+
227
+ # Warning: handler missing some domain locales
228
+ missing_locales = domain_locales - handler_locales
229
+ if missing_locales.any?
230
+ warn "Warning: Handler at #{handler_path} missing locales: #{missing_locales.inspect}"
231
+ end
232
+
233
+ # Validate data files exist
234
+ validate_data_files(handler_path, handler_locales)
235
+ end
236
+
237
+ # Validate data files exist for each locale
238
+ def validate_data_files(handler_path, locales)
239
+ locales.each do |locale|
240
+ found = [
241
+ "data_#{locale}.rb",
242
+ "data_#{locale}.json",
243
+ "data_#{locale}.yml",
244
+ "data_#{locale}.yaml",
245
+ "data/#{locale}.json",
246
+ "data/#{locale}.yml",
247
+ "data/#{locale}.yaml"
248
+ ].any? { |f| File.exist?(File.join(handler_path, f)) }
249
+
250
+ unless found
251
+ warn "Warning: Missing data file for locale :#{locale} in #{handler_path}"
252
+ end
253
+ end
254
+ end
255
+
256
+ # Add route to nested hash structure
257
+ def add_route_to_hash(routes, route_info, handler, localized_paths)
258
+ domain = route_info[:domain]
259
+ routes[domain] ||= {}
260
+
261
+ current_level = routes[domain]
262
+
263
+ # Navigate/create nested path structure
264
+ route_info[:path_parts].each do |part|
265
+ current_level[part] ||= {}
266
+ current_level = current_level[part]
267
+ end
268
+
269
+ # Handle root path (no path parts)
270
+ if route_info[:path_parts].empty?
271
+ current_level['/'] ||= {}
272
+ current_level = current_level['/']
273
+ end
274
+
275
+ # Build route definition
276
+ route_def = { to: handler }
277
+
278
+ # Add localized paths if declared
279
+ if localized_paths && !localized_paths.empty?
280
+ route_def[:localized] = localized_paths
281
+ end
282
+
283
+ # Add the route
284
+ current_level[route_info[:method]] = route_def
285
+
286
+ # Now register metadata AFTER route is added
287
+ if defined?(Aris::Utils::Sitemap) && handler.respond_to?(:sitemap_metadata) && handler.sitemap_metadata
288
+ path = route_info[:path_parts].empty? ? '/' : route_info[:path_parts].join('')
289
+ Aris::Utils::Sitemap.register(
290
+ domain: route_info[:domain],
291
+ path: path,
292
+ method: route_info[:method],
293
+ metadata: handler.sitemap_metadata
294
+ )
295
+ end
296
+
297
+ if defined?(Aris::Utils::Redirects) && handler.respond_to?(:redirect_metadata) && handler.redirect_metadata
298
+ path = route_info[:path_parts].empty? ? '/' : route_info[:path_parts].join('')
299
+ Aris::Utils::Redirects.register(
300
+ from_paths: handler.redirect_metadata[:paths],
301
+ to_path: path,
302
+ status: handler.redirect_metadata[:status]
303
+ )
304
+ end
305
+ end
306
+ end
307
+
308
+ def self.discover_and_define(routes_dir, namespace_handlers: true)
309
+ discovered = Discovery.discover(routes_dir, namespace_handlers: namespace_handlers)
310
+ self.routes(discovered, from_discovery: true)
311
+ end
312
+ end
@@ -0,0 +1,39 @@
1
+ # lib/aris/locale_injector.rb
2
+ module Aris
3
+ module LocaleInjector
4
+ extend self
5
+
6
+ # Inject locale-aware methods into request object after route matching
7
+ # @param request [Object] The request object (Rack::Request, Mock::Request, etc.)
8
+ # @param match_result [Hash] The route match result containing :locale, :domain
9
+ def inject_locale_methods(request, match_result)
10
+ return unless match_result && match_result[:locale]
11
+
12
+ locale = match_result[:locale]
13
+ domain = match_result[:domain]
14
+ domain_config = Aris::Router.domain_config(domain)
15
+
16
+ return unless domain_config
17
+
18
+ # Inject locale information methods
19
+ request.define_singleton_method(:locale) { locale }
20
+ request.define_singleton_method(:available_locales) { domain_config[:locales] }
21
+ request.define_singleton_method(:default_locale) { domain_config[:default_locale] }
22
+ request.define_singleton_method(:domain_config) { domain_config }
23
+
24
+ # Inject locale-aware path generation
25
+ request.define_singleton_method(:path_for) do |name, **opts|
26
+ opts[:locale] ||= locale
27
+ opts[:domain] ||= domain
28
+ Aris.path(name, **opts)
29
+ end
30
+
31
+ # Inject locale-aware URL generation
32
+ request.define_singleton_method(:url_for) do |name, **opts|
33
+ opts[:locale] ||= locale
34
+ opts[:domain] ||= domain
35
+ Aris.url(name, **opts)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,100 @@
1
+ module Aris
2
+ module PipelineRunner
3
+ extend self
4
+
5
+ def call(request:, route:, response:)
6
+
7
+ if route[:subdomain]
8
+ request.define_singleton_method(:subdomain) { route[:subdomain] }
9
+ route[:params] ||= {}
10
+ route[:params][:subdomain] = route[:subdomain]
11
+ end
12
+ if route[:use] && !route[:use].empty?
13
+ route[:use].each do |plugin|
14
+ result = plugin.call(request, response)
15
+ if result.is_a?(Array) || (result.respond_to?(:status) && result.respond_to?(:headers) && result.respond_to?(:body))
16
+ return result # Plugin halted pipeline
17
+ end
18
+ end
19
+ end
20
+
21
+ handler = route[:handler]
22
+ params = route[:params]
23
+ result = execute_handler(handler, request, params, response)
24
+ result = format_handler_result(result, response)
25
+
26
+ if route[:use] && !route[:use].empty?
27
+ route[:use].each do |plugin|
28
+ if plugin.respond_to?(:call_response)
29
+ plugin.call_response(request, result)
30
+ end
31
+ end
32
+ end
33
+
34
+ result
35
+ end
36
+
37
+ private
38
+
39
+ def execute_handler(handler, request, params, response)
40
+ case handler
41
+ when Proc, Method
42
+ if handler.parameters.length >= 3
43
+ handler.call(request, response, params)
44
+ else
45
+ handler.call(request, params)
46
+ end
47
+ when String
48
+ controller_name, action = handler.split('#')
49
+ controller_class = Object.const_get(controller_name)
50
+ controller = controller_class.new
51
+ controller.send(action, request, params)
52
+ else
53
+ if handler.respond_to?(:call)
54
+ method_obj = handler.method(:call)
55
+ if method_obj.parameters.length >= 3
56
+ handler.call(request, response, params)
57
+ else
58
+ handler.call(request, params)
59
+ end
60
+ else
61
+ raise ArgumentError, "Handler doesn't respond to call: #{handler.inspect}"
62
+ end
63
+ end
64
+ end
65
+
66
+ def format_handler_result(result, response)
67
+ case result
68
+ when Array
69
+ # Rack array [status, headers, body] - convert to response object
70
+ response.status = result[0]
71
+ response.headers.merge!(result[1])
72
+ response.body = result[2]
73
+ response
74
+ when Hash
75
+ # JSON response
76
+ response.status = 200
77
+ response.headers['content-type'] = 'application/json'
78
+ response.body = [result.to_json]
79
+ response
80
+ when String
81
+ # Plain text response
82
+ response.status = 200
83
+ response.headers['content-type'] = 'text/plain'
84
+ response.body = [result]
85
+ response
86
+ else
87
+ # Already a response object (or response-like)
88
+ if result.respond_to?(:status) && result.respond_to?(:headers) && result.respond_to?(:body)
89
+ result
90
+ else
91
+ # Treat as string
92
+ response.status = 200
93
+ response.headers['content-type'] = 'text/plain'
94
+ response.body = [result.to_s]
95
+ response
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,61 @@
1
+ # lib/aris/plugins/api_key_auth.rb
2
+
3
+ module Aris
4
+ module Plugins
5
+ class ApiKeyAuth
6
+ attr_reader :config
7
+
8
+ def initialize(**config)
9
+ @config = config
10
+ @header = config[:header] || 'X-API-Key'
11
+ @realm = config[:realm] || 'API'
12
+
13
+ # Validate config
14
+ if config[:validator]
15
+ @validator = config[:validator]
16
+ elsif config[:key]
17
+ @validator = ->(k) { k == config[:key] }
18
+ elsif config[:keys]
19
+ valid_keys = Array(config[:keys])
20
+ @validator = ->(k) { valid_keys.include?(k) }
21
+ else
22
+ raise ArgumentError, "ApiKeyAuth requires :validator, :key, or :keys"
23
+ end
24
+ end
25
+
26
+ def call(request, response)
27
+ # Extract key from header
28
+ header_key = "HTTP_#{@header.upcase.gsub('-', '_')}"
29
+ api_key = request.headers[header_key]
30
+
31
+ unless api_key && !api_key.empty?
32
+ return unauthorized_response(response, 'Missing API key')
33
+ end
34
+
35
+ unless @validator.call(api_key)
36
+ return unauthorized_response(response, 'Invalid API key')
37
+ end
38
+
39
+ # Attach key to request for handlers
40
+ request.instance_variable_set(:@api_key, api_key)
41
+ nil # Continue pipeline
42
+ end
43
+
44
+ def self.build(**config)
45
+ new(**config)
46
+ end
47
+
48
+ private
49
+
50
+ def unauthorized_response(response, message)
51
+ response.status = 401
52
+ response.headers['content-type'] = 'application/json'
53
+ response.headers['WWW-Authenticate'] = %(ApiKey realm="#{@realm}")
54
+ response.body = [JSON.generate({ error: 'Unauthorized', message: message })]
55
+ response
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ Aris.register_plugin(:api_key, plugin_class: Aris::Plugins::ApiKeyAuth)
@@ -0,0 +1,68 @@
1
+ # lib/aris/plugins/basic_auth.rb
2
+ require 'base64'
3
+
4
+ module Aris
5
+ module Plugins
6
+ class BasicAuth
7
+ attr_reader :config
8
+
9
+ def initialize(**config)
10
+ @config = config
11
+ @realm = config[:realm] || 'Restricted Area'
12
+
13
+ # Validate config
14
+ if config[:validator]
15
+ @validator = config[:validator]
16
+ elsif config[:username] && config[:password]
17
+ @validator = ->(u, p) { u == config[:username] && p == config[:password] }
18
+ else
19
+ raise ArgumentError, "BasicAuth requires either :validator or both :username and :password"
20
+ end
21
+ end
22
+
23
+ def call(request, response)
24
+ auth_header = request.headers['HTTP_AUTHORIZATION']
25
+
26
+ unless auth_header && auth_header.start_with?('Basic ')
27
+ return unauthorized_response(response, 'Missing or invalid Authorization header')
28
+ end
29
+
30
+ username, password = decode_credentials(auth_header)
31
+
32
+ unless username && password
33
+ return unauthorized_response(response, 'Invalid credentials format')
34
+ end
35
+
36
+ unless @validator.call(username, password)
37
+ return unauthorized_response(response, 'Invalid username or password')
38
+ end
39
+
40
+ # Attach username to request for handlers
41
+ request.instance_variable_set(:@current_user, username)
42
+ nil # Continue pipeline
43
+ end
44
+
45
+ def self.build(**config)
46
+ new(**config)
47
+ end
48
+
49
+ private
50
+
51
+ def decode_credentials(auth_header)
52
+ encoded = auth_header.sub('Basic ', '')
53
+ decoded = Base64.decode64(encoded)
54
+ decoded.split(':', 2)
55
+ rescue => e
56
+ [nil, nil]
57
+ end
58
+
59
+ def unauthorized_response(response, message)
60
+ response.status = 401
61
+ response.headers['content-type'] = 'text/plain'
62
+ response.headers['WWW-Authenticate'] = %(Basic realm="#{@realm}")
63
+ response.body = [message]
64
+ response
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,64 @@
1
+ # lib/aris/plugins/bearer_auth.rb
2
+
3
+ module Aris
4
+ module Plugins
5
+ class BearerAuth
6
+ attr_reader :config
7
+
8
+ def initialize(**config)
9
+ @config = config
10
+ @realm = config[:realm] || 'API'
11
+
12
+ # Validate config
13
+ if config[:validator]
14
+ @validator = config[:validator]
15
+ elsif config[:token]
16
+ @validator = ->(t) { t == config[:token] }
17
+ else
18
+ raise ArgumentError, "BearerAuth requires either :validator or :token"
19
+ end
20
+ end
21
+
22
+ def call(request, response)
23
+ auth_header = request.headers['HTTP_AUTHORIZATION']
24
+
25
+ unless auth_header && auth_header.start_with?('Bearer ')
26
+ return unauthorized_response(response, 'Missing or invalid Authorization header')
27
+ end
28
+
29
+ token = extract_token(auth_header)
30
+
31
+ unless token && !token.empty?
32
+ return unauthorized_response(response, 'Invalid token format')
33
+ end
34
+
35
+ unless @validator.call(token)
36
+ return unauthorized_response(response, 'Invalid or expired token')
37
+ end
38
+
39
+ # Attach token to request for handlers
40
+ request.instance_variable_set(:@bearer_token, token)
41
+ nil # Continue pipeline
42
+ end
43
+
44
+ def self.build(**config)
45
+ new(**config)
46
+ end
47
+
48
+ private
49
+
50
+ def extract_token(auth_header)
51
+ auth_header.sub('Bearer ', '').strip
52
+ end
53
+
54
+ def unauthorized_response(response, message)
55
+ response.status = 401
56
+ response.headers['content-type'] = 'application/json'
57
+ response.headers['WWW-Authenticate'] = %(Bearer realm="#{@realm}")
58
+ response.body = [JSON.generate({ error: 'Unauthorized', message: message })]
59
+ response
60
+ end
61
+ end
62
+ end
63
+ end
64
+ Aris.register_plugin(:bearer_auth, plugin_class: Aris::Plugins::BearerAuth)