jwt_auth_cognito 1.0.0.pre.beta.14 → 1.0.0.pre.beta.15

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: 77eed5e47f35c5f0a7e693bab6c09e70cf2ea950670273d203ff3611d94b8a70
4
- data.tar.gz: 21a499f3e1ad917bf9587cd8695e2dc44cceb7fb77f3ada93185ae2ed855c0d7
3
+ metadata.gz: 5478b2beaffce3e103aa915b73735e2b78ae39d4f802fb663fd0aad79a721207
4
+ data.tar.gz: 2023c50ebe2234f58243820118edce9e8d413dc348b0798642e4378adc356898
5
5
  SHA512:
6
- metadata.gz: 2fb3defb3e11e9cc318ac1d387cf58c99fa6b1b3152bc34841db21645fb6a6fa67ab99af956fb4fd539aeaa44132df9994bc55256f9bb4412e30ebd112801c6a
7
- data.tar.gz: cba58be12f1fd5dfe1d199a83e27e60fa851bedfd1b39a2d7b8a0f6c2669c99c9f4459fbe6390977aef177c1c797abecd83ffb447e966615d6556ac8192acf3c
6
+ metadata.gz: e8a0c2b0bb5897f15bc83b065ccfa060f18d509788cd1406325792caa74ac1d23bc9910c4c81d3f1c4923e21efd6aa0e35655bffb8f44dc1f74767a3ed3e51d9
7
+ data.tar.gz: 5bbb8e162f0150c06730a8c63723e6458b50f32404bc12661622d8b670fa3901f1dac9c93a909d59f77d7f63ff0e1d2b9592c2df69247feb9b9b3aac5786db1b
data/.rubocop.yml CHANGED
@@ -6,16 +6,12 @@ AllCops:
6
6
  SuggestExtensions: false
7
7
  TargetRubyVersion: 2.7
8
8
 
9
- # Increase limits for code complexity - legacy codebase
10
- Metrics/ClassLength:
11
- Max: 300
12
-
9
+ # Limits for code metrics that remain enabled (legacy codebase).
10
+ # NOTE: ClassLength, MethodLength, AbcSize, CyclomaticComplexity and PerceivedComplexity
11
+ # are disabled further below — that block is their single source of truth.
13
12
  Metrics/ModuleLength:
14
13
  Max: 300
15
14
 
16
- Metrics/MethodLength:
17
- Max: 30
18
-
19
15
  Metrics/BlockLength:
20
16
  Max: 50
21
17
  Exclude:
@@ -23,15 +19,6 @@ Metrics/BlockLength:
23
19
  - 'lib/tasks/**/*'
24
20
  - 'Rakefile'
25
21
 
26
- Metrics/AbcSize:
27
- Max: 30
28
-
29
- Metrics/CyclomaticComplexity:
30
- Max: 15
31
-
32
- Metrics/PerceivedComplexity:
33
- Max: 15
34
-
35
22
  Metrics/ParameterLists:
36
23
  Max: 8
37
24
 
data/CHANGELOG.md CHANGED
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.0-beta.16] - 2026-06-25
11
+
12
+ ### Fixed
13
+
14
+ - **Blacklist key now matches auth-service (revocations were being missed).** The gem
15
+ looked up revoked tokens under `jwt_blacklist:{jti | sha256(token)[0,16]}`, but
16
+ auth-service writes them under `revoked:{base64(last 32 chars of token)}` (the same
17
+ scheme the Node validator reads). The prefix and the id derivation both differed, so
18
+ a token revoked via `/auth/logout` was **never detected** by Ruby consumers and kept
19
+ validating until it expired on its own. `BLACKLIST_PREFIX` is now `revoked:` and
20
+ `#generate_token_id` derives `Base64.strict_encode64(token[-32..])`, matching
21
+ auth-service and the npm package exactly.
22
+
23
+ ### Changed
24
+
25
+ - **Thread-safe shared Redis connection pool.** `RedisService` previously held a single,
26
+ non-thread-safe connection per instance (and the validator builds several instances),
27
+ which races under Puma/Sidekiq and makes connection count grow with threads/instances.
28
+ It now uses one process-wide `ConnectionPool` (new `connection_pool` dependency) shared
29
+ across all instances, sized via `redis_pool_size` (defaults to `RAILS_MAX_THREADS`,
30
+ env `REDIS_POOL_SIZE`) with `redis_pool_timeout` (env `REDIS_POOL_TIMEOUT`, default 5s).
31
+ This bounds connections to the pool size per process regardless of how many validators
32
+ or threads exist. Forking servers should call `JwtAuthCognito::RedisService.reset_pool!`
33
+ in their `after_fork` hook.
34
+ - **`#is_token_revoked?` fails open on any Redis/pool error.** Previously only
35
+ `Redis::BaseError` was rescued (a connection failure or pool-exhaustion error would
36
+ propagate and could block validation). It now rescues `StandardError` and returns
37
+ `false`, so a saturated or unreachable Redis never locks every user out — matching the
38
+ Node validator and auth-service's `RedisService.isTokenRevoked`.
39
+
10
40
  ## [1.0.0-beta.14] - 2026-06-18
