reactive-actions 0.1.0.pre.alpha.2 β 0.1.0.pre.alpha.3
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/README.md +884 -152
- data/app/controllers/reactive_actions/reactive_actions_controller.rb +5 -1
- data/lib/generators/reactive_actions/install/install_generator.rb +122 -52
- data/lib/generators/reactive_actions/install/templates/initializer.rb +46 -1
- data/lib/reactive_actions/concerns/rate_limiter.rb +174 -0
- data/lib/reactive_actions/concerns/security_checks.rb +108 -0
- data/lib/reactive_actions/configuration.rb +23 -3
- data/lib/reactive_actions/controller/rate_limiter.rb +187 -0
- data/lib/reactive_actions/errors.rb +17 -0
- data/lib/reactive_actions/rate_limiter.rb +165 -0
- data/lib/reactive_actions/reactive_action.rb +5 -0
- data/lib/reactive_actions/version.rb +1 -1
- data/lib/reactive_actions.rb +5 -1
- metadata +6 -2
data/README.md
CHANGED
@@ -4,14 +4,14 @@ ReactiveActions is a Rails gem that provides a framework for handling reactive a
|
|
4
4
|
|
5
5
|
## π§ Status
|
6
6
|
|
7
|
-
This gem is currently in alpha (0.1.0-alpha.
|
7
|
+
This gem is currently in alpha (0.1.0-alpha.3). The API may change between versions.
|
8
8
|
|
9
9
|
## π¦ Installation
|
10
10
|
|
11
11
|
Add this line to your application's Gemfile:
|
12
12
|
|
13
13
|
```ruby
|
14
|
-
gem 'reactive-actions', '0.1.0-alpha.
|
14
|
+
gem 'reactive-actions', '0.1.0-alpha.3'
|
15
15
|
```
|
16
16
|
|
17
17
|
And then execute:
|
@@ -64,6 +64,17 @@ Add ReactiveActions JavaScript client? (y/n) y
|
|
64
64
|
β Added JavaScript client to importmap
|
65
65
|
β Added ReactiveActions import to app/javascript/application.js
|
66
66
|
|
67
|
+
Configure rate limiting? (optional but recommended for production) (y/n) y
|
68
|
+
Enable rate limiting features? (y/n) y
|
69
|
+
Enable global controller-level rate limiting? (recommended) (y/n) y
|
70
|
+
Global rate limit (requests per window): [600] 1000
|
71
|
+
Global rate limit window: [1.minute] 5.minutes
|
72
|
+
Configure custom rate limit key generator? (advanced) (y/n) n
|
73
|
+
β Rate limiting configured:
|
74
|
+
- Rate limiting: ENABLED
|
75
|
+
- Global rate limiting: ENABLED
|
76
|
+
- Global limit: 1000 requests per 5.minutes
|
77
|
+
|
67
78
|
Configure advanced options? (y/n) n
|
68
79
|
|
69
80
|
================================================================
|
@@ -85,18 +96,37 @@ $ rails generate reactive_actions:install --mount-path=/api/reactive
|
|
85
96
|
# Skip example action generation
|
86
97
|
$ rails generate reactive_actions:install --skip-example
|
87
98
|
|
99
|
+
# Enable rate limiting during installation
|
100
|
+
$ rails generate reactive_actions:install --enable-rate-limiting --enable-global-rate-limiting
|
101
|
+
|
102
|
+
# Configure rate limiting with custom limits
|
103
|
+
$ rails generate reactive_actions:install --enable-rate-limiting --global-rate-limit=1000 --global-rate-limit-window="5.minutes"
|
104
|
+
|
88
105
|
# Quiet installation with defaults
|
89
106
|
$ rails generate reactive_actions:install --quiet
|
90
107
|
```
|
91
108
|
|
92
109
|
### Available Options
|
93
110
|
|
111
|
+
**Basic Options:**
|
94
112
|
- `--skip-routes` - Skip adding routes to your application
|
95
113
|
- `--skip-javascript` - Skip adding JavaScript imports and setup
|
96
114
|
- `--skip-example` - Skip generating the example action file
|
97
115
|
- `--mount-path=PATH` - Specify custom mount path (default: `/reactive_actions`)
|
98
116
|
- `--quiet` - Run installation with minimal output and default settings
|
99
117
|
|
118
|
+
**JavaScript Client Options:**
|
119
|
+
- `--auto-initialize` - Auto-initialize ReactiveActions on page load (default: true)
|
120
|
+
- `--enable-dom-binding` - Enable automatic DOM binding (default: true)
|
121
|
+
- `--enable-mutation-observer` - Enable mutation observer for dynamic content (default: true)
|
122
|
+
- `--default-http-method=METHOD` - Default HTTP method for actions (default: 'POST')
|
123
|
+
|
124
|
+
**Rate Limiting Options:**
|
125
|
+
- `--enable-rate-limiting` - Enable rate limiting features
|
126
|
+
- `--enable-global-rate-limiting` - Enable global controller-level rate limiting
|
127
|
+
- `--global-rate-limit=NUMBER` - Global rate limit (requests per window, default: 600)
|
128
|
+
- `--global-rate-limit-window=DURATION` - Global rate limit window (default: '1.minute')
|
129
|
+
|
100
130
|
### What Gets Installed
|
101
131
|
|
102
132
|
The generator will:
|
@@ -106,6 +136,7 @@ The generator will:
|
|
106
136
|
- β
Add JavaScript to your `config/importmap.rb` (Rails 8 native)
|
107
137
|
- β
Automatically import ReactiveActions in your `application.js`
|
108
138
|
- β
Create an initializer file with configuration options
|
139
|
+
- β
Configure rate limiting settings (if enabled)
|
109
140
|
- β
Optionally configure advanced settings like custom delegated methods
|
110
141
|
|
111
142
|
## β‘ Rails 8 Native JavaScript Integration
|
@@ -544,173 +575,860 @@ class GenerateReportAction < ReactiveActions::ReactiveAction
|
|
544
575
|
end
|
545
576
|
```
|
546
577
|
|
547
|
-
##
|
578
|
+
## π Security Checks
|
548
579
|
|
549
|
-
|
580
|
+
ReactiveActions provides a comprehensive security system through the `SecurityChecks` module, allowing you to define custom security filters that run before your actions execute.
|
550
581
|
|
551
|
-
|
552
|
-
<!-- User List with Actions -->
|
553
|
-
<div class="user-list">
|
554
|
-
<% @users.each do |user| %>
|
555
|
-
<div class="user-card" id="user-<%= user.id %>">
|
556
|
-
<h3><%= user.name %></h3>
|
557
|
-
<p><%= user.email %></p>
|
558
|
-
|
559
|
-
<!-- Update user -->
|
560
|
-
<button reactive-action="click->put#update_user"
|
561
|
-
reactive-action-user-id="<%= user.id %>"
|
562
|
-
reactive-action-name="<%= user.name %>"
|
563
|
-
reactive-action-success="handleUserUpdate">
|
564
|
-
Quick Update
|
565
|
-
</button>
|
566
|
-
|
567
|
-
<!-- Delete user -->
|
568
|
-
<button reactive-action="click->delete#delete_user"
|
569
|
-
reactive-action-user-id="<%= user.id %>"
|
570
|
-
reactive-action-success="handleUserDelete"
|
571
|
-
class="danger">
|
572
|
-
Delete
|
573
|
-
</button>
|
574
|
-
|
575
|
-
<!-- Show preview on hover -->
|
576
|
-
<div reactive-action="mouseenter->get#show_user_preview mouseleave->post#hide_preview"
|
577
|
-
reactive-action-user-id="<%= user.id %>"
|
578
|
-
reactive-action-target="preview-<%= user.id %>">
|
579
|
-
<img src="<%= user.avatar %>" alt="Hover for details">
|
580
|
-
</div>
|
581
|
-
</div>
|
582
|
-
<% end %>
|
583
|
-
</div>
|
584
|
-
|
585
|
-
<!-- Live Search -->
|
586
|
-
<div class="search-container">
|
587
|
-
<input type="text"
|
588
|
-
reactive-action="input->get#search_users"
|
589
|
-
reactive-action-min-length="2"
|
590
|
-
reactive-action-success="updateSearchResults"
|
591
|
-
placeholder="Search users...">
|
592
|
-
|
593
|
-
<div id="search-results"></div>
|
594
|
-
</div>
|
595
|
-
|
596
|
-
<!-- Create User Form -->
|
597
|
-
<form reactive-action="submit->post#create_user"
|
598
|
-
reactive-action-success="handleUserCreate">
|
599
|
-
<input name="name" type="text" placeholder="Name" required>
|
600
|
-
<input name="email" type="email" placeholder="Email" required>
|
601
|
-
<button type="submit">Create User</button>
|
602
|
-
</form>
|
582
|
+
### Basic Security Checks
|
603
583
|
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
584
|
+
Add security checks to your actions using the `security_check` class method:
|
585
|
+
|
586
|
+
```ruby
|
587
|
+
# app/reactive_actions/protected_action.rb
|
588
|
+
class ProtectedAction < ReactiveActions::ReactiveAction
|
589
|
+
# Single security check
|
590
|
+
security_check :require_authentication
|
591
|
+
|
592
|
+
def action
|
593
|
+
@result = { message: "This action requires authentication" }
|
594
|
+
end
|
595
|
+
|
596
|
+
def response
|
597
|
+
render json: @result
|
598
|
+
end
|
599
|
+
|
600
|
+
private
|
601
|
+
|
602
|
+
def require_authentication
|
603
|
+
raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
|
604
|
+
end
|
605
|
+
end
|
606
|
+
```
|
607
|
+
|
608
|
+
### Multiple Security Checks
|
609
|
+
|
610
|
+
Chain multiple security checks for layered protection:
|
611
|
+
|
612
|
+
```ruby
|
613
|
+
# app/reactive_actions/admin_action.rb
|
614
|
+
class AdminAction < ReactiveActions::ReactiveAction
|
615
|
+
# Multiple security checks run in order
|
616
|
+
security_check :require_authentication
|
617
|
+
security_check :require_admin_role
|
618
|
+
|
619
|
+
def action
|
620
|
+
@result = { message: "Admin-only action executed successfully" }
|
621
|
+
end
|
622
|
+
|
623
|
+
def response
|
624
|
+
render json: @result
|
625
|
+
end
|
626
|
+
|
627
|
+
private
|
628
|
+
|
629
|
+
def require_authentication
|
630
|
+
raise ReactiveActions::SecurityCheckError, "Please log in" unless current_user
|
631
|
+
end
|
632
|
+
|
633
|
+
def require_admin_role
|
634
|
+
raise ReactiveActions::SecurityCheckError, "Admin access required" unless current_user.admin?
|
635
|
+
end
|
636
|
+
end
|
637
|
+
```
|
638
|
+
|
639
|
+
### Lambda-Based Security Checks
|
640
|
+
|
641
|
+
Use inline lambdas for simple or dynamic security checks:
|
642
|
+
|
643
|
+
```ruby
|
644
|
+
# app/reactive_actions/ownership_action.rb
|
645
|
+
class OwnershipAction < ReactiveActions::ReactiveAction
|
646
|
+
# Inline lambda security check
|
647
|
+
security_check -> {
|
648
|
+
raise ReactiveActions::SecurityCheckError, "Must be logged in" unless current_user
|
610
649
|
|
611
|
-
|
612
|
-
|
650
|
+
if action_params[:user_id].present?
|
651
|
+
unless current_user.id.to_s == action_params[:user_id].to_s
|
652
|
+
raise ReactiveActions::SecurityCheckError, "Can only access your own data"
|
653
|
+
end
|
654
|
+
end
|
613
655
|
}
|
614
|
-
}
|
615
656
|
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
657
|
+
def action
|
658
|
+
@result = { message: "Ownership check passed" }
|
659
|
+
end
|
660
|
+
|
661
|
+
def response
|
662
|
+
render json: @result
|
663
|
+
end
|
664
|
+
end
|
665
|
+
```
|
666
|
+
|
667
|
+
### Conditional Security Checks
|
668
|
+
|
669
|
+
Apply security checks conditionally using `:if`, `:unless`, `:only`, or `:except`:
|
670
|
+
|
671
|
+
```ruby
|
672
|
+
# app/reactive_actions/conditional_action.rb
|
673
|
+
class ConditionalAction < ReactiveActions::ReactiveAction
|
674
|
+
# Always require authentication
|
675
|
+
security_check :require_authentication
|
676
|
+
|
677
|
+
# Only require special access if special mode is enabled
|
678
|
+
security_check :require_special_access, if: -> { action_params[:special_mode] == "true" }
|
679
|
+
|
680
|
+
# Skip ownership check for admin users
|
681
|
+
security_check :require_ownership, unless: -> { current_user&.admin? }
|
682
|
+
|
683
|
+
def action
|
684
|
+
@result = { message: "Conditional security checks passed" }
|
685
|
+
end
|
686
|
+
|
687
|
+
def response
|
688
|
+
render json: @result
|
689
|
+
end
|
690
|
+
|
691
|
+
private
|
692
|
+
|
693
|
+
def require_authentication
|
694
|
+
raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
|
695
|
+
end
|
696
|
+
|
697
|
+
def require_special_access
|
698
|
+
unless current_user.special_access?
|
699
|
+
raise ReactiveActions::SecurityCheckError, "Special access required"
|
700
|
+
end
|
701
|
+
end
|
702
|
+
|
703
|
+
def require_ownership
|
704
|
+
resource_id = action_params[:resource_id]
|
705
|
+
resource = current_user.resources.find_by(id: resource_id)
|
706
|
+
raise ReactiveActions::SecurityCheckError, "Resource not found" unless resource
|
707
|
+
end
|
708
|
+
end
|
709
|
+
```
|
710
|
+
|
711
|
+
### Skipping Security Checks
|
712
|
+
|
713
|
+
For public actions that don't need any security checks:
|
714
|
+
|
715
|
+
```ruby
|
716
|
+
# app/reactive_actions/public_action.rb
|
717
|
+
class PublicAction < ReactiveActions::ReactiveAction
|
718
|
+
# Skip all security checks for this action
|
719
|
+
skip_security_checks
|
720
|
+
|
721
|
+
def action
|
722
|
+
@result = { message: "This is a public action" }
|
723
|
+
end
|
724
|
+
|
725
|
+
def response
|
726
|
+
render json: @result
|
727
|
+
end
|
728
|
+
end
|
729
|
+
```
|
730
|
+
|
731
|
+
### Security Check Options
|
732
|
+
|
733
|
+
The `security_check` method supports several options for fine-grained control:
|
734
|
+
|
735
|
+
```ruby
|
736
|
+
class ExampleAction < ReactiveActions::ReactiveAction
|
737
|
+
# Run only for specific actions (if you have multiple action methods)
|
738
|
+
security_check :check_method, only: [:create, :update]
|
739
|
+
|
740
|
+
# Skip for specific actions
|
741
|
+
security_check :check_method, except: [:index, :show]
|
742
|
+
|
743
|
+
# Conditional execution
|
744
|
+
security_check :check_method, if: :some_condition?
|
745
|
+
security_check :check_method, unless: :some_other_condition?
|
746
|
+
|
747
|
+
# Combine conditions
|
748
|
+
security_check :check_method, if: -> { params[:secure] == "true" }, unless: :development_mode?
|
749
|
+
|
750
|
+
private
|
751
|
+
|
752
|
+
def check_method
|
753
|
+
# Your security logic here
|
754
|
+
end
|
755
|
+
|
756
|
+
def some_condition?
|
757
|
+
# Your condition logic
|
758
|
+
end
|
759
|
+
|
760
|
+
def development_mode?
|
761
|
+
Rails.env.development?
|
762
|
+
end
|
763
|
+
end
|
764
|
+
```
|
765
|
+
|
766
|
+
### Security Error Handling
|
767
|
+
|
768
|
+
Security checks raise `ReactiveActions::SecurityCheckError` when they fail. This error is automatically caught and returned as a proper HTTP response:
|
769
|
+
|
770
|
+
```json
|
771
|
+
{
|
772
|
+
"success": false,
|
773
|
+
"error": {
|
774
|
+
"type": "SecurityCheckError",
|
775
|
+
"message": "Authentication required",
|
776
|
+
"code": "SECURITY_CHECK_FAILED"
|
623
777
|
}
|
624
778
|
}
|
779
|
+
```
|
780
|
+
|
781
|
+
### Real-World Security Examples
|
782
|
+
|
783
|
+
#### User Resource Access Control
|
784
|
+
```ruby
|
785
|
+
# app/reactive_actions/update_profile_action.rb
|
786
|
+
class UpdateProfileAction < ReactiveActions::ReactiveAction
|
787
|
+
security_check :require_authentication
|
788
|
+
security_check :verify_profile_ownership
|
789
|
+
|
790
|
+
def action
|
791
|
+
profile = current_user.profile
|
792
|
+
profile.update!(action_params.slice(:bio, :website, :location))
|
793
|
+
@result = { profile: profile.as_json }
|
794
|
+
end
|
795
|
+
|
796
|
+
def response
|
797
|
+
render json: @result
|
798
|
+
end
|
799
|
+
|
800
|
+
private
|
801
|
+
|
802
|
+
def require_authentication
|
803
|
+
raise ReactiveActions::SecurityCheckError, "Please log in" unless current_user
|
804
|
+
end
|
625
805
|
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
element.reset();
|
806
|
+
def verify_profile_ownership
|
807
|
+
profile_id = action_params[:profile_id]
|
808
|
+
return unless profile_id.present? # Skip check if no profile_id specified
|
630
809
|
|
631
|
-
|
632
|
-
|
810
|
+
unless current_user.profile.id.to_s == profile_id.to_s
|
811
|
+
raise ReactiveActions::SecurityCheckError, "Can only update your own profile"
|
812
|
+
end
|
813
|
+
end
|
814
|
+
end
|
815
|
+
```
|
816
|
+
|
817
|
+
#### Role-Based Access Control
|
818
|
+
```ruby
|
819
|
+
# app/reactive_actions/moderate_content_action.rb
|
820
|
+
class ModerateContentAction < ReactiveActions::ReactiveAction
|
821
|
+
security_check :require_authentication
|
822
|
+
security_check :require_moderator_role
|
823
|
+
|
824
|
+
def action
|
825
|
+
content = Content.find(action_params[:content_id])
|
826
|
+
content.update!(status: action_params[:status],
|
827
|
+
moderated_by: current_user.id)
|
828
|
+
@result = { content: content.as_json }
|
829
|
+
end
|
830
|
+
|
831
|
+
def response
|
832
|
+
render json: @result
|
833
|
+
end
|
834
|
+
|
835
|
+
private
|
836
|
+
|
837
|
+
def require_authentication
|
838
|
+
raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
|
839
|
+
end
|
840
|
+
|
841
|
+
def require_moderator_role
|
842
|
+
unless current_user.moderator? || current_user.admin?
|
843
|
+
raise ReactiveActions::SecurityCheckError, "Moderator access required"
|
844
|
+
end
|
845
|
+
end
|
846
|
+
end
|
847
|
+
```
|
848
|
+
|
849
|
+
#### API Key Validation
|
850
|
+
```ruby
|
851
|
+
# app/reactive_actions/api_action.rb
|
852
|
+
class ApiAction < ReactiveActions::ReactiveAction
|
853
|
+
security_check :validate_api_key
|
854
|
+
security_check :check_rate_limit
|
855
|
+
|
856
|
+
def action
|
857
|
+
@result = { data: "API response data" }
|
858
|
+
end
|
859
|
+
|
860
|
+
def response
|
861
|
+
render json: @result
|
862
|
+
end
|
863
|
+
|
864
|
+
private
|
865
|
+
|
866
|
+
def validate_api_key
|
867
|
+
api_key = action_params[:api_key] || controller.request.headers['X-API-Key']
|
633
868
|
|
634
|
-
|
635
|
-
|
636
|
-
|
869
|
+
unless api_key.present? && ApiKey.valid?(api_key)
|
870
|
+
raise ReactiveActions::SecurityCheckError, "Invalid or missing API key"
|
871
|
+
end
|
872
|
+
|
873
|
+
@api_key = ApiKey.find_by(key: api_key)
|
874
|
+
end
|
637
875
|
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
876
|
+
def check_rate_limit
|
877
|
+
return unless @api_key
|
878
|
+
|
879
|
+
if @api_key.rate_limit_exceeded?
|
880
|
+
raise ReactiveActions::SecurityCheckError, "Rate limit exceeded"
|
881
|
+
end
|
882
|
+
end
|
883
|
+
end
|
884
|
+
```
|
646
885
|
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
886
|
+
## π¦ Rate Limiting
|
887
|
+
|
888
|
+
ReactiveActions provides comprehensive rate limiting functionality to protect your application from abuse and ensure fair resource usage. Rate limiting is **disabled by default** and must be explicitly enabled in your configuration.
|
889
|
+
|
890
|
+
### π§ Configuration
|
891
|
+
|
892
|
+
Rate limiting is configured in your `config/initializers/reactive_actions.rb` file:
|
893
|
+
|
894
|
+
```ruby
|
895
|
+
ReactiveActions.configure do |config|
|
896
|
+
# Enable rate limiting functionality
|
897
|
+
config.rate_limiting_enabled = true
|
898
|
+
|
899
|
+
# Enable global controller-level rate limiting
|
900
|
+
config.global_rate_limiting_enabled = true
|
901
|
+
config.global_rate_limit = 600 # 600 requests per window
|
902
|
+
config.global_rate_limit_window = 1.minute # per minute
|
903
|
+
|
904
|
+
# Optional: Custom rate limit key generator
|
905
|
+
config.rate_limit_key_generator = ->(request, action_name) do
|
906
|
+
user_id = request.headers['X-User-ID'] || 'anonymous'
|
907
|
+
"#{action_name}:user:#{user_id}"
|
908
|
+
end
|
909
|
+
end
|
652
910
|
```
|
653
911
|
|
654
|
-
###
|
912
|
+
### Configuration Options
|
655
913
|
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
914
|
+
| Option | Default | Description |
|
915
|
+
|--------|---------|-------------|
|
916
|
+
| `rate_limiting_enabled` | `false` | Master switch for all rate limiting features |
|
917
|
+
| `global_rate_limiting_enabled` | `false` | Enable controller-level rate limiting |
|
918
|
+
| `global_rate_limit` | `600` | Global rate limit (requests per window) |
|
919
|
+
| `global_rate_limit_window` | `1.minute` | Time window for global rate limiting |
|
920
|
+
| `rate_limit_key_generator` | `nil` | Custom key generator proc |
|
921
|
+
|
922
|
+
### π― Action-Level Rate Limiting
|
923
|
+
|
924
|
+
Include the `RateLimiter` concern in your actions to add rate limiting functionality:
|
925
|
+
|
926
|
+
```ruby
|
927
|
+
# app/reactive_actions/api_action.rb
|
928
|
+
class ApiAction < ReactiveActions::ReactiveAction
|
929
|
+
include ReactiveActions::Concerns::RateLimiter
|
930
|
+
|
931
|
+
def action
|
932
|
+
# Basic rate limiting: 10 requests per minute per user
|
933
|
+
rate_limit!(key: "user:#{current_user&.id}", limit: 10, window: 1.minute)
|
934
|
+
|
935
|
+
@result = { data: "API response" }
|
936
|
+
end
|
937
|
+
|
938
|
+
def response
|
939
|
+
render json: @result
|
940
|
+
end
|
941
|
+
end
|
942
|
+
```
|
943
|
+
|
944
|
+
### π Key-Based Rate Limiting
|
945
|
+
|
946
|
+
Rate limiting works with different key strategies:
|
947
|
+
|
948
|
+
```ruby
|
949
|
+
class FlexibleRateLimitAction < ReactiveActions::ReactiveAction
|
950
|
+
include ReactiveActions::Concerns::RateLimiter
|
951
|
+
|
952
|
+
def action
|
953
|
+
case action_params[:rate_limit_type]
|
954
|
+
when 'user'
|
955
|
+
# User-specific rate limiting
|
956
|
+
rate_limit!(key: "user:#{current_user.id}", limit: 100, window: 1.hour)
|
957
|
+
|
958
|
+
when 'ip'
|
959
|
+
# IP-based rate limiting
|
960
|
+
rate_limit!(key: "ip:#{controller.request.remote_ip}", limit: 50, window: 15.minutes)
|
961
|
+
|
962
|
+
when 'api_key'
|
963
|
+
# API key-based rate limiting
|
964
|
+
api_key = action_params[:api_key]
|
965
|
+
rate_limit!(key: "api:#{api_key}", limit: 1000, window: 1.hour)
|
663
966
|
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
967
|
+
when 'global'
|
968
|
+
# Global rate limiting for expensive operations
|
969
|
+
rate_limit!(key: "global:expensive_operation", limit: 10, window: 1.minute)
|
970
|
+
end
|
971
|
+
|
972
|
+
@result = { message: "Rate limit check passed" }
|
973
|
+
end
|
974
|
+
|
975
|
+
def response
|
976
|
+
render json: @result
|
977
|
+
end
|
978
|
+
end
|
979
|
+
```
|
980
|
+
|
981
|
+
### π° Cost-Based Rate Limiting
|
982
|
+
|
983
|
+
Assign different costs to different operations:
|
984
|
+
|
985
|
+
```ruby
|
986
|
+
class CostBasedRateLimitAction < ReactiveActions::ReactiveAction
|
987
|
+
include ReactiveActions::Concerns::RateLimiter
|
988
|
+
|
989
|
+
def action
|
990
|
+
operation_type = action_params[:operation]
|
991
|
+
user_key = "user:#{current_user.id}"
|
992
|
+
|
993
|
+
case operation_type
|
994
|
+
when 'search'
|
995
|
+
# Light operation: cost 1
|
996
|
+
rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 1)
|
997
|
+
|
998
|
+
when 'export'
|
999
|
+
# Medium operation: cost 5
|
1000
|
+
rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 5)
|
1001
|
+
|
1002
|
+
when 'bulk_import'
|
1003
|
+
# Heavy operation: cost 20
|
1004
|
+
rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 20)
|
1005
|
+
|
1006
|
+
when 'report_generation'
|
1007
|
+
# Very heavy operation: cost 50
|
1008
|
+
rate_limit!(key: user_key, limit: 100, window: 1.minute, cost: 50)
|
1009
|
+
end
|
1010
|
+
|
1011
|
+
perform_operation(operation_type)
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
def response
|
1015
|
+
render json: @result
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
private
|
1019
|
+
|
1020
|
+
def perform_operation(type)
|
1021
|
+
@result = { operation: type, status: 'completed' }
|
1022
|
+
end
|
1023
|
+
end
|
1024
|
+
```
|
1025
|
+
|
1026
|
+
### π Rate Limiting Status and Management
|
1027
|
+
|
1028
|
+
Check and manage rate limiting status:
|
1029
|
+
|
1030
|
+
```ruby
|
1031
|
+
class RateLimitManagementAction < ReactiveActions::ReactiveAction
|
1032
|
+
include ReactiveActions::Concerns::RateLimiter
|
1033
|
+
|
1034
|
+
def action
|
1035
|
+
user_key = "user:#{current_user.id}"
|
1036
|
+
|
1037
|
+
case action_params[:action_type]
|
1038
|
+
when 'status'
|
1039
|
+
# Check current rate limit status without consuming a request
|
1040
|
+
status = rate_limit_status(key: user_key, limit: 100, window: 1.hour)
|
1041
|
+
@result = { rate_limit_status: status }
|
671
1042
|
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
1043
|
+
when 'check_would_exceed'
|
1044
|
+
# Check if a specific cost would exceed the limit
|
1045
|
+
cost = action_params[:cost] || 1
|
1046
|
+
would_exceed = rate_limit_would_exceed?(
|
1047
|
+
key: user_key,
|
1048
|
+
limit: 100,
|
1049
|
+
window: 1.hour,
|
1050
|
+
cost: cost
|
1051
|
+
)
|
1052
|
+
@result = { would_exceed: would_exceed, cost: cost }
|
679
1053
|
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
<img src="<%= product.image %>" alt="<%= product.name %>">
|
685
|
-
</div>
|
1054
|
+
when 'reset'
|
1055
|
+
# Reset rate limit for the user (admin functionality)
|
1056
|
+
reset_rate_limit!(key: user_key, window: 1.hour)
|
1057
|
+
@result = { message: "Rate limit reset for user", user_id: current_user.id }
|
686
1058
|
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
1059
|
+
when 'remaining'
|
1060
|
+
# Get remaining requests
|
1061
|
+
remaining = rate_limit_remaining(key: user_key, limit: 100, window: 1.hour)
|
1062
|
+
@result = { remaining: remaining }
|
1063
|
+
end
|
1064
|
+
end
|
1065
|
+
|
1066
|
+
def response
|
1067
|
+
render json: @result
|
1068
|
+
end
|
1069
|
+
end
|
1070
|
+
```
|
1071
|
+
|
1072
|
+
### π Global Controller-Level Rate Limiting
|
1073
|
+
|
1074
|
+
Enable global rate limiting across all ReactiveActions requests:
|
1075
|
+
|
1076
|
+
```ruby
|
1077
|
+
# config/initializers/reactive_actions.rb
|
1078
|
+
ReactiveActions.configure do |config|
|
1079
|
+
config.rate_limiting_enabled = true
|
1080
|
+
config.global_rate_limiting_enabled = true
|
1081
|
+
config.global_rate_limit = 600 # 10 requests per second
|
1082
|
+
config.global_rate_limit_window = 1.minute # per minute window
|
1083
|
+
end
|
1084
|
+
```
|
1085
|
+
|
1086
|
+
This automatically adds rate limiting to all ReactiveActions controller requests with appropriate headers:
|
1087
|
+
|
1088
|
+
```
|
1089
|
+
X-RateLimit-Limit: 600
|
1090
|
+
X-RateLimit-Remaining: 599
|
1091
|
+
X-RateLimit-Window: 60
|
1092
|
+
X-RateLimit-Reset: 1672531260
|
1093
|
+
Retry-After: 30 # (when rate limited)
|
1094
|
+
```
|
1095
|
+
|
1096
|
+
### ποΈ Advanced Rate Limiting Features
|
1097
|
+
|
1098
|
+
#### Scoped Keys
|
1099
|
+
```ruby
|
1100
|
+
class ScopedRateLimitAction < ReactiveActions::ReactiveAction
|
1101
|
+
include ReactiveActions::Concerns::RateLimiter
|
1102
|
+
|
1103
|
+
def action
|
1104
|
+
# Create scoped keys for different features
|
1105
|
+
api_key = rate_limit_key_for('api', identifier: current_user.id)
|
1106
|
+
search_key = rate_limit_key_for('search', identifier: current_user.id)
|
1107
|
+
upload_key = rate_limit_key_for('upload', identifier: current_user.id)
|
1108
|
+
|
1109
|
+
case action_params[:feature]
|
1110
|
+
when 'api'
|
1111
|
+
rate_limit!(key: api_key, limit: 1000, window: 1.hour)
|
1112
|
+
when 'search'
|
1113
|
+
rate_limit!(key: search_key, limit: 100, window: 1.minute)
|
1114
|
+
when 'upload'
|
1115
|
+
rate_limit!(key: upload_key, limit: 10, window: 1.minute)
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
@result = { feature: action_params[:feature], status: 'allowed' }
|
1119
|
+
end
|
1120
|
+
|
1121
|
+
def response
|
1122
|
+
render json: @result
|
1123
|
+
end
|
1124
|
+
end
|
1125
|
+
```
|
1126
|
+
|
1127
|
+
#### Custom Key Generators
|
1128
|
+
```ruby
|
1129
|
+
# config/initializers/reactive_actions.rb
|
1130
|
+
ReactiveActions.configure do |config|
|
1131
|
+
config.rate_limiting_enabled = true
|
1132
|
+
|
1133
|
+
# Custom key generator for sophisticated rate limiting
|
1134
|
+
config.rate_limit_key_generator = ->(request, action_name) do
|
1135
|
+
# Multi-factor key generation
|
1136
|
+
user_id = request.headers['X-User-ID']
|
1137
|
+
api_key = request.headers['X-API-Key']
|
1138
|
+
user_tier = request.headers['X-User-Tier'] || 'basic'
|
1139
|
+
|
1140
|
+
if api_key.present?
|
1141
|
+
# API requests get higher limits
|
1142
|
+
"api:#{api_key}:#{action_name}"
|
1143
|
+
elsif user_id.present?
|
1144
|
+
# User-based with tier consideration
|
1145
|
+
"user:#{user_tier}:#{user_id}:#{action_name}"
|
1146
|
+
else
|
1147
|
+
# Anonymous requests get IP-based limiting
|
1148
|
+
"ip:#{request.remote_ip}:#{action_name}"
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
end
|
1152
|
+
```
|
1153
|
+
|
1154
|
+
#### Rate Limiting with Security Integration
|
1155
|
+
```ruby
|
1156
|
+
class SecureRateLimitedAction < ReactiveActions::ReactiveAction
|
1157
|
+
include ReactiveActions::Concerns::RateLimiter
|
1158
|
+
|
1159
|
+
# Security checks run before rate limiting
|
1160
|
+
security_check :require_authentication
|
1161
|
+
|
1162
|
+
def action
|
1163
|
+
# Apply different limits based on user role
|
1164
|
+
limit = determine_user_limit
|
1165
|
+
window = determine_user_window
|
1166
|
+
|
1167
|
+
rate_limit!(
|
1168
|
+
key: "role:#{current_user.role}:#{current_user.id}",
|
1169
|
+
limit: limit,
|
1170
|
+
window: window
|
1171
|
+
)
|
1172
|
+
|
1173
|
+
perform_secure_operation
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
def response
|
1177
|
+
render json: @result
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
private
|
1181
|
+
|
1182
|
+
def require_authentication
|
1183
|
+
raise ReactiveActions::SecurityCheckError, "Authentication required" unless current_user
|
1184
|
+
end
|
1185
|
+
|
1186
|
+
def determine_user_limit
|
1187
|
+
case current_user.role
|
1188
|
+
when 'admin'
|
1189
|
+
1000 # Admins get higher limits
|
1190
|
+
when 'premium'
|
1191
|
+
500 # Premium users get medium limits
|
1192
|
+
when 'basic'
|
1193
|
+
100 # Basic users get standard limits
|
1194
|
+
else
|
1195
|
+
50 # Default for other roles
|
1196
|
+
end
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
def determine_user_window
|
1200
|
+
current_user.role == 'admin' ? 1.minute : 5.minutes
|
1201
|
+
end
|
1202
|
+
|
1203
|
+
def perform_secure_operation
|
1204
|
+
@result = {
|
1205
|
+
message: "Secure operation completed",
|
1206
|
+
user_role: current_user.role,
|
1207
|
+
rate_limit_applied: true
|
1208
|
+
}
|
1209
|
+
end
|
1210
|
+
end
|
1211
|
+
```
|
1212
|
+
|
1213
|
+
### ποΈ Custom Controller Rate Limiting
|
1214
|
+
|
1215
|
+
Add rate limiting to your own controllers:
|
1216
|
+
|
1217
|
+
```ruby
|
1218
|
+
class ApiController < ApplicationController
|
1219
|
+
include ReactiveActions::Controller::RateLimiter
|
708
1220
|
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
1221
|
+
# Rate limit specific actions
|
1222
|
+
rate_limit_action :show, limit: 100, window: 1.minute
|
1223
|
+
rate_limit_action :create, limit: 10, window: 1.minute, only: [:create]
|
1224
|
+
|
1225
|
+
# Skip rate limiting for certain actions
|
1226
|
+
skip_rate_limiting :health_check, :status
|
1227
|
+
|
1228
|
+
def show
|
1229
|
+
# This action is automatically rate limited
|
1230
|
+
render json: { data: "API response" }
|
1231
|
+
end
|
1232
|
+
|
1233
|
+
def create
|
1234
|
+
# This action has stricter rate limiting
|
1235
|
+
render json: { created: true }
|
1236
|
+
end
|
1237
|
+
|
1238
|
+
def health_check
|
1239
|
+
# This action skips rate limiting
|
1240
|
+
render json: { status: "ok" }
|
1241
|
+
end
|
1242
|
+
end
|
1243
|
+
```
|
1244
|
+
|
1245
|
+
### π Rate Limiting Monitoring and Logging
|
1246
|
+
|
1247
|
+
Monitor rate limiting events:
|
1248
|
+
|
1249
|
+
```ruby
|
1250
|
+
class MonitoredRateLimitAction < ReactiveActions::ReactiveAction
|
1251
|
+
include ReactiveActions::Concerns::RateLimiter
|
1252
|
+
|
1253
|
+
def action
|
1254
|
+
user_key = "user:#{current_user.id}"
|
1255
|
+
|
1256
|
+
begin
|
1257
|
+
# Log rate limiting attempt
|
1258
|
+
log_rate_limit_event('attempt', {
|
1259
|
+
user_id: current_user.id,
|
1260
|
+
action: 'api_call'
|
1261
|
+
})
|
1262
|
+
|
1263
|
+
rate_limit!(key: user_key, limit: 100, window: 1.hour)
|
1264
|
+
|
1265
|
+
# Log successful rate limit check
|
1266
|
+
log_rate_limit_event('success', {
|
1267
|
+
user_id: current_user.id,
|
1268
|
+
remaining: rate_limit_remaining(key: user_key, limit: 100, window: 1.hour)
|
1269
|
+
})
|
1270
|
+
|
1271
|
+
@result = { status: 'success' }
|
1272
|
+
|
1273
|
+
rescue ReactiveActions::RateLimitExceededError => e
|
1274
|
+
# Log rate limit exceeded
|
1275
|
+
log_rate_limit_event('exceeded', {
|
1276
|
+
user_id: current_user.id,
|
1277
|
+
limit: e.limit,
|
1278
|
+
current: e.current,
|
1279
|
+
retry_after: e.retry_after
|
1280
|
+
})
|
1281
|
+
|
1282
|
+
raise e
|
1283
|
+
end
|
1284
|
+
end
|
1285
|
+
|
1286
|
+
def response
|
1287
|
+
render json: @result
|
1288
|
+
end
|
1289
|
+
end
|
1290
|
+
```
|
1291
|
+
|
1292
|
+
### ποΈ Rate Limiting Configuration Options
|
1293
|
+
|
1294
|
+
#### Enable Rate Limiting During Installation
|
1295
|
+
|
1296
|
+
```bash
|
1297
|
+
# Enable rate limiting during installation
|
1298
|
+
$ rails generate reactive_actions:install --enable-rate-limiting --enable-global-rate-limiting --global-rate-limit=1000
|
1299
|
+
```
|
1300
|
+
|
1301
|
+
#### Runtime Configuration Checks
|
1302
|
+
|
1303
|
+
```ruby
|
1304
|
+
class ConditionalRateLimitAction < ReactiveActions::ReactiveAction
|
1305
|
+
include ReactiveActions::Concerns::RateLimiter
|
1306
|
+
|
1307
|
+
def action
|
1308
|
+
# Check if rate limiting is enabled before applying
|
1309
|
+
if rate_limiting_enabled?
|
1310
|
+
rate_limit!(key: "feature:#{action_params[:feature]}", limit: 50, window: 1.minute)
|
1311
|
+
@result = { rate_limiting: 'enabled', status: 'limited' }
|
1312
|
+
else
|
1313
|
+
@result = { rate_limiting: 'disabled', status: 'unlimited' }
|
1314
|
+
end
|
1315
|
+
end
|
1316
|
+
|
1317
|
+
def response
|
1318
|
+
render json: @result
|
1319
|
+
end
|
1320
|
+
end
|
1321
|
+
```
|
1322
|
+
|
1323
|
+
### β‘ Rate Limiting Error Handling
|
1324
|
+
|
1325
|
+
Rate limiting errors are automatically handled and return structured responses:
|
1326
|
+
|
1327
|
+
```json
|
1328
|
+
{
|
1329
|
+
"success": false,
|
1330
|
+
"error": {
|
1331
|
+
"type": "RateLimitExceededError",
|
1332
|
+
"message": "Rate limit exceeded: 101/100 requests in 1 minute",
|
1333
|
+
"code": "RATE_LIMIT_EXCEEDED",
|
1334
|
+
"limit": 100,
|
1335
|
+
"window": 60,
|
1336
|
+
"retry_after": 45
|
1337
|
+
}
|
1338
|
+
}
|
1339
|
+
```
|
1340
|
+
|
1341
|
+
### π Performance Considerations
|
1342
|
+
|
1343
|
+
Rate limiting uses Rails cache for storage:
|
1344
|
+
|
1345
|
+
- **Production**: Use Redis or Memcached for distributed caching
|
1346
|
+
- **Development**: Uses memory store automatically
|
1347
|
+
- **Test**: Uses memory store to avoid cache pollution
|
1348
|
+
|
1349
|
+
```ruby
|
1350
|
+
# config/environments/production.rb
|
1351
|
+
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
|
1352
|
+
```
|
1353
|
+
|
1354
|
+
### π§ Rate Limiting Best Practices
|
1355
|
+
|
1356
|
+
1. **Start Conservative**: Begin with generous limits and tighten based on usage patterns
|
1357
|
+
2. **Use Appropriate Windows**: Shorter windows (1-5 minutes) for responsive limiting
|
1358
|
+
3. **Different Limits for Different Operations**: Heavier operations should cost more
|
1359
|
+
4. **Monitor and Alert**: Set up monitoring for rate limit violations
|
1360
|
+
5. **Graceful Degradation**: Provide meaningful error messages and retry guidance
|
1361
|
+
6. **User Tier Consideration**: Different limits for different user tiers
|
1362
|
+
7. **API Documentation**: Document rate limits in your API documentation
|
1363
|
+
|
1364
|
+
## π» Simple DOM Binding Examples
|
1365
|
+
|
1366
|
+
### Basic Button Actions
|
1367
|
+
|
1368
|
+
```html
|
1369
|
+
<!-- Simple button click -->
|
1370
|
+
<button reactive-action="click->test">Test Action</button>
|
1371
|
+
|
1372
|
+
<!-- Button with data attributes -->
|
1373
|
+
<button reactive-action="click->update_status"
|
1374
|
+
reactive-action-status="active">
|
1375
|
+
Update Status
|
1376
|
+
</button>
|
1377
|
+
|
1378
|
+
<!-- Button with HTTP method -->
|
1379
|
+
<button reactive-action="click->delete#remove_item">Delete Item</button>
|
1380
|
+
```
|
1381
|
+
|
1382
|
+
### Form Examples
|
1383
|
+
|
1384
|
+
```html
|
1385
|
+
<!-- Simple form submission -->
|
1386
|
+
<form reactive-action="submit->create_item">
|
1387
|
+
<input name="title" type="text" required>
|
1388
|
+
<button type="submit">Create</button>
|
1389
|
+
</form>
|
1390
|
+
|
1391
|
+
<!-- Form with custom data -->
|
1392
|
+
<form reactive-action="submit->post#save_data"
|
1393
|
+
reactive-action-category="important">
|
1394
|
+
<input name="message" type="text" required>
|
1395
|
+
<button type="submit">Save</button>
|
1396
|
+
</form>
|
1397
|
+
```
|
1398
|
+
|
1399
|
+
### Input Events
|
1400
|
+
|
1401
|
+
```html
|
1402
|
+
<!-- Live search -->
|
1403
|
+
<input type="text"
|
1404
|
+
reactive-action="input->search"
|
1405
|
+
placeholder="Search...">
|
1406
|
+
|
1407
|
+
<!-- Select dropdown -->
|
1408
|
+
<select reactive-action="change->filter_results">
|
1409
|
+
<option value="all">All Items</option>
|
1410
|
+
<option value="active">Active Only</option>
|
1411
|
+
</select>
|
1412
|
+
```
|
1413
|
+
|
1414
|
+
### Success/Error Handling
|
1415
|
+
|
1416
|
+
```html
|
1417
|
+
<button reactive-action="click->test"
|
1418
|
+
reactive-action-success="showSuccess"
|
1419
|
+
reactive-action-error="showError">
|
1420
|
+
Test with Callbacks
|
1421
|
+
</button>
|
1422
|
+
|
1423
|
+
<script>
|
1424
|
+
function showSuccess(response) {
|
1425
|
+
alert('Success: ' + response.message);
|
1426
|
+
}
|
1427
|
+
|
1428
|
+
function showError(error) {
|
1429
|
+
alert('Error: ' + error.message);
|
1430
|
+
}
|
1431
|
+
</script>
|
714
1432
|
```
|
715
1433
|
|
716
1434
|
## Security
|
@@ -729,9 +1447,10 @@ ReactiveActions implements several security measures:
|
|
729
1447
|
```ruby
|
730
1448
|
# Always validate user permissions
|
731
1449
|
class SecureAction < ReactiveActions::ReactiveAction
|
1450
|
+
security_check :require_authentication
|
1451
|
+
security_check :validate_ownership
|
1452
|
+
|
732
1453
|
def action
|
733
|
-
raise ReactiveActions::UnauthorizedError unless current_user&.admin?
|
734
|
-
|
735
1454
|
# Validate and sanitize inputs
|
736
1455
|
user_id = action_params[:user_id].to_i
|
737
1456
|
raise ReactiveActions::InvalidParametersError if user_id <= 0
|
@@ -741,6 +1460,19 @@ class SecureAction < ReactiveActions::ReactiveAction
|
|
741
1460
|
|
742
1461
|
@result = User.find(user_id).update(permitted_params)
|
743
1462
|
end
|
1463
|
+
|
1464
|
+
private
|
1465
|
+
|
1466
|
+
def require_authentication
|
1467
|
+
raise ReactiveActions::SecurityCheckError unless current_user
|
1468
|
+
end
|
1469
|
+
|
1470
|
+
def validate_ownership
|
1471
|
+
user_id = action_params[:user_id].to_i
|
1472
|
+
unless current_user.id == user_id || current_user.admin?
|
1473
|
+
raise ReactiveActions::SecurityCheckError, "Access denied"
|
1474
|
+
end
|
1475
|
+
end
|
744
1476
|
end
|
745
1477
|
```
|
746
1478
|
|
@@ -765,6 +1497,8 @@ ReactiveActions provides structured error handling:
|
|
765
1497
|
- `InvalidParametersError` - Invalid parameter format
|
766
1498
|
- `UnauthorizedError` - Permission denied
|
767
1499
|
- `ActionExecutionError` - Runtime execution error
|
1500
|
+
- `SecurityCheckError` - Security check failed
|
1501
|
+
- `RateLimitExceededError` - Rate limit exceeded
|
768
1502
|
|
769
1503
|
## π Rails 8 Compatibility
|
770
1504
|
|
@@ -778,8 +1512,6 @@ Designed specifically for Rails 8:
|
|
778
1512
|
## πΊοΈ Roadmap & Future Improvements
|
779
1513
|
|
780
1514
|
Planned features:
|
781
|
-
- Security hooks for authentication/authorization
|
782
|
-
- Rate limiting and throttling
|
783
1515
|
- Enhanced error handling
|
784
1516
|
- Action composition for complex workflows
|
785
1517
|
- Built-in testing utilities
|