diff --git a/README.md b/README.md index 496e135e..79bbf5ef 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,11 @@ The official Node.js SDK for [Checkout.com](https://www.checkout.com) payment ga > **⚠️ Important:** Each Checkout.com account has its own unique base URL prefix. You must configure this prefix when initializing the SDK to connect to your specific account. Find your unique prefix in the [Dashboard → Developers → Overview](https://dashboard.checkout.com/developers). See [Base URL Configuration](#base-url-configuration-account-specific) for details. -> **⚠️ Deprecation Notice:** Initializing the SDK without the `subdomain` parameter is **deprecated** and will be removed in a future major version. Please ensure you provide your account-specific subdomain to avoid disruption when upgrading. +> **⚠️ Breaking change in the next major release:** Initializing the SDK without the `subdomain` parameter used to emit a deprecation warning. It now throws. You must either set `subdomain`, or explicitly opt out with `useLegacyDomain: true`, which is itself deprecated and exists only for emergencies. See [Legacy domain (emergency use only)](#legacy-domain-emergency-use-only). + +### Subdomain value + +Requests must be made through your merchant-specific subdomain (MSSD): the first 8 characters of your client ID (excluding `cli_`). For example, if your client ID is `cli_vkuhvk4vjn2edkps7dfsq6emqm`, your subdomain is `vkuhvk4v`, and the SDK sends requests to `https://vkuhvk4v.api.checkout.com`. Private Link merchants use their `pl-` prefixed subdomain (for example `pl-vkuhvk4v`), which the SDK also accepts. See [Base URLs](https://api-reference.checkout.com/#section/Base-URLs) and [API endpoints](https://www.checkout.com/docs/developer-resources/api/api-endpoints) for further details, and for where to find your unique client ID. # :rocket: Install @@ -166,7 +170,9 @@ const cko = new Checkout(null, { ### Important Notes -> **⚠️ Subdomain is always required:** The `subdomain` option must be passed explicitly when initializing the SDK. It cannot be set via environment variables. Find your unique prefix in [Dashboard → Developers → Overview](https://dashboard.checkout.com/developers). +> **⚠️ Subdomain is always required:** The `subdomain` option must be passed explicitly when initializing the SDK. It cannot be set via environment variables. Find your unique prefix in [Dashboard → Developers → Overview](https://dashboard.checkout.com/developers). Initialization throws a `ValueError` if neither `subdomain` nor `useLegacyDomain` is set, or if both are. + +> A custom `host` replaces the base URL outright, so neither option is required when you pass one. Previous (ABC) keys predate merchant-specific subdomains and are exempt. ## Set custom config Besides the authentication, you also have the option to configure some extra elements about the SDK @@ -499,6 +505,20 @@ You can see examples of how to use the SDK for every endpoint documented in our --- +## Legacy domain (emergency use only) + +> :warning: **Only use if merchant specific sub domains are causing issues.** Connecting through your merchant-specific subdomain (see [Subdomain value](#subdomain-value)) is the supported way of using the Checkout.com API, and non-subdomain usage will be deprecated. + +If, in exceptional circumstances, you cannot use your merchant-specific subdomain, you can explicitly opt out with `useLegacyDomain`: + +```js +const cko = new Checkout('sk_...', { + useLegacyDomain: true // deprecated, emergency fallback only +}); +``` + +This routes requests to `api.checkout.com` (or `api.sandbox.checkout.com`) and `access.checkout.com` (or `access.sandbox.checkout.com`). The option is marked `@deprecated` in the type definitions, so editors and `tsc` will flag it. Exactly one of `subdomain` or `useLegacyDomain` must be set: initialization throws a `ValueError` if both, or neither, are. Passing a custom `host` is a third route that bypasses this requirement entirely, since it replaces the base URL outright. + ## Contributing We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to get started. @@ -512,3 +532,4 @@ MIT License - see [LICENSE](LICENSE) for details. - 📧 Email: [support@checkout.com](mailto:support@checkout.com) - 📚 Documentation: [https://api-reference.checkout.com/](https://api-reference.checkout.com/) - 💬 Community: [GitHub Discussions](https://github.com/checkout/checkout-sdk-node/discussions) + diff --git a/src/EnvironmentSubdomain.js b/src/EnvironmentSubdomain.js index 3a73e097..c6ea48ff 100644 --- a/src/EnvironmentSubdomain.js +++ b/src/EnvironmentSubdomain.js @@ -5,6 +5,7 @@ */ import Environment from './Environment.js'; +import { ValueError } from './services/errors.js'; export default class EnvironmentSubdomain { constructor(environment, subdomain) { @@ -25,29 +26,28 @@ export default class EnvironmentSubdomain { } /** - * Applies subdomain transformation to any given URL. - * If the subdomain is valid (alphanumeric pattern), prepends it to the host. - * Otherwise, returns the original URL unchanged. - * + * Applies subdomain transformation to any given URL by prepending the subdomain to the host. + * * @param {string} originalUrl - the original URL to transform - * @param {string} subdomain - the subdomain to prepend - * @return {string} the transformed URL with subdomain, or original URL if subdomain is invalid + * @param {string} subdomain - the subdomain to prepend + * @return {string} the transformed URL with subdomain + * @throws {ValueError} if the subdomain is not a valid merchant-specific subdomain */ static createUrlWithSubdomain(originalUrl, subdomain) { if (!EnvironmentSubdomain.isValidSubdomain(subdomain)) { - return originalUrl; + throw new ValueError( + 'invalid environment subdomain - provide your merchant-specific subdomain, ' + + 'typically your client ID excluding the cli_ prefix, see ' + + 'https://api-reference.checkout.com/#section/Base-URLs' + ); } - try { - const url = new URL(originalUrl); - const newHost = subdomain + '.' + url.host; - url.host = newHost; - const result = url.toString().trim(); - // Only remove trailing slash if the URL ends with just a slash - return result.endsWith('/') ? result.slice(0, -1) : result; - } catch { - return originalUrl; - } + const url = new URL(originalUrl); + const newHost = subdomain + '.' + url.host; + url.host = newHost; + const result = url.toString().trim(); + // Only remove trailing slash if the URL ends with just a slash + return result.endsWith('/') ? result.slice(0, -1) : result; } /** diff --git a/src/auth-builder.js b/src/auth-builder.js index 6f05a833..70a1992f 100644 --- a/src/auth-builder.js +++ b/src/auth-builder.js @@ -1,6 +1,7 @@ import * as CONFIG from './config.js'; import Environment from './Environment.js'; import EnvironmentSubdomain from './EnvironmentSubdomain.js'; +import { ValueError } from './services/errors.js'; /** * Builds authentication configuration based on keys and options @@ -102,21 +103,11 @@ export class AuthBuilder { const isLive = this.determineEnvironment(key, options); const environment = isLive ? Environment.live() : Environment.sandbox(); - // Create EnvironmentSubdomain if subdomain provided and valid - const environmentSubdomain = - options?.subdomain && EnvironmentSubdomain.isValidSubdomain(options.subdomain) - ? new EnvironmentSubdomain(environment, options.subdomain) - : null; - - // Emit deprecation warning if subdomain is not provided - if (!environmentSubdomain) { - console.warn( - '[DEPRECATION WARNING] Initializing Checkout SDK without a subdomain is deprecated and will be removed in a future version. ' + - 'Please provide your account-specific subdomain using the "subdomain" option. ' + - 'You can find your subdomain in Dashboard → Developers → Overview. ' + - 'Example: new Checkout(key, { subdomain: "your-prefix" })' - ); - } + this.validateDomainOptions(key, options); + + const environmentSubdomain = options?.subdomain + ? new EnvironmentSubdomain(environment, options.subdomain) + : null; // Determine host URL const host = environmentSubdomain @@ -132,20 +123,15 @@ export class AuthBuilder { static setupCustomHost(options) { const isLive = !options.host.includes('sandbox'); const environment = isLive ? Environment.live() : Environment.sandbox(); - const environmentSubdomain = - options?.subdomain && EnvironmentSubdomain.isValidSubdomain(options.subdomain) - ? new EnvironmentSubdomain(environment, options.subdomain) - : null; - - // Emit deprecation warning if subdomain is not provided with custom host - if (!environmentSubdomain) { - console.warn( - '[DEPRECATION WARNING] Initializing Checkout SDK without a subdomain is deprecated and will be removed in a future version. ' + - 'Please provide your account-specific subdomain using the "subdomain" option. ' + - 'You can find your subdomain in Dashboard → Developers → Overview. ' + - 'Example: new Checkout(key, { host: "your-host", subdomain: "your-prefix" })' - ); - } + + // A custom host replaces the base URL outright, so the merchant has already said + // where requests go and neither option is required here. A subdomain that is provided + // anyway still has to be well formed, rather than being silently dropped. + this.validateSubdomainFormat(options?.subdomain); + + const environmentSubdomain = options?.subdomain + ? new EnvironmentSubdomain(environment, options.subdomain) + : null; return { host: options.host, @@ -154,6 +140,77 @@ export class AuthBuilder { }; } + /** + * The merchant-specific subdomain is mandatory. Callers must either set `subdomain`, or + * opt out explicitly with `useLegacyDomain: true`, which keeps requests on the shared + * hosts (api.checkout.com and access.checkout.com, or their sandbox equivalents). + * + * `useLegacyDomain` is deprecated from its first release: it exists for the rare case + * where the subdomain cannot be used, and will be removed. + * + * The Previous (ABC) platform predates merchant-specific subdomains, so keys of that + * shape are exempt. + * + * @throws {ValueError} if both options are set, if neither is set, or if the subdomain is + * not a valid merchant-specific subdomain + */ + static validateDomainOptions(key, options) { + const subdomain = options?.subdomain; + const useLegacyDomain = options?.useLegacyDomain === true; + + if (subdomain && useLegacyDomain) { + throw new ValueError( + 'subdomain and useLegacyDomain cannot both be set - provide only your ' + + 'merchant-specific subdomain' + ); + } + + this.validateSubdomainFormat(subdomain); + + // The Previous (ABC) exemption is inferred from the secret-key shape: only keys + // matching PREVIOUS_SECRET_KEY_REGEX are exempt; NAS keys and OAuth are not. + if (!subdomain && !useLegacyDomain && !this.isPreviousPlatform(key, options)) { + throw new ValueError( + 'subdomain is required - provide your merchant-specific subdomain (the first 8 ' + + 'characters of your client ID, see ' + + 'https://api-reference.checkout.com/#section/Base-URLs), or set ' + + 'useLegacyDomain: true to opt out only if merchant specific sub domains are ' + + 'causing issues' + ); + } + } + + /** + * A subdomain that is set at all must be a valid merchant-specific subdomain. Invalid values + * used to be dropped back to the shared host without a word. + * + * @throws {ValueError} if the subdomain is set and malformed + */ + static validateSubdomainFormat(subdomain) { + if (subdomain && !EnvironmentSubdomain.isValidSubdomain(subdomain)) { + throw new ValueError( + 'invalid environment subdomain - provide your merchant-specific subdomain, the ' + + 'first 8 characters of your client ID (see ' + + 'https://api-reference.checkout.com/#section/Base-URLs)' + ); + } + } + + /** + * Whether these credentials belong to the Previous (ABC) platform, which predates + * merchant-specific subdomains and is therefore exempt from requiring one. + */ + static isPreviousPlatform(key, options) { + if (options?.client || process.env.CKO_SECRET) { + return false; + } + const authKey = key || process.env.CKO_SECRET_KEY || ''; + const cleanKey = authKey.startsWith('Bearer') + ? authKey.replace('Bearer', '').trim() + : authKey; + return CONFIG.PREVIOUS_SECRET_KEY_REGEX.test(cleanKey); + } + /** * Determine if environment is live or sandbox */ diff --git a/src/config.js b/src/config.js index b7dff480..55fde5ba 100644 --- a/src/config.js +++ b/src/config.js @@ -28,6 +28,9 @@ export const ETAG_HEADER = 'etag'; export const DEFAULT_TIMEOUT = 15000; export const MBC_LIVE_SECRET_KEY_REGEX = /^sk_?(\w{8})-(\w{4})-(\w{4})-(\w{4})-(\w{12})$/; +// Previous (ABC) secret keys, live and sandbox. Used to exempt that platform from the +// mandatory merchant-specific subdomain, which it predates. +export const PREVIOUS_SECRET_KEY_REGEX = /^sk_(test_)?(\w{8})-(\w{4})-(\w{4})-(\w{4})-(\w{12})$/; export const NAS_LIVE_SECRET_KEY_REGEX = /^sk_?[a-z2-7]{26}[a-z2-7*#$=]$/; export const NAS_SANDBOX_SECRET_KEY_REGEX = /^sk_sbox_?[a-z2-7]{26}[a-z2-7*#$=]$/; export const NAS_LIVE_PUBLIC_KEY_REGEX = /^pk_?[a-z2-7]{26}[a-z2-7*#$=]$/; diff --git a/test/account-updater/account-updater-it.js b/test/account-updater/account-updater-it.js index caa62c51..6546fe4b 100644 --- a/test/account-updater/account-updater-it.js +++ b/test/account-updater/account-updater-it.js @@ -12,7 +12,10 @@ const cko = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET, { client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['vault:real-time-account-updater'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::AccountUpdater', () => { diff --git a/test/agentic-commerce/agentic-commerce-it.js b/test/agentic-commerce/agentic-commerce-it.js index 76c6dc40..74de4381 100644 --- a/test/agentic-commerce/agentic-commerce-it.js +++ b/test/agentic-commerce/agentic-commerce-it.js @@ -4,7 +4,10 @@ import Checkout from '../../src/Checkout.js'; const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe.skip('Integration::AgenticCommerce', () => { diff --git a/test/apple-pay/apple-pay-it.js b/test/apple-pay/apple-pay-it.js index 2609a8b5..3d05e374 100644 --- a/test/apple-pay/apple-pay-it.js +++ b/test/apple-pay/apple-pay-it.js @@ -12,7 +12,10 @@ const cko = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET, { client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['vault:apme-enrollment'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::Apple-Pay', () => { diff --git a/test/balances/balances-it.js b/test/balances/balances-it.js index 370b47db..c0bb3787 100644 --- a/test/balances/balances-it.js +++ b/test/balances/balances-it.js @@ -12,7 +12,10 @@ const cko = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET, { client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['balances'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::Balances', () => { diff --git a/test/card-metadata/card-metadata-it.js b/test/card-metadata/card-metadata-it.js index 67a378f7..b15c4bba 100644 --- a/test/card-metadata/card-metadata-it.js +++ b/test/card-metadata/card-metadata-it.js @@ -12,7 +12,10 @@ const cko = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET, { client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['vault:card-metadata'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::CardMetadata', () => { diff --git a/test/compliance-requests/compliance-requests-it.js b/test/compliance-requests/compliance-requests-it.js index b08ecd40..436d3707 100644 --- a/test/compliance-requests/compliance-requests-it.js +++ b/test/compliance-requests/compliance-requests-it.js @@ -4,7 +4,10 @@ import Checkout from '../../src/Checkout.js'; const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe.skip('Integration::ComplianceRequests', () => { diff --git a/test/config/config.js b/test/config/config.js index 61409403..cc1b969a 100644 --- a/test/config/config.js +++ b/test/config/config.js @@ -173,27 +173,43 @@ describe('NAS oAuth', () => { expect(cko.config.agent).to.be.undefined; }); - it('should initialize with oAuth credentials with bad subdomain', () => { - const cko = new Checkout('2p7YQ37fHiRr8O6lQAikl8enICesB1dvAJrpmE2nZfEOpxzE-', { - client: 'ack_vvzhoai466su3j3vbxb47ts5oe', - scope: ['gateway'], - environment: 'sandbox', - subdomain: ' ' - }); - expect(cko).to.be.instanceOf(Checkout); - expect(cko.config.client).to.equal('ack_vvzhoai466su3j3vbxb47ts5oe'); - expect(cko.config.host).to.equal('https://api.sandbox.checkout.com'); - expect(cko.config.scope[0]).to.equal('gateway'); - expect(cko.config.secret).to.equal('2p7YQ37fHiRr8O6lQAikl8enICesB1dvAJrpmE2nZfEOpxzE-'); - expect(cko.config.agent).to.be.undefined; + it('should fail with a bad subdomain', () => { + expect( + () => + new Checkout('2p7YQ37fHiRr8O6lQAikl8enICesB1dvAJrpmE2nZfEOpxzE-', { + client: 'ack_vvzhoai466su3j3vbxb47ts5oe', + scope: ['gateway'], + environment: 'sandbox', + subdomain: ' ' + }) + ).to.throw('invalid environment subdomain'); + }); + + it('should fail with an empty subdomain and no legacy-domain opt-out', () => { + expect( + () => + new Checkout('2p7YQ37fHiRr8O6lQAikl8enICesB1dvAJrpmE2nZfEOpxzE-', { + client: 'ack_vvzhoai466su3j3vbxb47ts5oe', + scope: ['gateway'], + environment: 'sandbox', + subdomain: '' + }) + ).to.throw('subdomain is required'); + }); + + it('should not exempt a NAS-shaped secret key from the subdomain requirement', () => { + // Only Previous (ABC) keys are exempt; NAS keys still require subdomain or opt-out + expect(() => new Checkout('sk_sbox_fghjovernsi764jybiuogokg7xz')).to.throw( + 'subdomain is required' + ); }); - it('should initialize with oAuth credentials with subdomain empty', () => { + it('should initialize with oAuth credentials and the legacy domain opt-out', () => { const cko = new Checkout('2p7YQ37fHiRr8O6lQAikl8enICesB1dvAJrpmE2nZfEOpxzE-', { client: 'ack_vvzhoai466su3j3vbxb47ts5oe', scope: ['gateway'], environment: 'sandbox', - subdomain: '' + useLegacyDomain: true }); expect(cko).to.be.instanceOf(Checkout); expect(cko.config.client).to.equal('ack_vvzhoai466su3j3vbxb47ts5oe'); diff --git a/test/customers/customers-it.js b/test/customers/customers-it.js index c1baa02f..15921f8a 100644 --- a/test/customers/customers-it.js +++ b/test/customers/customers-it.js @@ -13,7 +13,10 @@ afterEach(() => { const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::Customers', () => { diff --git a/test/disputes/disputes-it.js b/test/disputes/disputes-it.js index 1ab45728..d0787e0a 100644 --- a/test/disputes/disputes-it.js +++ b/test/disputes/disputes-it.js @@ -12,7 +12,10 @@ const cko = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET, { client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['disputes', 'disputes:view'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::Disputes::Arbitration', () => { diff --git a/test/environment-subdomain/environment-subdomain-integration.js b/test/environment-subdomain/environment-subdomain-integration.js index 1e4b4ea7..7145fe43 100644 --- a/test/environment-subdomain/environment-subdomain-integration.js +++ b/test/environment-subdomain/environment-subdomain-integration.js @@ -54,17 +54,16 @@ describe('SDK Subdomain Integration', () => { expect(cko.config.environmentSubdomain.getOAuthAuthorizationApi()).to.equal('https://prodmerch1.access.checkout.com/connect/token'); }); - it('should initialize without subdomain when subdomain is invalid', () => { - const cko = new Checkout(SECRET_KEY, { - client: CLIENT_ID, - scope: ['gateway'], - environment: 'sandbox', - subdomain: 'INVALID' // uppercase, should be rejected - }); - - expect(cko.config.host).to.equal('https://api.sandbox.checkout.com'); - expect(cko.config.environment).to.be.instanceOf(Environment); - expect(cko.config.environmentSubdomain).to.be.null; + it('should fail when the subdomain is invalid', () => { + expect( + () => + new Checkout(SECRET_KEY, { + client: CLIENT_ID, + scope: ['gateway'], + environment: 'sandbox', + subdomain: 'INVALID' // uppercase, rejected + }) + ).to.throw('invalid environment subdomain'); }); it('should initialize with short subdomain', () => { @@ -79,17 +78,43 @@ describe('SDK Subdomain Integration', () => { expect(cko.config.environmentSubdomain.subdomain).to.equal('ab'); }); - it('should initialize without subdomain when subdomain is empty', () => { + it('should fail when the subdomain is empty and the legacy domain is not requested', () => { + expect( + () => + new Checkout(SECRET_KEY, { + client: CLIENT_ID, + scope: ['gateway'], + environment: 'sandbox', + subdomain: '' + }) + ).to.throw('subdomain is required'); + }); + + it('should use the shared hosts with the legacy domain opt-out', () => { const cko = new Checkout(SECRET_KEY, { client: CLIENT_ID, scope: ['gateway'], environment: 'sandbox', - subdomain: '' + useLegacyDomain: true }); expect(cko.config.host).to.equal('https://api.sandbox.checkout.com'); + expect(cko.config.environment).to.be.instanceOf(Environment); expect(cko.config.environmentSubdomain).to.be.null; }); + + it('should fail when both the subdomain and the legacy domain are set', () => { + expect( + () => + new Checkout(SECRET_KEY, { + client: CLIENT_ID, + scope: ['gateway'], + environment: 'sandbox', + subdomain: 'configtest', + useLegacyDomain: true + }) + ).to.throw('cannot both be set'); + }); }); describe('Environment variables with subdomains', () => { @@ -189,11 +214,20 @@ describe('SDK Subdomain Integration', () => { expect(cko.config.environmentSubdomain.subdomain).to.equal('customlive'); }); - it('should ignore subdomain if invalid even with custom host', () => { + it('should still reject a malformed subdomain with a custom host', () => { + expect( + () => + new Checkout(SECRET_KEY, { + host: 'https://custom.example.com', + subdomain: 'INVALID!' + }) + ).to.throw('invalid environment subdomain'); + }); + + it('should not require a subdomain with a custom host', () => { const customHost = 'https://custom.example.com'; const cko = new Checkout(SECRET_KEY, { - host: customHost, - subdomain: 'INVALID!' + host: customHost }); expect(cko.config.host).to.equal(customHost); @@ -224,28 +258,39 @@ describe('SDK Subdomain Integration', () => { expect(typeof cko.config.subdomain).to.equal('string'); }); - it('should have correct config structure without subdomain', () => { + it('should have correct config structure with the legacy domain opt-out', () => { const cko = new Checkout(SECRET_KEY, { client: CLIENT_ID, - environment: 'sandbox' + environment: 'sandbox', + useLegacyDomain: true }); expect(cko.config).to.have.property('environment'); expect(cko.config.environmentSubdomain).to.be.null; expect(cko.config.environment).to.be.instanceOf(Environment); }); + + it('should fail without a subdomain and without the legacy domain opt-out', () => { + expect( + () => + new Checkout(SECRET_KEY, { + client: CLIENT_ID, + environment: 'sandbox' + }) + ).to.throw('subdomain is required'); + }); }); describe('Subdomain validation edge cases', () => { - it('should handle whitespace-only subdomain', () => { - const cko = new Checkout(SECRET_KEY, { - client: CLIENT_ID, - environment: 'sandbox', - subdomain: ' ' - }); - - expect(cko.config.host).to.equal('https://api.sandbox.checkout.com'); - expect(cko.config.environmentSubdomain).to.be.null; + it('should reject a whitespace-only subdomain', () => { + expect( + () => + new Checkout(SECRET_KEY, { + client: CLIENT_ID, + environment: 'sandbox', + subdomain: ' ' + }) + ).to.throw('invalid environment subdomain'); }); it('should handle numeric-only subdomain', () => { diff --git a/test/environment-subdomain/environment-subdomain.js b/test/environment-subdomain/environment-subdomain.js index 21707cb3..071278e8 100644 --- a/test/environment-subdomain/environment-subdomain.js +++ b/test/environment-subdomain/environment-subdomain.js @@ -1,6 +1,7 @@ import Environment from '../../src/Environment.js'; import EnvironmentSubdomain from '../../src/EnvironmentSubdomain.js'; import { expect } from 'chai'; +import { ValueError } from '../../src/services/errors.js'; describe('EnvironmentSubdomain', () => { let sandboxEnvironment; @@ -97,67 +98,53 @@ describe('EnvironmentSubdomain', () => { }); describe('invalid subdomains', () => { - it('should return original URL for null subdomain', () => { - const originalUrl = 'https://api.sandbox.checkout.com'; - const result = EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, null); - - expect(result).to.equal(originalUrl); + const originalUrl = 'https://api.sandbox.checkout.com'; + const expectThrow = (subdomain) => { + expect(() => EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, subdomain)) + .to.throw(ValueError, /invalid environment subdomain/); + }; + + it('should throw for null subdomain', () => { + expectThrow(null); }); - it('should return original URL for undefined subdomain', () => { - const originalUrl = 'https://api.sandbox.checkout.com'; - const result = EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, undefined); - - expect(result).to.equal(originalUrl); + it('should throw for undefined subdomain', () => { + expectThrow(undefined); }); - it('should return original URL for empty subdomain', () => { - const originalUrl = 'https://api.sandbox.checkout.com'; - const result = EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, ''); - - expect(result).to.equal(originalUrl); + it('should throw for empty subdomain', () => { + expectThrow(''); }); - it('should return original URL for subdomain with uppercase letters', () => { - const originalUrl = 'https://api.sandbox.checkout.com'; - const result = EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, 'ABC123'); - - expect(result).to.equal(originalUrl); + it('should throw for subdomain with uppercase letters', () => { + expectThrow('ABC123'); }); - it('should return original URL for subdomain with invalid special characters', () => { - const originalUrl = 'https://api.sandbox.checkout.com'; - expect(EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, 'test_123')).to.equal(originalUrl); - expect(EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, 'test@123')).to.equal(originalUrl); - expect(EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, 'test.123')).to.equal(originalUrl); + it('should throw for subdomain with invalid special characters', () => { + expectThrow('test_123'); + expectThrow('test@123'); + expectThrow('test.123'); }); - it('should return original URL for subdomain with trailing hyphen', () => { - const originalUrl = 'https://api.sandbox.checkout.com'; - expect(EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, 'foo-')).to.equal(originalUrl); + it('should throw for subdomain with trailing hyphen', () => { + expectThrow('foo-'); }); - it('should return original URL for subdomain with leading hyphen', () => { - const originalUrl = 'https://api.sandbox.checkout.com'; - expect(EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, '-foo')).to.equal(originalUrl); + it('should throw for subdomain with leading hyphen', () => { + expectThrow('-foo'); }); - it('should return original URL for non-pl hyphenated subdomain', () => { - const originalUrl = 'https://api.sandbox.checkout.com'; - expect(EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, 'test-123')).to.equal(originalUrl); + it('should throw for non-pl hyphenated subdomain', () => { + expectThrow('test-123'); }); it('should create URL with PrivateLink pl-{prefix} subdomain', () => { - const originalUrl = 'https://api.sandbox.checkout.com'; expect(EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, 'pl-vkuhvk4v')).to.equal('https://pl-vkuhvk4v.api.sandbox.checkout.com'); expect(EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, 'pl-abc123')).to.equal('https://pl-abc123.api.sandbox.checkout.com'); }); - it('should return original URL for subdomain with spaces', () => { - const originalUrl = 'https://api.sandbox.checkout.com'; - const result = EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, 'test 123'); - - expect(result).to.equal(originalUrl); + it('should throw for subdomain with spaces', () => { + expectThrow('test 123'); }); it('should create URL with short subdomain (2 chars)', () => { @@ -176,11 +163,10 @@ describe('EnvironmentSubdomain', () => { }); describe('error handling', () => { - it('should return original URL for malformed URLs', () => { - const originalUrl = 'not-a-valid-url'; - const result = EnvironmentSubdomain.createUrlWithSubdomain(originalUrl, 'test1234'); - - expect(result).to.equal(originalUrl); + it('should throw for malformed URLs', () => { + expect(() => + EnvironmentSubdomain.createUrlWithSubdomain('not-a-valid-url', 'test1234') + ).to.throw(); }); }); }); diff --git a/test/forward/forward-it.js b/test/forward/forward-it.js index 8012aa5b..2cf9cf5f 100644 --- a/test/forward/forward-it.js +++ b/test/forward/forward-it.js @@ -5,7 +5,10 @@ const cko = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET, { client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['forward'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::Forward', () => { diff --git a/test/google-pay/google-pay-it.js b/test/google-pay/google-pay-it.js index e02b808d..29a28aa1 100644 --- a/test/google-pay/google-pay-it.js +++ b/test/google-pay/google-pay-it.js @@ -4,7 +4,10 @@ import Checkout from '../../src/Checkout.js'; const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe.skip('Integration::GooglePay', () => { diff --git a/test/hosted-payments/hosted-payments-it.js b/test/hosted-payments/hosted-payments-it.js index 43ba95cc..6d560aee 100644 --- a/test/hosted-payments/hosted-payments-it.js +++ b/test/hosted-payments/hosted-payments-it.js @@ -4,7 +4,10 @@ import Checkout from '../../src/Checkout.js'; const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { pk: process.env.CHECKOUT_PREVIOUS_PUBLIC_KEY, environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); const processingChannelId = process.env.CHECKOUT_PROCESSING_CHANNEL_ID; diff --git a/test/hosted-payments/hosted-payments.js b/test/hosted-payments/hosted-payments.js index f59ff830..86544589 100644 --- a/test/hosted-payments/hosted-payments.js +++ b/test/hosted-payments/hosted-payments.js @@ -89,7 +89,7 @@ describe('Hosted Payments', () => { nock('https://123456789.api.sandbox.checkout.com').post('/hosted-payments').reply(401); try { - const cko = new Checkout('sk_'); + const cko = new Checkout('sk_', { subdomain: '123456789' }); const hostedResponse = await cko.hostedPayments.create({ amount: 10, diff --git a/test/http/httpClient-it.js b/test/http/httpClient-it.js index e964a3cf..654a1f4c 100644 --- a/test/http/httpClient-it.js +++ b/test/http/httpClient-it.js @@ -19,7 +19,10 @@ describe('Integration::HttpClient', () => { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 3000, httpClient: 'axios', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, } ); const token = await checkout.tokens.request( @@ -50,7 +53,10 @@ describe('Integration::HttpClient', () => { timeout: 3000, httpClient: 'axios', agent: new https.Agent({ keepAlive: true }), - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, } ); const token = await checkout.tokens.request( @@ -79,7 +85,10 @@ describe('Integration::HttpClient', () => { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 100, httpClient: 'axios', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, } ); try { @@ -107,7 +116,10 @@ describe('Integration::HttpClient', () => { { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 3000, - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, } ); const token = await checkout.tokens.request( @@ -137,7 +149,10 @@ describe('Integration::HttpClient', () => { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 3000, agent: new https.Agent({ keepAlive: true }), - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, } ); @@ -167,7 +182,10 @@ describe('Integration::HttpClient', () => { { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 200, - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, } ); try { diff --git a/test/identities/identities-common.js b/test/identities/identities-common.js index 49df9b29..13b819a7 100644 --- a/test/identities/identities-common.js +++ b/test/identities/identities-common.js @@ -10,5 +10,8 @@ export const cko = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['identity-verification'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); diff --git a/test/instruments/instruments-it.js b/test/instruments/instruments-it.js index 46784844..5325f4ac 100644 --- a/test/instruments/instruments-it.js +++ b/test/instruments/instruments-it.js @@ -11,7 +11,10 @@ afterEach(() => { const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { pk: process.env.CHECKOUT_PREVIOUS_PUBLIC_KEY, environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); const sepaRequest = { diff --git a/test/issuing/issuing-common.js b/test/issuing/issuing-common.js index 8a8b00d7..5a1faf45 100644 --- a/test/issuing/issuing-common.js +++ b/test/issuing/issuing-common.js @@ -11,7 +11,10 @@ export const cko_issuing = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_ISSUI client: process.env.CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID, scope: ['issuing:card-mgmt', 'issuing:client', 'issuing:controls-read', 'issuing:controls-write', 'issuing:transactions-read', 'vault'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); export const ISSUING_ENTITY_ID = process.env.CHECKOUT_ISSUING_ENTITY_ID || 'ent_mujh2nia2ypezmw5fo2fofk7ka'; diff --git a/test/network-tokens/network-tokens-it.js b/test/network-tokens/network-tokens-it.js index 3390608f..02f02a02 100644 --- a/test/network-tokens/network-tokens-it.js +++ b/test/network-tokens/network-tokens-it.js @@ -9,7 +9,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::NetworkTokens', () => { diff --git a/test/onboarding-simulator/onboarding-simulator-it.js b/test/onboarding-simulator/onboarding-simulator-it.js index 88c6a531..b9f1d4cb 100644 --- a/test/onboarding-simulator/onboarding-simulator-it.js +++ b/test/onboarding-simulator/onboarding-simulator-it.js @@ -4,7 +4,10 @@ import Checkout from '../../src/Checkout.js'; const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe.skip('Integration::OnboardingSimulator', () => { diff --git a/test/payment-contexts/payment-contexts-it.js b/test/payment-contexts/payment-contexts-it.js index 68d08a11..4d9a8570 100644 --- a/test/payment-contexts/payment-contexts-it.js +++ b/test/payment-contexts/payment-contexts-it.js @@ -9,7 +9,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); const processingChannelId = process.env.CHECKOUT_PROCESSING_CHANNEL_ID; diff --git a/test/payment-methods/payment-methods-it.js b/test/payment-methods/payment-methods-it.js index 6c4775ac..59e7d2f9 100644 --- a/test/payment-methods/payment-methods-it.js +++ b/test/payment-methods/payment-methods-it.js @@ -9,7 +9,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::PaymentMethods', () => { diff --git a/test/payment-sessions/payment-sessions-complete-it.js b/test/payment-sessions/payment-sessions-complete-it.js index 4673c971..fb037181 100644 --- a/test/payment-sessions/payment-sessions-complete-it.js +++ b/test/payment-sessions/payment-sessions-complete-it.js @@ -10,7 +10,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::Payment-Sessions::Complete', () => { diff --git a/test/payment-sessions/payment-sessions-it.js b/test/payment-sessions/payment-sessions-it.js index 5ac91b68..c052c5f7 100644 --- a/test/payment-sessions/payment-sessions-it.js +++ b/test/payment-sessions/payment-sessions-it.js @@ -9,7 +9,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); describe('Integration::Payment-Sessions', () => { diff --git a/test/payment-setups/payment-setups-it.js b/test/payment-setups/payment-setups-it.js index 376f7522..53fcf8b9 100644 --- a/test/payment-setups/payment-setups-it.js +++ b/test/payment-setups/payment-setups-it.js @@ -9,7 +9,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); const processingChannelId = process.env.CHECKOUT_PROCESSING_CHANNEL_ID; diff --git a/test/payments-links/payments-links.js b/test/payments-links/payments-links.js index 63fa6c54..d55b7c5e 100644 --- a/test/payments-links/payments-links.js +++ b/test/payments-links/payments-links.js @@ -95,7 +95,7 @@ describe('Payment Links', () => { nock('https://123456789.api.sandbox.checkout.com').post('/payment-links').reply(401); try { - const cko = new Checkout('sk_'); + const cko = new Checkout('sk_', { subdomain: '123456789' }); const linksResponse = await cko.paymentLinks.create({ amount: 10359, diff --git a/test/platforms/reserve-rules/reserve-rules-it.js b/test/platforms/reserve-rules/reserve-rules-it.js index ca147889..bbbc7993 100644 --- a/test/platforms/reserve-rules/reserve-rules-it.js +++ b/test/platforms/reserve-rules/reserve-rules-it.js @@ -8,7 +8,10 @@ describe('Integration::Platforms::Reserve Rules', () => { client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['accounts'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); let entityId; diff --git a/test/transfers/transfers.js b/test/transfers/transfers.js index 41a08464..4db9a187 100644 --- a/test/transfers/transfers.js +++ b/test/transfers/transfers.js @@ -19,7 +19,7 @@ describe('Transfers', () => { }, }); - const cko = new Checkout(SK); + const cko = new Checkout(SK, { subdomain: '123456789' }); const transfer = await cko.transfers.initiate({ reference: 'superhero1234', @@ -50,7 +50,7 @@ describe('Transfers', () => { }); // fake key - const cko = new Checkout('sk_o2nulev2arguvyf6w7sc5fkznas'); + const cko = new Checkout('sk_o2nulev2arguvyf6w7sc5fkznas', { subdomain: '123456789' }); const transfer = await cko.transfers.initiate( { @@ -121,7 +121,7 @@ describe('Transfers', () => { nock('https://transfers.sandbox.checkout.com').post('/transfers').reply(401); try { - const cko = new Checkout('test'); + const cko = new Checkout('test', { subdomain: '123456789' }); const transfer = await cko.transfers.initiate({ reference: 'superhero1234', @@ -144,7 +144,7 @@ describe('Transfers', () => { nock('https://transfers.sandbox.checkout.com').post('/transfers').reply(422, {}); try { - const cko = new Checkout(SK); + const cko = new Checkout(SK, { subdomain: '123456789' }); const transfer = await cko.transfers.initiate({ transfer_type: 'test', @@ -173,7 +173,7 @@ describe('Transfers', () => { }, }); - const cko = new Checkout(SK); + const cko = new Checkout(SK, { subdomain: '123456789' }); const transfer = await cko.transfers.retrieve('tra_lx6isvi4lahkrkn462bj77xnki'); @@ -200,7 +200,7 @@ describe('Transfers', () => { }); // fake key - const cko = new Checkout('sk_o2nulev2arguvyf6w7sc5fkznas'); + const cko = new Checkout('sk_o2nulev2arguvyf6w7sc5fkznas', { subdomain: '123456789' }); const transfer = await cko.transfers.retrieve('tra_lx6isvi4lahkrkn462bj77xnki'); @@ -213,7 +213,7 @@ describe('Transfers', () => { .reply(401); try { - const cko = new Checkout('test'); + const cko = new Checkout('test', { subdomain: '123456789' }); const transfer = await cko.transfers.retrieve('tra_lx6isvi4lahkrkn462bj77xnki'); } catch (err) { @@ -231,7 +231,7 @@ describe('Transfers', () => { }); try { - const cko = new Checkout(SK); + const cko = new Checkout(SK, { subdomain: '123456789' }); const transfer = await cko.transfers.retrieve('123'); } catch (err) { diff --git a/test/utils.js b/test/utils.js index 82a65b43..eba93365 100644 --- a/test/utils.js +++ b/test/utils.js @@ -5,7 +5,10 @@ const cko_platforms = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_SEC client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['accounts'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + // The sandbox OAuth clients are not provisioned for the merchant-specific + // subdomain, so the token request would come back invalid_client. Opting out + // explicitly until they are. + useLegacyDomain: true, }); /** diff --git a/types/dist/Checkout.d.ts b/types/dist/Checkout.d.ts index 5948e760..95867102 100644 --- a/types/dist/Checkout.d.ts +++ b/types/dist/Checkout.d.ts @@ -74,6 +74,8 @@ export type config = { access?: access; httpClient?: string; subdomain?: string; + /** @deprecated emergency fallback only, see the README. Use `subdomain` instead. */ + useLegacyDomain?: boolean; environment?: Environment; environmentSubdomain?: EnvironmentSubdomain; }; @@ -84,7 +86,19 @@ type options = { agent?: http.Agent; headers?: Record; httpClient?: string; + /** + * Your merchant-specific subdomain (MSSD): the first 8 characters of your client ID. + * Required, unless you explicitly opt out with `useLegacyDomain`. + */ subdomain?: string; + /** + * Sends every request to the shared hosts instead of your merchant-specific subdomain. + * + * @deprecated this is an emergency fallback for the rare case where the subdomain cannot + * be used, and will be removed in a future release. Set `subdomain` instead. + * See https://api-reference.checkout.com/#section/Base-URLs + */ + useLegacyDomain?: boolean; } & (staticKeyOptions | oauthOptions); type staticKeyOptions = { diff --git a/types/dist/EnvironmentSubdomain.d.ts b/types/dist/EnvironmentSubdomain.d.ts index 5d9460c5..9f1dfe18 100644 --- a/types/dist/EnvironmentSubdomain.d.ts +++ b/types/dist/EnvironmentSubdomain.d.ts @@ -16,9 +16,8 @@ export default class EnvironmentSubdomain { getOAuthAuthorizationApi(): string; /** - * Applies subdomain transformation to any given URL. - * If the subdomain is valid (alphanumeric pattern), prepends it to the host. - * Otherwise, returns the original URL unchanged. + * Applies subdomain transformation to any given URL by prepending the subdomain to the host. + * Throws a ValueError if the subdomain is not a valid merchant-specific subdomain. */ static createUrlWithSubdomain(originalUrl: string, subdomain: string): string;