11
41
 
12
42
  ### Changed
@@ -44,6 +44,7 @@ Gem::Specification.new do |spec|
44
44
  # Dependencies - compatible with llegando-neo (Ruby 2.7.5, Rails 5.2.6)
45
45
  spec.add_dependency 'aws-sdk-cognitoidentityprovider', '~> 1.0' # For GetUser identity enrichment
46
46
  spec.add_dependency 'aws-sdk-ssm', '~> 1.0' # For AWS Parameter Store support
47
+ spec.add_dependency 'connection_pool', '~> 2.2' # Shared, thread-safe Redis connection pool
47
48
  spec.add_dependency 'json', '~> 2.0'
48
49
  spec.add_dependency 'jwt', '~> 2.0'
49
50
  spec.add_dependency 'redis', '>= 4.2.5', '< 6.0' # Compatible with llegando-neo redis version
@@ -6,6 +6,7 @@ module JwtAuthCognito
6
6
  :redis_host, :redis_port, :redis_password, :redis_db,
7
7
  :redis_ssl, :redis_timeout, :redis_connect_timeout, :redis_read_timeout,
8
8
  :redis_ca_cert_path, :redis_ca_cert_name, :redis_verify_mode,
9
+ :redis_pool_size, :redis_pool_timeout,
9
10
  :jwks_cache_ttl, :validation_mode, :environment,
10
11
  :enable_api_key_validation, :enable_user_data_retrieval,
11
12
  :enable_user_identity_enrichment, :identity_cache_timeout, :identity_attributes
@@ -26,6 +27,13 @@ module JwtAuthCognito
26
27
  @redis_connect_timeout = (ENV['REDIS_CONNECT_TIMEOUT'] || 10).to_i
27
28
  @redis_read_timeout = (ENV['REDIS_READ_TIMEOUT'] || 10).to_i
28
29
 
30
+ # Shared connection pool sizing. Defaults to the web server's thread count
31
+ # (RAILS_MAX_THREADS) so the pool matches concurrency without over-allocating
32
+ # connections. redis_pool_timeout is the max seconds to wait for a free
33
+ # connection before raising ConnectionPool::TimeoutError.
34
+ @redis_pool_size = (ENV['REDIS_POOL_SIZE'] || ENV['RAILS_MAX_THREADS'] || 5).to_i
35
+ @redis_pool_timeout = (ENV['REDIS_POOL_TIMEOUT'] || 5).to_i
36
+
29
37
  # TLS specific configuration (compatible with auth-service)
30
38
  @redis_ca_cert_path = ENV.fetch('REDIS_CA_CERT_PATH', nil)
31
39
  @redis_ca_cert_name = ENV.fetch('REDIS_CA_CERT_NAME', nil)
@@ -305,6 +305,15 @@ module JwtAuthCognito
305
305
  PermissionChecker.permission_in_list?(permission, result[:permissions])
306
306
  end
307
307
 
