volcano-sdk 0.9.2 → 0.9.3

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: 92b94c10570347e736103b57ee437cf54d72ca334a8e87f3528087655fd80e78
4
- data.tar.gz: 68c2e41eac585bc12ee2086c5574431136de600236737e820224b88579ca91ca
3
+ metadata.gz: bb4b4a206147014ad99f42983e545238fd1c46600a127cbe932db4877afd6db9
4
+ data.tar.gz: 5064a928176a3dd8350d066000c405c1d03b87499b3cbc9bd0a9534b65785247
5
5
  SHA512:
6
- metadata.gz: 76a68bc5ba09715be91401c60e8b7183f75450907e51d9b9fc1a0437d262538de69c8b22ab78f184ff0b3cb57f267a8bf45efd8f29c8669b2c82a0a655a95806
7
- data.tar.gz: 1b055512555848049b55205e62fb9b60b126f333469d3c9eef8d09712201136c03dc70e93fe2cb2f67b388f6902f5dd4df8b76da7618c54ccfb9d9bb2ac3625e
6
+ metadata.gz: 9ea3fff976b1358651b6f0117a23ccb353edb01139b9876f07b2bca473acd607550779702d0a8821c5cc612194e0fb51f279df52fd41a192b44916d60dd50844
7
+ data.tar.gz: 7b4761b2c1c25f131968de7662668f04c83ad90fe13532b4b7b277685fe23966ca585f578d6337bbe28dbf9c1a584dc38c0d243420f56851af559f0f6fbce085
data/README.md CHANGED
@@ -4,6 +4,8 @@ Official Ruby SDK for Volcano. Requires Ruby 3.2 or later.
4
4
 
5
5
  Start with the [Ruby quickstart](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/README.md).
6
6
 
7
+ See [Authentication](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/authentication.md) for account, session, email, and OAuth workflows.
8
+
7
9
  ## Install
8
10
 
9
11
  Add the gem to your Gemfile and run `bundle install`:
@@ -41,7 +43,7 @@ failures raise typed errors under `Volcano::Error`.
41
43
  ```ruby
42
44
  result = client.auth.sign_up(
43
45
  email: "new-user@example.com",
44
- password: "secret",
46
+ password: "correct-horse-battery-staple",
45
47
  metadata: { display_name: "New User" }
46
48
  )
47
49
  puts result.message if result.confirmation_required
@@ -53,7 +55,7 @@ addresses. Pass `sign_in_when_allowed: true` to follow it with `sign_in` only wh
53
55
  confirmation is not required:
54
56
 
55
57
  ```ruby
56
- result = client.auth.sign_up(email: "new-user@example.com", password: "secret", sign_in_when_allowed: true)
58
+ result = client.auth.sign_up(email: "new-user@example.com", password: "correct-horse-battery-staple", sign_in_when_allowed: true)
57
59
  session = result.session # nil when no follow-up sign-in ran.
58
60
  ```
59
61
 
@@ -63,7 +65,7 @@ A failed follow-up raises its usual typed error; it does not undo the successful
63
65
  ### Sign in
64
66
 
65
67
  ```ruby
66
- session = client.auth.sign_in(email: "user@example.com", password: "secret")
68
+ session = client.auth.sign_in(email: "user@example.com", password: "correct-horse-battery-staple")
67
69
  current_session = client.auth.current_session
68
70
  raise "session changed" unless current_session == session
69
71
  ```
@@ -82,7 +84,8 @@ container/string/time subclasses, non-finite numbers, duplicate JSON keys, and
82
84
  structures deeper than 100 levels raise `TypeError`. Use the typed `auth.user`
83
85
  profile when you need Ruby `Time` values. Successful `user`, `update_user`,
84
86
  `convert_anonymous`, and `confirm_email_change` calls update this snapshot
85
- without changing credentials or emitting an authentication-state event.
87
+ without changing credentials or emitting an authentication-state event unless an HTTP 401
88
+ requires automatic refresh. Successful recovery rotates credentials and emits `:token_refreshed`.
86
89
  Previously returned sessions remain unchanged.
87
90
 
88
91
  ### Get the current user
@@ -103,7 +106,7 @@ is in flight raises
103
106
 
104
107
  ```ruby
