Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions generator/cybersource_node_sdk_gen.bat
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ git checkout ..\src\model\CreateAccessTokenRequest.js
git checkout ..\src\model\BadRequestError.js
git checkout ..\src\model\ResourceNotFoundError.js
git checkout ..\src\model\UnauthorizedClientError.js
git checkout ..\src\api\BatchUploadWithMTLSApi.js

git checkout ..\docs\OAuthApi.md
git checkout ..\docs\AccessTokenResponse.md
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"jwt-simple": "^0.5.6",
"memory-cache": "^0.2.0",
"node-forge": ">=1.0.0",
"openpgp": "^5.11.3",
"promise": "^8.3.0",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1",
Expand Down
123 changes: 123 additions & 0 deletions src/api/BatchUploadWithMTLSApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
const path = require('path');
const fs = require('fs');
const { handlePGPEncrypt } = require('../utilities/PGP/BatchUpload/PgpEncryptionUtility');
const {
handleUploadOperationUsingP12orPfx,
handleUploadOperationUsingPrivateKeyAndCerts
} = require('../utilities/PGP/BatchUpload/MutualAuthUploadUtility');
const BatchUploadUtility = require('../utilities/PGP/BatchUpload/BatchUploadUtility');

/**
* BatchUploadWithMTLSApi
* Class for uploading batch files to CyberSource using mutual TLS authentication.
* Supports PKCS#12 client certificates, and direct private key/certificate paths.
* Handles PGP encryption of files before upload.
*/
class BatchUploadWithMTLSApi {
constructor(logger = console) {
this.logger = logger;
}

uploadBatchAPIWithP12(opts, callback) {
try {
const {
inputFilePath,
environmentHostname,
publicKeyFilePath,
clientCertP12FilePath,
clientCertP12Password,
serverTrustCertPath,
rejectUnauthorizedFlag = true
} = opts;
if (rejectUnauthorizedFlag === false) {
this.logger.warn('rejectUnauthorized is set to false. SSL verification is DISABLED. This setting is NOT SAFE for production and should NOT be used in production environments!');
}
this.logger.info('Starting batch upload with p12/pfx for given file');
const endpoint = '/pts/v1/transaction-batch-upload';
const endpointUrl = BatchUploadUtility.getEndpointUrl(environmentHostname, endpoint);
BatchUploadUtility.validateBatchApiP12Inputs(
inputFilePath, environmentHostname, publicKeyFilePath, clientCertP12FilePath, serverTrustCertPath
);

handlePGPEncrypt(inputFilePath, publicKeyFilePath)
.then(encryptedBuffer => {
const uploadFileName = path.basename(inputFilePath) + '.pgp';
console.log('Encrypted file name:', uploadFileName);
const clientCertP12 = fs.readFileSync(clientCertP12FilePath);
const serverTrustCert = serverTrustCertPath ? fs.readFileSync(serverTrustCertPath) : undefined;
return handleUploadOperationUsingP12orPfx(
encryptedBuffer,
endpointUrl,
environmentHostname,
uploadFileName,
clientCertP12,
clientCertP12Password,
serverTrustCert,
rejectUnauthorizedFlag
);
})
.then(result => callback(null, result))
.catch(error => {
this.logger.error(error);
callback(error);
});
} catch (e) {
this.logger.error('Exception in Batch Upload API', e);
callback(e);
}
}

uploadBatchAPIWithKeys(opts, callback) {
try {
const {
inputFilePath,
environmentHostname,
publicKeyFilePath,
clientPrivateKeyFilePath,
clientCertFilePath,
serverTrustCertPath,
clientKeyPassword,
rejectUnauthorizedFlag = true

} = opts;
if (rejectUnauthorizedFlag === false) {
this.logger.warn('rejectUnauthorized is set to false. SSL verification is DISABLED. This setting is NOT SAFE for production and should NOT be used in production environments!');
}
this.logger.info('Starting batch upload with client private key and certs for given file');
const endpoint = '/pts/v1/transaction-batch-upload';
const endpointUrl = BatchUploadUtility.getEndpointUrl(environmentHostname, endpoint);
BatchUploadUtility.validateBatchApiKeysInputs(
inputFilePath, environmentHostname, publicKeyFilePath, clientPrivateKeyFilePath, clientCertFilePath, serverTrustCertPath
);

handlePGPEncrypt(inputFilePath, publicKeyFilePath)
.then(encryptedBuffer => {
const uploadFileName = path.basename(inputFilePath) + '.pgp';
const clientPrivateKey = fs.readFileSync(clientPrivateKeyFilePath);
const clientCert = fs.readFileSync(clientCertFilePath);
const serverTrustCert = serverTrustCertPath ? fs.readFileSync(serverTrustCertPath) : undefined;
return handleUploadOperationUsingPrivateKeyAndCerts(
encryptedBuffer,
endpointUrl,
environmentHostname,
uploadFileName,
clientPrivateKey,
clientCert,
serverTrustCert,
clientKeyPassword,
rejectUnauthorizedFlag
);
})
.then(result => callback(null, result))
.catch(error => {
this.logger.error(error);
callback(error);
});
} catch (e) {
this.logger.error('Exception in Batch Upload API', e);
callback(e);
}
}
}

