rack_jwt_aegis 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85f1445f011d0e42c075e3777164d61b215aa04b6f78e535db987c3f0e239c6f
4
- data.tar.gz: 94fa6e5dd9041709d394e1c42dc5cc938aeb4fb9325218a94cab6352a4a7cf47
3
+ metadata.gz: 355b1a9e875355d5a3c5c7ca9c83dd24af066e1c69e89c9030fab8bb2c5a3f71
4
+ data.tar.gz: e151caf3879396e067aaf2c7f5ba0228fece3de07c5bdf41ac3df3e37d271ab9
5
5
  SHA512:
6
- metadata.gz: 0e3e83285e30c8b86efb2977aba6cfeccf728539b689fb80fb658e29ac91e61c455ab48c5793b689293154d3fe985ad6fee71d3b2381261f34b481370c89e8a1
7
- data.tar.gz: f42e6cdb32d72f06ef14d7c016cbb7970ad5b04f9626b16cc1bf7a4062cdb6b68a899548e80259bac934622b2adc3080121c2b7e82daa84ab93f2df30ad34e63
6
+ metadata.gz: e9709c05464dade807e95a135d6c5fe7d4e301127c4ea23209086298bc3078882cd1c5454262b1bc1759d1b48b27ac7cd630690e735a210f354bbb20fea808e8
7
+ data.tar.gz: 505983dd1f954880f7ad1a9a6e33beaf3ff83734c5034f90f5515d1ac0d48bac7c6b4e75842aeb7860695bf10efd7aba24c012cafd97b7e1089270d853c0bb23
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2
data/.yardopts CHANGED
@@ -1,14 +1,8 @@
1
- --output-dir doc
2
- --readme README.md
3
- --main README.md
4
- --markup=markdown
1
+ --title "RackJwtAegis Rack Middleware API Documentation""
5
2
  --protected
6
- --private
7
- --no-private
8
- --title "RackJwtAegis Rack Middleware API Documentation"
9
- --charset utf-8
10
- lib/**/*.rb
3
+ --markup markdown
4
+ --readme README.md
5
+ 'lib/**/*.rb'
11
6
  -
