I understand all other parts of this code, but the request.rawBody part is where I’m stuck.
As I understand it, I don’t need to (and likely can’t) use request library, since Pipedream already received the payload in the initial step within my workflow and returned a JSON object.
I’ve tried JSON.stringify(steps.trigger.event.body), but that doesn’t seem to work. The hash doesn’t match the one sent in the x-signature of the header.
Is there another way I can hash the raw body of the payload in a manner identical to how Lemon Squeezy is doing it on their end?
Here’s the entire code of the step I’m building in case it’s helpful:
import crypto from 'crypto'
export default defineComponent({
async run({ steps, $ }) {
const secret = '[SECRET KEY]'
const algo = 'sha256'
const hmac = crypto.createHmac(algo, secret)
hmac.update(JSON.stringify(steps.trigger.event.body))
const crypt = hmac.digest('hex')
const result = Buffer.from(crypt)
console.log(crypt)
console.log(result)
const sig = Buffer.from(steps.trigger.event.headers["x-signature"])
console.log(steps.trigger.event.headers["x-signature"])
console.log(sig)
if (crypto.timingSafeEqual(result, sig)) {
console.log("Hashes match.")
} else {
console.log("Hashes do not match.")
}
},
})
Unfortunately that didn’t produce a hash match. So it’s either steps.trigger.event.body that’s causing the issue, or it’s using JSON.stringify. And Lemon Squeezy’s docs don’t really explain exactly how they’re hashing the payload when they send it, other than demonstrating that request.rawBody code sample.
I’ve try the same thing but it doesn’t look like steps.trigger.event.body return the body in a raw format. I’ve test the webhook on webhook.site and copy/paste the body in a hmac generator with my app client secret. It gives me the good hash but when I try with the output of JSON.stringify, it doesn’t work. I can’t pass the body directly steps.trigger.event.body because it’s an object…
I’ve already try this and it doesn’t work because JSON.stringify() change the indentation. The only way I got it to work was by recreating the body manually in a string and replacing the values with variables like this.
From there, I parse the body as JSON so I can get a workable object for further steps (in my case, subscribing the buyer to a ConvertKit form, and sending a log of the sale to Slack)
request.rawBody in the LemonSqueezy documentation is the same format as steps.trigger.event.body when you have the HTTP webhook configuration set to Raw Request:
What they’re really saying is, request.rawBody is the payload body as a JSON string not as an JS object.
I tried it myself, with slightly different code:
import crypto from 'crypto'
// To use previous step data, pass the `steps` object to the run() function
export default defineComponent({
props: {
secret: {
type: 'string',
label: "LemonSqueezy Webhook Secret",
description: "The secret from the LemonSqueezy webhook dashboard",
}
},
async run({ steps, $ }) {
const secret = this.secret;
const hmac = crypto.createHmac('sha256', secret);
const digest = Buffer.from(hmac.update(steps.trigger.event.body).digest('hex'), 'utf8');
const signature = Buffer.from(steps.trigger.event.headers[11] || '', 'utf8');
if (!crypto.timingSafeEqual(digest, signature)) {
$.flow.exit('Invalid webhook signature')
}
return true
},
})
Then after the webhook has been verified, then parse the raw body aka JSON string back into an object:
// in a downstream Node.js code step:
export default defineComponent({
async run({ steps, $ }) {
// Parse the "raw body" back into an Object for easier use.
return JSON.parse(steps.trigger.event.body)
},
})