Module: Invoq::Webhooks

Defined in:
lib/invoq/webhooks.rb

Constant Summary collapse

DEFAULT_TOLERANCE_SECONDS =
300
SIGNATURE_PATTERN =
/\A[a-f0-9]{64}\z/i
MISSING =
Object.new

Class Method Summary collapse

Class Method Details

.invoice_paid?(event) ⇒ Boolean

Returns:

  • (Boolean)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/invoq/webhooks.rb', line 58

def self.invoice_paid?(event)
  return false unless event.is_a?(Hash)
  return false unless event["type"] == "invoice.paid"
  return false unless event["id"].is_a?(String)
  return false unless invoice_mode?(event["mode"])
  return false unless event["created_at"].is_a?(String)

  data = event["data"]
  return false unless data.is_a?(Hash)

  invoice = data["invoice"]
  return false unless invoice.is_a?(Hash)

  reference_id = invoice.key?("reference_id") ? invoice["reference_id"] : MISSING
  fully_paid_at = invoice.key?("fully_paid_at") ? invoice["fully_paid_at"] : MISSING

  invoice["id"].is_a?(String) &&
    invoice_mode?(invoice["mode"]) &&
    invoice_paid_status?(invoice["status"]) &&
    invoice["amount"].is_a?(String) &&
    invoice["currency"] == "USD" &&
    invoice["amount_paid"].is_a?(String) &&
    (reference_id.is_a?(String) || reference_id.nil?) &&
    (fully_paid_at.is_a?(String) || fully_paid_at.nil?)
end

.is_invoice_paid(event) ⇒ Object



84
85
86
# File 'lib/invoq/webhooks.rb', line 84

def self.is_invoice_paid(event)
  invoice_paid?(event)
end

.verify_webhook(raw_body, headers, webhook_secret) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/invoq/webhooks.rb', line 14

def self.verify_webhook(raw_body, headers, webhook_secret)
  signature_header = get_signature_header(headers)

  if signature_header.nil? || signature_header.empty?
    raise signature_error("missing_signature", "Missing invoq-signature header.")
  end

  unless webhook_secret.is_a?(String) && !webhook_secret.empty?
    raise signature_error(
      "invalid_signature_header",
      "Webhook secret must be a non-empty string."
    )
  end

  timestamp, timestamp_seconds, signature = parse_signature_header(signature_header)
  now_seconds = Time.now.to_i

  if (now_seconds - timestamp_seconds).abs > DEFAULT_TOLERANCE_SECONDS
    raise signature_error(
      "timestamp_outside_tolerance",
      "Webhook timestamp is outside the allowed tolerance."
    )
  end

  expected_signature = hmac_sha256_hex(webhook_secret, timestamp, raw_body)

  unless constant_time_equal?(expected_signature, signature)
    raise signature_error("signature_mismatch", "Webhook signature mismatch.")
  end

  payload = JSON.parse(body_text(raw_body))

  unless payload.is_a?(Hash) && payload["type"].is_a?(String)
    raise signature_error(
      "invalid_payload",
      "Webhook payload must be an object with a string type."
    )
  end

  payload
rescue JSON::ParserError
  raise signature_error("invalid_payload", "Webhook payload is not valid JSON.")
end