booqable 1.1.0 → 1.2.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: 13ee5f72ca221d2613d8691dd8c04fa189a38fbed8276b60878d2cce4c7f4f23
4
- data.tar.gz: 54028d30a657d3651ad0d49fab923efba00e09fa9f0d2e36906e8cf068975abd
3
+ metadata.gz: ce7d8b0be4851e4e78229078331a2f73cbe7cb7702c1555df9dec0903be28d0e
4
+ data.tar.gz: c79ed318eb3a38534291ef5e4abeb6a5749c7a2684bcd1befbf43d6f7f1d0e8a
5
5
  SHA512:
6
- metadata.gz: 51cb9ed5b66956cee3e429c51057e12884574fe3c8b3b907d6998caa1b037b5ca861c929ea7b26e8364369bf424b3686feff274149fa6a169da09d8048118390
7
- data.tar.gz: d6389d7f934088b0447dba290054456f10380578f5b38622275d6e846c99872a5060c21b192b8524aa7d867fe1b58917c9ad03eebfacef716b83eca28dab8549
6
+ metadata.gz: 9d6dead61b0b4bfd8374c9b51aec7dccc724988b5417f9585018ee549840ee207756642b38aedbfcf4fdb4e313f9764fdc64b1d1d6e7593c057edb20d6095afc
7
+ data.tar.gz: c5e34b3a736ab3bdca397dfd1245d07b8dd8e246e32d4d0f24e097c41f52d47307c645a547592e60b90dc27f98e2e817193a1059f2696c0a726316c0693c1f88
data/CHANGELOG.md CHANGED
@@ -1,4 +1,10 @@
1
- ## [Unreleased]
1
+ ## [1.2.0] - 2026-05-27
2
+
3
+ - Add optional `around_refresh_token` configuration. When provided, the OAuth
4
+ middleware yields the read + expiry-check + refresh sequence to the callable
5
+ so host applications can serialize concurrent token refreshes (e.g. with a
6
+ database transaction and advisory lock). The gem keeps no lock dependency.
7
+
2
8
 
3
9
  ## [1.1.0] - 2026-03-11
4
10
 
