Module: Mailblastr::Webhooks
- Defined in:
- lib/mailblastr/webhooks.rb
Class Method Summary collapse
-
.create(params) ⇒ Object
Create a webhook.
-
.delete(webhook_id) ⇒ Object
DELETE /webhooks/:id.
-
.get(webhook_id) ⇒ Object
GET /webhooks/:id.
-
.list(params = {}) ⇒ Object
GET /webhooks.
-
.rotate(webhook_id) ⇒ Object
Rotate the signing secret; the new plaintext secret is returned once and the old one stops verifying immediately.
-
.test(webhook_id) ⇒ Object
Send a synchronous test delivery and return the endpoint's live result.
-
.update(webhook_id, params) ⇒ Object
PATCH /webhooks/:id — params: { endpoint:, events:, status: }.
-
.verify_signature(payload, headers, secret, tolerance: 300) ⇒ Object
Verify a webhook delivery's Svix-style signature against your endpoint's signing secret.
Class Method Details
.create(params) ⇒ Object
Create a webhook. The plaintext signing secret is returned ONCE, only here. POST /webhooks — params: { endpoint:, events: [...], secret: }
10 11 12 |
# File 'lib/mailblastr/webhooks.rb', line 10 def create(params) Client.request(:post, "/webhooks", body: params) end |
.delete(webhook_id) ⇒ Object
DELETE /webhooks/:id
42 43 44 |
# File 'lib/mailblastr/webhooks.rb', line 42 def delete(webhook_id) Client.request(:delete, "/webhooks/#{Client.path_escape(webhook_id)}") end |
.get(webhook_id) ⇒ Object
GET /webhooks/:id
15 16 17 |
# File 'lib/mailblastr/webhooks.rb', line 15 def get(webhook_id) Client.request(:get, "/webhooks/#{Client.path_escape(webhook_id)}") end |
.list(params = {}) ⇒ Object
GET /webhooks
20 21 22 |
# File 'lib/mailblastr/webhooks.rb', line 20 def list(params = {}) Client.request(:get, "/webhooks", query: Client.pagination(params)) end |
.rotate(webhook_id) ⇒ Object
Rotate the signing secret; the new plaintext secret is returned once and the old one stops verifying immediately. POST /webhooks/:id/rotate
31 32 33 |
# File 'lib/mailblastr/webhooks.rb', line 31 def rotate(webhook_id) Client.request(:post, "/webhooks/#{Client.path_escape(webhook_id)}/rotate") end |
.test(webhook_id) ⇒ Object
Send a synchronous test delivery and return the endpoint's live result. POST /webhooks/:id/test
37 38 39 |
# File 'lib/mailblastr/webhooks.rb', line 37 def test(webhook_id) Client.request(:post, "/webhooks/#{Client.path_escape(webhook_id)}/test") end |
.update(webhook_id, params) ⇒ Object
PATCH /webhooks/:id — params: { endpoint:, events:, status: }
25 26 27 |
# File 'lib/mailblastr/webhooks.rb', line 25 def update(webhook_id, params) Client.request(:patch, "/webhooks/#{Client.path_escape(webhook_id)}", body: params) end |
.verify_signature(payload, headers, secret, tolerance: 300) ⇒ Object
Verify a webhook delivery's Svix-style signature against your endpoint's signing secret. Pure local computation (OpenSSL HMAC-SHA256) — no HTTP.
payload MUST be the exact raw request body string (do not re-serialize
parsed JSON). headers is a Hash carrying svix-id / svix-timestamp /
svix-signature (read case-insensitively; array values use the first
element). The signature header may carry multiple space-separated
v1,<base64> entries — any one match makes the delivery valid.
Returns { valid: true } or { valid: false, reason: "..." }.
result = Mailblastr::Webhooks.verify_signature(request.raw_post, request.headers.to_h, secret)
head :unauthorized unless result[:valid]
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/mailblastr/webhooks.rb', line 59 def verify_signature(payload, headers, secret, tolerance: 300) id = read_header(headers, "svix-id") = read_header(headers, "svix-timestamp") sig_header = read_header(headers, "svix-signature") return { valid: false, reason: "missing_headers" } if id.nil? || .nil? || sig_header.nil? return { valid: false, reason: "missing_secret" } if secret.nil? || secret.to_s.empty? # Optional timestamp freshness check (default 5 minutes; 0 disables). if tolerance && tolerance.positive? ts = begin Integer(, 10) rescue ArgumentError, TypeError nil end return { valid: false, reason: "invalid_timestamp" } if ts.nil? return { valid: false, reason: "timestamp_out_of_tolerance" } if (Time.now.to_i - ts).abs > tolerance end signed = "#{id}.#{}.#{payload}" digest = OpenSSL::HMAC.digest("SHA256", secret_to_key(secret), signed) expected = [digest].pack("m0") # strict base64, no newline sig_header.split(" ").each do |part| part = part.strip next if part.empty? sig = part.start_with?("v1,") ? part[3..] : part return { valid: true } if secure_compare(sig, expected) end { valid: false, reason: "no_match" } end |