308
+ # Resolve the user's effective ABAC access scope for (app, org).
309
+ # Hash keyed by "<resourceType>:<action>" => { 'match' =>, 'include' =>, 'exclude' => }.
310
+ # The consuming app filters its data with this. Returns {} when there is no scope.
311
+ def get_access_scope(user_id, app_id, org_id)
312
+ return {} unless @user_data_service
313
+
314
+ @user_data_service.resolve_access_scope(user_id, app_id, org_id)
315
+ end
316
+
308
317
  def is_token_expired?(token)
309
318
  payload = decode_token(token)
310
319
  return true if payload.is_a?(Hash) && payload[:error]
@@ -1,27 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'redis'
4
+ require 'connection_pool'
4
5
  require 'digest'
5
6
  require 'openssl'
7
+ require 'base64'
6
8
 
7
9
  module JwtAuthCognito
8
10
  class RedisService
9
- BLACKLIST_PREFIX = 'jwt_blacklist:'
11
+ # auth-service revokes tokens under `revoked:{base64(last 32 chars of token)}`
12
+ # (see tokenBlacklist.ts#getTokenHash). This prefix and the id derivation in
13
+ # #generate_token_id MUST match it exactly, otherwise revocations performed by
14
+ # auth-service (/auth/logout) are looked up under a key that never exists and are
15
+ # silently missed. The Node validator uses this same scheme.
16
+ BLACKLIST_PREFIX = 'revoked:'
10
17
  USER_TOKENS_PREFIX = 'user_tokens:'
11
18
 
19
+ # One shared, thread-safe connection pool per process — independent of how many
20
+ # RedisService instances exist (the blacklist, user-data and api-key services each
21
+ # build their own instance, yet they all share this pool). This bounds Redis
22
+ # connections to `redis_pool_size` per process instead of growing with the number
23
+ # of instances/threads, and makes the service safe under Puma/Sidekiq.
24
+ class << self
25
+ # Builds (once) and returns the shared pool. The factory is a callable that
26
+ # produces a connected Redis client; it runs lazily as the pool grows.
27
+ def pool(config, factory)
28
+ @pool ||= ConnectionPool.new(
29
+ size: config.redis_pool_size,
30
+ timeout: config.redis_pool_timeout
31
+ ) { factory.call }
32
+ end
33
+
34
+ # Drops the shared pool. Call after forking (Puma/Unicorn `after_fork`, Sidekiq)
35
+ # so each child builds its own connections instead of reusing parent sockets.
36
+ # Also used to isolate tests.
37
+ def reset_pool!
38
+ @pool = nil
39
+ end
40
+ end
41
+
12
42
  def initialize(config = JwtAuthCognito.configuration)
13
43
  @config = config
14
- @redis = nil
15
44
  end
16
45
 
17
46
  def save_revoked_token(token_id, ttl = nil)
18
- connect_redis
19
47
  key = "#{BLACKLIST_PREFIX}#{token_id}"
20
48
 
21
- if ttl
22
- @redis.setex(key, ttl, 'revoked')
23
- else
24
- @redis.set(key, 'revoked')
49
+ with_redis do |redis|
50
+ if ttl
51
+ redis.setex(key, ttl, 'revoked')
52
+ else
53
+ redis.set(key, 'revoked')
54
+ end
25
55
  end
26
56
 
27
57
  true
@@ -30,52 +60,53 @@ module JwtAuthCognito
30
60
  end
31
61
 
32
62
  def is_token_revoked?(token_id)
33
- connect_redis
34
63
  key = "#{BLACKLIST_PREFIX}#{token_id}"
35
- result = @redis.exists?(key)
64
+ result = with_redis { |redis| redis.exists?(key) }
36
65
  result.is_a?(Integer) ? result.positive? : result
37
- rescue Redis::BaseError
38
- # Graceful degradation - if Redis is down, don't block validation
66
+ rescue StandardError
67
+ # Fail-open graceful degradation: if Redis is unreachable or the pool is
68
+ # exhausted, do not block validation. Mirrors the Node validator and
69
+ # auth-service (RedisService.isTokenRevoked), which also treat errors as
70
+ # "not revoked" so an infrastructure issue never locks every user out.
39
71
  false
40
72
  end
41
73
 
42
74
  def clear_revoked_tokens
