Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
ruby-version: ['2.4.2']
ruby-version: ['2.7', '3.1', '3.2', '3.3']
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
Expand All @@ -36,7 +36,7 @@ jobs:
if: ${{ github.ref == 'refs/heads/master' }}
strategy:
matrix:
ruby-version: [ '2.4.2' ]
ruby-version: ['2.7', '3.1', '3.2', '3.3']
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
Expand Down
6 changes: 6 additions & 0 deletions HISTORY
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
=== 4.6.0 2026-02-10
- Bump the minimum version of httparty to 0.23.3 to ensure protection against CVE-2025-68696
- Refactor Client to use dedicated internal HTTP clients for different API endpoints
- Fix Verification API methods to use correct version parameter
- Increase minimum required version for Ruby to 2.7.0

=== 4.5.1 2025-04-07
- Fix Verification URLs

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The official Ruby bindings for the latest version (v205) of the [Sift API](https

## Requirements

* Ruby 2.0.0 or above.
* Ruby 2.7.0 or above.


## Installation
Expand Down
81 changes: 49 additions & 32 deletions lib/sift/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
require "multi_json"
require "base64"

require_relative "./client/decision"
require_relative "./error"

module Sift

# Represents the payload returned from a call through the track API
Expand Down Expand Up @@ -94,17 +91,36 @@ class Client
API_ENDPOINT = ENV["SIFT_RUBY_API_URL"] || 'https://api.siftscience.com'
API3_ENDPOINT = ENV["SIFT_RUBY_API3_URL"] || 'https://api3.siftscience.com'

# Maintain backward compatibility for users who may rely on HTTParty methods
include HTTParty
base_uri API_ENDPOINT

attr_reader :api_key, :account_id

def self.build_auth_header(api_key)
{ "Authorization" => "Basic #{Base64.encode64(api_key)}" }
end
class << self
def build_auth_header(api_key)
{ "Authorization" => "Basic #{Base64.strict_encode64(api_key + ":")}" }
end

def user_agent
"sift-ruby/#{VERSION}"
end

# Factory methods for internal API executors that inherit from the current class context.
# This ensures that subclasses of Client propagate their HTTParty configuration
# to these internal clients.

def self.user_agent
"sift-ruby/#{VERSION}"
def api_client
@api_client ||= Class.new(self) do
base_uri API_ENDPOINT
end
end

def api3_client
@api3_client ||= Class.new(self) do
base_uri API3_ENDPOINT
end
end
end

# Constructor
Expand Down Expand Up @@ -256,7 +272,7 @@ def track(event, properties = {}, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

response = self.class.post(path, options)
response = self.class.api_client.post(path, options)
Response.new(response.body, response.code, response.response)
end

Expand Down Expand Up @@ -319,7 +335,7 @@ def score(user_id, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

response = self.class.get(Sift.score_api_path(user_id, version), options)
response = self.class.api_client.get(Sift.score_api_path(user_id, version), options)
Response.new(response.body, response.code, response.response)
end

Expand Down Expand Up @@ -364,6 +380,7 @@ def get_user_score(user_id, opts = {})
abuse_types = opts[:abuse_types]
api_key = opts[:api_key] || @api_key
timeout = opts[:timeout] || @timeout
version = opts[:version] || @version
include_score_percentiles = opts[:include_score_percentiles]

raise("user_id must be a non-empty string") if (!user_id.is_a? String) || user_id.to_s.empty?
Expand All @@ -382,7 +399,7 @@ def get_user_score(user_id, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

response = self.class.get(Sift.user_score_api_path(user_id, @version), options)
response = self.class.api_client.get(Sift.user_score_api_path(user_id, version), options)
Response.new(response.body, response.code, response.response)
end

Expand Down Expand Up @@ -420,6 +437,7 @@ def rescore_user(user_id, opts = {})
abuse_types = opts[:abuse_types]
api_key = opts[:api_key] || @api_key
timeout = opts[:timeout] || @timeout
version = opts[:version] || @version

raise("user_id must be a non-empty string") if (!user_id.is_a? String) || user_id.to_s.empty?
raise("Bad api_key parameter") if api_key.empty?
Expand All @@ -434,7 +452,7 @@ def rescore_user(user_id, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

response = self.class.post(Sift.user_score_api_path(user_id, @version), options)
response = self.class.api_client.post(Sift.user_score_api_path(user_id, version), options)
Response.new(response.body, response.code, response.response)
end

Expand Down Expand Up @@ -532,7 +550,7 @@ def unlabel(user_id, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

response = self.class.delete(Sift.users_label_api_path(user_id, version), options)
response = self.class.api_client.delete(Sift.users_label_api_path(user_id, version), options)
Response.new(response.body, response.code, response.response)
end

Expand Down Expand Up @@ -569,8 +587,7 @@ def get_workflow_status(run_id, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

uri = API3_ENDPOINT + Sift.workflow_status_path(account_id, run_id)
response = self.class.get(uri, options)
response = self.class.api3_client.get(Sift.workflow_status_path(account_id, run_id), options)
Response.new(response.body, response.code, response.response)
end

Expand Down Expand Up @@ -607,8 +624,7 @@ def get_user_decisions(user_id, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

uri = API3_ENDPOINT + Sift.user_decisions_api_path(account_id, user_id)
response = self.class.get(uri, options)
response = self.class.api3_client.get(Sift.user_decisions_api_path(account_id, user_id), options)
Response.new(response.body, response.code, response.response)
end

Expand Down Expand Up @@ -645,8 +661,7 @@ def get_order_decisions(order_id, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

uri = API3_ENDPOINT + Sift.order_decisions_api_path(account_id, order_id)
response = self.class.get(uri, options)
response = self.class.api3_client.get(Sift.order_decisions_api_path(account_id, order_id), options)
Response.new(response.body, response.code, response.response)
end

Expand Down Expand Up @@ -685,8 +700,7 @@ def get_session_decisions(user_id, session_id, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

uri = API3_ENDPOINT + Sift.session_decisions_api_path(account_id, user_id, session_id)
response = self.class.get(uri, options)
response = self.class.api3_client.get(Sift.session_decisions_api_path(account_id, user_id, session_id), options)
Response.new(response.body, response.code, response.response)
end

Expand Down Expand Up @@ -725,8 +739,7 @@ def get_content_decisions(user_id, content_id, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

uri = API3_ENDPOINT + Sift.content_decisions_api_path(account_id, user_id, content_id)
response = self.class.get(uri, options)
response = self.class.api3_client.get(Sift.content_decisions_api_path(account_id, user_id, content_id), options)
Response.new(response.body, response.code, response.response)
end

Expand All @@ -748,7 +761,7 @@ def apply_decision!(configs = {})

def build_default_headers_post(api_key)
{
"Authorization" => "Basic #{Base64.encode64(api_key+":")}",
"Authorization" => "Basic #{Base64.strict_encode64(api_key+":")}",
"User-Agent" => "SiftScience/v#{@version} sift-ruby/#{VERSION}",
"Content-Type" => "application/json"
}
Expand All @@ -768,7 +781,7 @@ def verification_send(properties = {}, opts = {})
:headers => build_default_headers_post(api_key)
}
options.merge!(:timeout => timeout) unless timeout.nil?
response = self.class.post(Sift.verification_api_send_path(@version), options)
response = self.class.api_client.post(Sift.verification_api_send_path(version), options)
Response.new(response.body, response.code, response.response)
end

Expand All @@ -787,7 +800,7 @@ def verification_resend(properties = {}, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

response = self.class.post(Sift.verification_api_resend_path(@version), options)
response = self.class.api_client.post(Sift.verification_api_resend_path(version), options)
Response.new(response.body, response.code, response.response)
end

Expand All @@ -806,7 +819,7 @@ def verification_check(properties = {}, opts = {})
}
options.merge!(:timeout => timeout) unless timeout.nil?

response = self.class.post(Sift.verification_api_check_path(@version), options)
response = self.class.api_client.post(Sift.verification_api_check_path(version), options)
Response.new(response.body, response.code, response.response)
end

Expand All @@ -831,7 +844,7 @@ def create_psp_merchant_profile(properties = {}, opts = {})
:basic_auth => { :username => api_key, :password => "" }
}
options.merge!(:timeout => timeout) unless timeout.nil?
response = self.class.post(API_ENDPOINT + Sift.psp_merchant_api_path(account_id), options)
response = self.class.api_client.post(Sift.psp_merchant_api_path(account_id), options)
Response.new(response.body, response.code, response.response)
end

Expand All @@ -858,7 +871,7 @@ def update_psp_merchant_profile(merchant_id, properties = {}, opts = {})
:basic_auth => { :username => api_key, :password => "" }
}
options.merge!(:timeout => timeout) unless timeout.nil?
response = self.class.put(API_ENDPOINT + Sift.psp_merchant_id_api_path(account_id, merchant_id), options)
response = self.class.api_client.put(Sift.psp_merchant_id_api_path(account_id, merchant_id), options)
Response.new(response.body, response.code, response.response)
end

Expand All @@ -882,7 +895,7 @@ def get_a_psp_merchant_profile(merchant_id, opts = {})
:basic_auth => { :username => api_key, :password => "" }
}
options.merge!(:timeout => timeout) unless timeout.nil?
response = self.class.get(API_ENDPOINT + Sift.psp_merchant_id_api_path(account_id, merchant_id), options)
response = self.class.api_client.get(Sift.psp_merchant_id_api_path(account_id, merchant_id), options)
Response.new(response.body, response.code, response.response)
end

Expand Down Expand Up @@ -911,7 +924,7 @@ def get_psp_merchant_profiles(batch_size = nil, batch_token = nil, opts = {})
:query => query
}
options.merge!(:timeout => timeout) unless timeout.nil?
response = self.class.get(API_ENDPOINT + Sift.psp_merchant_api_path(account_id), options)
response = self.class.api_client.get(Sift.psp_merchant_api_path(account_id), options)
Response.new(response.body, response.code, response.response)
end

Expand All @@ -926,7 +939,7 @@ def handle_response(response)
end

def decision_instance
@decision_instance ||= Decision.new(api_key, account_id)
@decision_instance ||= Decision.new(api_key, account_id, self.class)
end

def delete_nils(properties)
Expand All @@ -943,4 +956,8 @@ def delete_nils(properties)
end
end
end

require_relative "./client/decision"
require_relative "./error"

end
13 changes: 7 additions & 6 deletions lib/sift/client/decision.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ class Client
class Decision
FILTER_PARAMS = %w{ limit entity_type abuse_types from }

attr_reader :account_id, :api_key
attr_reader :account_id, :api_key, :client_class

def initialize(api_key, account_id)
def initialize(api_key, account_id, client_class = Sift::Client)
@account_id = account_id
@api_key = api_key
@client_class = client_class
end

def list(options = {})
Expand All @@ -25,7 +26,8 @@ def list(options = {})
else
Router.get(index_path, {
query: build_query(getter),
headers: auth_header
headers: auth_header,
client_class: client_class
})
end
end
Expand All @@ -44,7 +46,7 @@ def apply_to(configs = {})
getter = Utils::HashGetter.new(configs)
configs[:account_id] = account_id

ApplyTo.new(api_key, getter.get(:decision_id), configs).run
ApplyTo.new(api_key, getter.get(:decision_id), configs, client_class).run
end

def index_path
Expand All @@ -54,7 +56,7 @@ def index_path
private

def request_next_page(path)
Router.get(path, headers: auth_header)
Router.get(path, headers: auth_header, client_class: client_class)
end

def auth_header
Expand All @@ -63,4 +65,3 @@ def auth_header
end
end
end

10 changes: 7 additions & 3 deletions lib/sift/client/decision/apply_to.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class ApplyTo
time
}

attr_reader :decision_id, :configs, :getter, :api_key
attr_reader :decision_id, :configs, :getter, :api_key, :client_class

PROPERTIES.each do |attribute|
class_eval %{
Expand All @@ -31,11 +31,12 @@ def #{attribute}
}
end

def initialize(api_key, decision_id, configs)
def initialize(api_key, decision_id, configs, client_class = Sift::Client)
@api_key = api_key
@decision_id = decision_id
@configs = configs
@getter = Utils::HashGetter.new(configs)
@client_class = client_class
end

def run
Expand All @@ -58,7 +59,8 @@ def run
def send_request
Router.post(path, {
body: request_body,
headers: headers
headers: headers,
client_class: client_class
})
end

Expand All @@ -79,6 +81,8 @@ def errors
validator.valid_order?
elsif applying_to_session?
validator.valid_session?
elsif applying_to_content?
validator.valid_content?
else
validator.valid_user?
end
Expand Down
Loading