securial 0.5.0 → 0.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a67cb5167555d544a8787d5731542215aa5d43a50955cbb4a0a929d9df39ed08
4
- data.tar.gz: 6ee38fc5624167b7cf6d9307dc07224e0762a163145df864247350c523b09a5a
3
+ metadata.gz: 1628d056fdb0a93bc954107766c8f5ed2975d4690c58dbdbe56a0d45e63eefa3
4
+ data.tar.gz: 166ce5d80dc63642239a0f6c26702e4bbca469c2f7d886d322aa07e119ef8928
5
5
  SHA512:
6
- metadata.gz: a85b1cd94066087eab59f7c883e4b822a16e91e03a2df80e4100f7a81cc25712427d35719bb90dbf509c028081bbd215436d097e2056e054e13433fc1b97d9ef
7
- data.tar.gz: a39f8dfd001c239fc2f5612e9777f587d688c93de8070656fe7e8fa38a3610463ce18502bef35b12d75c45eb9ee5f27608f8a25d2dc00aa9394f2f8dd5a7bc38
6
+ metadata.gz: 997ba8dc253bf3772ef806e9c81cce2fb7a7b0dd263527f0896badf74e5ea09ab5560ca804162cde01441054ac5b5d0fca9c39dfc2607cf18086ec6bb4667af2
7
+ data.tar.gz: b7ff4072c37645e9dc6f1bd5551bb47ec829cbefd412221bcdf13e5a2e788450243c1e7853e918248a2b1faaea61aec40122ddea32f1ef80a1ae013367d9d349
data/README.md CHANGED
@@ -1,6 +1,3 @@
1
- ![image](https://github.com/user-attachments/assets/d7cb9645-c28e-4cca-9c1b-5085a91c11d4)
2
-
3
- ---
4
1
  # Securial
5
2
 
6
3
  [![Gem Version](https://img.shields.io/gem/v/securial?logo=rubygems&logoColor=ffffff&logoSize=auto&label=version&color=violet&cacheSeconds=120)](https://rubygems.org/gems/securial)
@@ -10,6 +7,16 @@
10
7
  [![Tests](https://github.com/alybadawy/securial/actions/workflows/ci.yml/badge.svg)](https://github.com/alybadawy/securial/actions)
11
8
  [![Coverage Status](https://coveralls.io/repos/github/AlyBadawy/Securial/badge.svg?branch=main)](https://coveralls.io/github/AlyBadawy/Securial?branch=main)
12
9
 
10
+ > [!WARNING]
11
+ >
12
+ > **Securial is currently in active development (major version zero).**
13
+ >
14
+ > While the gem is functional and versioned, it is not yet considered stable. Until v1.0.0 is released, any updates may introduce breaking changes as the API and features continue to evolve. If you plan to use Securial in production, please do so with caution and pin a specific version.
15
+ >
16
+ > You can track the roadmap and remaining tasks for the v1.0.0 release in [this GitHub issue](https://github.com/AlyBadawy/Securial/issues/64).
17
+
18
+ ---
19
+
13
20
  **Securial** is a mountable Rails engine that provides robust, extensible authentication and access control for Rails applications. It supports:
14
21
 
15
22
  - ✅ JWT-based authentication
@@ -20,33 +27,13 @@
20
27
  - ✅ Database-agnostic support
21
28
 
22
29
 
23
- ### 🚀 Why Choose Securial?
24
-
25
- Securial isn't just another auth library — it's designed to give you control, flexibility, and peace of mind when building secure Rails APIs.
30
+ ### 🚀 Why Securial?
26
31
 
27
- **🔧 Built for Developers**
28
- - Easy to mount and extend using familiar Rails conventions.
29
- - Fully customizable controllers, serializers, and logic — no more black-box auth.
32
+ Securial was built to offer a clean, modular, and API-first authentication system for Rails developers who want full control without the black-box complexity. Whether you're building for the web, mobile, or both, Securial gives you the flexibility to implement exactly what you need — from simple JWT authentication to more advanced setups involving sessions, API tokens, and role-based access.
30
33
 
31
- **🧩 Modular by Design**
32
- - Use only the components you need: JWT, sessions, API tokens — or all of them together.
33
- - Clean separation of concerns makes testing and debugging simpler.
34
+ It follows familiar Rails conventions, stays lightweight and database-agnostic, and keeps security at the core. With fully customizable controllers, serializers, and logic, Securial is designed to grow with your project — making it an ideal choice for everything from side projects to production-grade APIs.
34
35
 
35
- **⚡ API-First Approach**
36
- - JSON-only responses make Securial ideal for frontend frameworks and mobile apps.
37
- - No HTML views or form helpers — just clean endpoints that work out of the box.
38
36
 
39
- **🛡️ Secure by Default**
40
- - Uses industry best practices for token management and access control.
41
- - No reliance on client-side sessions or cookies.
42
-
43
- **📦 Lightweight, Database-Agnostic**
44
- - No assumptions about your schema or ORM — works with any relational database.
45
- - Minimal dependencies, fast to integrate.
46
-
47
- **🌱 Ready to Grow With You**
48
- - Start small with basic JWT auth, scale to multi-token API clients, admin scopes, or full RBAC.
49
- - Perfect for startups, side projects, and production APIs alike.
50
37
 
51
38
  ## 🚀 Installation
52
39
 
@@ -133,3 +120,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/alybad
133
120
  ## ⚖️ License
134
121
 
135
122
  The gem is available as open source under the terms of the [MIT license](https://github.com/AlyBadawy/Securial?tab=MIT-1-ov-file#readme).
123
+
124
+ ---
125
+
126
+ ![image](https://github.com/user-attachments/assets/d7cb9645-c28e-4cca-9c1b-5085a91c11d4)
@@ -32,9 +32,9 @@ module Securial
32
32
  if auth_header.present? && auth_header.start_with?("Bearer ")
33
33
  token = auth_header.split(" ").last
34
34
  begin
35
- decoded_token = Securial::Sessions::SessionEncoder.decode(token)
35
+ decoded_token = Securial::Auth::AuthEncoder.decode(token)
36
36
  Current.session = Session.find_by!(id: decoded_token["jti"], revoked: false)
37
- rescue Securial::Sessions::Errors::SessionDecodeError, ActiveRecord::RecordNotFound => e
37
+ rescue Securial::Auth::Errors::AuthDecodeError, ActiveRecord::RecordNotFound => e
38
38
  render status: :unauthorized, json: { error: "Invalid token: #{e.message}" } and return
39
39
  end
40
40
  else
@@ -43,19 +43,11 @@ module Securial
43
43
  end
44
44
 
45
45
  def start_new_session_for(user)
46
- user.sessions.create!(
47
- user_agent: request.user_agent,
48
- ip_address: request.remote_ip,
49
- refresh_token: SecureRandom.hex(64),
50
- last_refreshed_at: Time.current,
51
- refresh_token_expires_at: 1.week.from_now,
52
- ).tap do |session|
53
- Current.session = session
54
- end
46
+ Securial::Auth::SessionCreator.create_session(user, request)
55
47
  end
56
48
 
57
49
  def create_jwt_for_current_session
58
- Securial::Sessions::SessionEncoder.encode(Current.session)
50
+ Securial::Auth::AuthEncoder.encode(Current.session)
59
51
  end
60
52
 
61
53
  def internal_rails_request?
@@ -14,18 +14,12 @@ module Securial
14
14
  def login
15
15
  params.require([:email_address, :password])
16
16
  if user = User.authenticate_by(params.permit([:email_address, :password]))
17
- start_new_session_for user
18
- render status: :created,
19
- json: {
20
- access_token: create_jwt_for_current_session,
21
- refresh_token: Current.session.refresh_token,
22
- refresh_token_expires_at: Current.session.refresh_token_expires_at,
23
- }
17
+ render_login_response(user)
24
18
  else
25
19
  render status: :unauthorized,
26
20
  json: {
27
21
  error: "Invalid email address or password.",
28
- fix: "Make sure to send the correct 'email_address' and 'password' in the payload",
22
+ instructions: "Make sure to send the correct 'email_address' and 'password' in the payload",
29
23
  }
30
24
  end
31
25
  end
@@ -72,5 +66,23 @@ module Securial
72
66
  id = params[:id]
73
67
  @securial_session = id ? Current.user.sessions.find(params.expect(:id)) : Current.session
74
68
  end
69
+
70
+ def render_login_response(user)
71
+ if user.password_expired?
72
+ render status: :forbidden,
73
+ json: {
74
+ error: "Password expired",
75
+ instructions: "Please reset your password before logging in.",
76
+ }
77
+ else
78
+ start_new_session_for user
79
+ render status: :created,
80
+ json: {
81
+ access_token: create_jwt_for_current_session,
82
+ refresh_token: Current.session.refresh_token,
83
+ refresh_token_expires_at: Current.session.refresh_token_expires_at,
84
+ }
85
+ end
86
+ end
75
87
  end
76
88
  end
@@ -5,6 +5,8 @@ module Securial
5
5
  included do
6
6
  has_secure_password
7
7
 
8
+ before_save :update_password_changed_at, if: :will_save_change_to_password_digest?
9
+
8
10
  validates :password,
9
11
  length: {
10
12
  minimum: Securial.configuration.password_min_length,
@@ -43,5 +45,18 @@ module Securial
43
45
  reset_password_token_created_at: nil
44
46
  )
45
47
  end
48
+
49
+ def password_expired?
50
+ return false unless Securial.configuration.password_expires
51
+ return true unless password_changed_at
52
+
53
+ password_changed_at < Securial.configuration.password_expires_in.ago
54
+ end
55
+
56
+ private
57
+
58
+ def update_password_changed_at
59
+ self.password_changed_at = Time.current
60
+ end
46
61
  end
47
62
  end
@@ -15,8 +15,8 @@ module Securial
15
15
  end
16
16
 
17
17
  def refresh!
18
- raise Securial::Sessions::Errors::SessionRevokedError, "Session is revoked" if revoked
19
- raise Securial::Sessions::Errors::SessionExpiredError, "Session is expired" if refresh_token_expires_at < Time.current
18
+ raise Securial::Auth::Errors::AuthRevokedError, "Session is revoked" if revoked
19
+ raise Securial::Auth::Errors::AuthExpiredError, "Session is expired" if refresh_token_expires_at < Time.current
20
20
 
21
21
  update!(refresh_token: SecureRandom.hex(64),
22
22
  refresh_count: self.refresh_count + 1,
@@ -6,6 +6,8 @@ json.phone securial_user.phone
6
6
  json.username securial_user.username
7
7
  json.bio securial_user.bio
8
8
 
9
+ json.password_expired securial_user.password_expired?
10
+
9
11
  json.roles securial_user.roles, partial: "securial/roles/securial_role", as: :securial_role
10
12
 
11
13
  json.partial! "securial/shared/timestamps", record: securial_user
data/config/routes.rb CHANGED
@@ -28,7 +28,6 @@ Securial::Engine.routes.draw do
28
28
  post "login", to: "sessions#login", as: :login
29
29
  delete "logout", to: "sessions#logout", as: :logout
30
30
  put "refresh", to: "sessions#refresh", as: :refresh_session
31
- delete "revoke", to: "sessions#revoke", as: :revoke_current_session
32
31
  delete "id/:id/revoke", to: "sessions#revoke", as: :revoke_session_by_id
33
32
  delete "revoke_all", to: "sessions#revoke_all", as: :revoke_all_sessions
34
33
  end
@@ -1,13 +1,16 @@
1
1
  class CreateSecurialUsers < ActiveRecord::Migration[8.0]
2
2
  def change
3
3
  create_table :securial_users, id: :string do |t|
4
- t.string :email_address
5
- t.string :password_digest
6
- t.string :first_name
7
- t.string :last_name
8
- t.string :phone
9
- t.string :username
10
- t.string :bio
4
+ t.string :email_address
5
+ t.string :first_name
6
+ t.string :last_name
7
+ t.string :phone
8
+ t.string :username
9
+ t.string :bio
10
+ t.string :password_digest
11
+ t.string :reset_password_token
12
+ t.datetime :reset_password_token_created_at
13
+ t.datetime :password_changed_at
11
14
 
12
15
  t.timestamps
13
16
  end
@@ -14,7 +14,7 @@ Securial.configure do |config|
14
14
  config.log_file_level = :info
15
15
 
16
16
  # Set log level for stdout logger
17
- config.log_stdout_level = :info
17
+ config.log_stdout_level = :debug
18
18
 
19
19
  ##### User Roles
20
20
  ## Set the role for admin users
@@ -26,7 +26,7 @@ Securial.configure do |config|
26
26
  # in the `/securial/superusers` namespace.
27
27
  config.admin_role = :admin
28
28
 
29
- #### Session Configuration
29
+ ##### Session Configuration
30
30
  ## Set the session expiration duration
31
31
  # This is the time after which a session will be considered expired.
32
32
  # After this time, the session will be invalidated and the user
@@ -35,14 +35,6 @@ Securial.configure do |config|
35
35
  # The default is 3 minutes.
36
36
  config.session_expiration_duration = 3.minutes
37
37
 
38
- ## Set the session renewal duration
39
- # This is the time after which a session will be renewed.
40
- # After this time, the session will be renewed and the expiration
41
- # time will be extended. This is useful for keeping users logged in
42
- # without requiring them to log in again. The renewal time is set
43
- # in seconds, minutes, or hours. The default is 3 days.
44
- config.session_renewal_duration = 3.days
45
-
46
38
  ## Set the session secret
47
39
  # This secret is used to sign the session tokens and ensure
48
40
  # that they cannot be tampered with. It is important to keep this
@@ -56,7 +48,7 @@ Securial.configure do |config|
56
48
  # Other options include :hs256, :hs384, and :hs512
57
49
  config.session_algorithm = :hs256
58
50
 
59
- #### Securial Mailer Configuration
51
+ ##### Securial Mailer Configuration
60
52
  ## Set the mailer sender address
61
53
  # This is the email address that will be used as the sender
62
54
  # for all emails sent by the Securial engine. This includes
@@ -64,7 +56,7 @@ Securial.configure do |config|
64
56
  # notifications.
65
57
  config.mailer_sender = "no-reply@example.com"
66
58
 
67
- #### Password configuration
59
+ ##### Password configuration
68
60
  ## Set the password reset email subject
69
61
  # This is the subject line that will be used for the password reset
70
62
  # email. The default is "Password Reset Instructions".
@@ -107,7 +99,7 @@ Securial.configure do |config|
107
99
  # secret secure and not share it with anyone.
108
100
  config.reset_password_token_secret = "reset_secret"
109
101
 
110
- ### Timestamp Configuration
102
+ ##### Timestamp Configuration
111
103
  ## Set whether to use timestamps in the json responses.
112
104
  # The options are:
113
105
  # :none - no timestamps will be included in the json responses.
@@ -117,4 +109,42 @@ Securial.configure do |config|
117
109
  # :all - the created_at and updated_at timestamps will be included
118
110
  # for all users. This is useful for debugging and development purposes.
119
111
  config.timestamps_in_response = Rails.env.production? ? :admins_only : :all
112
+
113
+ ##### Response Configuration
114
+ ## Set the format of the JSON keys in the responses.
115
+ # The options are:
116
+ # :snake_case - the keys will be in snake_case format.
117
+ # :lowerCamelCase - the keys will be in lowerCamelCase format.
118
+ # :upperCamelCase - the keys will be in UpperCamelCase format.
119
+ config.response_keys_format = :snake_case
120
+
121
+ ##### Security Configuration
122
+ ## Set the security headers to be included in the responses.
123
+ # Read more about security headers here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers or in the gem documentation.
124
+ # The options are:
125
+ # :default - the default security headers will be included.
126
+ # :strict - the strict security headers will be included.
127
+ config.security_headers = :strict
128
+ ## set whether to enable request rate limiting
129
+ # This is useful for preventing abuse and denial of service attacks.
130
+ config.rate_limiting_enabled = true
131
+ ## Set the rate limit for requests
132
+ # This is the maximum number of requests that a user can make
133
+ # in a given time period. The default is 60 requests per minute.
134
+ # This is only applied if `rate_limiting_enabled` is set to true.
135
+ config.rate_limit_requests_per_minute = 60
136
+ ## Set the rate limit response status code
137
+ # This is the status code that will be returned when a user exceeds
138
+ # the rate limit. The status code should be a 4xx or 5xx code
139
+ # to indicate an error. Commonly used codes are 429 Too Many Requests
140
+ # or 503 Service Unavailable. The default is 429 Too Many Requests.
141
+ # This is only applied if `rate_limiting_enabled` is set to true.
142
+ config.rate_limit_response_status = 429
143
+ ## Set the rate limit response message
144
+ # This is the message that will be returned when a user exceeds
145
+ # the rate limit. The default is "Too many requests, please try again later."
146
+ # This is only applied if `rate_limiting_enabled` is set to true.
147
+ config.rate_limit_response_message = "Too many requests, please try again later."
148
+
149
+
120
150
  end
@@ -0,0 +1,25 @@
1
+ require "rails/generators"
2
+ require "rake"
3
+
4
+ module Securial
5
+ module Generators
6
+ module Install
7
+ class ViewsGenerator < Rails::Generators::Base
8
+ source_root Securial::Engine.root.join("app", "views", "securial").to_s
9
+ desc "Copies Securial model-related views to your application for customization."
10
+
11
+ def copy_model_views
12
+ Dir.glob(File.join(self.class.source_root, "**/*")).each do |path|
13
+ relative_path = Pathname.new(path).relative_path_from(Pathname.new(self.class.source_root))
14
+
15
+ if File.directory?(path)
16
+ empty_directory "app/views/securial/#{relative_path}"
17
+ else
18
+ copy_file path, "app/views/securial/#{relative_path}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "errors"
2
+ require_relative "auth_encoder"
3
+ require_relative "session_creator"
@@ -1,6 +1,6 @@
1
1
  module Securial
2
- module Sessions
3
- module SessionEncoder
2
+ module Auth
3
+ module AuthEncoder
4
4
  class << self
5
5
  def encode(session)
6
6
  return nil unless session && session.class == Securial::Session
@@ -21,7 +21,7 @@ module Securial
21
21
  begin
22
22
  JWT.encode(payload, secret, algorithm, { kid: "hmac" })
23
23
  rescue JWT::EncodeError => e
24
- raise Errors::SessionEncodeError, "Failed to encode session: #{e.message}"
24
+ raise Errors::AuthEncodeError, "Failed to encode session: #{e.message}"
25
25
  end
26
26
  end
27
27
 
@@ -29,7 +29,7 @@ module Securial
29
29
  begin
30
30
  decoded = JWT.decode(token, secret, true, { algorithm: algorithm, verify_jti: true, iss: "securial" })
31
31
  rescue JWT::DecodeError => e
32
- raise Securial::Sessions::Errors::SessionDecodeError, "Failed to decode session token: #{e.message}"
32
+ raise Securial::Auth::Errors::AuthDecodeError, "Failed to decode session token: #{e.message}"
33
33
  end
34
34
  decoded.first
35
35
  end
@@ -0,0 +1,15 @@
1
+ module Securial
2
+ module Auth
3
+ module Errors
4
+ class BaseAuthError < StandardError
5
+ def backtrace; []; end
6
+ end
7
+
8
+ class AuthEncodeError < BaseAuthError; end
9
+ class AuthDecodeError < BaseAuthError; end
10
+
11
+ class AuthRevokedError < BaseAuthError; end
12
+ class AuthExpiredError < BaseAuthError; end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ module Securial
2
+ module Auth
3
+ module SessionCreator
4
+ class << self
5
+ def create_session(user, request)
6
+ return nil unless user && user.persisted? && request.is_a?(ActionDispatch::Request)
7
+
8
+ user.sessions.create!(
9
+ user_agent: request.user_agent,
10
+ ip_address: request.remote_ip,
11
+ refresh_token: SecureRandom.hex(64),
12
+ last_refreshed_at: Time.current,
13
+ refresh_token_expires_at: 1.week.from_now,
14
+ ).tap do |session|
15
+ Current.session = session
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,41 +1,61 @@
1
+ require_relative "../helpers/_index"
1
2
  module Securial
2
3
  module Config
3
4
  VALID_SESSION_ENCRYPTION_ALGORITHMS = [:hs256, :hs384, :hs512].freeze
4
5
  VALID_TIMESTAMP_OPTIONS = [:all, :admins_only, :none].freeze
6
+ VALID_RESPONSE_KEYS_FORMATS = [:snake_case, :lowerCamelCase, :UpperCamelCase].freeze
7
+ VALID_SECURITY_HEADERS = [:strict, :default, :none].freeze
5
8
 
6
9
  class Configuration
7
- attr_accessor :log_to_file, :log_to_stdout
8
- attr_accessor :log_file_level, :log_stdout_level
9
- attr_accessor :admin_role
10
- attr_accessor :session_expiration_duration
11
- attr_accessor :session_secret, :session_algorithm
12
- attr_accessor :mailer_sender
13
- attr_accessor :password_reset_email_subject
14
- attr_accessor :password_min_length, :password_max_length
15
- attr_accessor :password_complexity
16
- attr_accessor :password_expires_in
17
- attr_accessor :reset_password_token_expires_in
18
- attr_accessor :reset_password_token_secret
19
- attr_accessor :timestamps_in_response
10
+ def self.config_attributes # rubocop:disable Metrics/MethodLength
11
+ {
12
+ log_to_file: !Rails.env.test?,
13
+ log_to_stdout: !Rails.env.test?,
14
+ log_file_level: :info,
15
+ log_stdout_level: :debug,
16
+ admin_role: :admin,
17
+ session_expiration_duration: 3.minutes,
18
+ session_secret: "secret",
19
+ session_algorithm: :hs256,
20
+ mailer_sender: "no-reply@example.com",
21
+ password_reset_email_subject: "SECURIAL: Password Reset Instructions",
22
+ password_min_length: 8,
23
+ password_max_length: 128,
24
+ password_complexity: Securial::RegexHelper::PASSWORD_REGEX,
25
+ password_expires: true,
26
+ password_expires_in: 90.days,
27
+ reset_password_token_expires_in: 2.hours,
28
+ reset_password_token_secret: "reset_secret",
29
+ timestamps_in_response: :all,
30
+ response_keys_format: :snake_case,
31
+ security_headers: :strict,
32
+ rate_limiting_enabled: true,
33
+ rate_limit_requests_per_minute: 60,
34
+ rate_limit_response_status: 429,
35
+ rate_limit_response_message: "Too many requests, please try again later.",
36
+ }
37
+ end
20
38
 
21
39
  def initialize
22
- @log_to_file = !Rails.env.test?
23
- @log_to_stdout = !Rails.env.test?
24
- @log_file_level = :info
25
- @log_stdout_level = :info
26
- @admin_role = :admin
27
- @session_expiration_duration = 3.minutes
28
- @session_secret = "secret"
29
- @session_algorithm = :hs256
30
- @mailer_sender = "no-reply@example.com"
31
- @password_reset_email_subject = "SECURIAL: Password Reset Instructions"
32
- @password_min_length = 8
33
- @password_max_length = 128
34
- @password_complexity = Securial::RegexHelper::PASSWORD_REGEX
35
- @password_expires_in = 90.days
36
- @reset_password_token_expires_in = 2.hours
37
- @reset_password_token_secret = "reset_secret"
38
- @timestamps_in_response = :all
40
+ self.class.config_attributes.each do |attr, default|
41
+ instance_variable_set("@#{attr}", default)
42
+ end
43
+ validate!
44
+ end
45
+
46
+ config_attributes.each_key do |attr|
47
+ define_method(attr) { instance_variable_get("@#{attr}") }
48
+
49
+ define_method("#{attr}=") do |value|
50
+ instance_variable_set("@#{attr}", value)
51
+ validate!
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def validate!
58
+ Securial::Config::Validation.validate_all!(self)
39
59
  end
40
60
  end
41
61
  end
@@ -13,7 +13,8 @@ module Securial
13
13
 
14
14
  class ConfigMailerSenderError < BaseConfigError; end
15
15
  class ConfigPasswordError < BaseConfigError; end
16
- class ConfigTimestampsInResponseError < BaseConfigError; end
16
+ class ConfigResponseError < BaseConfigError; end
17
+ class ConfigSecurityError < BaseConfigError; end
17
18
  end
18
19
  end
19
20
  end