rack_jwt_aegis 0.0.0 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/.yard/yard_gfm_config.rb +21 -0
- data/.yardopts +16 -0
- data/CHANGELOG.md +204 -0
- data/README.md +339 -45
- data/Rakefile +52 -0
- data/bin/console +11 -0
- data/bin/docs +20 -0
- data/bin/setup +8 -0
- data/exe/rack_jwt_aegis +235 -0
- data/lib/rack_jwt_aegis/configuration.rb +205 -44
- data/lib/rack_jwt_aegis/jwt_validator.rb +56 -14
- data/lib/rack_jwt_aegis/middleware.rb +72 -2
- data/lib/rack_jwt_aegis/multi_tenant_validator.rb +43 -18
- data/lib/rack_jwt_aegis/rbac_manager.rb +323 -76
- data/lib/rack_jwt_aegis/request_context.rb +64 -23
- data/lib/rack_jwt_aegis/version.rb +1 -1
- data/lib/rack_jwt_aegis.rb +36 -1
- metadata +24 -13
- data/examples/basic_usage.rb +0 -85
- /data/sig/{rack_jwt_bastion.rbs → rack_jwt_aegis.rbs} +0 -0
data/README.md
CHANGED
@@ -1,15 +1,19 @@
|
|
1
1
|
# Rack JWT Aegis
|
2
2
|
|
3
|
-
|
3
|
+
[](https://badge.fury.io/rb/rack_jwt_aegis)
|
4
|
+
[](https://github.com/kanutocd/rack_jwt_aegis/actions)
|
5
|
+
[](https://codecov.io/gh/kanutocd/rack_jwt_aegis)
|
6
|
+
[](https://www.ruby-lang.org/en/)
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
4
8
|
|
5
|
-
|
9
|
+
JWT authentication middleware for hierarchical multi-tenant Rack applications with 2-level tenant support.
|
6
10
|
|
7
11
|
## Features
|
8
12
|
|
9
13
|
- JWT token validation with configurable algorithms
|
10
|
-
- 2-level multi-tenant support (Company-Group → Company, Organization → Department, etc.)
|
14
|
+
- 2-level multi-tenant support (Example: Company-Group → Company, Organization → Department, etc.)
|
11
15
|
- Subdomain-based tenant isolation for top-level tenants
|
12
|
-
-
|
16
|
+
- URL pathname slug access control for sub-level tenants
|
13
17
|
- Configurable path exclusions for public endpoints
|
14
18
|
- Custom payload validation
|
15
19
|
- Debug mode for development
|
@@ -18,61 +22,95 @@ JWT authentication middleware for hierarchical multi-tenant Rack applications wi
|
|
18
22
|
|
19
23
|
Add this line to your application's Gemfile:
|
20
24
|
|
21
|
-
```
|
22
|
-
gem 'rack_jwt_aegis'
|
25
|
+
```bash
|
26
|
+
gem 'rack_jwt_aegis'
|
23
27
|
```
|
24
28
|
|
25
29
|
And then execute:
|
26
30
|
|
27
31
|
```bash
|
28
|
-
bundle install
|
32
|
+
bundle install
|
29
33
|
```
|
30
34
|
|
31
35
|
Or install it yourself as:
|
32
36
|
|
33
37
|
```bash
|
34
|
-
gem install rack_jwt_aegis
|
38
|
+
gem install rack_jwt_aegis
|
39
|
+
```
|
40
|
+
|
41
|
+
## CLI Tool
|
42
|
+
|
43
|
+
Rack JWT Aegis includes a command-line tool for generating secure JWT secrets:
|
44
|
+
|
45
|
+
```bash
|
46
|
+
# Generate a secure JWT secret
|
47
|
+
rack-jwt-aegis secret
|
48
|
+
|
49
|
+
# Generate base64-encoded secret
|
50
|
+
rack-jwt-aegis secret --format base64
|
51
|
+
|
52
|
+
# Generate secret in environment variable format
|
53
|
+
rack-jwt-aegis secret --env
|
54
|
+
|
55
|
+
# Generate multiple secrets
|
56
|
+
rack-jwt-aegis secret --count 3
|
57
|
+
|
58
|
+
# Quiet mode (secret only)
|
59
|
+
rack-jwt-aegis secret --quiet
|
60
|
+
|
61
|
+
# Custom length (32 bytes)
|
62
|
+
rack-jwt-aegis secret --length 32
|
63
|
+
|
64
|
+
# Show help
|
65
|
+
rack-jwt-aegis --help
|
35
66
|
```
|
36
67
|
|
68
|
+
### Security Features
|
69
|
+
|
70
|
+
- Uses `SecureRandom` for cryptographically secure generation
|
71
|
+
- Default 64-byte secrets provide ~512 bits of entropy
|
72
|
+
- Multiple output formats: hex, base64, raw
|
73
|
+
- Environment variable formatting for easy setup
|
74
|
+
|
37
75
|
## Quick Start
|
38
76
|
|
39
77
|
### Rails Application
|
40
78
|
|
41
79
|
```ruby
|
42
|
-
# config/application.rb
|
43
|
-
config.middleware.insert_before 0, RackJwtAegis::Middleware, {
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
}
|
80
|
+
# config/application.rb
|
81
|
+
config.middleware.insert_before 0, RackJwtAegis::Middleware, {
|
82
|
+
jwt_secret: ENV['JWT_SECRET'],
|
83
|
+
tenant_id_header_name: 'X-Tenant-Id',
|
84
|
+
skip_paths: ['/api/v1/login', '/health']
|
85
|
+
}
|
48
86
|
```
|
49
87
|
|
50
88
|
### Sinatra Application
|
51
89
|
|
52
90
|
```ruby
|
53
|
-
require 'rack_jwt_aegis'
|
91
|
+
require 'rack_jwt_aegis'
|
54
92
|
|
55
|
-
use RackJwtAegis::Middleware, {
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
}
|
93
|
+
use RackJwtAegis::Middleware, {
|
94
|
+
jwt_secret: ENV['JWT_SECRET'],
|
95
|
+
tenant_id_header_name: 'X-Tenant-Id',
|
96
|
+
skip_paths: ['/login', '/health']
|
97
|
+
}
|
60
98
|
```
|
61
99
|
|
62
100
|
### Pure Rack Application
|
63
101
|
|
64
102
|
```ruby
|
65
|
-
require 'rack_jwt_aegis'
|
103
|
+
require 'rack_jwt_aegis'
|
66
104
|
|
67
|
-
app = Rack::Builder.new do
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
105
|
+
app = Rack::Builder.new do
|
106
|
+
use RackJwtAegis::Middleware, {
|
107
|
+
jwt_secret: ENV['JWT_SECRET'],
|
108
|
+
validate_subdomain: true,
|
109
|
+
validate_pathname_slug: true
|
110
|
+
}
|
73
111
|
|
74
|
-
|
75
|
-
end
|
112
|
+
run YourApp.new
|
113
|
+
end
|
76
114
|
```
|
77
115
|
|
78
116
|
## Configuration Options
|
@@ -86,13 +124,30 @@ RackJwtAegis::Middleware.new(app, {
|
|
86
124
|
jwt_algorithm: 'HS256', # Default: 'HS256'
|
87
125
|
|
88
126
|
# Multi-Tenant Settings
|
89
|
-
|
127
|
+
tenant_id_header_name: 'X-Tenant-Id', # Default: 'X-Tenant-Id'
|
90
128
|
validate_subdomain: true, # Default: false
|
91
|
-
|
129
|
+
validate_pathname_slug: true, # Default: false
|
92
130
|
|
93
131
|
# Path Configuration
|
94
|
-
skip_paths: ['/health', '/api/v1/login'
|
95
|
-
|
132
|
+
skip_paths: ['/health', '/api/v1/login'],
|
133
|
+
pathname_slug_pattern: /^\/api\/v1\/([^\/]+)\//, # Default pattern
|
134
|
+
|
135
|
+
# RBAC Configuration
|
136
|
+
rbac_enabled: true, # Default: false
|
137
|
+
rbac_cache_store: :redis, # Required when RBAC enabled
|
138
|
+
rbac_cache_options: { url: ENV['REDIS_URL'] },
|
139
|
+
user_permissions_ttl: 3600, # Default: 1800 (30 minutes) - TTL for cached user permissions
|
140
|
+
|
141
|
+
# Cache Store Configuration (choose one approach)
|
142
|
+
# Option 1: Shared cache for both RBAC and permissions
|
143
|
+
cache_store: :memory, # :memory, :redis, :memcached, :solid_cache
|
144
|
+
cache_options: { url: ENV['REDIS_URL'] },
|
145
|
+
|
146
|
+
# Option 2: Separate cache stores for RBAC and permissions
|
147
|
+
rbac_cache_store: :redis, # For RBAC permissions data
|
148
|
+
rbac_cache_options: { url: ENV['REDIS_URL'] },
|
149
|
+
permission_cache_store: :memory, # For cached user permissions
|
150
|
+
permission_cache_options: {},
|
96
151
|
|
97
152
|
# Response Customization
|
98
153
|
unauthorized_response: { error: 'Authentication required' },
|
@@ -118,9 +173,9 @@ RackJwtAegis::Middleware.new(app, {
|
|
118
173
|
# Flexible Payload Mapping
|
119
174
|
payload_mapping: {
|
120
175
|
user_id: :sub, # Map 'sub' claim to user_id
|
121
|
-
|
122
|
-
|
123
|
-
|
176
|
+
tenant_id: :company_group_id, # Map 'company_group_id' claim
|
177
|
+
subdomain: :company_group_domain_name, # Map 'company_group_domain_name' claim
|
178
|
+
pathname_slugs: :accessible_company_slugs # Map array of accessible companies
|
124
179
|
},
|
125
180
|
|
126
181
|
# Custom Tenant Extraction
|
@@ -147,17 +202,79 @@ config.validate_subdomain = true
|
|
147
202
|
|
148
203
|
```ruby
|
149
204
|
# Validates that the requested company slug is accessible to the user
|
150
|
-
config.
|
151
|
-
config.
|
205
|
+
config.validate_pathname_slug = true
|
206
|
+
config.pathname_slug_pattern = /^\/api\/v1\/([^\/]+)\//
|
152
207
|
```
|
153
208
|
|
154
209
|
### Header-Based Validation
|
155
210
|
|
156
211
|
```ruby
|
157
|
-
# Validates the X-
|
158
|
-
config.
|
212
|
+
# Validates the X-Tenant-Id header against JWT payload
|
213
|
+
config.tenant_id_header_name = 'X-Tenant-Id'
|
214
|
+
```
|
215
|
+
|
216
|
+
The value from this request header entry will be used to verify the JWT's mapped `tenant_id` claim.
|
217
|
+
|
218
|
+
## Request Context Access
|
219
|
+
|
220
|
+
After successful JWT authentication, the middleware stores user context in the Rack environment for easy access in your application:
|
221
|
+
|
222
|
+
### Basic Usage
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
# In your controllers or middleware
|
226
|
+
class UsersController < ApplicationController
|
227
|
+
def index
|
228
|
+
# Check if request is authenticated
|
229
|
+
return unauthorized unless RackJwtAegis::RequestContext.authenticated?(request.env)
|
230
|
+
|
231
|
+
# Get user information
|
232
|
+
user_id = RackJwtAegis::RequestContext.user_id(request.env)
|
233
|
+
tenant_id = RackJwtAegis::RequestContext.tenant_id(request.env)
|
234
|
+
|
235
|
+
# Access full JWT payload
|
236
|
+
payload = RackJwtAegis::RequestContext.payload(request.env)
|
237
|
+
roles = payload['roles']
|
238
|
+
|
239
|
+
# Your business logic here
|
240
|
+
users = User.where(tenant_id: tenant_id)
|
241
|
+
render json: users
|
242
|
+
end
|
243
|
+
end
|
244
|
+
```
|
245
|
+
|
246
|
+
### Multi-Tenant Context
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
# Access subdomain information
|
250
|
+
subdomain = RackJwtAegis::RequestContext.subdomain(request.env)
|
251
|
+
# => "acme-group-of-companies"
|
252
|
+
|
253
|
+
# Check pathname slug access
|
254
|
+
accessible_companies = RackJwtAegis::RequestContext.pathname_slugs(request.env)
|
255
|
+
# => ["company-a", "company-b"]
|
256
|
+
|
257
|
+
# Check if user has access to specific company
|
258
|
+
has_access = RackJwtAegis::RequestContext.has_company_access?(request.env, "company-a")
|
259
|
+
# => true
|
260
|
+
|
261
|
+
# Helper methods for request objects
|
262
|
+
user_id = RackJwtAegis::RequestContext.current_user_id(request)
|
263
|
+
tenant_id = RackJwtAegis::RequestContext.current_tenant_id(request)
|
159
264
|
```
|
160
265
|
|
266
|
+
### Available Context Methods
|
267
|
+
|
268
|
+
- `authenticated?(env)` - Check if request is authenticated
|
269
|
+
- `payload(env)` - Get full JWT payload hash
|
270
|
+
- `user_id(env)` - Get authenticated user ID
|
271
|
+
- `tenant_id(env)` - Get tenant/company group ID
|
272
|
+
- `subdomain(env)` - Get subdomain from JWT
|
273
|
+
- `pathname_slugs(env)` - Get array of accessible company slugs
|
274
|
+
- `current_user_id(request)` - Helper for request objects
|
275
|
+
- `current_tenant_id(request)` - Helper for request objects
|
276
|
+
- `has_company_access?(env, slug)` - Check company access
|
277
|
+
|
161
278
|
## JWT Payload Structure
|
162
279
|
|
163
280
|
The middleware expects JWT payloads with the following structure:
|
@@ -165,9 +282,9 @@ The middleware expects JWT payloads with the following structure:
|
|
165
282
|
```json
|
166
283
|
{
|
167
284
|
"user_id": 12345,
|
168
|
-
"
|
169
|
-
"
|
170
|
-
"
|
285
|
+
"tenant_id": 67890,
|
286
|
+
"subdomain": "acme-group-of-companies", # the subdomain part of the host of the request url, e.g. `http://acme-group-of-companies.example.com`
|
287
|
+
"pathname_slugs": ["an-acme-company-subsidiary", "another-acme-company-the-user-has-access"], # the user has access to these kind of request urls: https://acme-group-of-companies.example.com/api/v1/an-acme-company-subsidiary/* or https://acme-group-of-companies.example.com/api/v1/another-acme-company-the-user-has-access/
|
171
288
|
"roles": ["admin", "user"],
|
172
289
|
"exp": 1640995200,
|
173
290
|
"iat": 1640991600
|
@@ -184,10 +301,164 @@ You can customize the payload mapping using the `payload_mapping` configuration
|
|
184
301
|
- Subdomain validation
|
185
302
|
- Request path filtering for public endpoints
|
186
303
|
|
187
|
-
## Performance
|
304
|
+
## Performance & Caching
|
188
305
|
|
189
306
|
- Skip paths are checked before JWT processing
|
190
307
|
- Low memory footprint
|
308
|
+
- Multi-tier permission caching system for RBAC performance
|
309
|
+
- TTL-based cache invalidation for user permissions
|
310
|
+
- Support for multiple cache stores: `:memory`, `:redis`, `:memcached`, `:solid_cache`
|
311
|
+
|
312
|
+
### Cache Store Configuration
|
313
|
+
|
314
|
+
#### Supported Cache Stores
|
315
|
+
|
316
|
+
1. **Memory Cache** (`:memory`) - For development and testing
|
317
|
+
2. **Redis Cache** (`:redis`) - For production with high availability
|
318
|
+
3. **Memcached Cache** (`:memcached`) - For distributed caching
|
319
|
+
4. **Solid Cache** (`:solid_cache`) - For Rails 8+ applications
|
320
|
+
|
321
|
+
#### Configuration Examples
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
# Memory cache (development/testing)
|
325
|
+
config.cache_store = :memory
|
326
|
+
|
327
|
+
# Redis cache
|
328
|
+
config.cache_store = :redis
|
329
|
+
config.cache_options = { url: ENV['REDIS_URL'] }
|
330
|
+
|
331
|
+
# Memcached cache
|
332
|
+
config.cache_store = :memcached
|
333
|
+
config.cache_options = { servers: ['localhost:11211'] }
|
334
|
+
|
335
|
+
# Solid Cache (Rails 8+)
|
336
|
+
config.cache_store = :solid_cache
|
337
|
+
```
|
338
|
+
|
339
|
+
#### Separate Cache Stores
|
340
|
+
|
341
|
+
You can configure separate cache stores for RBAC permissions data and cached user permissions:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
# Use Redis for RBAC data (shared across instances)
|
345
|
+
config.rbac_cache_store = :redis
|
346
|
+
config.rbac_cache_options = { url: ENV['REDIS_URL'] }
|
347
|
+
|
348
|
+
# Use memory for user permission cache (faster local access)
|
349
|
+
config.permission_cache_store = :memory
|
350
|
+
config.permission_cache_options = {}
|
351
|
+
```
|
352
|
+
|
353
|
+
## RBAC Cache Format
|
354
|
+
|
355
|
+
When RBAC is enabled, the middleware expects permissions to be stored in the cache with this exact format:
|
356
|
+
|
357
|
+
```json
|
358
|
+
{
|
359
|
+
"last_update": 1640995200,
|
360
|
+
"permissions": [
|
361
|
+
{
|
362
|
+
"123": [
|
363
|
+
"sales/invoices:get",
|
364
|
+
"sales/invoices:post",
|
365
|
+
"%r{sales/invoices/\\d+}:get",
|
366
|
+
"%r{sales/invoices/\\d+}:put",
|
367
|
+
"users/*:get"
|
368
|
+
]
|
369
|
+
},
|
370
|
+
{
|
371
|
+
"456": ["admin/*:*", "reports:get"]
|
372
|
+
}
|
373
|
+
]
|
374
|
+
}
|
375
|
+
```
|
376
|
+
|
377
|
+
### Format Specification
|
378
|
+
|
379
|
+
- **last_update**: Timestamp for cache invalidation
|
380
|
+
- **permissions**: Array of role permission objects
|
381
|
+
- **Role ID**: String or numeric identifier for user roles
|
382
|
+
- **Permission Format**: `"resource-endpoint:http-method"`
|
383
|
+
- **resource-endpoint**: API path (literal string or regex pattern)
|
384
|
+
- **http-method**: `get`, `post`, `put`, `delete`, or `*` (wildcard)
|
385
|
+
|
386
|
+
### Permission Examples
|
387
|
+
|
388
|
+
```ruby
|
389
|
+
# Literal path matching
|
390
|
+
"sales/invoices:get" # GET /api/v1/company/sales/invoices
|
391
|
+
"users/profile:put" # PUT /api/v1/company/users/profile
|
392
|
+
|
393
|
+
# Regex pattern matching
|
394
|
+
"%r{ sales/invoices/\\d+}:get" # GET /api/v1/company/sales/invoices/123
|
395
|
+
"%r{ users/\\d+/orders}:*" # Any method on /api/v1/company/users/123/orders
|
396
|
+
|
397
|
+
# Wildcard method
|
398
|
+
"reports:*" # Any method on reports endpoint
|
399
|
+
"admin/*:*" # Full admin access
|
400
|
+
```
|
401
|
+
|
402
|
+
### Request Authorization Flow
|
403
|
+
|
404
|
+
1. **Check User Permissions Cache**: Fast lookup in middleware cache
|
405
|
+
|
406
|
+
- **RBAC Update Check**: If RBAC permissions updated within TTL → **Nuke entire cache**
|
407
|
+
- **TTL Check**: If individual permission older than configured TTL → Remove only that permission
|
408
|
+
- If cache valid and permission found: **✅ Authorized**
|
409
|
+
- If cache valid but no permission: **❌ 403 Forbidden**
|
410
|
+
|
411
|
+
2. **RBAC Permissions Validation**: Full permission evaluation
|
412
|
+
|
413
|
+
- Extract user roles from JWT payload (`roles`, `role`, `user_roles`, or `role_ids` field)
|
414
|
+
- Load RBAC permissions collection and validate format
|
415
|
+
- For each user role, check if any permission matches:
|
416
|
+
- Extract resource path from request URL (removes subdomain/pathname slug)
|
417
|
+
- Match against permission patterns (literal or regex)
|
418
|
+
- Validate HTTP method (exact match or wildcard)
|
419
|
+
- If authorized: Cache permission for future requests
|
420
|
+
- Return 403 Forbidden if no matching permissions found
|
421
|
+
|
422
|
+
3. **Cache Storage**: Successful permissions cached with simple key-value format:
|
423
|
+
```json
|
424
|
+
{
|
425
|
+
"12345:acme-group.localhost.local/api/v1/company/sales/invoices:get": 1640995200,
|
426
|
+
"12345:acme-group.localhost.local/api/v1/company/sales/invoices:post": 1640995200
|
427
|
+
}
|
428
|
+
```
|
429
|
+
TTL configurable via `user_permissions_ttl` option (default: 30 minutes)
|
430
|
+
|
431
|
+
### Cache Invalidation Strategy
|
432
|
+
|
433
|
+
**RBAC Collection Updated** (e.g., role permissions changed):
|
434
|
+
|
435
|
+
- **Condition**: RBAC `last_update` within configured TTL
|
436
|
+
- **Action**: **Nuke entire cache** (all users, all permissions)
|
437
|
+
- **Reason**: Any permission could have changed, safer to re-evaluate everything
|
438
|
+
|
439
|
+
**Individual Permission TTL Expired**:
|
440
|
+
|
441
|
+
- **Condition**: Specific permission older than configured TTL
|
442
|
+
- **Action**: **Remove only that permission** (preserve others)
|
443
|
+
- **Reason**: Permission naturally aged out, other permissions still valid
|
444
|
+
|
445
|
+
### Example Authorization
|
446
|
+
|
447
|
+
Request: `POST https://acme-group.localhost/api/v1/an-acme-company/sales/invoices`
|
448
|
+
|
449
|
+
- User has role: `123`
|
450
|
+
- Role `123` permissions: `["sales/invoices:get", "sales/invoices:post"]`
|
451
|
+
- Extracted resource path: `sales/invoices`
|
452
|
+
- Request method: `POST`
|
453
|
+
- Result: **✅ Authorized** (matches `sales/invoices:post`)
|
454
|
+
|
455
|
+
Request: `DELETE https://acme-group.localhost/api/v1/an-acme-company/sales/invoices/456`
|
456
|
+
|
457
|
+
- User has role: `123`
|
458
|
+
- Role `123` permissions: `["sales/invoices:get", "%r{sales/invoices/\\d+}:put"]`
|
459
|
+
- Extracted resource path: `sales/invoices/456`
|
460
|
+
- Request method: `DELETE`
|
461
|
+
- Result: **❌ 403 Forbidden** (no DELETE permission)
|
191
462
|
|
192
463
|
## Error Handling
|
193
464
|
|
@@ -196,11 +467,34 @@ The middleware returns appropriate HTTP status codes:
|
|
196
467
|
- **401 Unauthorized** - Missing, invalid, or expired JWT
|
197
468
|
- **403 Forbidden** - Valid JWT but insufficient permissions/access
|
198
469
|
|
470
|
+
## Documentation
|
471
|
+
|
472
|
+
API documentation is available online and is automatically generated from the source code:
|
473
|
+
|
474
|
+
- **📚 [Online Documentation](https://kanutocd.github.io/rack_jwt_aegis/)** - Auto-deployed from the main branch
|
475
|
+
- **🔧 Generate Locally**: `bundle exec yard doc` and open `doc/index.html`
|
476
|
+
|
477
|
+
### Documentation Features
|
478
|
+
|
479
|
+
- Complete API reference for all classes and modules
|
480
|
+
- Code examples and usage patterns
|
481
|
+
- Architecture decision records (ADRs)
|
482
|
+
- Integration examples for common use cases
|
483
|
+
|
199
484
|
## Development
|
200
485
|
|
201
486
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
202
487
|
|
203
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
488
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
489
|
+
|
490
|
+
### Test Coverage
|
491
|
+
|
492
|
+
This project maintains high test coverage:
|
493
|
+
|
494
|
+
- **Line Coverage**: 97.8% (668/683 lines)
|
495
|
+
- **Branch Coverage**: 86.62% (259/299 branches)
|
496
|
+
|
497
|
+
Run tests with coverage: `bundle exec rake test`
|
204
498
|
|
205
499
|
## Contributing
|
206
500
|
|
data/Rakefile
CHANGED
@@ -9,4 +9,56 @@ Rake::TestTask.new(:test) do |t|
|
|
9
9
|
t.test_files = FileList['test/**/*_test.rb']
|
10
10
|
end
|
11
11
|
|
12
|
+
# YARD documentation tasks
|
13
|
+
begin
|
14
|
+
require 'yard'
|
15
|
+
require 'yard/rake/yardoc_task'
|
16
|
+
|
17
|
+
# Load custom GFM configuration if available
|
18
|
+
begin
|
19
|
+
require_relative '.yard/yard_gfm_config'
|
20
|
+
rescue LoadError
|
21
|
+
# GFM config not available, use default markdown processing
|
22
|
+
end
|
23
|
+
|
24
|
+
YARD::Rake::YardocTask.new do |t|
|
25
|
+
t.files = ['lib/**/*.rb']
|
26
|
+
t.options = [
|
27
|
+
'--output-dir', 'doc',
|
28
|
+
'--readme', 'README.md',
|
29
|
+
'--markup-provider', 'kramdown',
|
30
|
+
'--markup', 'markdown'
|
31
|
+
]
|
32
|
+
t.stats_options = ['--list-undoc']
|
33
|
+
end
|
34
|
+
|
35
|
+
desc 'Generate YARD documentation and open in browser'
|
36
|
+
task :docs do
|
37
|
+
Rake::Task['yard'].invoke
|
38
|
+
case RbConfig::CONFIG['host_os']
|
39
|
+
when /mswin|mingw|cygwin/
|
40
|
+
system 'start doc/index.html'
|
41
|
+
when /darwin/
|
42
|
+
system 'open doc/index.html'
|
43
|
+
when /linux|bsd/
|
44
|
+
system 'xdg-open doc/index.html'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
desc 'Run YARD documentation server'
|
49
|
+
task 'docs:server' do
|
50
|
+
puts 'Starting YARD documentation server at http://localhost:8808'
|
51
|
+
puts 'Press Ctrl+C to stop the server'
|
52
|
+
system 'yard server --reload --port 8808'
|
53
|
+
end
|
54
|
+
|
55
|
+
desc 'Check documentation coverage'
|
56
|
+
task 'docs:coverage' do
|
57
|
+
puts 'Generating YARD documentation coverage report...'
|
58
|
+
system 'yard stats --list-undoc'
|
59
|
+
end
|
60
|
+
rescue LoadError
|
61
|
+
puts 'YARD gem is not available. Install it with: gem install yard'
|
62
|
+
end
|
63
|
+
|
12
64
|
task default: :test
|
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'rack_jwt_aegis'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
require 'irb'
|
11
|
+
IRB.start(__FILE__)
|
data/bin/docs
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
puts '🔧 Generating documentation...'
|
7
|
+
system('bundle exec yard doc')
|
8
|
+
|
9
|
+
puts "\n📊 Documentation coverage:"
|
10
|
+
system('bundle exec yard stats --list-undoc')
|
11
|
+
|
12
|
+
puts "\n✅ Documentation generated successfully!"
|
13
|
+
puts '📂 Open doc/index.html in your browser to view the documentation'
|
14
|
+
|
15
|
+
# Check if we're in a git repository and suggest deployment
|
16
|
+
if system('git rev-parse --git-dir > /dev/null 2>&1')
|
17
|
+
puts "\n💡 To deploy to GitHub Pages:"
|
18
|
+
puts " git add . && git commit -m 'Update documentation' && git push"
|
19
|
+
puts ' Or trigger manually via GitHub Actions workflow dispatch'
|
20
|
+
end
|