diff --git a/github-webhook-handler.d.ts b/github-webhook-handler.d.ts deleted file mode 100644 index 123649e..0000000 --- a/github-webhook-handler.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/// - -import { IncomingMessage, ServerResponse } from 'node:http' -import { EventEmitter } from 'node:events' - -interface CreateHandlerOptions { - path: string - secret: string - events?: string | string[] -} - -interface Handler extends EventEmitter { - (req: IncomingMessage, res: ServerResponse, callback: (err?: Error) => void): void - sign(data: string | Buffer): string - verify(signature: string, data: string | Buffer): boolean -} - -declare function createHandler (options: CreateHandlerOptions | CreateHandlerOptions[]): Handler - -export default createHandler diff --git a/github-webhook-handler.js b/github-webhook-handler.js index 6bbd6e0..0754419 100644 --- a/github-webhook-handler.js +++ b/github-webhook-handler.js @@ -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 @@ -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') @@ -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++) { @@ -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)) @@ -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] @@ -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) } @@ -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') } @@ -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' }) @@ -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) })) } diff --git a/package.json b/package.json index a83a894..390454e 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -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": [ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4930d18 --- /dev/null +++ b/tsconfig.json @@ -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 +} diff --git a/types/github-webhook-handler.d.ts b/types/github-webhook-handler.d.ts new file mode 100644 index 0000000..77e985c --- /dev/null +++ b/types/github-webhook-handler.d.ts @@ -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 \ No newline at end of file diff --git a/types/github-webhook-handler.d.ts.map b/types/github-webhook-handler.d.ts.map new file mode 100644 index 0000000..9830adb --- /dev/null +++ b/types/github-webhook-handler.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"github-webhook-handler.d.ts","sourceRoot":"","sources":["../github-webhook-handler.js"],"names":[],"mappings":";;UAMc,MAAM;YACN,MAAM;;;;;;;WAMN,MAAM;;;;QACN,MAAM;;;;aACN,GAAG;;;;;;;;;;;;SAGH,MAAM;;;;UACN,MAAM;;AAwCpB;;;GAGG;AACH,qCAHW,oBAAoB,GAAG,oBAAoB,EAAE,GAC3C,YAAY,GAAG;IAAC,CAAC,GAAG,EAAE,OAAO,WAAW,EAAE,eAAe,EAAE,GAAG,EAAE,OAAO,WAAW,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IAAC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;CAAC,CAyIvP;6BAtM4B,aAAa"} \ No newline at end of file diff --git a/types/tsconfig.tsbuildinfo b/types/tsconfig.tsbuildinfo new file mode 100644 index 0000000..bfcb06f --- /dev/null +++ b/types/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["../github-webhook-handler.js"],"version":"5.9.3"} \ No newline at end of file