kinde_sdk 1.7.1 → 1.8.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: c317b417d2fc3da704816894bb6dc06cd7e14c93aa2b1f517f11bd326d80ec18
4
- data.tar.gz: a03e51851793bb4df9e87385e0110ca4a39acd9e8ff5bcfffecaf3a6ceeff7fe
3
+ metadata.gz: 5e745fcc09127b3a90b6b4049d9bc490f268896ab75612681a648600b4bb8033
4
+ data.tar.gz: 63cdad5a63d19c05b86cff557750ce5b75cdd8f65c99a30eb88ea6cc5032791e
5
5
  SHA512:
6
- metadata.gz: 26375435c668f53b6de31aee90cb19ff19502798f0dbac74102db2ad25d70bdb6861c5b29875baba8f00c68dd5f47f4076f8c6b046b6761f1cf76b08f3d3fe61
7
- data.tar.gz: 6888bc72e8209f4d785c4b9d333b8619ceed14ac8642eb12a78ed7e967aa88b1ac6a56ea00f6ac69e34f771830503e601c2667aed3c5064bdf27de3cc88daca8
6
+ metadata.gz: 6aada675872e57b16fce0d548f0b02c205a9c2e0aa71a8f6d2160424b4625fb237d84df2c549bbf3112efc5d49f2d0465934a387ec285355e52581e4c2ddd014
7
+ data.tar.gz: 2dd83d28cbd2fd1211411c84ce578721684e675b072588c97e4422b5f201bad5c623c0e57c49866b04aa8e6948b131a663fd6096896f33b263694a0bc683fef1
@@ -20,9 +20,17 @@ module KindeSdk
20
20
  def auth
21
21
  # Generate a secure random nonce for CSRF protection
22
22
  nonce = SecureRandom.urlsafe_base64(16)
23
-
23
+
24
24
  # Get authorization URL and PKCE code verifier from SDK
25
- auth_data = KindeSdk.auth_url(nonce: nonce)
25
+ auth_params = { nonce: nonce }
26
+
27
+ # Check for invitation_code in query parameters
28
+ # Validate to prevent injection and ensure it's a reasonable length
29
+ if params[:invitation_code].is_a?(String) && params[:invitation_code].strip.length > 0 && params[:invitation_code].length <= 255
30
+ auth_params[:invitation_code] = params[:invitation_code].strip
31
+ end
32
+
33
+ auth_data = KindeSdk.auth_url(**auth_params)
26
34
 
27
35
  # Store PKCE code verifier and nonce in session for validation
28
36
  session[:code_verifier] = auth_data[:code_verifier] if auth_data[:code_verifier].present?
data/kinde_api/Gemfile CHANGED
@@ -3,7 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :development, :test do
6
- gem 'rake', '~> 13.0.1'
6
+ gem 'rake', '~> 13.4.0'
7
7
  gem 'pry-byebug'
8
- gem 'rubocop', '~> 0.66.0'
8
+ gem 'rubocop', '~> 1.88.0'
9
9
  end
@@ -293,7 +293,7 @@ end
293
293
  ### HTTP request headers
294
294
 
295
295
  - **Content-Type**: Not defined
296
- - **Accept**: application/json; charset=utf-8, application/json
296
+ - **Accept**: application/json
297
297
 
298
298
 
299
299
  ## replace_connection
@@ -251,7 +251,7 @@ module KindeApi
251
251
  # header parameters
252
252
  header_params = opts[:header_params] || {}
253
253
  # HTTP header 'Accept' (if needed)
254
- header_params['Accept'] = @api_client.select_header_accept(['application/json; charset=utf-8', 'application/json'])
254
+ header_params['Accept'] = @api_client.select_header_accept(['application/json'])
255
255
 
256
256
  # form parameters
257
257
  form_params = opts[:form_params] || {}
@@ -328,9 +328,15 @@ module KindeApi
328
328
  # @return [String] the Accept header (e.g. application/json)
329
329
  def select_header_accept(accepts)
330
330
  return nil if accepts.nil? || accepts.empty?
331
+ # Prefer plain application/json; the API rejects charset variants in Accept.
332
+ plain_json = accepts.find { |s| s.strip.casecmp('application/json').zero? }
333
+ return plain_json if plain_json
331
334
  # use JSON when present, otherwise use all of the provided
332
335
  json_accept = accepts.find { |s| json_mime?(s) }
333
- json_accept || accepts.join(',')
336
+ if json_accept
337
+ return json_accept.strip.casecmp('application/json').zero? ? json_accept : 'application/json'
338
+ end
339
+ accepts.join(',')
334
340
  end
335
341
 
336
342
  # Return Content-Type header based on an array of content types provided.
@@ -46,7 +46,7 @@ module KindeApi
46
46
  {
47
47
  :'code' => :'String',
48
48
  :'message' => :'String',
49
- :'connections' => :'Array<Connection>',
49
+ :'connections' => :'Array<ConnectionConnection>',
50
50
  :'has_more' => :'Boolean'
51
51
  }
52
52
  end
@@ -187,7 +187,9 @@ describe KindeApi::ApiClient do
187
187
  expect(api_client.select_header_accept([])).to be_nil
188
188
 
189
189
  expect(api_client.select_header_accept(['application/json'])).to eq('application/json')
190
- expect(api_client.select_header_accept(['application/xml', 'application/json; charset=UTF8'])).to eq('application/json; charset=UTF8')
190
+ expect(api_client.select_header_accept(['application/json; charset=utf-8'])).to eq('application/json')
191
+ expect(api_client.select_header_accept(['application/xml', 'application/json; charset=UTF8'])).to eq('application/json')
192
+ expect(api_client.select_header_accept(['application/json; charset=utf-8', 'application/json'])).to eq('application/json')
191
193
  expect(api_client.select_header_accept(['APPLICATION/JSON', 'text/html'])).to eq('APPLICATION/JSON')
192
194
 
193
195
  expect(api_client.select_header_accept(['application/xml'])).to eq('application/xml')
@@ -38,8 +38,24 @@ describe KindeApi::GetConnectionsResponse do
38
38
  end
39
39
 
40
40
  describe 'test attribute "connections"' do
41
- it 'should work' do
42
- # assertion here. ref: https://www.relishapp.com/rspec/rspec-expectations/docs/built-in-matchers
41
+ it 'deserializes flat connection objects from the list endpoint' do
42
+ response = described_class.build_from_hash(
43
+ 'connections' => [
44
+ {
45
+ 'id' => 'conn_123',
46
+ 'name' => 'google',
47
+ 'display_name' => 'Google',
48
+ 'strategy' => 'oauth2'
49
+ }
50
+ ]
51
+ )
52
+
53
+ connection = response.connections.first
54
+ expect(connection).to be_a(KindeApi::ConnectionConnection)
55
+ expect(connection.id).to eq('conn_123')
56
+ expect(connection.name).to eq('google')
57
+ expect(connection.display_name).to eq('Google')
58
+ expect(connection.strategy).to eq('oauth2')
43
59
  end
44
60
  end
45
61
 
@@ -1,6 +1,7 @@
1
1
  module KindeSdk
2
2
  class Client
3
3
  module Entitlements
4
+ include KindeSdk::Logging
4
5
  # Get all entitlements for the authenticated user
