Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions github-webhook-handler.d.ts

This file was deleted.

65 changes: 60 additions & 5 deletions github-webhook-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@ import { EventEmitter } from 'node:events'
import crypto from 'node:crypto'
import bl from 'bl'

/**
* @typedef {Object} CreateHandlerOptions
* @property {string} path
* @property {string} secret
* @property {string | string[]} [events]
*/

/**
* @typedef {Object} WebhookEvent
* @property {string} event - The event type (e.g. 'push', 'issues')
* @property {string} id - The delivery ID from X-Github-Delivery header
* @property {any} payload - The parsed JSON payload
* @property {string} [protocol] - The request protocol
* @property {string} [host] - The request host header
* @property {string} url - The request URL
* @property {string} path - The matched handler path
*/

/**
* @param {string} url
* @param {CreateHandlerOptions | CreateHandlerOptions[]} arr
* @returns {CreateHandlerOptions}
*/
function findHandler (url, arr) {
if (!Array.isArray(arr)) {
return arr
Expand All @@ -17,6 +40,9 @@ function findHandler (url, arr) {
return ret
}

/**
* @param {CreateHandlerOptions} options
*/
function checkType (options) {
if (typeof options !== 'object') {
throw new TypeError('must provide an options object')
Expand All @@ -31,7 +57,12 @@ function checkType (options) {
}
}

/**
* @param {CreateHandlerOptions | CreateHandlerOptions[]} initOptions
* @returns {EventEmitter & {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, callback: (err?: Error) => void): void, sign(data: string | Buffer): string, verify(signature: string, data: string | Buffer): boolean}}
*/
function create (initOptions) {
/** @type {CreateHandlerOptions} */
let options
if (Array.isArray(initOptions)) {
for (let i = 0; i < initOptions.length; i++) {
Expand All @@ -41,18 +72,30 @@ function create (initOptions) {
checkType(initOptions)
}

// @ts-ignore - handler is a callable EventEmitter via setPrototypeOf
Object.setPrototypeOf(handler, EventEmitter.prototype)
// @ts-ignore
EventEmitter.call(handler)

handler.sign = sign
handler.verify = verify

// @ts-ignore
return handler

/**
* @param {string | Buffer} data
* @returns {string}
*/
function sign (data) {
return `sha1=${crypto.createHmac('sha1', options.secret).update(data).digest('hex')}`
}

/**
* @param {string} signature
* @param {string | Buffer} data
* @returns {boolean}
*/
function verify (signature, data) {
const sig = Buffer.from(signature)
const signed = Buffer.from(sign(data))
Expand All @@ -62,10 +105,16 @@ function create (initOptions) {
return crypto.timingSafeEqual(sig, signed)
}

/**
* @param {import('node:http').IncomingMessage} req
* @param {import('node:http').ServerResponse} res
* @param {(err?: Error) => void} callback
*/
function handler (req, res, callback) {
/** @type {string[] | undefined} */
let events

options = findHandler(req.url, initOptions)
options = findHandler(/** @type {string} */ (req.url), initOptions)

if (typeof options.events === 'string' && options.events !== '*') {
events = [options.events]
Expand All @@ -77,12 +126,16 @@ function create (initOptions) {
return callback()
}

/**
* @param {string} msg
*/
function hasError (msg) {
res.writeHead(400, { 'content-type': 'application/json' })
res.end(JSON.stringify({ error: msg }))

const err = new Error(msg)

// @ts-ignore - handler has EventEmitter prototype
handler.emit('error', err, req)
callback(err)
}
Expand All @@ -103,7 +156,7 @@ function create (initOptions) {
return hasError('No X-Github-Delivery found on request')
}

if (events && events.indexOf(event) === -1) {
if (events && events.indexOf(/** @type {string} */ (event)) === -1) {
return hasError('X-Github-Event is not acceptable')
}

Expand All @@ -114,14 +167,14 @@ function create (initOptions) {

let obj

if (!verify(sig, data)) {
if (!verify(/** @type {string} */ (sig), data)) {
return hasError('X-Hub-Signature does not match blob signature')
}

try {
obj = JSON.parse(data.toString())
} catch (e) {
return hasError(e)
return hasError(/** @type {Error} */ (e).message)
}

res.writeHead(200, { 'content-type': 'application/json' })
Expand All @@ -131,13 +184,15 @@ function create (initOptions) {
event,
id,
payload: obj,
protocol: req.protocol,
protocol: /** @type {any} */ (req).protocol,
host: req.headers.host,
url: req.url,
path: options.path
}

// @ts-ignore - handler has EventEmitter prototype
handler.emit(event, emitData)
// @ts-ignore - handler has EventEmitter prototype
handler.emit('*', emitData)
}))
}
Expand Down
17 changes: 13 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@
"description": "Web handler / middleware for processing GitHub Webhooks",
"type": "module",
"main": "github-webhook-handler.js",
"exports": "./github-webhook-handler.js",
"types": "github-webhook-handler.d.ts",
"exports": {
".": {
"import": "./github-webhook-handler.js",
"types": "./types/github-webhook-handler.d.ts"
}
},
"types": "types/github-webhook-handler.d.ts",
"engines": {
"node": ">=20"
},
"scripts": {
"lint": "standard",
"build": "true",
"build": "npm run build:types",
"build:types": "tsc --build",
"prepublishOnly": "npm run build",
"test:unit": "node --test test.js",
"test": "npm run lint && npm run test:unit"
},
Expand All @@ -35,9 +42,11 @@
"@semantic-release/github": "^12.0.2",
"@semantic-release/npm": "^13.1.3",
"@semantic-release/release-notes-generator": "^14.1.0",
"@types/node": "^25.0.10",
"conventional-changelog-conventionalcommits": "^9.1.0",
"semantic-release": "^25.0.2",
"standard": "^17.1.2"
"standard": "^17.1.2",
"typescript": "^5.9.3"
},
"release": {
"branches": [
Expand Down
37 changes: 37 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": false,
"noImplicitAny": true,
"noImplicitThis": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strictFunctionTypes": false,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"strict": true,
"alwaysStrict": true,
"esModuleInterop": true,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"declaration": true,
"declarationMap": true,
"outDir": "types",
"skipLibCheck": true,
"stripInternal": true,
"resolveJsonModule": true,
"baseUrl": ".",
"emitDeclarationOnly": true,
"paths": {
"github-webhook-handler": ["github-webhook-handler.js"]
}
},
"include": ["github-webhook-handler.js"],
"exclude": ["node_modules"],
"compileOnSave": false
}
47 changes: 47 additions & 0 deletions types/github-webhook-handler.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export default create;
export type CreateHandlerOptions = {
path: string;
secret: string;
events?: string | string[] | undefined;
};
export type WebhookEvent = {
/**
* - The event type (e.g. 'push', 'issues')
*/
event: string;
/**
* - The delivery ID from X-Github-Delivery header
*/
id: string;
/**
* - The parsed JSON payload
*/
payload: any;
/**
* - The request protocol
*/
protocol?: string | undefined;
/**
* - The request host header
*/
host?: string | undefined;
/**
* - The request URL
*/
url: string;
/**
* - The matched handler path
*/
path: string;
};
/**
* @param {CreateHandlerOptions | CreateHandlerOptions[]} initOptions
* @returns {EventEmitter & {(req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse, callback: (err?: Error) => void): void, sign(data: string | Buffer): string, verify(signature: string, data: string | Buffer): boolean}}
*/
declare function create(initOptions: CreateHandlerOptions | CreateHandlerOptions[]): EventEmitter & {
(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, callback: (err?: Error) => void): void;
sign(data: string | Buffer): string;
verify(signature: string, data: string | Buffer): boolean;
};
import { EventEmitter } from 'node:events';
//# sourceMappingURL=github-webhook-handler.d.ts.map
1 change: 1 addition & 0 deletions types/github-webhook-handler.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions types/tsconfig.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"root":["../github-webhook-handler.js"],"version":"5.9.3"}