Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .c8rc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"all": true,
"include": [
"src/**/*.js"
],
"exclude": [
"**/*.spec.js",
"**/*.test.js",
"test/**",
"dist/**",
"coverage/**",
"node_modules/**"
],
"reporter": [
"html",
"text",
"json"
],
"report-dir": "./coverage",
"skip-full": false
}
1,951 changes: 608 additions & 1,343 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 19 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
"scripts": {
"start": "nodemon --exec babel-node ./src/index.js",
"lint": "eslint --fix --ext .js src/",
"test": "nyc --reporter=html mocha --timeout 300000 'test/**/*.js'",
"posttest": "nyc report --reporter=json",
"test": "c8 --all --src=src mocha --timeout 300000 'test/**/*.js'",
"posttest": "c8 report --reporter=json",
"test:watch": "mocha --timeout 300000 'test/**/*.js' --watch",
"build": "babel src --out-dir ./dist --source-maps"
"build": "babel src --out-dir ./dist --source-maps --no-babelrc --presets=@babel/preset-env --env-name=production"
},
"dependencies": {
"axios": "^1.12.2",
Expand All @@ -26,14 +26,14 @@
"@babel/node": "^7.28.0",
"@babel/preset-env": "^7.28.3",
"@types/mocha": "^10.0.10",
"c8": "^10.1.3",
"chai": "^6.0.1",
"eslint": "^9.35.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"mocha": "^11.7.2",
"nock": "^14.0.10",
"nodemon": "^3.1.10",
"nyc": "^17.1.0",
"uuid": "^13.0.0"
},
"babel": {
Expand All @@ -47,7 +47,21 @@
}
}
]
]
],
"env": {
"test": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
}
},
"author": "Ioan Ghisoi",
"license": "MIT"
Expand Down
121 changes: 63 additions & 58 deletions src/Checkout.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,60 @@
import * as CONFIG from './config.js';
import * as ENDPOINTS from './index.js';
import Environment from './Environment.js';
import EnvironmentSubdomain from './EnvironmentSubdomain.js';

/**
* Determine the full URL based on the environment and subdomain.
*
* @param {string} environment
* @param {object} options
* @returns {string}
*/
const determineUrl = (environment, options) => {
const apiUrl = new URL(environment);

if (options && options.subdomain) {
const { subdomain } = options;
if (typeof subdomain === 'string' && /^[0-9a-z]+$/.test(subdomain)) {
const { protocol, port, hostname } = apiUrl;
return new URL(`${protocol}//${subdomain}.${hostname}${port ? `:${port}` : ''}`)
.toString()
.slice(0, -1);
}
}

return apiUrl.toString().slice(0, -1);
};

