x-core 1.0.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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +8 -0
  3. data/CHANGELOG.md +353 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +54 -0
  6. data/lib/x/core/app_only_authenticator.rb +198 -0
  7. data/lib/x/core/authenticator.rb +74 -0
  8. data/lib/x/core/authenticator_request.rb +56 -0
  9. data/lib/x/core/bearer_token_authenticator.rb +46 -0
  10. data/lib/x/core/built_response.rb +100 -0
  11. data/lib/x/core/client.rb +728 -0
  12. data/lib/x/core/client_app_only.rb +115 -0
  13. data/lib/x/core/client_credentials.rb +193 -0
  14. data/lib/x/core/client_internals.rb +315 -0
  15. data/lib/x/core/client_memo.rb +58 -0
  16. data/lib/x/core/client_settings.rb +166 -0
  17. data/lib/x/core/client_token_refresh.rb +281 -0
  18. data/lib/x/core/connection.rb +328 -0
  19. data/lib/x/core/connection_pool.rb +123 -0
  20. data/lib/x/core/connection_proxy.rb +151 -0
  21. data/lib/x/core/connection_request.rb +90 -0
  22. data/lib/x/core/credential_holder.rb +78 -0
  23. data/lib/x/core/credential_validator.rb +286 -0
  24. data/lib/x/core/errors/authorization_denied.rb +55 -0
  25. data/lib/x/core/errors/authorization_error.rb +41 -0
  26. data/lib/x/core/errors/bad_gateway.rb +9 -0
  27. data/lib/x/core/errors/bad_request.rb +10 -0
  28. data/lib/x/core/errors/callback_error.rb +88 -0
  29. data/lib/x/core/errors/client_error.rb +17 -0
  30. data/lib/x/core/errors/conflict.rb +9 -0
  31. data/lib/x/core/errors/error.rb +66 -0
  32. data/lib/x/core/errors/forbidden.rb +10 -0
  33. data/lib/x/core/errors/gateway_timeout.rb +9 -0
  34. data/lib/x/core/errors/gone.rb +9 -0
  35. data/lib/x/core/errors/http_error.rb +333 -0
  36. data/lib/x/core/errors/internal_server_error.rb +9 -0
  37. data/lib/x/core/errors/invalid_response.rb +96 -0
  38. data/lib/x/core/errors/method_not_allowed.rb +10 -0
  39. data/lib/x/core/errors/network_error.rb +59 -0
  40. data/lib/x/core/errors/not_acceptable.rb +10 -0
  41. data/lib/x/core/errors/not_found.rb +15 -0
  42. data/lib/x/core/errors/payload_too_large.rb +10 -0
  43. data/lib/x/core/errors/payment_required.rb +10 -0
  44. data/lib/x/core/errors/request_timeout.rb +9 -0
  45. data/lib/x/core/errors/server_error.rb +15 -0
  46. data/lib/x/core/errors/service_unavailable.rb +9 -0
  47. data/lib/x/core/errors/token_report_failed.rb +59 -0
  48. data/lib/x/core/errors/too_many_redirects.rb +53 -0
  49. data/lib/x/core/errors/too_many_requests.rb +80 -0
  50. data/lib/x/core/errors/unauthorized.rb +15 -0
  51. data/lib/x/core/errors/unavailable_for_legal_reasons.rb +10 -0
  52. data/lib/x/core/errors/unprocessable_entity.rb +10 -0
  53. data/lib/x/core/errors/unsupported_format.rb +22 -0
  54. data/lib/x/core/errors/unsupported_media_type.rb +10 -0
  55. data/lib/x/core/errors/unsupported_operation.rb +18 -0
  56. data/lib/x/core/oauth1_authenticator.rb +142 -0
  57. data/lib/x/core/oauth2_authenticator.rb +296 -0
  58. data/lib/x/core/oauth2_authorization.rb +405 -0
  59. data/lib/x/core/oauth2_refresh.rb +272 -0
  60. data/lib/x/core/oauth2_tokens.rb +277 -0
  61. data/lib/x/core/origin.rb +95 -0
  62. data/lib/x/core/problem.rb +302 -0
  63. data/lib/x/core/proxy_setting.rb +22 -0
  64. data/lib/x/core/rate_limit.rb +137 -0
  65. data/lib/x/core/rate_limit_handler.rb +166 -0
  66. data/lib/x/core/redirect_handler.rb +208 -0
  67. data/lib/x/core/refresh_reporter.rb +92 -0
  68. data/lib/x/core/request_builder.rb +155 -0
  69. data/lib/x/core/request_context.rb +85 -0
  70. data/lib/x/core/request_encoding.rb +135 -0
  71. data/lib/x/core/response.rb +196 -0
  72. data/lib/x/core/response_headers.rb +31 -0
  73. data/lib/x/core/response_parser.rb +155 -0
  74. data/lib/x/core/retry_handler.rb +149 -0
  75. data/lib/x/core/setting_validator.rb +399 -0
  76. data/lib/x/core/stream_body.rb +77 -0
  77. data/lib/x/core/stream_response.rb +267 -0
  78. data/lib/x/core/token_endpoint.rb +238 -0
  79. data/lib/x/core/version.rb +25 -0
  80. data/lib/x/core.rb +6 -0
  81. data/sig/manifest.yaml +8 -0
  82. data/sig/x-core.rbs +396 -0
  83. metadata +162 -0
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require_relative "rate_limit_handler"
5
+ require_relative "redirect_handler"
6
+ require_relative "response"
7
+ require_relative "retry_handler"
8
+ require_relative "setting_validator"
9
+
10
+ module X
11
+ module Core
12
+ # The settings of a client other than its credentials: its base URL, parsing classes, hook, and the settings of
13
+ # its connection and handlers, included into ClientInternals
14
+ #
15
+ # A client is built with the settings it keeps for as long as it lives. {Client#with} derives a client whose
16
+ # settings differ, rather than replacing the ones a client holds, so that a request never runs under a setting
17
+ # another thread is halfway through changing.
18
+ #
19
+ # @api private
20
+ module ClientSettings
21
+ extend Forwardable
22
+
23
+ # The base URL for API requests
24
+ #
25
+ # {Client#base_url} returns it.
26
+ #
27
+ # @api private
28
+ # @return [String] the base URL for API requests, which ends with a slash
29
+ attr_reader :base_url
30
+
31
+ # The default class for parsing JSON arrays
32
+ #
33
+ # {Client#default_array_class} returns it.
34
+ #
35
+ # @api private
36
+ # @return [Class] the default class for parsing JSON arrays
37
+ attr_reader :default_array_class
38
+
39
+ # The default class for parsing JSON objects
40
+ #
41
+ # {Client#default_object_class} returns it.
42
+ #
43
+ # @api private
44
+ # @return [Class, #from_response] the default class for parsing JSON objects
45
+ attr_reader :default_object_class
46
+
47
+ # The callable passed an X::Response after each request and streamed object
48
+ #
49
+ # {Client#on_response} returns it.
50
+ #
51
+ # @api private
52
+ # @return [#call, nil] the callable, or nil for none
53
+ attr_reader :on_response
54
+
55
+ # The headers sent with every request the client makes
56
+ #
57
+ # {Client#headers} returns it.
58
+ #
59
+ # @api private
60
+ # @return [Hash{String => String}] the headers, frozen
61
+ attr_reader :headers
62
+
63
+ def_delegators :@connection, :open_timeout, :read_timeout, :write_timeout, :keep_alive_timeout, :debug_output
64
+ def_delegators :@redirect_handler, :max_redirects
65
+ def_delegators :@rate_limit_handler, :max_rate_limit_retries, :max_rate_limit_wait
66
+ def_delegators :@retry_handler, :max_retries
67
+
68
+ # Send a request that is safe to send twice again after a failure
69
+ #
70
+ # It is what Client#with_retries does.
71
+ #
72
+ # @api private
73
+ # @yield sends the request
74
+ # @return [Object] what the block returns
75
+ def with_retries(&) = @rate_limit_handler.handing_down { @retry_handler.handle(idempotent: true, resend_unanswered: true, &) }
76
+
77
+ private
78
+
79
+ # Share the connections kept open by the client this one was copied from
80
+ #
81
+ # They are shared when this client opens its connections as that one does; see Connection#share_pool_of.
82
+ # A copy that differs only in what it sends, such as its headers, base URL, or credentials, would otherwise
83
+ # open connections of its own, with a TCP and TLS handshake for each, and keep them open, idle, until it is
84
+ # collected, so a copy made for each request would leave connections to each host behind it.
85
+ #
86
+ # The internals of the client copied call it on those of the copy with __send__, since it is private.
87
+ #
88
+ # @api private
89
+ # @param connection [Connection] the connection of the client this one was copied from
90
+ # @return [void]
91
+ def share_connection(connection) = @connection.share_pool_of(connection)
92
+
93
+ # The settings, as initialize accepts them
94
+ # @api private
95
+ # @return [Hash{Symbol => Object}] the settings
96
+ def settings
97
+ {base_url:, open_timeout:, read_timeout:, write_timeout:, keep_alive_timeout:, debug_output:, proxy_url:,
98
+ default_array_class:, default_object_class:, headers:, max_redirects:, max_rate_limit_retries:,
99
+ max_rate_limit_wait:, max_retries:, on_response:, save_tokens:, load_tokens:}
100
+ end
101
+
102
+ # Initialize the settings, and the handlers of redirects, rate limits, and retries
103
+ #
104
+ # An endpoint is resolved against the base URL, which drops the last segment of a path that does not end with a
105
+ # slash, so a slash is added to a base URL without one. The headers are copied and frozen, so that changing the
106
+ # Hash a client was built with never changes what it sends.
107
+ #
108
+ # @api private
109
+ # @param base_url [String] the base URL for API requests
110
+ # @param default_array_class [Class] the default class for parsing JSON arrays
111
+ # @param default_object_class [Class, #from_response] the default class for parsing JSON objects
112
+ # @param headers [Hash{String, Symbol => String}] the headers sent with every request
113
+ # @param on_response [#call, nil] the callable passed an X::Response after every request and streamed object
114
+ # @param max_redirects [Integer] the maximum number of redirects to follow
115
+ # @param max_rate_limit_retries [Integer] the maximum number of times to retry a request refused for a rate limit
116
+ # @param max_rate_limit_wait [Integer, Float] the maximum number of seconds to wait for a rate limit to reset
117
+ # @param max_retries [Integer] the maximum number of times to send an idempotent request again after a failure
118
+ # @return [void]
119
+ # @raise [ArgumentError] if on_response is neither nil nor responds to call
120
+ # @raise [ArgumentError] if default_array_class is not a Class, or default_object_class is neither a Class nor
121
+ # responds to from_response
122
+ def initialize_settings(base_url:, default_array_class:, default_object_class:, headers:, on_response:,
123
+ max_redirects:, max_rate_limit_retries:, max_rate_limit_wait:, max_retries:)
124
+ base_url = SettingValidator.base_url!(base_url)
125
+ @base_url = (base_url.end_with?("/") ? base_url : "#{base_url}/").freeze
126
+ @default_array_class = SettingValidator.array_class!(:default_array_class, default_array_class)
127
+ @default_object_class = SettingValidator.object_class!(:default_object_class, default_object_class)
128
+ @headers = SettingValidator.headers!(headers).freeze
129
+ @on_response = SettingValidator.callable!(:on_response, on_response)
130
+ @redirect_handler = RedirectHandler.new(connection: @connection, request_builder: @request_builder, max_redirects:)
131
+ @rate_limit_handler = RateLimitHandler.new(max_rate_limit_retries:, max_rate_limit_wait:)
132
+ @retry_handler = RetryHandler.new(max_retries:)
133
+ end
134
+
135
+ # The headers of a request, the client's under the request's own
136
+ #
137
+ # A header of the request is sent in place of one of the client whose name differs from it in case alone.
138
+ #
139
+ # @api private
140
+ # @param request_headers [Hash{String => String}] the headers passed to the request
141
+ # @return [Hash{String => String}] the headers to send
142
+ def headers_for(request_headers) = RequestBuilder.merge_headers(headers, request_headers)
143
+
144
+ # Pass a response to on_response and to the block of the request
145
+ #
146
+ # Both receive the one summary, the client's hook first, so that a hook which counts every request and a block
147
+ # which reads the response of one see the same object. Neither is built a summary when there is nothing to
148
+ # pass it to.
149
+ #
150
+ # @api private
151
+ # @param http_method [Symbol, String] the HTTP method of the request, in any case
152
+ # @param uri [URI::Generic] the URI of the request
153
+ # @param response [Net::HTTPResponse] the HTTP response
154
+ # @yieldparam response [Response] the summary of the response
155
+ # @return [void]
156
+ def report(http_method, uri, response, &block)
157
+ return unless on_response || block
158
+
159
+ summary = Response.new(http_response: response, http_method:, uri:)
160
+ on_response&.call(summary)
161
+ block&.call(summary)
162
+ end
163
+ end
164
+ private_constant :ClientSettings
165
+ end
166
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "app_only_authenticator"
4
+ require_relative "oauth2_authenticator"
5
+ require_relative "setting_validator"
6
+
7
+ module X
8
+ module Core
9
+ # The OAuth 2.0 authenticator of a client, which its copies share, and the tokens it last refreshed, included into
10
+ # ClientInternals
11
+ # @api private
12
+ module ClientTokenRefresh
13
+ # The message of the error raised for a copy given an expiration time or scopes for the access token it shares
14
+ SHARED_EXPIRATION = "A copy that shares the access token of the client shares its expiration time and scopes, " \
15
+ "so it cannot be given %s. Pass it beside the access token and refresh token it belongs to"
16
+ private_constant :SHARED_EXPIRATION
17
+ # The options that belong to the tokens of the client, which a copy that does not keep the client's
18
+ # authenticator holds only when it is given them
19
+ TOKEN_OPTIONS = %i[refresh_token expires_at scopes save_tokens load_tokens].freeze
20
+ # The options that are credentials, any of which a copy that authenticates as the client is not given
21
+ CREDENTIAL_OPTIONS = %i[api_key api_key_secret access_token access_token_secret bearer_token client_id client_secret refresh_token].freeze
22
+ private_constant :TOKEN_OPTIONS, :CREDENTIAL_OPTIONS
23
+
24
+ # The time the OAuth 2.0 access token expires, as last refreshed
25
+ #
26
+ # {Client#expires_at} returns it.
27
+ #
28
+ # @api private
29
+ # @return [Time, nil] the expiration time, or nil if it is not known
30
+ def expires_at
31
+ current = oauth2_authenticator_in_use
32
+ current ? current.expires_at : @expires_at
33
+ end
34
+
35
+ # The scopes X granted the OAuth 2.0 access token, as last refreshed
36
+ #
37
+ # {Client#scopes} returns it.
38
+ #
39
+ # @api private
40
+ # @return [Array<String>, nil] the scopes, or nil if they are not known
41
+ def scopes
42
+ current = oauth2_authenticator_in_use
43
+ current ? current.scopes : @scopes
44
+ end
45
+
46
+ private
47
+
48
+ # Initialize the callables that store and load the tokens of a refresh
49
+ # @api private
50
+ # @param save_tokens [#call, nil] the callable passed the OAuth2Tokens of each refresh
51
+ # @param load_tokens [#call, nil] the callable that returns the OAuth2Tokens in the store
52
+ # @return [void]
53
+ # @raise [ArgumentError] if save_tokens or load_tokens is neither nil nor responds to call
54
+ def initialize_token_hooks(save_tokens:, load_tokens:)
55
+ @save_tokens = SettingValidator.callable!(:save_tokens, save_tokens)
56
+ @load_tokens = SettingValidator.callable!(:load_tokens, load_tokens)
57
+ end
58
+
59
+ # Share the authenticator of the client this one was copied from
60
+ #
61
+ # A copy that holds the credentials of the client shares its OAuth 2.0 or app-only authenticator, so that the
62
+ # copy refreshes the tokens of the client, or sends the bearer token it fetched, rather than hold tokens of its
63
+ # own. A copy given an authenticator of its own keeps it. The internals of the client copied call it on those of
64
+ # the copy with __send__, since it is private.
65
+ #
66
+ # @api private
67
+ # @param copy [Client] the copy these are the internals of
68
+ # @param other [Authenticator] the authenticator of the client this one was copied from
69
+ # @param options [Hash] the options the copy was given in place of the client's
70
+ # @return [void]
71
+ def share_authenticator(copy, other, options)
72
+ return if options[:authenticator]
73
+
74
+ case other
75
+ when AppOnlyAuthenticator then share_app_only(other, options)
76
+ when OAuth2Authenticator then share_oauth2(copy, other, options)
77
+ end
78
+ end
79
+
80
+ # Share the OAuth 2.0 authenticator of the client this one was copied from
81
+ #
82
+ # A refresh by either client then reaches both. X accepts a refresh token once, so a copy that refreshed with
83
+ # an authenticator of its own would leave the original client with a refresh token that no longer works.
84
+ #
85
+ # Whether the two share it is decided by the options the copy was given, not by the tokens it was built with:
86
+ # a refresh on another thread may replace them while it is built, and a copy that held on to the ones replaced
87
+ # could never refresh again. The expiration time and scopes are facts about the access token the two hold, so a
88
+ # copy that shares it is refused either, rather than set it for the client it was copied from.
89
+ #
90
+ # @api private
91
+ # @param copy [Client] the copy these are the internals of
92
+ # @param other [OAuth2Authenticator] the authenticator of the client this one was copied from
93
+ # @param options [Hash] the options the copy was given in place of the client's
94
+ # @return [void]
95
+ # @raise [ArgumentError] if the copy shares the authenticator and was given an expiration time or scopes
96
+ def share_oauth2(copy, other, options)
97
+ return unless oauth2_authenticator_in_use && other.__send__(:holds?, options)
98
+
99
+ given = options.keys & %i[expires_at scopes]
100
+ raise ArgumentError, format(SHARED_EXPIRATION, given.join(" or ")) unless given.empty?
101
+
102
+ @authenticator = join(copy, other)
103
+ end
104
+
105
+ # The options of a copy, without the tokens of a client it does not share
106
+ #
107
+ # A copy that authenticates otherwise than the client authenticates with tokens that may be another user's. It
108
+ # holds the refresh token, expiration time, scopes, save_tokens, and load_tokens of the client only when it is
109
+ # given them: a refresh of the copy would otherwise spend the refresh token the client holds, which X accepts
110
+ # once, read the store of the client with its load_tokens and take the client's tokens in place of its own, or
111
+ # pass its tokens to the save_tokens of the client, which would store them in place of the client's.
112
+ #
113
+ # @api private
114
+ # @param copied [Hash{Symbol => Object}] the options the copy is built with
115
+ # @param options [Hash{Symbol => Object}] the options the copy was given
116
+ # @return [Hash{Symbol => Object}] the options, without those of the tokens of the client the copy was not given
117
+ def without_tokens_of_client(copied, options)
118
+ keeps_authenticator?(copied, options) ? copied : copied.except(*(TOKEN_OPTIONS - options.keys))
119
+ end
120
+
121
+ # Check whether a copy keeps the authenticator of the client
122
+ #
123
+ # A copy of a client given its authenticator keeps it unless given a credential or an authenticator, which
124
+ # replaces it. A copy of a client built from credentials keeps its OAuth 2.0 authenticator unless given an
125
+ # authenticator, or a client ID, client secret, access token, or refresh token the authenticator does not hold,
126
+ # and keeps any other authenticator unless given an authenticator or any credential.
127
+ #
128
+ # @api private
129
+ # @param copied [Hash{Symbol => Object}] the options the copy is built with
130
+ # @param options [Hash{Symbol => Object}] the options the copy was given
131
+ # @return [Boolean] whether the copy authenticates as the client does
132
+ def keeps_authenticator?(copied, options)
133
+ return copied[:authenticator].equal?(@authenticator) if @given_authenticator
134
+ return false if options[:authenticator]
135
+
136
+ current = oauth2_authenticator_in_use
137
+ current ? current.__send__(:holds?, options) : !options.keys.intersect?(CREDENTIAL_OPTIONS)
138
+ end
139
+
140
+ # Share the app-only authenticator of the client this one was copied from
141
+ #
142
+ # It is shared by a copy that authenticates as the app with the API key and secret the authenticator holds, at
143
+ # the origin it fetches the bearer token at, and was given no bearer token of its own, so the bearer token the
144
+ # client fetched, or fetches, is fetched once, and is sent to no other origin than the one that issued it.
145
+ #
146
+ # @api private
147
+ # @param other [AppOnlyAuthenticator] the authenticator of the client this one was copied from
148
+ # @param options [Hash] the options the copy was given in place of the client's
149
+ # @return [void]
150
+ def share_app_only(other, options)
151
+ return unless AppOnlyAuthenticator === @authenticator && other.__send__(:holds?, options)
152
+
153
+ @authenticator = other if other.__send__(:fetched_for?, base_url)
154
+ end
155
+
156
+ # Take an authenticator the client was given, as it would one it built
157
+ #
158
+ # The token requests of an authenticator that makes them are sent over the connection of the first client
159
+ # given it, and the refreshes of an OAuth 2.0 one reach the save_tokens of each client that shares it.
160
+ #
161
+ # @api private
162
+ # @param client [Client] the client these are the internals of
163
+ # @param authenticator [Authenticator] the authenticator
164
+ # @return [Authenticator] the authenticator
165
+ def take(client, authenticator)
166
+ case authenticator
167
+ when OAuth2Authenticator then join(client, authenticator.__send__(:token_requests_over, @connection, base_url, headers))
168
+ when AppOnlyAuthenticator then authenticator.__send__(:token_requests_over, @connection, base_url, headers)
169
+ else authenticator
170
+ end
171
+ end
172
+
173
+ # Join the clients whose save_tokens an OAuth 2.0 authenticator reports to
174
+ # @api private
175
+ # @param client [Client] the client these are the internals of
176
+ # @param authenticator [OAuth2Authenticator] the authenticator
177
+ # @return [OAuth2Authenticator] the authenticator
178
+ def join(client, authenticator)
179
+ clients = authenticator.__send__(:clients)
180
+ clients[client] = true
181
+ authenticator.__send__(:report_refreshes_to, -> { clients.keys.filter_map(&:save_tokens).uniq })
182
+ authenticator
183
+ end
184
+
185
+ # The OAuth 2.0 authenticator, if the client authenticates with one
186
+ # @api private
187
+ # @return [OAuth2Authenticator, nil] the authenticator or nil
188
+ def oauth2_authenticator_in_use
189
+ current = @authenticator
190
+ current if current.is_a?(OAuth2Authenticator)
191
+ end
192
+
193
+ # The OAuth 2.0 authenticator of the client's credentials, if they form a set
194
+ #
195
+ # A copy of a client shares the authenticator of the client it was copied from, rather than the one this
196
+ # builds, when the two hold the same credentials; see share_authenticator. A client ID and access token without
197
+ # a refresh token, as an authorization without offline.access issues them, build an authenticator that acts
198
+ # for the user and cannot refresh.
199
+ #
200
+ # @api private
201
+ # @param client [Client] the client these are the internals of
202
+ # @return [OAuth2Authenticator, nil] the OAuth 2.0 authenticator or nil
203
+ def oauth2_authenticator(client)
204
+ client_id = @client_id
205
+ access_token = @access_token
206
+ return unless client_id && access_token
207
+
208
+ new_oauth2_authenticator(client, client_id:, access_token:, refresh_token: @refresh_token)
209
+ end
210
+
211
+ # The OAuth 2.0 authenticator of the client's credentials, as last refreshed
212
+ #
213
+ # It is the one the client built of them, or shares with the client it was copied from. A client given its
214
+ # authenticator holds no credentials, so the tokens of that authenticator are none of its credentials, which a
215
+ # copy would be built with beside it.
216
+ #
217
+ # @api private
218
+ # @return [OAuth2Authenticator, nil] the authenticator, or nil for a client that holds no OAuth 2.0 credentials
219
+ def oauth2_credentials_in_use = (oauth2_authenticator_in_use unless @given_authenticator)
220
+
221
+ # The access token for OAuth authentication, as last refreshed
222
+ #
223
+ # It is private, as {ClientCredentials#api_key_secret} is, and as the access token of the authenticator is. A
224
+ # hook given to save_tokens is passed the OAuth2Tokens of the refresh it reports.
225
+ #
226
+ # @api private
227
+ # @return [String, nil] the access token for OAuth authentication
228
+ def access_token
229
+ current = oauth2_credentials_in_use
230
+ current ? current.__send__(:access_token) : @access_token
231
+ end
232
+
233
+ # The OAuth 2.0 refresh token, as last refreshed
234
+ #
235
+ # It is private for the reason {#access_token} is.
236
+ #
237
+ # @api private
238
+ # @return [String, nil] the OAuth 2.0 refresh token
239
+ def refresh_token
240
+ current = oauth2_credentials_in_use
241
+ current ? current.__send__(:refresh_token) : @refresh_token
242
+ end
243
+
244
+ # Build an OAuth 2.0 authenticator whose refreshes reach the clients that share it
245
+ #
246
+ # It sends its token requests over the client's connection. A public client has no client secret, and refreshes
247
+ # its tokens with its client ID alone.
248
+ #
249
+ # @api private
250
+ # @param client [Client] the client these are the internals of
251
+ # @param client_id [String] the OAuth 2.0 client ID
252
+ # @param access_token [String] the OAuth 2.0 access token
253
+ # @param refresh_token [String, nil] the OAuth 2.0 refresh token, or nil for an access token that is not refreshed
254
+ # @return [OAuth2Authenticator] the OAuth 2.0 authenticator
255
+ def new_oauth2_authenticator(client, client_id:, access_token:, refresh_token:)
256
+ authenticator = OAuth2Authenticator.new(client_id:, client_secret: @client_secret, access_token:, refresh_token:, expires_at: @expires_at, scopes: @scopes)
257
+ join(client, authenticator.__send__(:token_requests_over, @connection, base_url, headers))
258
+ end
259
+
260
+ # Run a request, again if a refresh replaces an OAuth 2.0 token the API rejects
261
+ #
262
+ # Only a rejection by the origin of the base URL, which the token is sent to, refreshes it; see {Origin}. An
263
+ # app-only bearer token the API rejects is fetched again the same way; see {AppOnlyAuthenticator}. A stream
264
+ # that Client#get_stream opens runs through it too.
265
+ #
266
+ # @api private
267
+ # @param client [Client] the client these are the internals of, which a refresh that fails to report is raised
268
+ # with
269
+ # @yield runs the request
270
+ # @return [Object] what the block returns
271
+ def refreshing_rejected_token(client, &)
272
+ case (current = @authenticator)
273
+ when OAuth2Authenticator then current.__send__(:retrying_rejected_token, URI(base_url), @connection, client, &)
274
+ when AppOnlyAuthenticator then current.__send__(:retrying_rejected_token, URI(base_url), &)
275
+ else yield
276
+ end
277
+ end
278
+ end
279
+ private_constant :ClientTokenRefresh
280
+ end
281
+ end