module.exports = BatchUploadWithMTLSApi;
118 changes: 118 additions & 0 deletions src/utilities/PGP/BatchUpload/BatchUploadUtility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const fs = require('fs');
const path = require('path');

const MAX_FILE_SIZE_BYTES = 75 * 1024 * 1024; // 75MB

class BatchUploadUtility {
/**
* Constructs the full endpoint URL for the given environment hostname and endpoint path.
* @param {string} environmentHostname - The environment hostname (with or without protocol prefix).
* @param {string} endpoint - The endpoint path to append.
* @returns {string} The full endpoint URL.
*/
static getEndpointUrl(environmentHostname, endpoint) {
const URL_PREFIX = 'https://';
let baseUrl;
if (environmentHostname.trim().toLowerCase().startsWith(URL_PREFIX)) {
baseUrl = environmentHostname.trim();
} else {
baseUrl = URL_PREFIX + environmentHostname.trim();
}
return baseUrl + endpoint;
}

/**
* Validates the input parameters for batch API using P12 client certificate.
* @param {string} inputFile - Path to the input CSV file for batch upload.
* @param {string} environmentHostname
* @param {string} pgpEncryptionCertPath
* @param {string} clientCertP12FilePath
* @param {string} serverTrustCertPath
*/
static validateBatchApiP12Inputs(inputFile, environmentHostname, pgpEncryptionCertPath, clientCertP12FilePath, serverTrustCertPath) {
this.validateInputFile(inputFile);
if (!environmentHostname || !environmentHostname.trim()) {
throw new Error('Environment Host Name for Batch Upload API cannot be null or empty.');
}
this.validatePathAndFile(pgpEncryptionCertPath, 'PGP Encryption Cert Path');
this.validatePathAndFile(clientCertP12FilePath, 'Client Cert P12 File Path');
// serverTrustCertPath is optional, but if provided, validate
if (serverTrustCertPath && serverTrustCertPath.trim()) {
this.validatePathAndFile(serverTrustCertPath, 'Server Trust Cert Path');
}
}

/**
* Validates the input parameters for batch API using direct key and certificate file paths.
* @param {string} inputFile - Path to the input CSV file for batch upload.
* @param {string} environmentHostname
* @param {string} pgpPublicKeyPath - Path to the PGP public key file (.asc).
* @param {string} clientPrivateKeyPath - Path to the client private key file (PEM).
* @param {string} clientCertPath - Path to the client X509 certificate file (PEM).
* @param {string} serverTrustCertPath - Path to the server trust X509 certificate file (PEM, optional).
*/
static validateBatchApiKeysInputs(inputFile, environmentHostname, pgpPublicKeyPath, clientPrivateKeyPath, clientCertPath, serverTrustCertPath) {
this.validateInputFile(inputFile);
if (!environmentHostname || !environmentHostname.trim()) {
throw new Error('Environment Host Name for Batch Upload API cannot be null or empty.');
}
this.validatePathAndFile(pgpPublicKeyPath, 'PGP Public Key Path');
this.validatePathAndFile(clientPrivateKeyPath, 'Client Private Key Path');
this.validatePathAndFile(clientCertPath, 'Client Certificate Path');
// serverTrustCertPath is optional, but if provided, validate
if (serverTrustCertPath && serverTrustCertPath.trim()) {
this.validatePathAndFile(serverTrustCertPath, 'Server Trust Certificate Path');
}
}

/**
* Validates the input file for batch upload.
* Checks for existence, file type (CSV), and maximum file size (75MB).
* @param {string} inputFile - Path to the input file to validate.
*/
static validateInputFile(inputFile) {
if (!inputFile || !fs.existsSync(inputFile) || !fs.statSync(inputFile).isFile()) {
throw new Error(`Input file is invalid or does not exist: ${inputFile}`);
}
// Only CSV files are allowed for batch API
if (!inputFile.toLowerCase().endsWith('.csv')) {
throw new Error(`Only CSV file type is allowed: ${path.basename(inputFile)}`);
}
// Max file size allowed is 75MB
const fileSize = fs.statSync(inputFile).size;
if (fileSize > MAX_FILE_SIZE_BYTES) {
throw new Error(`Input file size exceeds the maximum allowed size of 75MB: ${fileSize}`);
}
}

/**
* Validates that the given file path exists and is not empty.
* @param {string} filePath - The file path to validate.
* @param {string} pathType - A description of the path type (e.g., "Input file").
*/
static validatePathAndFile(filePath, pathType) {
if (!filePath || !filePath.trim()) {
throw new Error(`${pathType} path cannot be null or empty`);
}

// Normalize Windows-style paths that start with a slash before the drive letter
let normalizedPath = filePath;
if (path.sep === '\\' && normalizedPath.match(/^\/[A-Za-z]:.*/)) {
normalizedPath = normalizedPath.substring(1);
}

if (!fs.existsSync(normalizedPath)) {
throw new Error(`${pathType} does not exist: ${normalizedPath}`);
}
if (!fs.statSync(normalizedPath).isFile()) {
throw new Error(`${pathType} does not have valid file: ${normalizedPath}`);
}
try {
fs.accessSync(normalizedPath, fs.constants.R_OK);
} catch (err) {
throw new Error(`${pathType} is not readable: ${normalizedPath}`);
}
}
}

module.exports = BatchUploadUtility;
Loading