5
6
  # Matches the JavaScript SDK API: getEntitlements(options?)
6
7
  #
@@ -66,21 +67,6 @@ module KindeSdk
66
67
  # JavaScript SDK compatible aliases
67
68
  alias_method :hasBillingEntitlements, :has_billing_entitlements?
68
69
  alias_method :hasEntitlements, :has_entitlements?
69
-
70
- private
71
-
72
- # Configurable logging that works with or without Rails
73
- def log_error(message)
74
- if defined?(Rails) && Rails.logger
75
- Rails.logger.error(message)
76
- elsif @logger
77
- @logger.error(message)
78
- elsif respond_to?(:logger) && logger
79
- logger.error(message)
80
- else
81
- $stderr.puts "[KindeSdk] ERROR: #{message}"
82
- end
83
- end
84
70
  end
85
71
  end
86
- end
72
+ end
@@ -1,6 +1,7 @@
1
1
  module KindeSdk
2
2
  class Client
3
3
  module FeatureFlags
4
+ include KindeSdk::Logging
4
5
  # Get all feature flags for the authenticated user
5
6
  # Matches the JavaScript SDK API: getFlags(options?)
6
7
  #
@@ -137,27 +138,30 @@ module KindeSdk
137
138
  end
138
139
 
139
140
  # Extract the key from a flag object (handles both symbol and string keys)
141
+ # Uses key? to properly handle falsy values
140
142
  #
141
143
  # @param flag [Hash] Flag object
142
144
  # @return [String] The flag key
143
145
  def get_flag_key(flag)
144
- flag[:key] || flag['key']
146
+ flag.key?(:key) ? flag[:key] : flag['key']
145
147
  end
146
148
 
147
149
  # Extract the value from a flag object (handles both symbol and string keys)
150
+ # Uses key? to properly handle boolean false values (critical fix)
148
151
  #
149
152
  # @param flag [Hash] Flag object
150
153
  # @return [Object] The flag value
151
154
  def get_flag_value(flag)
152
- flag[:value] || flag['value']
155
+ flag.key?(:value) ? flag[:value] : flag['value']
153
156
  end
154
157
 
155
158
  # Extract the type from a flag object (handles both symbol and string keys)
159
+ # Uses key? to properly handle falsy values
156
160
  #
157
161
  # @param flag [Hash] Flag object
158
162
  # @return [String] The flag type
159
163
  def get_flag_type(flag)
160
- flag[:type] || flag['type']
164
+ flag.key?(:type) ? flag[:type] : flag['type']
161
165
  end
162
166
 
163
167
  # Legacy get_flag implementation for backward compatibility
@@ -282,19 +286,6 @@ module KindeSdk
282
286
  def check_type(value, type)
283
287
  type == "s" && value.is_a?(String) || type == "b" && (value == false || value == true) || type == "i" && value.is_a?(Integer)
284
288
  end
285
-
286
- # Configurable logging that works with or without Rails
287
- def log_error(message)
288
- if defined?(Rails) && Rails.logger
289
- Rails.logger.error(message)
290
- elsif @logger
291
- @logger.error(message)
292
- elsif respond_to?(:logger) && logger
293
- logger.error(message)
294
- else
295
- $stderr.puts "[KindeSdk] ERROR: #{message}"
296
- end
297
- end
298
289
  end
299
290
  end
300
291
  end
@@ -1,6 +1,7 @@
1
1
  module KindeSdk
2
2
  class Client
3
3
  module Permissions
4
+ include KindeSdk::Logging
4
5
  # Get all permissions for the authenticated user
5
6
  # Matches the JavaScript SDK API: getPermissions(options?)
6
7
  #
@@ -178,34 +179,6 @@ module KindeSdk
178
179
  get_permissions_from_token
179
180
  end
180
181
  end
181
-
182
- # Configurable logging that works with or without Rails
183
- #
184
- # @param message [String] The error message to log
185
- def log_error(message)
186
- if defined?(Rails) && Rails.logger
187
- Rails.logger.error(message)
188
- elsif @logger
189
- @logger.error(message)
190
- elsif respond_to?(:logger) && logger
191
- logger.error(message)
192
- else
193
- # Fallback to STDERR if no logger available
194
- $stderr.puts "[KindeSdk] ERROR: #{message}"
195
- end
196
- end
197
-
198
- def log_warning(message)
199
- if defined?(Rails) && Rails.logger
200
- Rails.logger.warn(message)
201
- elsif @logger
202
- @logger.warn(message)
203
- elsif respond_to?(:logger) && logger
204
- logger.warn(message)
205
- else
206
- $stderr.puts "[KindeSdk] WARNING: #{message}"
207
- end
208
- end
209
182
  end
210
183
  end
211
184
  end
@@ -1,6 +1,7 @@
1
1
  module KindeSdk
2
2
  class Client
3
3
  module Roles
4
+ include KindeSdk::Logging
4
5
  # Get all roles for the authenticated user
5
6
  # Matches the JavaScript SDK API: getRoles(options?)
6
7
  # Implements smart fallback: uses API automatically if token claims are empty
@@ -166,53 +167,20 @@ module KindeSdk
166
167
  end
167
168
 
168
169
  # Helper to extract field from hash or object
170
+ # Checks Hash first to avoid issues with Hash#key requiring an argument
171
+ #
172
+ # @param item [Hash, Object] The item to extract from
173
+ # @param field [Symbol] The field name to extract
174
+ # @return [Object, nil] The field value or nil
169
175
  def extract_field(item, field)
170
- if item.respond_to?(field)
176
+ if item.is_a?(Hash)
177
+ item.key?(field) ? item[field] : item[field.to_s]
178
+ elsif item.respond_to?(field)
171
179
  item.public_send(field)
172
- elsif item.is_a?(Hash)
173
- item[field] || item[field.to_s]
174
180
  else
175
181
  nil
176
182
  end
177
183
  end
178
-
179
- # Configurable logging that works with or without Rails
180
- # Supports multiple log levels for better debugging
181
- def log_error(message)
182
- write_log(:error, message)
183
- end
184
-
185
- def log_warning(message)
186
- write_log(:warn, message)
187
- end
188
-
189
- def log_info(message)
190
- write_log(:info, message)
191
- end
192
-
193
- def log_debug(message)
194
- write_log(:debug, message)
195
- end
196
-
197
- def write_log(level, message)
198
- formatted_message = "[KindeSdk::Roles] #{message}"
199
-
200
- if defined?(Rails) && Rails.logger
201
- Rails.logger.public_send(level, formatted_message)
202
- elsif @logger && @logger.respond_to?(level)
203
- @logger.public_send(level, formatted_message)
204
- elsif respond_to?(:logger) && logger && logger.respond_to?(level)
205
- logger.public_send(level, formatted_message)
206
- else
207
- # Fallback based on level
208
- case level
209
- when :error, :warn
210
- $stderr.puts formatted_message
211
- when :info, :debug
212
- $stdout.puts formatted_message if ENV['KINDE_DEBUG']
213
- end
214
- end
215
- end
216
184
  end
217
185
  end
218
- end
186
+ end
@@ -21,6 +21,7 @@ module KindeSdk
21
21
  end
22
22
 
23
23
  class Client
24
+ include Logging
24
25
  include FeatureFlags
