volcano-sdk 0.29.0 → 0.31.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/volcano/auth.rb +10 -47
  3. data/lib/volcano/auth_email_change.rb +1 -1
  4. data/lib/volcano/auth_get_user.rb +36 -77
  5. data/lib/volcano/auth_notification_dispatch.rb +16 -0
  6. data/lib/volcano/auth_oauth.rb +4 -0
  7. data/lib/volcano/auth_oauth_api.rb +8 -3
  8. data/lib/volcano/auth_oauth_sign_in.rb +1 -1
  9. data/lib/volcano/auth_oauth_token.rb +2 -0
  10. data/lib/volcano/auth_profile_fields.rb +98 -0
  11. data/lib/volcano/auth_refresh.rb +29 -32
  12. data/lib/volcano/auth_session_binding.rb +25 -0
  13. data/lib/volcano/auth_session_construction.rb +67 -0
  14. data/lib/volcano/auth_sessions.rb +9 -45
  15. data/lib/volcano/auth_sign_out.rb +22 -14
  16. data/lib/volcano/auth_sign_up.rb +2 -0
  17. data/lib/volcano/auth_state.rb +3 -2
  18. data/lib/volcano/auth_state_notifications.rb +7 -4
  19. data/lib/volcano/auth_update_user.rb +1 -1
  20. data/lib/volcano/session.rb +0 -73
  21. data/lib/volcano/session_credentials.rb +82 -0
  22. data/lib/volcano/session_operations.rb +13 -6
  23. data/lib/volcano/session_page_mapping.rb +89 -0
  24. data/lib/volcano/storage.rb +18 -34
  25. data/lib/volcano/storage_models.rb +18 -5
  26. data/lib/volcano/storage_mutations.rb +8 -3
  27. data/lib/volcano/storage_object_response.rb +88 -0
  28. data/lib/volcano/storage_response.rb +92 -0
  29. data/lib/volcano/storage_resumable_upload.rb +13 -2
  30. data/lib/volcano/storage_session_adapter.rb +61 -0
  31. data/lib/volcano/storage_transport_adapter.rb +73 -0
  32. data/lib/volcano/storage_upload_models.rb +51 -27
  33. data/lib/volcano/storage_upload_sessions.rb +1 -35
  34. data/lib/volcano/version.rb +1 -1
  35. data/lib/volcano.rb +18 -7
  36. data/sig/auth_credentials.rbs +15 -0
  37. data/sig/auth_oauth.rbs +16 -0
  38. data/sig/auth_profile.rbs +10 -0
  39. data/sig/auth_sessions.rbs +7 -0
  40. data/sig/auth_subscription.rbs +6 -0
  41. data/sig/session.rbs +1 -1
  42. data/sig/storage.rbs +29 -0
  43. data/sig/storage_records.rbs +3 -1
  44. data/sig/upload_session_status.rbs +3 -1
  45. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e946d4476603cf2c234194f51e63aadd8ca1327719bcae02bf098de61049b65b
4
- data.tar.gz: 38d3aa7dbbc508796620f75285b2a483b70a9e2d49a255aded0dba3fa023334b
3
+ metadata.gz: 36cf6c142c98f421f086d919b4d5e83034e95831d2b4246a892be57ebfaea9d5
4
+ data.tar.gz: 877909af108367683c4cbb434235baa27669af9d303dd0b52307919446f8c348
5
5
  SHA512:
6
- metadata.gz: 014b16629b5d06e89d39f010b1c8cb550582f3f7f224b6c6911324c6b2b07196eed9729305c2edd623d6fbedb2ba12a9460d4d128bda61202c9c88874823fada
7
- data.tar.gz: d98691e237fd5cdb277f701701e3549aa6e533179d36c0039e188a66143fe5992a1ac9565f210304704838d2e855a9512e9e2e60de7ec0b2abfa9a8a30ae4cb2
6
+ metadata.gz: 832893a1b1ba6caa88f144b81d8b8e155d2041be82fccffe4328eeb1f59f4fc8e3ce84adbb476f3b1a6b26ed719da7293f9dfb6a563270cdcb5222125661f378
7
+ data.tar.gz: acfb489b98c3e8dcca7918ee1136d4e04f62fec2a7287fcc16fcd952edd3bfae2d4973b6307ef53d577a08cb4104511666917e4435c3d7722bb3cde544d7ad3d
data/lib/volcano/auth.rb CHANGED
@@ -3,8 +3,13 @@
3
3
  module Volcano
4
4
  # Authenticates users and updates the client session.
5
5
  class Auth
6
- INCOMPLETE_SESSION = 'Expected a complete Volcano::Session'
7
- private_constant :INCOMPLETE_SESSION
6
+ include AuthRefresh
7
+ include AuthSignOut
8
+ include AuthProfile
9
+ include AuthUpdateUser
10
+ include AuthEmailChange
11
+ include AuthSessions
12
+ include AuthSessionConstruction
8
13
 