12
- README.md
13
- LICENSE.txt
14
- CODE_OF_CONDUCT.md
7
+ --files CHANGELOG.md LICENSE.txt CODE_OF_CONDUCT.md
8
+
data/CHANGELOG.md CHANGED
@@ -1,9 +1,218 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project will be documented in this file.
3
+ ## Unreleased
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+ ## [1.1.1] - 2026-06-13
6
+
7
+ ### 🚀 Added
8
+
9
+ #### Method-Aware Skip Routes
10
+
11
+ - Added `skip_routes` to support request-aware public route matching by path and HTTP verb.
12
+ - Kept `skip_paths` as a backward-compatible alias for all-method skips.
13
+ - Normalizes route rules once at configuration time so middleware checks stay fast per request.
14
+
15
+ #### Route Skipping Behavior
16
+
17
+ - Middleware now evaluates `METHOD + path` for skip decisions, so `POST /login` can be public while `GET /login` remains protected.
18
+ - Added support for blank verb lists to mean all methods on a route entry.
19
+ - Updated the Pink House API initializer to use the new `skip_routes` shape.
20
+
21
+ ### 🔧 Fixed
22
+
23
+ #### Simplified RBAC Cache Strategy
24
+
25
+ - **Streamlined Configuration**: Removed legacy cache configuration options to eliminate confusion
26
+ - **Removed**: `cache_store`, `cache_options`, and `cache_write_enabled` (deprecated in favor of dedicated RBAC cache stores)
27
+ - **Required**: When `rbac_enabled: true`, both `rbac_cache_store` and `permissions_cache_store` must be explicitly configured
28
+ - **Simplified**: Consistent naming with `rbac_cache_store_options` and `permissions_cache_store_options`
29
+
30
+ #### Cache Store Validation
31
+
32
+ - **Mandatory Configuration**: RBAC cache stores are now required when RBAC is enabled
33
+ - Prevents runtime errors from missing cache configuration
34
+ - Provides clear error messages during initialization: "rbac_cache_store and permissions_cache_store are required when RBAC is enabled"
35
+ - Ensures production deployments are properly configured for performance
36
+
37
+ #### Code Cleanup
38
+
39
+ - **Removed Complexity**: Eliminated dual cache mode logic and fallback mechanisms
40
+ - Simplified `RbacManager#setup_cache_adapters` method
41
+ - Removed conditional cache write logic that added complexity
42
+ - Cleaner permission checking flow with consistent cache behavior
43
+
44
+ #### Developer Experience
45
+
46
+ - **Clear Configuration**: Explicit cache store requirements eliminate guesswork
47
+ - **Better Error Messages**: Configuration validation happens at startup, not at runtime
48
+ - **Consistent Naming**: Standardized cache option parameter names across all adapters
49
+
50
+ ### 🏗️ Technical Details
51
+
52
+ #### Breaking Changes (Minor - Configuration Only)
53
+
54
+ - **Removed Configuration Options**:
55
+ - `cache_store` → Use `rbac_cache_store` and `permissions_cache_store` instead
56
+ - `cache_options` → Use `rbac_cache_store_options` and `permissions_cache_store_options` instead
57
+ - `cache_write_enabled` → Cache writing is now always enabled when RBAC is active
58
+
59
+ #### Migration Guide
60
+
61
+ **Before (v1.1.0):**
62
+ ```ruby
63
+ use RackJwtAegis::Middleware, {
64
+ jwt_secret: ENV['JWT_SECRET'],
65
+ rbac_enabled: true,
66
+ cache_store: :redis,
67
+ cache_options: { url: ENV['REDIS_URL'] },
68
+ cache_write_enabled: true
69
+ }
70
+ ```
71
+
72
+ **After (v1.1.1):**
73
+ ```ruby
74
+ use RackJwtAegis::Middleware, {
75
+ jwt_secret: ENV['JWT_SECRET'],
76
+ rbac_enabled: true,
77
+ rbac_cache_store: :redis,
78
+ rbac_cache_store_options: { url: ENV['REDIS_URL'] },
79
+ permissions_cache_store: :redis,
80
+ permissions_cache_store_options: { url: ENV['REDIS_URL'] }
81
+ }
82
+ ```
83
+
84
+ #### Benefits
85
+
86
+ - **🚀 Simpler Configuration**: No more dual cache modes or conditional logic
87
+ - **🛡️ Fail-Fast Validation**: Configuration errors caught at startup
88
+ - **📈 Better Performance**: Dedicated cache stores optimized for their specific use cases
89
+ - **🧹 Cleaner Code**: Reduced complexity in cache management logic
90
+
91
+ ---
92
+
93
+ ## [1.1.0] - 2025-08-14
94
+
95
+ ### 🚀 Added
96
+
97
+ #### Enhanced RBAC Cache Format
98
+
99
+ - **Improved Performance**: Changed RBAC cache format from array-based to flat object structure for O(1) role lookup
100
+ - **Before**: `"permissions": [{ "role-id": ["resource:method"] }]` (O(n) array iteration)
101
+ - **After**: `"permissions": { "role-id": ["resource:method"] }` (O(1) direct access)
102
+ - **Developer Experience**: Enhanced error detection with descriptive ConfigurationError exceptions
103
+ - Catches common migration mistakes when upgrading from array to object format
104
+ - Provides clear error messages with expected format examples
105
+ - Helps developers quickly identify and fix RBAC configuration issues
106
+
107
+ #### Documentation Improvements
108
+
109
+ - **Clean Documentation**: Fixed YARD documentation rendering issues with improved Markdown processing
110
+ - Added Redcarpet gem for better Markdown parsing in YARD
111
+ - Removed complex Jekyll integration that was causing rendering issues
112
+ - Maintained YARD for comprehensive API documentation with cleaner output
113
+ - **API Documentation**: Enhanced code comments for better YARD rendering
114
+ - Updated key method documentation with clearer parameter descriptions
115
+ - Improved examples and usage patterns in documentation
116
+ - Better integration with YARD's HTML generation
117
+
118
+ ### 🔧 Fixed
119
+
120
+ #### RBAC System Improvements
121
+
122
+ - **Cache Format Validation**: Added explicit validation for RBAC permissions format
123
+ - Raises `ConfigurationError` when permissions is not a Hash
124
+ - Provides helpful error messages for developers
125
+ - Maintains backward compatibility for other validation scenarios
126
+ - **Role Lookup Logic**: Optimized role permission checking with direct hash access
127
+ - Eliminated unnecessary array iteration in permission validation
128
+ - Improved performance for applications with many roles
129
+ - Maintains support for both string and integer role keys
130
+
131
+ #### Bug Fixes
132
+
133
+ - **Test Coverage**: Fixed `test_check_rbac_format?` test that was using old array format
134
+ - **Key Resolution**: Fixed JWT payload key resolution bug in `rbac_last_update_timestamp` method
135
+ - **Validation Logic**: Updated all validation tests to use new flat object format
136
+
137
+ ### 🏗️ Technical Details
138
+
139
+ #### Architecture Changes
140
+
141
+ - **RBAC Manager**: Updated `validate_rbac_cache_format` and `check_rbac_format?` methods
142
+ - Direct hash lookup: `permissions_data[role_id]` instead of array iteration
143
+ - Improved error handling with specific ConfigurationError exceptions
144
+ - Enhanced validation with clear developer feedback
145
+ - **Exception Handling**: Re-raises ConfigurationError while preserving other error handling
146
+ - Developer errors bubble up for immediate attention
147
+ - Runtime errors (cache issues) are still handled gracefully
148
+ - Maintains existing debug logging for troubleshooting
149
+
150
+ #### Performance Improvements
151
+
152
+ - **O(1) Role Lookup**: Direct hash access for role permissions
153
+ - **Reduced Memory**: Eliminated nested array structures
154
+ - **Faster Validation**: Simplified permission checking logic
155
+
156
+ #### Developer Experience
157
+
158
+ - **Better Error Messages**: Clear configuration error descriptions
159
+
160
+ ```ruby
161
+ # Example error message:
162
+ "RBAC permissions must be a Hash with role-id keys, not Array.
163
+ Expected format: {\"role-id\": [\"resource:method\", ...]}, but got: Array"
164
+ ```
165
+
166
+ - **Migration Support**: Catches common mistakes when upgrading cache format
167
+ - **Improved Documentation**: Cleaner YARD output with better Markdown rendering
168
+
169
+ ### 📚 Documentation Updates
170
+
171
+ - **README.md**: Updated RBAC cache format examples and specifications
172
+ - **YARD Integration**: Fixed documentation rendering with Redcarpet Markdown processor
173
+ - **Code Examples**: Updated all examples to use new flat object format
174
+
175
+ ### 🧪 Testing
176
+
177
+ - **Test Coverage Maintained**: All tests updated to use new cache format
178
+ - **Enhanced Validation**: Added tests for new configuration error scenarios
179
+ - **Comprehensive Coverage**: Validated migration scenarios and edge cases
180
+
181
+ ### ⚠️ Migration Guide
182
+
183
+ #### Updating RBAC Cache Format
184
+
185
+ **Before (v1.0.x):**
186
+
187
+ ```ruby
188
+ Rails.cache.write("permissions", {
189
+ 'last_update' => Time.now.to_i,
190
+ 'permissions' => [
191
+ { '123' => ['sales/invoices:get', 'sales/invoices:post'] },
192
+ { '456' => ['admin/*:*'] }
193
+ ]
194
+ })
195
+ ```
196
+
197
+ **After (v1.1.0+):**
198
+
199
+ ```ruby
200
+ Rails.cache.write("permissions", {
201
+ 'last_update' => Time.now.to_i,
202
+ 'permissions' => {
203
+ '123' => ['sales/invoices:get', 'sales/invoices:post'],
204
+ '456' => ['admin/*:*']
205
+ }
206
+ })
207
+ ```
208
+
209
+ #### Benefits of Migration
210
+
211
+ - **🚀 Better Performance**: O(1) role lookup instead of O(n) iteration
212
+ - **🛠️ Easier Management**: Direct role access for permission updates
213
+ - **📝 Cleaner Code**: Simpler data structure that's easier to understand and maintain
214
+
215
+ ---
7
216
 