105
108
  user = client.auth.update_user(
106
- password: "new-secret",
109
+ password: "new-correct-horse-battery-staple",
107
110
  metadata: { display_name: "Grace", avatar: nil }
108
111
  )
109
112
  raise "wrong user" unless user.id == session.user_id
@@ -207,8 +210,9 @@ hosted_url = client.auth.get_hosted_auth_url(
207
210
  ```
208
211
 
209
212
  Store `hosted_state` in the user's signed server-side session before redirecting
210
- to `hosted_url`. After parsing the returned fragment into a `Volcano::Session`,
211
- validate and adopt it atomically:
213
+ to `hosted_url`. In the callback, atomically fetch and delete the stored state before validation,
214
+ even if validation or adoption fails. Reject a missing or already-consumed state.
215
+ After parsing the returned fragment into a `Volcano::Session`, validate and adopt it:
212
216
 
213
217
  ```ruby
214
218
  session = client.auth.adopt_hosted_auth_session(
@@ -239,8 +243,9 @@ authorization_url = client.auth.sign_in_with_oauth(
239
243
  ```
240
244
 
241
245
  Store `oauth_state` in the user's signed server-side session, then redirect the
242
- user to the returned URL. In the callback, pass the returned and stored states
243
- to the SDK so it rejects login CSRF before exchanging the one-time code:
246
+ user to the returned URL. In the callback, atomically fetch and delete the stored nonce as `stored_oauth_state`;
247
+ reject a missing or already-consumed nonce. Pass the returned and consumed states to
248
+ the SDK so it rejects login CSRF before exchanging the one-time code:
244
249
 
245
250
  ```ruby
246
251
  session = client.auth.exchange_oauth_code(
@@ -338,9 +343,11 @@ replacement as an "other" session. If replacement occurs, the method raises
338
343
  client.auth.delete_session('00000000-0000-4000-8000-000000000099')
339
344
  ```
340
345
 
341
- The request uses the current access token. Deleting that token's own session
342
- clears local credentials, including when the request outcome is uncertain;
343
- deleting another session preserves them. If another authentication operation
346
+ The request uses the current access token. When its JWT contains a readable UUID `session_id`,
347
+ deleting that session clears local credentials even if the request outcome is uncertain.
348
+ Without that identifier, the SDK cannot recognize self-deletion. Deleting another session does not
349
+ itself clear local state. HTTP 401 recovery can rotate credentials and emit `:token_refreshed`;
350
+ a server-rejected refresh clears the captured session before the operation raises. If another authentication operation
344
351
  replaces the session before deletion finishes, the method raises
345
352
  `Volcano::Error::SessionChangedError` instead of clearing the replacement or
346
353
  acknowledging a stale result.
@@ -354,13 +361,12 @@ session = client.auth.sign_in_anonymously(metadata: { device: "mobile" })
354
361
  Anonymous sign-ins must be enabled for the project. Convert the account before
355
362
  signing out if the user needs to recover it later.
356
363
 
357
- Attach email credentials without changing the anonymous user's ID or current
358
- session:
364
+ Attach email credentials while preserving the anonymous user's ID:
359
365
 
360
366
  ```ruby
361
367
  user = client.auth.convert_anonymous(
362
368
  email: "user@example.com",
363
- password: "secure-password",
369
+ password: "a-long-example-password-2026",
364
370
  metadata: { display_name: "Ada" }
365
371
  )
366
372
  ```
@@ -371,7 +377,7 @@ as verified.
371
377
  ### Reset the password
372
378
 
373
379
  ```ruby
374
- client.auth.reset_password(token: "recovery-token", new_password: "new-secret")
380
+ client.auth.reset_password(token: "recovery-token", new_password: "new-correct-horse-battery-staple")
375
381
  ```
376
382
 
377
383
  Success returns `nil`. The reset revokes the recovered account's existing
@@ -384,9 +390,9 @@ Pass `access_token` to `Volcano::Client.new` to start without a refresh token or
384
390
  known user identity. Construction makes no request and leaves `refresh_token`,
385
391
  `user_id`, and `user` as `nil`. `auth.user` validates and caches the profile
386
392
  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.
393
+ `Volcano::Error::AuthenticationError`. `sign_out` clears local state and revokes the server
394
+ session when the access JWT contains a readable UUID `session_id`. Supplied credentials require both a refresh token and an access JWT with a readable UUID
395
+ `session_id` to enable refresh.
390
396
  See the [token bootstrap example](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/README.md#use-a-supplied-access-token).
391
397
 
392
398
  ### Adopt an existing session
@@ -412,8 +418,9 @@ raise "refresh failed" unless client.auth.current_session.equal?(refreshed)
412
418
  ```
413
419
 
414
420
  On success, `refresh_session` replaces the in-memory session and returns the
415
- immutable new snapshot. An authentication failure clears the session that
416
- initiated the request. Server and transport failures preserve it, and a late
421
+ immutable new snapshot. An authentication rejection from the refresh endpoint clears the captured session.
422
+ Missing refresh credentials, failed session-continuity checks, server errors, and
423
+ transport failures preserve it, and a late
417
424
  response never replaces a newer session. The SDK does not persist sessions.
418
425
 
419
426
  ### Observe auth-state changes
@@ -430,7 +437,8 @@ subscription.unsubscribe
430
437
  Registration immediately yields `:initial_session`. Successful session
431
438
  creation, refresh, and local clearing yield `:signed_in`, `:token_refreshed`,
432
439
  and `:signed_out`. Callbacks are delivered locally in transition order after the
433
- state lock is released, and callback failures cannot interrupt auth operations.
440
+ state lock is released. Callback `StandardError` failures are isolated; exceptions such as
441
+ `Interrupt` propagate after the session transition has committed.
434
442
  The SDK does not broadcast between processes or persist sessions.
435
443
 
436
444
  ### Sign out the current session
@@ -441,8 +449,10 @@ raise "still signed in" if client.auth.current_session
441
449
  ```
442
450
 
443
451
  Sign-out uses the refresh token directly when the SDK received both credentials together from
444
- sign-in or a validated refresh. Supplied credentials use the access-token session; on HTTP 401,
445
- the SDK can refresh once and revoke that same session without adopting the renewed credentials.
452
+ sign-in or a validated refresh. Supplied credentials use the access-token session when its JWT
453
+ contains a readable UUID `session_id`; on HTTP 401, the SDK can refresh once and revoke that
454
+ same session without adopting the renewed credentials. Without that identifier, sign-out uses
455
+ the supplied refresh token, or only clears local state if no refresh token is available.
446
456
  It revokes the captured session and clears the captured in-memory
447
457
  session. It succeeds without a request when no session exists. If revocation
448
458
  fails, the SDK still clears that session and raises the typed error. Sign-out waits for an already-running refresh and uses its validated credentials.
@@ -464,7 +474,7 @@ goes to the function's own domain rather than to `api_url`, so an egress rule
464
474
  that allows only the API host will block it; the resolved endpoint is cached for
465
475
  the lifetime the platform gives it. Deployments with no public function domain
466
476
  invoke through the API host instead. It uses the
467
- active user session when present, then a configured service key, then the
477
+ current session token, including a supplied `access_token`, then a configured service key, then the
468
478
  anonymous key. An anonymous key can invoke a public function without a user
469
479
  session; the function receives no user identity. The immutable result includes
470
480
  the response body, status, headers, and `X-Volcano-Version`. The body can be a
@@ -473,20 +483,33 @@ deeply frozen. Invalid JSON or JSON that cannot decode to valid UTF-8 is
473
483
  returned as the original decoded response text; malformed Unicode is not repaired.
474
484
  Ruby's standard JSON nesting limit (100) also falls back to text.
475
485
  A function's own
476
- non-2xx response is returned when the version header proves it ran; platform
477
- failures raise typed SDK errors.
486
+ non-2xx response is returned when Volcano confirms it ran; non-success platform
487
+ HTTP responses raise typed SDK errors.
488
+
489
+ Function resolution and invocation recover from a platform HTTP 401 before dispatch:
490
+ the SDK refreshes the captured session and retries the rejected request once.
491
+ Concurrent calls share successful recovery. Replacing or signing out that session
492
+ prevents replay under another identity. The call preserves its original payload values.
493
+ A function's own response, HTTP 403, or a network failure never triggers this retry.
494
+ Anonymous and service keys do not refresh.
495
+
496
+ See the [functions guide](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/functions.md).
478
497
 
479
498
  ### Read project logs
480
499
 
481
500
  ```ruby
482
501
  project_id = "00000000-0000-4000-8000-000000000001"
483
- page = client.logs.search(
502
+ logs_client = Volcano::Client.new(
503
+ anon_key: ENV.fetch("VOLCANO_ANON_KEY"),
504
+ access_token: ENV.fetch("VOLCANO_PROJECT_ACCESS_TOKEN")
505
+ )
506
+ page = logs_client.logs.search(
484
507
  project_id,
485
508
  { resource: { type: "function" }, limit: 100 }
486
509
  )
487
510
  page.data.each { |event| puts [event["timestamp"], event["body"]] }
488
511
 
489
- activity = client.logs.activity(
512
+ activity = logs_client.logs.activity(
490
513
  project_id,
491
514
  { resource: { type: "function" }, bucket_count: 24 }
492
515
  )
@@ -496,7 +519,9 @@ puts activity.total
496
519
  `search` returns an immutable page of retained runtime or deployment log
497
520
  events. Pass `next_cursor` back as `cursor` to continue a search. `activity`
498
521
  returns immutable time buckets using the same resource selector and query
499
- syntax. Both methods require an active user session.
522
+ syntax. Both methods require a platform user token or a project access token.
523
+ A `read_only` project token is sufficient; end-user sessions cannot read project logs.
524
+ See the [logs guide](https://github.com/Kong/volcano-sdk-ruby/blob/main/docs/logs.md).
500
525
 
501
526
  ### Query a database
502
527
 
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Volcano
4
+ # Keeps function resolution and invocation on the captured session lineage.
5
+ class FunctionAuth
6
+ def initialize(client)
7
+ @client = client
8
+ @binding = client.capture_session_binding
9
+ @fallback_token = client.function_token.dup.freeze
10
+ end
11
+
12
+ def run
13
+ @binding = @client.auth.__send__(:owned_session_binding, @binding) if @binding.last
14
+ yield(token)
15
+ rescue Error::AuthenticationError => e
16
+ raise unless @binding.last && e.status == 401
17
+
18
+ refresh(e)
19
+ yield(token)
20
+ ensure
21
+ validate
22
+ end
23
+
24
+ private
25
+
26
+ def token
27
+ return @client.auth.__send__(:owned_session_binding, @binding).last.access_token if @binding.last
28
+
29
+ validate
30
+ @fallback_token
31
+ end
32
+
33
+ def refresh(original)
34
+ # Resolution has released its cache lock before refresh callbacks run.
35
+ @client.auth.__send__(:refresh_captured_session, @binding)
36
+ rescue Error::SessionChangedError
37
+ raise
38
+ rescue Error::VolcanoError
39
+ raise original
40
+ end
41
+
42
+ def validate
43
+ if @binding.last
44
+ @client.auth.__send__(:validate_read_failure, @binding)
45
+ elsif @client.capture_session_binding[1] != @binding[1]
46
+ raise Error::SessionChangedError
47
+ end
48
+ end
49
+ end
50
+ private_constant :FunctionAuth
51
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'function_auth'
4
+
3
5
  module Volcano
4
6
  # Invokes deployed Volcano functions by name.
5
7
  class Functions
@@ -17,21 +19,33 @@ module Volcano
17
19
  end
18
20
 
19
21
  def invoke(name, payload = {})
22
+ auth = FunctionAuth.new(@client)
20
23
  validate_invocation(name, payload)
21
- authorization = @client.function_token
22
- resolution = resolve(authorization, name)
23
- response = Transport.invoke { invoke_resolved(authorization, resolution, payload.dup) }
24
+ invoke_owned(auth, name.dup.freeze, ImmutableQueryValue.capture(payload))
25
+ end
26
+
27
+ private
28
+
29
+ def invoke_owned(auth, name, payload)
30
+ authorization, resolution = auth.run { |token| [token, resolve(token, name)] }
31
+ response = authenticated_invoke(auth, resolution, payload)
24
32
  if stale_mapping?(response)
25
- # The function was deleted and recreated, so the cached identity no
26
- # longer exists. Resolve again before giving up.
27
33
  FunctionResolution.forget(@api_url, authorization, name)
28
- resolution = resolve(authorization, name)
29
- response = Transport.invoke { invoke_resolved(authorization, resolution, payload.dup) }
34
+ resolution = auth.run { |token| resolve(token, name) }
35
+ response = authenticated_invoke(auth, resolution, payload)
30
36
  end
31
37
  function_response(response)
32
38
  end
33
39
 
34
- private
40
+ def authenticated_invoke(auth, resolution, payload)
41
+ auth.run do |token|
42
+ response = Transport.invoke { invoke_resolved(token, resolution, payload) }
43
+ if response.status == 401 && header(response.headers, FUNCTION_INVOKED_HEADER).nil?
44
+ Transport.body(response, 200)
45
+ end
46
+ response
47
+ end
48
+ end
35
49
 
36
50
  # A platform 404 means the cached function identity is gone. A function that
37
51
  # answers 404 itself must be returned rather than retried: invoking twice
@@ -82,9 +96,7 @@ module Volcano
82
96
  end
83
97
 
84
98
  def cached_resolution(cached)
85
- return cached.resolution if cached.resolution
86
-
87
- raise Error::NotFoundError.new('Function was not found', status: 404)
99
+ cached.resolution || raise(Error::NotFoundError.new('Function was not found', status: 404))
88
100
  end
89
101
 
90
102
  def validate_invocation(name, payload)
@@ -101,10 +113,8 @@ module Volcano
101
113
 
102
114
  # Absent when the deployment serves no public invocation domain, as in
103
115
  # local development; the function is reached through the API instead.
104
- FunctionResolution::Resolution.new(
105
- function_id: function_id,
106
- invoke_url: FunctionResolution.valid_invoke_url(payload['invoke_url'], @api_url)
107
- )
116
+ invoke_url = FunctionResolution.valid_invoke_url(payload['invoke_url'], @api_url)
117
+ FunctionResolution::Resolution.new(function_id: function_id, invoke_url: invoke_url)
108
118
  end
109
119
 
110
120
  def cache_ttl(payload)
@@ -129,8 +139,6 @@ module Volcano
129
139
  )
130
140
  end
131
141
 
132
- def header(headers, name)
133
- headers&.find { |key, _| key.casecmp?(name) }&.last
134
- end
142
+ def header(headers, name) = headers&.find { |key, _| key.casecmp?(name) }&.last
135
143
  end
136
144
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Volcano
4
- VERSION = '0.9.2'
4
+ VERSION = '0.9.3'
5
5
  end
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.9.2
4
+ version: 0.9.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kong
@@ -153,6 +153,7 @@ files:
153
153
  - lib/volcano/connection_string.rb
154
154
  - lib/volcano/database.rb
155
155
  - lib/volcano/errors.rb
156
+ - lib/volcano/function_auth.rb
156
157
  - lib/volcano/function_resolution.rb
157
158
  - lib/volcano/function_response.rb
158
159
  - lib/volcano/functions.rb