data/README.md CHANGED
@@ -105,6 +105,30 @@ client = Booqable::Client.new(
105
105
  client.authenticate_with_code(params[:code])
106
106
  ```
107
107
 
108
+ #### Serializing concurrent token refreshes
109
+
110
+ When multiple processes share the same OAuth token (e.g. the same installation
111
+ serving concurrent requests), pass an `around_refresh_token` callable to
112
+ serialize the read + expiry-check + refresh sequence. The middleware yields
113
+ to the callable once per request; the host application decides how to lock.
114
+
115
+ ```ruby
116
+ Booqable::Client.new(
117
+ # ...other oauth options...
118
+ around_refresh_token: ->(&block) {
119
+ AppInstallation.transaction do
120
+ installation.with_advisory_lock!("app_installation:#{installation.id}", transaction: true) do
121
+ installation.reload
122
+ block.call
123
+ end
124
+ end
125
+ }
126
+ )
127
+ ```
128
+
129
+ The gem itself has no advisory-lock dependency — `around_refresh_token` is
130
+ just a callable that takes a block.
131
+
108
132
  ### Single-Use Token Authentication
109
133
 
110
134
  For server-to-server communication requiring enhanced security:
data/lib/booqable/auth.rb CHANGED
@@ -57,7 +57,8 @@ module Booqable
57
57
  api_endpoint: api_endpoint,
58
58
  redirect_uri: redirect_uri,
59
59
  read_token: read_token,
60
- write_token: write_token
60
+ write_token: write_token,
61
+ around_refresh_token: around_refresh_token
61
62
  } if oauth_authenticated?
62
63
 
63
64
  builder.use Booqable::Middleware::Auth::ApiKey, {
@@ -48,6 +48,7 @@ module Booqable
48
48
  :proxy,
49
49
  :read_token,
50
50
  :redirect_uri,
51
+ :around_refresh_token,
51
52
  :single_use_token,
52
53
  :single_use_token_algorithm,
53
54
  :single_use_token_company_id,
@@ -81,6 +82,7 @@ module Booqable
81
82
  proxy
82
83
  read_token
83
84
  redirect_uri
85
+ around_refresh_token
84
86
  single_use_token
85
87
  single_use_token_algorithm
86
88
  single_use_token_company_id
@@ -151,6 +151,17 @@ module Booqable
151
151
  Proc.new { }
152
152
  end
153
153
 
154
+ # Default `around_refresh_token` callable
155
+ #
156
+ # When non-nil, the OAuth middleware yields its read+check+refresh
157
+ # sequence to this callable so the host application can serialize
158
+ # concurrent refreshes (e.g. with an advisory lock).
159
+ #
160
+ # @return [Proc, nil]
161
+ def around_refresh_token
162
+ nil
163
+ end
164
+
154
165
  # Default API key from ENV
155
166
  # @return [String, nil] API key for authentication
156
167
  def api_key
@@ -24,6 +24,10 @@ module Booqable
24
24
  # @option options [String] :api_endpoint API endpoint URL for the OAuth provider
25
25
  # @option options [Proc] :read_token Proc to read stored token
26
26
  # @option options [Proc] :write_token Proc to store new token
27
+ # @option options [Proc, nil] :around_refresh_token Optional callable
28
+ # invoked with a block around the read+check+refresh sequence. The
29
+ # host application can use it to serialize concurrent refreshes
30
+ # (e.g. wrap the block in a database transaction + advisory lock).
27
31
  # @raise [KeyError] If required options are not provided
28
32
  def initialize(app, options = {})
29
33
  super(app)
@@ -33,6 +37,7 @@ module Booqable
33
37
  @api_endpoint = options.fetch(:api_endpoint)
34
38
  @read_token = options.fetch(:read_token)
35
39
  @write_token = options.fetch(:write_token)
40
+ @around_refresh_token = options[:around_refresh_token]
36
41
 
37
42
  @client = OAuthClient.new(
38
43
  client_id: @client_id,
@@ -50,10 +55,12 @@ module Booqable
50
55
  # @param env [Faraday::Env] The request environment
51
56
  # @return [Faraday::Response] The response from the next middleware
52
57
  def call(env)
53
- @token = @client.get_access_token_from_hash(@read_token.call)
58
+ around_refresh_token do
59
+ @token = @client.get_access_token_from_hash(@read_token.call)
54
60
 
55
- if @token.expired? || @token.expires_at.nil?
56
- @token = refresh_token!
61
+ if @token.expired? || @token.expires_at.nil?
62
+ @token = refresh_token!
63
+ end
57
64
  end
58
65
 
59
66
  env.request_headers["Authorization"] ||= "Bearer #{@token.token}"
@@ -63,6 +70,16 @@ module Booqable
63
70
 
64
71
  private
65
72
 
73
+ # Yield to the configured around-callback, if any
74
+ #
75
+ # When a host application provides one (e.g. an advisory lock), the
76
+ # read+check+refresh sequence runs inside it so concurrent callers
77
+ # cannot interleave a read with another caller's refresh.
78
+ def around_refresh_token(&block)
79
+ return yield unless @around_refresh_token
80
+ @around_refresh_token.call(&block)
81
+ end
82
+
66
83
  # Refresh the expired OAuth token
67
84
  #
68
85
  # Uses the refresh token to obtain a new access token and stores it
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Booqable
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: booqable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hrvoje Šimić
@@ -166,7 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
166
  - !ruby/object:Gem::Version
167
167
  version: '0'
168
168
  requirements: []
169
- rubygems_version: 3.6.9
169
+ rubygems_version: 4.0.10
170
170
  specification_version: 4
171
171
  summary: Official Booqable API client for Ruby.
172
172
  test_files: []