const determineHost = (key, options) => {
const setupConfig = (key, options) => {
// If specified, use the custom host
if (options && options.host) {
return options.host;
// For custom hosts, we still need to determine environment from the host
const isLive = !options.host.includes('sandbox');
const environment = isLive ? Environment.live() : Environment.sandbox();
const environmentSubdomain = (options && options.subdomain && EnvironmentSubdomain.isValidSubdomain(options.subdomain))
? new EnvironmentSubdomain(environment, options.subdomain)
: null;

return {
host: options.host,
environment,
environmentSubdomain
};
}

// Determine environment first
let isLive = false;

// Priority 1: oAuth environment vars
if (process.env.CKO_SECRET) {
if (
(process.env.CKO_ENVIRONMENT &&
process.env.CKO_ENVIRONMENT.toLowerCase().trim() === 'prod') ||
(process.env.CKO_ENVIRONMENT &&
process.env.CKO_ENVIRONMENT.toLowerCase().trim() === 'production') ||
(process.env.CKO_ENVIRONMENT &&
process.env.CKO_ENVIRONMENT.toLowerCase().trim() === 'live')
) {
return determineUrl(CONFIG.LIVE_BASE_URL, options);
}
return determineUrl(CONFIG.SANDBOX_BASE_URL, options);
isLive = (process.env.CKO_ENVIRONMENT &&
['prod', 'production', 'live'].includes(process.env.CKO_ENVIRONMENT.toLowerCase().trim()));
}
// Priority 2: oAuth declared vars
if (options && options.client) {
if (
(options.environment && options.environment.toLowerCase().trim() === 'prod') ||
(options.environment && options.environment.toLowerCase().trim() === 'production') ||
(options.environment && options.environment.toLowerCase().trim() === 'live')
) {
return determineUrl(CONFIG.LIVE_BASE_URL, options);
}
return determineUrl(CONFIG.SANDBOX_BASE_URL, options);
// Priority 2: oAuth declared vars
else if (options && options.client) {
isLive = (options.environment &&
['prod', 'production', 'live'].includes(options.environment.toLowerCase().trim()));
}

// Priority 3: MBC or NAS static keys
if (key.startsWith('Bearer')) {
key = key.replace('Bearer', '').trim();
else {
const cleanKey = key.startsWith('Bearer') ? key.replace('Bearer', '').trim() : key;
isLive = CONFIG.MBC_LIVE_SECRET_KEY_REGEX.test(cleanKey) || CONFIG.NAS_LIVE_SECRET_KEY_REGEX.test(cleanKey);
}
return CONFIG.MBC_LIVE_SECRET_KEY_REGEX.test(key) || CONFIG.NAS_LIVE_SECRET_KEY_REGEX.test(key)
? determineUrl(CONFIG.LIVE_BASE_URL, options)
: determineUrl(CONFIG.SANDBOX_BASE_URL, options);

// Create appropriate environment
const environment = isLive ? Environment.live() : Environment.sandbox();

// Create EnvironmentSubdomain if subdomain provided, otherwise null
const environmentSubdomain = (options && options.subdomain && EnvironmentSubdomain.isValidSubdomain(options.subdomain))
? new EnvironmentSubdomain(environment, options.subdomain)
: null;

// Determine host URL using the appropriate environment/environmentSubdomain
const host = environmentSubdomain ? environmentSubdomain.getCheckoutApi() : environment.getCheckoutApi();

return {
host,
environment,
environmentSubdomain
};
};

const determineSecretKey = (key) => {
Expand Down Expand Up @@ -116,39 +107,51 @@ export default class Checkout {
let auth;
if (process.env.CKO_SECRET) {
// For NAS with environment vars
const { host, environment, environmentSubdomain } = setupConfig(null, options);
auth = {
secret: process.env.CKO_SECRET,
client: process.env.CKO_CLIENT,
scope: process.env.CKO_SCOPE || 'gateway',
host: determineHost(null, options),
host,
environment,
environmentSubdomain,
access: null,
};
} else if (process.env.CKO_SECRET_KEY) {
// For MBC or NAS with static keys from environment vars
const { host, environment, environmentSubdomain } = setupConfig(determineSecretKey(key), options);
auth = {
sk: determineSecretKey(process.env.CKO_SECRET_KEY),
pk: determinePublicKey(process.env.CKO_PUBLIC_KEY),
host: determineHost(determineSecretKey(key), options),
host,
environment,
environmentSubdomain,
};
} else if (options && options.client) {
// For NAS with declared vars
const { host, environment, environmentSubdomain } = setupConfig(null, options);
auth = {
secret: key,
pk: determinePublicKey(options),
client: options.client,
scope: options.scope || 'gateway',
host: determineHost(null, options),
host,
environment,
environmentSubdomain,
access: null,
};
} else {
// For MBC or NAS with static keys with declared vars
const { host, environment, environmentSubdomain } = setupConfig(determineSecretKey(key), options);
auth = {
sk: determineSecretKey(key),
pk: determinePublicKey(options),
host: determineHost(determineSecretKey(key), options),
host,
environment,
environmentSubdomain,
};
}

this.config = {
...auth,
timeout: options && options.timeout ? options.timeout : CONFIG.DEFAULT_TIMEOUT,
Expand All @@ -159,6 +162,8 @@ export default class Checkout {
subdomain: options && options.subdomain ? options.subdomain : undefined,
};



this.payments = new ENDPOINTS.Payments(this.config);
this.sources = new ENDPOINTS.Sources(this.config);
this.tokens = new ENDPOINTS.Tokens(this.config);
Expand Down
40 changes: 40 additions & 0 deletions src/Environment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Environment class - handles basic environment URLs
* Similar to Java SDK Environment class
* Provides the base URLs for different environments without subdomain logic
*/

const ENVIRONMENTS = {
SANDBOX: {
checkoutApi: 'https://api.sandbox.checkout.com',
oAuthAuthorizationApi: 'https://access.sandbox.checkout.com/connect/token'
},
LIVE: {
checkoutApi: 'https://api.checkout.com',
oAuthAuthorizationApi: 'https://access.checkout.com/connect/token'
}
};

export default class Environment {
constructor(environment) {
this.environment = environment;
this.checkoutApi = ENVIRONMENTS[environment].checkoutApi;
this.oAuthAuthorizationApi = ENVIRONMENTS[environment].oAuthAuthorizationApi;
}

getCheckoutApi() {
return this.checkoutApi;
}

getOAuthAuthorizationApi() {
return this.oAuthAuthorizationApi;
}

static sandbox() {
return new Environment('SANDBOX');
}

static live() {
return new Environment('LIVE');
}
}
76 changes: 76 additions & 0 deletions src/EnvironmentSubdomain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* EnvironmentSubdomain class - handles environment URLs with subdomain support
* Similar to Java SDK EnvironmentSubdomain class
* Takes an Environment and applies subdomain transformation
*/

import Environment from './Environment.js';

export default class EnvironmentSubdomain {
constructor(environment, subdomain) {
this.environment = environment;
this.subdomain = subdomain;

// Apply subdomain transformation to both URLs
this.checkoutApi = EnvironmentSubdomain.createUrlWithSubdomain(environment.getCheckoutApi(), subdomain);
this.oAuthAuthorizationApi = EnvironmentSubdomain.createUrlWithSubdomain(environment.getOAuthAuthorizationApi(), subdomain);
}

getCheckoutApi() {
return this.checkoutApi;
}

getOAuthAuthorizationApi() {
return this.oAuthAuthorizationApi;
}

/**
* 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.
*
* @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
*/
static createUrlWithSubdomain(originalUrl, subdomain) {
if (!EnvironmentSubdomain.isValidSubdomain(subdomain)) {
return originalUrl;
}

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 (error) {
return originalUrl;
}
}

/**
* Validates if a subdomain string follows the required pattern.
* Must be alphanumeric (lowercase letters and numbers only).
*
* @param {string} subdomain - the subdomain to validate
* @return {boolean} true if valid, false otherwise
*/
static isValidSubdomain(subdomain) {
if (!subdomain || typeof subdomain !== 'string') {
return false;
}
const pattern = /^[0-9a-z]+$/;
return pattern.test(subdomain);
}

// Factory methods for easy creation
static sandbox(subdomain) {
return new EnvironmentSubdomain(Environment.sandbox(), subdomain);
}

static live(subdomain) {
return new EnvironmentSubdomain(Environment.live(), subdomain);
}
}
Loading