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.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/.cliff.toml +74 -0
  3. data/.github/workflows/ci.yml +2 -3
  4. data/.github/workflows/pages.yml +43 -0
  5. data/.github/workflows/release.yml +56 -0
  6. data/.standard.yml +4 -0
  7. data/CHANGELOG.md +48 -0
  8. data/CLAUDE.md +327 -0
  9. data/CONTRIBUTING.md +134 -0
  10. data/Gemfile +1 -0
  11. data/README.md +189 -133
  12. data/Rakefile +17 -2
  13. data/SECURITY.md +42 -349
  14. data/docs/architecture.md +83 -0
  15. data/docs/async-background-processing.md +64 -0
  16. data/docs/configuration.md +110 -287
  17. data/docs/factory-pattern.md +58 -0
  18. data/docs/gateway-context-variables.md +168 -0
  19. data/docs/gateway-development.md +159 -0
  20. data/docs/getting-started.md +90 -0
  21. data/docs/instrumentation.md +95 -175
  22. data/docs/platforms/instagram.md +278 -0
  23. data/docs/platforms/messenger.md +205 -0
  24. data/docs/platforms/telegram.md +109 -0
  25. data/docs/platforms/ussd.md +78 -0
  26. data/docs/platforms/whatsapp.md +147 -0
  27. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  28. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  29. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  30. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  31. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  32. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  33. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  34. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  35. data/docs/testing.md +33 -426
  36. data/examples/custom_session_id_example.rb +119 -0
  37. data/examples/http_controller.rb +22 -20
  38. data/examples/intercom_configuration_example.rb +113 -0
  39. data/examples/intercom_controller.rb +182 -0
  40. data/examples/multi_tenant_whatsapp_controller.rb +63 -168
  41. data/examples/simulator_controller.rb +0 -1
  42. data/examples/ussd_controller.rb +88 -160
  43. data/examples/whatsapp_controller.rb +18 -17
  44. data/examples/whatsapp_media_examples.rb +27 -79
  45. data/flow_chat.gemspec +4 -0
  46. data/lib/flow_chat/app.rb +211 -0
  47. data/lib/flow_chat/async_job.rb +176 -0
  48. data/lib/flow_chat/choice_titles.rb +95 -0
  49. data/lib/flow_chat/config.rb +126 -23
  50. data/lib/flow_chat/delivery_error.rb +9 -0
  51. data/lib/flow_chat/{base_executor.rb → executor.rb} +6 -11
  52. data/lib/flow_chat/factory.rb +94 -0
  53. data/lib/flow_chat/gateway_async_support.rb +106 -0
  54. data/lib/flow_chat/generic_async_job.rb +30 -0
  55. data/lib/flow_chat/http/configuration_error.rb +9 -0
  56. data/lib/flow_chat/http/gateway/simple.rb +104 -36
  57. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  58. data/lib/flow_chat/http/renderer.rb +3 -3
  59. data/lib/flow_chat/input.rb +86 -0
  60. data/lib/flow_chat/instagram/client.rb +32 -0
  61. data/lib/flow_chat/instagram/configuration.rb +147 -0
  62. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  63. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  64. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  65. data/lib/flow_chat/instagram/renderer.rb +23 -0
  66. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  67. data/lib/flow_chat/instrumentation/setup.rb +1 -1
  68. data/lib/flow_chat/instrumentation.rb +182 -0
  69. data/lib/flow_chat/intercom/client.rb +161 -0
  70. data/lib/flow_chat/intercom/configuration.rb +102 -0
  71. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  72. data/lib/flow_chat/intercom/gateway/intercom_api.rb +420 -0
  73. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  74. data/lib/flow_chat/intercom/renderer.rb +123 -0
  75. data/lib/flow_chat/media.rb +121 -0
  76. data/lib/flow_chat/messenger/client.rb +264 -0
  77. data/lib/flow_chat/messenger/configuration.rb +103 -0
  78. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  79. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  80. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  81. data/lib/flow_chat/messenger/renderer.rb +150 -0
  82. data/lib/flow_chat/meta/challenge.rb +24 -0
  83. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  84. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  85. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  86. data/lib/flow_chat/meta/messaging_gateway.rb +468 -0
  87. data/lib/flow_chat/meta/signature.rb +30 -0
  88. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  89. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  90. data/lib/flow_chat/named_configuration.rb +65 -0
  91. data/lib/flow_chat/phone_number_util.rb +37 -35
  92. data/lib/flow_chat/processor.rb +188 -0
  93. data/lib/flow_chat/prompt.rb +13 -16
  94. data/lib/flow_chat/renderers/markdown_support.rb +167 -0
  95. data/lib/flow_chat/security.rb +76 -0
  96. data/lib/flow_chat/session/middleware.rb +36 -11
  97. data/lib/flow_chat/simulator/controller.rb +31 -15
  98. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  99. data/lib/flow_chat/telegram/client.rb +283 -0
  100. data/lib/flow_chat/telegram/configuration.rb +78 -0
  101. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  102. data/lib/flow_chat/telegram/gateway/bot_api.rb +318 -0
  103. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +96 -0
  104. data/lib/flow_chat/telegram/renderer.rb +133 -0
  105. data/lib/flow_chat/telegram.rb +7 -0
  106. data/lib/flow_chat/text_truncator.rb +75 -0
  107. data/lib/flow_chat/ussd/gateway/nalo.rb +24 -4
  108. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  109. data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
  110. data/lib/flow_chat/ussd/renderer.rb +1 -1
  111. data/lib/flow_chat/version.rb +1 -1
  112. data/lib/flow_chat/whatsapp/client.rb +158 -20
  113. data/lib/flow_chat/whatsapp/configuration.rb +13 -52
  114. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  115. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +335 -248
  116. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +234 -0
  117. data/lib/flow_chat/whatsapp/renderer.rb +259 -64
  118. data/lib/flow_chat.rb +1 -1
  119. data/lib/tasks/release.rake +165 -0
  120. data/site/.nojekyll +0 -0
  121. data/site/.og-card.html +89 -0
  122. data/site/favicon.svg +6 -0
  123. data/site/index.html +209 -0
  124. data/site/og.png +0 -0
  125. metadata +132 -25
  126. data/docs/flows.md +0 -320
  127. data/docs/http-gateway-protocol.md +0 -432
  128. data/docs/images/simulator.png +0 -0
  129. data/docs/media.md +0 -153
  130. data/docs/sessions.md +0 -433
  131. data/docs/ussd-setup.md +0 -322
  132. data/docs/whatsapp-setup.md +0 -162
  133. data/examples/whatsapp_message_job.rb +0 -113
  134. data/lib/flow_chat/base_app.rb +0 -86
  135. data/lib/flow_chat/base_processor.rb +0 -146
  136. data/lib/flow_chat/http/app.rb +0 -6
  137. data/lib/flow_chat/http/middleware/executor.rb +0 -24
  138. data/lib/flow_chat/http/processor.rb +0 -33
  139. data/lib/flow_chat/session/rails_session_store.rb +0 -68
  140. data/lib/flow_chat/ussd/app.rb +0 -6
  141. data/lib/flow_chat/ussd/gateway/nsano.rb +0 -96
  142. data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
  143. data/lib/flow_chat/ussd/processor.rb +0 -39
  144. data/lib/flow_chat/whatsapp/app.rb +0 -29
  145. data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
  146. data/lib/flow_chat/whatsapp/processor.rb +0 -32
  147. data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
