monobank
Version:
Monobank API wrapper
164 lines (134 loc) • 4.85 kB
JavaScript
const Buffer = require('safe-buffer').Buffer;
const crypto = require('crypto');
const utils = require('./utils');
const Error = require('./Error');
const Webhook = {
DEFAULT_TOLERANCE: 300, // 5 minutes
constructEvent(payload, header, secret, tolerance) {
this.signature.verifyHeader(
payload,
header,
secret,
tolerance || Webhook.DEFAULT_TOLERANCE
);
const jsonPayload = JSON.parse(payload);
return jsonPayload;
},
/**
* Generates a header to be used for webhook mocking
*
* @typedef {object} opts
* @property {number} timestamp - Timestamp of the header. Defaults to Date.now()
* @property {string} payload - JSON stringified payload object, containing the 'id' and 'object' parameters
* @property {string} secret - Stripe webhook secret 'whsec_...'
* @property {string} scheme - Version of API to hit. Defaults to 'v1'.
* @property {string} signature - Computed webhook signature
*/
generateTestHeaderString: function(opts) {
if (!opts) {
throw new Error.StripeError({
message: 'Options are required',
});
}
opts.timestamp =
Math.floor(opts.timestamp) || Math.floor(Date.now() / 1000);
opts.scheme = opts.scheme || signature.EXPECTED_SCHEME;
opts.signature =
opts.signature ||
signature._computeSignature(
opts.timestamp + '.' + opts.payload,
opts.secret
);
var generatedHeader = [
't=' + opts.timestamp,
opts.scheme + '=' + opts.signature,
].join(',');
return generatedHeader;
},
};
const signature = {
EXPECTED_SCHEME: 'v1',
_computeSignature: (payload, secret) => {
return crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
},
verifyHeader(payload, header, secret, tolerance) {
payload = Buffer.isBuffer(payload) ? payload.toString('utf8') : payload;
header = Buffer.isBuffer(header) ? header.toString('utf8') : header;
const details = parseHeader(header, this.EXPECTED_SCHEME);
if (!details || details.timestamp === -1) {
throw new Error.StripeSignatureVerificationError({
message: 'Unable to extract timestamp and signatures from header',
detail: {
header,
payload,
},
});
}
if (!details.signatures.length) {
throw new Error.StripeSignatureVerificationError({
message: 'No signatures found with expected scheme',
detail: {
header,
payload,
},
});
}
const expectedSignature = this._computeSignature(
`${details.timestamp}.${payload}`,
secret
);
const signatureFound = !!details.signatures.filter(
utils.secureCompare.bind(utils, expectedSignature)
).length;
if (!signatureFound) {
throw new Error.StripeSignatureVerificationError({
message:
'No signatures found matching the expected signature for payload.' +
' Are you passing the raw request body you received from Stripe?' +
' https://github.com/stripe/stripe-node#webhook-signing',
detail: {
header,
payload,
},
});
}
const timestampAge = Math.floor(Date.now() / 1000) - details.timestamp;
if (tolerance > 0 && timestampAge > tolerance) {
throw new Error.StripeSignatureVerificationError({
message: 'Timestamp outside the tolerance zone',
detail: {
header,
payload,
},
});
}
return true;
},
};
function parseHeader(header, scheme) {
if (typeof header !== 'string') {
return null;
}
return header.split(',').reduce(
(accum, item) => {
const kv = item.split('=');
if (kv[0] === 't') {
accum.timestamp = kv[1];
}
if (kv[0] === scheme) {
accum.signatures.push(kv[1]);
}
return accum;
},
{
timestamp: -1,
signatures: [],
}
);
}
Webhook.signature = signature;
module.exports = Webhook;
;