From 771924c786439eacb4276c45de585b1550929555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Tue, 5 May 2026 16:53:10 +0200 Subject: [PATCH 1/5] fix: use correct base URLs for forward and identity services --- lib/checkout_sdk/checkout_api.rb | 8 ++++++- lib/checkout_sdk/environment.rb | 9 +++++++- lib/checkout_sdk/environment_subdomain.rb | 2 +- .../configuration/configuration_spec.rb | 23 +++++++++++++++++-- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/lib/checkout_sdk/checkout_api.rb b/lib/checkout_sdk/checkout_api.rb index ad2fef7..eae5634 100644 --- a/lib/checkout_sdk/checkout_api.rb +++ b/lib/checkout_sdk/checkout_api.rb @@ -101,7 +101,7 @@ def initialize(configuration) @payment_sessions = CheckoutSdk::Payments::PaymentSessionsClient.new api_client, configuration @payments_setups = CheckoutSdk::Payments::PaymentSetupsClient.new api_client, configuration @flow = CheckoutSdk::Payments::FlowClient.new api_client, configuration - @forward = CheckoutSdk::Forward::ForwardClient.new(api_client, configuration) + @forward = CheckoutSdk::Forward::ForwardClient.new(forward_client(configuration), configuration) end private @@ -134,5 +134,11 @@ def balances_client(configuration) def transfers_client(configuration) ApiClient.new(configuration, configuration.environment.transfers_uri) end + + # @param [CheckoutConfiguration] configuration + # @return [ApiClient] + def forward_client(configuration) + ApiClient.new(configuration, configuration.environment.forward_uri) + end end end diff --git a/lib/checkout_sdk/environment.rb b/lib/checkout_sdk/environment.rb index ec18d36..d4b58c3 100644 --- a/lib/checkout_sdk/environment.rb +++ b/lib/checkout_sdk/environment.rb @@ -11,6 +11,8 @@ module CheckoutSdk # @return [String] # @!attribute balances_uri # @return [String] + # @!attribute forward_uri + # @return [String] # @!attribute is_sandbox # @return [String] class Environment @@ -19,6 +21,7 @@ class Environment :files_uri, :transfers_uri, :balances_uri, + :forward_uri, :is_sandbox # @param [String] base_uri @@ -26,13 +29,15 @@ class Environment # @param [String] files_uri # @param [String] transfers_uri # @param [String] balances_uri + # @param [String] forward_uri # @param [TrueClass, FalseClass] is_sandbox - def initialize(base_uri, authorization_uri, files_uri, transfers_uri, balances_uri, is_sandbox) + def initialize(base_uri, authorization_uri, files_uri, transfers_uri, balances_uri, forward_uri, is_sandbox) @base_uri = base_uri @authorization_uri = authorization_uri @files_uri = files_uri @transfers_uri = transfers_uri @balances_uri = balances_uri + @forward_uri = forward_uri @is_sandbox = is_sandbox end @@ -43,6 +48,7 @@ def self.sandbox 'https://files.sandbox.checkout.com/', 'https://transfers.sandbox.checkout.com/', 'https://balances.sandbox.checkout.com/', + 'https://forward.sandbox.checkout.com/', true) end @@ -53,6 +59,7 @@ def self.production 'https://files.checkout.com/', 'https://transfers.checkout.com/', 'https://balances.checkout.com/', + 'https://forward.checkout.com/', false) end end diff --git a/lib/checkout_sdk/environment_subdomain.rb b/lib/checkout_sdk/environment_subdomain.rb index c0fd190..e6b61fb 100644 --- a/lib/checkout_sdk/environment_subdomain.rb +++ b/lib/checkout_sdk/environment_subdomain.rb @@ -33,7 +33,7 @@ def initialize(environment, subdomain) def create_url_with_subdomain(original_url, subdomain) new_environment = original_url - if subdomain =~ /^[0-9a-z]+$/ + if subdomain =~ /^[a-z0-9]+(-[a-z0-9]+)*$/ url_parts = URI.parse(original_url) new_host = "#{subdomain}.#{url_parts.host}" diff --git a/spec/checkout_sdk/configuration/configuration_spec.rb b/spec/checkout_sdk/configuration/configuration_spec.rb index 38721e1..f3f19cd 100644 --- a/spec/checkout_sdk/configuration/configuration_spec.rb +++ b/spec/checkout_sdk/configuration/configuration_spec.rb @@ -26,16 +26,31 @@ class FakeLogger expect(configuration.credentials).to eq(@credentials) expect(configuration.environment.base_uri).to eq(CheckoutSdk::Environment.sandbox.base_uri) expect(configuration.environment.base_uri).to eq("https://api.sandbox.checkout.com/") + expect(configuration.environment.forward_uri).to eq("https://forward.sandbox.checkout.com/") expect(configuration.http_client).to eq(@http_client) expect(configuration.environment_subdomain).to be_nil end + it 'should have correct forward URL for production' do + configuration = CheckoutSdk::CheckoutConfiguration.new( + @credentials, + CheckoutSdk::Environment.production, + @http_client, + @multipart_http_client, + @logger + ) + + expect(configuration.environment.forward_uri).to eq("https://forward.checkout.com/") + end + [ %w[a https://a.api.sandbox.checkout.com/], %w[ab https://ab.api.sandbox.checkout.com/], %w[abc https://abc.api.sandbox.checkout.com/], %w[abc1 https://abc1.api.sandbox.checkout.com/], - %w[12345domain https://12345domain.api.sandbox.checkout.com/] + %w[12345domain https://12345domain.api.sandbox.checkout.com/], + %w[test-123 https://test-123.api.sandbox.checkout.com/], + %w[pl-abc123 https://pl-abc123.api.sandbox.checkout.com/] ].each do |subdomain, expected_url| it "should create configuration with subdomain #{subdomain}" do environment_subdomain = CheckoutSdk::EnvironmentSubdomain.new(CheckoutSdk::Environment.sandbox, subdomain) @@ -63,7 +78,11 @@ class FakeLogger [' ', 'https://api.sandbox.checkout.com/'], [' - ', 'https://api.sandbox.checkout.com/'], ['a b', 'https://api.sandbox.checkout.com/'], - ['ab bc1', 'https://api.sandbox.checkout.com/'] + ['ab bc1', 'https://api.sandbox.checkout.com/'], + ['foo-', 'https://api.sandbox.checkout.com/'], + ['-foo', 'https://api.sandbox.checkout.com/'], + ['FOO', 'https://api.sandbox.checkout.com/'], + ['Foo-Bar', 'https://api.sandbox.checkout.com/'] ].each do |subdomain, expected_url| it "should create configuration with bad subdomain #{subdomain}" do environment_subdomain = CheckoutSdk::EnvironmentSubdomain.new(CheckoutSdk::Environment.sandbox, subdomain) From 5197b0a65e796a297159144dc5d767547156b332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Tue, 5 May 2026 16:53:10 +0200 Subject: [PATCH 2/5] chore: add .claude/, .cursor/ and CLAUDE.md to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 4bcbc48..6ae28b6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ # rspec failure tracking .rspec_status .idea + +.claude/ +.cursor/ +CLAUDE.md From 600d31ddc5f0e130262fd87952a953c7c11e36c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Mon, 11 May 2026 16:07:16 +0200 Subject: [PATCH 3/5] fix: use File.open block form to guarantee file descriptor closure RuboCop Style/FileOpen flagged ApiClient#upload because File.open without a block could leak the file descriptor if build_multipart_request raises before the ensure block is entered. Wrap the file lifecycle in a block so closure is guaranteed even if the multipart builder fails. --- lib/checkout_sdk/api_client.rb | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/checkout_sdk/api_client.rb b/lib/checkout_sdk/api_client.rb index ca44567..3c51dc3 100644 --- a/lib/checkout_sdk/api_client.rb +++ b/lib/checkout_sdk/api_client.rb @@ -92,17 +92,14 @@ def build_multipart_request(file_request, file) def upload(path, authorization, file_request) headers = default_headers(authorization) - file = File.open(file_request.file) - - form = build_multipart_request(file_request, file) - - begin - @log.info "post: /#{path}" - response = @multipart_client.run_request(:post, path, form, headers) - rescue Faraday::ClientError => e - raise CheckoutApiException, e.response - ensure - file.close + response = File.open(file_request.file) do |file| + form = build_multipart_request(file_request, file) + begin + @log.info "post: /#{path}" + @multipart_client.run_request(:post, path, form, headers) + rescue Faraday::ClientError => e + raise CheckoutApiException, e.response + end end parse_response(response) From 82866e869e67bef536a5a31a86368a3735a5e0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Mon, 11 May 2026 17:51:19 +0200 Subject: [PATCH 4/5] fix: tighten subdomain regex to PrivateLink prefix format Per the AWS PrivateLink docs (https://www.checkout.com/docs/developer-resources/api/private-connections/aws-privatelink), the valid subdomain is the first eight characters of the client_id (alphanumeric only), optionally with the literal pl- prefix when calling through PrivateLink. Tighten the regex from RFC-1123-style hyphenated to /^(?:pl-)?[a-z0-9]+$/ and update the test corpus: test-123 moves to the rejected list, pl-vkuhvk4v (the docs example) joins the accepted list, and pl-, foo-bar are added as rejected. --- lib/checkout_sdk/environment_subdomain.rb | 2 +- spec/checkout_sdk/configuration/configuration_spec.rb | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/checkout_sdk/environment_subdomain.rb b/lib/checkout_sdk/environment_subdomain.rb index e6b61fb..f5dab5f 100644 --- a/lib/checkout_sdk/environment_subdomain.rb +++ b/lib/checkout_sdk/environment_subdomain.rb @@ -33,7 +33,7 @@ def initialize(environment, subdomain) def create_url_with_subdomain(original_url, subdomain) new_environment = original_url - if subdomain =~ /^[a-z0-9]+(-[a-z0-9]+)*$/ + if subdomain =~ /^(?:pl-)?[a-z0-9]+$/ url_parts = URI.parse(original_url) new_host = "#{subdomain}.#{url_parts.host}" diff --git a/spec/checkout_sdk/configuration/configuration_spec.rb b/spec/checkout_sdk/configuration/configuration_spec.rb index f3f19cd..314375c 100644 --- a/spec/checkout_sdk/configuration/configuration_spec.rb +++ b/spec/checkout_sdk/configuration/configuration_spec.rb @@ -49,7 +49,7 @@ class FakeLogger %w[abc https://abc.api.sandbox.checkout.com/], %w[abc1 https://abc1.api.sandbox.checkout.com/], %w[12345domain https://12345domain.api.sandbox.checkout.com/], - %w[test-123 https://test-123.api.sandbox.checkout.com/], + %w[pl-vkuhvk4v https://pl-vkuhvk4v.api.sandbox.checkout.com/], %w[pl-abc123 https://pl-abc123.api.sandbox.checkout.com/] ].each do |subdomain, expected_url| it "should create configuration with subdomain #{subdomain}" do @@ -82,7 +82,10 @@ class FakeLogger ['foo-', 'https://api.sandbox.checkout.com/'], ['-foo', 'https://api.sandbox.checkout.com/'], ['FOO', 'https://api.sandbox.checkout.com/'], - ['Foo-Bar', 'https://api.sandbox.checkout.com/'] + ['Foo-Bar', 'https://api.sandbox.checkout.com/'], + ['test-123', 'https://api.sandbox.checkout.com/'], + ['foo-bar', 'https://api.sandbox.checkout.com/'], + ['pl-', 'https://api.sandbox.checkout.com/'] ].each do |subdomain, expected_url| it "should create configuration with bad subdomain #{subdomain}" do environment_subdomain = CheckoutSdk::EnvironmentSubdomain.new(CheckoutSdk::Environment.sandbox, subdomain) From aab9c645786c18b110f11996c914050bc08841c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Tue, 12 May 2026 11:09:05 +0200 Subject: [PATCH 5/5] feat(oauth): add FORWARD_SECRETS and IDENTITY_VERIFICATION scope constants Per the swagger spec, /forward/secrets endpoints require the forward:secrets scope in addition to forward, and all identity endpoints (applicants, identity-verifications, aml-verifications, face-authentications, id-document-verifications) require the identity-verification scope. Expose both as typed constants so OAuth clients can request them without hardcoding the strings. --- lib/checkout_sdk/oauth_scopes.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/checkout_sdk/oauth_scopes.rb b/lib/checkout_sdk/oauth_scopes.rb index c7ff65c..01d4798 100644 --- a/lib/checkout_sdk/oauth_scopes.rb +++ b/lib/checkout_sdk/oauth_scopes.rb @@ -48,5 +48,7 @@ module OAuthScopes ISSUING_CONTROLS_READ = 'issuing:controls-read' ISSUING_CONTROLS_WRITE = 'issuing:controls-write' FORWARD = 'forward' + FORWARD_SECRETS = 'forward:secrets' + IDENTITY_VERIFICATION = 'identity-verification' end end