class Attio::Util::WebhookSignature::Handler
Helper class for webhook handlers
def extract_body(request)
def extract_body(request) case request when Hash request[:body] || request["body"] || "" when defined?(Rack::Request) && Rack::Request request.body.rewind request.body.read when defined?(ActionDispatch::Request) && ActionDispatch::Request request.raw_post else raise ArgumentError, "Unsupported request type: #{request.class}" end end
def extract_headers(request)
def extract_headers(request) case request when Hash request[:headers] || request["headers"] || {} when defined?(Rack::Request) && Rack::Request request.env.select { |k, _| k.start_with?("HTTP_") }.transform_keys { |k| k.sub(/^HTTP_/, "").downcase } when defined?(ActionDispatch::Request) && ActionDispatch::Request request.headers.to_h else raise ArgumentError, "Unsupported request type: #{request.class}" end end
def initialize(secret)
def initialize(secret) @secret = secret validate_secret! end
def parse_and_verify(request)
def parse_and_verify(request) verify_request(request) body = extract_body(request) JSON.parse(body, symbolize_names: true) rescue JSON::ParserError => e raise SignatureVerificationError, "Invalid JSON payload: #{e.message}" end
def validate_secret!
def validate_secret! raise ArgumentError, "Webhook secret is required" if secret.nil? || secret.empty? end
def verify_request(request)
def verify_request(request) headers = extract_headers(request) body = extract_body(request) signature_data = WebhookSignature.extract_from_headers(headers) WebhookSignature.verify!( payload: body, signature: signature_data[:signature], timestamp: signature_data[:timestamp], secret: secret ) end