data/SECURITY.md CHANGED
@@ -1,365 +1,58 @@
1
- # FlowChat Security Guide
1
+ # Security Policy
2
2
 
3
- This guide covers the security features and best practices for FlowChat, including webhook signature validation and simulator authentication.
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
- ## Overview
8
+ ## Supported Versions
6
9
 
7
- FlowChat includes comprehensive security features to protect your WhatsApp and USSD applications:
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
- - **Webhook Signature Validation**: Verify that WhatsApp webhooks are authentic
10
- - **Simulator Authentication**: Secure access to the testing simulator
11
- - **Configuration Validation**: Prevent insecure configurations
12
- - **Environment-Specific Security**: Different security levels per environment
14
+ | Version | Supported |
15
+ | ------- | ------------------ |
16
+ | Latest release (`0.10.x`) | :white_check_mark: |
17
+ | Older releases | :x: |
13
18
 
14
- ## WhatsApp Webhook Security
19
+ ## Reporting a Vulnerability
15
20
 
16
- ### Signature Validation
21
+ **Please do not report security vulnerabilities through public GitHub issues,
22
+ discussions, or pull requests.**
17
23
 
18
- FlowChat automatically validates WhatsApp webhook signatures using HMAC-SHA256 to ensure requests come from WhatsApp.
24
+ Instead, use one of the following private channels:
19
25
 
20
- #### Required Configuration
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
- For webhook signature validation, you need to configure your WhatsApp app secret:
31
+ To help us triage quickly, please include as much of the following as you can:
23
32
 
24
- ```ruby
25
- # Using Rails credentials
26
- rails credentials:edit
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
- ```yaml
30
- whatsapp:
31
- app_secret: "your_whatsapp_app_secret"
32
- # ... other credentials
33
- ```
38
+ ## What to Expect
34
39
 
35
- Or using environment variables:
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
- ```bash
38
- export WHATSAPP_APP_SECRET="your_whatsapp_app_secret"
39
- ```
47
+ Please give us a reasonable opportunity to address the issue before any public
48
+ disclosure.
40
49
 
41
- #### Security Modes
50
+ ## Scope
42
51
 
43
- FlowChat supports two security modes for webhook validation:
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
- **1. Full Security (Recommended for Production)**
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)