Module: Mailblastr::Webhooks

Defined in:
lib/mailblastr/webhooks.rb

Class Method Summary collapse

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")
  timestamp = read_header(headers, "svix-timestamp")
  sig_header = read_header(headers, "svix-signature")
  return { valid: false, reason: "missing_headers" } if id.nil? || timestamp.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(timestamp, 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}.#{timestamp}.#{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