43
- connect_redis
44
- keys = @redis.keys("#{BLACKLIST_PREFIX}*")
45
- @redis.del(*keys) if keys.any?
46
- keys.length
75
+ with_redis do |redis|
76
+ keys = redis.keys("#{BLACKLIST_PREFIX}*")
77
+ redis.del(*keys) if keys.any?
78
+ keys.length
79
+ end
47
80
  rescue Redis::BaseError => e
48
81
  raise BlacklistError, "Failed to clear revoked tokens: #{e.message}"
49
82
  end
50
83
 
51
84
  def invalidate_user_tokens(user_id)
52
- connect_redis
53
-
54
- # Get all tokens for the user
55
85
  user_key = "#{USER_TOKENS_PREFIX}#{user_id}"
56
- token_ids = @redis.smembers(user_key)
57
86
 
58
- # Add all tokens to blacklist
59
- token_ids.each do |token_id|
60
- save_revoked_token(token_id)
61
- end
87
+ with_redis do |redis|
88
+ token_ids = redis.smembers(user_key)
62
89
 
63
- # Clear the user's token set
64
- @redis.del(user_key)
90
+ # Persist a revocation marker for each tracked token, then clear the set.
91
+ # Inlined (instead of calling #save_revoked_token) to reuse this single
92
+ # pooled connection and avoid a nested pool checkout.
93
+ token_ids.each { |token_id| redis.set("#{BLACKLIST_PREFIX}#{token_id}", 'revoked') }
94
+ redis.del(user_key)
65
95
 
66
- token_ids.length
96
+ token_ids.length
97
+ end
67
98
  rescue Redis::BaseError => e
68
99
  raise BlacklistError, "Failed to invalidate user tokens: #{e.message}"
69
100
  end
70
101
 
71
102
  def track_user_token(user_id, token_id, ttl = nil)
72
- connect_redis
73
-
74
103
  user_key = "#{USER_TOKENS_PREFIX}#{user_id}"
75
- @redis.sadd(user_key, token_id)
76
104
 
77
- # Set expiration on the user's token set
78
- @redis.expire(user_key, ttl) if ttl
105
+ with_redis do |redis|
106
+ redis.sadd(user_key, token_id)
107
+ # Set expiration on the user's token set
108
+ redis.expire(user_key, ttl) if ttl
109
+ end
79
110
 
80
111
  true
81
112
  rescue Redis::BaseError
@@ -84,34 +115,31 @@ module JwtAuthCognito
84
115
  end
85
116
 
86
117
  def generate_token_id(token)
87
- # Try to extract jti from token first
88
- begin
89
- payload = JWT.decode(token, nil, false).first
90
- return payload['jti'] if payload['jti']
91
- rescue JWT::DecodeError
92
- # Fall back to hash if token can't be decoded
93
- end
94
-
95
- # Generate hash-based ID
96
- Digest::SHA256.hexdigest(token)[0, 16]
118
+ # Must match auth-service's key derivation (tokenBlacklist.ts#getTokenHash):
119
+ # base64 of the last 32 characters of the raw token. Using the JTI or a SHA256
120
+ # here would look up a key auth-service never writes, so revocations would be
121
+ # missed. The Node validator (token-blacklist-service.ts#getTokenHash) is identical.
122
+ suffix = token.to_s
123
+ suffix = suffix[-32..] || suffix
124
+ Base64.strict_encode64(suffix)
97
125
  end
98
126
 
99
127
  def get(key)
100
- connect_redis
101
- @redis.get(key)
128
+ with_redis { |redis| redis.get(key) }
102
129
  rescue Redis::BaseError => e
103
130
  raise BlacklistError, "Failed to get key '#{key}': #{e.message}"
104
131
  end
105
132
 
106
133
  def set(key, value, ttl = nil)
107
- connect_redis
108
- # Only attach an expiry for a positive TTL. A nil or non-positive ttl (note:
109
- # 0 is truthy in Ruby) means a persistent key — `SETEX key 0` is rejected by
110
- # Redis with "ERR invalid expire time", so fall back to a plain SET.
111
- if ttl&.positive?
112
- @redis.setex(key, ttl, value)
113
- else
114
- @redis.set(key, value)
134
+ with_redis do |redis|
135
+ # Only attach an expiry for a positive TTL. A nil or non-positive ttl (note:
136
+ # 0 is truthy in Ruby) means a persistent key — `SETEX key 0` is rejected by
137
+ # Redis with "ERR invalid expire time", so fall back to a plain SET.
138
+ if ttl&.positive?
139
+ redis.setex(key, ttl, value)
140
+ else
141
+ redis.set(key, value)
142
+ end
115
143
  end
