diff --git a/src/Webhooks.ts b/src/Webhooks.ts index 038c205d29..fc3dec8907 100644 --- a/src/Webhooks.ts +++ b/src/Webhooks.ts @@ -65,6 +65,9 @@ export type WebhookObject = { receivedAt: number ) => Promise; generateTestHeaderString: (opts: WebhookTestHeaderOptions) => string; + generateTestHeaderStringAsync: ( + opts: WebhookTestHeaderOptions + ) => Promise; }; export function createWebhooks( @@ -142,31 +145,30 @@ export function createWebhooks( * @property {CryptoProvider} cryptoProvider - Crypto provider to use for computing the signature if none was provided. Defaults to NodeCryptoProvider. */ generateTestHeaderString: function(opts: WebhookTestHeaderOptions): string { - if (!opts) { - throw new StripeError({ - message: 'Options are required', - }); - } - - opts.timestamp = - Math.floor(opts.timestamp) || Math.floor(Date.now() / 1000); - opts.scheme = opts.scheme || signature.EXPECTED_SCHEME; - - opts.cryptoProvider = opts.cryptoProvider || getCryptoProvider(); + const preparedOpts = prepareOptions(opts); - opts.signature = - opts.signature || - opts.cryptoProvider.computeHMACSignature( - opts.timestamp + '.' + opts.payload, - opts.secret + const signature = + preparedOpts.signature || + preparedOpts.cryptoProvider.computeHMACSignature( + preparedOpts.payloadString, + preparedOpts.secret ); - const generatedHeader = [ - 't=' + opts.timestamp, - opts.scheme + '=' + opts.signature, - ].join(','); - - return generatedHeader; + return preparedOpts.generateHeaderString(signature); + }, + generateTestHeaderStringAsync: async function( + opts: WebhookTestHeaderOptions + ) { + const preparedOpts = prepareOptions(opts); + + const signature = + preparedOpts.signature || + (await preparedOpts.cryptoProvider.computeHMACSignatureAsync( + preparedOpts.payloadString, + preparedOpts.secret + )); + + return preparedOpts.generateHeaderString(signature); }, }; @@ -443,6 +445,38 @@ export function createWebhooks( return webhooksCryptoProviderInstance!; } + function prepareOptions( + opts: WebhookTestHeaderOptions + ): WebhookTestHeaderOptions & { + payloadString: string; + generateHeaderString: (signature: string) => string; + } { + if (!opts) { + throw new StripeError({ + message: 'Options are required', + }); + } + + const timestamp = + Math.floor(opts.timestamp) || Math.floor(Date.now() / 1000); + const scheme = opts.scheme || signature.EXPECTED_SCHEME; + const cryptoProvider = opts.cryptoProvider || getCryptoProvider(); + const payloadString = `${timestamp}.${opts.payload}`; + + const generateHeaderString = (signature: string): string => { + return `t=${timestamp},${scheme}=${signature}`; + }; + + return { + ...opts, + timestamp, + scheme, + cryptoProvider, + payloadString, + generateHeaderString, + }; + } + Webhook.signature = signature; return Webhook; diff --git a/test/Webhook.spec.ts b/test/Webhook.spec.ts index 705d89aed6..deae6648de 100644 --- a/test/Webhook.spec.ts +++ b/test/Webhook.spec.ts @@ -41,6 +41,30 @@ function createWebhooksTestSuite(stripe) { }); }); + describe('.generateTestHeaderStringAsync', () => { + it('should throw when no opts are passed', async () => { + await expect( + stripe.webhooks.generateTestHeaderStringAsync() + ).to.be.rejectedWith('Options are required'); + }); + + it('should correctly construct a webhook header', async () => { + const header = await stripe.webhooks.generateTestHeaderStringAsync({ + payload: EVENT_PAYLOAD_STRING, + secret: SECRET, + }); + + expect(header).to.not.be.undefined; + expect(header.split(',')).to.have.lengthOf(2); + expect(header).to.equal( + stripe.webhooks.generateTestHeaderString({ + payload: EVENT_PAYLOAD_STRING, + secret: SECRET, + }) + ); + }); + }); + const makeConstructEventTests = ( constructEventFn: typeof stripe.webhooks.construct ) => {