9
14
  def initialize(client, transport, api_url:)
10
15
  @client = client
@@ -20,10 +25,10 @@ module Volcano
20
25
  @client.store_session(owned_complete_session(session), event: nil)
21
26
  end
22
27
 
23
- def on_auth_state_change(&callback)
24
- raise ArgumentError, 'callback block required' unless callback
28
+ def on_auth_state_change(&)
29
+ raise ArgumentError, 'callback block required' unless block_given?
25
30
 
26
- @client.subscribe_auth_state_change(&callback)
31
+ @client.subscribe_auth_state_change(&)
27
32
  end
28
33
 
29
34
  def sign_in(email:, password:)
@@ -70,47 +75,5 @@ module Volcano
70
75
  )
71
76
  end
72
77
  end
73
-
74
- def build_session(payload)
75
- owned_session(
76
- access_token: payload.fetch('access_token'),
77
- refresh_token: payload.fetch('refresh_token'),
78
- user_id: payload.fetch('user').fetch('id'),
79
- user: payload.fetch('user')
80
- )
81
- end
82
-
83
- def parse_refresh_session(payload)
84
- owned_complete_session(build_session(payload))
85
- rescue KeyError, TypeError, NoMethodError, ArgumentError => e
86
- raise Error::TransportError, INCOMPLETE_SESSION, cause: e
87
- end
88
-
89
- def complete_session?(session)
90
- return false unless session.is_a?(Session)
91
-
92
- values = [session.access_token, session.refresh_token, session.user_id]
93
- values.all? { |value| value.is_a?(String) && !value.strip.empty? } &&
94
- matching_session_user?(session)
95
- end
96
-
97
- def matching_session_user?(session)
98
- session.user.nil? || (session.user.is_a?(Hash) && session.user['id'] == session.user_id)
99
- end
100
-
101
- def owned_complete_session(session)
102
- raise ArgumentError, INCOMPLETE_SESSION unless complete_session?(session)
103
-
104
- owned_session(**session.to_h)
105
- end
106
-
107
- def owned_session(access_token:, refresh_token:, user_id:, user: nil)
108
- Session.new(
109
- access_token: access_token.dup.freeze,
110
- refresh_token: refresh_token.dup.freeze,
111
- user_id: user_id.dup.freeze,
112
- user: user
113
- )
114
- end
115
78
  end
116
79
  end
@@ -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
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ # Builds and validates owned credential snapshots for the auth facade.
5
+ module AuthSessionConstruction
6
+ INCOMPLETE_SESSION = 'Expected a complete Volcano::Session'
7
+ private_constant :INCOMPLETE_SESSION
8
+
9
+ private
10
+
11
+ def build_session(payload)
12
+ raise TypeError, INCOMPLETE_SESSION unless payload.is_a?(Hash)
13
+
14
+ user = payload.fetch('user')
15
+ raise TypeError, INCOMPLETE_SESSION unless user.is_a?(Hash)
16
+
17
+ owned_session(
18
+ access_token: required_session_string(payload.fetch('access_token')),
19
+ refresh_token: required_session_string(payload.fetch('refresh_token')),
20
+ user_id: required_session_string(user.fetch('id')),
21
+ user: user
22
+ )
23
+ end
24
+
25
+ def parse_refresh_session(payload)
26
+ owned_complete_session(build_session(payload))
27
+ rescue KeyError, TypeError, NoMethodError, ArgumentError => e
28
+ raise Error::TransportError, INCOMPLETE_SESSION, cause: e
29
+ end
30
+
31
+ def complete_session?(session)
32
+ values = [session.access_token, session.refresh_token, session.user_id]
33
+ values.all? { |value| value.is_a?(String) && !value.strip.empty? } &&
34
+ matching_session_user?(session)
35
+ end
36
+
37
+ def matching_session_user?(session)
38
+ session.user.nil? || (session.user.is_a?(Hash) && session.user['id'] == session.user_id)
39
+ end
40
+
41
+ def owned_complete_session(session)
42
+ raise ArgumentError, INCOMPLETE_SESSION unless session.is_a?(Session) && complete_session?(session)
43
+
44
+ owned_session(
45
+ access_token: required_session_string(session.access_token),
46
+ refresh_token: required_session_string(session.refresh_token),
47
+ user_id: required_session_string(session.user_id),
48
+ user: session.user
49
+ )
50
+ end
51
+
52
+ def required_session_string(value)
53
+ return value if value.is_a?(String) && !value.strip.empty?
54
+
55
+ raise ArgumentError, INCOMPLETE_SESSION
56
+ end
57
+
58
+ def owned_session(access_token:, refresh_token:, user_id:, user: nil)
59
+ Session.new(
60
+ access_token: access_token.dup.freeze,
61
+ refresh_token: refresh_token.dup.freeze,
62
+ user_id: user_id.dup.freeze,
63
+ user: user
64
+ )
65
+ end
66
+ end
67
+ end