amazon-payments
Version:
API wrapper for Amazon Payments
146 lines (118 loc) • 4.53 kB
JavaScript
;
const constants = {
AGENT: 'madisonreed-amazon-payments',
RETRIES: 3,
AMAZON_SIGNATURE_ALGORITHM: 'AMZN-PAY-RSASSA-PSS',
VERSION: 2,
};
const crypto = require('crypto');
const request = require('request');
const urlLib = require('url');
module.exports = {
signHeaders: signHeaders,
retryLogic: retryLogic,
sendRequest: sendRequest,
invokeApi: invokeApi,
prepareOptions: prepareOptions
}
function getTimestamp() {
const date = new Date();
return date.toISOString().replace(/[:-]/g,"").replace(/\..+/g,"Z");
}
function invokeApi(environment, apiOptions) {
const options = {
method: apiOptions.method,
json: false,
headers: apiOptions.headers,
url: environment.apiEndpoint,
body: apiOptions.payload
};
const count = 1;
const response = this.retryLogic(options, count);
return response;
}
function prepareOptions(options) {
options.headers = options.headers || {};
// if user doesn't pass in a string, assume it's a JS object and convert it to a JSON string
if (!(typeof options.payload === 'string' || options.payload instanceof String)) {
options.payload = JSON.stringify(options.payload);
}
return options;
}
function sign(privateKey, stringToSign) {
const sign = crypto.createSign('RSA-SHA256').update(stringToSign);
return sign.sign({
key: privateKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: 20
}, 'base64');
}
function retryLogic(options, count) {
const response = this.sendRequest(options, count);
if (count > constants.RETRIES) {
return response.then(function (result) {
return result;
}).catch(err => {
return err;
});
}
return response.then(function (result) {
return result;
}).catch(err => {
if ((response.statusCode == 429) || (response.statusCode >= 500 && response.statusCode < 600)) {
return this.retryLogic(options, count += 1);
} else {
return err;
}
})
}
function sendRequest(options, count) {
const delayTime = count === 1 ? 0 : (2 ** (count - 1)) * 1000;
return new Promise(function (resolve, reject) {
setTimeout(function () {
request(options, function (err, response, body) {
if (err) {
reject(err);
} else if (response.statusCode >= 400 && response.statusCode < 600) {
reject(response);
} else {
resolve(response);
}
});
}, delayTime);
});
}
/* Expected options:
options.method
options.payload
options.headers
*/
function signHeaders(config, environment, options) {
const headers = {};
Object.assign(headers, options.headers);
// headers['x-amz-pay-region'] = config.region;
headers['x-amz-pay-host'] = urlLib.parse(environment.apiEndpoint).hostname;
headers['x-amz-pay-date'] = getTimestamp();
headers['content-type'] = 'application/json';
headers['accept'] = 'application/json';
headers['user-agent'] = `${constants.AGENT}/${constants.VERSION} (JS/${process.versions.node}; ${process.platform})`;
const lowercaseSortedHeaderKeys = Object.keys(headers).sort(function (a, b) {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
const signedHeaders = lowercaseSortedHeaderKeys.join(';');
let payload = options.payload;
if (payload === null || payload === undefined || environment.apiEndpoint.includes('/account-management/v1/accounts')) {
payload = ''; // do not sign payload for payment critical data APIs
}
let canonicalRequest = options.method + '\n' + urlLib.parse(environment.apiEndpoint).pathname + '\n\n';
lowercaseSortedHeaderKeys.forEach(item => canonicalRequest += item.toLowerCase() + ':' + headers[item] + '\n');
canonicalRequest += '\n' + signedHeaders + '\n' + crypto.createHash('SHA256').update(payload).digest('hex');
const stringToSign = constants.AMAZON_SIGNATURE_ALGORITHM + '\n' +
crypto.createHash('SHA256').update(canonicalRequest).digest('hex');
const signature = sign(config.privateKey, stringToSign);
headers['authorization'] = constants.AMAZON_SIGNATURE_ALGORITHM
+ ' PublicKeyId=' + config.publicKeyId
+ ', SignedHeaders=' + signedHeaders
+ ', Signature=' + signature;
return headers;
}