25
26
  include Permissions
26
27
  include Roles
@@ -120,7 +121,7 @@ module KindeSdk
120
121
  begin
121
122
  { url: URI.parse(result['url']) }
122
123
  rescue URI::InvalidURIError => e
123
- Rails.logger.error(e)
124
+ log_error(e.message)
124
125
  raise StandardError, "Invalid URL format received from API: #{result['url']}"
125
126
  end
126
127
  end
@@ -176,7 +177,7 @@ module KindeSdk
176
177
  def entitlements(page_size: 10, starting_after: nil)
177
178
  frontend.get_entitlements(page_size: page_size, starting_after: starting_after)
178
179
  rescue StandardError => e
179
- Rails.logger.error("Failed to fetch entitlements: #{e.message}")
180
+ log_error("Failed to fetch entitlements: #{e.message}")
180
181
  raise KindeSdk::APIError, "Unable to fetch entitlements: #{e.message}"
181
182
  end
182
183
 
@@ -191,7 +192,7 @@ module KindeSdk
191
192
 
192
193
  paginate_all_results('entitlements') { |starting_after| entitlements(page_size: 100, starting_after: starting_after) }
193
194
  rescue StandardError => e
194
- Rails.logger.error("Failed to fetch all entitlements: #{e.message}")
195
+ log_error("Failed to fetch all entitlements: #{e.message}")
195
196
  raise KindeSdk::APIError, "Unable to fetch all entitlements: #{e.message}"
196
197
  end
197
198
 
@@ -206,7 +207,7 @@ module KindeSdk
206
207
  def entitlement(key)
207
208
  frontend.get_entitlement(key)
208
209
  rescue StandardError => e
209
- Rails.logger.error("Failed to fetch entitlement for #{key}: #{e.message}")
210
+ log_error("Failed to fetch entitlement for #{key}: #{e.message}")
210
211
  raise KindeSdk::APIError, "Unable to fetch entitlement: #{e.message}"
211
212
  end
212
213
 
@@ -221,7 +222,7 @@ module KindeSdk
221
222
 
222
223
  entitlements.find { |entitlement| entitlement.feature_key == key }
223
224
  rescue StandardError => e
224
- Rails.logger.error("Failed to get entitlement for #{key}: #{e.message}")
225
+ log_error("Failed to get entitlement for #{key}: #{e.message}")
225
226
  raise KindeSdk::APIError, "Unable to get entitlement: #{e.message}"
226
227
  end
227
228
 
@@ -231,9 +232,9 @@ module KindeSdk
231
232
  # @return [Boolean] True if the user has the entitlement, false otherwise
232
233
  def has_entitlement?(feature_key)
233
234
  entitlement_response = entitlement(feature_key)
234
- entitlement_response&.data&.entitlement.present?
235
+ !entitlement_response&.data&.entitlement.nil?
235
236
  rescue StandardError => e
236
- Rails.logger.error("Error checking entitlement for #{feature_key}: #{e.message}")
237
+ log_error("Error checking entitlement for #{feature_key}: #{e.message}")
237
238
  false
238
239
  end
239
240
 
@@ -244,7 +245,7 @@ module KindeSdk
244
245
  def hasEntitlement(key)
245
246
  getEntitlement(key) != nil
246
247
  rescue StandardError => e
247
- Rails.logger.error("Error checking entitlement for #{key}: #{e.message}")
248
+ log_error("Error checking entitlement for #{key}: #{e.message}")
248
249
  false
249
250
  end
250
251
 
@@ -257,7 +258,7 @@ module KindeSdk
257
258
  entitlement = getEntitlement(key)
258
259
  entitlement ? entitlement.entitlement_limit_max : nil
259
260
  rescue StandardError => e
260
- Rails.logger.error("Error getting entitlement limit for #{key}: #{e.message}")
261
+ log_error("Error getting entitlement limit for #{key}: #{e.message}")
261
262
  nil
262
263
  end
263
264
 
@@ -273,7 +274,7 @@ module KindeSdk
273
274
  def user_feature_flags(page_size: 10, starting_after: nil)
274
275
  frontend.get_feature_flags(page_size: page_size, starting_after: starting_after)
275
276
  rescue StandardError => e
276
- Rails.logger.error("Failed to fetch feature flags: #{e.message}")
277
+ log_error("Failed to fetch feature flags: #{e.message}")
277
278
  raise KindeSdk::APIError, "Unable to fetch feature flags: #{e.message}"
278
279
  end
279
280
 
@@ -286,7 +287,7 @@ module KindeSdk
286
287
  def user_permissions(page_size: 10, starting_after: nil)
287
288
  frontend.get_user_permissions(page_size: page_size, starting_after: starting_after)
288
289
  rescue StandardError => e
289
- Rails.logger.error("Failed to fetch permissions: #{e.message}")
290
+ log_error("Failed to fetch permissions: #{e.message}")
290
291
  raise KindeSdk::APIError, "Unable to fetch permissions: #{e.message}"
291
292
  end
292
293
 
@@ -299,7 +300,7 @@ module KindeSdk
299
300
  def user_properties(page_size: 10, starting_after: nil)
300
301
  frontend.get_user_properties(page_size: page_size, starting_after: starting_after)
301
302
  rescue StandardError => e
302
- Rails.logger.error("Failed to fetch properties: #{e.message}")
303
+ log_error("Failed to fetch properties: #{e.message}")
303
304
  raise KindeSdk::APIError, "Unable to fetch properties: #{e.message}"
304
305
  end
305
306
 
@@ -312,7 +313,7 @@ module KindeSdk
312
313
  def user_roles(page_size: 10, starting_after: nil)
313
314
  frontend.get_user_roles(page_size: page_size, starting_after: starting_after)
314
315
  rescue StandardError => e
315
- Rails.logger.error("Failed to fetch roles: #{e.message}")
316
+ log_error("Failed to fetch roles: #{e.message}")
316
317
  raise KindeSdk::APIError, "Unable to fetch roles: #{e.message}"
317
318
  end
318
319
 
@@ -325,7 +326,7 @@ module KindeSdk
325
326
  def portal_link(return_url:, page: PortalPage::PROFILE)
326
327
  frontend.get_portal_link(subnav: page, return_url: return_url)
327
328
  rescue StandardError => e
328
- Rails.logger.error("Failed to get portal link: #{e.message}")
329
+ log_error("Failed to get portal link: #{e.message}")
329
330
  raise KindeSdk::APIError, "Unable to get portal link: #{e.message}"
330
331
  end
331
332
 
@@ -347,7 +348,7 @@ module KindeSdk
347
348
 
348
349
  frontend.get_portal_link(subnav: sub_nav, return_url: return_url)
349
350
  rescue StandardError => e
350
- Rails.logger.error("Failed to generate portal URL: #{e.message}")
351
+ log_error("Failed to generate portal URL: #{e.message}")
351
352
  raise KindeSdk::APIError, "Unable to generate portal URL: #{e.message}"
352
353
  end
353
354
 
@@ -358,7 +359,7 @@ module KindeSdk
358
359
  def enhanced_user_profile
359
360
  frontend.get_user_profile_v2
360
361
  rescue StandardError => e
