refactor: webhook handler to improve readability and error handling

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei
2023-12-28 11:25:46 +08:00
parent e31b98a37f
commit af0268e759
3 changed files with 58 additions and 47 deletions

View File

@@ -0,0 +1,5 @@
export class InvalidSignatureError extends Error {
constructor() {
super('Invalid Signature')
}
}

View File

@@ -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(

View File

@@ -1,3 +1,4 @@
export * from './event.enum'
export * from './handler'
export * from './types'
export * from './error'