flow_chat 0.8.2 → 0.10.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/.cliff.toml +74 -0
- data/.github/workflows/ci.yml +2 -3
- data/.github/workflows/pages.yml +43 -0
- data/.github/workflows/release.yml +56 -0
- data/.standard.yml +4 -0
- data/CHANGELOG.md +48 -0
- data/CLAUDE.md +327 -0
- data/CONTRIBUTING.md +134 -0
- data/Gemfile +1 -0
- data/README.md +189 -133
- data/Rakefile +17 -2
- data/SECURITY.md +42 -349
- data/docs/architecture.md +83 -0
- data/docs/async-background-processing.md +64 -0
- data/docs/configuration.md +110 -287
- data/docs/factory-pattern.md +58 -0
- data/docs/gateway-context-variables.md +168 -0
- data/docs/gateway-development.md +159 -0
- data/docs/getting-started.md +90 -0
- data/docs/instrumentation.md +95 -175
- data/docs/platforms/instagram.md +278 -0
- data/docs/platforms/messenger.md +205 -0
- data/docs/platforms/telegram.md +109 -0
- data/docs/platforms/ussd.md +78 -0
- data/docs/platforms/whatsapp.md +147 -0
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
- data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
- data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
- data/docs/testing.md +33 -426
- data/examples/custom_session_id_example.rb +119 -0
- data/examples/http_controller.rb +22 -20
- data/examples/intercom_configuration_example.rb +113 -0
- data/examples/intercom_controller.rb +182 -0
- data/examples/multi_tenant_whatsapp_controller.rb +63 -168
- data/examples/simulator_controller.rb +0 -1
- data/examples/ussd_controller.rb +88 -160
- data/examples/whatsapp_controller.rb +18 -17
- data/examples/whatsapp_media_examples.rb +27 -79
- data/flow_chat.gemspec +4 -0
- data/lib/flow_chat/app.rb +211 -0
- data/lib/flow_chat/async_job.rb +176 -0
- data/lib/flow_chat/choice_titles.rb +95 -0
- data/lib/flow_chat/config.rb +126 -23
- data/lib/flow_chat/delivery_error.rb +9 -0
- data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
- data/lib/flow_chat/factory.rb +94 -0
- data/lib/flow_chat/gateway_async_support.rb +106 -0
- data/lib/flow_chat/generic_async_job.rb +30 -0
- data/lib/flow_chat/http/configuration_error.rb +9 -0
- data/lib/flow_chat/http/gateway/simple.rb +104 -36
- data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
- data/lib/flow_chat/http/renderer.rb +3 -3
- data/lib/flow_chat/input.rb +86 -0
- data/lib/flow_chat/instagram/client.rb +32 -0
- data/lib/flow_chat/instagram/configuration.rb +147 -0
- data/lib/flow_chat/instagram/configuration_error.rb +7 -0
- data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
- data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
- data/lib/flow_chat/instagram/renderer.rb +23 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
- data/lib/flow_chat/instrumentation/setup.rb +1 -1
- data/lib/flow_chat/instrumentation.rb +182 -0
- data/lib/flow_chat/intercom/client.rb +161 -0
- data/lib/flow_chat/intercom/configuration.rb +102 -0
- data/lib/flow_chat/intercom/configuration_error.rb +9 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +420 -0
- data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
- data/lib/flow_chat/intercom/renderer.rb +123 -0
- data/lib/flow_chat/media.rb +121 -0
- data/lib/flow_chat/messenger/client.rb +264 -0
- data/lib/flow_chat/messenger/configuration.rb +103 -0
- data/lib/flow_chat/messenger/configuration_error.rb +9 -0
- data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
- data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
- data/lib/flow_chat/messenger/renderer.rb +150 -0
- data/lib/flow_chat/meta/challenge.rb +24 -0
- data/lib/flow_chat/meta/choice_ladder.rb +37 -0
- data/lib/flow_chat/meta/configuration_error.rb +7 -0
- data/lib/flow_chat/meta/gateway_identity.rb +38 -0
- data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
- data/lib/flow_chat/meta/signature.rb +30 -0
- data/lib/flow_chat/meta/signature_validation.rb +66 -0
- data/lib/flow_chat/meta/webhook_verification.rb +43 -0
- data/lib/flow_chat/named_configuration.rb +65 -0
- data/lib/flow_chat/phone_number_util.rb +37 -35
- data/lib/flow_chat/processor.rb +188 -0
- data/lib/flow_chat/prompt.rb +13 -16
- data/lib/flow_chat/renderers/markdown_support.rb +167 -0
- data/lib/flow_chat/security.rb +76 -0
- data/lib/flow_chat/session/middleware.rb +36 -11
- data/lib/flow_chat/simulator/controller.rb +31 -15
- data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
- data/lib/flow_chat/telegram/client.rb +283 -0
- data/lib/flow_chat/telegram/configuration.rb +78 -0
- data/lib/flow_chat/telegram/configuration_error.rb +9 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +318 -0
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +96 -0
- data/lib/flow_chat/telegram/renderer.rb +133 -0
- data/lib/flow_chat/telegram.rb +7 -0
- data/lib/flow_chat/text_truncator.rb +75 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
- data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +158 -20
- data/lib/flow_chat/whatsapp/configuration.rb +13 -52
- data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +335 -248
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +234 -0
- data/lib/flow_chat/whatsapp/renderer.rb +259 -64
- data/lib/flow_chat.rb +1 -1
- data/lib/tasks/release.rake +165 -0
- data/site/.nojekyll +0 -0
- data/site/.og-card.html +89 -0
- data/site/favicon.svg +6 -0
- data/site/index.html +209 -0
- data/site/og.png +0 -0
- metadata +132 -25
- data/docs/flows.md +0 -320
- data/docs/http-gateway-protocol.md +0 -432
- data/docs/images/simulator.png +0 -0
- data/docs/media.md +0 -153
- data/docs/sessions.md +0 -433
- data/docs/ussd-setup.md +0 -322
- data/docs/whatsapp-setup.md +0 -162
- data/examples/whatsapp_message_job.rb +0 -113
- data/lib/flow_chat/base_app.rb +0 -86
- data/lib/flow_chat/base_processor.rb +0 -146
- data/lib/flow_chat/http/app.rb +0 -6
- data/lib/flow_chat/http/middleware/executor.rb +0 -24
- data/lib/flow_chat/http/processor.rb +0 -33
- data/lib/flow_chat/session/rails_session_store.rb +0 -68
- data/lib/flow_chat/ussd/app.rb +0 -6
- data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
- data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
- data/lib/flow_chat/ussd/processor.rb +0 -39
- data/lib/flow_chat/whatsapp/app.rb +0 -29
- data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
- data/lib/flow_chat/whatsapp/processor.rb +0 -32
- data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
data/SECURITY.md
CHANGED
|
@@ -1,365 +1,58 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Security Policy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
We take the security of FlowChat seriously. Because FlowChat is a framework for
|
|
4
|
+
building conversational interfaces that other applications are built on, a
|
|
5
|
+
vulnerability here can affect many downstream apps. We appreciate your help in
|
|
6
|
+
disclosing issues responsibly.
|
|
4
7
|
|
|
5
|
-
##
|
|
8
|
+
## Supported Versions
|
|
6
9
|
|
|
7
|
-
FlowChat
|
|
10
|
+
FlowChat is pre-1.0 and evolving quickly. Security fixes are released against
|
|
11
|
+
the **latest published version** only. If you are running an older release,
|
|
12
|
+
please upgrade before reporting an issue to confirm it still reproduces.
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
| Version | Supported |
|
|
15
|
+
| ------- | ------------------ |
|
|
16
|
+
| Latest release (`0.10.x`) | :white_check_mark: |
|
|
17
|
+
| Older releases | :x: |
|
|
13
18
|
|
|
14
|
-
##
|
|
19
|
+
## Reporting a Vulnerability
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
**Please do not report security vulnerabilities through public GitHub issues,
|
|
22
|
+
discussions, or pull requests.**
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
Instead, use one of the following private channels:
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
- **Preferred:** [Open a private vulnerability report](https://github.com/radioactive-labs/flow_chat/security/advisories/new)
|
|
27
|
+
via GitHub Security Advisories.
|
|
28
|
+
- **Email:** [sfroelich01@gmail.com](mailto:sfroelich01@gmail.com) with the
|
|
29
|
+
subject line `[SECURITY] FlowChat`.
|
|
21
30
|
|
|
22
|
-
|
|
31
|
+
To help us triage quickly, please include as much of the following as you can:
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
- The FlowChat version (and Rails version) affected.
|
|
34
|
+
- A description of the vulnerability and its impact.
|
|
35
|
+
- Steps to reproduce, or a proof-of-concept.
|
|
36
|
+
- Any known workarounds.
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
whatsapp:
|
|
31
|
-
app_secret: "your_whatsapp_app_secret"
|
|
32
|
-
# ... other credentials
|
|
33
|
-
```
|
|
38
|
+
## What to Expect
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
- **Acknowledgement:** We aim to acknowledge your report within **3 business days**.
|
|
41
|
+
- **Assessment:** We will investigate and let you know whether the report is
|
|
42
|
+
accepted, along with our expected timeline for a fix.
|
|
43
|
+
- **Disclosure:** We follow a coordinated disclosure process. We will work with
|
|
44
|
+
you to agree on a disclosure date once a fix is available, and we are happy to
|
|
45
|
+
credit you in the advisory unless you prefer to remain anonymous.
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```
|
|
47
|
+
Please give us a reasonable opportunity to address the issue before any public
|
|
48
|
+
disclosure.
|
|
40
49
|
|
|
41
|
-
|
|
50
|
+
## Scope
|
|
42
51
|
|
|
43
|
-
FlowChat
|
|
52
|
+
Security reports about the FlowChat framework code in this repository are in
|
|
53
|
+
scope. Issues in applications *built with* FlowChat, or in third-party
|
|
54
|
+
dependencies, should be reported to their respective maintainers — though if you
|
|
55
|
+
believe a dependency issue is triggered by how FlowChat uses it, we would like
|
|
56
|
+
to hear about it.
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```ruby
|
|
48
|
-
config = FlowChat::Whatsapp::Configuration.new
|
|
49
|
-
config.app_secret = "your_whatsapp_app_secret" # Required
|
|
50
|
-
config.skip_signature_validation = false # Default: enforce validation
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
**2. Development Mode (Testing Only)**
|
|
54
|
-
|
|
55
|
-
```ruby
|
|
56
|
-
config = FlowChat::Whatsapp::Configuration.new
|
|
57
|
-
config.app_secret = nil # Not required when disabled
|
|
58
|
-
config.skip_signature_validation = true # Explicitly disable validation
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
⚠️ **Security Warning**: Never disable signature validation in production environments.
|
|
62
|
-
|
|
63
|
-
#### Configuration Error Handling
|
|
64
|
-
|
|
65
|
-
When `app_secret` is missing and validation is not explicitly disabled, FlowChat raises a `ConfigurationError`:
|
|
66
|
-
|
|
67
|
-
```ruby
|
|
68
|
-
begin
|
|
69
|
-
processor.run WelcomeFlow, :main_page
|
|
70
|
-
rescue FlowChat::Whatsapp::ConfigurationError => e
|
|
71
|
-
Rails.logger.error "Security configuration error: #{e.message}"
|
|
72
|
-
head :internal_server_error
|
|
73
|
-
end
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
The error message provides clear guidance:
|
|
77
|
-
|
|
78
|
-
```
|
|
79
|
-
WhatsApp app_secret is required for webhook signature validation.
|
|
80
|
-
Either configure app_secret or set skip_signature_validation=true to explicitly disable validation.
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Environment-Specific Security
|
|
84
|
-
|
|
85
|
-
Configure different security levels per environment:
|
|
86
|
-
|
|
87
|
-
```ruby
|
|
88
|
-
# config/initializers/flowchat.rb
|
|
89
|
-
case Rails.env
|
|
90
|
-
when 'development'
|
|
91
|
-
# Relaxed security for easier development
|
|
92
|
-
config.skip_signature_validation = true
|
|
93
|
-
|
|
94
|
-
when 'test'
|
|
95
|
-
# Skip validation for deterministic testing
|
|
96
|
-
config.skip_signature_validation = true
|
|
97
|
-
|
|
98
|
-
when 'staging', 'production'
|
|
99
|
-
# Full security for production-like environments
|
|
100
|
-
config.skip_signature_validation = false
|
|
101
|
-
|
|
102
|
-
# Ensure app_secret is configured
|
|
103
|
-
if ENV['WHATSAPP_APP_SECRET'].blank?
|
|
104
|
-
raise "WHATSAPP_APP_SECRET required for #{Rails.env} environment"
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Simulator Security
|
|
110
|
-
|
|
111
|
-
### Authentication System
|
|
112
|
-
|
|
113
|
-
The FlowChat simulator uses secure HMAC-SHA256 signed cookies for authentication. This prevents unauthorized access to your simulator endpoints.
|
|
114
|
-
|
|
115
|
-
#### Required Configuration
|
|
116
|
-
|
|
117
|
-
Configure a simulator secret in your initializer:
|
|
118
|
-
|
|
119
|
-
```ruby
|
|
120
|
-
# config/initializers/flowchat.rb
|
|
121
|
-
FlowChat::Config.simulator_secret = "your_secure_secret_here"
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
#### Environment-Specific Secrets
|
|
125
|
-
|
|
126
|
-
Use different secrets per environment:
|
|
127
|
-
|
|
128
|
-
```ruby
|
|
129
|
-
case Rails.env
|
|
130
|
-
when 'development', 'test'
|
|
131
|
-
# Use Rails secret key with environment suffix
|
|
132
|
-
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_#{Rails.env}"
|
|
133
|
-
|
|
134
|
-
when 'staging', 'production'
|
|
135
|
-
# Use environment variable for production
|
|
136
|
-
FlowChat::Config.simulator_secret = ENV['FLOWCHAT_SIMULATOR_SECRET']
|
|
137
|
-
|
|
138
|
-
if FlowChat::Config.simulator_secret.blank?
|
|
139
|
-
Rails.logger.warn "FLOWCHAT_SIMULATOR_SECRET not configured. Simulator will be unavailable."
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
#### Cookie Security
|
|
145
|
-
|
|
146
|
-
Simulator cookies are automatically configured with security best practices:
|
|
147
|
-
|
|
148
|
-
- **HMAC-SHA256 signed**: Prevents tampering
|
|
149
|
-
- **24-hour expiration**: Limits exposure window
|
|
150
|
-
- **Secure flag**: Only sent over HTTPS in production
|
|
151
|
-
- **HttpOnly flag**: Prevents XSS access
|
|
152
|
-
- **SameSite=Lax**: CSRF protection
|
|
153
|
-
|
|
154
|
-
#### Enabling Simulator Mode
|
|
155
|
-
|
|
156
|
-
Enable simulator mode only when needed:
|
|
157
|
-
|
|
158
|
-
```ruby
|
|
159
|
-
# Enable simulator only in development/staging
|
|
160
|
-
enable_simulator = Rails.env.development? || Rails.env.staging?
|
|
161
|
-
|
|
162
|
-
processor = FlowChat::Whatsapp::Processor.new(self, enable_simulator: enable_simulator) do |config|
|
|
163
|
-
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
164
|
-
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
165
|
-
end
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Simulator Request Flow
|
|
169
|
-
|
|
170
|
-
1. **User visits simulator**: Browser requests `/simulator`
|
|
171
|
-
2. **Cookie generation**: Server generates HMAC-signed cookie
|
|
172
|
-
3. **Simulator requests**: Include valid cookie for authentication
|
|
173
|
-
4. **Cookie validation**: Server validates HMAC signature and timestamp
|
|
174
|
-
5. **Request processing**: Continues if authentication succeeds
|
|
175
|
-
|
|
176
|
-
## Security Best Practices
|
|
177
|
-
|
|
178
|
-
### Production Checklist
|
|
179
|
-
|
|
180
|
-
✅ **WhatsApp Security**
|
|
181
|
-
- Configure `app_secret` for webhook validation
|
|
182
|
-
- Set `skip_signature_validation = false`
|
|
183
|
-
- Use environment variables for secrets
|
|
184
|
-
- Handle `ConfigurationError` exceptions
|
|
185
|
-
|
|
186
|
-
✅ **Simulator Security**
|
|
187
|
-
- Configure `simulator_secret` using environment variables
|
|
188
|
-
- Enable simulator only in development/staging
|
|
189
|
-
- Use unique secrets per environment
|
|
190
|
-
|
|
191
|
-
✅ **Environment Configuration**
|
|
192
|
-
- Different security levels per environment
|
|
193
|
-
- Fail fast on missing required configuration
|
|
194
|
-
- Log security warnings appropriately
|
|
195
|
-
|
|
196
|
-
✅ **Error Handling**
|
|
197
|
-
- Catch and log `ConfigurationError` exceptions
|
|
198
|
-
- Return appropriate HTTP status codes
|
|
199
|
-
- Don't expose sensitive information in errors
|
|
200
|
-
|
|
201
|
-
### Development Guidelines
|
|
202
|
-
|
|
203
|
-
**DO:**
|
|
204
|
-
- Use Rails `secret_key_base` + suffix for development secrets
|
|
205
|
-
- Skip webhook validation in development for easier testing
|
|
206
|
-
- Enable simulator mode for testing
|
|
207
|
-
- Use test-specific credentials in test environment
|
|
208
|
-
|
|
209
|
-
**DON'T:**
|
|
210
|
-
- Hardcode secrets in source code
|
|
211
|
-
- Disable security in production
|
|
212
|
-
- Use production secrets in development
|
|
213
|
-
- Commit secrets to version control
|
|
214
|
-
|
|
215
|
-
### Example Secure Configuration
|
|
216
|
-
|
|
217
|
-
```ruby
|
|
218
|
-
# config/initializers/flowchat.rb
|
|
219
|
-
case Rails.env
|
|
220
|
-
when 'development'
|
|
221
|
-
FlowChat::Config.simulator_secret = Rails.application.secret_key_base + "_dev"
|
|
222
|
-
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
|
223
|
-
|
|
224
|
-
when 'test'
|
|
225
|
-
FlowChat::Config.simulator_secret = "test_secret_#{Rails.application.secret_key_base}"
|
|
226
|
-
FlowChat::Config.whatsapp.message_handling_mode = :simulator
|
|
227
|
-
|
|
228
|
-
when 'staging'
|
|
229
|
-
FlowChat::Config.simulator_secret = ENV.fetch('FLOWCHAT_SIMULATOR_SECRET')
|
|
230
|
-
FlowChat::Config.whatsapp.message_handling_mode = :inline
|
|
231
|
-
|
|
232
|
-
when 'production'
|
|
233
|
-
FlowChat::Config.simulator_secret = ENV.fetch('FLOWCHAT_SIMULATOR_SECRET')
|
|
234
|
-
FlowChat::Config.whatsapp.message_handling_mode = :background
|
|
235
|
-
FlowChat::Config.whatsapp.background_job_class = 'WhatsappMessageJob'
|
|
236
|
-
end
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
## Testing Security Features
|
|
240
|
-
|
|
241
|
-
### Webhook Signature Validation Tests
|
|
242
|
-
|
|
243
|
-
```ruby
|
|
244
|
-
test "webhook accepts valid signature" do
|
|
245
|
-
payload = valid_webhook_payload.to_json
|
|
246
|
-
signature = OpenSSL::HMAC.hexdigest(
|
|
247
|
-
OpenSSL::Digest.new("sha256"),
|
|
248
|
-
"your_app_secret",
|
|
249
|
-
payload
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
post "/whatsapp/webhook",
|
|
253
|
-
params: payload,
|
|
254
|
-
headers: {
|
|
255
|
-
"Content-Type" => "application/json",
|
|
256
|
-
"X-Hub-Signature-256" => "sha256=#{signature}"
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
assert_response :success
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
test "webhook rejects invalid signature" do
|
|
263
|
-
post "/whatsapp/webhook",
|
|
264
|
-
params: valid_webhook_payload,
|
|
265
|
-
headers: { "X-Hub-Signature-256" => "sha256=invalid_signature" }
|
|
266
|
-
|
|
267
|
-
assert_response :unauthorized
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
test "webhook rejects missing signature" do
|
|
271
|
-
post "/whatsapp/webhook", params: valid_webhook_payload
|
|
272
|
-
assert_response :unauthorized
|
|
273
|
-
end
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### Simulator Authentication Tests
|
|
277
|
-
|
|
278
|
-
```ruby
|
|
279
|
-
test "simulator requires valid authentication" do
|
|
280
|
-
post "/whatsapp/webhook", params: {
|
|
281
|
-
simulator_mode: true,
|
|
282
|
-
# ... webhook payload
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
assert_response :unauthorized # No valid simulator cookie
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
test "simulator accepts valid authentication" do
|
|
289
|
-
# Generate valid simulator cookie
|
|
290
|
-
timestamp = Time.now.to_i
|
|
291
|
-
message = "simulator:#{timestamp}"
|
|
292
|
-
signature = OpenSSL::HMAC.hexdigest(
|
|
293
|
-
OpenSSL::Digest.new("sha256"),
|
|
294
|
-
FlowChat::Config.simulator_secret,
|
|
295
|
-
message
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
post "/whatsapp/webhook",
|
|
299
|
-
params: { simulator_mode: true, /* ... */ },
|
|
300
|
-
cookies: { flowchat_simulator: "#{timestamp}:#{signature}" }
|
|
301
|
-
|
|
302
|
-
assert_response :success
|
|
303
|
-
end
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
## Troubleshooting
|
|
307
|
-
|
|
308
|
-
### Common Issues
|
|
309
|
-
|
|
310
|
-
**1. ConfigurationError: app_secret required**
|
|
311
|
-
|
|
312
|
-
```
|
|
313
|
-
WhatsApp app_secret is required for webhook signature validation.
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
**Solution**: Configure `WHATSAPP_APP_SECRET` environment variable or disable validation explicitly.
|
|
317
|
-
|
|
318
|
-
**2. Invalid webhook signature**
|
|
319
|
-
|
|
320
|
-
```
|
|
321
|
-
Invalid webhook signature received
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
**Solution**: Verify your `app_secret` matches your WhatsApp app configuration.
|
|
325
|
-
|
|
326
|
-
**3. Simulator authentication failed**
|
|
327
|
-
|
|
328
|
-
```
|
|
329
|
-
Invalid simulator cookie format
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
**Solution**: Ensure `FlowChat::Config.simulator_secret` is properly configured.
|
|
333
|
-
|
|
334
|
-
**4. Missing simulator secret**
|
|
335
|
-
|
|
336
|
-
```
|
|
337
|
-
Simulator secret not configured
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
**Solution**: Set `FLOWCHAT_SIMULATOR_SECRET` environment variable or configure in initializer.
|
|
341
|
-
|
|
342
|
-
### Debug Mode
|
|
343
|
-
|
|
344
|
-
Enable debug logging for security events:
|
|
345
|
-
|
|
346
|
-
```ruby
|
|
347
|
-
# config/initializers/flowchat.rb
|
|
348
|
-
if Rails.env.development?
|
|
349
|
-
FlowChat::Config.logger.level = Logger::DEBUG
|
|
350
|
-
end
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
This will log security validation attempts and help troubleshoot configuration issues.
|
|
354
|
-
|
|
355
|
-
## Security Updates
|
|
356
|
-
|
|
357
|
-
This security system was introduced in FlowChat v2.0.0 and includes:
|
|
358
|
-
|
|
359
|
-
- HMAC-SHA256 webhook signature validation
|
|
360
|
-
- Secure simulator authentication with signed cookies
|
|
361
|
-
- Environment-specific security configuration
|
|
362
|
-
- Comprehensive error handling and logging
|
|
363
|
-
- Timing-attack resistant signature comparison
|
|
364
|
-
|
|
365
|
-
For the latest security updates and recommendations, check the FlowChat changelog and security advisories.
|
|
58
|
+
Thank you for helping keep FlowChat and its community safe.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
FlowChat turns a stateless webhook into a stateful conversation by re-running your flow method from the top on every request and replaying its answers out of a session. This document explains the pieces that make that work.
|
|
4
|
+
|
|
5
|
+
## The middleware stack
|
|
6
|
+
|
|
7
|
+
A `FlowChat::Processor` builds one middleware stack per request, in a fixed order. Each layer wraps the next and calls it:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Gateway
|
|
11
|
+
-> Session::Middleware
|
|
12
|
+
-> platform middleware (and your custom middleware)
|
|
13
|
+
-> Executor
|
|
14
|
+
-> your Flow
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The order is set in `Processor#create_middleware_stack`:
|
|
18
|
+
|
|
19
|
+
1. **Gateway** runs first. It parses the platform's webhook into normalized context values (`request.msisdn`, `context.input`, and so on) and, after the inner stack returns, renders the result back to the platform. See [gateway-development.md](gateway-development.md).
|
|
20
|
+
2. **`Session::Middleware`** computes the session id from the configured boundaries and identifier, and attaches the session store. See [configuration.md](configuration.md#sessions).
|
|
21
|
+
3. **Platform middleware** is inserted by the gateway if it defines `configure_middleware_stack`. USSD uses this to add pagination and choice-number mapping, and it is where your custom middleware runs. A gateway without that hook just runs your custom middleware here.
|
|
22
|
+
4. **Executor** runs last. Nothing runs after it. It instantiates your flow and calls the action.
|
|
23
|
+
|
|
24
|
+
## The Executor and control flow by exception
|
|
25
|
+
|
|
26
|
+
`FlowChat::Executor` builds a `FlowChat::App`, instantiates your flow with it, and calls the action method. Control flow is driven by exceptions raised from inside the flow and caught here:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
flow = flow_class.new(app)
|
|
30
|
+
flow.send(action)
|
|
31
|
+
raise FlowChat::Interrupt::Terminate, "Unexpected end of flow."
|
|
32
|
+
rescue FlowChat::Interrupt::RestartFlow
|
|
33
|
+
retry
|
|
34
|
+
rescue FlowChat::Interrupt::Prompt => e
|
|
35
|
+
[:prompt, e.prompt, e.choices, e.media]
|
|
36
|
+
rescue FlowChat::Interrupt::Terminate => e
|
|
37
|
+
context.session.destroy
|
|
38
|
+
[:terminal, e.prompt, nil, e.media]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The three interrupts live in `FlowChat::Interrupt`:
|
|
42
|
+
|
|
43
|
+
| Interrupt | Raised by | Effect |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `Prompt` | `prompt.ask` / `select` / `yes?` when no input is available | Unwinds to the Executor, which returns the prompt for the gateway to render. Carries the message, choices, and media. |
|
|
46
|
+
| `Terminate` | `app.say` and `prompt.say` | Ends the flow, destroys the session, and returns a terminal message. |
|
|
47
|
+
| `RestartFlow` | `app.go_back` | Caught with `retry`, which re-runs the flow from the top against a fresh `App`. |
|
|
48
|
+
|
|
49
|
+
If the action method returns normally without prompting or terminating, the Executor raises `Terminate` with "Unexpected end of flow", since a flow is expected to either prompt for more input or end with a message.
|
|
50
|
+
|
|
51
|
+
These interrupts subclass `Exception`, not `StandardError`. This is deliberate: a `rescue` in your flow code (a bare `rescue => e` catches `StandardError`) will not swallow a prompt or terminate and break the engine. Avoid `rescue Exception` inside a flow, since that would catch them.
|
|
52
|
+
|
|
53
|
+
## The replay model
|
|
54
|
+
|
|
55
|
+
There is no saved program counter between requests. Each webhook rebuilds the `App` and re-runs the flow method from the first line. Progress is reconstructed from the session:
|
|
56
|
+
|
|
57
|
+
- `app.screen(key)` checks the session for `key`. If a value is stored, it returns immediately without yielding the block. This fast-forwards through every screen already answered.
|
|
58
|
+
- The first screen without a stored answer yields a `Prompt`. If the current turn has input for it, the prompt validates and transforms it, stores the result with `session.set(key, value)`, and returns, so execution falls through to the next screen. If not, the prompt raises `Interrupt::Prompt` and the turn ends there.
|
|
59
|
+
|
|
60
|
+
Two rules follow. One inbound message is consumed by one screen per turn: once a screen takes the input, later screens in the same run see no input and prompt. And a given screen key may be presented only once per run; re-entering a key raises `ArgumentError`. To revisit a screen, use `app.go_back`, which clears the current screen's answer and raises `RestartFlow`.
|
|
61
|
+
|
|
62
|
+
## The App
|
|
63
|
+
|
|
64
|
+
`FlowChat::App` is the single object your flow talks to. It wraps the context and exposes:
|
|
65
|
+
|
|
66
|
+
- `screen(key)`, the unit of interaction.
|
|
67
|
+
- `say(msg, media:)`, to end the flow.
|
|
68
|
+
- `go_back`, to return to the previous screen.
|
|
69
|
+
- Read accessors for the turn: `text`, `media`, `location`, `contact`, `attachment_type`, and identity values `msisdn`, `user_id`, `platform`, `gateway`, `message_id`, `timestamp`, `contact_name`.
|
|
70
|
+
|
|
71
|
+
`Flow` itself is a thin base class: it stores the `app` and nothing else. All conversation logic lives in the methods you write.
|
|
72
|
+
|
|
73
|
+
## Sessions
|
|
74
|
+
|
|
75
|
+
The session is a key-value store keyed by the session id. `FlowChat::Session::CacheSessionStore` persists it in `FlowChat::Config.cache`, so a session outlives the request. `screen` reads and writes it, `Terminate` destroys it. Boundaries and identifiers, which decide what shares a session, are covered in [configuration.md](configuration.md#sessions).
|
|
76
|
+
|
|
77
|
+
## Async
|
|
78
|
+
|
|
79
|
+
Gateways with an outbound API can run the flow in a background job instead of inline. The gateway detects async support, serializes the request, and enqueues a job that reconstructs a controller and re-runs the same stack in the background. USSD cannot do this, since its protocol needs a synchronous response. See [async-background-processing.md](async-background-processing.md) and [factory-pattern.md](factory-pattern.md).
|
|
80
|
+
|
|
81
|
+
## Instrumentation
|
|
82
|
+
|
|
83
|
+
Each stage emits `ActiveSupport::Notifications` events (flow execution, messages received and sent, session lifecycle, pagination, webhook verification). Subscribe to feed metrics and logs into your own backend. See [instrumentation.md](instrumentation.md).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Async and background processing
|
|
2
|
+
|
|
3
|
+
A webhook has a response deadline. If a flow does slow work (calling the platform API, hitting your own services) the webhook can time out and the platform will retry, sometimes delivering the same message twice. Async processing acknowledges the webhook immediately and runs the flow in a background job.
|
|
4
|
+
|
|
5
|
+
This works on gateways with an outbound API (WhatsApp, Telegram, HTTP, Intercom), because the job can send the reply through that API afterward. USSD cannot use it: its protocol requires the answer in the webhook response itself.
|
|
6
|
+
|
|
7
|
+
## Enabling it
|
|
8
|
+
|
|
9
|
+
Call `use_async` in the processor. It has two forms.
|
|
10
|
+
|
|
11
|
+
Use a factory, and let FlowChat provide the job class:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
config.use_async(factory: :whatsapp)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or use your own ActiveJob subclass, with optional job params:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
config.use_async(MyFlowJob, deployment_id: 123)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The `factory:` form is the common one and needs no custom job class. See [factory-pattern.md](factory-pattern.md).
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
The `GatewayAsyncSupport` concern, mixed into every async-capable gateway, does the detection and enqueueing.
|
|
28
|
+
|
|
29
|
+
1. On a real webhook, the gateway calls `should_enqueue_async?`. It returns true when async is enabled, the gateway supports it (`async_supported?`), and the request is not already running in the background.
|
|
30
|
+
2. If so, the gateway serializes the request (params, method, headers, host, path, body, remote ip) and enqueues the job with `perform_later(request_context: ..., **job_params)`, then returns an immediate acknowledgement to the platform.
|
|
31
|
+
3. The job reconstructs a `FlowChat::BackgroundController` from the serialized request. It quacks like a Rails controller (its `render` and `head` are no-ops, its `request` is a `BackgroundRequest` rebuilt from the serialized data), so the same gateway code runs against it.
|
|
32
|
+
4. Running in the background, `should_enqueue_async?` now returns false (the controller is a `BackgroundController`), so the gateway processes the flow inline and sends the reply through the platform API.
|
|
33
|
+
|
|
34
|
+
The same gateway and the same flow run in both passes. The only difference is who calls them: the webhook the first time, the job the second.
|
|
35
|
+
|
|
36
|
+
## Custom jobs
|
|
37
|
+
|
|
38
|
+
`FlowChat::AsyncJob` is the base class. It handles reconstructing the controller in `perform`; you implement `execute(controller, **job_params)`:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
class MyFlowJob < FlowChat::AsyncJob
|
|
42
|
+
def execute(controller, **job_params)
|
|
43
|
+
deployment_id = job_params[:deployment_id]
|
|
44
|
+
processor = FlowChat::Processor.new(controller) do |config|
|
|
45
|
+
config.use_gateway FlowChat::Whatsapp::Gateway::CloudApi
|
|
46
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
|
47
|
+
config.use_async(MyFlowJob, deployment_id: deployment_id)
|
|
48
|
+
end
|
|
49
|
+
processor.run(WhatsAppFlow, :start)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The job params you pass to `use_async` are forwarded to `execute`, so the background pass can rebuild the same processor. `FlowChat::GenericAsyncJob` is exactly this pattern wrapped around a factory, which is why the `factory:` form needs no job class of your own.
|
|
55
|
+
|
|
56
|
+
## ActiveJob is optional
|
|
57
|
+
|
|
58
|
+
`FlowChat::AsyncJob` subclasses `ActiveJob::Base` when ActiveJob is available. Without ActiveJob, FlowChat falls back to a plain class, so requiring the gem does not fail; you supply the queueing yourself in that case.
|
|
59
|
+
|
|
60
|
+
## Related
|
|
61
|
+
|
|
62
|
+
- [Factory pattern](factory-pattern.md)
|
|
63
|
+
- [Configuration](configuration.md#async-processing)
|
|
64
|
+
- [Architecture](architecture.md)
|