rack_jwt_aegis 0.0.0 → 1.0.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.
data/README.md CHANGED
@@ -1,16 +1,22 @@
1
1
  # Rack JWT Aegis
2
2
 
3
- JWT authentication middleware for hierarchical multi-tenant Rack applications with 2-level tenant support.
3
+ [![Gem Version](https://badge.fury.io/rb/rack_jwt_aegis.svg)](https://badge.fury.io/rb/rack_jwt_aegis)
4
+ [![CI](https://github.com/kanutocd/rack_jwt_aegis/workflows/CI/badge.svg)](https://github.com/kanutocd/rack_jwt_aegis/actions)
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/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
8
 
5
- **Note: This is version 0.0.0 - a placeholder release to reserve the gem name. Implementation is in progress.**
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
- - Company slug access control for sub-level tenants
16
+ - URL pathname slug access control for sub-level tenants
17
+ - **RBAC (Role-Based Access Control)** with flexible role extraction from JWT payloads
13
18
  - Configurable path exclusions for public endpoints
19
+ - **Flexible payload mapping** for custom JWT claim names
14
20
  - Custom payload validation
15
21
  - Debug mode for development
16
22
 
@@ -18,61 +24,95 @@ JWT authentication middleware for hierarchical multi-tenant Rack applications wi
18
24
 
19
25
  Add this line to your application's Gemfile:
20
26
 
21
- ```ruby
22
- gem 'rack_jwt_aegis'
27
+ ```bash
28
+ gem 'rack_jwt_aegis'
23
29
  ```
24
30
 
25
31
  And then execute:
26
32
 
27
33
  ```bash
28
- bundle install
34
+ bundle install
29
35
  ```
30
36
 
31
37
  Or install it yourself as:
32
38
 
33
39
  ```bash
34
- gem install rack_jwt_aegis
40
+ gem install rack_jwt_aegis
35
41
  ```
36
42
 
43
+ ## CLI Tool
44
+
45
+ Rack JWT Aegis includes a command-line tool for generating secure JWT secrets:
46
+
47
+ ```bash
48
+ # Generate a secure JWT secret
49
+ rack-jwt-aegis secret
50
+
51
+ # Generate base64-encoded secret
52
+ rack-jwt-aegis secret --format base64
53
+
54
+ # Generate secret in environment variable format
55
+ rack-jwt-aegis secret --env
56
+
57
+ # Generate multiple secrets
58
+ rack-jwt-aegis secret --count 3
59
+
60
+ # Quiet mode (secret only)
61
+ rack-jwt-aegis secret --quiet
62
+
63
+ # Custom length (32 bytes)
64
+ rack-jwt-aegis secret --length 32
65
+
66
+ # Show help
67
+ rack-jwt-aegis --help
68
+ ```
69
+
70
+ ### Security Features
71
+
72
+ - Uses `SecureRandom` for cryptographically secure generation
73
+ - Default 64-byte secrets provide ~512 bits of entropy
74
+ - Multiple output formats: hex, base64, raw
75
+ - Environment variable formatting for easy setup
76
+
37
77
  ## Quick Start
38
78
 
39
79
  ### Rails Application
40
80
 
41
81
  ```ruby
42
- # config/application.rb
43
- config.middleware.insert_before 0, RackJwtAegis::Middleware, {
44
- jwt_secret: ENV['JWT_SECRET'],
45
- company_header_name: 'X-Company-Group-Id',
46
- skip_paths: ['/api/v1/login', '/api/v1/refresh', '/health']
47
- }
82
+ # config/application.rb
83
+ config.middleware.insert_before 0, RackJwtAegis::Middleware, {
84
+ jwt_secret: ENV['JWT_SECRET'],
85
+ tenant_id_header_name: 'X-Tenant-Id',
86
+ skip_paths: ['/api/v1/login', '/health']
87
+ }
48
88
  ```
49
89
 
50
90
  ### Sinatra Application
51
91
 
52
92
  ```ruby
53
- require 'rack_jwt_aegis'
93
+ require 'rack_jwt_aegis'
54
94
 
55
- use RackJwtAegis::Middleware, {
56
- jwt_secret: ENV['JWT_SECRET'],
57
- company_header_name: 'X-Company-Group-Id',
58
- skip_paths: ['/login', '/health']
59
- }
95
+ use RackJwtAegis::Middleware, {
96
+ jwt_secret: ENV['JWT_SECRET'],
97
+ tenant_id_header_name: 'X-Tenant-Id',
98
+ skip_paths: ['/login', '/health']
99
+ }
60
100
  ```
61
101
 
62
102
  ### Pure Rack Application
63
103
 
64
104
  ```ruby
65
- require 'rack_jwt_aegis'
105
+ require 'rack_jwt_aegis'
66
106
 
67
- app = Rack::Builder.new do
68
- use RackJwtAegis::Middleware, {
69
- jwt_secret: ENV['JWT_SECRET'],
70
- validate_subdomain: true,
71
- validate_company_slug: true
72
- }
107
+ app = Rack::Builder.new do
108
+ use RackJwtAegis::Middleware, {
109
+ jwt_secret: ENV['JWT_SECRET'],
110
+ validate_subdomain: true,
111
+ validate_pathname_slug: true
112
+ }
73
113
 
74
- run YourApp.new
75
- end
114
+ run YourApp.new
115
+ end
76
116
  ```
77
117
 
78
118
  ## Configuration Options
@@ -86,13 +126,30 @@ RackJwtAegis::Middleware.new(app, {
86
126
  jwt_algorithm: 'HS256', # Default: 'HS256'
87
127
 
88
128
  # Multi-Tenant Settings
89
- company_header_name: 'X-Company-Group-Id', # Default: 'X-Company-Group-Id'
129
+ tenant_id_header_name: 'X-Tenant-Id', # Default: 'X-Tenant-Id'
90
130
  validate_subdomain: true, # Default: false
91
- validate_company_slug: true, # Default: false
131
+ validate_pathname_slug: true, # Default: false
92
132
 
93
133
  # Path Configuration
94
- skip_paths: ['/health', '/api/v1/login', '/api/v1/refresh'],
95
- company_slug_pattern: /^\/api\/v1\/([^\/]+)\//, # Default pattern
134
+ skip_paths: ['/health', '/api/v1/login'],
135
+ pathname_slug_pattern: /^\/api\/v1\/([^\/]+)\//, # Default pattern
136
+
137
+ # RBAC Configuration
138
+ rbac_enabled: true, # Default: false
139
+ rbac_cache_store: :redis, # Required when RBAC enabled
140
+ rbac_cache_options: { url: ENV['REDIS_URL'] },
141
+ user_permissions_ttl: 3600, # Default: 1800 (30 minutes) - TTL for cached user permissions
142
+
143
+ # Cache Store Configuration (choose one approach)
144
+ # Option 1: Shared cache for both RBAC and permissions
145
+ cache_store: :memory, # :memory, :redis, :memcached, :solid_cache
146
+ cache_options: { url: ENV['REDIS_URL'] },
147
+
148
+ # Option 2: Separate cache stores for RBAC and permissions
149
+ rbac_cache_store: :redis, # For RBAC permissions data
150
+ rbac_cache_options: { url: ENV['REDIS_URL'] },
151
+ permission_cache_store: :memory, # For cached user permissions
152
+ permission_cache_options: {},
96
153
 
97
154
  # Response Customization
98
155
  unauthorized_response: { error: 'Authentication required' },
@@ -118,16 +175,10 @@ RackJwtAegis::Middleware.new(app, {
118
175
  # Flexible Payload Mapping
119
176
  payload_mapping: {
120
177
  user_id: :sub, # Map 'sub' claim to user_id
121
- company_group_id: :company_id, # Map 'company_id' claim
122
- company_group_domain: :domain, # Map 'domain' claim
123
- company_slugs: :accessible_companies # Map array of accessible companies
124
- },
125
-
126
- # Custom Tenant Extraction
127
- tenant_strategy: :custom,
128
- tenant_extractor: ->(request) {
129
- # Extract tenant from custom header or logic
130
- request.get_header('HTTP_X_TENANT_ID')
178
+ tenant_id: :company_group_id, # Map 'company_group_id' claim
179
+ subdomain: :company_group_domain_name, # Map 'company_group_domain_name' claim
180
+ pathname_slugs: :accessible_company_slugs, # Map array of accessible companies
181
+ role_ids: :user_roles # Map 'user_roles' claim for RBAC authorization
131
182
  }
132
183
  })
133
184
  ```
@@ -147,17 +198,79 @@ config.validate_subdomain = true
147
198
 
148
199
  ```ruby
149
200
  # Validates that the requested company slug is accessible to the user
150
- config.validate_company_slug = true
151
- config.company_slug_pattern = /^\/api\/v1\/([^\/]+)\//
201
+ config.validate_pathname_slug = true
202
+ config.pathname_slug_pattern = /^\/api\/v1\/([^\/]+)\//
152
203
  ```
153
204
 
154
205
  ### Header-Based Validation
155
206
 
156
207
  ```ruby
157
- # Validates the X-Company-Group-Id header against JWT payload
158
- config.company_header_name = 'X-Company-Group-Id'
208
+ # Validates the X-Tenant-Id header against JWT payload
209
+ config.tenant_id_header_name = 'X-Tenant-Id'
210
+ ```
211
+
212
+ The value from this request header entry will be used to verify the JWT's mapped `tenant_id` claim.
213
+
214
+ ## Request Context Access
215
+
216
+ After successful JWT authentication, the middleware stores user context in the Rack environment for easy access in your application:
217
+
218
+ ### Basic Usage
219
+
220
+ ```ruby
221
+ # In your controllers or middleware
222
+ class UsersController < ApplicationController
223
+ def index
224
+ # Check if request is authenticated
225
+ return unauthorized unless RackJwtAegis::RequestContext.authenticated?(request.env)
226
+
227
+ # Get user information
228
+ user_id = RackJwtAegis::RequestContext.user_id(request.env)
229
+ tenant_id = RackJwtAegis::RequestContext.tenant_id(request.env)
230
+
231
+ # Access full JWT payload
232
+ payload = RackJwtAegis::RequestContext.payload(request.env)
233
+ roles = payload['roles']
234
+
235
+ # Your business logic here
236
+ users = User.where(tenant_id: tenant_id)
237
+ render json: users
238
+ end
239
+ end
159
240
  ```
160
241
 
242
+ ### Multi-Tenant Context
243
+
244
+ ```ruby
245
+ # Access subdomain information
246
+ subdomain = RackJwtAegis::RequestContext.subdomain(request.env)
247
+ # => "acme-group-of-companies"
248
+
249
+ # Check pathname slug access
250
+ accessible_companies = RackJwtAegis::RequestContext.pathname_slugs(request.env)
251
+ # => ["company-a", "company-b"]
252
+
253
+ # Check if user has access to specific company
254
+ has_access = RackJwtAegis::RequestContext.has_company_access?(request.env, "company-a")
255
+ # => true
256
+
257
+ # Helper methods for request objects
258
+ user_id = RackJwtAegis::RequestContext.current_user_id(request)
259
+ tenant_id = RackJwtAegis::RequestContext.current_tenant_id(request)
260
+ ```
261
+
262
+ ### Available Context Methods
263
+
264
+ - `authenticated?(env)` - Check if request is authenticated
265
+ - `payload(env)` - Get full JWT payload hash
266
+ - `user_id(env)` - Get authenticated user ID
267
+ - `tenant_id(env)` - Get tenant/company group ID
268
+ - `subdomain(env)` - Get subdomain from JWT
269
+ - `pathname_slugs(env)` - Get array of accessible company slugs
270
+ - `current_user_id(request)` - Helper for request objects
271
+ - `current_tenant_id(request)` - Helper for request objects
272
+ - `has_company_access?(env, slug)` - Check company access
273
+
161
274
  ## JWT Payload Structure
162
275
 
163
276
  The middleware expects JWT payloads with the following structure:
@@ -165,10 +278,11 @@ The middleware expects JWT payloads with the following structure:
165
278
  ```json
166
279
  {
167
280
  "user_id": 12345,
168
- "company_group_id": 67890,
169
- "company_group_domain": "acme.example.com",
170
- "company_slugs": ["acme", "acme-corp"],
171
- "roles": ["admin", "user"],
281
+ "tenant_id": 67890,
282
+ "subdomain": "acme-group-of-companies", # the subdomain part of the host of the request url, e.g. `http://acme-group-of-companies.example.com`
283
+ "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/
284
+ "role_ids": ["123", "456"], # Role IDs for RBAC authorization (can also be integers)
285
+ "roles": ["admin", "user"], # Legacy role names (kept for backward compatibility)
172
286
  "exp": 1640995200,
173
287
  "iat": 1640991600
174
288
  }
@@ -176,6 +290,69 @@ The middleware expects JWT payloads with the following structure:
176
290
 
177
291
  You can customize the payload mapping using the `payload_mapping` configuration option.
178
292
 
293
+ ### RBAC Role Extraction
294
+
295
+ When RBAC is enabled, the middleware extracts user roles from the JWT payload for authorization. The default payload mapping includes:
296
+
297
+ ```ruby
298
+ payload_mapping: {
299
+ user_id: :user_id,
300
+ tenant_id: :tenant_id,
301
+ subdomain: :subdomain,
302
+ pathname_slugs: :pathname_slugs,
303
+ role_ids: :role_ids # Default field for user roles
304
+ }
305
+ ```
306
+
307
+ #### Role Field Resolution
308
+
309
+ The middleware looks for roles in the following priority order:
310
+
311
+ 1. **Configured Field**: Uses the `role_ids` mapping (e.g., if mapped to `:user_roles`, looks for `user_roles` field)
312
+ 2. **Fallback Fields**: If the mapped field is not found, tries these common alternatives:
313
+ - `roles` - Array of role identifiers
314
+ - `role` - Single role identifier
315
+ - `user_roles` - Array of user role identifiers
316
+ - `role_ids` - Array of role IDs (numeric or string)
317
+
318
+ #### Custom Role Field Mapping
319
+
320
+ You can customize the role field using payload mapping:
321
+
322
+ ```ruby
323
+ # Use a custom field name for roles
324
+ payload_mapping: {
325
+ role_ids: :user_permissions # Look for roles in 'user_permissions' field
326
+ }
327
+
328
+ # JWT payload would contain:
329
+ {
330
+ "user_id": 123,
331
+ "user_permissions": ["admin", "manager"],
332
+ ...
333
+ }
334
+ ```
335
+
336
+ #### Role Format Support
337
+
338
+ The middleware supports flexible role formats:
339
+
340
+ ```ruby
341
+ # Array of strings (recommended)
342
+ "role_ids": ["123", "456", "admin"]
343
+
344
+ # Array of integers
345
+ "role_ids": [123, 456]
346
+
347
+ # Single string
348
+ "role_ids": "admin"
349
+
350
+ # Single integer
351
+ "role_ids": 123
352
+ ```
353
+
354
+ All role values are normalized to strings internally for consistent matching against RBAC cache permissions.
355
+
179
356
  ## Security Features
180
357
 
181
358
  - JWT signature verification
@@ -184,10 +361,165 @@ You can customize the payload mapping using the `payload_mapping` configuration
184
361
  - Subdomain validation
185
362
  - Request path filtering for public endpoints
186
363
 
187
- ## Performance
364
+ ## Performance & Caching
188
365
 
189
366
  - Skip paths are checked before JWT processing
190
367
  - Low memory footprint
368
+ - Multi-tier permission caching system for RBAC performance
369
+ - TTL-based cache invalidation for user permissions
370
+ - Support for multiple cache stores: `:memory`, `:redis`, `:memcached`, `:solid_cache`
371
+
372
+ ### Cache Store Configuration
373
+
374
+ #### Supported Cache Stores
375
+
376
+ 1. **Memory Cache** (`:memory`) - For development and testing
377
+ 2. **Redis Cache** (`:redis`) - For production with high availability
378
+ 3. **Memcached Cache** (`:memcached`) - For distributed caching
379
+ 4. **Solid Cache** (`:solid_cache`) - For Rails 8+ applications
380
+
381
+ #### Configuration Examples
382
+
383
+ ```ruby
384
+ # Memory cache (development/testing)
385
+ config.cache_store = :memory
386
+
387
+ # Redis cache
388
+ config.cache_store = :redis
389
+ config.cache_options = { url: ENV['REDIS_URL'] }
390
+
391
+ # Memcached cache
392
+ config.cache_store = :memcached
393
+ config.cache_options = { servers: ['localhost:11211'] }
394
+
395
+ # Solid Cache (Rails 8+)
396
+ config.cache_store = :solid_cache
397
+ ```
398
+
399
+ #### Separate Cache Stores
400
+
401
+ You can configure separate cache stores for RBAC permissions data and cached user permissions:
402
+
403
+ ```ruby
404
+ # Use Redis for RBAC data (shared across instances)
405
+ config.rbac_cache_store = :redis
406
+ config.rbac_cache_options = { url: ENV['REDIS_URL'] }
407
+
408
+ # Use memory for user permission cache (faster local access)
409
+ config.permission_cache_store = :memory
410
+ config.permission_cache_options = {}
411
+ ```
412
+
413
+ ## RBAC Cache Format
414
+
415
+ When RBAC is enabled, the middleware expects permissions to be stored in the cache with this exact format:
416
+
417
+ ```json
418
+ {
419
+ "last_update": 1640995200,
420
+ "permissions": [
421
+ {
422
+ "123": [
423
+ "sales/invoices:get",
424
+ "sales/invoices:post",
425
+ "%r{sales/invoices/\\d+}:get",
426
+ "%r{sales/invoices/\\d+}:put",
427
+ "users/*:get"
428
+ ]
429
+ },
430
+ {
431
+ "456": ["admin/*:*", "reports:get"]
432
+ }
433
+ ]
434
+ }
435
+ ```
436
+
437
+ ### Format Specification
438
+
439
+ - **last_update**: Timestamp for cache invalidation
440
+ - **permissions**: Array of role permission objects
441
+ - **Role ID**: String or numeric identifier for user roles
442
+ - **Permission Format**: `"resource-endpoint:http-method"`
443
+ - **resource-endpoint**: API path (literal string or regex pattern)
444
+ - **http-method**: `get`, `post`, `put`, `delete`, or `*` (wildcard)
445
+
446
+ ### Permission Examples
447
+
448
+ ```ruby
449
+ # Literal path matching
450
+ "sales/invoices:get" # GET /api/v1/company/sales/invoices
451
+ "users/profile:put" # PUT /api/v1/company/users/profile
452
+
453
+ # Regex pattern matching
454
+ "%r{ sales/invoices/\\d+}:get" # GET /api/v1/company/sales/invoices/123
455
+ "%r{ users/\\d+/orders}:*" # Any method on /api/v1/company/users/123/orders
456
+
457
+ # Wildcard method
458
+ "reports:*" # Any method on reports endpoint
459
+ "admin/*:*" # Full admin access
460
+ ```
461
+
462
+ ### Request Authorization Flow
463
+
464
+ 1. **Check User Permissions Cache**: Fast lookup in middleware cache
465
+
466
+ - **RBAC Update Check**: If RBAC permissions updated within TTL → **Nuke entire cache**
467
+ - **TTL Check**: If individual permission older than configured TTL → Remove only that permission
468
+ - If cache valid and permission found: **✅ Authorized**
469
+ - If cache valid but no permission: **❌ 403 Forbidden**
470
+
471
+ 2. **RBAC Permissions Validation**: Full permission evaluation
472
+
473
+ - Extract user roles from JWT payload using configurable field mapping (default: `role_ids`)
474
+ - Fallback to common fields: `roles`, `role`, `user_roles`, or `role_ids` if mapped field not found
475
+ - Load RBAC permissions collection and validate format
476
+ - For each user role, check if any permission matches:
477
+ - Extract resource path from request URL (removes subdomain/pathname slug)
478
+ - Match against permission patterns (literal or regex)
479
+ - Validate HTTP method (exact match or wildcard)
480
+ - If authorized: Cache permission for future requests
481
+ - Return 403 Forbidden if no matching permissions found
482
+
483
+ 3. **Cache Storage**: Successful permissions cached with simple key-value format:
484
+ ```json
485
+ {
486
+ "12345:acme-group.localhost.local/api/v1/company/sales/invoices:get": 1640995200,
487
+ "12345:acme-group.localhost.local/api/v1/company/sales/invoices:post": 1640995200
488
+ }
489
+ ```
490
+ TTL configurable via `user_permissions_ttl` option (default: 30 minutes)
491
+
492
+ ### Cache Invalidation Strategy
493
+
494
+ **RBAC Collection Updated** (e.g., role permissions changed):
495
+
496
+ - **Condition**: RBAC `last_update` within configured TTL
497
+ - **Action**: **Nuke entire cache** (all users, all permissions)
498
+ - **Reason**: Any permission could have changed, safer to re-evaluate everything
499
+
500
+ **Individual Permission TTL Expired**:
501
+
502
+ - **Condition**: Specific permission older than configured TTL
503
+ - **Action**: **Remove only that permission** (preserve others)
504
+ - **Reason**: Permission naturally aged out, other permissions still valid
505
+
506
+ ### Example Authorization
507
+
508
+ Request: `POST https://acme-group.localhost/api/v1/an-acme-company/sales/invoices`
509
+
510
+ - User has role: `123`
511
+ - Role `123` permissions: `["sales/invoices:get", "sales/invoices:post"]`
512
+ - Extracted resource path: `sales/invoices`
513
+ - Request method: `POST`
514
+ - Result: **✅ Authorized** (matches `sales/invoices:post`)
515
+
516
+ Request: `DELETE https://acme-group.localhost/api/v1/an-acme-company/sales/invoices/456`
517
+
518
+ - User has role: `123`
519
+ - Role `123` permissions: `["sales/invoices:get", "%r{sales/invoices/\\d+}:put"]`
520
+ - Extracted resource path: `sales/invoices/456`
521
+ - Request method: `DELETE`
522
+ - Result: **❌ 403 Forbidden** (no DELETE permission)
191
523
 
192
524
  ## Error Handling
193
525
 
@@ -196,11 +528,34 @@ The middleware returns appropriate HTTP status codes:
196
528
  - **401 Unauthorized** - Missing, invalid, or expired JWT
197
529
  - **403 Forbidden** - Valid JWT but insufficient permissions/access
198
530
 
531
+ ## Documentation
532
+
533
+ API documentation is available online and is automatically generated from the source code:
534
+
535
+ - **📚 [Online Documentation](https://kanutocd.github.io/rack_jwt_aegis/)** - Auto-deployed from the main branch
536
+ - **🔧 Generate Locally**: `bundle exec yard doc` and open `doc/index.html`
537
+
538
+ ### Documentation Features
539
+
540
+ - Complete API reference for all classes and modules
541
+ - Code examples and usage patterns
542
+ - Architecture decision records (ADRs)
543
+ - Integration examples for common use cases
544
+
199
545
  ## Development
200
546
 
201
547
  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
548
 
203
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
549
+ To install this gem onto your local machine, run `bundle exec rake install`.
550
+
551
+ ### Test Coverage
552
+
553
+ This project maintains high test coverage:
554
+
555
+ - **Line Coverage**: 97.81% (670/685 lines)
556
+ - **Branch Coverage**: 87.13% (264/303 branches)
557
+
558
+ Run tests with coverage: `bundle exec rake test`
204
559
 
205
560
  ## Contributing
206
561
 
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
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here