volcano-sdk 0.29.0 → 0.30.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e946d4476603cf2c234194f51e63aadd8ca1327719bcae02bf098de61049b65b
4
- data.tar.gz: 38d3aa7dbbc508796620f75285b2a483b70a9e2d49a255aded0dba3fa023334b
3
+ metadata.gz: 296cdb524e3552873235adccfe2d5c98b39a3da451c06777020b59d45c6a4be0
4
+ data.tar.gz: 9ca783889458304d91b40601f673a4f5015f117ee4f30f90c23891e351cafe3a
5
5
  SHA512:
6
- metadata.gz: 014b16629b5d06e89d39f010b1c8cb550582f3f7f224b6c6911324c6b2b07196eed9729305c2edd623d6fbedb2ba12a9460d4d128bda61202c9c88874823fada
7
- data.tar.gz: d98691e237fd5cdb277f701701e3549aa6e533179d36c0039e188a66143fe5992a1ac9565f210304704838d2e855a9512e9e2e60de7ec0b2abfa9a8a30ae4cb2
6
+ metadata.gz: 81067b14e84482e5dfebf935d27051c109ff14b87d144814a36db6ef5a7de6003e0cde0fe4ed4c54c1e06dcd9db79d8f5088c1aa21fbd4bbc3419da9a585f44e
7
+ data.tar.gz: 4346575e985ab233b5afd723f36e1044b4e9e001b1eefa461a2514977f10a240a88a73978f30d88b2b74a3ec2a6bd4565d1e1ab92968e1988069492cbae9c739
data/lib/volcano/auth.rb CHANGED
@@ -3,6 +3,13 @@
3
3
  module Volcano
4
4
  # Authenticates users and updates the client session.
5
5
  class Auth
6
+ include AuthRefresh
7
+ include AuthSignOut
8
+ include AuthProfile
9
+ include AuthUpdateUser
10
+ include AuthEmailChange
11
+ include AuthSessions
12
+
6
13
  INCOMPLETE_SESSION = 'Expected a complete Volcano::Session'
7
14
  private_constant :INCOMPLETE_SESSION
8
15
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Volcano
4
4
  # Email-change request behavior for the authentication facade.
5
- class Auth
5
+ module AuthEmailChange
6
6
  INVALID_EMAIL_CHANGE_RESULT = 'Expected a valid email-change acknowledgement'
7
7
  private_constant :INVALID_EMAIL_CHANGE_RESULT
8
8
 
@@ -1,21 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'time'
4
-
5
3
  module Volcano
6
- # Server-validated current-user behavior for the authentication facade.
7
- class Auth
8
- INVALID_USER = 'Expected a complete user profile'
9
- RFC3339_OFFSET = /(?:[Zz]|[+-]\d{2}:\d{2})\z/
10
- USER_OPTIONAL_VALUES = %w[project_id email_confirmed user_metadata app_metadata avatar_url].freeze
11
- USER_OPTIONAL_STRINGS = %w[project_id avatar_url].freeze
12
- USER_STATUSES = %w[active banned deleted].freeze
13
- USER_TIMESTAMPS = %w[banned_until last_sign_in_at created_at updated_at].freeze
14
- private_constant :INVALID_USER, :RFC3339_OFFSET, :USER_OPTIONAL_STRINGS,
15
- :USER_OPTIONAL_VALUES, :USER_STATUSES, :USER_TIMESTAMPS
4
+ # Current-user requests and session cache updates.
5
+ module AuthProfile
6
+ include AuthProfileFields
16
7
 
17
8
  def user
18
- profile_request { method(:get_user_response) }
9
+ profile_request { ->(token) { get_user_response(token) } }
19
10
  end
20
11
 
21
12
  alias get_user user
@@ -32,7 +23,7 @@ module Volcano
32
23
  end
33
24
 
34
25
  def cache_current_user(payload, generation)
35
- profile = user_payload(payload)
26
+ profile = profile_user_payload(payload)
36
27
  user = build_user(profile)
37
28
  raise Error::SessionChangedError unless @client.update_session_user_if_current?(profile, generation)
38
29
 
@@ -46,79 +37,47 @@ module Volcano
46
37
  end
47
38
 
48
39
  def build_user(payload)
49
- raise Error::AuthenticationError, INVALID_USER unless valid_user?(payload)
50
-
51
- User.new(**user_attributes(payload))
40
+ new_profile_user(user_identity(payload), user_optional_attributes(payload), user_timestamps(payload))
52
41
  rescue ArgumentError => e
53
42
  raise Error::AuthenticationError, INVALID_USER, cause: e
54
43
  end
55
44
 
56
- def user_attributes(payload)
57
- {
58
- id: payload.fetch('id'),
59
- email: payload.fetch('email'),
60
- status: payload.fetch('status'),
61
- **USER_OPTIONAL_VALUES.to_h { |name| [name.to_sym, payload[name]] },
62
- **USER_TIMESTAMPS.to_h { |name| [name.to_sym, parse_time(payload[name])] }
63
- }
64
- end
65
-
66
- def user_payload(payload)
67
- raise Error::AuthenticationError, INVALID_USER unless payload.is_a?(Hash)
68
-
69
- payload['user']
70
- end
71
-
72
- def valid_user?(payload)
73
- return false unless payload.is_a?(Hash)
74
-
75
- required_user_fields?(payload) && valid_optional_user_fields?(payload)
76
- end
77
-
78
- def valid_optional_user_fields?(payload)
79
- valid_email_confirmation?(payload) && valid_metadata?(payload) &&
80
- valid_optional_strings?(payload) && valid_timestamps?(payload)
81
- end
82
-
83
- def required_user_fields?(payload)
84
- complete_string?(payload['id']) && payload['email'].is_a?(String) &&
85
- USER_STATUSES.include?(payload['status'])
86
- end
87
-
88
- def valid_email_confirmation?(payload)
89
- [true, false, nil].include?(payload['email_confirmed'])
45
+ def new_profile_user(identity, optional, timestamps)
46
+ User.new(
47
+ id: identity[:id], email: identity[:email], status: identity[:status],
48
+ project_id: optional[:project_id], email_confirmed: optional[:email_confirmed],
49
+ user_metadata: optional[:user_metadata], app_metadata: optional[:app_metadata],
50
+ avatar_url: optional[:avatar_url], banned_until: timestamps[:banned_until],
51
+ last_sign_in_at: timestamps[:last_sign_in_at], created_at: timestamps[:created_at],
52
+ updated_at: timestamps[:updated_at]
53
+ )
90
54
  end