361
- Rails.logger.error("Failed to fetch enhanced profile: #{e.message}")
362
+ log_error("Failed to fetch enhanced profile: #{e.message}")
362
363
  raise KindeSdk::APIError, "Unable to fetch enhanced profile: #{e.message}"
363
364
  end
364
365
 
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KindeSdk
4
+ # Shared logging module for consistent logging across all SDK components.
5
+ # Supports Rails logger, custom logger, or falls back to stdout/stderr.
6
+ #
7
+ # Can be used as:
8
+ # - Instance methods via `include KindeSdk::Logging`
9
+ # - Class methods via `include KindeSdk::Logging` inside `class << self`
10
+ # - Direct calls via `KindeSdk::Logging.log_error("message")`
11
+ #
12
+ # @example Using in a class (instance methods)
13
+ # class MyClass
14
+ # include KindeSdk::Logging
15
+ #
16
+ # def my_method
17
+ # log_info("Starting operation")
18
+ # log_error("Something went wrong")
19
+ # end
20
+ # end
21
+ #
22
+ # @example Using for class methods
23
+ # class MyClass
24
+ # class << self
25
+ # include KindeSdk::Logging
26
+ #
27
+ # def my_class_method
28
+ # log_error("Class-level error")
29
+ # end
30
+ # end
31
+ # end
32
+ module Logging
33
+ # Log an error message
34
+ #
35
+ # @param message [String] The message to log
36
+ def log_error(message)
37
+ Logging.write_log(:error, message)
38
+ end
39
+
40
+ # Log a warning message
41
+ #
42
+ # @param message [String] The message to log
43
+ def log_warning(message)
44
+ Logging.write_log(:warn, message)
45
+ end
46
+
47
+ # Log an info message
48
+ #
49
+ # @param message [String] The message to log
50
+ def log_info(message)
51
+ Logging.write_log(:info, message)
52
+ end
53
+
54
+ # Log a debug message
55
+ #
56
+ # @param message [String] The message to log
57
+ def log_debug(message)
58
+ Logging.write_log(:debug, message)
59
+ end
60
+
61
+ class << self
62
+ # Write a log message at the specified level
63
+ # Tries multiple logger sources in order of preference:
64
+ # 1. Rails.logger (if Rails is defined)
65
+ # 2. Fallback to $stderr/$stdout based on level
66
+ #
67
+ # @param level [Symbol] The log level (:error, :warn, :info, :debug)
68
+ # @param message [String] The message to log
69
+ def write_log(level, message)
70
+ formatted_message = "[KindeSdk] #{message}"
71
+
72
+ logger_instance = resolve_logger
73
+ if logger_instance&.respond_to?(level)
74
+ logger_instance.public_send(level, formatted_message)
75
+ else
76
+ # Fallback based on level
77
+ case level
78
+ when :error, :warn
79
+ $stderr.puts formatted_message
80
+ when :info, :debug
81
+ $stdout.puts formatted_message if ENV['KINDE_DEBUG']
82
+ end
83
+ end
84
+ end
85
+
86
+ # Resolve the appropriate logger instance
87
+ #
88
+ # @return [Logger, nil] The logger to use, or nil if none available
89
+ def resolve_logger
90
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
91
+ Rails.logger
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+
@@ -0,0 +1,58 @@
1
+ module KindeSdk
2
+ # Normalizes token hash key types for oauth2 round-trips and SDK consumers.
3
+ # Accepts string or symbol keys on input; internal storage uses symbol keys.
4
+ module TokenHash
5
+ PUBLIC_KEYS = %i[
6
+ access_token refresh_token expires_at id_token scope token_type expires_in
7
+ ].freeze
8
+
9
+ module_function
10
+
11
+ # Symbolize keys while preserving the full oauth2 hash contract.
12
+ def normalize(raw)
13
+ return {} if raw.nil?
14
+ return {} if raw.respond_to?(:empty?) && raw.empty?
15
+
16
+ raw.each_with_object({}) do |(key, value), memo|
17
+ memo[key.to_sym] = value
18
+ end.tap do |hash|
19
+ hash[:access_token] = hash[:access_token].to_s if hash[:access_token]
20
+ end
21
+ end
22
+
23
+ def access_token(raw)
24
+ normalize(raw)[:access_token]
25
+ end
26
+
27
+ # Reduced shape for documented public SDK responses (e.g. fetch_tokens).
28
+ def public_tokens(raw)
29
+ normalize(raw).slice(*PUBLIC_KEYS).compact
30
+ end
31
+
32
+ def from_access_token(token)
33
+ public_tokens(
34
+ access_token: token.token,
35
+ refresh_token: token.refresh_token,
36
+ expires_at: token.expires_at,
37
+ id_token: token_param(token, "id_token"),
38
+ scope: token_param(token, "scope"),
39
+ token_type: token_param(token, "token_type"),
40
+ expires_in: token_param(token, "expires_in")
41
+ )
42
+ end
43
+
44
+ # String keys for session storage and documented public API responses.
45
+ def for_session(raw)
46
+ public_tokens(raw).transform_keys(&:to_s)
47
+ end
48
+
49
+ # Full oauth2 hash with string keys for refresh_token responses.
50
+ def for_refresh_response(raw)
51
+ normalize(raw).transform_keys(&:to_s)
52
+ end
53
+
54
+ def token_param(token, key)
55
+ token.params[key] || token.params[key.to_sym]
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,7 @@
1
1
  module KindeSdk
2
2
  class TokenManager
3
+ include Logging
4
+
3
5
  attr_reader :tokens, :bearer_token, :expires_at
4
6
 
5
7
  def initialize(tokens = nil)
@@ -7,19 +9,19 @@ module KindeSdk
7
9
  end
8
10
 
9
11
  def set_tokens(tokens)
10
- @tokens = tokens.transform_keys(&:to_sym).freeze
12
+ @tokens = TokenHash.normalize(tokens).freeze
11
13
  @bearer_token = @tokens[:access_token]
12
14
  @expires_at = @tokens[:expires_at]
13
15
  @tokens
14
16
  end
15
17
 
16
18
  def token_expired?
17
- return true unless @tokens.present?
19
+ return true if @tokens.nil? || @tokens.empty?
18
20
  begin
19
21
  KindeSdk.validate_jwt_token(@tokens)
20
22
  @expires_at.to_i > 0 && (@expires_at <= Time.now.to_i)
21
- rescue Exception => e
22
- Rails.logger.error("Error checking token expiration: #{e.message}")
23
+ rescue StandardError => e
24
+ log_error("Error checking token expiration: #{e.message}")
23
25
  true
24
26
  end
25
27
  end
@@ -31,6 +33,8 @@ module KindeSdk
31
33
  end
32
34
 
33
35
  class << self
36
+ extend KindeSdk::Logging
37
+
34
38
  def create_store(tokens = nil)
35
39
  TokenStore.new(tokens)
36
40
  end
@@ -71,7 +75,7 @@ module KindeSdk
71
75
  def safe_refresh(tokens)
72
76
  KindeSdk.refresh_token(tokens)
73
77
  rescue StandardError => e
74
- Rails.logger.error("Token refresh failed: #{e.message}")
78
+ log_error("Token refresh failed: #{e.message}")
75
79
  nil
76
80
  end
77
81
 
@@ -82,4 +86,4 @@ module KindeSdk
82
86
  end
83
87
  end
