Module: Mailkube::Webhooks
- Defined in:
- lib/mailkube/webhooks.rb,
sig/mailkube/webhooks.rbs
Overview
Webhook signature verification.
Verification is pure and dependency-free: no client instance, no configuration, so you call it directly inside your webhook handler.
Constant Summary collapse
- DEFAULT_TOLERANCE =
How stale a webhook timestamp may be, in seconds, before it is rejected.
300- SIGNATURE_PREFIX =
The prefix the server puts before the hex digest in
X-Webhook-Sig. "sha256="- PARSE_OPTIONS =
Decode webhook payloads deep-frozen, so a receiver cannot mutate an event it is about to forward or log. Passed positionally because that is the shape
JSON.parsedeclares ((source, opts)); as keywords Steep reports an unexpected keyword. { freeze: true }.freeze
Class Method Summary collapse
-
.parse_event(payload) ⇒ Events::Event
Parse a raw webhook body into a typed event.
-
.verify(payload:, headers:, secret:, tolerance: DEFAULT_TOLERANCE) ⇒ Events::Event
Verify a webhook's signature and return the parsed event.
-
.verify_signature(payload:, headers:, secret:, tolerance: DEFAULT_TOLERANCE) ⇒ Object
headersis any mapping that yields name/value pairs, not just a Hash:ActionDispatch::Http::Headersis Enumerable-only and yields CGI env names, and a Rails receiver passes it straight through fromrequest.headers.
Class Method Details
.parse_event(payload) ⇒ Events::Event
Parse a raw webhook body into a typed event.
An event type this release has never heard of comes back as Events::UnknownEvent rather
than raising: a receiver keeps working when the platform adds a type, with no SDK upgrade.
That is the contract's deliberate inversion of the response-model rules, and it is why the
dispatch is a fetch with a default rather than a conditional — an unknown type is the last
row of the table, not an error path.
Unknown fields survive too, at every depth: see Events::Node.
67 68 69 70 71 72 73 74 |
# File 'lib/mailkube/webhooks.rb', line 67 def self.parse_event(payload) body = JSON.parse(payload, PARSE_OPTIONS) raise Error, "webhook payload is not a JSON object" unless body.is_a?(Hash) Events::REGISTRY.fetch(body["type"], Events::UnknownEvent).new(body) rescue JSON::ParserError => e raise Error, "webhook payload is not valid JSON: #{e.}" end |
.verify(payload:, headers:, secret:, tolerance: DEFAULT_TOLERANCE) ⇒ Events::Event
Verify a webhook's signature and return the parsed event.
The combinator most handlers actually want. It composes cleanly only because verify_signature returns the verified payload rather than true.
88 89 90 |
# File 'lib/mailkube/webhooks.rb', line 88 def self.verify(payload:, headers:, secret:, tolerance: DEFAULT_TOLERANCE) parse_event(verify_signature(payload: payload, headers: headers, secret: secret, tolerance: tolerance)) end |
.verify_signature(payload:, headers:, secret:, tolerance: DEFAULT_TOLERANCE) ⇒ Object
headers is any mapping that yields name/value pairs, not just a Hash:
ActionDispatch::Http::Headers is Enumerable-only and yields CGI env names, and a Rails
receiver passes it straight through from request.headers.
40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/mailkube/webhooks.rb', line 40 def self.verify_signature(payload:, headers:, secret:, tolerance: DEFAULT_TOLERANCE) lookup = normalize_headers(headers) id = lookup["x-webhook-id"] = lookup["x-webhook-ts"] signature = lookup["x-webhook-sig"] if id.nil? || .nil? || signature.nil? raise SignatureVerificationError, "missing required webhook signature headers" end check_freshness(, tolerance) check_signature(payload, id, , signature, secret) payload end |