8
217
  ## [1.0.2] - 2025-08-13
9
218
 
@@ -174,8 +383,8 @@ use RackJwtAegis::Middleware, {
174
383
  pathname_slug_pattern: /^\/api\/v1\/([^\/]+)\//,
175
384
  rbac_enabled: true,
176
385
  rbac_cache_store: :redis,
177
- permission_cache_store: :memory,
178
- user_permissions_ttl: 300,
386
+ permissions_cache_store: :memory,
387
+ cached_permissions_ttl: 300,
179
388
  cache_write_enabled: true,
180
389
  skip_paths: [/^\/health/, /^\/metrics/, /^\/api\/public/],
181
390
  custom_payload_validation: ->(payload) { payload['active'] == true },
@@ -249,5 +458,8 @@ This 1.0.0 release represents a production-ready JWT authentication middleware w
249
458
 
250
459
  **Deprecations**: None (initial release).
251
460
 
461
+ [1.1.1]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.1.1
462
+ [1.1.0]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.1.0
463
+ [1.0.2]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.0.2
252
464
  [1.0.1]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.0.1
253
465
  [1.0.0]: https://github.com/kanutocd/rack_jwt_aegis/releases/tag/v1.0.0
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/rack_jwt_aegis.svg)](https://badge.fury.io/rb/rack_jwt_aegis)
4
4
  [![CI](https://github.com/kanutocd/rack_jwt_aegis/workflows/CI/badge.svg)](https://github.com/kanutocd/rack_jwt_aegis/actions)
5
5
  [![Coverage Status](https://codecov.io/gh/kanutocd/rack_jwt_aegis/branch/main/graph/badge.svg)](https://codecov.io/gh/kanutocd/rack_jwt_aegis)
6
- [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.1.0-ruby.svg)](https://www.ruby-lang.org/en/)
6
+ [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.2.0-ruby.svg)](https://www.ruby-lang.org/en/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
9
9
  JWT authentication and authorization middleware for hierarchical multi-tenant Rack applications with 2-level tenant support.
@@ -147,28 +147,24 @@ RackJwtAegis::Middleware.new(app, {
147
147
  pathname_slug_pattern: /^\/api\/v1\/([^\/]+)\//, # Default pattern
148
148
 
149
149
  # RBAC Configuration
150
- rbac_enabled: true, # Default: false
151
- rbac_cache_store: :redis, # Required when RBAC enabled
152
- rbac_cache_options: { url: ENV['REDIS_URL'] },
153
- user_permissions_ttl: 3600, # Default: 1800 (30 minutes) - TTL for cached user permissions
154
-
155
- # Cache Store Configuration (choose one approach)
156
- # Option 1: Shared cache for both RBAC and permissions
157
- cache_store: :memory, # :memory, :redis, :memcached, :solid_cache
158
- cache_options: { url: ENV['REDIS_URL'] },
159
-
160
- # Option 2: Separate cache stores for RBAC and permissions
161
- rbac_cache_store: :redis, # For RBAC permissions data
162
- rbac_cache_options: { url: ENV['REDIS_URL'] },
163
- permission_cache_store: :memory, # For cached user permissions
164
- permission_cache_options: {},
150
+ rbac_enabled: true, # Default: false
151
+ rbac_cache_store: :redis, # Required when RBAC enabled. Default: :memory
152
+ rbac_cache_store_options: { url: ENV['REDIS_URL'] }, # Cache Store specific options. Default: {}
153
+
154
+ cached_permissions_ttl: 3600, # Default: 1800 (30 minutes) - TTL for cached user permissions
155
+ permissions_cache_store: :solid_cache, # Required when RBAC enabled. Default: :memory
156
+ permissions_cache_store_options: {}, # Cache Store specific options. Default: {}
157
+
158
+ # Or can also be the same Redis instance
159
+ # permissions_cache_store: :redis, # Required when RBAC enabled. Default: :memory
160
+ # permissions_cache_store_options: { url: ENV['REDIS_URL'] },
165
161
 
166
162
  # Response Customization
167
163
  unauthorized_response: { error: 'Authentication required' },
168
164
  forbidden_response: { error: 'Access denied' },
169
165
 
170
166
  # Debugging
171
- debug_mode: Rails.env.development? # Default: false
167
+ debug_mode: Rails.env.development? # Default: when in Rails, Rails.env.development? otherwise false
172
168
  })
173
169
  ```
174
170
 
@@ -273,7 +269,7 @@ accessible_companies = RackJwtAegis::RequestContext.pathname_slugs(request.env)
273
269
  # => ["company-a", "company-b"]
274
270
 
275
271
  # Check if user has access to specific company
276
- has_access = RackJwtAegis::RequestContext.has_company_access?(request.env, "company-a")
272
+ has_access = RackJwtAegis::RequestContext.has_pathname_slug_access?(request.env, "company-a")
277
273
  # => true
278
274
 
279
275
  # Helper methods for request objects
@@ -291,7 +287,7 @@ tenant_id = RackJwtAegis::RequestContext.current_tenant_id(request)
291
287
  - `pathname_slugs(env)` - Get array of accessible company slugs
292
288
  - `current_user_id(request)` - Helper for request objects
293
289
  - `current_tenant_id(request)` - Helper for request objects
294
- - `has_company_access?(env, slug)` - Check company access
290
+ - `has_pathname_slug_access?(env, slug)` - Check company access
295
291
 
296
292
  ## JWT Payload Structure
297
293
 
@@ -404,18 +400,19 @@ All role values are normalized to strings internally for consistent matching aga
404
400
 
405
401
  ```ruby
406
402
  # Memory cache (development/testing)
407
- config.cache_store = :memory
403
+ config.rbac_cache_store = :memory # Default. When in Rails, it will default to Rails.application.config.cache_store
404
+ config.permissions_cache_store = :memory # Default. When in Rails, it will default to Rails.application.config.cache_store
408
405
 
409
406
  # Redis cache
410
- config.cache_store = :redis
411
- config.cache_options = { url: ENV['REDIS_URL'] }
407
+ config.rbac_cache_store = :redis
408
+ config.rbac_cache_store_options = { url: ENV['REDIS_URL'] }
412
409
 
413
410
  # Memcached cache
414
- config.cache_store = :memcached
415
- config.cache_options = { servers: ['localhost:11211'] }
411
+ config.rbac_cache_store = :memcached
412
+ config.rbac_cache_store_options = { servers: ['localhost:11211'] }
416
413
 
417
414
  # Solid Cache (Rails 8+)
418
- config.cache_store = :solid_cache
415
+ config.rbac_cache_store = :solid_cache
419
416
  ```
420
417
 
421
418
  #### Separate Cache Stores
@@ -425,11 +422,11 @@ You can configure separate cache stores for RBAC permissions data and cached use
425
422
  ```ruby
426
423
  # Use Redis for RBAC data (shared across instances)
427
424
  config.rbac_cache_store = :redis
428
- config.rbac_cache_options = { url: ENV['REDIS_URL'] }
425
+ config.rbac_cache_store_options = { url: ENV['REDIS_URL'] }
429
426
 
430
427
  # Use memory for user permission cache (faster local access)
431
- config.permission_cache_store = :memory
432
- config.permission_cache_options = {}
428
+ config.permissions_cache_store = :memory
429
+ config.permissions_cache_store_options = {}
433
430
  ```
434
431
 
435
432
  ## RBAC Cache Format
@@ -439,31 +436,28 @@ When RBAC is enabled, the middleware expects permissions to be stored in the cac
439
436
  ```json
440
437
  {
441
438
  "last_update": 1640995200,
442
- "permissions": [
443
- {
444
- "123": [
445
- "sales/invoices:get",
446
- "sales/invoices:post",
447
- "%r{sales/invoices/\\d+}:get",
448
- "%r{sales/invoices/\\d+}:put",
449
- "users/*:get"
450
- ]
451
- },
452
- {
453
- "456": ["admin/*:*", "reports:get"]
454
- }
455
- ]
439
+ "permissions": {
440
+ "123": [
441
+ "sales/invoices:get",
442
+ "sales/invoices:post",
443
+ "%r{sales/invoices/\\d+}:get",
444
+ "%r{sales/invoices/\\d+}:put",
445
+ "users/*:get"
446
+ ],
447
+ "456": ["admin/*:*", "reports:get"]
448
+ }
456
449
  }
457
450
  ```
458
451
 
459
452
  ### Format Specification
460
453
 
461
454
  - **last_update**: Timestamp for cache invalidation
462
- - **permissions**: Array of role permission objects
463
- - **Role ID**: String or numeric identifier for user roles
464
- - **Permission Format**: `"resource-endpoint:http-method"`
465
- - **resource-endpoint**: API path (literal string or regex pattern)
466
- - **http-method**: `get`, `post`, `put`, `delete`, or `*` (wildcard)
455
+ - **permissions**: Object containing direct role-to-permissions mapping:
456
+ - **Role ID** (key): String or numeric identifier
457
+ - **Permissions** (value): Array of permission strings
458
+ - **Permission Format**: `"resource-endpoint:http-method"`
459
+ - **resource-endpoint**: API path (literal string or regex pattern)
460
+ - **http-method**: `get`, `post`, `put`, `delete`, or `*` (wildcard)
467
461
 
468
462
  ### Permission Examples
469
463
 
@@ -509,7 +503,7 @@ When RBAC is enabled, the middleware expects permissions to be stored in the cac
509
503
  "12345:acme-group.localhost.local/api/v1/company/sales/invoices:post": 1640995200
510
504
  }
511
505
  ```
512
- TTL configurable via `user_permissions_ttl` option (default: 30 minutes)
506
+ TTL configurable via `cached_permissions_ttl` option (default: 30 minutes)
513
507
 
514
508
  ### Cache Invalidation Strategy
515
509
 
@@ -4,13 +4,13 @@ module RackJwtAegis
4
4
  class CacheAdapter
5
5
  def self.build(store_type, options = {})
6
6
  case store_type
7
- when :memory
7
+ when :memory, :memory_store
8
8
  MemoryAdapter.new(options)
9
- when :redis
9
+ when :redis, :redis_cache_store
10
10
  RedisAdapter.new(options)
11
- when :memcached
11
+ when :memcached, :mem_cache_store
12
12
  MemcachedAdapter.new(options)
13
- when :solid_cache
13
+ when :solid_cache, :solid_cache_store
14
14
  SolidCacheAdapter.new(options)
15
15
  else
16
16
  raise ConfigurationError, "Unsupported cache store: #{store_type}"
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ratomic'
4
+
5
+ module RackJwtAegis
6
+ class CircuitBreaker
7
+ STATE_FAILURE_COUNT = 'failure_count'
8
+ STATE_OPENED_AT = 'opened_at'
9
+
10
+ def initialize(failure_threshold:, cooldown_seconds:)
11
+ @failure_threshold = failure_threshold.to_i
12
+ @cooldown_seconds = cooldown_seconds.to_i
13
+ @state = Ratomic::Map.new
14
+ reset
15
+ end
16
+
17
+ def allow_request?
18
+ return true unless open?
19
+ return false unless cooldown_elapsed?
20
+
21
+ reset
22
+ true
23
+ end
24
+
25
+ def record_success
26
+ reset
27
+ end
28
+
29
+ def record_failure
30
+ failures = @state.increment(STATE_FAILURE_COUNT)
31
+ @state[STATE_OPENED_AT] = Time.now.to_f if failures >= @failure_threshold
32
+ failures
33
+ end
34
+
35
+ def open?
36
+ !@state[STATE_OPENED_AT].nil?
37
+ end
38
+
39
+ private
40
+
41
+ def reset
42
+ @state[STATE_FAILURE_COUNT] = 0
43
+ @state.delete(STATE_OPENED_AT)
44
+ end
45
+
46
+ def cooldown_elapsed?
47
+ opened_at = @state[STATE_OPENED_AT]
48
+ opened_at && (Time.now.to_f - opened_at) >= @cooldown_seconds
49
+ end
50
+ end
51
+ end