84
88
  end
85
- end
89
+ end
@@ -7,7 +7,7 @@ module KindeSdk
7
7
  end
8
8
 
9
9
  def set_tokens(tokens)
10
- @tokens = (tokens || {}).transform_keys(&:to_sym)
10
+ @tokens = TokenHash.normalize(tokens)
11
11
  @bearer_token = @tokens[:access_token]
12
12
  @expires_at = @tokens[:expires_at]
13
13
  @tokens
@@ -1,3 +1,3 @@
1
1
  module KindeSdk
2
- VERSION = "1.7.1"
2
+ VERSION = "1.8.0"
3
3
  end
data/lib/kinde_sdk.rb CHANGED
@@ -1,7 +1,16 @@
1
1
  require "logger"
2
- require "rails"
2
+
3
+ # Rails is optional - SDK works in non-Rails Ruby applications
4
+ begin
5
+ require "rails"
6
+ rescue LoadError
7
+ # Rails not available, that's fine
8
+ end
9
+
3
10
  require "kinde_sdk/version"
4
11
  require "kinde_sdk/configuration"
12
+ require "kinde_sdk/logging"
13
+ require "kinde_sdk/token_hash"
5
14
  require "kinde_sdk/client/feature_flags"
6
15
  require "kinde_sdk/client/permissions"
7
16
  require "kinde_sdk/client"
@@ -24,6 +33,8 @@ module KindeSdk
24
33
 
25
34
 
26
35
  class << self
36
+ include Logging
37
+
27
38
  attr_accessor :config
28
39
 
29
40
  if defined?(Rails)
@@ -55,6 +66,13 @@ module KindeSdk
55
66
  scope: @config.scope,
56
67
  supports_reauth: "true"
57
68
  }.merge(**kwargs)
