Module: Polar::Webhooks

Defined in:
lib/polar/webhooks.rb

Defined Under Namespace

Classes: Event

Class Method Summary collapse

Class Method Details

.compute_signature(payload, timestamp, secret) ⇒ String

Compute expected signature for payload

Parameters:

  • payload (String)

    The raw request body

  • timestamp (String)

    The timestamp

  • secret (String)

    The webhook secret

Returns:

  • (String)

    The computed signature



89
90
91
92
93
94
95
96
97
# File 'lib/polar/webhooks.rb', line 89

def compute_signature(payload, timestamp, secret)
  signed_payload = "#{timestamp}.#{payload}"

  OpenSSL::HMAC.hexdigest(
    OpenSSL::Digest.new('sha256'),
    secret,
    signed_payload
  )
end

.event_type?(payload, event_type) ⇒ Boolean

Check if event is a specific type

Parameters:

  • payload (String, Hash)

    The webhook payload

  • event_type (String)

    The expected event type

Returns:

  • (Boolean)

    True if event matches type



125
126
127
# File 'lib/polar/webhooks.rb', line 125

def event_type?(payload, event_type)
  parse_event_type(payload) == event_type
end

.extract_signature(headers) ⇒ String

Extract signature from webhook headers

Parameters:

  • headers (Hash)

    The request headers

Returns:

  • (String)

    The signature

Raises:



55
56
57
58
59
60
61
62
# File 'lib/polar/webhooks.rb', line 55

def extract_signature(headers)
  signature = headers['polar-signature'] || headers['Polar-Signature']

  raise WebhookVerificationError, 'Missing polar-signature header' unless signature

  # Remove the signature prefix if present (e.g., "v1=signature")
  signature.split('=').last
end

.extract_timestamp(headers) ⇒ String

Extract timestamp from webhook headers

Parameters:

  • headers (Hash)

    The request headers

Returns:

  • (String)

    The timestamp

Raises:



44
45
46
47
48
49
50
# File 'lib/polar/webhooks.rb', line 44

def extract_timestamp(headers)
  timestamp = headers['polar-timestamp'] || headers['Polar-Timestamp']

  raise WebhookVerificationError, 'Missing polar-timestamp header' unless timestamp

  timestamp
end

.parse_event_type(payload) ⇒ String

Parse webhook event type from payload

Parameters:

  • payload (String, Hash)

    The webhook payload

Returns:

  • (String)

    The event type



114
115
116
117
118
119
# File 'lib/polar/webhooks.rb', line 114

def parse_event_type(payload)
  data = payload.is_a?(String) ? JSON.parse(payload) : payload
  data['type'] || data['event_type']
rescue JSON::ParserError
  nil
end

.secure_compare(a, b) ⇒ Boolean

Secure string comparison to prevent timing attacks

Parameters:

  • a (String)

    First string

  • b (String)

    Second string

Returns:

  • (Boolean)

    True if strings are equal



103
104
105
106
107
108
109
# File 'lib/polar/webhooks.rb', line 103

def secure_compare(a, b)
  return false unless a.length == b.length

  result = 0
  a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
  result == 0
end

.validate_event(payload, headers, secret) ⇒ Hash

Validate webhook event signature

Parameters:

  • payload (String)

    The raw request body

  • headers (Hash)

    The request headers

  • secret (String)

    The webhook secret

Returns:

  • (Hash)

    The validated event data

Raises:



15
16
17
18
19
20
21
22
23
24
25
# File 'lib/polar/webhooks.rb', line 15

def validate_event(payload, headers, secret)
  timestamp = extract_timestamp(headers)
  signature = extract_signature(headers)

  verify_timestamp(timestamp)
  verify_signature(payload, timestamp, signature, secret)

  JSON.parse(payload)
rescue JSON::ParserError => e
  raise WebhookError, "Invalid JSON payload: #{e.message}"
end

.verify_signature(payload, timestamp, signature, secret) ⇒ Boolean

Verify webhook signature without parsing payload

Parameters:

  • payload (String)

    The raw request body

  • timestamp (String)

    The timestamp from headers

  • signature (String)

    The signature from headers

  • secret (String)

    The webhook secret

Returns:

  • (Boolean)

    True if signature is valid

Raises:



33
34
35
36
37
38
39
# File 'lib/polar/webhooks.rb', line 33

def verify_signature(payload, timestamp, signature, secret)
  expected_signature = compute_signature(payload, timestamp, secret)

  raise WebhookVerificationError, 'Invalid webhook signature' unless secure_compare(signature, expected_signature)

  true
end

.verify_timestamp(timestamp, tolerance: 300) ⇒ Object

Verify timestamp is within tolerance

Parameters:

  • timestamp (String)

    The timestamp to verify

  • tolerance (Integer) (defaults to: 300)

    Maximum age in seconds (default: 300 = 5 minutes)

Raises:



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/polar/webhooks.rb', line 67

def verify_timestamp(timestamp, tolerance: 300)
  begin
    webhook_time = Time.at(timestamp.to_i)
  rescue ArgumentError
    raise WebhookVerificationError, 'Invalid timestamp format'
  end

  current_time = Time.now
  age = current_time - webhook_time

  raise WebhookVerificationError, "Webhook timestamp too old (#{age.to_i}s > #{tolerance}s)" if age > tolerance

  raise WebhookVerificationError, 'Webhook timestamp too far in future' if age < -tolerance

  true
end