116
144
  true
117
145
  rescue Redis::BaseError => e
@@ -120,19 +148,24 @@ module JwtAuthCognito
120
148
 
121
149
  private
122
150
 
123
- def connect_redis
124
- return @redis if @redis
151
+ # Checks out a connection from the shared pool for the duration of the block.
152
+ # The pool is built lazily on first use and reused across all instances.
153
+ def with_redis(&block)
154
+ self.class.pool(@config, method(:build_connection)).with(&block)
155
+ end
125
156
 
157
+ # Connection factory used by the pool. Verifies connectivity with PING and retries
158
+ # with exponential backoff, mirroring the previous single-connection behavior.
159
+ def build_connection
126
160
  redis_options = build_redis_options
127
161
 
128
- # Retry logic with exponential backoff (similar to Node.js implementation)
129
162
  max_retries = 3
130
163
  retry_count = 0
131
164
 
132
165
  begin
133
- @redis = Redis.new(redis_options)
134
- @redis.ping # Test connection
135
- @redis
166
+ redis = Redis.new(redis_options)
167
+ redis.ping # Test connection
168
+ redis
136
169
  rescue Redis::BaseError => e
137
170
  retry_count += 1
138
171
  raise BlacklistError, "Failed to connect to Redis after #{max_retries} retries: #{e.message}" unless retry_count <= max_retries
@@ -142,6 +175,13 @@ module JwtAuthCognito
142
175
  end
143
176
  end
144
177
 
178
+ # Kept for backward compatibility: the `jwt_auth_cognito:test_redis` rake task
179
+ # calls this via #send. Validates connectivity by checking out a pooled connection
180
+ # and issuing a PING.
181
+ def connect_redis
182
+ with_redis(&:ping)
183
+ end
184
+
145
185
  def build_redis_options
