1- const crypto = require ( "node:crypto" ) ;
2-
31const ApiError = require ( "./error" ) ;
42
53/**
@@ -72,12 +70,10 @@ async function validateWebhook(requestData, secret) {
7270
7371 const signedContent = `${ id } .${ timestamp } .${ body } ` ;
7472
75- const secretBytes = Buffer . from ( signingSecret . split ( "_" ) [ 1 ] , "base64" ) ;
76-
77- const computedSignature = crypto
78- . createHmac ( "sha256" , secretBytes )
79- . update ( signedContent )
80- . digest ( "base64" ) ;
73+ const computedSignature = await createHMACSHA256 (
74+ signingSecret . split ( "_" ) . pop ( ) ,
75+ signedContent
76+ ) ;
8177
8278 const expectedSignatures = signature
8379 . split ( " " )
@@ -88,6 +84,62 @@ async function validateWebhook(requestData, secret) {
8884 ) ;
8985}
9086
87+ /**
88+ * @param {string } secret - base64 encoded string
89+ * @param {string } data - text body of request
90+ */
91+ async function createHMACSHA256 ( secret , data ) {
92+ const encoder = new TextEncoder ( ) ;
93+ let crypto = globalThis . crypto ;
94+
95+ // In Node 18 the `crypto` global is behind a --no-experimental-global-webcrypto flag
96+ if ( typeof crypto === "undefined" && typeof require === "function" ) {
97+ crypto = require ( "node:crypto" ) . webcrypto ;
98+ }
99+
100+ const key = await crypto . subtle . importKey (
101+ "raw" ,
102+ base64ToBytes ( secret ) ,
103+ { name : "HMAC" , hash : "SHA-256" } ,
104+ false ,
105+ [ "sign" ]
106+ ) ;
107+
108+ const signature = await crypto . subtle . sign ( "HMAC" , key , encoder . encode ( data ) ) ;
109+ return bytesToBase64 ( signature ) ;
110+ }
111+
112+ /**
113+ * Convert a base64 encoded string into bytes.
114+ *
115+ * @param {string } the base64 encoded string
116+ * @return {Uint8Array }
117+ *
118+ * Two functions for encoding/decoding base64 strings using web standards. Not
119+ * intended to be used to encode/decode arbitrary string data.
120+ * See: https://developer.mozilla.org/en-US/docs/Glossary/Base64#javascript_support
121+ * See: https://stackoverflow.com/a/31621532
122+ *
123+ * Performance might take a hit because of the conversion to string and then to binary,
124+ * if this is the case we might want to look at an alternative solution.
125+ * See: https://jsben.ch/wnaZC
126+ */
127+ function base64ToBytes ( base64 ) {
128+ return Uint8Array . from ( atob ( base64 ) , ( m ) => m . codePointAt ( 0 ) ) ;
129+ }
130+
131+ /**
132+ * Convert a base64 encoded string into bytes.
133+ *
134+ * See {@link base64ToBytes} for caveats.
135+ *
136+ * @param {Uint8Array | ArrayBuffer } the base64 encoded string
137+ * @return {string }
138+ */
139+ function bytesToBase64 ( bytes ) {
140+ return btoa ( String . fromCharCode . apply ( null , new Uint8Array ( bytes ) ) ) ;
141+ }
142+
91143/**
92144 * Automatically retry a request if it fails with an appropriate status code.
93145 *
0 commit comments