Skip to content
Draft
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.0']
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.0' ]
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
77 changes: 46 additions & 31 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.encode64(api_key)}" }
end

def self.user_agent
"sift-ruby/#{VERSION}"
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 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 @@ -382,7 +398,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 @@ -434,7 +450,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 +548,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 +585,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 +622,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 +659,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 +698,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 +737,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 Down Expand Up @@ -768,7 +779,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 +798,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 +817,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 +842,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 +869,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 +893,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 +922,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 +937,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 +954,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
11 changes: 7 additions & 4 deletions lib/sift/router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@
require_relative "./client"

module Sift
class Router
include HTTParty
class Router < Client

class << self
def get(path, options = {})
client_class = options.delete(:client_class) || Sift::Client
options[:base_uri] = nil
serialize_body(options)
add_default_headers(options)
wrap_response(super(path, options))
wrap_response(client_class.api3_client.get(path, options))
end

def post(path, options = {})
client_class = options.delete(:client_class) || Sift::Client
options[:base_uri] = nil
serialize_body(options)
add_default_headers(options)
wrap_response(super(path, options))
wrap_response(client_class.api3_client.post(path, options))
end

def serialize_body(options)
Expand Down
2 changes: 1 addition & 1 deletion lib/sift/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Sift
VERSION = "4.5.1"
VERSION = "4.6.0"
API_VERSION = "205"
VERIFICATION_API_VERSION = "1.1"
end
Loading