From c82abe8bd6c5f04cc2e20a28e358f0715d881dd6 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:43:02 +0200 Subject: [PATCH 1/4] fix: use correct base URLs for forward and identity services --- .../java/com/checkout/CheckoutApiImpl.java | 50 ++++++++++++++++--- .../java/com/checkout/CustomEnvironment.java | 4 ++ src/main/java/com/checkout/Environment.java | 10 ++++ .../com/checkout/EnvironmentSubdomain.java | 2 +- src/main/java/com/checkout/IEnvironment.java | 4 ++ .../DefaultCheckoutConfigurationTest.java | 30 ++++++++++- 6 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java index 6ff3f5760..f8546abf6 100644 --- a/src/main/java/com/checkout/CheckoutApiImpl.java +++ b/src/main/java/com/checkout/CheckoutApiImpl.java @@ -137,12 +137,12 @@ public CheckoutApiImpl(final CheckoutConfiguration configuration) { this.applePayClient = new ApplePayClientImpl(this.apiClient, configuration); this.googlePayClient = new GooglePayClientImpl(this.apiClient, configuration); this.complianceClient = new ComplianceClientImpl(this.apiClient, configuration); - this.forwardClient = new ForwardClientImpl(this.apiClient, configuration); - this.faceAuthenticationClient = new FaceAuthenticationClientImpl(this.apiClient, configuration); - this.applicantClient = new ApplicantClientImpl(this.apiClient, configuration); - this.identityVerificationClient = new IdentityVerificationClientImpl(this.apiClient, configuration); - this.idDocumentVerificationClient = new IdDocumentVerificationClientImpl(this.apiClient, configuration); - this.amlScreeningClient = new AmlScreeningClientImpl(this.apiClient, configuration); + this.forwardClient = new ForwardClientImpl(getForwardClient(configuration), configuration); + this.faceAuthenticationClient = new FaceAuthenticationClientImpl(getIdentityClient(configuration), configuration); + this.applicantClient = new ApplicantClientImpl(getIdentityClient(configuration), configuration); + this.identityVerificationClient = new IdentityVerificationClientImpl(getIdentityClient(configuration), configuration); + this.idDocumentVerificationClient = new IdDocumentVerificationClientImpl(getIdentityClient(configuration), configuration); + this.amlScreeningClient = new AmlScreeningClientImpl(getIdentityClient(configuration), configuration); this.networkTokensClient = new NetworkTokensClientImpl(this.apiClient, configuration); this.standaloneAccountUpdaterClient = new StandaloneAccountUpdaterClientImpl(this.apiClient, configuration); this.agenticCommerceClient = new AgenticCommerceClientImpl(this.apiClient, configuration); @@ -296,6 +296,14 @@ private ApiClient getBalancesClient(final CheckoutConfiguration configuration) { return new ApiClientImpl(configuration, new BalancesApiUriStrategy(configuration)); } + private ApiClient getForwardClient(final CheckoutConfiguration configuration) { + return new ApiClientImpl(configuration, new ForwardApiUriStrategy(configuration)); + } + + private ApiClient getIdentityClient(final CheckoutConfiguration configuration) { + return new ApiClientImpl(configuration, new IdentityApiUriStrategy(configuration)); + } + private static class FilesApiUriStrategy implements UriStrategy { private final CheckoutConfiguration configuration; @@ -341,4 +349,34 @@ public URI getUri() { } + private static class ForwardApiUriStrategy implements UriStrategy { + + private final CheckoutConfiguration configuration; + + private ForwardApiUriStrategy(final CheckoutConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public URI getUri() { + return configuration.getEnvironment().getForwardApi(); + } + + } + + private static class IdentityApiUriStrategy implements UriStrategy { + + private final CheckoutConfiguration configuration; + + private IdentityApiUriStrategy(final CheckoutConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public URI getUri() { + return configuration.getEnvironment().getIdentityApi(); + } + + } + } diff --git a/src/main/java/com/checkout/CustomEnvironment.java b/src/main/java/com/checkout/CustomEnvironment.java index 856f0a9d3..1a0766afc 100644 --- a/src/main/java/com/checkout/CustomEnvironment.java +++ b/src/main/java/com/checkout/CustomEnvironment.java @@ -17,6 +17,10 @@ public final class CustomEnvironment implements IEnvironment { private URI balancesApi; + private URI forwardApi; + + private URI identityApi; + private URI oAuthAuthorizationApi; private boolean sandbox; diff --git a/src/main/java/com/checkout/Environment.java b/src/main/java/com/checkout/Environment.java index 48dd74a16..41ac60e47 100644 --- a/src/main/java/com/checkout/Environment.java +++ b/src/main/java/com/checkout/Environment.java @@ -13,12 +13,16 @@ public enum Environment implements IEnvironment { create("https://files.sandbox.checkout.com/"), create("https://transfers.sandbox.checkout.com/"), create("https://balances.sandbox.checkout.com/"), + create("https://forward.sandbox.checkout.com/"), + create("https://identity-verification.sandbox.checkout.com/"), create("https://access.sandbox.checkout.com/connect/token"), true), PRODUCTION(create("https://api.checkout.com/"), create("https://files.checkout.com/"), create("https://transfers.checkout.com/"), create("https://balances.checkout.com/"), + create("https://forward.checkout.com/"), + create("https://identity-verification.checkout.com/"), create("https://access.checkout.com/connect/token"), false); @@ -26,6 +30,8 @@ public enum Environment implements IEnvironment { private final URI filesApi; private final URI transfersApi; private final URI balancesApi; + private final URI forwardApi; + private final URI identityApi; private final URI oAuthAuthorizationApi; private final boolean sandbox; @@ -33,12 +39,16 @@ public enum Environment implements IEnvironment { final URI filesApi, final URI transfersApi, final URI balancesApi, + final URI forwardApi, + final URI identityApi, final URI oAuthAuthorizationApi, final boolean sandbox) { this.checkoutApi = checkoutApi; this.filesApi = filesApi; this.transfersApi = transfersApi; this.balancesApi = balancesApi; + this.forwardApi = forwardApi; + this.identityApi = identityApi; this.oAuthAuthorizationApi = oAuthAuthorizationApi; this.sandbox = sandbox; } diff --git a/src/main/java/com/checkout/EnvironmentSubdomain.java b/src/main/java/com/checkout/EnvironmentSubdomain.java index 918847350..73befb7cd 100644 --- a/src/main/java/com/checkout/EnvironmentSubdomain.java +++ b/src/main/java/com/checkout/EnvironmentSubdomain.java @@ -40,7 +40,7 @@ private static URI createUrlWithSubdomain(URI originalUrl, String subdomain) { throw new CheckoutException(e); } - Pattern pattern = Pattern.compile("^[0-9a-z]+$"); + Pattern pattern = Pattern.compile("^[a-z0-9]+(-[a-z0-9]+)*$"); Matcher matcher = pattern.matcher(subdomain); if (matcher.matches()) { String host = originalUrl.getHost(); diff --git a/src/main/java/com/checkout/IEnvironment.java b/src/main/java/com/checkout/IEnvironment.java index 3241bfe54..7a6b5a229 100644 --- a/src/main/java/com/checkout/IEnvironment.java +++ b/src/main/java/com/checkout/IEnvironment.java @@ -12,6 +12,10 @@ public interface IEnvironment { URI getBalancesApi(); + URI getForwardApi(); + + URI getIdentityApi(); + URI getOAuthAuthorizationApi(); boolean isSandbox(); diff --git a/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java b/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java index e66e170a4..b5be1f7d3 100644 --- a/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java +++ b/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java @@ -53,7 +53,7 @@ void shouldCreateConfiguration() { } @ParameterizedTest - @ValueSource(strings = {"a", "ab", "abc", "abc1", "12345domain", "a1b2c3d4", "12345678", "abcdefgh", "1234doma"}) + @ValueSource(strings = {"a", "ab", "abc", "abc1", "12345domain", "a1b2c3d4", "12345678", "abcdefgh", "1234doma", "test-123", "pl-abc123", "pl-loquesea", "vkuhvk4v"}) void shouldCreateConfigurationWithSubdomain(String subdomain) { final StaticKeysSdkCredentials credentials = Mockito.mock(StaticKeysSdkCredentials.class); @@ -65,7 +65,7 @@ void shouldCreateConfigurationWithSubdomain(String subdomain) { } @ParameterizedTest - @ValueSource(strings = {"", " ", " ", " - ", "a b", "ab c1"}) + @ValueSource(strings = {"", " ", " ", " - ", "a b", "ab c1", "foo-", "-foo", "ABC123", "FOO"}) void shouldCreateConfigurationWithBadSubdomain(String subdomain) { final StaticKeysSdkCredentials credentials = Mockito.mock(StaticKeysSdkCredentials.class); @@ -88,6 +88,28 @@ void shouldCreateConfigurationWithSubdomainForProduction() { assertEquals("https://" + subdomain + ".access.checkout.com/connect/token", configuration.getEnvironmentSubdomain().getOAuthAuthorizationApi().toString()); } + @Test + void shouldHaveCorrectSandboxUrls() { + + assertEquals(create("https://api.sandbox.checkout.com/"), Environment.SANDBOX.getCheckoutApi()); + assertEquals(create("https://files.sandbox.checkout.com/"), Environment.SANDBOX.getFilesApi()); + assertEquals(create("https://transfers.sandbox.checkout.com/"), Environment.SANDBOX.getTransfersApi()); + assertEquals(create("https://balances.sandbox.checkout.com/"), Environment.SANDBOX.getBalancesApi()); + assertEquals(create("https://forward.sandbox.checkout.com/"), Environment.SANDBOX.getForwardApi()); + assertEquals(create("https://identity-verification.sandbox.checkout.com/"), Environment.SANDBOX.getIdentityApi()); + } + + @Test + void shouldHaveCorrectProductionUrls() { + + assertEquals(create("https://api.checkout.com/"), Environment.PRODUCTION.getCheckoutApi()); + assertEquals(create("https://files.checkout.com/"), Environment.PRODUCTION.getFilesApi()); + assertEquals(create("https://transfers.checkout.com/"), Environment.PRODUCTION.getTransfersApi()); + assertEquals(create("https://balances.checkout.com/"), Environment.PRODUCTION.getBalancesApi()); + assertEquals(create("https://forward.checkout.com/"), Environment.PRODUCTION.getForwardApi()); + assertEquals(create("https://identity-verification.checkout.com/"), Environment.PRODUCTION.getIdentityApi()); + } + @Test void shouldCreateConfiguration_defaultHttpClientBuilderAndExecutor() { @@ -136,6 +158,8 @@ void shouldCreateConfigurationWithCustomEnvironment() { .filesApi(create("https://the.files.uri/")) .transfersApi(create("https://the.transfers.uri/")) .balancesApi(create("https://the.balances.uri/")) + .forwardApi(create("https://the.forward.uri/")) + .identityApi(create("https://the.identity.uri/")) .build(); final StaticKeysSdkCredentials credentials = Mockito.mock(StaticKeysSdkCredentials.class); @@ -147,6 +171,8 @@ void shouldCreateConfigurationWithCustomEnvironment() { assertEquals(environment.getFilesApi(), configuration.getEnvironment().getFilesApi()); assertEquals(environment.getTransfersApi(), configuration.getEnvironment().getTransfersApi()); assertEquals(environment.getBalancesApi(), configuration.getEnvironment().getBalancesApi()); + assertEquals(environment.getForwardApi(), configuration.getEnvironment().getForwardApi()); + assertEquals(environment.getIdentityApi(), configuration.getEnvironment().getIdentityApi()); } /** From 1460e49c5f99f8f0421447b2188c5590e6f1fdd3 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 15:48:37 +0200 Subject: [PATCH 2/4] refactor: reuse identity ApiClient across identity clients Cache the identity ApiClient instance once in the CheckoutApiImpl constructor instead of constructing a new ApiClientImpl (with its own IdentityApiUriStrategy and HTTP client) inside each of the five identity clients. Behaviour unchanged; avoids redundant allocations for the five clients that all target the same identity host. --- src/main/java/com/checkout/CheckoutApiImpl.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/checkout/CheckoutApiImpl.java b/src/main/java/com/checkout/CheckoutApiImpl.java index f8546abf6..87dd3d148 100644 --- a/src/main/java/com/checkout/CheckoutApiImpl.java +++ b/src/main/java/com/checkout/CheckoutApiImpl.java @@ -138,11 +138,12 @@ public CheckoutApiImpl(final CheckoutConfiguration configuration) { this.googlePayClient = new GooglePayClientImpl(this.apiClient, configuration); this.complianceClient = new ComplianceClientImpl(this.apiClient, configuration); this.forwardClient = new ForwardClientImpl(getForwardClient(configuration), configuration); - this.faceAuthenticationClient = new FaceAuthenticationClientImpl(getIdentityClient(configuration), configuration); - this.applicantClient = new ApplicantClientImpl(getIdentityClient(configuration), configuration); - this.identityVerificationClient = new IdentityVerificationClientImpl(getIdentityClient(configuration), configuration); - this.idDocumentVerificationClient = new IdDocumentVerificationClientImpl(getIdentityClient(configuration), configuration); - this.amlScreeningClient = new AmlScreeningClientImpl(getIdentityClient(configuration), configuration); + final ApiClient identityApiClient = getIdentityClient(configuration); + this.faceAuthenticationClient = new FaceAuthenticationClientImpl(identityApiClient, configuration); + this.applicantClient = new ApplicantClientImpl(identityApiClient, configuration); + this.identityVerificationClient = new IdentityVerificationClientImpl(identityApiClient, configuration); + this.idDocumentVerificationClient = new IdDocumentVerificationClientImpl(identityApiClient, configuration); + this.amlScreeningClient = new AmlScreeningClientImpl(identityApiClient, configuration); this.networkTokensClient = new NetworkTokensClientImpl(this.apiClient, configuration); this.standaloneAccountUpdaterClient = new StandaloneAccountUpdaterClientImpl(this.apiClient, configuration); this.agenticCommerceClient = new AgenticCommerceClientImpl(this.apiClient, configuration); From 1a50931ddfe19f7f2804e50bbdcd59cbf62a9458 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:44:58 +0200 Subject: [PATCH 3/4] 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. --- src/main/java/com/checkout/EnvironmentSubdomain.java | 2 +- .../java/com/checkout/DefaultCheckoutConfigurationTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/checkout/EnvironmentSubdomain.java b/src/main/java/com/checkout/EnvironmentSubdomain.java index 73befb7cd..54841a6ed 100644 --- a/src/main/java/com/checkout/EnvironmentSubdomain.java +++ b/src/main/java/com/checkout/EnvironmentSubdomain.java @@ -40,7 +40,7 @@ private static URI createUrlWithSubdomain(URI originalUrl, String subdomain) { throw new CheckoutException(e); } - Pattern pattern = Pattern.compile("^[a-z0-9]+(-[a-z0-9]+)*$"); + Pattern pattern = Pattern.compile("^(?:pl-)?[a-z0-9]+$"); Matcher matcher = pattern.matcher(subdomain); if (matcher.matches()) { String host = originalUrl.getHost(); diff --git a/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java b/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java index b5be1f7d3..e8f9da6e4 100644 --- a/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java +++ b/src/test/java/com/checkout/DefaultCheckoutConfigurationTest.java @@ -53,7 +53,7 @@ void shouldCreateConfiguration() { } @ParameterizedTest - @ValueSource(strings = {"a", "ab", "abc", "abc1", "12345domain", "a1b2c3d4", "12345678", "abcdefgh", "1234doma", "test-123", "pl-abc123", "pl-loquesea", "vkuhvk4v"}) + @ValueSource(strings = {"a", "ab", "abc", "abc1", "12345domain", "a1b2c3d4", "12345678", "abcdefgh", "1234doma", "pl-abc123", "pl-loquesea", "vkuhvk4v", "pl-vkuhvk4v"}) void shouldCreateConfigurationWithSubdomain(String subdomain) { final StaticKeysSdkCredentials credentials = Mockito.mock(StaticKeysSdkCredentials.class); @@ -65,7 +65,7 @@ void shouldCreateConfigurationWithSubdomain(String subdomain) { } @ParameterizedTest - @ValueSource(strings = {"", " ", " ", " - ", "a b", "ab c1", "foo-", "-foo", "ABC123", "FOO"}) + @ValueSource(strings = {"", " ", " ", " - ", "a b", "ab c1", "foo-", "-foo", "ABC123", "FOO", "test-123", "foo-bar", "pl-"}) void shouldCreateConfigurationWithBadSubdomain(String subdomain) { final StaticKeysSdkCredentials credentials = Mockito.mock(StaticKeysSdkCredentials.class); From 620cd0b952faaf488707220f95f7f5a52a62fb70 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:08:58 +0200 Subject: [PATCH 4/4] feat(oauth): add IDENTITY_VERIFICATION scope constant Per the swagger spec, all identity endpoints (applicants, identity-verifications, aml-verifications, face-authentications, id-document-verifications) require the OAuth scope identity-verification. Expose it as a typed enum constant so OAuth clients can request it without hardcoding the string. --- src/main/java/com/checkout/OAuthScope.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/checkout/OAuthScope.java b/src/main/java/com/checkout/OAuthScope.java index 3c53ca693..259668811 100644 --- a/src/main/java/com/checkout/OAuthScope.java +++ b/src/main/java/com/checkout/OAuthScope.java @@ -58,7 +58,8 @@ public enum OAuthScope { VAULT_TOKENIZATION("vault:tokenization"), VAULT_NETWORK_TOKENS("vault:network-tokens"), FORWARD("forward"), - FORWARD_SECRETS("forward:secrets"); + FORWARD_SECRETS("forward:secrets"), + IDENTITY_VERIFICATION("identity-verification"); private final String scope;