booqable 1.1.0 → 1.2.1
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 +4 -4
- data/CHANGELOG.md +14 -1
- data/README.md +24 -0
- data/lib/booqable/auth.rb +2 -1
- data/lib/booqable/client.rb +42 -0
- data/lib/booqable/configurable.rb +2 -0
- data/lib/booqable/default.rb +11 -0
- data/lib/booqable/middleware/auth/oauth.rb +20 -3
- data/lib/booqable/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb7d9650b7c441ded7b819566722c9dd860d83c28338407897c3e359367954a3
|
|
4
|
+
data.tar.gz: 6fd5b29991f14b4503b244fcb05db3613c7d87baf215904274066081a2d71107
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38b96b39b3d88bfe1209a102f15117f2f7292d38f0ffebb5d2d0e80cbf93d8cb5622462b9bfef7a3cc784a52f25c4bdcad97333de5401a7d8cc2ca0abcbaae15
|
|
7
|
+
data.tar.gz: f53f2f5c9347c8f3ea266abfc8882b42e8e1239c9df104cb46277a2467ad22710fc68a7417808eb32ac37d1ee0a3891a9e704e558101dbb30962b54da2d1b917
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [1.2.1] - 2026-06-10
|
|
2
|
+
|
|
3
|
+
- Require `oauth2 >= 2.0.22` to address GHSA-pp92-crg2-gfv9, where a
|
|
4
|
+
protocol-relative redirect `Location` could override the request authority and
|
|
5
|
+
leak the bearer `Authorization` header to an attacker-controlled host.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## [1.2.0] - 2026-05-27
|
|
9
|
+
|
|
10
|
+
- Add optional `around_refresh_token` configuration. When provided, the OAuth
|
|
11
|
+
middleware yields the read + expiry-check + refresh sequence to the callable
|
|
12
|
+
so host applications can serialize concurrent token refreshes (e.g. with a
|
|
13
|
+
database transaction and advisory lock). The gem keeps no lock dependency.
|
|
14
|
+
|
|
2
15
|
|
|
3
16
|
## [1.1.0] - 2026-03-11
|
|
4
17
|
|
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, {
|
data/lib/booqable/client.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "did_you_mean"
|
|
4
|
+
|
|
3
5
|
module Booqable
|
|
4
6
|
# Client for the Booqable API
|
|
5
7
|
#
|
|
@@ -40,11 +42,22 @@ module Booqable
|
|
|
40
42
|
access_token
|
|
41
43
|
]
|
|
42
44
|
|
|
45
|
+
# Accepted aliases for configuration options, mapping the alias to its
|
|
46
|
+
# canonical {Booqable::Configurable} key.
|
|
47
|
+
OPTION_ALIASES = {
|
|
48
|
+
skip_retries: :no_retries
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
43
51
|
# Initialize a new Client
|
|
44
52
|
#
|
|
45
53
|
# @param options [Hash] Configuration options for the client
|
|
46
54
|
# @see Booqable::Configurable For a complete list of supported configuration options
|
|
47
55
|
def initialize(options = {})
|
|
56
|
+
options = normalize_aliases(options)
|
|
57
|
+
|
|
58
|
+
unknown_keys = options.keys.map(&:to_sym) - Booqable::Configurable.keys
|
|
59
|
+
raise ArgumentError, unknown_options_message(unknown_keys) unless unknown_keys.empty?
|
|
60
|
+
|
|
48
61
|
# Use options passed in, but fall back to module defaults
|
|
49
62
|
#
|
|
50
63
|
# This may look like a `.keys.each` which should be replaced with `#each_key`, but
|
|
@@ -94,5 +107,34 @@ module Booqable
|
|
|
94
107
|
|
|
95
108
|
inspected
|
|
96
109
|
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
# Rewrite any aliased option keys to their canonical configuration key.
|
|
114
|
+
#
|
|
115
|
+
# @param options [Hash] options as passed to {#initialize}
|
|
116
|
+
# @return [Hash] options with aliases replaced by their canonical keys
|
|
117
|
+
def normalize_aliases(options)
|
|
118
|
+
options.to_h do |key, value|
|
|
119
|
+
[ OPTION_ALIASES.fetch(key.to_sym, key), value ]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Build an error message for unknown configuration options, suggesting
|
|
124
|
+
# similarly-named valid options via did_you_mean when one is close enough.
|
|
125
|
+
#
|
|
126
|
+
# @param unknown_keys [Array<Symbol>] options that aren't valid config keys
|
|
127
|
+
# @return [String]
|
|
128
|
+
def unknown_options_message(unknown_keys)
|
|
129
|
+
dictionary = Booqable::Configurable.keys + OPTION_ALIASES.keys
|
|
130
|
+
spell_checker = DidYouMean::SpellChecker.new(dictionary: dictionary)
|
|
131
|
+
|
|
132
|
+
message = "unknown configuration option(s): #{unknown_keys.join(", ")}"
|
|
133
|
+
|
|
134
|
+
suggestions = unknown_keys.flat_map { |key| spell_checker.correct(key) }.uniq
|
|
135
|
+
message += ". Did you mean: #{suggestions.join(", ")}?" unless suggestions.empty?
|
|
136
|
+
|
|
137
|
+
message
|
|
138
|
+
end
|
|
97
139
|
end
|
|
98
140
|
end
|
|
@@ -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
|
data/lib/booqable/default.rb
CHANGED
|
@@ -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
|
-
|
|
58
|
+
around_refresh_token do
|
|
59
|
+
@token = @client.get_access_token_from_hash(@read_token.call)
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
data/lib/booqable/version.rb
CHANGED
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
|
|
4
|
+
version: 1.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hrvoje Šimić
|
|
@@ -86,6 +86,9 @@ dependencies:
|
|
|
86
86
|
- - "~>"
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
88
|
version: '2.0'
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: 2.0.22
|
|
89
92
|
type: :runtime
|
|
90
93
|
prerelease: false
|
|
91
94
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -93,6 +96,9 @@ dependencies:
|
|
|
93
96
|
- - "~>"
|
|
94
97
|
- !ruby/object:Gem::Version
|
|
95
98
|
version: '2.0'
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: 2.0.22
|
|
96
102
|
- !ruby/object:Gem::Dependency
|
|
97
103
|
name: jwt
|
|
98
104
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -166,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
166
172
|
- !ruby/object:Gem::Version
|
|
167
173
|
version: '0'
|
|
168
174
|
requirements: []
|
|
169
|
-
rubygems_version:
|
|
175
|
+
rubygems_version: 4.0.10
|
|
170
176
|
specification_version: 4
|
|
171
177
|
summary: Official Booqable API client for Ruby.
|
|
172
178
|
test_files: []
|