Class: AfconWave::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/afconwave.rb

Overview

─── Main Client ──────────────────────────────────────────────────────────────

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(secret_key:, base_url: 'https://api.afconwave.com/v1', timeout: 30) ⇒ Client

Returns a new instance of Client.



32
33
34
35
36
# File 'lib/afconwave.rb', line 32

def initialize(secret_key:, base_url: 'https://api.afconwave.com/v1', timeout: 30)
  @secret_key = secret_key
  @base_url = base_url
  @timeout = timeout
end

Instance Attribute Details

#base_urlObject

Returns the value of attribute base_url.



30
31
32
# File 'lib/afconwave.rb', line 30

def base_url
  @base_url
end

#secret_keyObject

Returns the value of attribute secret_key.



30
31
32
# File 'lib/afconwave.rb', line 30

def secret_key
  @secret_key
end

#timeoutObject

Returns the value of attribute timeout.



30
31
32
# File 'lib/afconwave.rb', line 30

def timeout
  @timeout
end

Class Method Details

.verify_webhook_signature(payload:, signature:, secret:, tolerance: 300) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/afconwave.rb', line 38

def self.verify_webhook_signature(payload:, signature:, secret:, tolerance: 300)
  return false if signature.nil? || secret.nil?

  # 1. Verify Signature (timing-safe compare via OpenSSL stdlib)
  expected = OpenSSL::HMAC.hexdigest('sha256', secret, payload)

  # OpenSSL.fixed_length_secure_compare requires equal-length inputs.
  return false unless signature.is_a?(String) && expected.bytesize == signature.bytesize
  return false unless OpenSSL.fixed_length_secure_compare(expected, signature)

  # 2. Verify Timestamp (Replay Protection)
  begin
    data = JSON.parse(payload)
    timestamp = data['timestamp'] || data['created_at'] || data['createdAt']
    
    if timestamp
      current_time = Time.now.to_i # seconds
      # Handle both ms and seconds
      webhook_time = timestamp > 10**10 ? timestamp / 1000 : timestamp
      age = (current_time - webhook_time).abs

      return false if age > tolerance
    end
  rescue JSON::ParserError
    # Non-JSON payload, signature is valid but can't check timestamp
  end

  true
end

Instance Method Details

#create_payment(**data) ⇒ Object



114
# File 'lib/afconwave.rb', line 114

def create_payment(**data); payments.create(**data); end

#create_payout(**data) ⇒ Object



117
# File 'lib/afconwave.rb', line 117

def create_payout(**data); payouts.create(**data); end

#cryptoObject



70
# File 'lib/afconwave.rb', line 70

def crypto; @crypto ||= Resource::Crypto.new(self); end

#disputesObject



72
# File 'lib/afconwave.rb', line 72

def disputes; @disputes ||= Resource::Disputes.new(self); end

#get_balancesObject

─── Top-level Convenience Methods ───────────────────────────────────────



113
# File 'lib/afconwave.rb', line 113

def get_balances; request(method: 'GET', path: '/balances'); end

#list_payments(**params) ⇒ Object



116
# File 'lib/afconwave.rb', line 116

def list_payments(**params); payments.list(**params); end

#paymentsObject



68
# File 'lib/afconwave.rb', line 68

def payments; @payments ||= Resource::Payments.new(self); end

#payoutsObject



69
# File 'lib/afconwave.rb', line 69

def payouts; @payouts ||= Resource::Payouts.new(self); end

#refundsObject



71
# File 'lib/afconwave.rb', line 71

def refunds; @refunds ||= Resource::Refunds.new(self); end

#request(method:, path:, data: nil, params: nil) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/afconwave.rb', line 74

def request(method:, path:, data: nil, params: nil)
  uri = URI("#{base_url}#{path}")
  uri.query = URI.encode_www_form(params) if params

  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true if uri.scheme == 'https'
  http.read_timeout = @timeout

  req = case method.upcase
        when 'POST'
          request = Net::HTTP::Post.new(uri)
          request.body = data.to_json if data
          request
        when 'GET'
          Net::HTTP::Get.new(uri)
        else
          raise Error.new("Unsupported method #{method}")
        end

  req['Authorization'] = "Bearer #{secret_key}"
  req['Content-Type'] = 'application/json'
  req['Accept'] = 'application/json'
  req['User-Agent'] = "AfconWave-Ruby-SDK/1.1.0"

  response = http.request(req)
  res_data = JSON.parse(response.body) rescue { 'error' => 'Invalid JSON response' }

  unless response.is_a?(Net::HTTPSuccess)
    case response.code.to_i
    when 401 then raise AuthError.new(res_data['error'] || 'Invalid API Key', status_code: 401)
    else raise Error.new(res_data['error'] || response.message, status_code: response.code.to_i, code: res_data['code'])
    end
  end

  res_data['data'] || res_data
end

#retrieve_payment(id) ⇒ Object



115
# File 'lib/afconwave.rb', line 115

def retrieve_payment(id); payments.retrieve(id); end