volcano-sdk 0.7.2 → 0.9.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: b992adf24aad6b7edea16323e1f9b21c8db08cefe1380cffaf12c99ed436baf4
4
- data.tar.gz: 3770340779a1a5cf0b985b6290a185223632f85863e56ab33c4f8f443ce02f0e
3
+ metadata.gz: bf20d0cdb7959be9f989d8b1f1e5f88b53a065a7b899b42f7c3a09af5d1327c6
4
+ data.tar.gz: 3350d3a721b2d25b67aaf0a76a8cab45cb1ca26c69d386c600789bb60589cfca
5
5
  SHA512:
6
- metadata.gz: 8bd2583130b36e6ede81cdb096cfe97cff10aa9aebe8fb086cff5dd1d13c8883f6bc196b9d528c4e437fb7533df568aab7fce759d0a79f134983b825b730f9d7
7
- data.tar.gz: d5118276bfe5a10d157e637643827a07586cba53d632b5f966501f3ffa9bf5562c1d1a682d8286026f0109f77b19cf701cd3a536d0fc419badf7e34e8d9f3a89
6
+ metadata.gz: 94a0671011cb9256c3bf768fe05fbea0e1d49fb8c3d29adb694cc5a01eb83e80fb0d978b4aa1c83919a3b38ca7cdd6069a7af5b41eaefa743622bdc745f68083
7
+ data.tar.gz: 756140f1665c4f360839a37da9ad15de033cf45373bee40a57b7b109d8d0830094af36974923841d538722869bd9f7fe987c7e3f50a55d5b5f9e657d6106d84f
data/README.md CHANGED
@@ -378,6 +378,17 @@ Success returns `nil`. The reset revokes the recovered account's existing
378
378
  sessions and does not sign it in. The client keeps any unrelated local session
379
379
  unchanged; sign in with the new password when the reset flow completes.
380
380
 
