hooksmith 0.1.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/README.md +319 -42
- data/lib/hooksmith/config/event_store.rb +47 -0
- data/lib/hooksmith/config/provider.rb +47 -0
- data/lib/hooksmith/configuration.rb +62 -23
- data/lib/hooksmith/dispatcher.rb +74 -29
- data/lib/hooksmith/errors.rb +240 -0
- data/lib/hooksmith/event_recorder.rb +54 -0
- data/lib/hooksmith/idempotency.rb +107 -0
- data/lib/hooksmith/instrumentation.rb +112 -0
- data/lib/hooksmith/jobs/dispatcher_job.rb +63 -0
- data/lib/hooksmith/rails/webhooks_controller.rb +152 -0
- data/lib/hooksmith/request.rb +110 -0
- data/lib/hooksmith/verifiers/base.rb +79 -0
- data/lib/hooksmith/verifiers/bearer_token.rb +79 -0
- data/lib/hooksmith/verifiers/hmac.rb +184 -0
- data/lib/hooksmith/version.rb +1 -1
- data/lib/hooksmith.rb +54 -1
- metadata +35 -4
- data/hooksmith-0.1.0.gem +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c87efd7d2887fc38d26d7615fbb913efaa086f09b0753b84479d41f87c4a4fd
|
|
4
|
+
data.tar.gz: 395f26ad9af550a7d64596edd3f23a486db879f8fc71aa9e2997a4e468eb785e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2d5953050726bec3601f37656b51a8d98f242e119058df1bca090568e9d99fd47dcde8d0788a4077dfc2dbe27f426a06d70ce85546edd36f0bbb80f91c1c237
|
|
7
|
+
data.tar.gz: 8e20347d959d17c8c03da4e969e06c5158ad9c083ae675aaaeeee86fb7cc8051ea11cdc780922d4c504b0775f2d754309aa97a6f0e587678dd8a96abe46bae89
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.0.0] - 2025-12-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Request Verification Interface** - Built-in verifiers for HMAC and Bearer token authentication
|
|
8
|
+
- `Hooksmith::Verifiers::Hmac` for HMAC-based signature verification
|
|
9
|
+
- `Hooksmith::Verifiers::BearerToken` for Bearer token authentication
|
|
10
|
+
- `Hooksmith::Verifiers::Base` for creating custom verifiers
|
|
11
|
+
- `Hooksmith::Request` wrapper for accessing headers and body
|
|
12
|
+
|
|
13
|
+
- **Error Taxonomy** - Explicit error classes for different failure modes
|
|
14
|
+
- `Hooksmith::Error` base class for all Hooksmith errors
|
|
15
|
+
- `Hooksmith::VerificationError` for signature verification failures
|
|
16
|
+
|
|
17
|
+
- **Idempotency Support** - Prevent duplicate webhook processing
|
|
18
|
+
- Configurable idempotency key extraction per provider
|
|
19
|
+
- Pre-built extractors for Stripe, GitHub, and generic webhooks
|
|
20
|
+
- `Hooksmith::Idempotency.extract_key` and `already_processed?` methods
|
|
21
|
+
|
|
22
|
+
- **ActiveJob Integration** - Process webhooks asynchronously
|
|
23
|
+
- `Hooksmith::Jobs::DispatcherJob` for background processing
|
|
24
|
+
- Automatic idempotency checking (can be skipped)
|
|
25
|
+
|
|
26
|
+
- **Instrumentation** - ActiveSupport::Notifications hooks for observability
|
|
27
|
+
- `dispatch.hooksmith` event wrapping the entire dispatch flow
|
|
28
|
+
- `process.hooksmith` event wrapping processor execution
|
|
29
|
+
- `no_processor.hooksmith` event when no processor matches
|
|
30
|
+
- `multiple_processors.hooksmith` event when multiple processors match
|
|
31
|
+
- `error.hooksmith` event when an error occurs
|
|
32
|
+
|
|
33
|
+
- **Rails Controller Concern** - Standardized webhook handling
|
|
34
|
+
- `Hooksmith::Rails::WebhooksController` concern
|
|
35
|
+
- `handle_webhook` for synchronous processing
|
|
36
|
+
- `handle_webhook_async` for background processing
|
|
37
|
+
- Automatic CSRF protection skip
|
|
38
|
+
- Optional signature verification integration
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- Internal storage now uses strings instead of symbols to prevent Symbol DoS attacks
|
|
43
|
+
- `MultipleProcessorsError` no longer includes full payload in error message to prevent PII exposure
|
|
44
|
+
|
|
45
|
+
### Security
|
|
46
|
+
|
|
47
|
+
- Fixed Symbol DoS vulnerability in Dispatcher (provider/event were converted to symbols from untrusted input)
|
|
48
|
+
- Fixed PII exposure in `MultipleProcessorsError` (full payload was included in error message)
|
|
49
|
+
|
|
50
|
+
## [0.2.0] - 2025-03-15
|
|
51
|
+
|
|
52
|
+
- Added event store for persisting webhook events
|
|
53
|
+
- Added configurable mapper for event persistence
|
|
54
|
+
|
|
3
55
|
## [0.1.0] - 2025-03-12
|
|
4
56
|
|
|
5
57
|
- Initial release
|
data/README.md
CHANGED
|
@@ -6,16 +6,21 @@
|
|
|
6
6
|
|
|
7
7
|
- **DSL for Registration:** Group processors by provider and event.
|
|
8
8
|
- **Flexible Dispatcher:** Dynamically selects the appropriate processor based on payload conditions.
|
|
9
|
+
- **Request Verification:** Built-in support for HMAC and Bearer token authentication.
|
|
10
|
+
- **Idempotency Support:** Prevent duplicate webhook processing with configurable key extraction.
|
|
11
|
+
- **ActiveJob Integration:** Process webhooks asynchronously with automatic idempotency checks.
|
|
12
|
+
- **Instrumentation:** ActiveSupport::Notifications hooks for observability.
|
|
13
|
+
- **Rails Controller Concern:** Standardized webhook handling with consistent response codes.
|
|
9
14
|
- **Rails Integration:** Automatically configures with Rails using a Railtie.
|
|
10
15
|
- **Lightweight Logging:** Built-in logging that can be switched to `Rails.logger` when in a Rails environment.
|
|
11
|
-
- **Tested with Minitest:**
|
|
16
|
+
- **Tested with Minitest:** Comprehensive test coverage for robust behavior.
|
|
12
17
|
|
|
13
18
|
## Installation
|
|
14
19
|
|
|
15
20
|
Add this line to your application's Gemfile:
|
|
16
21
|
|
|
17
22
|
```ruby
|
|
18
|
-
gem 'hooksmith', '~>
|
|
23
|
+
gem 'hooksmith', '~> 1.0'
|
|
19
24
|
```
|
|
20
25
|
|
|
21
26
|
Then execute:
|
|
@@ -29,91 +34,363 @@ Or install it yourself as:
|
|
|
29
34
|
gem install hooksmith
|
|
30
35
|
```
|
|
31
36
|
|
|
32
|
-
##
|
|
37
|
+
## Quick Start
|
|
33
38
|
|
|
34
|
-
###
|
|
39
|
+
### 1. Configure Providers and Processors
|
|
35
40
|
|
|
36
41
|
Configure your webhook processors in an initializer (e.g., `config/initializers/hooksmith.rb`):
|
|
37
42
|
|
|
38
43
|
```ruby
|
|
39
44
|
Hooksmith.configure do |config|
|
|
40
45
|
config.provider(:stripe) do |stripe|
|
|
41
|
-
stripe.register(:charge_succeeded, 'Stripe::
|
|
42
|
-
stripe.register(:
|
|
46
|
+
stripe.register(:charge_succeeded, 'Stripe::ChargeSucceededProcessor')
|
|
47
|
+
stripe.register(:payment_failed, 'Stripe::PaymentFailedProcessor')
|
|
43
48
|
end
|
|
44
49
|
|
|
45
|
-
config.provider(:
|
|
46
|
-
|
|
50
|
+
config.provider(:github) do |github|
|
|
51
|
+
github.register(:push, 'Github::PushProcessor')
|
|
52
|
+
github.register(:pull_request, 'Github::PullRequestProcessor')
|
|
47
53
|
end
|
|
48
54
|
end
|
|
49
55
|
```
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
### 2. Create a Processor
|
|
58
|
+
|
|
52
59
|
Create a processor by inheriting from `Hooksmith::Processor::Base`:
|
|
53
60
|
|
|
54
61
|
```ruby
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# Only handle events with a tenant_payment_id.
|
|
60
|
-
def can_handle?(payload)
|
|
61
|
-
payload.dig("metadata", "id").present?
|
|
62
|
-
end
|
|
62
|
+
class Stripe::ChargeSucceededProcessor < Hooksmith::Processor::Base
|
|
63
|
+
def can_handle?(payload)
|
|
64
|
+
payload.dig('data', 'object', 'status') == 'succeeded'
|
|
65
|
+
end
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
puts "Processed id : #{id}"
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
67
|
+
def process!
|
|
68
|
+
charge_id = payload.dig('data', 'object', 'id')
|
|
69
|
+
Payment.find_by(stripe_charge_id: charge_id)&.mark_as_paid!
|
|
71
70
|
end
|
|
72
71
|
end
|
|
73
72
|
```
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
Use the dispatcher in your webhook controller:
|
|
74
|
+
### 3. Handle Webhooks in Your Controller
|
|
78
75
|
|
|
79
76
|
```ruby
|
|
80
77
|
class WebhooksController < ApplicationController
|
|
81
78
|
skip_before_action :verify_authenticity_token
|
|
82
79
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
def stripe
|
|
81
|
+
Hooksmith::Dispatcher.new(
|
|
82
|
+
provider: :stripe,
|
|
83
|
+
event: params[:type],
|
|
84
|
+
payload: params.to_unsafe_h
|
|
85
|
+
).run!
|
|
87
86
|
|
|
88
|
-
Hooksmith::Dispatcher.new(provider: provider, event: event, payload: payload).run!
|
|
89
87
|
head :ok
|
|
90
|
-
rescue Hooksmith::MultipleProcessorsError => e
|
|
91
|
-
render json: { error: e.message }, status: :unprocessable_entity
|
|
92
88
|
rescue StandardError => e
|
|
93
|
-
|
|
89
|
+
head :internal_server_error
|
|
94
90
|
end
|
|
95
91
|
end
|
|
96
92
|
```
|
|
97
93
|
|
|
98
|
-
##
|
|
99
|
-
|
|
94
|
+
## Request Verification
|
|
95
|
+
|
|
96
|
+
Hooksmith provides built-in verifiers for common authentication patterns.
|
|
97
|
+
|
|
98
|
+
### HMAC Verification
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
Hooksmith.configure do |config|
|
|
102
|
+
config.provider(:stripe) do |stripe|
|
|
103
|
+
stripe.verifier = Hooksmith::Verifiers::Hmac.new(
|
|
104
|
+
secret: ENV['STRIPE_WEBHOOK_SECRET'],
|
|
105
|
+
header: 'Stripe-Signature',
|
|
106
|
+
algorithm: :sha256
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Bearer Token Verification
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
Hooksmith.configure do |config|
|
|
116
|
+
config.provider(:internal) do |internal|
|
|
117
|
+
internal.verifier = Hooksmith::Verifiers::BearerToken.new(
|
|
118
|
+
token: ENV['WEBHOOK_SECRET_TOKEN'],
|
|
119
|
+
header: 'Authorization'
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Custom Verifier
|
|
126
|
+
|
|
127
|
+
Create your own verifier by inheriting from `Hooksmith::Verifiers::Base`:
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
class MyCustomVerifier < Hooksmith::Verifiers::Base
|
|
131
|
+
def verify!(request)
|
|
132
|
+
signature = request.headers['X-Custom-Signature']
|
|
133
|
+
expected = compute_signature(request.body)
|
|
134
|
+
|
|
135
|
+
raise Hooksmith::VerificationError, 'Invalid signature' unless secure_compare(signature, expected)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def compute_signature(body)
|
|
141
|
+
OpenSSL::HMAC.hexdigest('SHA256', @secret, body)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Idempotency Support
|
|
147
|
+
|
|
148
|
+
Prevent duplicate webhook processing by configuring idempotency key extraction:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
Hooksmith.configure do |config|
|
|
152
|
+
config.provider(:stripe) do |stripe|
|
|
153
|
+
stripe.idempotency_key = ->(payload) { payload.dig('id') }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
config.provider(:github) do |github|
|
|
157
|
+
github.idempotency_key = Hooksmith::Idempotency::GITHUB
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Pre-built Extractors
|
|
163
|
+
|
|
164
|
+
Hooksmith includes pre-built extractors for common providers:
|
|
165
|
+
|
|
166
|
+
- `Hooksmith::Idempotency::STRIPE` - Extracts from `id` field
|
|
167
|
+
- `Hooksmith::Idempotency::GITHUB` - Extracts from `X-GitHub-Delivery` header
|
|
168
|
+
- `Hooksmith::Idempotency::GENERIC` - Extracts from `id`, `event_id`, or `request_id`
|
|
169
|
+
|
|
170
|
+
### Checking for Duplicates
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
key = Hooksmith::Idempotency.extract_key(provider: 'stripe', payload: params)
|
|
174
|
+
if Hooksmith::Idempotency.already_processed?(provider: 'stripe', key: key)
|
|
175
|
+
head :ok
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## ActiveJob Integration
|
|
100
181
|
|
|
182
|
+
Process webhooks asynchronously with automatic idempotency checking:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
class WebhooksController < ApplicationController
|
|
186
|
+
def stripe
|
|
187
|
+
Hooksmith::Jobs::DispatcherJob.perform_later(
|
|
188
|
+
provider: 'stripe',
|
|
189
|
+
event: params[:type],
|
|
190
|
+
payload: params.to_unsafe_h
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
head :ok
|
|
194
|
+
end
|
|
195
|
+
end
|
|
101
196
|
```
|
|
197
|
+
|
|
198
|
+
Skip idempotency checking if needed:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
Hooksmith::Jobs::DispatcherJob.perform_later(
|
|
202
|
+
provider: 'stripe',
|
|
203
|
+
event: params[:type],
|
|
204
|
+
payload: params.to_unsafe_h,
|
|
205
|
+
skip_idempotency_check: true
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Rails Controller Concern
|
|
210
|
+
|
|
211
|
+
Use the built-in controller concern for standardized webhook handling:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
class WebhooksController < ApplicationController
|
|
215
|
+
include Hooksmith::Rails::WebhooksController
|
|
216
|
+
|
|
217
|
+
def stripe
|
|
218
|
+
handle_webhook(
|
|
219
|
+
provider: 'stripe',
|
|
220
|
+
event: params[:type],
|
|
221
|
+
payload: params.to_unsafe_h
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def github
|
|
226
|
+
handle_webhook_async(
|
|
227
|
+
provider: 'github',
|
|
228
|
+
event: request.headers['X-GitHub-Event'],
|
|
229
|
+
payload: params.to_unsafe_h
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The concern provides:
|
|
236
|
+
- Automatic CSRF protection skip
|
|
237
|
+
- Optional signature verification (if configured)
|
|
238
|
+
- Consistent response codes (200, 401, 500)
|
|
239
|
+
- `handle_webhook` for synchronous processing
|
|
240
|
+
- `handle_webhook_async` for background processing
|
|
241
|
+
|
|
242
|
+
## Instrumentation
|
|
243
|
+
|
|
244
|
+
Hooksmith emits ActiveSupport::Notifications events for observability:
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
ActiveSupport::Notifications.subscribe(/hooksmith/) do |name, start, finish, id, payload|
|
|
248
|
+
Rails.logger.info "#{name}: #{payload.inspect} (#{finish - start}s)"
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Available Events
|
|
253
|
+
|
|
254
|
+
| Event | Description |
|
|
255
|
+
|-------|-------------|
|
|
256
|
+
| `dispatch.hooksmith` | Wraps the entire dispatch flow |
|
|
257
|
+
| `process.hooksmith` | Wraps processor execution |
|
|
258
|
+
| `no_processor.hooksmith` | When no processor matches |
|
|
259
|
+
| `multiple_processors.hooksmith` | When multiple processors match |
|
|
260
|
+
| `error.hooksmith` | When an error occurs |
|
|
261
|
+
|
|
262
|
+
### Subscribing to Specific Events
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
Hooksmith::Instrumentation.subscribe('process') do |name, start, finish, id, payload|
|
|
266
|
+
StatsD.timing("webhooks.#{payload[:provider]}.#{payload[:event]}", finish - start)
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Persisting Webhook Events
|
|
271
|
+
|
|
272
|
+
Hooksmith can optionally persist incoming webhook events to your database:
|
|
273
|
+
|
|
274
|
+
### 1. Create a Model
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
class WebhookEvent < ApplicationRecord
|
|
278
|
+
self.table_name = 'webhook_events'
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 2. Create a Migration
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
create_table :webhook_events do |t|
|
|
286
|
+
t.string :provider
|
|
287
|
+
t.string :event
|
|
288
|
+
t.jsonb :payload
|
|
289
|
+
t.string :idempotency_key
|
|
290
|
+
t.datetime :received_at
|
|
291
|
+
t.timestamps
|
|
292
|
+
|
|
293
|
+
t.index [:provider, :idempotency_key], unique: true
|
|
294
|
+
t.index :event
|
|
295
|
+
t.index :received_at
|
|
296
|
+
end
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 3. Configure the Event Store
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
Hooksmith.configure do |config|
|
|
303
|
+
config.event_store do |store|
|
|
304
|
+
store.enabled = true
|
|
305
|
+
store.model_class_name = 'WebhookEvent'
|
|
306
|
+
store.record_timing = :before
|
|
307
|
+
store.mapper = ->(provider:, event:, payload:) {
|
|
308
|
+
{
|
|
309
|
+
provider: provider.to_s,
|
|
310
|
+
event: event.to_s,
|
|
311
|
+
payload: payload,
|
|
312
|
+
received_at: Time.current
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Error Handling
|
|
320
|
+
|
|
321
|
+
Hooksmith provides specific error classes for different failure modes:
|
|
322
|
+
|
|
323
|
+
| Error Class | Description |
|
|
324
|
+
|-------------|-------------|
|
|
325
|
+
| `Hooksmith::Error` | Base error class |
|
|
326
|
+
| `Hooksmith::VerificationError` | Request signature verification failed |
|
|
327
|
+
| `Hooksmith::MultipleProcessorsError` | Multiple processors matched the payload |
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
begin
|
|
331
|
+
Hooksmith::Dispatcher.new(provider:, event:, payload:).run!
|
|
332
|
+
rescue Hooksmith::VerificationError => e
|
|
333
|
+
render json: { error: 'Invalid signature' }, status: :unauthorized
|
|
334
|
+
rescue Hooksmith::MultipleProcessorsError => e
|
|
335
|
+
render json: { error: 'Ambiguous processor' }, status: :unprocessable_entity
|
|
336
|
+
rescue StandardError => e
|
|
337
|
+
render json: { error: 'Processing failed' }, status: :internal_server_error
|
|
338
|
+
end
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Testing
|
|
342
|
+
|
|
343
|
+
The gem includes a full test suite using Minitest. Run the tests with:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
102
346
|
bundle exec rake test
|
|
103
347
|
```
|
|
104
348
|
|
|
105
|
-
|
|
349
|
+
### Testing Your Processors
|
|
106
350
|
|
|
107
351
|
```ruby
|
|
108
|
-
|
|
109
|
-
|
|
352
|
+
class ChargeSucceededProcessorTest < ActiveSupport::TestCase
|
|
353
|
+
test 'processes successful charges' do
|
|
354
|
+
payload = { 'data' => { 'object' => { 'id' => 'ch_123', 'status' => 'succeeded' } } }
|
|
355
|
+
processor = Stripe::ChargeSucceededProcessor.new(payload)
|
|
356
|
+
|
|
357
|
+
assert processor.can_handle?(payload)
|
|
358
|
+
processor.process!
|
|
359
|
+
|
|
360
|
+
assert Payment.find_by(stripe_charge_id: 'ch_123').paid?
|
|
361
|
+
end
|
|
362
|
+
end
|
|
110
363
|
```
|
|
111
364
|
|
|
112
|
-
|
|
365
|
+
## Configuration Reference
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
Hooksmith.configure do |config|
|
|
369
|
+
config.provider(:stripe) do |stripe|
|
|
370
|
+
stripe.register(:charge_succeeded, 'Stripe::ChargeSucceededProcessor')
|
|
371
|
+
|
|
372
|
+
stripe.verifier = Hooksmith::Verifiers::Hmac.new(
|
|
373
|
+
secret: ENV['STRIPE_WEBHOOK_SECRET'],
|
|
374
|
+
header: 'Stripe-Signature',
|
|
375
|
+
algorithm: :sha256
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
stripe.idempotency_key = ->(payload) { payload['id'] }
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
config.event_store do |store|
|
|
382
|
+
store.enabled = true
|
|
383
|
+
store.model_class_name = 'WebhookEvent'
|
|
384
|
+
store.record_timing = :before
|
|
385
|
+
store.mapper = ->(provider:, event:, payload:) { ... }
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
```
|
|
113
389
|
|
|
114
390
|
## Contributing
|
|
115
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/gregorypreve/hooksmith.
|
|
116
391
|
|
|
392
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/RivageImmo/Hooksmith.
|
|
117
393
|
|
|
118
394
|
## License
|
|
395
|
+
|
|
119
396
|
The gem is available as open source under the terms of the MIT License.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hooksmith
|
|
4
|
+
module Config
|
|
5
|
+
# EventStore holds settings for optional event persistence.
|
|
6
|
+
class EventStore
|
|
7
|
+
# Whether persistence is enabled.
|
|
8
|
+
attr_accessor :enabled
|
|
9
|
+
# Class name of the model used to persist events. Must respond to .create!(attrs)
|
|
10
|
+
attr_accessor :model_class_name
|
|
11
|
+
# Proc to map provider/event/payload to attributes persisted
|
|
12
|
+
attr_accessor :mapper
|
|
13
|
+
# When to record: :before, :after, or :both
|
|
14
|
+
attr_accessor :record_timing
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@enabled = false
|
|
18
|
+
# No default model in the gem; applications should provide their own model
|
|
19
|
+
@model_class_name = nil
|
|
20
|
+
@record_timing = :before
|
|
21
|
+
@mapper = default_mapper
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def model_class
|
|
25
|
+
return nil if model_class_name.nil?
|
|
26
|
+
|
|
27
|
+
Object.const_get(model_class_name)
|
|
28
|
+
rescue NameError
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def default_mapper
|
|
35
|
+
lambda do |provider:, event:, payload:|
|
|
36
|
+
now = Time.respond_to?(:current) ? Time.current : Time.now
|
|
37
|
+
{
|
|
38
|
+
provider: provider.to_s,
|
|
39
|
+
event: event.to_s,
|
|
40
|
+
payload:,
|
|
41
|
+
received_at: now
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hooksmith
|
|
4
|
+
module Config
|
|
5
|
+
# Provider is used internally by the DSL to collect processor registrations,
|
|
6
|
+
# optional verifier configuration, and idempotency settings.
|
|
7
|
+
#
|
|
8
|
+
# Uses string keys internally to prevent Symbol DoS attacks when processing
|
|
9
|
+
# untrusted webhook input.
|
|
10
|
+
class Provider
|
|
11
|
+
# @return [String] the provider name.
|
|
12
|
+
attr_reader :provider
|
|
13
|
+
# @return [Array<Hash>] list of entries registered.
|
|
14
|
+
attr_reader :entries
|
|
15
|
+
# @return [Hooksmith::Verifiers::Base, nil] the verifier for this provider.
|
|
16
|
+
# @example
|
|
17
|
+
# config.provider(:stripe) do |stripe|
|
|
18
|
+
# stripe.verifier = Hooksmith::Verifiers::Hmac.new(
|
|
19
|
+
# secret: ENV['STRIPE_WEBHOOK_SECRET'],
|
|
20
|
+
# header: 'Stripe-Signature'
|
|
21
|
+
# )
|
|
22
|
+
# end
|
|
23
|
+
attr_accessor :verifier
|
|
24
|
+
# @return [Proc, nil] the idempotency key extractor for this provider.
|
|
25
|
+
# @example
|
|
26
|
+
# config.provider(:stripe) do |stripe|
|
|
27
|
+
# stripe.idempotency_key = ->(payload) { payload['id'] }
|
|
28
|
+
# end
|
|
29
|
+
attr_accessor :idempotency_key
|
|
30
|
+
|
|
31
|
+
def initialize(provider)
|
|
32
|
+
@provider = provider.to_s
|
|
33
|
+
@entries = []
|
|
34
|
+
@verifier = nil
|
|
35
|
+
@idempotency_key = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Registers a processor for a specific event.
|
|
39
|
+
#
|
|
40
|
+
# @param event [Symbol, String] the event name.
|
|
41
|
+
# @param processor_class_name [String] the processor class name.
|
|
42
|
+
def register(event, processor_class_name)
|
|
43
|
+
entries << { event: event.to_s, processor: processor_class_name }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|