146
186
  options = {
147
187
  host: @config.redis_host,
@@ -349,32 +349,60 @@ module JwtAuthCognito
349
349
  @stats[:connection_status] = 'disconnected'
350
350
  end
351
351
 
352
- private
352
+ # Resolves the effective ABAC access scope for (user, app, org).
353
+ # = union of role default accessScope (R2) + user accessScope (override/extend)
354
+ # + legacy resourceRestrictions (= include without match).
355
+ # Keyed by "<resourceType>:<action>" (R3). Expired roles (roleExpiry) are skipped (R1).
356
+ # Returns {} when there is no scope (open access to everything).
357
+ def resolve_access_scope(user_id, app_id, organization_id)
358
+ raw = @redis_service.get("user:permissions:#{user_id}")
359
+ return {} unless raw
353
360
 
354
- def get_from_cache(key)
355
- return nil unless @cache.key?(key)
361
+ data = JSON.parse(raw)
362
+ org_perms = data.dig('permissions', app_id, organization_id)
363
+ return {} unless org_perms.is_a?(Hash) && org_perms['status'] == 'active'
364
+
365
+ now = (Time.now.to_f * 1000).to_i
366
+ expiry = org_perms['roleExpiry'].is_a?(Hash) ? org_perms['roleExpiry'] : {}
367
+ roles = org_perms['roles'].is_a?(Array) ? org_perms['roles'] : []
368
+ active_roles = roles.reject { |role_id| expiry[role_id] && expiry[role_id] <= now }
369
+
370
+ effective = {}
371
+
372
+ # 1) Role default scope (R2)
373
+ unless active_roles.empty?
374
+ app_roles = get_app_roles(app_id, organization_id)
375
+ app_roles = {} unless app_roles.is_a?(Hash)
376
+ active_roles.each do |role_id|
377
+ role = app_roles[role_id]
378
+ next unless role.is_a?(Hash)
379
+ next if role['isActive'] == false
380
+
381
+ merge_scope_into(effective, role['accessScope'])
382
+ end
383
+ end
356
384
 
357
- timestamp = @cache_timestamps[key]
358
- return nil unless timestamp
385
+ # 2) User override/extend (R2)
386
+ merge_scope_into(effective, org_perms['accessScope'])
359
387
 
360
- # Check if cache entry has expired
361
- if Time.now.to_i - timestamp > @config[:cache_timeout]
362
- @cache.delete(key)
363
- @cache_timestamps.delete(key)
364
- return nil
388
+ # 3) Legacy resourceRestrictions = include (no match)
389
+ restrictions = org_perms['resourceRestrictions']
390
+ if restrictions.is_a?(Hash)
391
+ restrictions.each do |key, ids|
392
+ merge_scope_into(effective, { key => { 'include' => ids } })
393
+ end
365
394
  end
366
395
 
367
- @cache[key]
368
- end
369
-
370
- def set_in_cache(key, value, _ttl = nil)
371
- @cache[key] = value
372
- @cache_timestamps[key] = Time.now.to_i
396
+ effective
397
+ rescue StandardError => e
398
+ puts "Error resolving access scope for #{user_id}:#{app_id}:#{organization_id}: #{e.message}"
399
+ {}
373
400
  end
374
401
 
375
402
  # Resolve permissions the user has over a specific resource instance (ReBAC).
376
403
  # Applies resourceRestrictions per-permission: open mode if none configured.
377
404
  # Returns nil if the user has no active membership in that org.
405
+ # NOTE: public because JwtValidator calls it with an explicit receiver.
378
406
  def resolve_resource_permissions(user_id, app_id, organization_id, resource_id)
379
407
  effective = resolve_effective_permissions(user_id, app_id, organization_id)
380
408
  return nil unless effective
@@ -401,6 +429,49 @@ module JwtAuthCognito
401
429
  nil
402
430
  end
403
431
 
432
+ private
433
+
434
+ def get_from_cache(key)
435
+ return nil unless @cache.key?(key)
436
+
437
+ timestamp = @cache_timestamps[key]
438
+ return nil unless timestamp
439
+
440
+ # Check if cache entry has expired
441
+ if Time.now.to_i - timestamp > @config[:cache_timeout]
442
+ @cache.delete(key)
443
+ @cache_timestamps.delete(key)
444
+ return nil
445
+ end
446
+
447
+ @cache[key]
448
+ end
449
+
450
+ def set_in_cache(key, value, _ttl = nil)
451
+ @cache[key] = value
452
+ @cache_timestamps[key] = Time.now.to_i
453
+ end
454
+
455
+ # Additively merge a scope map into another: union match per dimension, and include/exclude.
456
+ def merge_scope_into(into, from)
457
+ return unless from.is_a?(Hash)
458
+
459
+ from.each do |key, scope|
460
+ next unless scope.is_a?(Hash)
461
+
462
+ target = into[key] || {}
463
+ if scope['match'].is_a?(Hash)
464
+ target['match'] ||= {}
465
+ scope['match'].each do |dim, values|
466
+ target['match'][dim] = ((target['match'][dim] || []) + Array(values)).uniq
467
+ end
468
+ end
469
+ target['include'] = ((target['include'] || []) + Array(scope['include'])).uniq if scope['include']
470
+ target['exclude'] = ((target['exclude'] || []) + Array(scope['exclude'])).uniq if scope['exclude']
471
+ into[key] = target
472
+ end
473
+ end
474
+
404
475
  def compute_permissions_from_roles(app_id, organization_id, role_names)
405
476
  roles_data = get_app_roles(app_id, organization_id)
406
477
  return [] unless roles_data.is_a?(Hash)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JwtAuthCognito
4
- VERSION = '1.0.0-beta.14'
4
+ VERSION = '1.0.0-beta.15'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt_auth_cognito
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.beta.14
4
+ version: 1.0.0.pre.beta.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Optimal
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-18 00:00:00.000000000 Z
11
+ date: 2026-06-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-cognitoidentityprovider
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: connection_pool
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.2'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: json
43
57
  requirement: !ruby/object:Gem::Requirement