91
55
 
92
- def valid_metadata?(payload)
93
- %w[user_metadata app_metadata].all? do |name|
94
- payload[name].nil? || payload[name].is_a?(Hash)
95
- end
96
- end
97
-
98
- def valid_optional_strings?(payload)
99
- USER_OPTIONAL_STRINGS.all? do |name|
100
- payload[name].nil? || payload[name].is_a?(String)
101
- end
102
- end
103
-
104
- def valid_timestamps?(payload)
105
- USER_TIMESTAMPS.all? { |name| valid_timestamp?(payload[name]) }
106
- end
107
-
108
- def valid_timestamp?(value)
109
- return true if value.nil?
110
-
111
- value.is_a?(String) && RFC3339_OFFSET.match?(value) && Time.iso8601(value)
112
- rescue ArgumentError
113
- false
56
+ def user_identity(payload)
57
+ {
58
+ id: profile_required_id(payload['id']),
59
+ email: profile_required_string(payload['email']),
60
+ status: profile_status(payload['status'])
61
+ }
114
62
  end
115
63
 
116
- def parse_time(value)
117
- Time.iso8601(value).freeze unless value.nil?
64
+ def user_optional_attributes(payload)
65
+ {
66
+ project_id: profile_optional_string(payload['project_id']),
67
+ email_confirmed: profile_optional_boolean(payload['email_confirmed']),
68
+ user_metadata: profile_optional_object(payload['user_metadata']),
69
+ app_metadata: profile_optional_object(payload['app_metadata']),
70
+ avatar_url: profile_optional_string(payload['avatar_url'])
71
+ }
118
72
  end
119
73
 
120
- def complete_string?(value)
121
- value.is_a?(String) && !value.strip.empty?
74
+ def user_timestamps(payload)
75
+ {
76
+ banned_until: profile_parse_time(payload['banned_until']),
77
+ last_sign_in_at: profile_parse_time(payload['last_sign_in_at']),
78
+ created_at: profile_parse_time(payload['created_at']),
79
+ updated_at: profile_parse_time(payload['updated_at'])
80
+ }
122
81
  end
123
82
  end
124
83
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ # Runs queued auth-state callbacks outside session coordination locks.
5
+ module AuthNotificationDispatch
6
+ private
7
+
8
+ def run_auth_notifications(notifications)
9
+ notifications.each do |notification|
10
+ notification.call
11
+ nil
12
+ end
13
+ end
14
+ end
15
+ private_constant :AuthNotificationDispatch
16
+ end
@@ -88,5 +88,9 @@ module Volcano
88
88
  def valid_linked_oauth_provider?(provider, linked_at, updated_at)
89
89
  complete_string?(provider) && linked_at.is_a?(Time) && updated_at.is_a?(Time)
90
90
  end
91
+
92
+ def complete_string?(value)
93
+ value.is_a?(String) && !value.strip.empty?
94
+ end
91
95
  end
92
96
  end
@@ -6,9 +6,12 @@ module Volcano
6
6
  def call_oauth_api(provider, endpoint:, method: 'GET', body: nil)
7
7
  session_payload(200, decode: :oauth_api_data) do
8
8
  provider_name = oauth_provider_name(provider)
9
- request = { endpoint: endpoint.dup.freeze, method: method.dup.freeze,
10
- body: body.nil? ? nil : JSON.parse(JSON.generate(body), freeze: true) }
11
- ->(token) { oauth_api_response(token, provider_name, **request) }
9
+ owned_endpoint = endpoint.dup.freeze
10
+ owned_method = method.dup.freeze
11
+ owned_body = body.nil? ? nil : JSON.parse(JSON.generate(body), freeze: true)
12
+ lambda do |token|
13
+ oauth_api_response(token, provider_name, endpoint: owned_endpoint, method: owned_method, body: owned_body)
14
+ end
12
15
  end
13
16
  end
14
17
 
@@ -24,6 +27,8 @@ module Volcano
24
27
  end
25
28
 
26
29
  def oauth_api_data(response_body)
30
+ raise TypeError, 'Expected OAuth provider API response data' unless response_body.is_a?(Hash)
31
+
27
32
  data = response_body.fetch('data')
28
33
  freeze_oauth_api_data(data)
29
34
  rescue KeyError, NoMethodError
@@ -20,7 +20,7 @@ module Volcano
20
20
  def exchange_oauth_code(code:, redirect_to:, state:, expected_state:)
21
21
  validate_oauth_callback_state(state, expected_state)
22
22
  generation, = @client.capture_session
23
- session = build_session(Transport.body(oauth_exchange_response(code, redirect_to), 200))
23
+ session = owned_complete_session(build_session(Transport.body(oauth_exchange_response(code, redirect_to), 200)))
24
24
  raise Error::SessionChangedError unless @client.store_session_if_current?(session, generation)
25
25
 
26
26
  session
@@ -32,6 +32,8 @@ module Volcano
32
32
  end
33
33
 
34
34
  def oauth_provider_token_status(body)
35
+ raise TypeError, 'Expected complete OAuth provider token status' unless body.is_a?(Hash)
36
+
35
37
  message, provider, expires_in = body.fetch_values('message', 'provider', 'expires_in')
36
38
  OAuthProviderTokenStatus.new(message:, provider:, expires_in:)
37
39
  rescue KeyError, NoMethodError
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Volcano
6
+ # Validates and maps the current-user JSON response.
7
+ module AuthProfileFields
8
+ INVALID_USER = 'Expected a complete user profile'
9
+ RFC3339_OFFSET = /(?:[Zz]|[+-]\d{2}:\d{2})\z/
10
+ USER_STATUSES = %w[active banned deleted].freeze
11
+ private_constant :INVALID_USER, :RFC3339_OFFSET, :USER_STATUSES
12
+
13
+ private
14
+
15
+ def profile_user_payload(payload)
16
+ raise Error::AuthenticationError, INVALID_USER unless payload.is_a?(Hash)
17
+
18
+ profile_json_object(payload['user'])
19
+ end
20
+
21
+ def profile_json_object(payload)
22
+ raise Error::AuthenticationError, INVALID_USER unless payload.is_a?(Hash)
23
+
24
+ payload.to_h do |key, value|
25
+ raise Error::AuthenticationError, INVALID_USER unless key.is_a?(String)
26
+
27
+ [key, profile_json_value(value)]
28
+ end
29
+ end
30
+
31
+ def profile_json_value(value)
32
+ case value
33
+ when Array then value.map { |item| profile_json_value(item) }
34
+ when Hash then profile_json_object(value)
35
+ else profile_json_scalar(value)
36
+ end
37
+ end
38
+
39
+ def profile_json_scalar(value)
40
+ case value
41
+ when nil, true, false, Integer, String then value
42
+ when Float then profile_finite_float(value)
43
+ else raise Error::AuthenticationError, INVALID_USER
44
+ end
45
+ end
46
+
47
+ def profile_finite_float(value)
48
+ raise Error::AuthenticationError, INVALID_USER unless value.finite?
49
+
50
+ value
51
+ end
52
+
53
+ def profile_required_string(value)
54
+ raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String)
55
+
56
+ value
57
+ end
58
+
59
+ def profile_required_id(value)
60
+ raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String) && !value.strip.empty?
61
+
62
+ value
63
+ end
64
+
65
+ def profile_status(value)
66
+ raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String) && USER_STATUSES.include?(value)
67
+
68
+ value
69
+ end
70
+
71
+ def profile_optional_string(value)
72
+ return if value.nil?
73
+
74
+ profile_required_string(value)
75
+ end
76
+
77
+ def profile_optional_object(value)
78
+ profile_json_object(value) unless value.nil?
79
+ end
80
+
81
+ def profile_optional_boolean(value)
82
+ return if value.nil?
83
+ return true if value == true
84
+ return false if value == false
85
+
86
+ raise Error::AuthenticationError, INVALID_USER
87
+ end
88
+
89
+ def profile_parse_time(value)
90
+ return if value.nil?
91
+ raise Error::AuthenticationError, INVALID_USER unless value.is_a?(String) && RFC3339_OFFSET.match?(value)
92
+
93
+ Time.iso8601(value).freeze
94
+ rescue ArgumentError
95
+ raise Error::AuthenticationError, INVALID_USER
96
+ end
97
+ end
98
+ end
@@ -2,12 +2,16 @@
2
2
 
3
3
  module Volcano
4
4
  # Refreshes only the session captured by the initiating operation.
5
- class Auth
5
+ module AuthRefresh
6
+ include AuthNotificationDispatch
7
+ include AuthSessionBinding
8
+
6
9
  def refresh_session
7
10
  binding = @client.capture_session_binding
8
- raise Error::AuthenticationError, 'No active session' unless binding.last
11
+ session = binding.last
12
+ raise Error::AuthenticationError, 'No active session' unless session
9
13
 
10
- refresh_captured_session(binding)
14
+ refresh_captured_session([binding.first, binding[1], session])
11
15
  end
12
16
 
13
17
  def session_request(binding: @client.capture_session_binding)
@@ -26,7 +30,7 @@ module Volcano
26
30
  response
27
31
  end
28
32
 
29
- alias session_read session_request
33
+ def session_read(...) = session_request(...)
30
34
 
31
35
  private
32
36
 
@@ -51,6 +55,7 @@ module Volcano
51
55
  end
52
56
 
53
57
  def refresh_with_notifications(binding)
58
+ # @type var notifications: Array[Method]
54
59
  notifications = []
55
60
  active_generation, = owned_session_binding(binding)
56
61
  binding[1].refresh { perform_refresh(binding, notifications) } if active_generation == binding.first
@@ -60,31 +65,17 @@ module Volcano
60
65
  raise
61
66
  ensure
62
67
  # Only the dispatcher owner drains, after refresh coordination is released.
63
- notifications.each(&:call)
64
- end
65
-
66
- def owned_session_binding(binding)
67
- active = @client.capture_session_binding
68
- raise Error::AuthenticationError, 'No active session' if rejected_refresh?(binding, active)
69
-
70
- _, active_lineage, current = active
71
- raise Error::SessionChangedError unless current && active_lineage == binding[1]
72
-
73
- active
74
- end
75
-
76
- def rejected_refresh?(binding, active)
77
- generation, lineage, = binding
78
- @client.refresh_rejected?(generation, lineage) &&
79
- active.first == generation + 1 && active.last.nil?
68
+ run_auth_notifications(notifications)
80
69
  end
81
70
 
82
71
  def perform_refresh(binding, notifications)
83
72
  active = owned_session_binding(binding)
84
73
  return active.last if active.first != binding.first
85
- raise Error::AuthenticationError, 'No refresh token' unless binding.last.refresh_token
86
74
 
87
- session = refreshed_session(binding, notifications)
75
+ refresh_token = binding.last.refresh_token
76
+ raise Error::AuthenticationError, 'No refresh token' unless refresh_token
77
+
78
+ session = refreshed_session(binding, refresh_token, notifications)
88
79
  unless binding[1].closing?
