github-webhook-handler 1.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: 'github-actions'
4
+ directory: '/'
5
+ schedule:
6
+ interval: 'weekly'
7
+ commit-message:
8
+ prefix: 'chore'
9
+ include: 'scope'
10
+ cooldown:
11
+ default-days: 5
12
+ - package-ecosystem: 'npm'
13
+ directory: '/'
14
+ schedule:
15
+ interval: 'weekly'
16
+ commit-message:
17
+ prefix: 'chore'
18
+ include: 'scope'
19
+ cooldown:
20
+ default-days: 5
@@ -0,0 +1,56 @@
1
+ name: Test & Maybe Release
2
+ on: [push, pull_request]
3
+
4
+ jobs:
5
+ test:
6
+ strategy:
7
+ fail-fast: false
8
+ matrix:
9
+ node: [lts/*, current]
10
+ os: [macos-latest, ubuntu-latest, windows-latest]
11
+ runs-on: ${{ matrix.os }}
12
+ steps:
13
+ - name: Checkout Repository
14
+ uses: actions/checkout@v6
15
+ - name: Use Node.js ${{ matrix.node }}
16
+ uses: actions/setup-node@v6
17
+ with:
18
+ node-version: ${{ matrix.node }}
19
+ - name: Install Dependencies
20
+ run: npm install --no-progress
21
+ - name: Check build is up to date
22
+ run: |
23
+ npm run build
24
+ git diff --exit-code || (echo "::error::Build artifacts not committed. Run 'npm run build' and commit the changes." && exit 1)
25
+ - name: Run tests
26
+ run: npm test
27
+
28
+ release:
29
+ name: Release
30
+ needs: test
31
+ runs-on: ubuntu-latest
32
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
33
+ permissions:
34
+ contents: write
35
+ issues: write
36
+ pull-requests: write
37
+ id-token: write
38
+ steps:
39
+ - name: Checkout
40
+ uses: actions/checkout@v6
41
+ with:
42
+ fetch-depth: 0
43
+ - name: Setup Node.js
44
+ uses: actions/setup-node@v6
45
+ with:
46
+ node-version: lts/*
47
+ registry-url: 'https://registry.npmjs.org'
48
+ - name: Install dependencies
49
+ run: npm install --no-progress --no-package-lock --no-save
50
+ - name: Build
51
+ run: npm run build
52
+ - name: Release
53
+ env:
54
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55
+ NPM_CONFIG_PROVENANCE: true
56
+ run: npx semantic-release
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ ## [2.1.0](https://github.com/rvagg/github-webhook-handler/compare/v2.0.0...v2.1.0) (2026-01-28)
2
+
3
+ ### Features
4
+
5
+ * add types via jsdoc tsc, remove old handrolled types ([#59](https://github.com/rvagg/github-webhook-handler/issues/59)) ([f8f20d8](https://github.com/rvagg/github-webhook-handler/commit/f8f20d864cf9529b2eebd953099967267b068ba4))
6
+
7
+ ## [2.0.0](https://github.com/rvagg/github-webhook-handler/compare/v1.0.0...v2.0.0) (2026-01-28)
8
+
9
+ ### ⚠ BREAKING CHANGES
10
+
11
+ * modernise, ESM, promises, update deps, GHA, auto-release (#58)
12
+
13
+ ### Features
14
+
15
+ * modernise, ESM, promises, update deps, GHA, auto-release ([#58](https://github.com/rvagg/github-webhook-handler/issues/58)) ([bc84845](https://github.com/rvagg/github-webhook-handler/commit/bc848457d5c28110ac24335af520ec3b921355fc))
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # github-webhook-handler
2
2
 
3
- [![Build Status](https://travis-ci.org/rvagg/github-webhook-handler.svg?branch=master)](https://travis-ci.org/rvagg/github-webhook-handler)
4
-
5
- [![NPM](https://nodei.co/npm/github-webhook-handler.svg)](https://nodei.co/npm/github-webhook-handler/)
3
+ [![NPM](https://nodei.co/npm/github-webhook-handler.svg?style=flat&data=n,v&color=blue)](https://nodei.co/npm/github-webhook-handler/)
6
4
 
7
5
  GitHub allows you to register **[Webhooks](https://developer.github.com/webhooks/)** for your repositories. Each time an event occurs on your repository, whether it be pushing code, filling issues or creating pull requests, the webhook address you register can be configured to be pinged with details.
8
6
 
9
7
  This library is a small handler (or "middleware" if you must) for Node.js web servers that handles all the logic of receiving and verifying webhook requests from GitHub.
10
8
 
9
+ ## Requirements
10
+
11
+ Node.js >= 20
12
+
11
13
  ## Tips
12
14
 
13
15
  In Github Webhooks settings, Content type must be `application/json`.
@@ -17,28 +19,29 @@ In Github Webhooks settings, Content type must be `application/json`.
17
19
  ## Example
18
20
 
19
21
  ```js
20
- var http = require('http')
21
- var createHandler = require('github-webhook-handler')
22
- var handler = createHandler({ path: '/webhook', secret: 'myhashsecret' })
22
+ import http from 'node:http'
23
+ import createHandler from 'github-webhook-handler'
23
24
 
24
- http.createServer(function (req, res) {
25
- handler(req, res, function (err) {
25
+ const handler = createHandler({ path: '/webhook', secret: 'myhashsecret' })
26
+
27
+ http.createServer((req, res) => {
28
+ handler(req, res, (err) => {
26
29
  res.statusCode = 404
27
30
  res.end('no such location')
28
31
  })
29
32
  }).listen(7777)
30
33
 
31
- handler.on('error', function (err) {
34
+ handler.on('error', (err) => {
32
35
  console.error('Error:', err.message)
33
36
  })
34
37
 
35
- handler.on('push', function (event) {
38
+ handler.on('push', (event) => {
36
39
  console.log('Received a push event for %s to %s',
37
40
  event.payload.repository.name,
38
41
  event.payload.ref)
39
42
  })
40
43
 
41
- handler.on('issues', function (event) {
44
+ handler.on('issues', (event) => {
42
45
  console.log('Received an issue event for %s action=%s: #%d %s',
43
46
  event.payload.repository.name,
44
47
  event.payload.action,
@@ -47,11 +50,11 @@ handler.on('issues', function (event) {
47
50
  })
48
51
  ```
49
52
 
50
- for multiple handlers, please see [multiple-handlers-issue](https://github.com/rvagg/github-webhook-handler/pull/22#issuecomment-274301907).
53
+ For multiple handlers, see [multiple-handlers-issue](https://github.com/rvagg/github-webhook-handler/pull/22#issuecomment-274301907).
51
54
 
52
55
  ## API
53
56
 
54
- github-webhook-handler exports a single function, use this function to *create* a webhook handler by passing in an *options* object. Your options object should contain:
57
+ github-webhook-handler exports a single function. Use this function to *create* a webhook handler by passing in an *options* object. Your options object should contain:
55
58
 
56
59
  * `"path"`: the complete case sensitive path/route to match when looking at `req.url` for incoming requests. Any request not matching this path will cause the callback function to the handler to be called (sometimes called the `next` handler).
57
60
  * `"secret"`: this is a hash key used for creating the SHA-1 HMAC signature of the JSON blob sent by GitHub. You should register the same secret key with GitHub. Any request not delivering a `X-Hub-Signature` that matches the signature generated using this key against the blob will be rejected and cause an `'error'` event (also the callback will be called with an `Error` object).
@@ -66,13 +69,14 @@ See the [GitHub Webhooks documentation](https://developer.github.com/webhooks/)
66
69
  Included in the distribution is an *events.json* file which maps the event names to descriptions taken from the API:
67
70
 
68
71
  ```js
69
- var events = require('github-webhook-handler/events')
70
- Object.keys(events).forEach(function (event) {
71
- console.log(event, '=', events[event])
72
- })
72
+ import events from 'github-webhook-handler/events.json' with { type: 'json' }
73
+
74
+ for (const [event, description] of Object.entries(events)) {
75
+ console.log(event, '=', description)
76
+ }
73
77
  ```
74
78
 
75
- Additionally, there is a special `'*'` even you can listen to in order to receive _everything_.
79
+ Additionally, there is a special `'*'` event you can listen to in order to receive _everything_.
76
80
 
77
81
  ## License
78
82
 
@@ -1,7 +1,30 @@
1
- const EventEmitter = require('events')
2
- const crypto = require('crypto')
3
- const bl = require('bl')
4
-
1
+ import { EventEmitter } from 'node:events'
2
+ import crypto from 'node:crypto'
3
+ import bl from 'bl'
4
+
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
+ */
5
28
  function findHandler (url, arr) {
6
29
  if (!Array.isArray(arr)) {
7
30
  return arr
@@ -17,23 +40,30 @@ function findHandler (url, arr) {
17
40
  return ret
18
41
  }
19
42
 
43
+ /**
44
+ * @param {CreateHandlerOptions} options
45
+ */
20
46
  function checkType (options) {
21
47
  if (typeof options !== 'object') {
22
48
  throw new TypeError('must provide an options object')
23
49
  }
24
50
 
25
51
  if (typeof options.path !== 'string') {
26
- throw new TypeError('must provide a \'path\' option')
52
+ throw new TypeError("must provide a 'path' option")
27
53
  }
28
54
 
29
55
  if (typeof options.secret !== 'string') {
30
- throw new TypeError('must provide a \'secret\' option')
56
+ throw new TypeError("must provide a 'secret' option")
31
57
  }
32
58
  }
33
59
 
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
+ */
34
64
  function create (initOptions) {
65
+ /** @type {CreateHandlerOptions} */
35
66
  let options
36
- // validate type of options
37
67
  if (Array.isArray(initOptions)) {
38
68
  for (let i = 0; i < initOptions.length; i++) {
39
69
  checkType(initOptions[i])
@@ -42,19 +72,30 @@ function create (initOptions) {
42
72
  checkType(initOptions)
43
73
  }
44
74
 
45
- // make it an EventEmitter
75
+ // @ts-ignore - handler is a callable EventEmitter via setPrototypeOf
46
76
  Object.setPrototypeOf(handler, EventEmitter.prototype)
77
+ // @ts-ignore
47
78
  EventEmitter.call(handler)
48
79
 
49
80
  handler.sign = sign
50
81
  handler.verify = verify
51
82
 
83
+ // @ts-ignore
52
84
  return handler
53
85
 
86
+ /**
87
+ * @param {string | Buffer} data
88
+ * @returns {string}
89
+ */
54
90
  function sign (data) {
55
91
  return `sha1=${crypto.createHmac('sha1', options.secret).update(data).digest('hex')}`
56
92
  }
57
93
 
94
+ /**
95
+ * @param {string} signature
96
+ * @param {string | Buffer} data
97
+ * @returns {boolean}
98
+ */
58
99
  function verify (signature, data) {
59
100
  const sig = Buffer.from(signature)
60
101
  const signed = Buffer.from(sign(data))
@@ -64,10 +105,16 @@ function create (initOptions) {
64
105
  return crypto.timingSafeEqual(sig, signed)
65
106
  }
66
107
 
108
+ /**
109
+ * @param {import('node:http').IncomingMessage} req
110
+ * @param {import('node:http').ServerResponse} res
111
+ * @param {(err?: Error) => void} callback
112
+ */
67
113
  function handler (req, res, callback) {
114
+ /** @type {string[] | undefined} */
68
115
  let events
69
116
 
70
- options = findHandler(req.url, initOptions)
117
+ options = findHandler(/** @type {string} */ (req.url), initOptions)
71
118
 
72
119
  if (typeof options.events === 'string' && options.events !== '*') {
73
120
  events = [options.events]
@@ -79,12 +126,16 @@ function create (initOptions) {
79
126
  return callback()
80
127
  }
81
128
 
129
+ /**
130
+ * @param {string} msg
131
+ */
82
132
  function hasError (msg) {
83
133
  res.writeHead(400, { 'content-type': 'application/json' })
84
134
  res.end(JSON.stringify({ error: msg }))
85
135
 
86
136
  const err = new Error(msg)
87
137
 
138
+ // @ts-ignore - handler has EventEmitter prototype
88
139
  handler.emit('error', err, req)
89
140
  callback(err)
90
141
  }
@@ -105,7 +156,7 @@ function create (initOptions) {
105
156
  return hasError('No X-Github-Delivery found on request')
106
157
  }
107
158
 
108
- if (events && events.indexOf(event) === -1) {
159
+ if (events && events.indexOf(/** @type {string} */ (event)) === -1) {
109
160
  return hasError('X-Github-Event is not acceptable')
110
161
  }
111
162
 
@@ -116,33 +167,35 @@ function create (initOptions) {
116
167
 
117
168
  let obj
118
169
 
119
- if (!verify(sig, data)) {
170
+ if (!verify(/** @type {string} */ (sig), data)) {
120
171
  return hasError('X-Hub-Signature does not match blob signature')
121
172
  }
122
173
 
123
174
  try {
124
175
  obj = JSON.parse(data.toString())
125
176
  } catch (e) {
126
- return hasError(e)
177
+ return hasError(/** @type {Error} */ (e).message)
127
178
  }
128
179
 
129
180
  res.writeHead(200, { 'content-type': 'application/json' })
130
181
  res.end('{"ok":true}')
131
182
 
132
183
  const emitData = {
133
- event: event,
134
- id: id,
184
+ event,
185
+ id,
135
186
  payload: obj,
136
- protocol: req.protocol,
187
+ protocol: /** @type {any} */ (req).protocol,
137
188
  host: req.headers.host,
138
189
  url: req.url,
139
190
  path: options.path
140
191
  }
141
192
 
193
+ // @ts-ignore - handler has EventEmitter prototype
142
194
  handler.emit(event, emitData)
195
+ // @ts-ignore - handler has EventEmitter prototype
143
196
  handler.emit('*', emitData)
144
197
  }))
145
198
  }
146
199
  }
147
200
 
148
- module.exports = create
201
+ export default create
package/package.json CHANGED
@@ -1,12 +1,26 @@
1
1
  {
2
2
  "name": "github-webhook-handler",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Web handler / middleware for processing GitHub Webhooks",
5
+ "type": "module",
5
6
  "main": "github-webhook-handler.js",
6
- "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",
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
7
17
  "scripts": {
8
- "lint": "standard *.js",
9
- "test": "npm run lint && node test.js"
18
+ "lint": "standard",
19
+ "build": "npm run build:types",
20
+ "build:types": "tsc --build",
21
+ "prepublishOnly": "npm run build",
22
+ "test:unit": "node --test test.js",
23
+ "test": "npm run lint && npm run test:unit"
10
24
  },
11
25
  "keywords": [
12
26
  "github",
@@ -19,13 +33,100 @@
19
33
  },
20
34
  "license": "MIT",
21
35
  "dependencies": {
22
- "bl": "~4.0.0"
36
+ "bl": "^6.1.6"
23
37
  },
24
38
  "devDependencies": {
25
- "@types/node": "*",
26
- "run-series": "~1.1.8",
27
- "standard": "~14.3.1",
28
- "tape": "~4.11.0",
29
- "through2": "~3.0.1"
39
+ "@semantic-release/changelog": "^6.0.3",
40
+ "@semantic-release/commit-analyzer": "^13.0.1",
41
+ "@semantic-release/git": "^10.0.1",
42
+ "@semantic-release/github": "^12.0.2",
43
+ "@semantic-release/npm": "^13.1.3",
44
+ "@semantic-release/release-notes-generator": "^14.1.0",
45
+ "@types/node": "^25.0.10",
46
+ "conventional-changelog-conventionalcommits": "^9.1.0",
47
+ "semantic-release": "^25.0.2",
48
+ "standard": "^17.1.2",
49
+ "typescript": "^5.9.3"
50
+ },
51
+ "release": {
52
+ "branches": [
53
+ "master"
54
+ ],
55
+ "plugins": [
56
+ [
57
+ "@semantic-release/commit-analyzer",
58
+ {
59
+ "preset": "conventionalcommits",
60
+ "releaseRules": [
61
+ {
62
+ "breaking": true,
63
+ "release": "major"
64
+ },
65
+ {
66
+ "revert": true,
67
+ "release": "patch"
68
+ },
69
+ {
70
+ "type": "feat",
71
+ "release": "minor"
72
+ },
73
+ {
74
+ "type": "fix",
75
+ "release": "patch"
76
+ },
77
+ {
78
+ "type": "chore",
79
+ "release": "patch"
80
+ },
81
+ {
82
+ "type": "docs",
83
+ "release": "patch"
84
+ },
85
+ {
86
+ "type": "test",
87
+ "release": "patch"
88
+ },
89
+ {
90
+ "scope": "no-release",
91
+ "release": false
92
+ }
93
+ ]
94
+ }
95
+ ],
96
+ [
97
+ "@semantic-release/release-notes-generator",
98
+ {
99
+ "preset": "conventionalcommits",
100
+ "presetConfig": {
101
+ "types": [
102
+ {
103
+ "type": "feat",
104
+ "section": "Features"
105
+ },
106
+ {
107
+ "type": "fix",
108
+ "section": "Bug Fixes"
109
+ },
110
+ {
111
+ "type": "chore",
112
+ "section": "Trivial Changes"
113
+ },
114
+ {
115
+ "type": "docs",
116
+ "section": "Trivial Changes"
117
+ },
118
+ {
119
+ "type": "test",
120
+ "section": "Tests"
121
+ }
122
+ ]
123
+ }
124
+ }
125
+ ],
126
+ "@semantic-release/changelog",
127
+ "@semantic-release/npm",
128
+ "@semantic-release/github",
129
+ "@semantic-release/git"
130
+ ]
30
131
  }
31
132
  }
package/test.js CHANGED
@@ -1,15 +1,15 @@
1
- const test = require('tape')
2
- const crypto = require('crypto')
3
- const handler = require('./')
4
- const through2 = require('through2')
5
- const series = require('run-series')
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import crypto from 'node:crypto'
4
+ import { PassThrough } from 'node:stream'
5
+ import handler from './github-webhook-handler.js'
6
6
 
7
7
  function signBlob (key, blob) {
8
8
  return `sha1=${crypto.createHmac('sha1', key).update(blob).digest('hex')}`
9
9
  }
10
10
 
11
11
  function mkReq (url, method) {
12
- const req = through2()
12
+ const req = new PassThrough()
13
13
  req.method = method || 'POST'
14
14
  req.url = url
15
15
  req.headers = {
@@ -22,116 +22,102 @@ function mkReq (url, method) {
22
22
 
23
23
  function mkRes () {
24
24
  const res = {
25
- writeHead: function (statusCode, headers) {
25
+ writeHead (statusCode, headers) {
26
26
  res.$statusCode = statusCode
27
27
  res.$headers = headers
28
28
  },
29
-
30
- end: function (content) {
29
+ end (content) {
31
30
  res.$end = content
32
31
  }
33
32
  }
34
-
35
33
  return res
36
34
  }
37
35
 
38
- test('handler without full options throws', (t) => {
39
- t.plan(4)
40
-
41
- t.equal(typeof handler, 'function', 'handler exports a function')
42
-
43
- t.throws(handler, /must provide an options object/, 'throws if no options')
44
-
45
- t.throws(handler.bind(null, {}), /must provide a 'path' option/, 'throws if no path option')
46
-
47
- t.throws(handler.bind(null, { path: '/' }), /must provide a 'secret' option/, 'throws if no secret option')
36
+ test('handler without full options throws', () => {
37
+ assert.strictEqual(typeof handler, 'function', 'handler exports a function')
38
+ assert.throws(() => handler(), /must provide an options object/, 'throws if no options')
39
+ assert.throws(() => handler({}), /must provide a 'path' option/, 'throws if no path option')
40
+ assert.throws(() => handler({ path: '/' }), /must provide a 'secret' option/, 'throws if no secret option')
48
41
  })
49
42
 
50
- test('handler without full options throws in array', (t) => {
51
- t.plan(2)
52
-
53
- t.throws(handler.bind(null, [{}]), /must provide a 'path' option/, 'throws if no path option')
54
-
55
- t.throws(handler.bind(null, [{ path: '/' }]), /must provide a 'secret' option/, 'throws if no secret option')
43
+ test('handler without full options throws in array', () => {
44
+ assert.throws(() => handler([{}]), /must provide a 'path' option/, 'throws if no path option')
45
+ assert.throws(() => handler([{ path: '/' }]), /must provide a 'secret' option/, 'throws if no secret option')
56
46
  })
57
47
 
58
- test('handler ignores invalid urls', (t) => {
48
+ test('handler ignores invalid urls', async () => {
59
49
  const options = { path: '/some/url', secret: 'bogus' }
60
50
  const h = handler(options)
61
51
 
62
- t.plan(6)
63
-
64
- h(mkReq('/'), mkRes(), (err) => {
65
- t.error(err)
66
- t.ok(true, 'request was ignored')
52
+ await new Promise((resolve) => {
53
+ h(mkReq('/'), mkRes(), (err) => {
54
+ assert.ifError(err)
55
+ resolve()
56
+ })
67
57
  })
68
58
 
69
- // near match
70
- h(mkReq('/some/url/'), mkRes(), (err) => {
71
- t.error(err)
72
- t.ok(true, 'request was ignored')
59
+ await new Promise((resolve) => {
60
+ h(mkReq('/some/url/'), mkRes(), (err) => {
61
+ assert.ifError(err)
62
+ resolve()
63
+ })
73
64
  })
74
65
 
75
- // partial match
76
- h(mkReq('/some'), mkRes(), (err) => {
77
- t.error(err)
78
- t.ok(true, 'request was ignored')
66
+ await new Promise((resolve) => {
67
+ h(mkReq('/some'), mkRes(), (err) => {
68
+ assert.ifError(err)
69
+ resolve()
70
+ })
79
71
  })
80
72
  })
81
73
 
82
- test('handler ingores non-POST requests', (t) => {
74
+ test('handler ignores non-POST requests', async () => {
83
75
  const options = { path: '/some/url', secret: 'bogus' }
84
76
  const h = handler(options)
85
77
 
86
- t.plan(4)
87
-
88
- h(mkReq('/some/url', 'GET'), mkRes(), (err) => {
89
- t.error(err)
90
- t.ok(true, 'request was ignored')
78
+ await new Promise((resolve) => {
79
+ h(mkReq('/some/url', 'GET'), mkRes(), (err) => {
80
+ assert.ifError(err)
81
+ resolve()
82
+ })
91
83
  })
92
84
 
93
- h(mkReq('/some/url?test=param', 'GET'), mkRes(), (err) => {
94
- t.error(err)
95
- t.ok(true, 'request was ignored')
85
+ await new Promise((resolve) => {
86
+ h(mkReq('/some/url?test=param', 'GET'), mkRes(), (err) => {
87
+ assert.ifError(err)
88
+ resolve()
89
+ })
96
90
  })
97
91
  })
98
92
 
99
- test('handler accepts valid urls', (t) => {
93
+ test('handler accepts valid urls', async () => {
100
94
  const options = { path: '/some/url', secret: 'bogus' }
101
95
  const h = handler(options)
102
96
 
103
- t.plan(1)
104
-
105
- h(mkReq('/some/url'), mkRes(), (err) => {
106
- t.error(err)
107
- t.fail(false, 'should not call')
97
+ let callbackCalled = false
98
+ h(mkReq('/some/url'), mkRes(), () => {
99
+ callbackCalled = true
108
100
  })
109
101
 
110
- setTimeout(t.ok.bind(t, true, 'done'))
102
+ await new Promise((resolve) => setTimeout(resolve, 10))
103
+ assert.strictEqual(callbackCalled, false, 'callback should not be called for valid POST')
111
104
  })
112
105
 
113
- test('handler accepts valid urls in Array', (t) => {
106
+ test('handler accepts valid urls in Array', async () => {
114
107
  const options = [{ path: '/some/url', secret: 'bogus' }, { path: '/someOther/url', secret: 'bogus' }]
115
108
  const h = handler(options)
116
109
 
117
- t.plan(1)
118
-
119
- h(mkReq('/some/url'), mkRes(), (err) => {
120
- t.error(err)
121
- t.fail(false, 'should not call')
122
- })
123
-
124
- h(mkReq('/someOther/url'), mkRes(), (err) => {
125
- t.error(err)
126
- t.fail(false, 'should not call')
127
- })
110
+ let callbackCalled = false
111
+ h(mkReq('/some/url'), mkRes(), () => { callbackCalled = true })
112
+ h(mkReq('/someOther/url'), mkRes(), () => { callbackCalled = true })
128
113
 
129
- setTimeout(t.ok.bind(t, true, 'done'))
114
+ await new Promise((resolve) => setTimeout(resolve, 10))
115
+ assert.strictEqual(callbackCalled, false, 'callback should not be called for valid POSTs')
130
116
  })
131
117
 
132
- test('handler can reject events', (t) => {
118
+ test('handler can reject events', async () => {
133
119
  const acceptableEvents = {
134
- undefined: undefined,
120
+ undefined,
135
121
  'a string equal to the event': 'bogus',
136
122
  'a string equal to *': '*',
137
123
  'an array containing the event': ['bogus'],
@@ -141,74 +127,55 @@ test('handler can reject events', (t) => {
141
127
  'a string not equal to the event or *': 'not-bogus',
142
128
  'an array not containing the event or *': ['not-bogus']
143
129
  }
144
- const acceptable = Object.keys(acceptableEvents)
145
- const unacceptable = Object.keys(unacceptableEvents)
146
- const acceptableTests = acceptable.map((events) => {
147
- return acceptableReq.bind(null, events)
148
- })
149
- const unacceptableTests = unacceptable.map((events) => {
150
- return unacceptableReq.bind(null, events)
151
- })
152
-
153
- t.plan(acceptable.length + unacceptable.length)
154
- series(acceptableTests.concat(unacceptableTests))
155
130
 
156
- function acceptableReq (events, callback) {
131
+ for (const [desc, events] of Object.entries(acceptableEvents)) {
157
132
  const h = handler({
158
133
  path: '/some/url',
159
134
  secret: 'bogus',
160
- events: acceptableEvents[events]
135
+ events
161
136
  })
162
137
 
163
- h(mkReq('/some/url'), mkRes(), (err) => {
164
- t.error(err)
165
- t.fail(false, 'should not call')
166
- })
138
+ let callbackCalled = false
139
+ h(mkReq('/some/url'), mkRes(), () => { callbackCalled = true })
167
140
 
168
- setTimeout(() => {
169
- t.ok(true, 'accepted because options.events was ' + events)
170
- callback()
171
- })
141
+ await new Promise((resolve) => setTimeout(resolve, 10))
142
+ assert.strictEqual(callbackCalled, false, `accepted because options.events was ${desc}`)
172
143
  }
173
144
 
174
- function unacceptableReq (events, callback) {
145
+ for (const [desc, events] of Object.entries(unacceptableEvents)) {
175
146
  const h = handler({
176
147
  path: '/some/url',
177
148
  secret: 'bogus',
178
- events: unacceptableEvents[events]
149
+ events
179
150
  })
180
151
 
181
152
  h.on('error', () => {})
182
153
 
183
- h(mkReq('/some/url'), mkRes(), (err) => {
184
- t.ok(err, 'rejected because options.events was ' + events)
185
- callback()
154
+ await new Promise((resolve) => {
155
+ h(mkReq('/some/url'), mkRes(), (err) => {
156
+ assert.ok(err, `rejected because options.events was ${desc}`)
157
+ resolve()
158
+ })
186
159
  })
187
160
  }
188
161
  })
189
162
 
190
- // because we don't inherit in a traditional way
191
- test('handler is an EventEmitter', (t) => {
192
- t.plan(5)
193
-
163
+ test('handler is an EventEmitter', () => {
194
164
  const h = handler({ path: '/', secret: 'bogus' })
195
165
 
196
- t.equal(typeof h.on, 'function', 'has h.on()')
197
- t.equal(typeof h.emit, 'function', 'has h.emit()')
198
- t.equal(typeof h.removeListener, 'function', 'has h.removeListener()')
199
-
200
- h.on('ping', (pong) => {
201
- t.equal(pong, 'pong', 'got event')
202
- })
166
+ assert.strictEqual(typeof h.on, 'function', 'has h.on()')
167
+ assert.strictEqual(typeof h.emit, 'function', 'has h.emit()')
168
+ assert.strictEqual(typeof h.removeListener, 'function', 'has h.removeListener()')
203
169
 
170
+ let received
171
+ h.on('ping', (pong) => { received = pong })
204
172
  h.emit('ping', 'pong')
173
+ assert.strictEqual(received, 'pong', 'got event')
205
174
 
206
- t.throws(h.emit.bind(h, 'error', new Error('threw an error')), /threw an error/, 'acts like an EE')
175
+ assert.throws(() => h.emit('error', new Error('threw an error')), /threw an error/, 'acts like an EE')
207
176
  })
208
177
 
209
- test('handler accepts a signed blob', (t) => {
210
- t.plan(4)
211
-
178
+ test('handler accepts a signed blob', async () => {
212
179
  const obj = { some: 'github', object: 'with', properties: true }
213
180
  const json = JSON.stringify(obj)
214
181
  const h = handler({ path: '/', secret: 'bogus' })
@@ -218,54 +185,69 @@ test('handler accepts a signed blob', (t) => {
218
185
  req.headers['x-hub-signature'] = signBlob('bogus', json)
219
186
  req.headers['x-github-event'] = 'push'
220
187
 
221
- h.on('push', (event) => {
222
- t.deepEqual(event, { event: 'push', id: 'bogus', payload: obj, url: '/', host: undefined, protocol: undefined, path: '/' })
223
- t.equal(res.$statusCode, 200, 'correct status code')
224
- t.deepEqual(res.$headers, { 'content-type': 'application/json' })
225
- t.equal(res.$end, '{"ok":true}', 'got correct content')
188
+ const eventPromise = new Promise((resolve) => {
189
+ h.on('push', (event) => {
190
+ assert.deepStrictEqual(event, {
191
+ event: 'push',
192
+ id: 'bogus',
193
+ payload: obj,
194
+ url: '/',
195
+ host: undefined,
196
+ protocol: undefined,
197
+ path: '/'
198
+ })
199
+ assert.strictEqual(res.$statusCode, 200, 'correct status code')
200
+ assert.deepStrictEqual(res.$headers, { 'content-type': 'application/json' })
201
+ assert.strictEqual(res.$end, '{"ok":true}', 'got correct content')
202
+ resolve()
203
+ })
226
204
  })
227
205
 
228
- h(req, res, (err) => {
229
- t.error(err)
230
- t.fail(true, 'should not get here!')
206
+ h(req, res, () => {
207
+ assert.fail('should not get here')
231
208
  })
232
209
 
233
- process.nextTick(() => {
234
- req.end(json)
235
- })
210
+ process.nextTick(() => req.end(json))
211
+ await eventPromise
236
212
  })
237
213
 
238
- test('handler accepts multi blob in Array', (t) => {
239
- t.plan(4)
240
-
214
+ test('handler accepts multi blob in Array', async () => {
241
215
  const obj = { some: 'github', object: 'with', properties: true }
242
216
  const json = JSON.stringify(obj)
243
217
  const h = handler([{ path: '/', secret: 'bogus' }, { path: '/some/url', secret: 'bogus' }])
244
218
  const req = mkReq('/some/url')
245
219
  const res = mkRes()
220
+
246
221
  req.headers['x-hub-signature'] = signBlob('bogus', json)
247
222
  req.headers['x-github-event'] = 'push'
248
223
 
249
- h.on('push', (event) => {
250
- t.deepEqual(event, { event: 'push', id: 'bogus', payload: obj, url: '/some/url', host: undefined, protocol: undefined, path: '/some/url' })
251
- t.equal(res.$statusCode, 200, 'correct status code')
252
- t.deepEqual(res.$headers, { 'content-type': 'application/json' })
253
- t.equal(res.$end, '{"ok":true}', 'got correct content')
224
+ const eventPromise = new Promise((resolve) => {
225
+ h.on('push', (event) => {
226
+ assert.deepStrictEqual(event, {
227
+ event: 'push',
228
+ id: 'bogus',
229
+ payload: obj,
230
+ url: '/some/url',
231
+ host: undefined,
232
+ protocol: undefined,
233
+ path: '/some/url'
234
+ })
235
+ assert.strictEqual(res.$statusCode, 200, 'correct status code')
236
+ assert.deepStrictEqual(res.$headers, { 'content-type': 'application/json' })
237
+ assert.strictEqual(res.$end, '{"ok":true}', 'got correct content')
238
+ resolve()
239
+ })
254
240
  })
255
241
 
256
- h(req, res, (err) => {
257
- t.error(err)
258
- t.fail(true, 'should not get here!')
242
+ h(req, res, () => {
243
+ assert.fail('should not get here')
259
244
  })
260
245
 
261
- process.nextTick(() => {
262
- req.end(json)
263
- })
246
+ process.nextTick(() => req.end(json))
247
+ await eventPromise
264
248
  })
265
249
 
266
- test('handler accepts a signed blob with alt event', (t) => {
267
- t.plan(4)
268
-
250
+ test('handler accepts a signed blob with alt event', async () => {
269
251
  const obj = { some: 'github', object: 'with', properties: true }
270
252
  const json = JSON.stringify(obj)
271
253
  const h = handler({ path: '/', secret: 'bogus' })
@@ -275,30 +257,35 @@ test('handler accepts a signed blob with alt event', (t) => {
275
257
  req.headers['x-hub-signature'] = signBlob('bogus', json)
276
258
  req.headers['x-github-event'] = 'issue'
277
259
 
278
- h.on('push', (event) => {
279
- t.fail(true, 'should not get here!')
280
- })
281
-
282
- h.on('issue', (event) => {
283
- t.deepEqual(event, { event: 'issue', id: 'bogus', payload: obj, url: '/', host: undefined, protocol: undefined, path: '/' })
284
- t.equal(res.$statusCode, 200, 'correct status code')
285
- t.deepEqual(res.$headers, { 'content-type': 'application/json' })
286
- t.equal(res.$end, '{"ok":true}', 'got correct content')
260
+ h.on('push', () => assert.fail('should not get here'))
261
+
262
+ const eventPromise = new Promise((resolve) => {
263
+ h.on('issue', (event) => {
264
+ assert.deepStrictEqual(event, {
265
+ event: 'issue',
266
+ id: 'bogus',
267
+ payload: obj,
268
+ url: '/',
269
+ host: undefined,
270
+ protocol: undefined,
271
+ path: '/'
272
+ })
273
+ assert.strictEqual(res.$statusCode, 200, 'correct status code')
274
+ assert.deepStrictEqual(res.$headers, { 'content-type': 'application/json' })
275
+ assert.strictEqual(res.$end, '{"ok":true}', 'got correct content')
276
+ resolve()
277
+ })
287
278
  })
288
279
 
289
- h(req, res, (err) => {
290
- t.error(err)
291
- t.fail(true, 'should not get here!')
280
+ h(req, res, () => {
281
+ assert.fail('should not get here')
292
282
  })
293
283
 
294
- process.nextTick(() => {
295
- req.end(json)
296
- })
284
+ process.nextTick(() => req.end(json))
285
+ await eventPromise
297
286
  })
298
287
 
299
- test('handler rejects a badly signed blob', (t) => {
300
- t.plan(6)
301
-
288
+ test('handler rejects a badly signed blob', async () => {
302
289
  const obj = { some: 'github', object: 'with', properties: true }
303
290
  const json = JSON.stringify(obj)
304
291
  const h = handler({ path: '/', secret: 'bogus' })
@@ -306,33 +293,30 @@ test('handler rejects a badly signed blob', (t) => {
306
293
  const res = mkRes()
307
294
 
308
295
  req.headers['x-hub-signature'] = signBlob('bogus', json)
309
- // break signage by a tiny bit
310
296
  req.headers['x-hub-signature'] = '0' + req.headers['x-hub-signature'].substring(1)
311
297
 
312
- h.on('error', (err, _req) => {
313
- t.ok(err, 'got an error')
314
- t.strictEqual(_req, req, 'was given original request object')
315
- t.equal(res.$statusCode, 400, 'correct status code')
316
- t.deepEqual(res.$headers, { 'content-type': 'application/json' })
317
- t.equal(res.$end, '{"error":"X-Hub-Signature does not match blob signature"}', 'got correct content')
298
+ const errorPromise = new Promise((resolve) => {
299
+ h.on('error', (err, _req) => {
300
+ assert.ok(err, 'got an error')
301
+ assert.strictEqual(_req, req, 'was given original request object')
302
+ assert.strictEqual(res.$statusCode, 400, 'correct status code')
303
+ assert.deepStrictEqual(res.$headers, { 'content-type': 'application/json' })
304
+ assert.strictEqual(res.$end, '{"error":"X-Hub-Signature does not match blob signature"}', 'got correct content')
305
+ resolve()
306
+ })
318
307
  })
319
308
 
320
- h.on('push', (event) => {
321
- t.fail(true, 'should not get here!')
322
- })
309
+ h.on('push', () => assert.fail('should not get here'))
323
310
 
324
311
  h(req, res, (err) => {
325
- t.ok(err, 'got error on callback')
312
+ assert.ok(err, 'got error on callback')
326
313
  })
327
314
 
328
- process.nextTick(() => {
329
- req.end(json)
330
- })
315
+ process.nextTick(() => req.end(json))
316
+ await errorPromise
331
317
  })
332
318
 
333
- test('handler responds on a bl error', (t) => {
334
- t.plan(4)
335
-
319
+ test('handler responds on a stream error', async () => {
336
320
  const obj = { some: 'github', object: 'with', properties: true }
337
321
  const json = JSON.stringify(obj)
338
322
  const h = handler({ path: '/', secret: 'bogus' })
@@ -342,29 +326,25 @@ test('handler responds on a bl error', (t) => {
342
326
  req.headers['x-hub-signature'] = signBlob('bogus', json)
343
327
  req.headers['x-github-event'] = 'issue'
344
328
 
345
- h.on('push', (event) => {
346
- t.fail(true, 'should not get here!')
347
- })
348
-
349
- h.on('issue', (event) => {
350
- t.fail(true, 'should never get here!')
351
- })
329
+ h.on('push', () => assert.fail('should not get here'))
330
+ h.on('issue', () => assert.fail('should never get here'))
352
331
 
353
- h.on('error', (err) => {
354
- t.ok(err, 'got an error')
355
- t.equal(res.$statusCode, 400, 'correct status code')
332
+ const errorPromise = new Promise((resolve) => {
333
+ h.on('error', (err) => {
334
+ assert.ok(err, 'got an error')
335
+ assert.strictEqual(res.$statusCode, 400, 'correct status code')
336
+ resolve()
337
+ })
356
338
  })
357
339
 
358
340
  h(req, res, (err) => {
359
- t.ok(err)
341
+ assert.ok(err)
360
342
  })
361
343
 
362
- res.end = () => {
363
- t.equal(res.$statusCode, 400, 'correct status code')
364
- }
365
-
366
344
  req.write('{')
367
345
  process.nextTick(() => {
368
- req.emit('error', new Error('simulated explosion'))
346
+ req.destroy(new Error('simulated explosion'))
369
347
  })
348
+
349
+ await errorPromise
370
350
  })
package/tsconfig.json ADDED
@@ -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
+ }
@@ -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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1 @@
1
+ {"root":["../github-webhook-handler.js"],"version":"5.9.3"}
package/.travis.yml DELETED
@@ -1,14 +0,0 @@
1
- sudo: false
2
- language: node_js
3
- node_js:
4
- - 8
5
- - 10
6
- - 12
7
- - lts/*
8
- - current
9
- branches:
10
- only:
11
- - master
12
- notifications:
13
- email:
14
- - rod@vagg.org
@@ -1,18 +0,0 @@
1
- ///<reference types="node" />
2
-
3
- import { IncomingMessage, ServerResponse } from "http";
4
- import { EventEmitter } from "events";
5
-
6
- interface CreateHandlerOptions {
7
- path: string;
8
- secret: string;
9
- events?: string | string[];
10
- }
11
-
12
- interface handler extends EventEmitter {
13
- (req: IncomingMessage, res: ServerResponse, callback: (err: Error) => void): void;
14
- }
15
-
16
- declare function createHandler(options: CreateHandlerOptions|CreateHandlerOptions[]): handler;
17
-
18
- export = createHandler;