69
+
70
+ if params[:invitation_code].is_a?(String) && !params[:invitation_code].strip.empty?
71
+ params[:invitation_code] = params[:invitation_code].strip
72
+ params[:is_invitation] = "true"
73
+ else
74
+ params.delete(:invitation_code)
75
+ end
58
76
  return { url: @config.oauth_client(
59
77
  client_id: client_id,
60
78
  client_secret: client_secret,
@@ -98,14 +116,7 @@ module KindeSdk
98
116
  authorize_url: "#{domain}/oauth2/auth",
99
117
  token_url: "#{domain}/oauth2/token").auth_code.get_token(code.to_s, params)
100
118
 
101
- {
102
- access_token: token.token, # The access token
103
- id_token: token.params['id_token'], # The ID token from params
104
- expires_at: token.expires_at, # Optional: expiration time
105
- refresh_token: token.refresh_token, # Optional: if present
106
- scope: token.params['scope'], # The scopes requested
107
- token_type: token.params['token_type'] # The token type
108
- }.compact
119
+ TokenHash.from_access_token(token)
109
120
  end
110
121
 
111
122
  # tokens_hash #=>
@@ -118,14 +129,46 @@ module KindeSdk
118
129
  #
119
130
  # @return [KindeSdk::Client]
120
131
  def client(tokens_hash, auto_refresh_tokens = @config.auto_refresh_tokens, force_api = @config.force_api)
121
- sdk_api_client = api_client(tokens_hash[:access_token] || tokens_hash["access_token"])
122
- KindeSdk::Client.new(sdk_api_client, tokens_hash, auto_refresh_tokens, force_api)
132
+ normalized_tokens = TokenHash.normalize(tokens_hash)
133
+ bearer_token = normalized_tokens[:access_token]
134
+ if bearer_token.nil? || bearer_token.empty?
135
+ raise AuthenticationError, "Missing access_token in token hash"
136
+ end
137
+
138
+ sdk_api_client = api_client(bearer_token)
139
+ KindeSdk::Client.new(sdk_api_client, normalized_tokens, auto_refresh_tokens, force_api)
123
140
  end
124
141
 
125
142
  def logout_url(logout_url: @config.logout_url, domain: @config.domain)
143
+ raise ArgumentError, "domain is required for logout_url" if domain.nil? || domain.to_s.strip.empty?
144
+
126
145
  query = logout_url ? URI.encode_www_form(redirect: logout_url) : nil
127
- host = URI::parse(domain).host
128
- URI::HTTP.build(host: host, path: '/logout', query: query).to_s
146
+ normalized_domain = domain.to_s.strip
147
+
148
+ # Only allow http/https schemes; prepend https:// if no scheme present
149
+ if normalized_domain.match?(%r{\Ahttps?://}i)
150
+ # Valid http/https scheme - use as-is
151
+ elsif normalized_domain.match?(%r{\A\w+://})
152
+ # Invalid scheme (ftp://, file://, etc.)
153
+ raise ArgumentError, "invalid scheme in domain: #{domain} (only http/https allowed)"
154
+ else
155
+ # No scheme - default to https
156
+ normalized_domain = "https://#{normalized_domain}"
157
+ end
158
+
159
+ begin
160
+ parsed = URI.parse(normalized_domain)
161
+ rescue URI::InvalidURIError
162
+ raise ArgumentError, "invalid domain format: #{domain}"
163
+ end
164
+ raise ArgumentError, "invalid domain format: #{domain}" if parsed.host.nil? || parsed.host.empty?
165
+
166
+ scheme = parsed.scheme || 'https'
167
+ # Only omit port if it matches the default for the scheme
168
+ default_port = scheme == 'https' ? 443 : 80
169
+ host_with_port = parsed.port && parsed.port != default_port ? "#{parsed.host}:#{parsed.port}" : parsed.host
170
+
171
+ "#{scheme}://#{host_with_port}/logout#{query ? "?#{query}" : ''}"
129
172
  end
130
173
 
131
174
  def client_credentials_access(
@@ -154,13 +197,13 @@ module KindeSdk
154
197
  begin
155
198
  validate_jwt_token(hash)
156
199
  OAuth2::AccessToken.from_hash(@config.oauth_client(
157
- client_id: client_id,
200
+ client_id: client_id,
158
201
  client_secret: client_secret,
159
202
  domain: domain,
160
203
  authorize_url: "#{domain}/oauth2/auth",
161
- token_url: "#{domain}/oauth2/token"), hash).expired?
204
+ token_url: "#{domain}/oauth2/token"), TokenHash.normalize(hash)).expired?
162
205
  rescue JWT::DecodeError, OAuth2::Error => e
163
- Rails.logger.error("Error checking token expiration: #{e.message}")
206
+ log_error("Error checking token expiration: #{e.message}")
164
207
  true
165
208
  end
166
209
  end
@@ -172,12 +215,14 @@ module KindeSdk
172
215
  audience: "#{@config.domain}/api",
173
216
  domain: @config.domain
174
217
  )
175
- OAuth2::AccessToken.from_hash(@config.oauth_client(
176
- client_id: client_id,
218
+ oauth_client = @config.oauth_client(
219
+ client_id: client_id,
177
220
  client_secret: client_secret,
178
221
  domain: domain,
179
222
  authorize_url: "#{domain}/oauth2/auth",
180
- token_url: "#{domain}/oauth2/token"), hash).refresh.to_hash
223
+ token_url: "#{domain}/oauth2/token")
224
+ refreshed = OAuth2::AccessToken.from_hash(oauth_client, TokenHash.normalize(hash)).refresh
225
+ TokenHash.for_refresh_response(refreshed.to_hash)
181
226
  end
182
227
 
183
228
  # init sdk api client by bearer token
@@ -205,7 +250,7 @@ module KindeSdk
205
250
  begin
206
251
  jwt_validation(token, "#{@config.domain}#{@config.jwks_url}", @config.expected_issuer, @config.expected_audience)
207
252
  rescue JWT::DecodeError
208
- Rails.logger.error("Invalid JWT token: #{key}")
253
+ log_error("Invalid JWT token: #{key}")
209
254
  raise JWT::DecodeError, "Invalid #{key.to_s.capitalize.gsub('_', ' ')}"
210
255
  end
211
256
  end
@@ -253,12 +298,13 @@ module KindeSdk
253
298
  payload, _header = JWT.decode(jwt_token, nil, true, algorithms: algorithms, jwks: jwks)
254
299
  { valid: true, payload: payload }
255
300
  rescue JWT::DecodeError => e
256
- Rails.logger.error("Token validation failed: #{e.message}")
301
+ log_error("Token validation failed: #{e.message}")
257
302
  raise JWT::DecodeError, "Token validation failed: #{e.message}"
258
303
  rescue StandardError => e
259
- Rails.logger.error("Unexpected error: #{e.message}")
304
+ log_error("Unexpected error: #{e.message}")
260
305
  raise StandardError, "Unexpected error: #{e.message}"
261
306
  end
262
307
 
263
308
  end
264
309
  end
310
+
data/spec/examples.txt CHANGED
@@ -1,29 +1,45 @@
1
1
  example_id | status | run_time |
2
2
  ------------------------------------- | ------ | --------------- |
3
- ./spec/kinde_sdk_spec.rb[1:1:1] | passed | 0.01547 seconds |
4
- ./spec/kinde_sdk_spec.rb[1:1:2] | passed | 0.01147 seconds |
5
- ./spec/kinde_sdk_spec.rb[1:2:1] | passed | 0.01382 seconds |
6
- ./spec/kinde_sdk_spec.rb[1:2:2:1] | passed | 0.02244 seconds |
7
- ./spec/kinde_sdk_spec.rb[1:3:1] | passed | 0.00956 seconds |
8
- ./spec/kinde_sdk_spec.rb[1:4:1] | passed | 0.06429 seconds |
9
- ./spec/kinde_sdk_spec.rb[1:4:2:1] | passed | 0.01294 seconds |
10
- ./spec/kinde_sdk_spec.rb[1:5:1] | passed | 0.05019 seconds |
11
- ./spec/kinde_sdk_spec.rb[1:5:2:1] | passed | 0.02212 seconds |
12
- ./spec/kinde_sdk_spec.rb[1:6:1:1] | passed | 0.05796 seconds |
13
- ./spec/kinde_sdk_spec.rb[1:6:1:2] | passed | 0.01345 seconds |
14
- ./spec/kinde_sdk_spec.rb[1:6:1:3] | passed | 0.0155 seconds |
15
- ./spec/kinde_sdk_spec.rb[1:6:2:1] | passed | 0.01915 seconds |
16
- ./spec/kinde_sdk_spec.rb[1:6:2:2:1:1] | passed | 0.02559 seconds |
17
- ./spec/kinde_sdk_spec.rb[1:6:2:2:2:1] | passed | 0.01714 seconds |
18
- ./spec/kinde_sdk_spec.rb[1:6:3:1] | passed | 0.02433 seconds |
19
- ./spec/kinde_sdk_spec.rb[1:6:3:2] | passed | 0.01565 seconds |
20
- ./spec/kinde_sdk_spec.rb[1:6:3:3] | passed | 0.01722 seconds |
21
- ./spec/kinde_sdk_spec.rb[1:6:3:4] | passed | 0.07104 seconds |
22
- ./spec/kinde_sdk_spec.rb[1:6:3:5] | passed | 0.02051 seconds |
23
- ./spec/kinde_sdk_spec.rb[1:6:3:6] | passed | 0.0496 seconds |
24
- ./spec/kinde_sdk_spec.rb[1:6:4:1:1] | passed | 0.02547 seconds |
25
- ./spec/kinde_sdk_spec.rb[1:7] | passed | 0.06144 seconds |
26
- ./spec/kinde_sdk_spec.rb[1:8:1] | passed | 0.03108 seconds |
27
- ./spec/kinde_sdk_spec.rb[1:8:2] | passed | 0.051 seconds |
28
- ./spec/kinde_sdk_spec.rb[1:8:3] | passed | 0.014 seconds |
29
- ./spec/kinde_sdk_spec.rb[1:8:4] | passed | 0.0682 seconds |
3
+ ./spec/kinde_sdk_spec.rb[1:1:1] | passed | 0.05436 seconds |
4
+ ./spec/kinde_sdk_spec.rb[1:1:2] | passed | 0.05822 seconds |
5
+ ./spec/kinde_sdk_spec.rb[1:1:3] | passed | 0.03791 seconds |
6
+ ./spec/kinde_sdk_spec.rb[1:1:4] | passed | 0.02654 seconds |
7
+ ./spec/kinde_sdk_spec.rb[1:1:5] | passed | 0.02896 seconds |
8
+ ./spec/kinde_sdk_spec.rb[1:1:6] | passed | 0.07656 seconds |
9
+ ./spec/kinde_sdk_spec.rb[1:1:7] | passed | 0.06324 seconds |
10
+ ./spec/kinde_sdk_spec.rb[1:2:1] | passed | 0.03315 seconds |
11
+ ./spec/kinde_sdk_spec.rb[1:2:2:1] | passed | 0.03904 seconds |
12
+ ./spec/kinde_sdk_spec.rb[1:3:1] | passed | 0.02823 seconds |
13
+ ./spec/kinde_sdk_spec.rb[1:4:1] | passed | 0.06163 seconds |
14
+ ./spec/kinde_sdk_spec.rb[1:4:2:1] | passed | 0.17631 seconds |
15
+ ./spec/kinde_sdk_spec.rb[1:5:1] | passed | 0.07435 seconds |
16
+ ./spec/kinde_sdk_spec.rb[1:5:2:1] | passed | 0.05682 seconds |
17
+ ./spec/kinde_sdk_spec.rb[1:6:1] | passed | 0.04508 seconds |
18
+ ./spec/kinde_sdk_spec.rb[1:7:1:1] | passed | 0.04243 seconds |
19
+ ./spec/kinde_sdk_spec.rb[1:7:1:2] | passed | 0.14146 seconds |
20
+ ./spec/kinde_sdk_spec.rb[1:7:2:1] | passed | 0.05834 seconds |
21
+ ./spec/kinde_sdk_spec.rb[1:7:2:2] | passed | 0.04263 seconds |
22
+ ./spec/kinde_sdk_spec.rb[1:7:2:3] | passed | 0.04012 seconds |
23
+ ./spec/kinde_sdk_spec.rb[1:7:3:1] | passed | 0.08297 seconds |
24
+ ./spec/kinde_sdk_spec.rb[1:7:3:2:1:1] | passed | 0.11155 seconds |
25
+ ./spec/kinde_sdk_spec.rb[1:7:3:2:2:1] | passed | 0.04904 seconds |
26
+ ./spec/kinde_sdk_spec.rb[1:7:4:1] | passed | 0.06419 seconds |
27
+ ./spec/kinde_sdk_spec.rb[1:7:4:2] | passed | 0.09575 seconds |
28
+ ./spec/kinde_sdk_spec.rb[1:7:4:3] | passed | 0.04359 seconds |
29
+ ./spec/kinde_sdk_spec.rb[1:7:4:4] | passed | 0.04235 seconds |
30
+ ./spec/kinde_sdk_spec.rb[1:7:4:5] | passed | 0.11869 seconds |
31
+ ./spec/kinde_sdk_spec.rb[1:7:4:6] | passed | 0.07644 seconds |
32
+ ./spec/kinde_sdk_spec.rb[1:7:5:1:1] | passed | 0.06087 seconds |
33
+ ./spec/kinde_sdk_spec.rb[1:8] | passed | 0.07786 seconds |
34
+ ./spec/kinde_sdk_spec.rb[1:9:1] | passed | 0.05067 seconds |
35
+ ./spec/kinde_sdk_spec.rb[1:9:2] | passed | 0.08124 seconds |
36
+ ./spec/kinde_sdk_spec.rb[1:9:3] | passed | 0.0672 seconds |
37
+ ./spec/kinde_sdk_spec.rb[1:9:4] | passed | 0.07944 seconds |
38
+ ./spec/kinde_sdk_spec.rb[2:1:1] | passed | 0.00005 seconds |
39
+ ./spec/kinde_sdk_spec.rb[2:1:2] | passed | 0.00005 seconds |
40
+ ./spec/kinde_sdk_spec.rb[2:1:3] | passed | 0.00005 seconds |
41
+ ./spec/kinde_sdk_spec.rb[2:1:4] | passed | 0.00008 seconds |
42
+ ./spec/kinde_sdk_spec.rb[2:2:1] | passed | 0.00005 seconds |
43
+ ./spec/kinde_sdk_spec.rb[2:3:1] | passed | 0.00465 seconds |
44
+ ./spec/kinde_sdk_spec.rb[2:4:1] | passed | 0.00006 seconds |
45
+ ./spec/kinde_sdk_spec.rb[2:5:1] | passed | 0.00059 seconds |
@@ -88,6 +88,36 @@ RSpec.describe KindeSdk do
88
88
  auth_obj = described_class.auth_url(redirect_uri: "localhost:5000/another_callback")
89
89
  expect(auth_obj[:url]).to match(/localhost%3A5000%2Fanother_callback/)
90
90
  end
91
+
92
+ it "includes invitation_code and is_invitation when invitation_code is provided" do
93
+ auth_obj = described_class.auth_url(invitation_code: "test_invitation_123")
94
+ expect(auth_obj[:url]).to include("invitation_code=test_invitation_123")
95
+ expect(auth_obj[:url]).to include("is_invitation=true")
96
+ end
97
+
98
+ it "does not include is_invitation when invitation_code is not provided" do
99
+ auth_obj = described_class.auth_url
100
+ expect(auth_obj[:url]).not_to include("is_invitation")
101
+ expect(auth_obj[:url]).not_to include("invitation_code")
102
+ end
103
+
104
+ it "does not include is_invitation for empty invitation_code" do
105
+ auth_obj = described_class.auth_url(invitation_code: "")
106
+ expect(auth_obj[:url]).not_to include("is_invitation")
107
+ expect(auth_obj[:url]).not_to include("invitation_code")
108
+ end
109
+
110
+ it "does not include is_invitation for whitespace-only invitation_code" do
111
+ auth_obj = described_class.auth_url(invitation_code: " ")
112
+ expect(auth_obj[:url]).not_to include("is_invitation")
113
+ expect(auth_obj[:url]).not_to include("invitation_code")
114
+ end
115
+
116
+ it "includes invitation_code and is_invitation for valid invitation_code with whitespace" do
117
+ auth_obj = described_class.auth_url(invitation_code: " abc123 ")
118
+ expect(auth_obj[:url]).to include("invitation_code=abc123")
119
+ expect(auth_obj[:url]).to include("is_invitation=true")
120
+ end
91
121
  end
92
122
 
93
123
  describe "#logout_url" do
@@ -190,6 +220,51 @@ RSpec.describe KindeSdk do
190
220
  end
191
221
  end
192
222
 
223
+ describe "#refresh_token" do
224
+ it "returns string-keyed tokens with the full oauth2 hash contract" do
225
+ refreshed = double(
226
+ "OAuth2::AccessToken",
227
+ token: token,
228
+ refresh_token: refresh_token,
229
+ expires_at: Time.now.to_i + 7200,
230
+ params: {}
231
+ )
232
+ allow(refreshed).to receive(:to_hash).and_return(
233
+ access_token: token,
234
+ refresh_token: refresh_token,
235
+ expires_at: Time.now.to_i + 7200,
236
+ mode: :header,
237
+ header_format: "Bearer %s"
238
+ )
239
+
240
+ access_token_object = double(refresh: refreshed)
241
+ allow(OAuth2::AccessToken).to receive(:from_hash).and_return(access_token_object)
242
+
243
+ stored_tokens = {
244
+ "access_token" => token,
245
+ "refresh_token" => refresh_token,
246
+ "expires_at" => Time.now.to_i + 3600,
247
+ "mode" => :query,
248
+ "param_name" => "access_token"
249
+ }
250
+
251
+ expect(OAuth2::AccessToken).to receive(:from_hash) do |_client, normalized|
252
+ expect(normalized[:access_token]).to eq(token)
253
+ expect(normalized[:mode]).to eq(:query)
254
+ expect(normalized[:param_name]).to eq("access_token")
255
+ access_token_object
256
+ end
257
+
258
+ result = described_class.refresh_token(stored_tokens)
259
+
260
+ expect(result["access_token"]).to eq(token)
261
+ expect(result["refresh_token"]).to eq(refresh_token)
262
+ expect(result["mode"]).to eq(:header)
263
+ expect(result["header_format"]).to eq("Bearer %s")
264
+ expect(result[:access_token]).to be_nil
265
+ end
266
+ end
267
+
193
268
  describe "client" do
194
269
  let(:hash_to_encode) do
195
270
  { "aud" => [],
@@ -213,6 +288,24 @@ RSpec.describe KindeSdk do
213
288
  let(:tokens_hash) { { access_token: token, expires_at: expires_at, refresh_token: refresh_token } }
214
289
  let(:client) { described_class.client(tokens_hash, auto_refresh_tokens) }
215
290
 
291
+ context "with string-key session tokens" do
292
+ it "accepts string keys" do
293
+ session_tokens = {
294
+ "access_token" => token,
295
+ "refresh_token" => refresh_token,
296
+ "expires_at" => expires_at
297
+ }
298
+
299
+ expect(described_class.client(session_tokens, false).bearer_token).to eq(token)
300
+ end
301
+
302
+ it "raises when access_token is missing" do
303
+ expect {
304
+ described_class.client({ "refresh_token" => refresh_token }, false)
305
+ }.to raise_error(KindeSdk::AuthenticationError, /Missing access_token/)
306
+ end
307
+ end
308
+
216
309
  context "with session integration" do
217
310
  before do
218
311
  KindeSdk::Current.set_session(mock_session)
@@ -447,4 +540,136 @@ RSpec.describe KindeSdk do
447
540
  end
448
541
  end
449
542
 
543
+ RSpec.describe KindeSdk::TokenHash do
544
+ describe ".normalize" do
545
+ it "accepts string keys" do
546
+ hash = described_class.normalize(
547
+ "access_token" => "abc",
548
+ "refresh_token" => "def",
549
+ "expires_at" => 123
550
+ )
551
+
552
+ expect(hash).to eq(
553
+ access_token: "abc",
554
+ refresh_token: "def",
555
+ expires_at: 123
556
+ )
557
+ end
558
+
559
+ it "accepts symbol keys" do
560
+ hash = described_class.normalize(
561
+ access_token: "abc",
562
+ refresh_token: "def",
563
+ expires_at: 123
564
+ )
565
+
566
+ expect(hash[:access_token]).to eq("abc")
567
+ end
568
+
569
+ it "returns an empty hash for nil" do
570
+ expect(described_class.normalize(nil)).to eq({})
571
+ end
572
+
573
+ it "preserves oauth2 reconstruction fields" do
574
+ hash = described_class.normalize(
575
+ "access_token" => "abc",
576
+ "mode" => :header,
577
+ header_format: "Bearer %s",
578
+ "token_name" => :access_token,
579
+ "expires_latency" => 5,
580
+ "expires" => 3600
581
+ )
582
+
583
+ expect(hash).to eq(
584
+ access_token: "abc",
585
+ mode: :header,
586
+ header_format: "Bearer %s",
587
+ token_name: :access_token,
588
+ expires_latency: 5,
589
+ expires: 3600
590
+ )
591
+ end
592
+ end
593
+
594
+ describe ".public_tokens" do
595
+ it "returns only the documented public SDK token fields" do
596
+ hash = described_class.public_tokens(
597
+ access_token: "abc",
598
+ refresh_token: "def",
599
+ expires_at: 123,
600
+ mode: :header,
601
+ header_format: "Bearer %s"
602
+ )
603
+
604
+ expect(hash).to eq(
605
+ access_token: "abc",
606
+ refresh_token: "def",
607
+ expires_at: 123
608
+ )
609
+ end
610
+ end
611
+
612
+ describe ".from_access_token" do
613
+ let(:oauth_token) do
614
+ double(
615
+ "OAuth2::AccessToken",
616
+ token: "access-token-value",
617
+ refresh_token: "refresh-token-value",
618
+ expires_at: 1_700_000_000,
619
+ params: {
620
+ "id_token" => "id-token",
621
+ "scope" => "openid profile",
622
+ "token_type" => "bearer"
623
+ }
624
+ )
625
+ end
626
+
627
+ it "builds a normalized hash without calling oauth2 #to_hash" do
628
+ expect(oauth_token).not_to receive(:to_hash)
629
+
630
+ hash = described_class.from_access_token(oauth_token)
631
+
632
+ expect(hash).to eq(
633
+ access_token: "access-token-value",
634
+ refresh_token: "refresh-token-value",
635
+ expires_at: 1_700_000_000,
636
+ id_token: "id-token",
637
+ scope: "openid profile",
638
+ token_type: "bearer"
639
+ )
640
+ end
641
+ end
642
+
643
+ describe ".for_refresh_response" do
644
+ it "returns string keys while preserving the full oauth2 hash contract" do
645
+ response = described_class.for_refresh_response(
646
+ access_token: "abc",
647
+ refresh_token: "def",
648
+ expires_at: 123,
649
+ mode: :header,
650
+ param_name: "access_token"
651
+ )
652
+
653
+ expect(response).to eq(
654
+ "access_token" => "abc",
655
+ "refresh_token" => "def",
656
+ "expires_at" => 123,
657
+ "mode" => :header,
658
+ "param_name" => "access_token"
659
+ )
660
+ end
661
+ end
662
+
663
+ describe ".for_session" do
664
+ it "returns string keys for documented session/API compatibility" do
665
+ session_hash = described_class.for_session(access_token: "abc", expires_at: 123)
666
+
667
+ expect(session_hash).to eq(
668
+ "access_token" => "abc",
669
+ "expires_at" => 123
670
+ )
671
+ expect(session_hash["access_token"]).to eq("abc")
672
+ end
673
+ end
674
+ end
450
675
 
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kinde_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.1
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kinde Australia Pty Ltd
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 2026-01-08 00:00:00.000000000 Z
11
+ date: 2026-07-16 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: typhoeus
@@ -105,34 +106,34 @@ dependencies:
105
106
  requirements:
106
107
  - - "~>"
107
108
  - !ruby/object:Gem::Version
108
- version: '2.2'
109
+ version: '3.0'
109
110
  type: :runtime
110
111
  prerelease: false
111
112
  version_requirements: !ruby/object:Gem::Requirement
112
113
  requirements:
113
114
  - - "~>"
114
115
  - !ruby/object:Gem::Version
115
- version: '2.2'
116
+ version: '3.0'
116
117
  - !ruby/object:Gem::Dependency
117
118
  name: rspec
118
119
  requirement: !ruby/object:Gem::Requirement
119
120
  requirements:
120
- - - "~>"
121
- - !ruby/object:Gem::Version
122
- version: '3.6'
123
121
  - - ">="
124
122
  - !ruby/object:Gem::Version
125
123
  version: 3.6.0
124
+ - - "~>"
125
+ - !ruby/object:Gem::Version
126
+ version: '3.6'
126
127
  type: :development
127
128
  prerelease: false
128
129
  version_requirements: !ruby/object:Gem::Requirement
129
130
  requirements:
130
- - - "~>"
131
- - !ruby/object:Gem::Version
132
- version: '3.6'
133
131
  - - ">="
134
132
  - !ruby/object:Gem::Version
135
133
  version: 3.6.0
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '3.6'
136
137
  description: Integrate the Kinde API into any ruby-based applications, Rails or non-Rails
137
138
  email:
138
139
  - support@kinde.com
@@ -935,7 +936,9 @@ files:
935
936
  - lib/kinde_sdk/engine.rb
936
937
  - lib/kinde_sdk/errors.rb
937
938
  - lib/kinde_sdk/internal/frontend_client.rb
939
+ - lib/kinde_sdk/logging.rb
938
940
  - lib/kinde_sdk/middleware.rb
941
+ - lib/kinde_sdk/token_hash.rb
939
942
  - lib/kinde_sdk/token_manager.rb
940
943
  - lib/kinde_sdk/token_store.rb
941
944
  - lib/kinde_sdk/version.rb
@@ -946,6 +949,7 @@ homepage: https://kinde.com/
946
949
  licenses:
947
950
  - MIT
948
951
  metadata: {}
952
+ post_install_message:
949
953
  rdoc_options: []
950
954
  require_paths:
951
955
  - lib
@@ -961,7 +965,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
961
965
  - !ruby/object:Gem::Version
962
966
  version: '0'
963
967
  requirements: []
964
- rubygems_version: 3.6.2
968
+ rubygems_version: 3.0.3.1
969
+ signing_key:
965
970
  specification_version: 4
966
971
  summary: Kinde Management API Ruby Gem
967
972
  test_files: