Skip to content

Commit f8f20d8

Browse files
authored
feat: add types via jsdoc tsc, remove old handrolled types (#59)
1 parent 4ae0b1d commit f8f20d8

File tree

7 files changed

+159
-29
lines changed

7 files changed

+159
-29
lines changed

github-webhook-handler.d.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

github-webhook-handler.js

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@ import { EventEmitter } from 'node:events'
22
import crypto from 'node:crypto'
33
import bl from 'bl'
44

5+
/**
6+
* @typedef {Object} CreateHandlerOptions
7+
* @property {string} path
8+
* @property {string} secret
9+
* @property {string | string[]} [events]
10+
*/
11+
12+
/**
13+
* @typedef {Object} WebhookEvent
14+
* @property {string} event - The event type (e.g. 'push', 'issues')
15+
* @property {string} id - The delivery ID from X-Github-Delivery header
16+
* @property {any} payload - The parsed JSON payload
17+
* @property {string} [protocol] - The request protocol
18+
* @property {string} [host] - The request host header
19+
* @property {string} url - The request URL
20+
* @property {string} path - The matched handler path
21+
*/
22+
23+
/**
24+
* @param {string} url
25+
* @param {CreateHandlerOptions | CreateHandlerOptions[]} arr
26+
* @returns {CreateHandlerOptions}
27+
*/
528
function findHandler (url, arr) {
629
if (!Array.isArray(arr)) {
730
return arr
@@ -17,6 +40,9 @@ function findHandler (url, arr) {
1740
return ret
1841
}
1942

43+
/**
44+
* @param {CreateHandlerOptions} options
45+
*/
2046
function checkType (options) {
2147
if (typeof options !== 'object') {
2248
throw new TypeError('must provide an options object')
@@ -31,7 +57,12 @@ function checkType (options) {
3157
}
3258
}
3359

60+
/**
61+
* @param {CreateHandlerOptions | CreateHandlerOptions[]} initOptions
62+
* @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}}
63+
*/
3464
function create (initOptions) {
65+
/** @type {CreateHandlerOptions} */
3566
let options
3667
if (Array.isArray(initOptions)) {
3768
for (let i = 0; i < initOptions.length; i++) {
@@ -41,18 +72,30 @@ function create (initOptions) {
4172
checkType(initOptions)
4273
}
4374

75+
// @ts-ignore - handler is a callable EventEmitter via setPrototypeOf
4476
Object.setPrototypeOf(handler, EventEmitter.prototype)
77+
// @ts-ignore
4578
EventEmitter.call(handler)
4679

4780
handler.sign = sign
4881
handler.verify = verify
4982

83+
// @ts-ignore
5084
return handler
5185

86+
/**
87+
* @param {string | Buffer} data
88+
* @returns {string}
89+
*/
5290
function sign (data) {
5391
return `sha1=${crypto.createHmac('sha1', options.secret).update(data).digest('hex')}`
5492
}
5593

94+
/**
95+
* @param {string} signature
96+
* @param {string | Buffer} data
97+
* @returns {boolean}
98+
*/
5699
function verify (signature, data) {
57100
const sig = Buffer.from(signature)
58101
const signed = Buffer.from(sign(data))
@@ -62,10 +105,16 @@ function create (initOptions) {
62105
return crypto.timingSafeEqual(sig, signed)
63106
}
64107

108+
/**
109+
* @param {import('node:http').IncomingMessage} req
110+
* @param {import('node:http').ServerResponse} res
111+
* @param {(err?: Error) => void} callback
112+
*/
65113
function handler (req, res, callback) {
114+
/** @type {string[] | undefined} */
66115
let events
67116

68-
options = findHandler(req.url, initOptions)
117+
options = findHandler(/** @type {string} */ (req.url), initOptions)
69118

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

129+
/**
130+
* @param {string} msg
131+
*/
80132
function hasError (msg) {
81133
res.writeHead(400, { 'content-type': 'application/json' })
82134
res.end(JSON.stringify({ error: msg }))
83135

84136
const err = new Error(msg)
85137

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

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

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

115168
let obj
116169

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

121174
try {
122175
obj = JSON.parse(data.toString())
123176
} catch (e) {
124-
return hasError(e)
177+
return hasError(/** @type {Error} */ (e).message)
125178
}
126179

127180
res.writeHead(200, { 'content-type': 'application/json' })
@@ -131,13 +184,15 @@ function create (initOptions) {
131184
event,
132185
id,
133186
payload: obj,
134-
protocol: req.protocol,
187+
protocol: /** @type {any} */ (req).protocol,
135188
host: req.headers.host,
136189
url: req.url,
137190
path: options.path
138191
}
139192

193+
// @ts-ignore - handler has EventEmitter prototype
140194
handler.emit(event, emitData)
195+
// @ts-ignore - handler has EventEmitter prototype
141196
handler.emit('*', emitData)
142197
}))
143198
}

package.json

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@
44
"description": "Web handler / middleware for processing GitHub Webhooks",
55
"type": "module",
66
"main": "github-webhook-handler.js",
7-
"exports": "./github-webhook-handler.js",
8-
"types": "github-webhook-handler.d.ts",
7+
"exports": {
8+
".": {
9+
"import": "./github-webhook-handler.js",
10+
"types": "./types/github-webhook-handler.d.ts"
11+
}
12+
},
13+
"types": "types/github-webhook-handler.d.ts",
914
"engines": {
1015
"node": ">=20"
1116
},
1217
"scripts": {
1318
"lint": "standard",
14-
"build": "true",
19+
"build": "npm run build:types",
20+
"build:types": "tsc --build",
21+
"prepublishOnly": "npm run build",
1522
"test:unit": "node --test test.js",
1623
"test": "npm run lint && npm run test:unit"
1724
},
@@ -35,9 +42,11 @@
3542
"@semantic-release/github": "^12.0.2",
3643
"@semantic-release/npm": "^13.1.3",
3744
"@semantic-release/release-notes-generator": "^14.1.0",
45+
"@types/node": "^25.0.10",
3846
"conventional-changelog-conventionalcommits": "^9.1.0",
3947
"semantic-release": "^25.0.2",
40-
"standard": "^17.1.2"
48+
"standard": "^17.1.2",
49+
"typescript": "^5.9.3"
4150
},
4251
"release": {
4352
"branches": [

tsconfig.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"compilerOptions": {
3+
"allowJs": true,
4+
"checkJs": true,
5+
"forceConsistentCasingInFileNames": true,
6+
"noImplicitReturns": false,
7+
"noImplicitAny": true,
8+
"noImplicitThis": true,
9+
"noFallthroughCasesInSwitch": true,
10+
"noUnusedLocals": true,
11+
"noUnusedParameters": true,
12+
"strictFunctionTypes": false,
13+
"strictNullChecks": true,
14+
"strictPropertyInitialization": true,
15+
"strictBindCallApply": true,
16+
"strict": true,
17+
"alwaysStrict": true,
18+
"esModuleInterop": true,
19+
"target": "ES2022",
20+
"module": "NodeNext",
21+
"moduleResolution": "NodeNext",
22+
"declaration": true,
23+
"declarationMap": true,
24+
"outDir": "types",
25+
"skipLibCheck": true,
26+
"stripInternal": true,
27+
"resolveJsonModule": true,
28+
"baseUrl": ".",
29+
"emitDeclarationOnly": true,
30+
"paths": {
31+
"github-webhook-handler": ["github-webhook-handler.js"]
32+
}
33+
},
34+
"include": ["github-webhook-handler.js"],
35+
"exclude": ["node_modules"],
36+
"compileOnSave": false
37+
}

types/github-webhook-handler.d.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export default create;
2+
export type CreateHandlerOptions = {
3+
path: string;
4+
secret: string;
5+
events?: string | string[] | undefined;
6+
};
7+
export type WebhookEvent = {
8+
/**
9+
* - The event type (e.g. 'push', 'issues')
10+
*/
11+
event: string;
12+
/**
13+
* - The delivery ID from X-Github-Delivery header
14+
*/
15+
id: string;
16+
/**
17+
* - The parsed JSON payload
18+
*/
19+
payload: any;
20+
/**
21+
* - The request protocol
22+
*/
23+
protocol?: string | undefined;
24+
/**
25+
* - The request host header
26+
*/
27+
host?: string | undefined;
28+
/**
29+
* - The request URL
30+
*/
31+
url: string;
32+
/**
33+
* - The matched handler path
34+
*/
35+
path: string;
36+
};
37+
/**
38+
* @param {CreateHandlerOptions | CreateHandlerOptions[]} initOptions
39+
* @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}}
40+
*/
41+
declare function create(initOptions: CreateHandlerOptions | CreateHandlerOptions[]): EventEmitter & {
42+
(req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse, callback: (err?: Error) => void): void;
43+
sign(data: string | Buffer): string;
44+
verify(signature: string, data: string | Buffer): boolean;
45+
};
46+
import { EventEmitter } from 'node:events';
47+
//# sourceMappingURL=github-webhook-handler.d.ts.map

types/github-webhook-handler.d.ts.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

types/tsconfig.tsbuildinfo

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"root":["../github-webhook-handler.js"],"version":"5.9.3"}

0 commit comments

Comments
 (0)