From af9cce5c1418bee0f8a7e48b7b4306f1459a648e Mon Sep 17 00:00:00 2001 From: Michel Goldstein Date: Mon, 9 Feb 2026 16:54:12 -0800 Subject: [PATCH 1/8] Bumping version of httparty to ensure protection from CVE-2025-68696 --- lib/sift/client.rb | 56 +++++++++++++++++++++++++--------------------- sift.gemspec | 2 +- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/lib/sift/client.rb b/lib/sift/client.rb index b510ae3..0f4652c 100644 --- a/lib/sift/client.rb +++ b/lib/sift/client.rb @@ -88,15 +88,26 @@ def original_request end end + # Internal HTTParty client for api.siftscience.com endpoints + # Handles Events, Labels, Scores, PSP Merchant, and Verification APIs + class ApiClient + include HTTParty + base_uri ENV["SIFT_RUBY_API_URL"] || 'https://api.siftscience.com' + end + + # Internal HTTParty client for api3.siftscience.com endpoints + # Handles Decisions and Workflows APIs + class Api3Client + include HTTParty + base_uri ENV["SIFT_RUBY_API3_URL"] || 'https://api3.siftscience.com' + end + # This class wraps accesses through the API # class Client API_ENDPOINT = ENV["SIFT_RUBY_API_URL"] || 'https://api.siftscience.com' API3_ENDPOINT = ENV["SIFT_RUBY_API3_URL"] || 'https://api3.siftscience.com' - include HTTParty - base_uri API_ENDPOINT - attr_reader :api_key, :account_id def self.build_auth_header(api_key) @@ -256,7 +267,7 @@ def track(event, properties = {}, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = self.class.post(path, options) + response = ApiClient.post(path, options) Response.new(response.body, response.code, response.response) end @@ -319,7 +330,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 = ApiClient.get(Sift.score_api_path(user_id, version), options) Response.new(response.body, response.code, response.response) end @@ -382,7 +393,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 = ApiClient.get(Sift.user_score_api_path(user_id, @version), options) Response.new(response.body, response.code, response.response) end @@ -434,7 +445,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 = ApiClient.post(Sift.user_score_api_path(user_id, @version), options) Response.new(response.body, response.code, response.response) end @@ -532,7 +543,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 = ApiClient.delete(Sift.users_label_api_path(user_id, version), options) Response.new(response.body, response.code, response.response) end @@ -569,8 +580,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 = Api3Client.get(Sift.workflow_status_path(account_id, run_id), options) Response.new(response.body, response.code, response.response) end @@ -607,8 +617,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 = Api3Client.get(Sift.user_decisions_api_path(account_id, user_id), options) Response.new(response.body, response.code, response.response) end @@ -645,8 +654,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 = Api3Client.get(Sift.order_decisions_api_path(account_id, order_id), options) Response.new(response.body, response.code, response.response) end @@ -685,8 +693,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 = Api3Client.get(Sift.session_decisions_api_path(account_id, user_id, session_id), options) Response.new(response.body, response.code, response.response) end @@ -725,8 +732,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 = Api3Client.get(Sift.content_decisions_api_path(account_id, user_id, content_id), options) Response.new(response.body, response.code, response.response) end @@ -768,7 +774,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 = ApiClient.post(Sift.verification_api_send_path(@version), options) Response.new(response.body, response.code, response.response) end @@ -787,7 +793,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 = ApiClient.post(Sift.verification_api_resend_path(@version), options) Response.new(response.body, response.code, response.response) end @@ -806,7 +812,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 = ApiClient.post(Sift.verification_api_check_path(@version), options) Response.new(response.body, response.code, response.response) end @@ -831,7 +837,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 = ApiClient.post(Sift.psp_merchant_api_path(account_id), options) Response.new(response.body, response.code, response.response) end @@ -858,7 +864,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 = ApiClient.put(Sift.psp_merchant_id_api_path(account_id, merchant_id), options) Response.new(response.body, response.code, response.response) end @@ -882,7 +888,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 = ApiClient.get(Sift.psp_merchant_id_api_path(account_id, merchant_id), options) Response.new(response.body, response.code, response.response) end @@ -911,7 +917,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 = ApiClient.get(Sift.psp_merchant_api_path(account_id), options) Response.new(response.body, response.code, response.response) end diff --git a/sift.gemspec b/sift.gemspec index 6900b76..58e941d 100644 --- a/sift.gemspec +++ b/sift.gemspec @@ -26,7 +26,7 @@ Gem::Specification.new do |s| s.add_development_dependency "webmock", ">= 1.16.0", "< 2" # Gems that must be intalled for sift to work - s.add_dependency "httparty", ">= 0.11.0" + s.add_dependency "httparty", ">= 0.23.3" s.add_dependency "multi_json", ">= 1.0" s.add_development_dependency("rake") From a164d448f5fe56ed2a2ce362cc5896b604574469 Mon Sep 17 00:00:00 2001 From: Michel Goldstein Date: Mon, 9 Feb 2026 16:59:58 -0800 Subject: [PATCH 2/8] Bump version --- HISTORY | 3 +++ lib/sift/version.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY b/HISTORY index cd01ca6..071a28f 100644 --- a/HISTORY +++ b/HISTORY @@ -1,3 +1,6 @@ +=== 4.6.0 2026-02-10 +- Bump the minimum version of httparty to 0.23.3 to ensure protection against CVE-2025-68696 + === 4.5.1 2025-04-07 - Fix Verification URLs diff --git a/lib/sift/version.rb b/lib/sift/version.rb index 659643f..9f7968d 100644 --- a/lib/sift/version.rb +++ b/lib/sift/version.rb @@ -1,5 +1,5 @@ module Sift - VERSION = "4.5.1" + VERSION = "4.6.0" API_VERSION = "205" VERIFICATION_API_VERSION = "1.1" end From c2aff9fed8d136495f43ecc4759da3161dda9509 Mon Sep 17 00:00:00 2001 From: Michel Goldstein Date: Mon, 9 Feb 2026 17:22:08 -0800 Subject: [PATCH 3/8] Implement PR feedback --- HISTORY | 2 ++ lib/sift/client.rb | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/HISTORY b/HISTORY index 071a28f..0e428a3 100644 --- a/HISTORY +++ b/HISTORY @@ -1,5 +1,7 @@ === 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 === 4.5.1 2025-04-07 - Fix Verification URLs diff --git a/lib/sift/client.rb b/lib/sift/client.rb index 0f4652c..063249a 100644 --- a/lib/sift/client.rb +++ b/lib/sift/client.rb @@ -108,6 +108,10 @@ 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) From 096a6cc6cae3eb2a9cc5996aaac47f4fce2391a9 Mon Sep 17 00:00:00 2001 From: Michel Goldstein Date: Tue, 10 Feb 2026 15:59:50 -0800 Subject: [PATCH 4/8] Increase min Ruby version to 2.7.0 --- .github/workflows/ci.yml | 4 ++-- HISTORY | 1 + README.md | 2 +- sift.gemspec | 10 ++++++---- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 193c3a2..a27f778 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/HISTORY b/HISTORY index 0e428a3..4c6d5e8 100644 --- a/HISTORY +++ b/HISTORY @@ -2,6 +2,7 @@ - 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 diff --git a/README.md b/README.md index 274ffa1..1b92c2d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/sift.gemspec b/sift.gemspec index 58e941d..682261c 100644 --- a/sift.gemspec +++ b/sift.gemspec @@ -7,10 +7,10 @@ Gem::Specification.new do |s| s.version = Sift::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Fred Sadaghiani", "Yoav Schatzberg", "Jacob Burnim"] - s.email = ["support@siftscience.com"] - s.homepage = "http://siftscience.com" - s.summary = %q{Sift Science Ruby API Gem} - s.description = %q{Sift Science Ruby API. Please see http://siftscience.com for more details.} + s.email = ["support@sift.com"] + s.homepage = "http://sift.com" + s.summary = %q{Sift Ruby API Gem} + s.description = %q{Sift Ruby API. Please see http://sift.com for more details.} s.rubyforge_project = "sift" @@ -19,6 +19,8 @@ Gem::Specification.new do |s| s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] + s.required_ruby_version = '>= 2.7.0' + # Gems that must be intalled for sift to compile and build s.add_development_dependency "rspec", "~> 3.5" s.add_development_dependency "rspec_junit_formatter" From b4b50e3fb4cfa1873dd27ed8295ffa31cdec92db Mon Sep 17 00:00:00 2001 From: Michel Goldstein Date: Tue, 10 Feb 2026 17:12:45 -0800 Subject: [PATCH 5/8] Refactor: Client and Router inherit from shared HTTParty config parent --- lib/sift/client.rb | 18 +++++++++++++++--- lib/sift/router.rb | 7 ++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/sift/client.rb b/lib/sift/client.rb index b510ae3..bd86035 100644 --- a/lib/sift/client.rb +++ b/lib/sift/client.rb @@ -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 @@ -943,4 +940,19 @@ def delete_nils(properties) end end end + + require_relative "./client/decision" + require_relative "./error" + + # Internal HTTParty client for api.siftscience.com endpoints + # Handles Events, Labels, Scores, PSP Merchant, and Verification APIs + class ApiClient < Client + base_uri API_ENDPOINT + end + + # Internal HTTParty client for api3.siftscience.com endpoints + # Handles Decisions and Workflows APIs + class Api3Client < Client + base_uri API3_ENDPOINT + end end diff --git a/lib/sift/router.rb b/lib/sift/router.rb index 69e4015..20f5354 100644 --- a/lib/sift/router.rb +++ b/lib/sift/router.rb @@ -2,17 +2,18 @@ require_relative "./client" module Sift - class Router - include HTTParty + class Router < Client class << self def get(path, options = {}) + options[:base_uri] = nil serialize_body(options) add_default_headers(options) wrap_response(super(path, options)) end def post(path, options = {}) + options[:base_uri] = nil serialize_body(options) add_default_headers(options) wrap_response(super(path, options)) @@ -38,4 +39,4 @@ def wrap_response(response) end end -end +end \ No newline at end of file From 73c98d0ec0648f76fac8073a08eb6947bbe0925e Mon Sep 17 00:00:00 2001 From: Michel Goldstein Date: Thu, 12 Feb 2026 16:35:22 -0800 Subject: [PATCH 6/8] New approach to client resolution --- lib/sift/client.rb | 91 +++++++++++++--------------- lib/sift/client/decision.rb | 13 ++-- lib/sift/client/decision/apply_to.rb | 10 ++- lib/sift/router.rb | 8 ++- 4 files changed, 61 insertions(+), 61 deletions(-) diff --git a/lib/sift/client.rb b/lib/sift/client.rb index 654cb42..194b385 100644 --- a/lib/sift/client.rb +++ b/lib/sift/client.rb @@ -85,20 +85,6 @@ def original_request end end - # Internal HTTParty client for api.siftscience.com endpoints - # Handles Events, Labels, Scores, PSP Merchant, and Verification APIs - class ApiClient - include HTTParty - base_uri ENV["SIFT_RUBY_API_URL"] || 'https://api.siftscience.com' - end - - # Internal HTTParty client for api3.siftscience.com endpoints - # Handles Decisions and Workflows APIs - class Api3Client - include HTTParty - base_uri ENV["SIFT_RUBY_API3_URL"] || 'https://api3.siftscience.com' - end - # This class wraps accesses through the API # class Client @@ -111,12 +97,30 @@ class Client 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 user_agent + "sift-ruby/#{VERSION}" + end - def self.user_agent - "sift-ruby/#{VERSION}" + # 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 @@ -152,7 +156,7 @@ def initialize(opts = {}) @account_id = opts[:account_id] || Sift.account_id @version = opts[:version] || API_VERSION @verification_version = opts[:verification_version] || VERIFICATION_API_VERSION - @timeout = opts[:timeout] || 2 # 2-second timeout by default + @timeout = opts[:timeout] @path = opts[:path] || Sift.rest_api_path(@version) raise("api_key") if !@api_key.is_a?(String) || @api_key.empty? @@ -268,7 +272,7 @@ def track(event, properties = {}, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = ApiClient.post(path, options) + response = self.class.api_client.post(path, options) Response.new(response.body, response.code, response.response) end @@ -331,7 +335,7 @@ def score(user_id, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = ApiClient.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 @@ -394,7 +398,7 @@ def get_user_score(user_id, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = ApiClient.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 @@ -446,7 +450,7 @@ def rescore_user(user_id, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = ApiClient.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 @@ -544,7 +548,7 @@ def unlabel(user_id, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = ApiClient.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 @@ -581,7 +585,7 @@ def get_workflow_status(run_id, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = Api3Client.get(Sift.workflow_status_path(account_id, run_id), 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 @@ -618,7 +622,7 @@ def get_user_decisions(user_id, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = Api3Client.get(Sift.user_decisions_api_path(account_id, user_id), 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 @@ -655,7 +659,7 @@ def get_order_decisions(order_id, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = Api3Client.get(Sift.order_decisions_api_path(account_id, order_id), 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 @@ -694,7 +698,7 @@ def get_session_decisions(user_id, session_id, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = Api3Client.get(Sift.session_decisions_api_path(account_id, user_id, session_id), 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 @@ -733,7 +737,7 @@ def get_content_decisions(user_id, content_id, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = Api3Client.get(Sift.content_decisions_api_path(account_id, user_id, content_id), 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 @@ -775,7 +779,7 @@ def verification_send(properties = {}, opts = {}) :headers => build_default_headers_post(api_key) } options.merge!(:timeout => timeout) unless timeout.nil? - response = ApiClient.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 @@ -794,7 +798,7 @@ def verification_resend(properties = {}, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = ApiClient.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 @@ -813,7 +817,7 @@ def verification_check(properties = {}, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = ApiClient.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 @@ -838,7 +842,7 @@ def create_psp_merchant_profile(properties = {}, opts = {}) :basic_auth => { :username => api_key, :password => "" } } options.merge!(:timeout => timeout) unless timeout.nil? - response = ApiClient.post(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 @@ -865,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 = ApiClient.put(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 @@ -889,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 = ApiClient.get(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 @@ -918,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 = ApiClient.get(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 @@ -933,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) @@ -954,15 +958,4 @@ def delete_nils(properties) require_relative "./client/decision" require_relative "./error" - # Internal HTTParty client for api.siftscience.com endpoints - # Handles Events, Labels, Scores, PSP Merchant, and Verification APIs - class ApiClient < Client - base_uri API_ENDPOINT - end - - # Internal HTTParty client for api3.siftscience.com endpoints - # Handles Decisions and Workflows APIs - class Api3Client < Client - base_uri API3_ENDPOINT - end end diff --git a/lib/sift/client/decision.rb b/lib/sift/client/decision.rb index 2054197..09a3ab2 100644 --- a/lib/sift/client/decision.rb +++ b/lib/sift/client/decision.rb @@ -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 = {}) @@ -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 @@ -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 @@ -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 @@ -63,4 +65,3 @@ def auth_header end end end - diff --git a/lib/sift/client/decision/apply_to.rb b/lib/sift/client/decision/apply_to.rb index b4274f8..d62e874 100644 --- a/lib/sift/client/decision/apply_to.rb +++ b/lib/sift/client/decision/apply_to.rb @@ -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 %{ @@ -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 @@ -58,7 +59,8 @@ def run def send_request Router.post(path, { body: request_body, - headers: headers + headers: headers, + client_class: client_class }) end @@ -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 diff --git a/lib/sift/router.rb b/lib/sift/router.rb index 20f5354..1da1888 100644 --- a/lib/sift/router.rb +++ b/lib/sift/router.rb @@ -6,17 +6,19 @@ 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) @@ -39,4 +41,4 @@ def wrap_response(response) end end -end \ No newline at end of file +end From f80a7dd0a46556d00c05615a329e379c4ef175a4 Mon Sep 17 00:00:00 2001 From: Michel Goldstein Date: Thu, 12 Feb 2026 16:42:09 -0800 Subject: [PATCH 7/8] Add new unit test for configurations --- spec/unit/configuration_spec.rb | 67 +++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 spec/unit/configuration_spec.rb diff --git a/spec/unit/configuration_spec.rb b/spec/unit/configuration_spec.rb new file mode 100644 index 0000000..8717f9e --- /dev/null +++ b/spec/unit/configuration_spec.rb @@ -0,0 +1,67 @@ +require_relative "../spec_helper" +require "sift" +require "logger" + +describe "Sift::Client Configuration Patterns" do + let(:api_key) { "test_api_key" } + + it "propagates global Sift::Client configuration to internal clients" do + Sift::Client.default_timeout 5 + + # Internal executors should inherit this + expect(Sift::Client.api_client.default_options[:timeout]).to eq(5) + expect(Sift::Client.api3_client.default_options[:timeout]).to eq(5) + + # Reset + Sift::Client.default_timeout 2 + end + + it "allows independent subclass configurations" do + class SubclassA < Sift::Client; end + class SubclassB < Sift::Client; end + + SubclassA.default_timeout 10 + SubclassB.default_timeout 20 + + expect(SubclassA.api_client.default_options[:timeout]).to eq(10) + expect(SubclassB.api_client.default_options[:timeout]).to eq(20) + + # Ensure they didn't leak to parent + expect(Sift::Client.api_client.default_options[:timeout]).to be <= 5 + end + + it "propagates complex settings like loggers to subclasses" do + class LoggingClient < Sift::Client; end + + logger = Logger.new(nil) + LoggingClient.logger logger, :debug, :curl + + expect(LoggingClient.api_client.default_options[:logger]).to eq(logger) + expect(LoggingClient.api_client.default_options[:log_level]).to eq(:debug) + expect(LoggingClient.api_client.default_options[:log_format]).to eq(:curl) + + # Ensure Sift::Client remains untouched + expect(Sift::Client.api_client.default_options[:logger]).to be_nil + end + + it "respects inheritance chain for Decisions and Router" do + class DecisionClient < Sift::Client; end + DecisionClient.default_timeout 15 + + client = DecisionClient.new(api_key: api_key, account_id: "acc") + + # Verify the executor used by the Router is the one from DecisionClient + expect(DecisionClient.api3_client.default_options[:timeout]).to eq(15) + + # We want to ensure that when Router.get is called, it uses DecisionClient.api3_client + # In lib/sift/router.rb, we have: + # wrap_response(client_class.api3_client.get(path, options)) + + expect(DecisionClient.api3_client).to receive(:get).and_call_original + + # Mock the request to avoid network calls + stub_request(:get, /api3.siftscience.com/) + + client.get_user_decisions("user_1") + end +end From acb74bcec75a1ce9e9d70eac88f0dce257b1eb91 Mon Sep 17 00:00:00 2001 From: Michel Goldstein Date: Thu, 12 Feb 2026 17:12:06 -0800 Subject: [PATCH 8/8] Minor fixes --- lib/sift/client.rb | 8 ++--- spec/unit/client_validationapi_spec.rb | 46 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/sift/client.rb b/lib/sift/client.rb index 194b385..167d510 100644 --- a/lib/sift/client.rb +++ b/lib/sift/client.rb @@ -156,7 +156,7 @@ def initialize(opts = {}) @account_id = opts[:account_id] || Sift.account_id @version = opts[:version] || API_VERSION @verification_version = opts[:verification_version] || VERIFICATION_API_VERSION - @timeout = opts[:timeout] + @timeout = opts[:timeout] || 2 # 2-second timeout by default @path = opts[:path] || Sift.rest_api_path(@version) raise("api_key") if !@api_key.is_a?(String) || @api_key.empty? @@ -779,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.api_client.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 @@ -798,7 +798,7 @@ def verification_resend(properties = {}, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = self.class.api_client.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 @@ -817,7 +817,7 @@ def verification_check(properties = {}, opts = {}) } options.merge!(:timeout => timeout) unless timeout.nil? - response = self.class.api_client.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 diff --git a/spec/unit/client_validationapi_spec.rb b/spec/unit/client_validationapi_spec.rb index 5d79d53..a006606 100644 --- a/spec/unit/client_validationapi_spec.rb +++ b/spec/unit/client_validationapi_spec.rb @@ -88,4 +88,50 @@ def valid_check_properties end + # Regression tests for verification API version bug fix + # These tests ensure verification methods use verification_version (1.1) + # instead of the events API version (205) + + it "Uses verification API version (1.1) not events API version (205) for verification_send" do + api_key = "test_key" + response_json = { :status => 0, :error_message => "OK"} + + # Should call v1.1, NOT v205 + stub_request(:post, "https://test_key:@api.siftscience.com/v1.1/verification/send") + .to_return(:status => 200, :body => MultiJson.dump(response_json)) + + # Client defaults: version=205 (events), verification_version=1.1 + response = Sift::Client.new(:api_key => api_key).verification_send(valid_send_properties) + + expect(response.ok?).to eq(true) + end + + it "Uses verification API version (1.1) not events API version (205) for verification_resend" do + api_key = "test_key" + response_json = { :status => 0, :error_message => "OK"} + + # Should call v1.1, NOT v205 + stub_request(:post, "https://test_key:@api.siftscience.com/v1.1/verification/resend") + .to_return(:status => 200, :body => MultiJson.dump(response_json)) + + # Client defaults: version=205 (events), verification_version=1.1 + response = Sift::Client.new(:api_key => api_key).verification_resend(valid_resend_properties) + + expect(response.ok?).to eq(true) + end + + it "Uses verification API version (1.1) not events API version (205) for verification_check" do + api_key = "test_key" + response_json = { :status => 0, :error_message => "OK"} + + # Should call v1.1, NOT v205 + stub_request(:post, "https://test_key:@api.siftscience.com/v1.1/verification/check") + .to_return(:status => 200, :body => MultiJson.dump(response_json)) + + # Client defaults: version=205 (events), verification_version=1.1 + response = Sift::Client.new(:api_key => api_key).verification_check(valid_check_properties) + + expect(response.ok?).to eq(true) + end + end