Class: Attio::Webhooks
- Inherits:
-
Object
- Object
- Attio::Webhooks
- Defined in:
- lib/attio/webhooks.rb
Overview
Webhook handling for Attio events
Defined Under Namespace
Classes: Event, InvalidSignatureError, InvalidTimestampError, MissingHeaderError
Constant Summary collapse
- DEFAULT_TOLERANCE =
Default time window for timestamp validation (5 minutes)
300
Instance Attribute Summary collapse
- #handlers ⇒ Object readonly
- #secret ⇒ Object readonly
- #tolerance ⇒ Object readonly
Instance Method Summary collapse
- #dispatch_event(event) ⇒ Object private
- #extract_header(headers, name) ⇒ Object private
-
#initialize(secret:, tolerance: DEFAULT_TOLERANCE) ⇒ Webhooks
constructor
Initialize webhook handler.
-
#on(event_type) {|event| ... } ⇒ Object
Register an event handler.
-
#on_any {|event| ... } ⇒ Object
Register a global handler for all events.
-
#process(payload, headers) ⇒ Event
Process incoming webhook.
- #secure_compare?(expected, actual) ⇒ Boolean private
-
#verify_signature?(payload, signature) ⇒ Boolean
Verify webhook authenticity using HMAC-SHA256.
- #verify_timestamp!(timestamp) ⇒ Object private
- #verify_webhook!(payload, headers) ⇒ Object private
Constructor Details
#initialize(secret:, tolerance: DEFAULT_TOLERANCE) ⇒ Webhooks
Initialize webhook handler
33 34 35 36 37 38 |
# File 'lib/attio/webhooks.rb', line 33 def initialize(secret:, tolerance: DEFAULT_TOLERANCE) @secret = secret @tolerance = tolerance @handlers = {} @global_handlers = [] end |
Instance Attribute Details
#handlers ⇒ Object (readonly)
27 28 29 |
# File 'lib/attio/webhooks.rb', line 27 def handlers @handlers end |
#secret ⇒ Object (readonly)
27 28 29 |
# File 'lib/attio/webhooks.rb', line 27 def secret @secret end |
#tolerance ⇒ Object (readonly)
27 28 29 |
# File 'lib/attio/webhooks.rb', line 27 def tolerance @tolerance end |
Instance Method Details
#dispatch_event(event) ⇒ Object (private)
123 124 125 126 127 128 129 |
# File 'lib/attio/webhooks.rb', line 123 private def dispatch_event(event) # Call specific handlers @handlers[event.type].each { |handler| handler.call(event) } if @handlers[event.type] # Call global handlers @global_handlers.each { |handler| handler.call(event) } end |
#extract_header(headers, name) ⇒ Object (private)
102 103 104 105 106 107 108 109 110 111 |
# File 'lib/attio/webhooks.rb', line 102 private def extract_header(headers, name) # Handle different header formats (Rack, Rails, etc.) value = headers[name] || headers[name.downcase] || headers[name.upcase.gsub("-", "_")] raise MissingHeaderError, "Missing required header: #{name}" unless value value end |
#on(event_type) {|event| ... } ⇒ Object
Register an event handler
45 46 47 48 |
# File 'lib/attio/webhooks.rb', line 45 def on(event_type, &block) @handlers[event_type] ||= [] @handlers[event_type] << block end |
#on_any {|event| ... } ⇒ Object
Register a global handler for all events
53 54 55 |
# File 'lib/attio/webhooks.rb', line 53 def on_any(&block) @global_handlers << block end |
#process(payload, headers) ⇒ Event
Process incoming webhook
64 65 66 67 68 69 70 |
# File 'lib/attio/webhooks.rb', line 64 def process(payload, headers) verify_webhook!(payload, headers) event = Event.new(JSON.parse(payload)) dispatch_event(event) event end |
#secure_compare?(expected, actual) ⇒ Boolean (private)
131 132 133 134 135 136 137 138 139 140 |
# File 'lib/attio/webhooks.rb', line 131 private def secure_compare?(expected, actual) return false unless expected.bytesize == actual.bytesize expected_bytes = expected.unpack("C*") actual_bytes = actual.unpack("C*") result = 0 expected_bytes.zip(actual_bytes) { |x, y| result |= x ^ y } result == 0 end |
#verify_signature?(payload, signature) ⇒ Boolean
Verify webhook authenticity using HMAC-SHA256
77 78 79 80 81 82 83 84 85 86 |
# File 'lib/attio/webhooks.rb', line 77 def verify_signature?(payload, signature) expected = OpenSSL::HMAC.hexdigest( OpenSSL::Digest.new("sha256"), @secret, payload ) # Use secure comparison to prevent timing attacks secure_compare?(expected, signature) end |
#verify_timestamp!(timestamp) ⇒ Object (private)
113 114 115 116 117 118 119 120 121 |
# File 'lib/attio/webhooks.rb', line 113 private def () webhook_time = Time.at(.to_i) current_time = Time.now return unless (current_time - webhook_time).abs > tolerance raise InvalidTimestampError, "Webhook timestamp outside of tolerance (#{tolerance}s)" end |
#verify_webhook!(payload, headers) ⇒ Object (private)
88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/attio/webhooks.rb', line 88 private def verify_webhook!(payload, headers) signature = extract_header(headers, "Attio-Signature") = extract_header(headers, "Attio-Timestamp") # Verify timestamp to prevent replay attacks () # Verify signature signed_payload = "#{}.#{payload}" return if verify_signature?(signed_payload, signature) raise InvalidSignatureError, "Webhook signature verification failed" end |