From b34b0d1ca3d71f1b8820bfd6860599d2b1217500 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, 10 Aug 2026 14:22:04 +0200 Subject: [PATCH 1/9] feat: require the subdomain option or an explicit useLegacyDomain opt-out Initializing without a subdomain already emitted a deprecation warning, so this finishes the job: it now throws a ValueError. Callers must either set `subdomain`, or opt out explicitly with `useLegacyDomain: true`, which ships deprecated in the type definitions. Both, or neither, throws. An invalid subdomain also throws now instead of being silently dropped back to the shared host, which is a second breaking change: a malformed value currently works and the caller never finds out. Two exemptions. A custom `host` replaces the base URL outright, so the merchant has already said where requests go. Previous (ABC) keys predate merchant-specific subdomains, matched by the new PREVIOUS_SECRET_KEY_REGEX, which covers both live and sandbox key shapes (MBC_LIVE_SECRET_KEY_REGEX only matched live). Mirrors checkout-sdk-net#590. Refs INT-1688. --- README.md | 24 +- package.json | 2 +- src/auth-builder.js | 100 +++-- src/config.js | 409 +++++++++--------- test/config/config.js | 39 +- .../environment-subdomain-integration.js | 89 ++-- test/hosted-payments/hosted-payments.js | 2 +- test/payments-links/payments-links.js | 2 +- test/transfers/transfers.js | 16 +- types/dist/Checkout.d.ts | 316 +++++++------- types/dist/EnvironmentSubdomain.d.ts | 2 +- 11 files changed, 562 insertions(+), 439 deletions(-) diff --git a/README.md b/README.md index 496e135e..7e65c4f2 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 5.0.0:** 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`. 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. + ## Contributing We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to get started. diff --git a/package.json b/package.json index 7038b145..c52b9cd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "checkout-sdk-node", - "version": "4.1.0", + "version": "5.0.0", "description": "Official Node.js SDK for Checkout.com payment gateway - Full API coverage with TypeScript support", "type": "module", "engines": { diff --git a/src/auth-builder.js b/src/auth-builder.js index 6f05a833..880f14c9 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,12 @@ 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. + const environmentSubdomain = options?.subdomain + ? new EnvironmentSubdomain(environment, options.subdomain) + : null; return { host: options.host, @@ -154,6 +137,65 @@ 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' + ); + } + + 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)' + ); + } + + 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' + ); + } + } + + /** + * 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..4eb7619f 100644 --- a/src/config.js +++ b/src/config.js @@ -1,203 +1,206 @@ -/** Base URLs for main API. Per API Reference Base URLs, account-specific URLs use {prefix}.api.(sandbox.)checkout.com; see EnvironmentSubdomain when subdomain is set. */ -export const SANDBOX_BASE_URL = 'https://api.sandbox.checkout.com'; -export const LIVE_BASE_URL = 'https://api.checkout.com'; -export const SANDBOX_ACCESS_URL = 'https://access.sandbox.checkout.com/connect/token'; -export const LIVE_ACCESS_URL = 'https://access.checkout.com/connect/token'; - -export const PLATFORMS_FILES_LIVE_URL = 'https://files.checkout.com/files'; -export const PLATFORMS_FILES_SANDBOX_URL = 'https://files.sandbox.checkout.com/files'; - -export const TRANSFERS_SANDBOX_URL = 'https://transfers.sandbox.checkout.com/transfers'; -export const TRANSFERS_LIVE_URL = 'https://transfers.checkout.com/transfers'; - -// Forward host root (no trailing slash). The `forward` and `secrets` path -// segments are appended by the ForwardClient methods (matching every other SDK). -export const FORWARD_SANDBOX_URL = 'https://forward.sandbox.checkout.com'; -export const FORWARD_LIVE_URL = 'https://forward.checkout.com'; - -export const BALANCES_SANDBOX_URL = 'https://balances.sandbox.checkout.com/balances'; -export const BALANCES_LIVE_URL = 'https://balances.checkout.com/balances'; - -export const IDENTITY_VERIFICATION_SANDBOX_URL = 'https://identity-verification.sandbox.checkout.com'; -export const IDENTITY_VERIFICATION_LIVE_URL = 'https://identity-verification.checkout.com'; - -export const REQUEST_ID_HEADER = 'cko-request-id'; -export const API_VERSION_HEADER = 'cko-version'; -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})$/; -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*#$=]$/; -export const NAS_SANDBOX_PUBLIC_KEY_REGEX = /^pk_sbox_?[a-z2-7]{26}[a-z2-7*#$=]$/; -export const PAYMENT_TYPES = { - regular: 'Regular', - recurring: 'Recurring', - moto: 'MOTO', - installment: 'Installment', - unscheduled: 'Unscheduled', -}; -export const CURRENCIES = { - ALL: 'ALL', - STN: 'STN', - EEK: 'EEK', - BHD: 'BHD', - SCR: 'SCR', - DJF: 'DJF', - EGP: 'EGP', - MDL: 'MDL', - MZN: 'MZN', - BND: 'BND', - ZMK: 'ZMK', - SHP: 'SHP', - LBP: 'LBP', - AWG: 'AWG', - JMD: 'JMD', - KES: 'KES', - BYN: 'BYN', - KHR: 'KHR', - LAK: 'LAK', - MVR: 'MVR', - AOA: 'AOA', - TJS: 'TJS', - SVC: 'SVC', - GNF: 'GNF', - BRL: 'BRL', - MOP: 'MOP', - BOB: 'BOB', - CDF: 'CDF', - NAD: 'NAD', - LYD: 'LYD', - VUV: 'VUV', - QAR: 'QAR', - CLP: 'CLP', - HRK: 'HRK', - ISK: 'ISK', - FKP: 'FKP', - XCD: 'XCD', - NOK: 'NOK', - CUP: 'CUP', - VND: 'VND', - PEN: 'PEN', - KMF: 'KMF', - LVL: 'LVL', - MMK: 'MMK', - TRY: 'TRY', - VEF: 'VEF', - AUD: 'AUD', - TWD: 'TWD', - PKR: 'PKR', - SLL: 'SLL', - BGN: 'BGN', - LRD: 'LRD', - LKR: 'LKR', - XAF: 'XAF', - JOD: 'JOD', - ANG: 'ANG', - BSD: 'BSD', - CAD: 'CAD', - GIP: 'GIP', - MNT: 'MNT', - LTL: 'LTL', - BBD: 'BBD', - CLF: 'CLF', - BWP: 'BWP', - COP: 'COP', - PHP: 'PHP', - HUF: 'HUF', - FJD: 'FJD', - MWK: 'MWK', - THB: 'THB', - XPF: 'XPF', - RSD: 'RSD', - SAR: 'SAR', - UYU: 'UYU', - BZD: 'BZD', - SYP: 'SYP', - GMD: 'GMD', - SZL: 'SZL', - SBD: 'SBD', - ETB: 'ETB', - CHF: 'CHF', - MXN: 'MXN', - ARS: 'ARS', - GTQ: 'GTQ', - GHS: 'GHS', - NIO: 'NIO', - JPY: 'JPY', - BDT: 'BDT', - UZS: 'UZS', - SOS: 'SOS', - BTN: 'BTN', - NZD: 'NZD', - TZS: 'TZS', - IQD: 'IQD', - MGA: 'MGA', - DZD: 'DZD', - GYD: 'GYD', - USD: 'USD', - KWD: 'KWD', - CNY: 'CNY', - PYG: 'PYG', - SGD: 'SGD', - KZT: 'KZT', - PGK: 'PGK', - AMD: 'AMD', - GBP: 'GBP', - AFN: 'AFN', - CRC: 'CRC', - XOF: 'XOF', - YER: 'YER', - MRU: 'MRU', - DKK: 'DKK', - TOP: 'TOP', - INR: 'INR', - SDG: 'SDG', - DOP: 'DOP', - ZWL: 'ZWL', - UGX: 'UGX', - SEK: 'SEK', - LSL: 'LSL', - MYR: 'MYR', - TMT: 'TMT', - OMR: 'OMR', - BMD: 'BMD', - KRW: 'KRW', - HKD: 'HKD', - KGS: 'KGS', - BAM: 'BAM', - NGN: 'NGN', - ILS: 'ILS', - MUR: 'MUR', - RON: 'RON', - TND: 'TND', - AED: 'AED', - PAB: 'PAB', - NPR: 'NPR', - TTD: 'TTD', - RWF: 'RWF', - HTG: 'HTG', - IDR: 'IDR', - EUR: 'EUR', - KYD: 'KYD', - IRR: 'IRR', - KPW: 'KPW', - MKD: 'MKD', - SRD: 'SRD', - HNL: 'HNL', - AZN: 'AZN', - ERN: 'ERN', - CZK: 'CZK', - CVE: 'CVE', - BIF: 'BIF', - MAD: 'MAD', - RUB: 'RUB', - UAH: 'UAH', - WST: 'WST', - PLN: 'PLN', - ZAR: 'ZAR', - GEL: 'GEL', - ZMW: 'ZMW', -}; +/** Base URLs for main API. Per API Reference Base URLs, account-specific URLs use {prefix}.api.(sandbox.)checkout.com; see EnvironmentSubdomain when subdomain is set. */ +export const SANDBOX_BASE_URL = 'https://api.sandbox.checkout.com'; +export const LIVE_BASE_URL = 'https://api.checkout.com'; +export const SANDBOX_ACCESS_URL = 'https://access.sandbox.checkout.com/connect/token'; +export const LIVE_ACCESS_URL = 'https://access.checkout.com/connect/token'; + +export const PLATFORMS_FILES_LIVE_URL = 'https://files.checkout.com/files'; +export const PLATFORMS_FILES_SANDBOX_URL = 'https://files.sandbox.checkout.com/files'; + +export const TRANSFERS_SANDBOX_URL = 'https://transfers.sandbox.checkout.com/transfers'; +export const TRANSFERS_LIVE_URL = 'https://transfers.checkout.com/transfers'; + +// Forward host root (no trailing slash). The `forward` and `secrets` path +// segments are appended by the ForwardClient methods (matching every other SDK). +export const FORWARD_SANDBOX_URL = 'https://forward.sandbox.checkout.com'; +export const FORWARD_LIVE_URL = 'https://forward.checkout.com'; + +export const BALANCES_SANDBOX_URL = 'https://balances.sandbox.checkout.com/balances'; +export const BALANCES_LIVE_URL = 'https://balances.checkout.com/balances'; + +export const IDENTITY_VERIFICATION_SANDBOX_URL = 'https://identity-verification.sandbox.checkout.com'; +export const IDENTITY_VERIFICATION_LIVE_URL = 'https://identity-verification.checkout.com'; + +export const REQUEST_ID_HEADER = 'cko-request-id'; +export const API_VERSION_HEADER = 'cko-version'; +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*#$=]$/; +export const NAS_SANDBOX_PUBLIC_KEY_REGEX = /^pk_sbox_?[a-z2-7]{26}[a-z2-7*#$=]$/; +export const PAYMENT_TYPES = { + regular: 'Regular', + recurring: 'Recurring', + moto: 'MOTO', + installment: 'Installment', + unscheduled: 'Unscheduled', +}; +export const CURRENCIES = { + ALL: 'ALL', + STN: 'STN', + EEK: 'EEK', + BHD: 'BHD', + SCR: 'SCR', + DJF: 'DJF', + EGP: 'EGP', + MDL: 'MDL', + MZN: 'MZN', + BND: 'BND', + ZMK: 'ZMK', + SHP: 'SHP', + LBP: 'LBP', + AWG: 'AWG', + JMD: 'JMD', + KES: 'KES', + BYN: 'BYN', + KHR: 'KHR', + LAK: 'LAK', + MVR: 'MVR', + AOA: 'AOA', + TJS: 'TJS', + SVC: 'SVC', + GNF: 'GNF', + BRL: 'BRL', + MOP: 'MOP', + BOB: 'BOB', + CDF: 'CDF', + NAD: 'NAD', + LYD: 'LYD', + VUV: 'VUV', + QAR: 'QAR', + CLP: 'CLP', + HRK: 'HRK', + ISK: 'ISK', + FKP: 'FKP', + XCD: 'XCD', + NOK: 'NOK', + CUP: 'CUP', + VND: 'VND', + PEN: 'PEN', + KMF: 'KMF', + LVL: 'LVL', + MMK: 'MMK', + TRY: 'TRY', + VEF: 'VEF', + AUD: 'AUD', + TWD: 'TWD', + PKR: 'PKR', + SLL: 'SLL', + BGN: 'BGN', + LRD: 'LRD', + LKR: 'LKR', + XAF: 'XAF', + JOD: 'JOD', + ANG: 'ANG', + BSD: 'BSD', + CAD: 'CAD', + GIP: 'GIP', + MNT: 'MNT', + LTL: 'LTL', + BBD: 'BBD', + CLF: 'CLF', + BWP: 'BWP', + COP: 'COP', + PHP: 'PHP', + HUF: 'HUF', + FJD: 'FJD', + MWK: 'MWK', + THB: 'THB', + XPF: 'XPF', + RSD: 'RSD', + SAR: 'SAR', + UYU: 'UYU', + BZD: 'BZD', + SYP: 'SYP', + GMD: 'GMD', + SZL: 'SZL', + SBD: 'SBD', + ETB: 'ETB', + CHF: 'CHF', + MXN: 'MXN', + ARS: 'ARS', + GTQ: 'GTQ', + GHS: 'GHS', + NIO: 'NIO', + JPY: 'JPY', + BDT: 'BDT', + UZS: 'UZS', + SOS: 'SOS', + BTN: 'BTN', + NZD: 'NZD', + TZS: 'TZS', + IQD: 'IQD', + MGA: 'MGA', + DZD: 'DZD', + GYD: 'GYD', + USD: 'USD', + KWD: 'KWD', + CNY: 'CNY', + PYG: 'PYG', + SGD: 'SGD', + KZT: 'KZT', + PGK: 'PGK', + AMD: 'AMD', + GBP: 'GBP', + AFN: 'AFN', + CRC: 'CRC', + XOF: 'XOF', + YER: 'YER', + MRU: 'MRU', + DKK: 'DKK', + TOP: 'TOP', + INR: 'INR', + SDG: 'SDG', + DOP: 'DOP', + ZWL: 'ZWL', + UGX: 'UGX', + SEK: 'SEK', + LSL: 'LSL', + MYR: 'MYR', + TMT: 'TMT', + OMR: 'OMR', + BMD: 'BMD', + KRW: 'KRW', + HKD: 'HKD', + KGS: 'KGS', + BAM: 'BAM', + NGN: 'NGN', + ILS: 'ILS', + MUR: 'MUR', + RON: 'RON', + TND: 'TND', + AED: 'AED', + PAB: 'PAB', + NPR: 'NPR', + TTD: 'TTD', + RWF: 'RWF', + HTG: 'HTG', + IDR: 'IDR', + EUR: 'EUR', + KYD: 'KYD', + IRR: 'IRR', + KPW: 'KPW', + MKD: 'MKD', + SRD: 'SRD', + HNL: 'HNL', + AZN: 'AZN', + ERN: 'ERN', + CZK: 'CZK', + CVE: 'CVE', + BIF: 'BIF', + MAD: 'MAD', + RUB: 'RUB', + UAH: 'UAH', + WST: 'WST', + PLN: 'PLN', + ZAR: 'ZAR', + GEL: 'GEL', + ZMW: 'ZMW', +}; diff --git a/test/config/config.js b/test/config/config.js index 61409403..a66486d5 100644 --- a/test/config/config.js +++ b/test/config/config.js @@ -173,27 +173,36 @@ 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 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/environment-subdomain/environment-subdomain-integration.js b/test/environment-subdomain/environment-subdomain-integration.js index 1e4b4ea7..5812eb2e 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,10 @@ describe('SDK Subdomain Integration', () => { expect(cko.config.environmentSubdomain.subdomain).to.equal('customlive'); }); - it('should ignore subdomain if invalid even with custom host', () => { + 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 +248,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/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/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/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/types/dist/Checkout.d.ts b/types/dist/Checkout.d.ts index 5948e760..4a51824d 100644 --- a/types/dist/Checkout.d.ts +++ b/types/dist/Checkout.d.ts @@ -1,151 +1,165 @@ -//@ts-ignore -import * as http from 'http'; - -import { - Access, - AccountUpdater, - AgenticCommerce, - ApplePay, - Balances, - Baloto, - Boleto, - CardMetadata, - ComplianceRequests, - Customers, - Disputes, - Events, - Fawry, - Files, - Financial, - Forex, - Forward, - Giropay, - GooglePay, - HostedPayments, - Ideal, - Identities, - Instruments, - Issuing, - Klarna, - NetworkTokens, - OnboardingSimulator, - Oxxo, - PagoFacil, - PaymentContexts, - PaymentLinks, - PaymentMethods, - PaymentSessions, - PaymentSetups, - Payments, - Platforms, - Rapipago, - Reconciliation, - Reports, - Risk, - Sepa, - Sessions, - Sources, - Tokens, - Transfers, - Webhooks, - Workflows, -} from './index'; - -import Environment from './Environment'; -import EnvironmentSubdomain from './EnvironmentSubdomain'; - -export type access = { - token: string; - type: string; - scope: string; - expires: Date; -}; - -export type config = { - host: string; - sk?: string; - pk?: string; - secret?: string; - client?: string; - scope?: string | Array; - timeout: number; - agent?: http.Agent; - headers?: Record; - access?: access; - httpClient?: string; - subdomain?: string; - environment?: Environment; - environmentSubdomain?: EnvironmentSubdomain; -}; - -type options = { - host?: string; - timeout?: number; - agent?: http.Agent; - headers?: Record; - httpClient?: string; - subdomain?: string; -} & (staticKeyOptions | oauthOptions); - -type staticKeyOptions = { - pk?: string; -}; - -type oauthOptions = { - client: string; - scope?: string | Array; - environment?: string; -}; - -export default class Checkout { - payments: Payments; - sources: Sources; - tokens: Tokens; - instruments: Instruments; - webhooks: Webhooks; - events: Events; - disputes: Disputes; - files: Files; - reconciliation: Reconciliation; - customers: Customers; - hostedPayments: HostedPayments; - giropay: Giropay; - ideal: Ideal; - fawry: Fawry; - pagoFacil: PagoFacil; - rapipago: Rapipago; - boleto: Boleto; - baloto: Baloto; - oxxo: Oxxo; - klarna: Klarna; - sepa: Sepa; - paymentLinks: PaymentLinks; - access: Access; - forex: Forex; - applePay: ApplePay; - sessions: Sessions; - workflows: Workflows; - platforms: Platforms; - transfers: Transfers; - balances: Balances; - cardMetadata: CardMetadata; - reports: Reports; - financial: Financial; - issuing: Issuing; - paymentContexts: PaymentContexts; - paymentSessions: PaymentSessions; - paymentSetups: PaymentSetups; - forward: Forward; - paymentMethods: PaymentMethods; - networkTokens: NetworkTokens; - identities: Identities; - accountUpdater: AccountUpdater; - risk: Risk; - agenticCommerce: AgenticCommerce; - complianceRequests: ComplianceRequests; - googlePay: GooglePay; - onboardingSimulator: OnboardingSimulator; - config: config; - - constructor(key?: string, options?: options); -} +//@ts-ignore +import * as http from 'http'; + +import { + Access, + AccountUpdater, + AgenticCommerce, + ApplePay, + Balances, + Baloto, + Boleto, + CardMetadata, + ComplianceRequests, + Customers, + Disputes, + Events, + Fawry, + Files, + Financial, + Forex, + Forward, + Giropay, + GooglePay, + HostedPayments, + Ideal, + Identities, + Instruments, + Issuing, + Klarna, + NetworkTokens, + OnboardingSimulator, + Oxxo, + PagoFacil, + PaymentContexts, + PaymentLinks, + PaymentMethods, + PaymentSessions, + PaymentSetups, + Payments, + Platforms, + Rapipago, + Reconciliation, + Reports, + Risk, + Sepa, + Sessions, + Sources, + Tokens, + Transfers, + Webhooks, + Workflows, +} from './index'; + +import Environment from './Environment'; +import EnvironmentSubdomain from './EnvironmentSubdomain'; + +export type access = { + token: string; + type: string; + scope: string; + expires: Date; +}; + +export type config = { + host: string; + sk?: string; + pk?: string; + secret?: string; + client?: string; + scope?: string | Array; + timeout: number; + agent?: http.Agent; + headers?: Record; + access?: access; + httpClient?: string; + subdomain?: string; + /** @deprecated emergency fallback only, see the README. Use `subdomain` instead. */ + useLegacyDomain?: boolean; + environment?: Environment; + environmentSubdomain?: EnvironmentSubdomain; +}; + +type options = { + host?: string; + timeout?: number; + 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 = { + pk?: string; +}; + +type oauthOptions = { + client: string; + scope?: string | Array; + environment?: string; +}; + +export default class Checkout { + payments: Payments; + sources: Sources; + tokens: Tokens; + instruments: Instruments; + webhooks: Webhooks; + events: Events; + disputes: Disputes; + files: Files; + reconciliation: Reconciliation; + customers: Customers; + hostedPayments: HostedPayments; + giropay: Giropay; + ideal: Ideal; + fawry: Fawry; + pagoFacil: PagoFacil; + rapipago: Rapipago; + boleto: Boleto; + baloto: Baloto; + oxxo: Oxxo; + klarna: Klarna; + sepa: Sepa; + paymentLinks: PaymentLinks; + access: Access; + forex: Forex; + applePay: ApplePay; + sessions: Sessions; + workflows: Workflows; + platforms: Platforms; + transfers: Transfers; + balances: Balances; + cardMetadata: CardMetadata; + reports: Reports; + financial: Financial; + issuing: Issuing; + paymentContexts: PaymentContexts; + paymentSessions: PaymentSessions; + paymentSetups: PaymentSetups; + forward: Forward; + paymentMethods: PaymentMethods; + networkTokens: NetworkTokens; + identities: Identities; + accountUpdater: AccountUpdater; + risk: Risk; + agenticCommerce: AgenticCommerce; + complianceRequests: ComplianceRequests; + googlePay: GooglePay; + onboardingSimulator: OnboardingSimulator; + config: config; + + constructor(key?: string, options?: options); +} diff --git a/types/dist/EnvironmentSubdomain.d.ts b/types/dist/EnvironmentSubdomain.d.ts index 5d9460c5..12008d6b 100644 --- a/types/dist/EnvironmentSubdomain.d.ts +++ b/types/dist/EnvironmentSubdomain.d.ts @@ -17,7 +17,7 @@ export default class EnvironmentSubdomain { /** * Applies subdomain transformation to any given URL. - * If the subdomain is valid (alphanumeric pattern), prepends it to the host. + * Prepends the subdomain to the host when it matches the required pattern. * Otherwise, returns the original URL unchanged. */ static createUrlWithSubdomain(originalUrl: string, subdomain: string): string; From 1402a346f7f8180ca3553b9db9443ba183fcbdd8 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, 11 Aug 2026 10:35:09 +0200 Subject: [PATCH 2/9] test: fall back to the shared hosts when CHECKOUT_MERCHANT_SUBDOMAIN is unset CI failed at module load with "subdomain is required". Twenty-four test files read the subdomain from CHECKOUT_MERCHANT_SUBDOMAIN, but no workflow exports it, so the option arrived undefined. That used to be harmless, it just meant no subdomain; now it throws. They all spread domainOptions() instead, which uses the subdomain when the variable is set and the legacy hosts otherwise. Exporting the variable in CI would not be enough on its own: the sandbox OAuth clients are not provisioned for the subdomain, so the token request would come back invalid_client. This only reproduced in CI because the variable happens to be set locally. --- test/account-updater/account-updater-it.js | 3 ++- test/agentic-commerce/agentic-commerce-it.js | 3 ++- test/apple-pay/apple-pay-it.js | 3 ++- test/balances/balances-it.js | 3 ++- test/card-metadata/card-metadata-it.js | 3 ++- .../compliance-requests-it.js | 3 ++- test/customers/customers-it.js | 3 ++- test/disputes/disputes-it.js | 3 ++- test/domain-options.js | 16 ++++++++++++++++ test/forward/forward-it.js | 3 ++- test/google-pay/google-pay-it.js | 3 ++- test/hosted-payments/hosted-payments-it.js | 3 ++- test/http/httpClient-it.js | 13 +++++++------ test/identities/identities-common.js | 3 ++- test/instruments/instruments-it.js | 3 ++- test/issuing/issuing-common.js | 3 ++- test/network-tokens/network-tokens-it.js | 3 ++- .../onboarding-simulator-it.js | 3 ++- test/payment-contexts/payment-contexts-it.js | 3 ++- test/payment-methods/payment-methods-it.js | 3 ++- .../payment-sessions-complete-it.js | 3 ++- test/payment-sessions/payment-sessions-it.js | 3 ++- test/payment-setups/payment-setups-it.js | 3 ++- test/platforms/reserve-rules/reserve-rules-it.js | 3 ++- test/utils.js | 3 ++- 25 files changed, 69 insertions(+), 29 deletions(-) create mode 100644 test/domain-options.js diff --git a/test/account-updater/account-updater-it.js b/test/account-updater/account-updater-it.js index caa62c51..cbec48dc 100644 --- a/test/account-updater/account-updater-it.js +++ b/test/account-updater/account-updater-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -12,7 +13,7 @@ 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, + ...domainOptions(), }); describe('Integration::AccountUpdater', () => { diff --git a/test/agentic-commerce/agentic-commerce-it.js b/test/agentic-commerce/agentic-commerce-it.js index 76c6dc40..954885a6 100644 --- a/test/agentic-commerce/agentic-commerce-it.js +++ b/test/agentic-commerce/agentic-commerce-it.js @@ -1,10 +1,11 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; 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, + ...domainOptions(), }); describe.skip('Integration::AgenticCommerce', () => { diff --git a/test/apple-pay/apple-pay-it.js b/test/apple-pay/apple-pay-it.js index 2609a8b5..e45ab346 100644 --- a/test/apple-pay/apple-pay-it.js +++ b/test/apple-pay/apple-pay-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -12,7 +13,7 @@ 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, + ...domainOptions(), }); describe('Integration::Apple-Pay', () => { diff --git a/test/balances/balances-it.js b/test/balances/balances-it.js index 370b47db..6de1c3e1 100644 --- a/test/balances/balances-it.js +++ b/test/balances/balances-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -12,7 +13,7 @@ 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, + ...domainOptions(), }); describe('Integration::Balances', () => { diff --git a/test/card-metadata/card-metadata-it.js b/test/card-metadata/card-metadata-it.js index 67a378f7..11fb9d64 100644 --- a/test/card-metadata/card-metadata-it.js +++ b/test/card-metadata/card-metadata-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -12,7 +13,7 @@ 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, + ...domainOptions(), }); describe('Integration::CardMetadata', () => { diff --git a/test/compliance-requests/compliance-requests-it.js b/test/compliance-requests/compliance-requests-it.js index b08ecd40..54785a14 100644 --- a/test/compliance-requests/compliance-requests-it.js +++ b/test/compliance-requests/compliance-requests-it.js @@ -1,10 +1,11 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; 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, + ...domainOptions(), }); describe.skip('Integration::ComplianceRequests', () => { diff --git a/test/customers/customers-it.js b/test/customers/customers-it.js index c1baa02f..fe489e7e 100644 --- a/test/customers/customers-it.js +++ b/test/customers/customers-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -13,7 +14,7 @@ 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, + ...domainOptions(), }); describe('Integration::Customers', () => { diff --git a/test/disputes/disputes-it.js b/test/disputes/disputes-it.js index 1ab45728..cb551874 100644 --- a/test/disputes/disputes-it.js +++ b/test/disputes/disputes-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -12,7 +13,7 @@ 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, + ...domainOptions(), }); describe('Integration::Disputes::Arbitration', () => { diff --git a/test/domain-options.js b/test/domain-options.js new file mode 100644 index 00000000..02f8ff7e --- /dev/null +++ b/test/domain-options.js @@ -0,0 +1,16 @@ +/** + * Every client the suite builds has to choose a domain now that the merchant-specific subdomain + * is mandatory, so they all spread these options. + * + * The subdomain is used when CHECKOUT_MERCHANT_SUBDOMAIN is set, otherwise the client falls back + * to the shared hosts. CI does not export that variable, and it would not help if it did: the + * sandbox OAuth clients are not provisioned for the subdomain, so pointing the token request at + * {subdomain}.access.sandbox.checkout.com returns invalid_client. Until those clients are bound to + * the subdomain, the suite has to be able to run on the legacy hosts. + */ +export const domainOptions = () => { + const subdomain = process.env.CHECKOUT_MERCHANT_SUBDOMAIN; + return subdomain && subdomain.trim() !== '' + ? { subdomain } + : { useLegacyDomain: true }; +}; diff --git a/test/forward/forward-it.js b/test/forward/forward-it.js index 8012aa5b..ae2895ae 100644 --- a/test/forward/forward-it.js +++ b/test/forward/forward-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import Checkout from '../../src/Checkout.js'; @@ -5,7 +6,7 @@ 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, + ...domainOptions(), }); describe('Integration::Forward', () => { diff --git a/test/google-pay/google-pay-it.js b/test/google-pay/google-pay-it.js index e02b808d..aeea8f39 100644 --- a/test/google-pay/google-pay-it.js +++ b/test/google-pay/google-pay-it.js @@ -1,10 +1,11 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; 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, + ...domainOptions(), }); describe.skip('Integration::GooglePay', () => { diff --git a/test/hosted-payments/hosted-payments-it.js b/test/hosted-payments/hosted-payments-it.js index 43ba95cc..2ae5269d 100644 --- a/test/hosted-payments/hosted-payments-it.js +++ b/test/hosted-payments/hosted-payments-it.js @@ -1,10 +1,11 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; 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, + ...domainOptions(), }); const processingChannelId = process.env.CHECKOUT_PROCESSING_CHANNEL_ID; diff --git a/test/http/httpClient-it.js b/test/http/httpClient-it.js index e964a3cf..629984ca 100644 --- a/test/http/httpClient-it.js +++ b/test/http/httpClient-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; import nock from "nock"; import https from 'https'; @@ -19,7 +20,7 @@ describe('Integration::HttpClient', () => { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 3000, httpClient: 'axios', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), } ); const token = await checkout.tokens.request( @@ -50,7 +51,7 @@ describe('Integration::HttpClient', () => { timeout: 3000, httpClient: 'axios', agent: new https.Agent({ keepAlive: true }), - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), } ); const token = await checkout.tokens.request( @@ -79,7 +80,7 @@ describe('Integration::HttpClient', () => { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 100, httpClient: 'axios', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), } ); try { @@ -107,7 +108,7 @@ describe('Integration::HttpClient', () => { { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 3000, - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), } ); const token = await checkout.tokens.request( @@ -137,7 +138,7 @@ describe('Integration::HttpClient', () => { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 3000, agent: new https.Agent({ keepAlive: true }), - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), } ); @@ -167,7 +168,7 @@ describe('Integration::HttpClient', () => { { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 200, - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), } ); try { diff --git a/test/identities/identities-common.js b/test/identities/identities-common.js index 49df9b29..88ec60c5 100644 --- a/test/identities/identities-common.js +++ b/test/identities/identities-common.js @@ -4,11 +4,12 @@ * Requires: CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET * Optional: CHECKOUT_MERCHANT_SUBDOMAIN */ +import { domainOptions } from '../domain-options.js'; import Checkout from '../../src/Checkout.js'; 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, + ...domainOptions(), }); diff --git a/test/instruments/instruments-it.js b/test/instruments/instruments-it.js index 46784844..9b3d1555 100644 --- a/test/instruments/instruments-it.js +++ b/test/instruments/instruments-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; import nock from "nock"; import Checkout from '../../src/Checkout.js' @@ -11,7 +12,7 @@ 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, + ...domainOptions(), }); const sepaRequest = { diff --git a/test/issuing/issuing-common.js b/test/issuing/issuing-common.js index 8a8b00d7..1bdf94f5 100644 --- a/test/issuing/issuing-common.js +++ b/test/issuing/issuing-common.js @@ -5,13 +5,14 @@ * Requires: CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID, CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET * Optional: CHECKOUT_MERCHANT_SUBDOMAIN, CHECKOUT_ISSUING_ENTITY_ID, CHECKOUT_ISSUING_CARD_PRODUCT_ID */ +import { domainOptions } from '../domain-options.js'; import Checkout from '../../src/Checkout.js'; export const cko_issuing = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET, { 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, + ...domainOptions(), }); 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..e0b4a7a0 100644 --- a/test/network-tokens/network-tokens-it.js +++ b/test/network-tokens/network-tokens-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -9,7 +10,7 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), }); describe('Integration::NetworkTokens', () => { diff --git a/test/onboarding-simulator/onboarding-simulator-it.js b/test/onboarding-simulator/onboarding-simulator-it.js index 88c6a531..6d1a1cee 100644 --- a/test/onboarding-simulator/onboarding-simulator-it.js +++ b/test/onboarding-simulator/onboarding-simulator-it.js @@ -1,10 +1,11 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; 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, + ...domainOptions(), }); describe.skip('Integration::OnboardingSimulator', () => { diff --git a/test/payment-contexts/payment-contexts-it.js b/test/payment-contexts/payment-contexts-it.js index 68d08a11..a60574d2 100644 --- a/test/payment-contexts/payment-contexts-it.js +++ b/test/payment-contexts/payment-contexts-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; import nock from "nock"; import Checkout from '../../src/Checkout.js' @@ -9,7 +10,7 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), }); 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..b584eaa1 100644 --- a/test/payment-methods/payment-methods-it.js +++ b/test/payment-methods/payment-methods-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -9,7 +10,7 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), }); 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..7dd00d7f 100644 --- a/test/payment-sessions/payment-sessions-complete-it.js +++ b/test/payment-sessions/payment-sessions-complete-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -10,7 +11,7 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), }); 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..c218b273 100644 --- a/test/payment-sessions/payment-sessions-it.js +++ b/test/payment-sessions/payment-sessions-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; import nock from "nock"; import Checkout from '../../src/Checkout.js' @@ -9,7 +10,7 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), }); describe('Integration::Payment-Sessions', () => { diff --git a/test/payment-setups/payment-setups-it.js b/test/payment-setups/payment-setups-it.js index 376f7522..a1003cd4 100644 --- a/test/payment-setups/payment-setups-it.js +++ b/test/payment-setups/payment-setups-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; import nock from "nock"; import Checkout from '../../src/Checkout.js' @@ -9,7 +10,7 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), }); const processingChannelId = process.env.CHECKOUT_PROCESSING_CHANNEL_ID; diff --git a/test/platforms/reserve-rules/reserve-rules-it.js b/test/platforms/reserve-rules/reserve-rules-it.js index ca147889..feabc076 100644 --- a/test/platforms/reserve-rules/reserve-rules-it.js +++ b/test/platforms/reserve-rules/reserve-rules-it.js @@ -1,3 +1,4 @@ +import { domainOptions } from '../../domain-options.js'; import { expect } from "chai"; import { createEntity, generateFutureDate } from '../../utils.js'; import Checkout from '../../../src/Checkout.js'; @@ -8,7 +9,7 @@ describe('Integration::Platforms::Reserve Rules', () => { client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['accounts'], environment: 'sandbox', - subdomain: process.env.CHECKOUT_MERCHANT_SUBDOMAIN, + ...domainOptions(), }); let entityId; diff --git a/test/utils.js b/test/utils.js index 82a65b43..50d7ea0b 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,3 +1,4 @@ +import { domainOptions } from './domain-options.js'; import Checkout from '../src/Checkout.js'; import { v4 as uuidv4 } from 'uuid'; @@ -5,7 +6,7 @@ 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, + ...domainOptions(), }); /** From 69099c5ad030c6b52a21dd17fd6d7db76e3caa4e 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, 11 Aug 2026 13:25:47 +0200 Subject: [PATCH 3/9] fix: reject a malformed subdomain even when a custom host is set Flagged in review. A custom host is exempt from *requiring* a subdomain, since it replaces the base URL outright, but a subdomain passed alongside one was neither validated nor used: it was quietly dropped. Now the format check applies whenever a subdomain is present, wherever the base URL comes from. --- src/auth-builder.js | 29 ++++++++++++++----- .../environment-subdomain-integration.js | 10 +++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/auth-builder.js b/src/auth-builder.js index 880f14c9..be8c2b5b 100644 --- a/src/auth-builder.js +++ b/src/auth-builder.js @@ -125,7 +125,10 @@ export class AuthBuilder { const environment = isLive ? Environment.live() : Environment.sandbox(); // A custom host replaces the base URL outright, so the merchant has already said - // where requests go and neither option is required here. + // 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; @@ -162,13 +165,7 @@ export class AuthBuilder { ); } - 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)' - ); - } + this.validateSubdomainFormat(subdomain); if (!subdomain && !useLegacyDomain && !this.isPreviousPlatform(key, options)) { throw new ValueError( @@ -181,6 +178,22 @@ export class AuthBuilder { } } + /** + * 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. diff --git a/test/environment-subdomain/environment-subdomain-integration.js b/test/environment-subdomain/environment-subdomain-integration.js index 5812eb2e..7145fe43 100644 --- a/test/environment-subdomain/environment-subdomain-integration.js +++ b/test/environment-subdomain/environment-subdomain-integration.js @@ -214,6 +214,16 @@ describe('SDK Subdomain Integration', () => { expect(cko.config.environmentSubdomain.subdomain).to.equal('customlive'); }); + 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, { From ecb0ca6037a7be0204290ee48edd0b8aea609681 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, 11 Aug 2026 17:27:41 +0200 Subject: [PATCH 4/9] test: add a switch to run the suite against the merchant subdomain The suite could only run against the shared hosts, so the subdomain path this PR makes mandatory had no integration coverage. Reviewers flagged that on every SDK, and it is the right thing to flag. The domain helper now has two modes. Default is unchanged, the shared hosts, because the sandbox OAuth clients are not provisioned for the subdomain and the token request returns invalid_client. Set CHECKOUT_TEST_USE_SUBDOMAIN=true and the suite runs against CHECKOUT_MERCHANT_SUBDOMAIN instead, so once sandbox is provisioned like production it is a one-line change in the workflows, already wired and documented, rather than a rewrite of every fixture. The switch is deliberately separate from CHECKOUT_MERCHANT_SUBDOMAIN, which CI already exports: provisioning should drive the behaviour, not the presence of a secret. --- .github/workflows/build-master.yml | 5 +++++ .github/workflows/build-pull-request.yml | 5 +++++ .github/workflows/build-release.yml | 5 +++++ README.md | 13 +++++++++++++ test/domain-options.js | 22 +++++++++++++++------- 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-master.yml b/.github/workflows/build-master.yml index 65d8708f..1d0b50e1 100644 --- a/.github/workflows/build-master.yml +++ b/.github/workflows/build-master.yml @@ -22,6 +22,11 @@ jobs: CHECKOUT_PREVIOUS_SECRET_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_SECRET_KEY }} CHECKOUT_PREVIOUS_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_PUBLIC_KEY }} CHECKOUT_DEFAULT_SECRET_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_SECRET_KEY }} + CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} + # Flip to 'true' once the sandbox OAuth clients are provisioned for the + # merchant-specific subdomain, and the suite will run against it instead of + # the shared hosts. See test/domain-options.js. + CHECKOUT_TEST_USE_SUBDOMAIN: 'false' CHECKOUT_DEFAULT_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_PUBLIC_KEY }} CHECKOUT_DEFAULT_OAUTH_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET }} diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index cc13a479..5bb05fdd 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -21,6 +21,11 @@ jobs: CHECKOUT_PREVIOUS_SECRET_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_SECRET_KEY }} CHECKOUT_PREVIOUS_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_PUBLIC_KEY }} CHECKOUT_DEFAULT_SECRET_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_SECRET_KEY }} + CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} + # Flip to 'true' once the sandbox OAuth clients are provisioned for the + # merchant-specific subdomain, and the suite will run against it instead of + # the shared hosts. See test/domain-options.js. + CHECKOUT_TEST_USE_SUBDOMAIN: 'false' CHECKOUT_DEFAULT_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_PUBLIC_KEY }} CHECKOUT_DEFAULT_OAUTH_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET }} diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 91d705fc..46b41052 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -23,6 +23,11 @@ jobs: CHECKOUT_PREVIOUS_SECRET_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_SECRET_KEY }} CHECKOUT_PREVIOUS_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_PUBLIC_KEY }} CHECKOUT_DEFAULT_SECRET_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_SECRET_KEY }} + CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} + # Flip to 'true' once the sandbox OAuth clients are provisioned for the + # merchant-specific subdomain, and the suite will run against it instead of + # the shared hosts. See test/domain-options.js. + CHECKOUT_TEST_USE_SUBDOMAIN: 'false' CHECKOUT_DEFAULT_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_PUBLIC_KEY }} CHECKOUT_DEFAULT_OAUTH_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET }} diff --git a/README.md b/README.md index 7e65c4f2..c4906427 100644 --- a/README.md +++ b/README.md @@ -532,3 +532,16 @@ 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) + +## Running the tests against your subdomain + +The test suite builds every client through `test/domain-options.js`, which has two modes. By default it uses the shared hosts, because the sandbox OAuth clients are not provisioned for merchant-specific subdomains and the token request would come back `invalid_client`. To run against a subdomain instead: + +```bash +export CHECKOUT_MERCHANT_SUBDOMAIN="your_subdomain" +export CHECKOUT_TEST_USE_SUBDOMAIN=true +npm test +``` + +The switch is separate from `CHECKOUT_MERCHANT_SUBDOMAIN` on purpose: CI already exports that secret, so provisioning is what should flip the behaviour, not the presence of a value. Once sandbox is provisioned like production, set `CHECKOUT_TEST_USE_SUBDOMAIN: 'true'` in the workflows and CI exercises the subdomain path end to end. + diff --git a/test/domain-options.js b/test/domain-options.js index 02f8ff7e..91f9bd78 100644 --- a/test/domain-options.js +++ b/test/domain-options.js @@ -1,16 +1,24 @@ /** * Every client the suite builds has to choose a domain now that the merchant-specific subdomain - * is mandatory, so they all spread these options. + * is mandatory, so they all spread these options. There are deliberately two modes. * - * The subdomain is used when CHECKOUT_MERCHANT_SUBDOMAIN is set, otherwise the client falls back - * to the shared hosts. CI does not export that variable, and it would not help if it did: the - * sandbox OAuth clients are not provisioned for the subdomain, so pointing the token request at - * {subdomain}.access.sandbox.checkout.com returns invalid_client. Until those clients are bound to - * the subdomain, the suite has to be able to run on the legacy hosts. + * Default: the shared hosts. The sandbox OAuth clients are not provisioned for the + * merchant-specific subdomain, so pointing the token request at + * {subdomain}.access.sandbox.checkout.com returns invalid_client for every integration test. + * + * Opt-in: set CHECKOUT_TEST_USE_SUBDOMAIN=true and the suite runs against + * CHECKOUT_MERCHANT_SUBDOMAIN instead, exercising end to end the path merchants are being moved + * to. Once sandbox is provisioned like production, set that variable in the workflows and this + * becomes the mode CI runs in. The switch is deliberately separate from + * CHECKOUT_MERCHANT_SUBDOMAIN, which CI exports, so provisioning drives the change rather than + * the presence of a secret. */ +export const useSubdomain = () => + (process.env.CHECKOUT_TEST_USE_SUBDOMAIN || '').toLowerCase() === 'true'; + export const domainOptions = () => { const subdomain = process.env.CHECKOUT_MERCHANT_SUBDOMAIN; - return subdomain && subdomain.trim() !== '' + return useSubdomain() && subdomain && subdomain.trim() !== '' ? { subdomain } : { useLegacyDomain: true }; }; From 40bb33c3b4086705b2fd9b47389557bed6c2c242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:19:54 +0200 Subject: [PATCH 5/9] revert: leave the version bump to the release Versions are bumped on master during the release, not in a feature branch, per the release workflow. This branch should carry only the change itself; the major bump is classified and applied when the release is cut. --- README.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c4906427..eafe7aa3 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ 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. -> **⚠️ Breaking change in 5.0.0:** 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). +> **⚠️ 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 diff --git a/package.json b/package.json index c52b9cd6..7038b145 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "checkout-sdk-node", - "version": "5.0.0", + "version": "4.1.0", "description": "Official Node.js SDK for Checkout.com payment gateway - Full API coverage with TypeScript support", "type": "module", "engines": { From 122c10ebb3a745296bcee3a74603346e9be04869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armando=20Rodr=C3=ADguez?= <127134616+armando-rodriguez-cko@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:52:05 +0200 Subject: [PATCH 6/9] revert: drop the test domain helpers and the workflow variable Two problems with the previous approach. It needed a new variable in 21 workflow files, which is not viable without access to create secrets. And it wrapped the builder chain in a configureDomain helper that is not part of the public API, so the tests stopped looking like the code a merchant would actually write. Every fixture now calls the real opt-out inline, in the chain, with a comment saying why: the sandbox OAuth clients are not provisioned for the merchant-specific subdomain, so the token request comes back invalid_client. When sandbox is provisioned, those calls become the subdomain setter. The unit tests covering all four combinations are untouched: they already used the public API directly. --- .github/workflows/build-master.yml | 5 --- .github/workflows/build-pull-request.yml | 5 --- .github/workflows/build-release.yml | 5 --- README.md | 12 ------- test/account-updater/account-updater-it.js | 6 ++-- test/agentic-commerce/agentic-commerce-it.js | 6 ++-- test/apple-pay/apple-pay-it.js | 6 ++-- test/balances/balances-it.js | 6 ++-- test/card-metadata/card-metadata-it.js | 6 ++-- .../compliance-requests-it.js | 6 ++-- test/customers/customers-it.js | 6 ++-- test/disputes/disputes-it.js | 6 ++-- test/domain-options.js | 24 -------------- test/forward/forward-it.js | 6 ++-- test/google-pay/google-pay-it.js | 6 ++-- test/hosted-payments/hosted-payments-it.js | 6 ++-- test/http/httpClient-it.js | 31 ++++++++++++++----- test/identities/identities-common.js | 6 ++-- test/instruments/instruments-it.js | 6 ++-- test/issuing/issuing-common.js | 6 ++-- test/network-tokens/network-tokens-it.js | 6 ++-- .../onboarding-simulator-it.js | 6 ++-- test/payment-contexts/payment-contexts-it.js | 6 ++-- test/payment-methods/payment-methods-it.js | 6 ++-- .../payment-sessions-complete-it.js | 6 ++-- test/payment-sessions/payment-sessions-it.js | 6 ++-- test/payment-setups/payment-setups-it.js | 6 ++-- .../reserve-rules/reserve-rules-it.js | 6 ++-- test/utils.js | 6 ++-- 29 files changed, 116 insertions(+), 104 deletions(-) delete mode 100644 test/domain-options.js diff --git a/.github/workflows/build-master.yml b/.github/workflows/build-master.yml index 1d0b50e1..65d8708f 100644 --- a/.github/workflows/build-master.yml +++ b/.github/workflows/build-master.yml @@ -22,11 +22,6 @@ jobs: CHECKOUT_PREVIOUS_SECRET_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_SECRET_KEY }} CHECKOUT_PREVIOUS_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_PUBLIC_KEY }} CHECKOUT_DEFAULT_SECRET_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_SECRET_KEY }} - CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} - # Flip to 'true' once the sandbox OAuth clients are provisioned for the - # merchant-specific subdomain, and the suite will run against it instead of - # the shared hosts. See test/domain-options.js. - CHECKOUT_TEST_USE_SUBDOMAIN: 'false' CHECKOUT_DEFAULT_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_PUBLIC_KEY }} CHECKOUT_DEFAULT_OAUTH_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET }} diff --git a/.github/workflows/build-pull-request.yml b/.github/workflows/build-pull-request.yml index 5bb05fdd..cc13a479 100644 --- a/.github/workflows/build-pull-request.yml +++ b/.github/workflows/build-pull-request.yml @@ -21,11 +21,6 @@ jobs: CHECKOUT_PREVIOUS_SECRET_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_SECRET_KEY }} CHECKOUT_PREVIOUS_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_PUBLIC_KEY }} CHECKOUT_DEFAULT_SECRET_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_SECRET_KEY }} - CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} - # Flip to 'true' once the sandbox OAuth clients are provisioned for the - # merchant-specific subdomain, and the suite will run against it instead of - # the shared hosts. See test/domain-options.js. - CHECKOUT_TEST_USE_SUBDOMAIN: 'false' CHECKOUT_DEFAULT_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_PUBLIC_KEY }} CHECKOUT_DEFAULT_OAUTH_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET }} diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 46b41052..91d705fc 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -23,11 +23,6 @@ jobs: CHECKOUT_PREVIOUS_SECRET_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_SECRET_KEY }} CHECKOUT_PREVIOUS_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_PREVIOUS_PUBLIC_KEY }} CHECKOUT_DEFAULT_SECRET_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_SECRET_KEY }} - CHECKOUT_MERCHANT_SUBDOMAIN: ${{ secrets.IT_CHECKOUT_MERCHANT_SUBDOMAIN }} - # Flip to 'true' once the sandbox OAuth clients are provisioned for the - # merchant-specific subdomain, and the suite will run against it instead of - # the shared hosts. See test/domain-options.js. - CHECKOUT_TEST_USE_SUBDOMAIN: 'false' CHECKOUT_DEFAULT_PUBLIC_KEY: ${{ secrets.IT_CHECKOUT_DEFAULT_PUBLIC_KEY }} CHECKOUT_DEFAULT_OAUTH_CLIENT_ID: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_ID }} CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET: ${{ secrets.IT_CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET }} diff --git a/README.md b/README.md index eafe7aa3..73a2cb58 100644 --- a/README.md +++ b/README.md @@ -533,15 +533,3 @@ MIT License - see [LICENSE](LICENSE) for details. - 📚 Documentation: [https://api-reference.checkout.com/](https://api-reference.checkout.com/) - 💬 Community: [GitHub Discussions](https://github.com/checkout/checkout-sdk-node/discussions) -## Running the tests against your subdomain - -The test suite builds every client through `test/domain-options.js`, which has two modes. By default it uses the shared hosts, because the sandbox OAuth clients are not provisioned for merchant-specific subdomains and the token request would come back `invalid_client`. To run against a subdomain instead: - -```bash -export CHECKOUT_MERCHANT_SUBDOMAIN="your_subdomain" -export CHECKOUT_TEST_USE_SUBDOMAIN=true -npm test -``` - -The switch is separate from `CHECKOUT_MERCHANT_SUBDOMAIN` on purpose: CI already exports that secret, so provisioning is what should flip the behaviour, not the presence of a value. Once sandbox is provisioned like production, set `CHECKOUT_TEST_USE_SUBDOMAIN: 'true'` in the workflows and CI exercises the subdomain path end to end. - diff --git a/test/account-updater/account-updater-it.js b/test/account-updater/account-updater-it.js index cbec48dc..6546fe4b 100644 --- a/test/account-updater/account-updater-it.js +++ b/test/account-updater/account-updater-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -13,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', - ...domainOptions(), + // 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 954885a6..74de4381 100644 --- a/test/agentic-commerce/agentic-commerce-it.js +++ b/test/agentic-commerce/agentic-commerce-it.js @@ -1,11 +1,13 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; 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', - ...domainOptions(), + // 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 e45ab346..3d05e374 100644 --- a/test/apple-pay/apple-pay-it.js +++ b/test/apple-pay/apple-pay-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -13,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', - ...domainOptions(), + // 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 6de1c3e1..c0bb3787 100644 --- a/test/balances/balances-it.js +++ b/test/balances/balances-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -13,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', - ...domainOptions(), + // 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 11fb9d64..b15c4bba 100644 --- a/test/card-metadata/card-metadata-it.js +++ b/test/card-metadata/card-metadata-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -13,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', - ...domainOptions(), + // 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 54785a14..436d3707 100644 --- a/test/compliance-requests/compliance-requests-it.js +++ b/test/compliance-requests/compliance-requests-it.js @@ -1,11 +1,13 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; 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', - ...domainOptions(), + // 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/customers/customers-it.js b/test/customers/customers-it.js index fe489e7e..15921f8a 100644 --- a/test/customers/customers-it.js +++ b/test/customers/customers-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -14,7 +13,10 @@ afterEach(() => { const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, environment: 'sandbox', - ...domainOptions(), + // 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 cb551874..d0787e0a 100644 --- a/test/disputes/disputes-it.js +++ b/test/disputes/disputes-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -13,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', - ...domainOptions(), + // 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/domain-options.js b/test/domain-options.js deleted file mode 100644 index 91f9bd78..00000000 --- a/test/domain-options.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Every client the suite builds has to choose a domain now that the merchant-specific subdomain - * is mandatory, so they all spread these options. There are deliberately two modes. - * - * Default: the shared hosts. The sandbox OAuth clients are not provisioned for the - * merchant-specific subdomain, so pointing the token request at - * {subdomain}.access.sandbox.checkout.com returns invalid_client for every integration test. - * - * Opt-in: set CHECKOUT_TEST_USE_SUBDOMAIN=true and the suite runs against - * CHECKOUT_MERCHANT_SUBDOMAIN instead, exercising end to end the path merchants are being moved - * to. Once sandbox is provisioned like production, set that variable in the workflows and this - * becomes the mode CI runs in. The switch is deliberately separate from - * CHECKOUT_MERCHANT_SUBDOMAIN, which CI exports, so provisioning drives the change rather than - * the presence of a secret. - */ -export const useSubdomain = () => - (process.env.CHECKOUT_TEST_USE_SUBDOMAIN || '').toLowerCase() === 'true'; - -export const domainOptions = () => { - const subdomain = process.env.CHECKOUT_MERCHANT_SUBDOMAIN; - return useSubdomain() && subdomain && subdomain.trim() !== '' - ? { subdomain } - : { useLegacyDomain: true }; -}; diff --git a/test/forward/forward-it.js b/test/forward/forward-it.js index ae2895ae..2cf9cf5f 100644 --- a/test/forward/forward-it.js +++ b/test/forward/forward-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import Checkout from '../../src/Checkout.js'; @@ -6,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', - ...domainOptions(), + // 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 aeea8f39..29a28aa1 100644 --- a/test/google-pay/google-pay-it.js +++ b/test/google-pay/google-pay-it.js @@ -1,11 +1,13 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; 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', - ...domainOptions(), + // 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 2ae5269d..6d560aee 100644 --- a/test/hosted-payments/hosted-payments-it.js +++ b/test/hosted-payments/hosted-payments-it.js @@ -1,11 +1,13 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; 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', - ...domainOptions(), + // 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/http/httpClient-it.js b/test/http/httpClient-it.js index 629984ca..654a1f4c 100644 --- a/test/http/httpClient-it.js +++ b/test/http/httpClient-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; import nock from "nock"; import https from 'https'; @@ -20,7 +19,10 @@ describe('Integration::HttpClient', () => { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 3000, httpClient: 'axios', - ...domainOptions(), + // 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( @@ -51,7 +53,10 @@ describe('Integration::HttpClient', () => { timeout: 3000, httpClient: 'axios', agent: new https.Agent({ keepAlive: true }), - ...domainOptions(), + // 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( @@ -80,7 +85,10 @@ describe('Integration::HttpClient', () => { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 100, httpClient: 'axios', - ...domainOptions(), + // 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 { @@ -108,7 +116,10 @@ describe('Integration::HttpClient', () => { { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 3000, - ...domainOptions(), + // 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( @@ -138,7 +149,10 @@ describe('Integration::HttpClient', () => { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 3000, agent: new https.Agent({ keepAlive: true }), - ...domainOptions(), + // 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, } ); @@ -168,7 +182,10 @@ describe('Integration::HttpClient', () => { { pk: process.env.CHECKOUT_DEFAULT_PUBLIC_KEY, timeout: 200, - ...domainOptions(), + // 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 88ec60c5..13b819a7 100644 --- a/test/identities/identities-common.js +++ b/test/identities/identities-common.js @@ -4,12 +4,14 @@ * Requires: CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET * Optional: CHECKOUT_MERCHANT_SUBDOMAIN */ -import { domainOptions } from '../domain-options.js'; import Checkout from '../../src/Checkout.js'; 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', - ...domainOptions(), + // 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 9b3d1555..5325f4ac 100644 --- a/test/instruments/instruments-it.js +++ b/test/instruments/instruments-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; import nock from "nock"; import Checkout from '../../src/Checkout.js' @@ -12,7 +11,10 @@ afterEach(() => { const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { pk: process.env.CHECKOUT_PREVIOUS_PUBLIC_KEY, environment: 'sandbox', - ...domainOptions(), + // 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 1bdf94f5..5a1faf45 100644 --- a/test/issuing/issuing-common.js +++ b/test/issuing/issuing-common.js @@ -5,14 +5,16 @@ * Requires: CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID, CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET * Optional: CHECKOUT_MERCHANT_SUBDOMAIN, CHECKOUT_ISSUING_ENTITY_ID, CHECKOUT_ISSUING_CARD_PRODUCT_ID */ -import { domainOptions } from '../domain-options.js'; import Checkout from '../../src/Checkout.js'; export const cko_issuing = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET, { 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', - ...domainOptions(), + // 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 e0b4a7a0..02f02a02 100644 --- a/test/network-tokens/network-tokens-it.js +++ b/test/network-tokens/network-tokens-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -10,7 +9,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - ...domainOptions(), + // 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 6d1a1cee..b9f1d4cb 100644 --- a/test/onboarding-simulator/onboarding-simulator-it.js +++ b/test/onboarding-simulator/onboarding-simulator-it.js @@ -1,11 +1,13 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; 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', - ...domainOptions(), + // 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 a60574d2..4d9a8570 100644 --- a/test/payment-contexts/payment-contexts-it.js +++ b/test/payment-contexts/payment-contexts-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; import nock from "nock"; import Checkout from '../../src/Checkout.js' @@ -10,7 +9,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - ...domainOptions(), + // 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 b584eaa1..59e7d2f9 100644 --- a/test/payment-methods/payment-methods-it.js +++ b/test/payment-methods/payment-methods-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -10,7 +9,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - ...domainOptions(), + // 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 7dd00d7f..fb037181 100644 --- a/test/payment-sessions/payment-sessions-complete-it.js +++ b/test/payment-sessions/payment-sessions-complete-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from 'chai'; import nock from 'nock'; import Checkout from '../../src/Checkout.js'; @@ -11,7 +10,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - ...domainOptions(), + // 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 c218b273..c052c5f7 100644 --- a/test/payment-sessions/payment-sessions-it.js +++ b/test/payment-sessions/payment-sessions-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; import nock from "nock"; import Checkout from '../../src/Checkout.js' @@ -10,7 +9,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - ...domainOptions(), + // 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 a1003cd4..53fcf8b9 100644 --- a/test/payment-setups/payment-setups-it.js +++ b/test/payment-setups/payment-setups-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../domain-options.js'; import { expect } from "chai"; import nock from "nock"; import Checkout from '../../src/Checkout.js' @@ -10,7 +9,10 @@ afterEach(() => { }); const cko = new Checkout(process.env.CHECKOUT_DEFAULT_SECRET_KEY, { - ...domainOptions(), + // 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/platforms/reserve-rules/reserve-rules-it.js b/test/platforms/reserve-rules/reserve-rules-it.js index feabc076..bbbc7993 100644 --- a/test/platforms/reserve-rules/reserve-rules-it.js +++ b/test/platforms/reserve-rules/reserve-rules-it.js @@ -1,4 +1,3 @@ -import { domainOptions } from '../../domain-options.js'; import { expect } from "chai"; import { createEntity, generateFutureDate } from '../../utils.js'; import Checkout from '../../../src/Checkout.js'; @@ -9,7 +8,10 @@ describe('Integration::Platforms::Reserve Rules', () => { client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['accounts'], environment: 'sandbox', - ...domainOptions(), + // 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/utils.js b/test/utils.js index 50d7ea0b..eba93365 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,4 +1,3 @@ -import { domainOptions } from './domain-options.js'; import Checkout from '../src/Checkout.js'; import { v4 as uuidv4 } from 'uuid'; @@ -6,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', - ...domainOptions(), + // 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, }); /** From 746d499ae40df481ab63ead30e3583211e32e6af 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, 31 Aug 2026 10:47:22 +0200 Subject: [PATCH 7/9] test: run the issuing and payout schedule suites with the default OAuth client The dedicated sandbox clients are not provisioned for the merchant subdomain; the default client now carries every scope the suites need. --- test/issuing/access/access-it.js | 2 +- test/issuing/cardholders/cardholders-it.js | 2 +- test/issuing/cards/cards-it.js | 2 +- test/issuing/controls/controls-it.js | 2 +- test/issuing/issuing-common.js | 6 +++--- test/issuing/simulate/simulate-it.js | 2 +- test/issuing/transactions/transactions-it.js | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/issuing/access/access-it.js b/test/issuing/access/access-it.js index bebc8fa4..9caf5f07 100644 --- a/test/issuing/access/access-it.js +++ b/test/issuing/access/access-it.js @@ -12,7 +12,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Access - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Access - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { it('should request cardholder access token', async function () { const cardholder = await createCardholder(); const response = await cko_issuing.issuing.requestCardholderAccessToken({ diff --git a/test/issuing/cardholders/cardholders-it.js b/test/issuing/cardholders/cardholders-it.js index 264461a7..be33f81a 100644 --- a/test/issuing/cardholders/cardholders-it.js +++ b/test/issuing/cardholders/cardholders-it.js @@ -16,7 +16,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Cardholders - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Cardholders - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { let cardholder; before(async function () { diff --git a/test/issuing/cards/cards-it.js b/test/issuing/cards/cards-it.js index 07527f1e..ba6cc4bd 100644 --- a/test/issuing/cards/cards-it.js +++ b/test/issuing/cards/cards-it.js @@ -18,7 +18,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Cards - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Cards - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { let cardholder; let card; diff --git a/test/issuing/controls/controls-it.js b/test/issuing/controls/controls-it.js index 3136003c..070e0473 100644 --- a/test/issuing/controls/controls-it.js +++ b/test/issuing/controls/controls-it.js @@ -16,7 +16,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Controls - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Controls - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { let cardholder; let card; let control; diff --git a/test/issuing/issuing-common.js b/test/issuing/issuing-common.js index 5a1faf45..71dd842d 100644 --- a/test/issuing/issuing-common.js +++ b/test/issuing/issuing-common.js @@ -2,13 +2,13 @@ * Shared setup for Issuing integration tests. * Aligned with checkout-sdk-net IssuingCommon.cs * - * Requires: CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID, CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET + * Requires: CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET * Optional: CHECKOUT_MERCHANT_SUBDOMAIN, CHECKOUT_ISSUING_ENTITY_ID, CHECKOUT_ISSUING_CARD_PRODUCT_ID */ import Checkout from '../../src/Checkout.js'; -export const cko_issuing = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET, { - client: process.env.CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID, +export const cko_issuing = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET, { + client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, scope: ['issuing:card-mgmt', 'issuing:client', 'issuing:controls-read', 'issuing:controls-write', 'issuing:transactions-read', 'vault'], environment: 'sandbox', // The sandbox OAuth clients are not provisioned for the merchant-specific diff --git a/test/issuing/simulate/simulate-it.js b/test/issuing/simulate/simulate-it.js index 795b2d04..37ed0014 100644 --- a/test/issuing/simulate/simulate-it.js +++ b/test/issuing/simulate/simulate-it.js @@ -17,7 +17,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Simulate - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Simulate - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { let cardholder; let card; let cardDetails; diff --git a/test/issuing/transactions/transactions-it.js b/test/issuing/transactions/transactions-it.js index 4b553411..c0b3ad5c 100644 --- a/test/issuing/transactions/transactions-it.js +++ b/test/issuing/transactions/transactions-it.js @@ -12,7 +12,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Transactions - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Transactions - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { it('should get transactions list', async () => { const response = await cko_issuing.issuing.getTransactions(); expect(response).to.not.be.null; From 44258860ee98a4f3f42d23581d265ebe0a7043f5 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, 31 Aug 2026 11:14:55 +0200 Subject: [PATCH 8/9] Revert "test: run the issuing and payout schedule suites with the default OAuth client" This reverts commit 746d499ae40df481ab63ead30e3583211e32e6af. --- test/issuing/access/access-it.js | 2 +- test/issuing/cardholders/cardholders-it.js | 2 +- test/issuing/cards/cards-it.js | 2 +- test/issuing/controls/controls-it.js | 2 +- test/issuing/issuing-common.js | 6 +++--- test/issuing/simulate/simulate-it.js | 2 +- test/issuing/transactions/transactions-it.js | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/issuing/access/access-it.js b/test/issuing/access/access-it.js index 9caf5f07..bebc8fa4 100644 --- a/test/issuing/access/access-it.js +++ b/test/issuing/access/access-it.js @@ -12,7 +12,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Access - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Access - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { it('should request cardholder access token', async function () { const cardholder = await createCardholder(); const response = await cko_issuing.issuing.requestCardholderAccessToken({ diff --git a/test/issuing/cardholders/cardholders-it.js b/test/issuing/cardholders/cardholders-it.js index be33f81a..264461a7 100644 --- a/test/issuing/cardholders/cardholders-it.js +++ b/test/issuing/cardholders/cardholders-it.js @@ -16,7 +16,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Cardholders - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Cardholders - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { let cardholder; before(async function () { diff --git a/test/issuing/cards/cards-it.js b/test/issuing/cards/cards-it.js index ba6cc4bd..07527f1e 100644 --- a/test/issuing/cards/cards-it.js +++ b/test/issuing/cards/cards-it.js @@ -18,7 +18,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Cards - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Cards - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { let cardholder; let card; diff --git a/test/issuing/controls/controls-it.js b/test/issuing/controls/controls-it.js index 070e0473..3136003c 100644 --- a/test/issuing/controls/controls-it.js +++ b/test/issuing/controls/controls-it.js @@ -16,7 +16,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Controls - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Controls - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { let cardholder; let card; let control; diff --git a/test/issuing/issuing-common.js b/test/issuing/issuing-common.js index 71dd842d..5a1faf45 100644 --- a/test/issuing/issuing-common.js +++ b/test/issuing/issuing-common.js @@ -2,13 +2,13 @@ * Shared setup for Issuing integration tests. * Aligned with checkout-sdk-net IssuingCommon.cs * - * Requires: CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET + * Requires: CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID, CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET * Optional: CHECKOUT_MERCHANT_SUBDOMAIN, CHECKOUT_ISSUING_ENTITY_ID, CHECKOUT_ISSUING_CARD_PRODUCT_ID */ import Checkout from '../../src/Checkout.js'; -export const cko_issuing = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET, { - client: process.env.CHECKOUT_DEFAULT_OAUTH_CLIENT_ID, +export const cko_issuing = new Checkout(process.env.CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET, { + 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', // The sandbox OAuth clients are not provisioned for the merchant-specific diff --git a/test/issuing/simulate/simulate-it.js b/test/issuing/simulate/simulate-it.js index 37ed0014..795b2d04 100644 --- a/test/issuing/simulate/simulate-it.js +++ b/test/issuing/simulate/simulate-it.js @@ -17,7 +17,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Simulate - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Simulate - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { let cardholder; let card; let cardDetails; diff --git a/test/issuing/transactions/transactions-it.js b/test/issuing/transactions/transactions-it.js index c0b3ad5c..4b553411 100644 --- a/test/issuing/transactions/transactions-it.js +++ b/test/issuing/transactions/transactions-it.js @@ -12,7 +12,7 @@ afterEach(() => { nock.enableNetConnect(); }); -describe.skip('Integration::Issuing::Transactions - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_CLIENT_SECRET with Issuing enabled', function () { +describe.skip('Integration::Issuing::Transactions - AuthenticationError: Requires CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_ID and CHECKOUT_DEFAULT_OAUTH_ISSUING_CLIENT_SECRET with Issuing enabled', function () { it('should get transactions list', async () => { const response = await cko_issuing.issuing.getTransactions(); expect(response).to.not.be.null; From f6b1fed9603eeb05e6b12b49abe41ecb2c583f3c 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, 31 Aug 2026 12:45:02 +0200 Subject: [PATCH 9/9] fix: make EnvironmentSubdomain throw on invalid input and address the review findings - EnvironmentSubdomain.createUrlWithSubdomain now throws a ValueError on an invalid subdomain instead of silently returning the un-prefixed URL, and no longer swallows URL parsing errors; JSDoc, the .d.ts and the unit tests are updated to match the other SDKs. - Restore the original CRLF line endings in src/config.js and types/dist/Checkout.d.ts so the diff only shows the real changes. - Document the custom host route in the README legacy-domain section and the Private Link pl- prefixed subdomain in the Subdomain value section. - Add a unit test asserting a NAS-shaped secret key is not exempt from the subdomain requirement, and a comment documenting that the Previous (ABC) exemption is inferred from the secret-key shape. --- README.md | 4 +- src/EnvironmentSubdomain.js | 34 +- src/auth-builder.js | 2 + src/config.js | 412 +++++++++--------- test/config/config.js | 7 + .../environment-subdomain.js | 76 ++-- types/dist/Checkout.d.ts | 330 +++++++------- types/dist/EnvironmentSubdomain.d.ts | 5 +- 8 files changed, 432 insertions(+), 438 deletions(-) diff --git a/README.md b/README.md index 73a2cb58..79bbf5ef 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The official Node.js SDK for [Checkout.com](https://www.checkout.com) payment ga ### 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`. 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. +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 @@ -517,7 +517,7 @@ const cko = new Checkout('sk_...', { }); ``` -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. +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 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 be8c2b5b..70a1992f 100644 --- a/src/auth-builder.js +++ b/src/auth-builder.js @@ -167,6 +167,8 @@ export class AuthBuilder { 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 ' + diff --git a/src/config.js b/src/config.js index 4eb7619f..55fde5ba 100644 --- a/src/config.js +++ b/src/config.js @@ -1,206 +1,206 @@ -/** Base URLs for main API. Per API Reference Base URLs, account-specific URLs use {prefix}.api.(sandbox.)checkout.com; see EnvironmentSubdomain when subdomain is set. */ -export const SANDBOX_BASE_URL = 'https://api.sandbox.checkout.com'; -export const LIVE_BASE_URL = 'https://api.checkout.com'; -export const SANDBOX_ACCESS_URL = 'https://access.sandbox.checkout.com/connect/token'; -export const LIVE_ACCESS_URL = 'https://access.checkout.com/connect/token'; - -export const PLATFORMS_FILES_LIVE_URL = 'https://files.checkout.com/files'; -export const PLATFORMS_FILES_SANDBOX_URL = 'https://files.sandbox.checkout.com/files'; - -export const TRANSFERS_SANDBOX_URL = 'https://transfers.sandbox.checkout.com/transfers'; -export const TRANSFERS_LIVE_URL = 'https://transfers.checkout.com/transfers'; - -// Forward host root (no trailing slash). The `forward` and `secrets` path -// segments are appended by the ForwardClient methods (matching every other SDK). -export const FORWARD_SANDBOX_URL = 'https://forward.sandbox.checkout.com'; -export const FORWARD_LIVE_URL = 'https://forward.checkout.com'; - -export const BALANCES_SANDBOX_URL = 'https://balances.sandbox.checkout.com/balances'; -export const BALANCES_LIVE_URL = 'https://balances.checkout.com/balances'; - -export const IDENTITY_VERIFICATION_SANDBOX_URL = 'https://identity-verification.sandbox.checkout.com'; -export const IDENTITY_VERIFICATION_LIVE_URL = 'https://identity-verification.checkout.com'; - -export const REQUEST_ID_HEADER = 'cko-request-id'; -export const API_VERSION_HEADER = 'cko-version'; -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*#$=]$/; -export const NAS_SANDBOX_PUBLIC_KEY_REGEX = /^pk_sbox_?[a-z2-7]{26}[a-z2-7*#$=]$/; -export const PAYMENT_TYPES = { - regular: 'Regular', - recurring: 'Recurring', - moto: 'MOTO', - installment: 'Installment', - unscheduled: 'Unscheduled', -}; -export const CURRENCIES = { - ALL: 'ALL', - STN: 'STN', - EEK: 'EEK', - BHD: 'BHD', - SCR: 'SCR', - DJF: 'DJF', - EGP: 'EGP', - MDL: 'MDL', - MZN: 'MZN', - BND: 'BND', - ZMK: 'ZMK', - SHP: 'SHP', - LBP: 'LBP', - AWG: 'AWG', - JMD: 'JMD', - KES: 'KES', - BYN: 'BYN', - KHR: 'KHR', - LAK: 'LAK', - MVR: 'MVR', - AOA: 'AOA', - TJS: 'TJS', - SVC: 'SVC', - GNF: 'GNF', - BRL: 'BRL', - MOP: 'MOP', - BOB: 'BOB', - CDF: 'CDF', - NAD: 'NAD', - LYD: 'LYD', - VUV: 'VUV', - QAR: 'QAR', - CLP: 'CLP', - HRK: 'HRK', - ISK: 'ISK', - FKP: 'FKP', - XCD: 'XCD', - NOK: 'NOK', - CUP: 'CUP', - VND: 'VND', - PEN: 'PEN', - KMF: 'KMF', - LVL: 'LVL', - MMK: 'MMK', - TRY: 'TRY', - VEF: 'VEF', - AUD: 'AUD', - TWD: 'TWD', - PKR: 'PKR', - SLL: 'SLL', - BGN: 'BGN', - LRD: 'LRD', - LKR: 'LKR', - XAF: 'XAF', - JOD: 'JOD', - ANG: 'ANG', - BSD: 'BSD', - CAD: 'CAD', - GIP: 'GIP', - MNT: 'MNT', - LTL: 'LTL', - BBD: 'BBD', - CLF: 'CLF', - BWP: 'BWP', - COP: 'COP', - PHP: 'PHP', - HUF: 'HUF', - FJD: 'FJD', - MWK: 'MWK', - THB: 'THB', - XPF: 'XPF', - RSD: 'RSD', - SAR: 'SAR', - UYU: 'UYU', - BZD: 'BZD', - SYP: 'SYP', - GMD: 'GMD', - SZL: 'SZL', - SBD: 'SBD', - ETB: 'ETB', - CHF: 'CHF', - MXN: 'MXN', - ARS: 'ARS', - GTQ: 'GTQ', - GHS: 'GHS', - NIO: 'NIO', - JPY: 'JPY', - BDT: 'BDT', - UZS: 'UZS', - SOS: 'SOS', - BTN: 'BTN', - NZD: 'NZD', - TZS: 'TZS', - IQD: 'IQD', - MGA: 'MGA', - DZD: 'DZD', - GYD: 'GYD', - USD: 'USD', - KWD: 'KWD', - CNY: 'CNY', - PYG: 'PYG', - SGD: 'SGD', - KZT: 'KZT', - PGK: 'PGK', - AMD: 'AMD', - GBP: 'GBP', - AFN: 'AFN', - CRC: 'CRC', - XOF: 'XOF', - YER: 'YER', - MRU: 'MRU', - DKK: 'DKK', - TOP: 'TOP', - INR: 'INR', - SDG: 'SDG', - DOP: 'DOP', - ZWL: 'ZWL', - UGX: 'UGX', - SEK: 'SEK', - LSL: 'LSL', - MYR: 'MYR', - TMT: 'TMT', - OMR: 'OMR', - BMD: 'BMD', - KRW: 'KRW', - HKD: 'HKD', - KGS: 'KGS', - BAM: 'BAM', - NGN: 'NGN', - ILS: 'ILS', - MUR: 'MUR', - RON: 'RON', - TND: 'TND', - AED: 'AED', - PAB: 'PAB', - NPR: 'NPR', - TTD: 'TTD', - RWF: 'RWF', - HTG: 'HTG', - IDR: 'IDR', - EUR: 'EUR', - KYD: 'KYD', - IRR: 'IRR', - KPW: 'KPW', - MKD: 'MKD', - SRD: 'SRD', - HNL: 'HNL', - AZN: 'AZN', - ERN: 'ERN', - CZK: 'CZK', - CVE: 'CVE', - BIF: 'BIF', - MAD: 'MAD', - RUB: 'RUB', - UAH: 'UAH', - WST: 'WST', - PLN: 'PLN', - ZAR: 'ZAR', - GEL: 'GEL', - ZMW: 'ZMW', -}; +/** Base URLs for main API. Per API Reference Base URLs, account-specific URLs use {prefix}.api.(sandbox.)checkout.com; see EnvironmentSubdomain when subdomain is set. */ +export const SANDBOX_BASE_URL = 'https://api.sandbox.checkout.com'; +export const LIVE_BASE_URL = 'https://api.checkout.com'; +export const SANDBOX_ACCESS_URL = 'https://access.sandbox.checkout.com/connect/token'; +export const LIVE_ACCESS_URL = 'https://access.checkout.com/connect/token'; + +export const PLATFORMS_FILES_LIVE_URL = 'https://files.checkout.com/files'; +export const PLATFORMS_FILES_SANDBOX_URL = 'https://files.sandbox.checkout.com/files'; + +export const TRANSFERS_SANDBOX_URL = 'https://transfers.sandbox.checkout.com/transfers'; +export const TRANSFERS_LIVE_URL = 'https://transfers.checkout.com/transfers'; + +// Forward host root (no trailing slash). The `forward` and `secrets` path +// segments are appended by the ForwardClient methods (matching every other SDK). +export const FORWARD_SANDBOX_URL = 'https://forward.sandbox.checkout.com'; +export const FORWARD_LIVE_URL = 'https://forward.checkout.com'; + +export const BALANCES_SANDBOX_URL = 'https://balances.sandbox.checkout.com/balances'; +export const BALANCES_LIVE_URL = 'https://balances.checkout.com/balances'; + +export const IDENTITY_VERIFICATION_SANDBOX_URL = 'https://identity-verification.sandbox.checkout.com'; +export const IDENTITY_VERIFICATION_LIVE_URL = 'https://identity-verification.checkout.com'; + +export const REQUEST_ID_HEADER = 'cko-request-id'; +export const API_VERSION_HEADER = 'cko-version'; +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*#$=]$/; +export const NAS_SANDBOX_PUBLIC_KEY_REGEX = /^pk_sbox_?[a-z2-7]{26}[a-z2-7*#$=]$/; +export const PAYMENT_TYPES = { + regular: 'Regular', + recurring: 'Recurring', + moto: 'MOTO', + installment: 'Installment', + unscheduled: 'Unscheduled', +}; +export const CURRENCIES = { + ALL: 'ALL', + STN: 'STN', + EEK: 'EEK', + BHD: 'BHD', + SCR: 'SCR', + DJF: 'DJF', + EGP: 'EGP', + MDL: 'MDL', + MZN: 'MZN', + BND: 'BND', + ZMK: 'ZMK', + SHP: 'SHP', + LBP: 'LBP', + AWG: 'AWG', + JMD: 'JMD', + KES: 'KES', + BYN: 'BYN', + KHR: 'KHR', + LAK: 'LAK', + MVR: 'MVR', + AOA: 'AOA', + TJS: 'TJS', + SVC: 'SVC', + GNF: 'GNF', + BRL: 'BRL', + MOP: 'MOP', + BOB: 'BOB', + CDF: 'CDF', + NAD: 'NAD', + LYD: 'LYD', + VUV: 'VUV', + QAR: 'QAR', + CLP: 'CLP', + HRK: 'HRK', + ISK: 'ISK', + FKP: 'FKP', + XCD: 'XCD', + NOK: 'NOK', + CUP: 'CUP', + VND: 'VND', + PEN: 'PEN', + KMF: 'KMF', + LVL: 'LVL', + MMK: 'MMK', + TRY: 'TRY', + VEF: 'VEF', + AUD: 'AUD', + TWD: 'TWD', + PKR: 'PKR', + SLL: 'SLL', + BGN: 'BGN', + LRD: 'LRD', + LKR: 'LKR', + XAF: 'XAF', + JOD: 'JOD', + ANG: 'ANG', + BSD: 'BSD', + CAD: 'CAD', + GIP: 'GIP', + MNT: 'MNT', + LTL: 'LTL', + BBD: 'BBD', + CLF: 'CLF', + BWP: 'BWP', + COP: 'COP', + PHP: 'PHP', + HUF: 'HUF', + FJD: 'FJD', + MWK: 'MWK', + THB: 'THB', + XPF: 'XPF', + RSD: 'RSD', + SAR: 'SAR', + UYU: 'UYU', + BZD: 'BZD', + SYP: 'SYP', + GMD: 'GMD', + SZL: 'SZL', + SBD: 'SBD', + ETB: 'ETB', + CHF: 'CHF', + MXN: 'MXN', + ARS: 'ARS', + GTQ: 'GTQ', + GHS: 'GHS', + NIO: 'NIO', + JPY: 'JPY', + BDT: 'BDT', + UZS: 'UZS', + SOS: 'SOS', + BTN: 'BTN', + NZD: 'NZD', + TZS: 'TZS', + IQD: 'IQD', + MGA: 'MGA', + DZD: 'DZD', + GYD: 'GYD', + USD: 'USD', + KWD: 'KWD', + CNY: 'CNY', + PYG: 'PYG', + SGD: 'SGD', + KZT: 'KZT', + PGK: 'PGK', + AMD: 'AMD', + GBP: 'GBP', + AFN: 'AFN', + CRC: 'CRC', + XOF: 'XOF', + YER: 'YER', + MRU: 'MRU', + DKK: 'DKK', + TOP: 'TOP', + INR: 'INR', + SDG: 'SDG', + DOP: 'DOP', + ZWL: 'ZWL', + UGX: 'UGX', + SEK: 'SEK', + LSL: 'LSL', + MYR: 'MYR', + TMT: 'TMT', + OMR: 'OMR', + BMD: 'BMD', + KRW: 'KRW', + HKD: 'HKD', + KGS: 'KGS', + BAM: 'BAM', + NGN: 'NGN', + ILS: 'ILS', + MUR: 'MUR', + RON: 'RON', + TND: 'TND', + AED: 'AED', + PAB: 'PAB', + NPR: 'NPR', + TTD: 'TTD', + RWF: 'RWF', + HTG: 'HTG', + IDR: 'IDR', + EUR: 'EUR', + KYD: 'KYD', + IRR: 'IRR', + KPW: 'KPW', + MKD: 'MKD', + SRD: 'SRD', + HNL: 'HNL', + AZN: 'AZN', + ERN: 'ERN', + CZK: 'CZK', + CVE: 'CVE', + BIF: 'BIF', + MAD: 'MAD', + RUB: 'RUB', + UAH: 'UAH', + WST: 'WST', + PLN: 'PLN', + ZAR: 'ZAR', + GEL: 'GEL', + ZMW: 'ZMW', +}; diff --git a/test/config/config.js b/test/config/config.js index a66486d5..cc1b969a 100644 --- a/test/config/config.js +++ b/test/config/config.js @@ -197,6 +197,13 @@ describe('NAS oAuth', () => { ).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 and the legacy domain opt-out', () => { const cko = new Checkout('2p7YQ37fHiRr8O6lQAikl8enICesB1dvAJrpmE2nZfEOpxzE-', { client: 'ack_vvzhoai466su3j3vbxb47ts5oe', 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/types/dist/Checkout.d.ts b/types/dist/Checkout.d.ts index 4a51824d..95867102 100644 --- a/types/dist/Checkout.d.ts +++ b/types/dist/Checkout.d.ts @@ -1,165 +1,165 @@ -//@ts-ignore -import * as http from 'http'; - -import { - Access, - AccountUpdater, - AgenticCommerce, - ApplePay, - Balances, - Baloto, - Boleto, - CardMetadata, - ComplianceRequests, - Customers, - Disputes, - Events, - Fawry, - Files, - Financial, - Forex, - Forward, - Giropay, - GooglePay, - HostedPayments, - Ideal, - Identities, - Instruments, - Issuing, - Klarna, - NetworkTokens, - OnboardingSimulator, - Oxxo, - PagoFacil, - PaymentContexts, - PaymentLinks, - PaymentMethods, - PaymentSessions, - PaymentSetups, - Payments, - Platforms, - Rapipago, - Reconciliation, - Reports, - Risk, - Sepa, - Sessions, - Sources, - Tokens, - Transfers, - Webhooks, - Workflows, -} from './index'; - -import Environment from './Environment'; -import EnvironmentSubdomain from './EnvironmentSubdomain'; - -export type access = { - token: string; - type: string; - scope: string; - expires: Date; -}; - -export type config = { - host: string; - sk?: string; - pk?: string; - secret?: string; - client?: string; - scope?: string | Array; - timeout: number; - agent?: http.Agent; - headers?: Record; - access?: access; - httpClient?: string; - subdomain?: string; - /** @deprecated emergency fallback only, see the README. Use `subdomain` instead. */ - useLegacyDomain?: boolean; - environment?: Environment; - environmentSubdomain?: EnvironmentSubdomain; -}; - -type options = { - host?: string; - timeout?: number; - 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 = { - pk?: string; -}; - -type oauthOptions = { - client: string; - scope?: string | Array; - environment?: string; -}; - -export default class Checkout { - payments: Payments; - sources: Sources; - tokens: Tokens; - instruments: Instruments; - webhooks: Webhooks; - events: Events; - disputes: Disputes; - files: Files; - reconciliation: Reconciliation; - customers: Customers; - hostedPayments: HostedPayments; - giropay: Giropay; - ideal: Ideal; - fawry: Fawry; - pagoFacil: PagoFacil; - rapipago: Rapipago; - boleto: Boleto; - baloto: Baloto; - oxxo: Oxxo; - klarna: Klarna; - sepa: Sepa; - paymentLinks: PaymentLinks; - access: Access; - forex: Forex; - applePay: ApplePay; - sessions: Sessions; - workflows: Workflows; - platforms: Platforms; - transfers: Transfers; - balances: Balances; - cardMetadata: CardMetadata; - reports: Reports; - financial: Financial; - issuing: Issuing; - paymentContexts: PaymentContexts; - paymentSessions: PaymentSessions; - paymentSetups: PaymentSetups; - forward: Forward; - paymentMethods: PaymentMethods; - networkTokens: NetworkTokens; - identities: Identities; - accountUpdater: AccountUpdater; - risk: Risk; - agenticCommerce: AgenticCommerce; - complianceRequests: ComplianceRequests; - googlePay: GooglePay; - onboardingSimulator: OnboardingSimulator; - config: config; - - constructor(key?: string, options?: options); -} +//@ts-ignore +import * as http from 'http'; + +import { + Access, + AccountUpdater, + AgenticCommerce, + ApplePay, + Balances, + Baloto, + Boleto, + CardMetadata, + ComplianceRequests, + Customers, + Disputes, + Events, + Fawry, + Files, + Financial, + Forex, + Forward, + Giropay, + GooglePay, + HostedPayments, + Ideal, + Identities, + Instruments, + Issuing, + Klarna, + NetworkTokens, + OnboardingSimulator, + Oxxo, + PagoFacil, + PaymentContexts, + PaymentLinks, + PaymentMethods, + PaymentSessions, + PaymentSetups, + Payments, + Platforms, + Rapipago, + Reconciliation, + Reports, + Risk, + Sepa, + Sessions, + Sources, + Tokens, + Transfers, + Webhooks, + Workflows, +} from './index'; + +import Environment from './Environment'; +import EnvironmentSubdomain from './EnvironmentSubdomain'; + +export type access = { + token: string; + type: string; + scope: string; + expires: Date; +}; + +export type config = { + host: string; + sk?: string; + pk?: string; + secret?: string; + client?: string; + scope?: string | Array; + timeout: number; + agent?: http.Agent; + headers?: Record; + access?: access; + httpClient?: string; + subdomain?: string; + /** @deprecated emergency fallback only, see the README. Use `subdomain` instead. */ + useLegacyDomain?: boolean; + environment?: Environment; + environmentSubdomain?: EnvironmentSubdomain; +}; + +type options = { + host?: string; + timeout?: number; + 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 = { + pk?: string; +}; + +type oauthOptions = { + client: string; + scope?: string | Array; + environment?: string; +}; + +export default class Checkout { + payments: Payments; + sources: Sources; + tokens: Tokens; + instruments: Instruments; + webhooks: Webhooks; + events: Events; + disputes: Disputes; + files: Files; + reconciliation: Reconciliation; + customers: Customers; + hostedPayments: HostedPayments; + giropay: Giropay; + ideal: Ideal; + fawry: Fawry; + pagoFacil: PagoFacil; + rapipago: Rapipago; + boleto: Boleto; + baloto: Baloto; + oxxo: Oxxo; + klarna: Klarna; + sepa: Sepa; + paymentLinks: PaymentLinks; + access: Access; + forex: Forex; + applePay: ApplePay; + sessions: Sessions; + workflows: Workflows; + platforms: Platforms; + transfers: Transfers; + balances: Balances; + cardMetadata: CardMetadata; + reports: Reports; + financial: Financial; + issuing: Issuing; + paymentContexts: PaymentContexts; + paymentSessions: PaymentSessions; + paymentSetups: PaymentSetups; + forward: Forward; + paymentMethods: PaymentMethods; + networkTokens: NetworkTokens; + identities: Identities; + accountUpdater: AccountUpdater; + risk: Risk; + agenticCommerce: AgenticCommerce; + complianceRequests: ComplianceRequests; + googlePay: GooglePay; + onboardingSimulator: OnboardingSimulator; + config: config; + + constructor(key?: string, options?: options); +} diff --git a/types/dist/EnvironmentSubdomain.d.ts b/types/dist/EnvironmentSubdomain.d.ts index 12008d6b..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. - * Prepends the subdomain to the host when it matches the required pattern. - * 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;