refactor: webhook handler to improve readability and error handling
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
5
packages/webhook/src/error.ts
Normal file
5
packages/webhook/src/error.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class InvalidSignatureError extends Error {
|
||||
constructor() {
|
||||
super('Invalid Signature')
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,15 @@ import type { IncomingMessage, ServerResponse } from 'http'
|
||||
import type { BusinessEvents } from './event.enum'
|
||||
import type { ExtendedEventEmitter } from './types'
|
||||
|
||||
import { InvalidSignatureError } from './error'
|
||||
|
||||
interface CreateHandlerOptions {
|
||||
secret: string
|
||||
events?: 'all' | BusinessEvents[]
|
||||
}
|
||||
|
||||
export type RequestWithJSONBody = IncomingMessage & Request & { body: object }
|
||||
type Handler = {
|
||||
(req: IncomingMessage, res: ServerResponse): void
|
||||
(req: RequestWithJSONBody, res: ServerResponse): void
|
||||
} & {
|
||||
emitter: ExtendedEventEmitter
|
||||
}
|
||||
@@ -21,48 +23,34 @@ export const createHandler = (options: CreateHandlerOptions): Handler => {
|
||||
|
||||
const handler: Handler = async function (req, res) {
|
||||
try {
|
||||
const signature = req.headers['x-webhook-signature']
|
||||
assert(
|
||||
typeof signature === 'string',
|
||||
'X-Webhook-Signature must be string',
|
||||
)
|
||||
const event = req.headers['x-webhook-event']
|
||||
const signature256 = req.headers['x-webhook-signature256']
|
||||
assert(
|
||||
typeof signature256 === 'string',
|
||||
'X-Webhook-Signature256 must be string',
|
||||
)
|
||||
const data = await readDataFromRequest({ req, secret })
|
||||
|
||||
const obj = (req as any).body || (await parseJSONFromRequest(req))
|
||||
const stringifyPayload = JSON.stringify(obj)
|
||||
const isValid =
|
||||
verifyWebhook(secret, stringifyPayload, signature256 as string) &&
|
||||
verifyWebhookSha1(secret, stringifyPayload, signature as string)
|
||||
if (isValid) {
|
||||
if (event === 'health_check') {
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(JSON.stringify({ ok: 1 }))
|
||||
return
|
||||
}
|
||||
const { event, payload } = data
|
||||
|
||||
handler.emitter.emit(event as BusinessEvents, obj)
|
||||
handler.emitter.emit('*', {
|
||||
type: event,
|
||||
payload: obj,
|
||||
})
|
||||
if (event === 'health_check') {
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(JSON.stringify({ ok: 1 }))
|
||||
} else {
|
||||
console.error('revice a invalidate webhook payload', req.headers)
|
||||
return
|
||||
}
|
||||
|
||||
handler.emitter.emit(event as BusinessEvents, payload)
|
||||
handler.emitter.emit('*', {
|
||||
type: event,
|
||||
payload,
|
||||
})
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(JSON.stringify({ ok: 1 }))
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidSignatureError) {
|
||||
handler.emitter.emit('error', new Error('invalidate signature'))
|
||||
|
||||
res.statusCode = 400
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(JSON.stringify({ ok: 0, message: 'Invalid Signature' }))
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
res.statusCode = 500
|
||||
res.end(JSON.stringify({ ok: 0, message: err.message }))
|
||||
}
|
||||
@@ -72,20 +60,37 @@ export const createHandler = (options: CreateHandlerOptions): Handler => {
|
||||
return handler
|
||||
}
|
||||
|
||||
function parseJSONFromRequest(req: IncomingMessage) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = ''
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk
|
||||
})
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(body))
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
export const readDataFromRequest = async ({
|
||||
req,
|
||||
secret,
|
||||
}: {
|
||||
secret: string
|
||||
req: RequestWithJSONBody
|
||||
}) => {
|
||||
const signature = req.headers['x-webhook-signature']
|
||||
assert(typeof signature === 'string', 'X-Webhook-Signature must be string')
|
||||
const event = req.headers['x-webhook-event']
|
||||
const signature256 = req.headers['x-webhook-signature256']
|
||||
assert(
|
||||
typeof signature256 === 'string',
|
||||
'X-Webhook-Signature256 must be string',
|
||||
)
|
||||
|
||||
const obj = req.body
|
||||
const stringifyPayload = JSON.stringify(obj)
|
||||
const isValid =
|
||||
verifyWebhook(secret, stringifyPayload, signature256 as string) &&
|
||||
verifyWebhookSha1(secret, stringifyPayload, signature as string)
|
||||
|
||||
if (isValid) {
|
||||
return {
|
||||
event,
|
||||
payload: obj,
|
||||
}
|
||||
} else {
|
||||
console.error('revice a invalidate webhook payload', req.headers)
|
||||
throw new InvalidSignatureError()
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyWebhook(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './event.enum'
|
||||
export * from './handler'
|
||||
export * from './types'
|
||||
export * from './error'
|
||||
|
||||
Reference in New Issue
Block a user