89
80
  @client.store_session_if_current?(
90
81
  session, binding.first, event: :token_refreshed, notifications: notifications
@@ -93,17 +84,23 @@ module Volcano
93
84
  session
94
85
  end
95
86
 
96
- def refreshed_session(binding, notifications)
87
+ def refreshed_session(binding, refresh_token, notifications)
97
88
  _, owner, current = binding
98
89
  verified = owner.verified_pair?(current)
99
- SessionCredentials.validate_refresh_source(current, verified: verified)
100
- owner.verify_pair(nil)
101
- session = parse_refresh_session(refresh_payload(binding, notifications))
90
+ begin
91
+ SessionCredentials.validate_refresh_source(current, verified: verified)
92
+ owner.verify_pair(nil)
93
+ parse_and_verify_refresh(owner, current, binding, refresh_token, notifications)
94
+ rescue Error::RateLimitedError
95
+ owner.verify_pair(current) if verified
96
+ raise
97
+ end
98
+ end
99
+
100
+ def parse_and_verify_refresh(owner, current, binding, refresh_token, notifications)
101
+ session = parse_refresh_session(refresh_payload(binding, refresh_token, notifications))
102
102
  verify_refreshed_session(owner, current, session)
103
103
  session
104
- rescue Error::RateLimitedError
105
- owner.verify_pair(current) if verified
106
- raise
107
104
  end
108
105
 
109
106
  def verify_refreshed_session(owner, current, session)
@@ -111,8 +108,8 @@ module Volcano
111
108
  owner.verify_pair(session)
112
109
  end
113
110
 
114
- def refresh_payload(binding, notifications)
115
- Transport.body(refresh_response(binding.last.refresh_token), 200)
111
+ def refresh_payload(binding, refresh_token, notifications)
112
+ Transport.body(refresh_response(refresh_token), 200)
116
113
  rescue Error::AuthenticationError
117
114
  generation, owner, = binding
118
115
  @client.reject_refresh_if_current?(generation, notifications: notifications) unless owner.closing?
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ # Rejects stale or replaced auth-session lineages.
5
+ module AuthSessionBinding
6
+ private
7
+
8
+ def owned_session_binding(binding)
9
+ active = @client.capture_session_binding
10
+ raise Error::AuthenticationError, 'No active session' if rejected_refresh?(binding, active)
11
+
12
+ _, active_lineage, current = active
13
+ raise Error::SessionChangedError unless current && active_lineage == binding[1]
14
+
15
+ [active.first, active_lineage, current]
16
+ end
17
+
18
+ def rejected_refresh?(binding, active)
19
+ generation, lineage, = binding
20
+ @client.refresh_rejected?(generation, lineage) &&
21
+ active.first == generation + 1 && active.last.nil?
22
+ end
23
+ end
24
+ private_constant :AuthSessionBinding
25
+ end
@@ -1,49 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
3
+ require_relative 'session_page_mapping'
4
4
 
5
5
  module Volcano
6
- # Maps the internal wire response to immutable public session values.
7
- module SessionPageMapping
8
- SESSION_FIELDS = %i[
9
- id user_id provider expires_at is_active is_current user_agent ip_address last_ip_address
10
- last_activity_at session_started_at created_at updated_at
11
- ].freeze
12
- REQUIRED_SESSION_FIELDS = SESSION_FIELDS.first(6).freeze
13
-
14
- private
15
-
16
- def session_page(body)
17
- raise TypeError, 'Expected a complete session page' unless body.is_a?(Hash)
18
-
19
- sessions = body.fetch('sessions')
20
- raise TypeError, 'Expected a complete session page' unless sessions.is_a?(Array)
21
-
22
- SessionPage.new(
23
- sessions: sessions.map { |attributes| auth_session(attributes) },
24
- total: body.fetch('total'), page: body.fetch('page'), limit: body.fetch('limit'),
25
- total_pages: body.fetch('total_pages')
26
- )
27
- rescue KeyError
28
- raise TypeError, 'Expected a complete session page'
29
- end
30
-
31
- def auth_session(attributes)
32
- raise TypeError, 'Expected a complete authentication session' unless attributes.is_a?(Hash)
33
-
34
- required = REQUIRED_SESSION_FIELDS.to_h { |name| [name, attributes.fetch(name.to_s)] }
35
- optional = (SESSION_FIELDS - REQUIRED_SESSION_FIELDS).to_h do |name|
36
- [name, attributes[name.to_s]]
37
- end
38
- AuthSession.new(**required, **optional)
39
- rescue KeyError
40
- raise TypeError, 'Expected a complete authentication session'
41
- end
42
- end
43
- private_constant :SessionPageMapping
44
-
45
6
  # Multi-device session behavior for the authentication facade.
46
- class Auth
7
+ module AuthSessions
47
8
  include SessionPageMapping
48
9
 
49
10
  def list_sessions(page: 1, limit: 20)
@@ -57,10 +18,11 @@ module Volcano
57
18
 
58
19
  def delete_session(session_id)
59
20
  binding = @client.capture_session_binding
60
- raise Error::AuthenticationError, 'No active session' unless binding.last
21
+ session = binding.last
22
+ raise Error::AuthenticationError, 'No active session' unless session
61
23
 
62
24
  request_id = session_id.dup.freeze
63
- deletes_current = same_session_id?(binding.last.access_token, request_id)
25
+ deletes_current = same_session_id?(session.access_token, request_id)
64
26
  error = delete_bound_session_error(binding, request_id)
65
27
  current_unchanged = session_unchanged_after_deletion?(deletes_current, error, binding)
66
28
  raise Error::SessionChangedError, cause: error unless current_unchanged
@@ -119,12 +81,14 @@ module Volcano
119
81
 
120
82
  def deletion_binding_current?(binding, error)
121
83
  active = @client.capture_session_binding
122
- (active.last && active[1] == binding[1]) || (error && rejected_refresh?(binding, active))
84
+ return true if active.last && active[1] == binding[1]
85
+
86
+ !error.nil? && rejected_refresh?(binding, active)
123
87
  end
124
88
 
125
89
  def same_session_id?(access_token, session_id)
126
90
  current_session_id = access_token_session_id(access_token)
127
- session_id.is_a?(String) && current_session_id&.casecmp?(session_id)
91
+ session_id.is_a?(String) && current_session_id&.casecmp?(session_id) == true
128
92
  end
129
93
 
130
94
  def access_token_session_id(access_token)
@@ -2,24 +2,28 @@
2
2
 
3
3
  module Volcano
4
4
  # Revokes the captured server session and clears its local lineage.
5
- class Auth
5
+ module AuthSignOut
6
+ include AuthNotificationDispatch
7
+
6
8
  def sign_out
9
+ # @type var notifications: Array[Method]
10
+ notifications = []
7
11
  binding = @client.capture_session_binding
8
- return binding[1].wait_for_sign_out unless binding.last
12
+ session = binding.last
13
+ return binding[1].wait_for_sign_out unless session
9
14
 
10
- notifications = []
11
15
  binding[1].sign_out do |preceding, pending|
12
- sign_out_captured(binding, preceding, pending, notifications)
16
+ sign_out_captured(binding, session, preceding, pending, notifications)
13
17
  end
14
18
  ensure
15
- notifications&.each(&:call)
19
+ run_auth_notifications(notifications)
16
20
  end
17
21
 
18
22
  private
19
23
 
20
- def sign_out_captured(binding, preceding, pending, notifications)
24
+ def sign_out_captured(binding, session, preceding, pending, notifications)
21
25
  generation, owner, = binding
22
- current, refresh_error = revocation_credentials(binding, preceding)
26
+ current, refresh_error = revocation_credentials(owner, session, preceding)
23
27
  error = revocation_error(current, owner, pending ? refresh_error : nil, joined: pending)
24
28
  cleared = @client.clear_session_if_current?(generation, lineage: owner, notifications: notifications)
25
29
  raise Error::SessionChangedError, cause: error unless cleared
@@ -27,10 +31,13 @@ module Volcano
27
31
  raise error if error
28
32
  end
29
33
 
30
- def revocation_credentials(binding, preceding)
31
- [binding[1].result(preceding) || binding.last, nil]
34
+ def revocation_credentials(owner, session, preceding)
35
+ resolved = owner.result(preceding) || session
36
+ raise TypeError, 'Expected a session from refresh' unless resolved.is_a?(Session)
37
+
38
+ [resolved, nil]
32
39
  rescue Error::VolcanoError => e
33
- [binding.last, e]
40
+ [session, e]
34
41
  end
35
42
 
36
43
  def logout_response(refresh_token)
@@ -67,16 +74,17 @@ module Volcano
67
74
 
68
75
  def revoke_access_session(session, session_id, refresh_error, joined:)
69
76
  error = delete_session_error(session.access_token, session_id)
70
- return error unless refreshable_revocation?(error, session)
77
+ refresh_token = refreshable_revocation_token(error, session)
78
+ return error unless refresh_token
71
79
  return refresh_error || error if joined
72
80
 
73
- refreshed = parse_refresh_session(Transport.body(refresh_response(session.refresh_token), 200))
81
+ refreshed = parse_refresh_session(Transport.body(refresh_response(refresh_token), 200))
74
82
  SessionCredentials.validate_refresh(session, refreshed)
75
83
  delete_session_error(refreshed.access_token, session_id)
76
84
  end
77
85
 
78
- def refreshable_revocation?(error, session)
79
- error&.status == 401 && session.refresh_token
86
+ def refreshable_revocation_token(error, session)
87
+ session.refresh_token if error&.status == 401
80
88
  end
81
89
  end
82
90
  end
@@ -81,9 +81,10 @@ module Volcano
81
81
 
82
82
  def update_user_if_current?(user, generation)
83
83
  @mutex.synchronize do
84
- return false unless generation == @generation && @session
84
+ current = @session
85
+ return false unless generation == @generation && current
85
86
 
86
- @session = SessionCredentials.with_user(@session, user)
87
+ @session = SessionCredentials.with_user(current, user)
87
88
  true
88
89
  end
89
90
  end
@@ -3,8 +3,8 @@
3
3
  module Volcano
4
4
  # Queues auth events under the state mutex and invokes subscribers outside it.
5
5
  module AuthStateNotifications
6
- CALLBACK_FAILURES = [Exception].freeze
7
- private_constant :CALLBACK_FAILURES
6
+ CALLBACK_FAILURE = Exception
7
+ private_constant :CALLBACK_FAILURE
8
8
 
9
9
  private
10
10
 
@@ -26,12 +26,14 @@ module Volcano
26
26
  end
27
27
 
28
28
  def drain_notifications
29
+ # @type var failure: Exception?
29
30
  failure = nil
30
31
  loop do
31
32
  notification = next_notification
32
33
  break unless notification
33
34
 
34
- current_failure = notify(*notification)
35
+ callback_ids, event, session = notification
36
+ current_failure = notify(callback_ids, event, session)
35
37
  failure ||= current_failure
36
38
  end
37
39
  raise failure if failure
@@ -48,12 +50,13 @@ module Volcano
48
50
  end
49
51
 
50
52
  def notify(callback_ids, event, session)
53
+ # @type var failure: Exception?
51
54
  failure = nil
52
55
  callback_ids.each do |callback_id|
53
56
  @mutex.synchronize { @callbacks[callback_id] }&.call(event, session)
54
57
  rescue StandardError => e
55
58
  Warning.warn("Volcano auth-state callback failed (#{e.class})\n")
56
- rescue *CALLBACK_FAILURES => e
59
+ rescue CALLBACK_FAILURE => e
57
60
  unsubscribe(callback_id)
58
61
  failure ||= e
59
62
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Volcano
4
4
  # Current-user update behavior for the authentication facade.
5
- class Auth
5
+ module AuthUpdateUser
6
6
  def update_user(password: nil, metadata: nil)
7
7
  profile_request do
8
8
  request_password = password&.dup&.freeze
@@ -9,79 +9,6 @@ module Volcano
9
9
  end
10
10
  private_constant :SESSION_USER_CODER
11
11
 
12
- # Owns credential snapshots and preserves validated identity during refresh.
13
- module SessionCredentials
14
- def self.build(access_token, refresh_token)
15
- raise ArgumentError, 'refresh_token requires access_token' if access_token.nil? && !refresh_token.nil?
16
- return if access_token.nil?
17
-
18
- Session.new(access_token: credential(access_token, :access_token),
19
- refresh_token: credential(refresh_token, :refresh_token))
20
- end
21
-
22
- def self.credential(value, name)
23
- return if value.nil?
24
-
25
- raise ArgumentError, "#{name} must be a non-empty string" unless value.is_a?(String) && !value.strip.empty?
26
-
27
- value.dup.freeze
28
- end
29
- private_class_method :credential
30
-
31
- def self.validate_refresh_source(current, verified: false)
32
- return if verified || session_id(current.access_token)
33
-
34
- raise Error::AuthenticationError, 'Cannot refresh supplied credentials without a session identifier'
35
- end
36
-
37
- def self.validate_refresh(current, refreshed)
38
- return unless current
39
-
40
- validate_refresh_session_id(current, refreshed)
41
- return unless current.user_id && current.user_id != refreshed&.user_id
42
-
43
- raise Error::AuthenticationError, 'Refreshed session belongs to a different user'
44
- end
45
-
46
- def self.validate_refresh_session_id(current, refreshed)
47
- expected = session_id(current.access_token)
48
- return unless expected && expected != session_id(refreshed&.access_token)
49
-
50
- raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
51
- end
52
- private_class_method :validate_refresh_session_id
53
-
54
- # Untrusted continuity constraint, never authenticated user identity.
55
- def self.session_id(access_token)
56
- payload = token_payload(access_token)
57
- return unless payload.is_a?(Hash)
58
-
59
- value = payload['session_id']
60
- value.downcase if value.is_a?(String) && value.match?(/\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i)
61
- end
62
-
63
- def self.token_payload(access_token)
64
- return unless access_token.is_a?(String)
65
-
66
- parts = access_token.split('.')
67
- return unless parts.length == 3
68
-
69
- encoded = parts.fetch(1).tr('-_', '+/')
70
- JSON.parse((encoded + ('=' * (-encoded.length % 4))).unpack1('m0'))
71
- rescue ArgumentError, JSON::ParserError
72
- nil
73
- end
74
- private_class_method :token_payload
75
-
76
- def self.with_user(session, user)
77
- user_id = (session.user_id || user.fetch('id')).dup.freeze
78
- raise Error::AuthenticationError, 'Profile belongs to a different user' unless user['id'] == user_id
79
-
80
- Session.new(**session.to_h, user_id: user_id, user: user)
81
- end
82
- end
83
- private_constant :SessionCredentials
84
-
85
12
  # Local credentials; refresh credentials and user identity may be unknown.
86
13
  Session = Data.define(:access_token, :refresh_token, :user_id, :user) do
87
14
  def initialize(access_token:, refresh_token: nil, user_id: nil, user: nil)
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'json'
5
+ require_relative 'session'
6
+
7
+ module Volcano
8
+ # Owns credential snapshots and preserves validated identity during refresh.
9
+ module SessionCredentials
10
+ def self.build(access_token, refresh_token)
11
+ raise ArgumentError, 'refresh_token requires access_token' if access_token.nil? && !refresh_token.nil?
12
+ return if access_token.nil?
13
+
14
+ Session.new(access_token: credential(access_token, :access_token),
15
+ refresh_token: refresh_token.nil? ? nil : credential(refresh_token, :refresh_token))
16
+ end
17
+
18
+ def self.credential(value, name)
19
+ raise ArgumentError, "#{name} must be a non-empty string" unless value.is_a?(String) && !value.strip.empty?
20
+
21
+ value.dup.freeze
22
+ end
23
+ private_class_method :credential
24
+
25
+ def self.validate_refresh_source(current, verified: false)
26
+ return if verified || session_id(current.access_token)
27
+
28
+ raise Error::AuthenticationError, 'Cannot refresh supplied credentials without a session identifier'
29
+ end
30
+
31
+ def self.validate_refresh(current, refreshed)
32
+ return unless current
33
+
34
+ validate_refresh_session_id(current, refreshed)
35
+ return unless current.user_id && current.user_id != refreshed&.user_id
36
+
37
+ raise Error::AuthenticationError, 'Refreshed session belongs to a different user'
38
+ end
39
+
40
+ def self.validate_refresh_session_id(current, refreshed)
41
+ expected = session_id(current.access_token)
42
+ return unless expected && expected != session_id(refreshed&.access_token)
43
+
44
+ raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
45
+ end
46
+ private_class_method :validate_refresh_session_id
47
+
48
+ # Untrusted continuity constraint, never authenticated user identity.
49
+ def self.session_id(access_token)
50
+ payload = token_payload(access_token)
51
+ return unless payload.is_a?(Hash)
52
+
53
+ value = payload['session_id']
54
+ value.downcase if value.is_a?(String) && value.match?(/\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i)
55
+ end
56
+
57
+ def self.token_payload(access_token)
58
+ return unless access_token.is_a?(String)
59
+
60
+ parts = access_token.split('.')
61
+ return unless parts.length == 3
62
+
63
+ encoded = parts.fetch(1).tr('-_', '+/')
64
+ JSON.parse(Base64.strict_decode64(encoded + ('=' * (-encoded.length % 4))))
65
+ rescue ArgumentError, JSON::ParserError
66
+ nil
67
+ end
68
+ private_class_method :token_payload
69
+
70
+ def self.with_user(session, user)
71
+ user_id = session.user_id || user.fetch('id')
72
+ raise Error::AuthenticationError, 'Profile has no valid user identifier' unless user_id.is_a?(String)
73
+
74
+ user_id = user_id.dup.freeze
75
+ raise Error::AuthenticationError, 'Profile belongs to a different user' unless user['id'] == user_id
76
+
77
+ Session.new(access_token: session.access_token, refresh_token: session.refresh_token,
78
+ user_id: user_id, user: user)
79
+ end
80
+ end
81
+ private_constant :SessionCredentials
82
+ end
@@ -42,7 +42,9 @@ module Volcano
42
42
  def refresh(&)
43
43
  operation, owner = @mutex.synchronize do
44
44
  raise Error::SessionChangedError if @sign_out || @locally_cleared
45
- next [@refresh, false] if @refresh && !@refresh.done
45
+
46
+ current = @refresh
47
+ next [current, false] if current && !current.done
46
48
 
47
49
  @refresh = Outcome.new(false)
48
50
  [@refresh, true]
@@ -51,11 +53,7 @@ module Volcano
51
53
  end
52
54
 
53
55
  def sign_out
54
- operation, owner, preceding, pending = @mutex.synchronize do
55
- first = @sign_out.nil?
56
- @sign_out ||= Outcome.new(false)
57
- [@sign_out, first, @refresh, @refresh && !@refresh.done]
58
- end
56
+ operation, owner, preceding, pending = @mutex.synchronize { claim_sign_out }
59
57
  execute(operation, owner) do
60
58
  yield(preceding, pending)
61
59
  ensure
@@ -79,10 +77,19 @@ module Volcano
79
77
 
80
78
  private
81
79
 
80
+ def claim_sign_out
81
+ first = @sign_out.nil? == true
82
+ operation = @sign_out ||= Outcome.new(false)
83
+ current = @refresh
84
+ [operation, first, current, current && !current.done]
85
+ end
86
+
82
87
  def execute(operation, owner)
83
88
  return result(operation) unless owner
84
89
 
85
90
  completed = false
91
+ # @type var value: Object?
92
+ value = nil
86
93
  begin
87
94
  value = yield
88
95
  completed = true
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ # Maps the internal wire response to immutable public session values.
5
+ module SessionPageMapping
6
+ private
7
+
8
+ def session_page(body)
9
+ raise TypeError, 'Expected a complete session page' unless body.is_a?(Hash)
10
+
11
+ sessions = body.fetch('sessions')
12
+ raise TypeError, 'Expected a complete session page' unless sessions.is_a?(Array)
13
+
14
+ SessionPage.new(
15
+ sessions: sessions.map { |attributes| auth_session(attributes) },
16
+ total: required_integer(body, 'total'), page: required_integer(body, 'page'),
17
+ limit: required_integer(body, 'limit'), total_pages: required_integer(body, 'total_pages')
18
+ )
19
+ rescue KeyError
20
+ raise TypeError, 'Expected a complete session page'
21
+ end
22
+
23
+ def auth_session(attributes)
24
+ raise TypeError, 'Expected a complete authentication session' unless attributes.is_a?(Hash)
25
+
26
+ build_auth_session(attributes)
27
+ rescue KeyError
28
+ raise TypeError, 'Expected a complete authentication session'
29
+ end
30
+
31
+ def build_auth_session(attributes)
32
+ AuthSession.new(
33
+ id: required_string(attributes, 'id'), user_id: required_string(attributes, 'user_id'),
34
+ provider: required_string(attributes, 'provider'), expires_at: required_time(attributes, 'expires_at'),
35
+ is_active: required_boolean(attributes, 'is_active'), is_current: required_boolean(attributes, 'is_current'),
36
+ user_agent: optional_string(attributes, 'user_agent'), ip_address: optional_string(attributes, 'ip_address'),
37
+ last_ip_address: optional_string(attributes, 'last_ip_address'),
38
+ last_activity_at: optional_time(attributes, 'last_activity_at'),
39
+ session_started_at: optional_time(attributes, 'session_started_at'),
40
+ created_at: optional_time(attributes, 'created_at'), updated_at: optional_time(attributes, 'updated_at')
41
+ )
42
+ end
43
+
44
+ def required_integer(body, name)
45
+ value = body.fetch(name)
46
+ raise TypeError, 'Expected a complete session page' unless value.is_a?(Integer)
47
+
48
+ value
49
+ end
50
+
51
+ def required_string(body, name)
52
+ value = body.fetch(name)
53
+ raise TypeError, 'Expected a complete authentication session' unless value.is_a?(String)
54
+
55
+ value
56
+ end
57
+
58
+ def required_time(body, name)
59
+ value = body.fetch(name)
60
+ raise TypeError, 'Expected a complete authentication session' unless value.is_a?(Time)
61
+
62
+ value
63
+ end
64
+
65
+ def required_boolean(body, name)
66
+ value = body.fetch(name)
67
+ unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
68
+ raise TypeError, 'Expected a complete authentication session'
69
+ end
70
+
71
+ value
72
+ end
73
+
74
+ def optional_string(body, name)
75
+ value = body[name]
76
+ raise TypeError, 'Expected a complete authentication session' unless value.nil? || value.is_a?(String)
77
+
78
+ value
79
+ end
80
+
81
+ def optional_time(body, name)
82
+ value = body[name]
83
+ raise TypeError, 'Expected a complete authentication session' unless value.nil? || value.is_a?(Time)
84
+
85
+ value
86
+ end
87
+ end
88
+ private_constant :SessionPageMapping
89
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Volcano
4
- VERSION = '0.29.0'
4
+ VERSION = '0.30.0'
5
5
  end
data/lib/volcano.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'volcano/connection_string'
5
5
  require_relative 'volcano/models'
6
6
  require_relative 'volcano/user'
7
7
  require_relative 'volcano/session'
8
+ require_relative 'volcano/session_credentials'
8
9
  require_relative 'volcano/function_response'
9
10
  require_relative 'volcano/durable_models'
10
11
  require_relative 'volcano/log_responses'
@@ -16,16 +17,21 @@ require_relative 'volcano/errors'
16
17
  require_relative 'volcano/redaction'
17
18
  require_relative 'volcano/transport'
18
19
  require_relative 'volcano/generated_transport'
19
- require_relative 'volcano/auth'
20
+ require_relative 'volcano/auth_notification_dispatch'
21
+ require_relative 'volcano/auth_session_binding'
20
22
  require_relative 'volcano/auth_sign_out'
21
23
  require_relative 'volcano/auth_refresh'
24
+ require_relative 'volcano/auth_profile_fields'
25
+ require_relative 'volcano/auth_get_user'
26
+ require_relative 'volcano/auth_update_user'
27
+ require_relative 'volcano/auth_email_change'
28
+ require_relative 'volcano/auth_sessions'
29
+ require_relative 'volcano/auth'
22
30
  require_relative 'volcano/auth_sign_up'
23
31
  require_relative 'volcano/sign_up_result'
24
32
  require_relative 'volcano/auth_anonymous'
25
33
  require_relative 'volcano/auth_confirm_email'
26
34
  require_relative 'volcano/auth_resend_confirmation'
27
- require_relative 'volcano/auth_email_change'
28
- require_relative 'volcano/auth_sessions'
29
35
  require_relative 'volcano/auth_oauth'
30
36
  require_relative 'volcano/auth_oauth_sign_in'
31
37
  require_relative 'volcano/auth_hosted'
@@ -33,8 +39,6 @@ require_relative 'volcano/auth_oauth_token'
33
39
  require_relative 'volcano/auth_oauth_api'
34
40
  require_relative 'volcano/auth_reset_password_for_email'
35
41
  require_relative 'volcano/auth_reset_password'
36
- require_relative 'volcano/auth_get_user'
37
- require_relative 'volcano/auth_update_user'
38
42
  require_relative 'volcano/database'
39
43
  require_relative 'volcano/function_resolution'
40
44
  require_relative 'volcano/functions'
@@ -58,8 +62,10 @@ require_relative 'volcano/client'
58
62
 
59
63
  # Public namespace for the Volcano Ruby SDK.
60
64
  module Volcano
61
- private_constant :GeneratedTransport, :LockAutoRenewal, :LockLeaseClock, :LockRenewer, :LockSession, :LockResponse,
62
- :Redaction, :SessionOperations
65
+ private_constant :AuthRefresh, :AuthSignOut, :AuthProfile, :AuthProfileFields, :AuthUpdateUser, :AuthEmailChange,
66
+ :AuthSessions,
67
+ :GeneratedTransport, :LockAutoRenewal, :LockLeaseClock, :LockRenewer,
68
+ :LockSession, :LockResponse, :Redaction, :SessionOperations
63
69
 
64
70
  Realtime.private_constant :Protocol, :ProtocolDispatch, :ProtocolPublicationDispatch, :ProtocolRecovery,
65
71
  :ProtocolRecoveryDispatch, :ProtocolRecoveryPosition
@@ -0,0 +1,16 @@
1
+ module Volcano
2
+ type oauth_api_input = session_input
3
+
4
+ class Auth
5
+ def list_linked_oauth_providers: () -> Array[LinkedOAuthProvider]
6
+ def link_oauth_provider: (String provider) -> String
7
+ def unlink_oauth_provider: (String provider) -> nil
8
+ def get_oauth_provider_token: (String provider) -> OAuthProviderTokenStatus
9
+ def refresh_oauth_provider_token: (String provider) -> OAuthProviderTokenStatus
10
+ def call_oauth_api: (String provider, endpoint: String, ?method: String, ?body: oauth_api_input) -> Object?
11
+ def sign_in_with_oauth: (String provider, redirect_to: String, state: String) -> String
12
+ def exchange_oauth_code: (code: String, redirect_to: String, state: String, expected_state: String) -> Session
13
+ def get_hosted_auth_url: (project_id: String, state: String, ?action: String) -> String
14
+ def adopt_hosted_auth_session: (Session session, state: String, expected_state: String) -> Session
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module Volcano
2
+ class Auth
3
+ def user: () -> User
4
+ def get_user: () -> User
5
+ def update_user: (?password: String?, ?metadata: session_user_input?) -> User
6
+ def request_email_change: (new_email: String) -> EmailChangeResult
7
+ def cancel_email_change: () -> nil
8
+ def confirm_email_change: (token: String) -> User
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Volcano
2
+ class Auth
3
+ def list_sessions: (?page: Integer, ?limit: Integer) -> SessionPage
4
+ def delete_all_other_sessions: () -> nil
5
+ def delete_session: (String session_id) -> nil
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module Volcano
2
+ class AuthSubscription
3
+ def initialize: () ?{ () -> void } -> void
4
+ def unsubscribe: () -> nil
5
+ end
6
+ end
data/sig/session.rbs CHANGED
@@ -1,7 +1,7 @@
1
1
  module Volcano
2
2
  type json_value = nil | bool | Integer | Float | String | Array[json_value] | Hash[String, json_value]
3
3
  type session_input = nil | bool | Integer | Float | String | Symbol | Time | Array[session_input] | Hash[String | Symbol, session_input]
4
- type session_user_input = Hash[String, session_input] | Hash[Symbol, session_input] | Hash[String | Symbol, session_input]
4
+ type session_user_input = Hash[String, json_value] | Hash[String, session_input] | Hash[Symbol, session_input] | Hash[String | Symbol, session_input]
5
5
 
6
6
  class Session < Data
7
7
  attr_reader access_token: String
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: volcano-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kong
@@ -136,14 +136,17 @@ files:
136
136
  - lib/volcano/auth_email_change.rb
137
137
  - lib/volcano/auth_get_user.rb
138
138
  - lib/volcano/auth_hosted.rb
139
+ - lib/volcano/auth_notification_dispatch.rb
139
140
  - lib/volcano/auth_oauth.rb
140
141
  - lib/volcano/auth_oauth_api.rb
141
142
  - lib/volcano/auth_oauth_sign_in.rb
142
143
  - lib/volcano/auth_oauth_token.rb
144
+ - lib/volcano/auth_profile_fields.rb
143
145
  - lib/volcano/auth_refresh.rb
144
146
  - lib/volcano/auth_resend_confirmation.rb
145
147
  - lib/volcano/auth_reset_password.rb
146
148
  - lib/volcano/auth_reset_password_for_email.rb
149
+ - lib/volcano/auth_session_binding.rb
147
150
  - lib/volcano/auth_sessions.rb
148
151
  - lib/volcano/auth_sign_out.rb
149
152
  - lib/volcano/auth_sign_up.rb
@@ -592,7 +595,9 @@ files:
592
595
  - lib/volcano/realtime/reconnect.rb
593
596
  - lib/volcano/redaction.rb
594
597
  - lib/volcano/session.rb
598
+ - lib/volcano/session_credentials.rb
595
599
  - lib/volcano/session_operations.rb
600
+ - lib/volcano/session_page_mapping.rb
596
601
  - lib/volcano/sign_up_result.rb
597
602
  - lib/volcano/storage.rb
598
603
  - lib/volcano/storage_models.rb
@@ -604,7 +609,11 @@ files:
604
609
  - lib/volcano/transport.rb
605
610
  - lib/volcano/user.rb
606
611
  - lib/volcano/version.rb
612
+ - sig/auth_oauth.rbs
613
+ - sig/auth_profile.rbs
607
614
  - sig/auth_session_records.rbs
615
+ - sig/auth_sessions.rbs
616
+ - sig/auth_subscription.rbs
608
617
  - sig/connection_string.rbs
609
618
  - sig/durable_records.rbs
610
619
  - sig/email_change_result.rbs