381
+ ### Start with a supplied access token
382
+
383
+ Pass `access_token` to `Volcano::Client.new` to start without a refresh token or
384
+ known user identity. Construction makes no request and leaves `refresh_token`,
385
+ `user_id`, and `user` as `nil`. `auth.user` validates and caches the profile
386
+ without changing credentials. Without a refresh token, `refresh_session` raises
387
+ `Volcano::Error::AuthenticationError`. `sign_out` revokes the server session using
388
+ the access token and clears local state. Supply `refresh_token` alongside
389
+ `access_token` to enable refresh.
390
+ See the [token bootstrap example](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/README.md#use-a-supplied-access-token).
391
+
381
392
  ### Adopt an existing session
382
393
 
383
394
  ```ruby
@@ -429,10 +440,11 @@ client.auth.sign_out
429
440
  raise "still signed in" if client.auth.current_session
430
441
  ```
431
442
 
432
- `sign_out` revokes the current refresh token and clears the captured in-memory
443
+ `sign_out` uses the access-token session when available, even if a refresh token was supplied.
444
+ It revokes the captured session and clears the captured in-memory
433
445
  session. It succeeds without a request when no session exists. If revocation
434
- fails, the SDK still clears that session and raises the typed error. A session
435
- established while sign-out is pending remains current.
446
+ fails, the SDK still clears that session and raises the typed error. A concurrent
447
+ refresh of the same session is cleared; a separate sign-in or adoption remains current.
436
448
 
437
449
  ### Invoke a function
438
450
 
@@ -659,6 +671,13 @@ client.locks.with_lock("deploy", ttl: 30) do |guard|
659
671
  end
660
672
  ```
661
673
 
674
+ Acquisition accepts caller-owned UUID `token` and `request_id` values and retries
675
+ an ambiguous transport failure or HTTP 503 once with the same request and credential.
676
+ Retain those IDs to recover an uncertain acquisition. Other lock methods accept
677
+ `request_id`; block-scoped helpers forward initial IDs only to acquisition.
678
+ See the [lock guide](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/locks.md)
679
+ for examples and fencing requirements.
680
+
662
681
  `locks.get` returns immutable lock availability, expiry, and fencing-token
663
682
  state without acquiring the lock.
664
683
  `locks.renew` returns a new immutable lease and leaves the previous value
data/lib/volcano/auth.rb CHANGED
@@ -34,16 +34,6 @@ module Volcano
34
34
  sign_in_for_generation(email:, password:, generation:)
35
35
  end
36
36
 
37
- def sign_out
38
- generation, current = @client.capture_session
39
- return unless current
40
-
41
- error = revocation_error(current.refresh_token)
42
- raise Error::SessionChangedError, cause: error unless @client.clear_session_if_current?(generation)
43
-
44
- raise error if error
45
- end
46
-
47
37
  private
48
38
 
49
39
  def sign_in_for_generation(email:, password:, generation:)
@@ -66,22 +56,6 @@ module Volcano
66
56
  end
67
57
  end
68
58
 
69
- def logout_response(refresh_token)
70
- Transport.invoke do
71
- @transport.auth_logout(
72
- authorization: @client.anon_token,
73
- refresh_token: refresh_token
74
- )
75
- end
76
- end
77
-
78
- def revocation_error(refresh_token)
79
- Transport.body(logout_response(refresh_token), 204)
80
- nil
81
- rescue Error::VolcanoError => e
82
- e
83
- end
84
-
85
59
  def build_session(payload)
86
60
  owned_session(
87
61
  access_token: payload.fetch('access_token'),
@@ -78,6 +78,9 @@ module Volcano
78
78
  end
79
79
 
80
80
  def perform_refresh(binding, notifications)
81
+ raise Error::AuthenticationError, 'No refresh token' unless binding.last.refresh_token
82
+
83
+ SessionCredentials.validate_refresh_source(binding.last)
81
84
  session = refreshed_session(binding, notifications)
82
85
  stored = @client.store_session_if_current?(
83
86
  session, binding.first, event: :token_refreshed, notifications: notifications
@@ -86,7 +89,11 @@ module Volcano
86
89
  end
87
90
 
88
91
  def refreshed_session(binding, notifications)
89
- owned_complete_session(build_session(refresh_payload(binding, notifications)))
92
+ parse_refresh_session(refresh_payload(binding, notifications))
93
+ end
94
+
95
+ def parse_refresh_session(payload)
96
+ owned_complete_session(build_session(payload))
90
97
  rescue KeyError, TypeError, NoMethodError, ArgumentError => e
91
98
  raise Error::TransportError, INCOMPLETE_SESSION, cause: e
92
99
  end
@@ -124,16 +124,7 @@ module Volcano
124
124
  end
125
125
 
126
126
  def access_token_session_id(access_token)
127
- parts = access_token.split('.')
128
- return unless parts.length == 3
129
-
130
- encoded = parts.fetch(1).tr('-_', '+/')
131
- padding = '=' * (-encoded.length % 4)
132
- payload = JSON.parse((encoded + padding).unpack1('m0'))
133
- session_id = payload['session_id']
134
- session_id if session_id.is_a?(String) && !session_id.empty?
135
- rescue ArgumentError, JSON::ParserError
136
- nil
127
+ SessionCredentials.session_id(access_token)
137
128
  end
138
129
  end
139
130
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ # Revokes the captured server session and clears its local lineage.
5
+ class Auth
6
+ def sign_out
7
+ binding = @client.capture_session_binding
8
+ return unless binding.last
9
+
10
+ notifications = []
11
+ @refresh_lock.synchronize { sign_out_captured(binding, notifications) }
12
+ ensure
13
+ notifications&.each(&:call)
14
+ end
15
+
16
+ private
17
+
18
+ def sign_out_captured(binding, notifications)
19
+ generation, lineage, current = revocation_binding(binding)
20
+ error = revocation_error(current)
21
+ lineage = nil unless access_token_session_id(current.access_token)
22
+ cleared = @client.clear_session_if_current?(generation, lineage: lineage, notifications: notifications)
23
+ raise Error::SessionChangedError, cause: error unless cleared
24
+
25
+ raise error if error
26
+ end
27
+
28
+ def revocation_binding(binding)
29
+ active = @client.capture_session_binding
30
+ active.last && active[1] == binding[1] ? active : binding
31
+ end
32
+
33
+ def logout_response(refresh_token)
34
+ Transport.invoke do
35
+ @transport.auth_logout(
36
+ authorization: @client.anon_token,
37
+ refresh_token: refresh_token
38
+ )
39
+ end
40
+ end
41
+
42
+ def revocation_error(session)
43
+ session_id = access_token_session_id(session.access_token)
44
+ return revoke_access_session(session, session_id) if session_id
45
+ return unless session.refresh_token
46
+
47
+ Transport.body(logout_response(session.refresh_token), 204)
48
+ nil
49
+ rescue Error::VolcanoError => e
50
+ e
51
+ end
52
+
53
+ def revoke_access_session(session, session_id)
54
+ error = delete_session_error(session.access_token, session_id)
55
+ return error unless error&.status == 401 && session.refresh_token
56
+
57
+ refreshed = parse_refresh_session(Transport.body(refresh_response(session.refresh_token), 200))
58
+ SessionCredentials.validate_refresh(session, refreshed)
59
+ delete_session_error(refreshed.access_token, session_id)
60
+ end
61
+ end
62
+ end
@@ -24,11 +24,11 @@ module Volcano
24
24
  CALLBACK_FAILURES = [Exception].freeze
25
25
  private_constant :CALLBACK_FAILURES
26
26
 
27
- def initialize
27
+ def initialize(session: nil)
28
28
  @mutex = Mutex.new
29
29
  @generation = 0
30
30
  @lineage = 0
31
- @session = nil
31
+ @session = session
32
32
  @callbacks = {}
33
33
  @next_callback_id = 0
34
34
  @notifications = []
@@ -48,9 +48,9 @@ module Volcano
48
48
  drain_notifications if dispatch
49
49
  end
50
50
 
51
- def store_if_current?(session, generation, event: :signed_in, notifications: nil)
51
+ def store_if_current?(session, generation, event: :signed_in, notifications: nil, lineage: nil)
52
52
  dispatch = @mutex.synchronize do
53
- return false unless generation == @generation
53
+ return false unless lineage.nil? ? generation == @generation : lineage == @lineage
54
54
 
55
55
  enqueue_notification(replace(session, event), event, session)
56
56
  end
@@ -63,9 +63,8 @@ module Volcano
63
63
  def update_user_if_current?(user, generation)
64
64
  @mutex.synchronize do
65
65
  return false unless generation == @generation && @session
66
- raise Error::AuthenticationError, 'Profile belongs to a different user' unless user['id'] == @session.user_id
67
66
 
68
- @session = Session.new(**@session.to_h, user: user)
67
+ @session = SessionCredentials.with_user(@session, user)
69
68
  true
70
69
  end
71
70
  end
@@ -82,6 +81,7 @@ module Volcano
82
81
  private
83
82
 
84
83
  def replace(session, event)
84
+ SessionCredentials.validate_refresh(@session, session) if event == :token_refreshed
85
85
  @session = session
86
86
  @generation += 1
87
87
  @lineage += 1 unless event == :token_refreshed
@@ -19,13 +19,14 @@ module Volcano
19
19
  api_url: 'https://api.volcano.dev',
20
20
  service_key: nil,
21
21
  timeout: 60,
22
- **adapters
22
+ **options
23
23
  )
24
- transport, socket_factory, reconnect_delay = extract_adapters(adapters)
24
+ session = SessionCredentials.build(options.delete(:access_token), options.delete(:refresh_token))
25
+ transport, socket_factory, reconnect_delay = extract_adapters(options)
25
26
  @api_url = api_url.delete_suffix('/')
26
27
  @anon_key = anon_key
27
28
  @service_key = service_key
28
- @auth_state = AuthState.new
29
+ @auth_state = AuthState.new(session: session)
29
30
  @transport = transport || GeneratedTransport.new(api_url: @api_url, timeout: timeout)
30
31
  initialize_facades(socket_factory, reconnect_delay)
31
32
  end
@@ -81,8 +82,8 @@ module Volcano
81
82
  @auth_state.store_if_current?(session, generation, event: event, notifications: notifications)
82
83
  end
83
84
 
84
- def clear_session_if_current?(generation, event: :signed_out, notifications: nil)
85
- @auth_state.store_if_current?(nil, generation, event: event, notifications: notifications)
85
+ def clear_session_if_current?(generation, event: :signed_out, notifications: nil, lineage: nil)
86
+ @auth_state.store_if_current?(nil, generation, event: event, notifications: notifications, lineage: lineage)
86
87
  end
87
88
 
88
89
  def subscribe_auth_state_change(...)
@@ -47,57 +47,57 @@ module Volcano
47
47
  @api_factory = api_factory || method(:build_apis)
48
48
  end
49
49
 
50
- def acquire_project_lock(authorization:, key:, ttl:, token:)
50
+ def acquire_project_lock(authorization:, key:, ttl:, token:, request_id: SecureRandom.uuid)
51
51
  invoke do
52
52
  apis = @api_factory.call(authorization)
53
53
  body = Generated::ProjectLockLeaseRequest.new(ttl_seconds: ttl)
54
- result = apis.locks.acquire_project_lock_with_http_info(key, token, SecureRandom.uuid, body)
54
+ result = apis.locks.acquire_project_lock_with_http_info(key, token, request_id, body)
55
55
  data, status, headers = result
56
56
  response(data, status, headers)
57
57
  end
58
58
  end
59
59
 
60
- def get_project_lock(authorization:, key:)
60
+ def get_project_lock(authorization:, key:, request_id: SecureRandom.uuid)
61
61
  invoke do
62
62
  apis = @api_factory.call(authorization)
63
63
  data, status, headers = apis.locks.get_project_lock_with_http_info(
64
64
  key,
65
- SecureRandom.uuid
65
+ request_id
66
66
  )
67
67
  response(data, status, headers)
68
68
  end
69
69
  end
70
70
 
71
- def force_release_project_lock(authorization:, key:)
71
+ def force_release_project_lock(authorization:, key:, request_id: SecureRandom.uuid)
72
72
  invoke do
73
73
  apis = @api_factory.call(authorization)
74
74
  data, status, headers = apis.locks.force_release_project_lock_with_http_info(
75
75
  key,
76
- SecureRandom.uuid
76
+ request_id
77
77
  )
78
78
  response(data, status, headers)
79
79
  end
80
80
  end
81
81
 
82
- def renew_project_lock(authorization:, key:, ttl:, token:)
82
+ def renew_project_lock(authorization:, key:, ttl:, token:, request_id: SecureRandom.uuid)
83
83
  invoke do
84
84
  apis = @api_factory.call(authorization)
85
85
  body = Generated::ProjectLockLeaseRequest.new(ttl_seconds: ttl)
86
86
  result = apis.locks.renew_project_lock_with_http_info(
87
- key, token, SecureRandom.uuid, body
87
+ key, token, request_id, body
88
88
  )
89
89
  data, status, headers = result
90
90
  response(data, status, headers)
91
91
  end
92
92
  end
93
93
 
94
- def release_project_lock(authorization:, key:, token:)
94
+ def release_project_lock(authorization:, key:, token:, request_id: SecureRandom.uuid)
95
95
  invoke do
96
96
  apis = @api_factory.call(authorization)
97
97
  data, status, headers = apis.locks.release_project_lock_with_http_info(
98
98
  key,
99
99
  token,
100
- SecureRandom.uuid
100
+ request_id
101
101
  )
102
102
  response(data, status, headers)
103
103
  end
@@ -10,10 +10,11 @@ module Volcano
10
10
  RENEWAL_REQUEST_BUDGET_SECONDS = 1.0
11
11
  RENEWER_SHUTDOWN_TIMEOUT_SECONDS = 1.0
12
12
 
13
- def with_lock(key, ttl:, &)
13
+ def with_lock(key, ttl:, token: nil, request_id: nil, &)
14
14
  validate_ttl(ttl)
15
15
  started_at = LockLeaseClock.capture
16
- guard = LockGuard.new(acquire(key, ttl: ttl), ttl: ttl, started_at: started_at)
16
+ lease = acquire(key, ttl: ttl, token: token, request_id: request_id)
17
+ guard = LockGuard.new(lease, ttl: ttl, started_at: started_at)
17
18
  owned_key = guard.lease.key
18
19
  renewer = lock_renewer(owned_key, guard, ttl)
19
20
  LockSession.new(self, owned_key, guard, renewer, method(:renewal_delay)).run(&)
data/lib/volcano/locks.rb CHANGED
@@ -13,8 +13,8 @@ module Volcano
13
13
  @transport = transport
14
14
  end
15
15
 
16
- def get(key)
17
- payload = Transport.body(get_response(key), 200)
16
+ def get(key, request_id: nil)
17
+ payload = lock_request(:get_project_lock, 200, key: key, request_id: request_id)
18
18
  LockState.new(
19
19
  held: payload.fetch('held'),
20
20
  expires_at: parse_time(payload['expires_at']),
@@ -22,84 +22,68 @@ module Volcano
22
22
  )
23
23
  end
24
24
 
25
- def acquire(key, ttl:)
26
- token = SecureRandom.uuid
27
- payload = Transport.body(acquire_response(key, ttl, token), 201)
28
- LockLease.new(
29
- key: key,
30
- token: token,
31
- expires_at: parse_time(payload['expires_at']),
32
- fencing_token: payload['fencing_token']
33
- )
25
+ def acquire(key, ttl:, token: nil, request_id: nil)
26
+ validate_ttl(ttl)
27
+ parameters = {
28
+ authorization: @client.service_token.dup.freeze, key: key.dup.freeze,
29
+ ttl: ttl, token: request_uuid(token, 'token'), request_id: request_uuid(request_id, 'request_id')
30
+ }.freeze
31
+ payload = acquire_payload(parameters)
32
+ build_lease(parameters[:key], parameters[:token], payload)
34
33
  end
35
34
 
36
- def renew(key, lease, ttl:)
37
- payload = Transport.body(renew_response(key, lease, ttl), 200)
38
- LockLease.new(
39
- key: key,
40
- token: lease.token,
41
- expires_at: parse_time(payload['expires_at']),
42
- fencing_token: payload['fencing_token']
43
- )
35
+ def renew(key, lease, ttl:, request_id: nil)
36
+ validate_ttl(ttl)
37
+ payload = lock_request(:renew_project_lock, 200, key: key, ttl: ttl,
38
+ token: lease.token, request_id: request_id)
39
+ build_lease(key, lease.token, payload)
44
40
  end
45
41
 
46
- def release(key, lease)
47
- response = Transport.invoke do
48
- @transport.release_project_lock(
49
- authorization: @client.service_token,
50
- key: key,
51
- token: lease.token
52
- )
53
- end
54
- Transport.body(response, 204)
42
+ def release(key, lease, request_id: nil)
43
+ lock_request(:release_project_lock, 204, key: key, token: lease.token, request_id: request_id)
55
44
  nil
56
45
  end
57
46
 
58
- def force_release(key)
59
- Transport.body(force_release_response(key), 204)
47
+ def force_release(key, request_id: nil)
48
+ lock_request(:force_release_project_lock, 204, key: key, request_id: request_id)
60
49
  nil
61
50
  end
62
51
 
63
52
  private
64
53
 
65
- def get_response(key)
66
- Transport.invoke do
67
- @transport.get_project_lock(
68
- authorization: @client.service_token,
69
- key: key
70
- )
71
- end
54
+ def acquire_payload(parameters)
55
+ perform_request(:acquire_project_lock, 201, parameters)
56
+ rescue Error::TransportError, Error::ServerError => e
57
+ raise unless e.status.nil? || e.status == 503
58
+
59
+ perform_request(:acquire_project_lock, 201, parameters)
60
+ end
61
+
62
+ def lock_request(operation, status, **parameters)
63
+ parameters[:authorization] = @client.service_token
64
+ parameters[:request_id] = request_uuid(parameters[:request_id], 'request_id')
65
+ perform_request(operation, status, parameters)
72
66
  end
73
67
 
74
- def force_release_response(key)
75
- Transport.invoke do
76
- @transport.force_release_project_lock(
77
- authorization: @client.service_token,
78
- key: key
79
- )
80
- end
68
+ def perform_request(operation, status, parameters)
69
+ response = Transport.invoke { @transport.public_send(operation, **parameters) }
70
+ Transport.body(response, status)
81
71
  end
82
72
 
83
- def acquire_response(key, ttl, token)
84
- Transport.invoke do
85
- @transport.acquire_project_lock(
86
- authorization: @client.service_token,
87
- key: key,
88
- ttl: ttl,
89
- token: token
90
- )
91
- end
73
+ def request_uuid(value, name)
74
+ return SecureRandom.uuid.freeze if value.nil?
75
+
76
+ valid = value.is_a?(String) && value.match?(/\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i)
77
+ raise ArgumentError, "#{name} must be a UUID string" unless valid
78
+
79
+ value.dup.freeze
92
80
  end
93
81
 
94
- def renew_response(key, lease, ttl)
95
- Transport.invoke do
96
- @transport.renew_project_lock(
97
- authorization: @client.service_token,
98
- key: key,
99
- ttl: ttl,
100
- token: lease.token
101
- )
102
- end
82
+ def build_lease(key, token, payload)
83
+ LockLease.new(
84
+ key: key, token: token, expires_at: parse_time(payload['expires_at']),
85
+ fencing_token: payload['fencing_token']
86
+ )
103
87
  end
104
88
 
105
89
  def parse_time(value)
@@ -13,17 +13,18 @@ module Volcano
13
13
  session = session_for_lineage(session_lineage)
14
14
  protocol = build_protocol(socket)
15
15
  result = protocol.connect(token: session.access_token)
16
- session = session_for_lineage(session_lineage)
17
- activate_protocol(protocol, session, result)
16
+ session_for_lineage(session_lineage)
17
+ activate_protocol(protocol, session_lineage, result)
18
18
  rescue StandardError => e
19
19
  handle_connection_failure(socket, e)
20
20
  end
21
21
 
22
22
  def active_session_lineage
23
23
  _, lineage, session = @client.capture_session_binding
24
- return lineage if session
24
+ raise Error::AuthenticationError, 'No active session' unless session
25
25
 
26
- raise Error::AuthenticationError, 'No active session'
26
+ session_for_lineage(lineage)
27
+ lineage
27
28
  end
28
29
 
29
30
  def session_for_lineage(expected_lineage)
@@ -33,9 +34,9 @@ module Volcano
33
34
  raise Error::SessionChangedError
34
35
  end
35
36
 
36
- def activate_protocol(protocol, session, result)
37
+ def activate_protocol(protocol, session_lineage, result)
37
38
  @protocol = protocol
38
- @protocol_user_id = session.user_id
39
+ @protocol_session_lineage = session_lineage
39
40
  protocol_connected(result)
40
41
  protocol
41
42
  end
@@ -55,7 +56,7 @@ module Volcano
55
56
  ensure
56
57
  @manual_disconnect = false
57
58
  @channels.each_value(&:mark_closed)
58
- @protocol_user_id = nil
59
+ @protocol_session_lineage = nil
59
60
  end
60
61
 
61
62
  def handle_connection_failure(socket, error)
@@ -117,7 +117,7 @@ module Volcano
117
117
  def reset_after_protocol_loss
118
118
  protocol = @protocol
119
119
  @protocol = nil
120
- @protocol_user_id = nil
120
+ @protocol_session_lineage = nil
121
121
  @channels.each_value { |channel| channel.protocol_lost(protocol) } if protocol
122
122
  end
123
123
  end
@@ -30,15 +30,14 @@ module Volcano
30
30
 
31
31
  def capture_protocol_session
32
32
  generation, lineage, session = capture_session_binding
33
- return [generation, lineage, session] if session && session.user_id == @protocol_user_id
33
+ return [generation, lineage, session] if session && lineage == @protocol_session_lineage
34
34
 
35
35
  raise Error::SessionChangedError
36
36
  end
37
37
 
38
38
  def access_token_for_protocol_lineage(expected_lineage)
39
39
  _, lineage, session = capture_session_binding
40
- return session.access_token if session && lineage == expected_lineage &&
41
- session.user_id == @protocol_user_id
40
+ return session.access_token if session && lineage == expected_lineage && lineage == @protocol_session_lineage
42
41
 
43
42
  raise Error::SessionChangedError
44
43
  end
@@ -73,7 +73,7 @@ module Volcano
73
73
  @channels = {}
74
74
  @connection_callbacks = { connect: {}, disconnect: {}, error: {} }
75
75
  @next_connection_callback_id = 0
76
- @protocol_user_id = nil
76
+ @protocol_session_lineage = nil
77
77
  @reconnect_task = nil
78
78
  @closed = false
79
79
  end
@@ -9,9 +9,78 @@ module Volcano
9
9
  end
10
10
  private_constant :SESSION_USER_CODER
11
11
 
12
- # In-memory credentials with an optional, unverified local user snapshot.
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)
32
+ return if current.user_id || session_id(current.access_token)
33
+
34
+ raise Error::AuthenticationError, 'Cannot refresh unknown identity without a session identifier'
35
+ end
36
+
37
+ def self.validate_refresh(current, refreshed)
38
+ return unless current
39
+
40
+ validate_refresh_source(current)
41
+ expected = session_id(current.access_token)
42
+ if expected && expected != session_id(refreshed&.access_token)
43
+ raise Error::AuthenticationError, 'Refreshed credentials belong to a different server session'
44
+ end
45
+ return unless current.user_id && current.user_id != refreshed&.user_id
46
+
47
+ raise Error::AuthenticationError, 'Refreshed session belongs to a different user'
48
+ end
49
+
50
+ # Untrusted continuity constraint, never authenticated user identity.
51
+ def self.session_id(access_token)
52
+ payload = token_payload(access_token)
53
+ return unless payload.is_a?(Hash)
54
+
55
+ value = payload['session_id']
56
+ 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)
57
+ end
58
+
59
+ def self.token_payload(access_token)
60
+ return unless access_token.is_a?(String)
61
+
62
+ parts = access_token.split('.')
63
+ return unless parts.length == 3
64
+
65
+ encoded = parts.fetch(1).tr('-_', '+/')
66
+ JSON.parse((encoded + ('=' * (-encoded.length % 4))).unpack1('m0'))
67
+ rescue ArgumentError, JSON::ParserError
68
+ nil
69
+ end
70
+ private_class_method :token_payload
71
+
72
+ def self.with_user(session, user)
73
+ user_id = (session.user_id || user.fetch('id')).dup.freeze
74
+ raise Error::AuthenticationError, 'Profile belongs to a different user' unless user['id'] == user_id
75
+
76
+ Session.new(**session.to_h, user_id: user_id, user: user)
77
+ end
78
+ end
79
+ private_constant :SessionCredentials
80
+
81
+ # Local credentials; refresh credentials and user identity may be unknown.
13
82
  Session = Data.define(:access_token, :refresh_token, :user_id, :user) do
14
- def initialize(access_token:, refresh_token:, user_id:, user: nil)
83
+ def initialize(access_token:, refresh_token: nil, user_id: nil, user: nil)
15
84
  super(access_token:, refresh_token:, user_id:, user: immutable_user(user))
16
85
  end
17
86
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Volcano
4
- VERSION = '0.7.2'
4
+ VERSION = '0.9.0'
5
5
  end
data/lib/volcano.rb CHANGED
@@ -13,6 +13,7 @@ require_relative 'volcano/redaction'
13
13
  require_relative 'volcano/transport'
14
14
  require_relative 'volcano/generated_transport'
15
15
  require_relative 'volcano/auth'
16
+ require_relative 'volcano/auth_sign_out'
16
17
  require_relative 'volcano/auth_refresh'
17
18
  require_relative 'volcano/auth_sign_up'
18
19
  require_relative 'volcano/sign_up_result'
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.7.2
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kong
@@ -145,6 +145,7 @@ files:
145
145
  - lib/volcano/auth_reset_password.rb
146
146
  - lib/volcano/auth_reset_password_for_email.rb
147
147
  - lib/volcano/auth_sessions.rb
148
+ - lib/volcano/auth_sign_out.rb
148
149
  - lib/volcano/auth_sign_up.rb
149
150
  - lib/volcano/auth_state.rb
150
151
  - lib/volcano/auth_update_user.rb