github-webhook-handler 0.7.1 → 2.0.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,9 @@
1
+ ## [2.0.0](https://github.com/rvagg/github-webhook-handler/compare/v1.0.0...v2.0.0) (2026-01-28)
2
+
3
+ ### ⚠ BREAKING CHANGES
4
+
5
+ * modernise, ESM, promises, update deps, GHA, auto-release (#58)
6
+
7
+ ### Features
8
+
9
+ * 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,12 +1,15 @@
1
1
  # github-webhook-handler
2
2
 
3
- [![NPM](https://nodei.co/npm/github-webhook-handler.png?downloads=true&downloadRank=true)](https://nodei.co/npm/github-webhook-handler/)
4
- [![NPM](https://nodei.co/npm-dl/github-webhook-handler.png?months=6&height=3)](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/)
5
4
 
6
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.
7
6
 
8
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.
9
8
 
9
+ ## Requirements
10
+
11
+ Node.js >= 20
12
+
10
13
  ## Tips
11
14
 
12
15
  In Github Webhooks settings, Content type must be `application/json`.
@@ -16,28 +19,29 @@ In Github Webhooks settings, Content type must be `application/json`.
16
19
  ## Example
17
20
 
18
21
  ```js
19
- var http = require('http')
20
- var createHandler = require('github-webhook-handler')
21
- var handler = createHandler({ path: '/webhook', secret: 'myhashsecret' })
22
+ import http from 'node:http'
23
+ import createHandler from 'github-webhook-handler'
24
+
25
+ const handler = createHandler({ path: '/webhook', secret: 'myhashsecret' })
22
26
 
23
- http.createServer(function (req, res) {
24
- handler(req, res, function (err) {
27
+ http.createServer((req, res) => {
28
+ handler(req, res, (err) => {
25
29
  res.statusCode = 404
26
30
  res.end('no such location')
27
31
  })
28
32
  }).listen(7777)
29
33
 
30
- handler.on('error', function (err) {
34
+ handler.on('error', (err) => {
31
35
  console.error('Error:', err.message)
32
36
  })
33
37
 
34
- handler.on('push', function (event) {
38
+ handler.on('push', (event) => {
35
39
  console.log('Received a push event for %s to %s',
36
40
  event.payload.repository.name,
37
41
  event.payload.ref)
38
42
  })
39
43
 
40
- handler.on('issues', function (event) {
44
+ handler.on('issues', (event) => {
41
45
  console.log('Received an issue event for %s action=%s: #%d %s',
42
46
  event.payload.repository.name,
43
47
  event.payload.action,
@@ -46,11 +50,11 @@ handler.on('issues', function (event) {
46
50
  })
47
51
  ```
48
52
 
49
- 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).
50
54
 
51
55
  ## API
52
56
 
53
- 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:
54
58
 
55
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).
56
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).
@@ -65,14 +69,15 @@ See the [GitHub Webhooks documentation](https://developer.github.com/webhooks/)
65
69
  Included in the distribution is an *events.json* file which maps the event names to descriptions taken from the API:
66
70
 
67
71
  ```js
68
- var events = require('github-webhook-handler/events')
69
- Object.keys(events).forEach(function (event) {
70
- console.log(event, '=', events[event])
71
- })
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
+ }
72
77
  ```
73
78
 
74
- 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_.
75
80
 
76
81
  ## License
77
82
 
78
- **github-webhook-handler** is Copyright (c) 2014 Rod Vagg [@rvagg](https://twitter.com/rvagg) and licensed under the MIT License. All rights not explicitly granted in the MIT License are reserved. See the included [LICENSE.md](./LICENSE.md) file for more details.
83
+ **github-webhook-handler** is Copyright (c) 2014 Rod Vagg and licensed under the MIT License. All rights not explicitly granted in the MIT License are reserved. See the included [LICENSE.md](./LICENSE.md) file for more details.
@@ -1,16 +1,20 @@
1
- ///<reference types="node" />
1
+ /// <reference types="node" />
2
2
 
3
- import { IncomingMessage, ServerResponse } from "http";
4
- import { EventEmitter } from "events";
3
+ import { IncomingMessage, ServerResponse } from 'node:http'
4
+ import { EventEmitter } from 'node:events'
5
5
 
6
6
  interface CreateHandlerOptions {
7
- path: string;
8
- secret: string;
9
- events?: string | string[];
7
+ path: string
8
+ secret: string
9
+ events?: string | string[]
10
10
  }
11
11
 
12
- interface handler extends EventEmitter {
13
- (req: IncomingMessage, res: ServerResponse, callback: (err: Error) => void): void;
12
+ interface Handler extends EventEmitter {
13
+ (req: IncomingMessage, res: ServerResponse, callback: (err?: Error) => void): void
14
+ sign(data: string | Buffer): string
15
+ verify(signature: string, data: string | Buffer): boolean
14
16
  }
15
17
 
16
- export default function createHandler(options: CreateHandlerOptions): handler;
18
+ declare function createHandler (options: CreateHandlerOptions | CreateHandlerOptions[]): Handler
19
+
20
+ export default createHandler
@@ -1,29 +1,47 @@
1
- const EventEmitter = require('events').EventEmitter
2
- , inherits = require('util').inherits
3
- , crypto = require('crypto')
4
- , bl = require('bl')
5
- , bufferEq = require('buffer-equal-constant-time')
6
-
7
- function create (options) {
8
- if (typeof options != 'object')
9
- throw new TypeError('must provide an options object')
1
+ import { EventEmitter } from 'node:events'
2
+ import crypto from 'node:crypto'
3
+ import bl from 'bl'
4
+
5
+ function findHandler (url, arr) {
6
+ if (!Array.isArray(arr)) {
7
+ return arr
8
+ }
10
9
 
11
- if (typeof options.path != 'string')
12
- throw new TypeError('must provide a \'path\' option')
10
+ let ret = arr[0]
11
+ for (let i = 0; i < arr.length; i++) {
12
+ if (url === arr[i].path) {
13
+ ret = arr[i]
14
+ }
15
+ }
16
+
17
+ return ret
18
+ }
13
19
 
14
- if (typeof options.secret != 'string')
15
- throw new TypeError('must provide a \'secret\' option')
20
+ function checkType (options) {
21
+ if (typeof options !== 'object') {
22
+ throw new TypeError('must provide an options object')
23
+ }
16
24
 
17
- var events
25
+ if (typeof options.path !== 'string') {
26
+ throw new TypeError("must provide a 'path' option")
27
+ }
18
28
 
19
- if (typeof options.events == 'string' && options.events != '*')
20
- events = [ options.events ]
29
+ if (typeof options.secret !== 'string') {
30
+ throw new TypeError("must provide a 'secret' option")
31
+ }
32
+ }
21
33
 
22
- else if (Array.isArray(options.events) && options.events.indexOf('*') == -1)
23
- events = options.events
34
+ function create (initOptions) {
35
+ let options
36
+ if (Array.isArray(initOptions)) {
37
+ for (let i = 0; i < initOptions.length; i++) {
38
+ checkType(initOptions[i])
39
+ }
40
+ } else {
41
+ checkType(initOptions)
42
+ }
24
43
 
25
- // make it an EventEmitter, sort of
26
- handler.__proto__ = EventEmitter.prototype
44
+ Object.setPrototypeOf(handler, EventEmitter.prototype)
27
45
  EventEmitter.call(handler)
28
46
 
29
47
  handler.sign = sign
@@ -31,54 +49,74 @@ function create (options) {
31
49
 
32
50
  return handler
33
51
 
34
-
35
52
  function sign (data) {
36
- return 'sha1=' + crypto.createHmac('sha1', options.secret).update(data).digest('hex')
53
+ return `sha1=${crypto.createHmac('sha1', options.secret).update(data).digest('hex')}`
37
54
  }
38
55
 
39
56
  function verify (signature, data) {
40
- return bufferEq(Buffer.from(signature), Buffer.from(sign(data)))
57
+ const sig = Buffer.from(signature)
58
+ const signed = Buffer.from(sign(data))
59
+ if (sig.length !== signed.length) {
60
+ return false
61
+ }
62
+ return crypto.timingSafeEqual(sig, signed)
41
63
  }
42
64
 
43
65
  function handler (req, res, callback) {
44
- if (req.url.split('?').shift() !== options.path || req.method !== 'POST')
66
+ let events
67
+
68
+ options = findHandler(req.url, initOptions)
69
+
70
+ if (typeof options.events === 'string' && options.events !== '*') {
71
+ events = [options.events]
72
+ } else if (Array.isArray(options.events) && options.events.indexOf('*') === -1) {
73
+ events = options.events
74
+ }
75
+
76
+ if (req.url !== options.path || req.method !== 'POST') {
45
77
  return callback()
78
+ }
46
79
 
47
80
  function hasError (msg) {
48
81
  res.writeHead(400, { 'content-type': 'application/json' })
49
82
  res.end(JSON.stringify({ error: msg }))
50
83
 
51
- var err = new Error(msg)
84
+ const err = new Error(msg)
52
85
 
53
86
  handler.emit('error', err, req)
54
87
  callback(err)
55
88
  }
56
89
 
57
- var sig = req.headers['x-hub-signature']
58
- , event = req.headers['x-github-event']
59
- , id = req.headers['x-github-delivery']
90
+ const sig = req.headers['x-hub-signature']
91
+ const event = req.headers['x-github-event']
92
+ const id = req.headers['x-github-delivery']
60
93
 
61
- if (!sig)
94
+ if (!sig) {
62
95
  return hasError('No X-Hub-Signature found on request')
96
+ }
63
97
 
64
- if (!event)
98
+ if (!event) {
65
99
  return hasError('No X-Github-Event found on request')
100
+ }
66
101
 
67
- if (!id)
102
+ if (!id) {
68
103
  return hasError('No X-Github-Delivery found on request')
104
+ }
69
105
 
70
- if (events && events.indexOf(event) == -1)
106
+ if (events && events.indexOf(event) === -1) {
71
107
  return hasError('X-Github-Event is not acceptable')
108
+ }
72
109
 
73
- req.pipe(bl(function (err, data) {
110
+ req.pipe(bl((err, data) => {
74
111
  if (err) {
75
112
  return hasError(err.message)
76
113
  }
77
114
 
78
- var obj
115
+ let obj
79
116
 
80
- if (!verify(sig, data))
117
+ if (!verify(sig, data)) {
81
118
  return hasError('X-Hub-Signature does not match blob signature')
119
+ }
82
120
 
83
121
  try {
84
122
  obj = JSON.parse(data.toString())
@@ -89,13 +127,14 @@ function create (options) {
89
127
  res.writeHead(200, { 'content-type': 'application/json' })
90
128
  res.end('{"ok":true}')
91
129
 
92
- var emitData = {
93
- event : event
94
- , id : id
95
- , payload : obj
96
- , protocol: req.protocol
97
- , host : req.headers['host']
98
- , url : req.url
130
+ const emitData = {
131
+ event,
132
+ id,
133
+ payload: obj,
134
+ protocol: req.protocol,
135
+ host: req.headers.host,
136
+ url: req.url,
137
+ path: options.path
99
138
  }
100
139
 
101
140
  handler.emit(event, emitData)
@@ -104,5 +143,4 @@ function create (options) {
104
143
  }
105
144
  }
106
145
 
107
-
108
- module.exports = create
146
+ export default create
package/package.json CHANGED
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "github-webhook-handler",
3
- "version": "0.7.1",
3
+ "version": "2.0.0",
4
4
  "description": "Web handler / middleware for processing GitHub Webhooks",
5
+ "type": "module",
5
6
  "main": "github-webhook-handler.js",
7
+ "exports": "./github-webhook-handler.js",
6
8
  "types": "github-webhook-handler.d.ts",
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
7
12
  "scripts": {
8
- "test": "node test.js"
13
+ "lint": "standard",
14
+ "build": "true",
15
+ "test:unit": "node --test test.js",
16
+ "test": "npm run lint && npm run test:unit"
9
17
  },
10
18
  "keywords": [
11
19
  "github",
@@ -18,13 +26,98 @@
18
26
  },
19
27
  "license": "MIT",
20
28
  "dependencies": {
21
- "bl": "~1.1.2",
22
- "buffer-equal-constant-time": "~1.0.1"
29
+ "bl": "^6.1.6"
23
30
  },
24
31
  "devDependencies": {
25
- "@types/node": "*",
26
- "run-series": "~1.1.4",
27
- "tape": "~4.6.0",
28
- "through2": "~2.0.1"
32
+ "@semantic-release/changelog": "^6.0.3",
33
+ "@semantic-release/commit-analyzer": "^13.0.1",
34
+ "@semantic-release/git": "^10.0.1",
35
+ "@semantic-release/github": "^12.0.2",
36
+ "@semantic-release/npm": "^13.1.3",
37
+ "@semantic-release/release-notes-generator": "^14.1.0",
38
+ "conventional-changelog-conventionalcommits": "^9.1.0",
39
+ "semantic-release": "^25.0.2",
40
+ "standard": "^17.1.2"
41
+ },
42
+ "release": {
43
+ "branches": [
44
+ "master"
45
+ ],
46
+ "plugins": [
47
+ [
48
+ "@semantic-release/commit-analyzer",
49
+ {
50
+ "preset": "conventionalcommits",
51
+ "releaseRules": [
52
+ {
53
+ "breaking": true,
54
+ "release": "major"
55
+ },
56
+ {
57
+ "revert": true,
58
+ "release": "patch"
59
+ },
60
+ {
61
+ "type": "feat",
62
+ "release": "minor"
63
+ },
64
+ {
65
+ "type": "fix",
66
+ "release": "patch"
67
+ },
68
+ {
69
+ "type": "chore",
70
+ "release": "patch"
71
+ },
72
+ {
73
+ "type": "docs",
74
+ "release": "patch"
75
+ },
76
+ {
77
+ "type": "test",
78
+ "release": "patch"
79
+ },
80
+ {
81
+ "scope": "no-release",
82
+ "release": false
83
+ }
84
+ ]
85
+ }
86
+ ],
87
+ [
88
+ "@semantic-release/release-notes-generator",
89
+ {
90
+ "preset": "conventionalcommits",
91
+ "presetConfig": {
92
+ "types": [
93
+ {
94
+ "type": "feat",
95
+ "section": "Features"
96
+ },
97
+ {
98
+ "type": "fix",
99
+ "section": "Bug Fixes"
100
+ },
101
+ {
102
+ "type": "chore",
103
+ "section": "Trivial Changes"
104
+ },
105
+ {
106
+ "type": "docs",
107
+ "section": "Trivial Changes"
108
+ },
109
+ {
110
+ "type": "test",
111
+ "section": "Tests"
112
+ }
113
+ ]
114
+ }
115
+ }
116
+ ],
117
+ "@semantic-release/changelog",
118
+ "@semantic-release/npm",
119
+ "@semantic-release/github",
120
+ "@semantic-release/git"
121
+ ]
29
122
  }
30
123
  }
package/test.js CHANGED
@@ -1,333 +1,350 @@
1
- const test = require('tape')
2
- , crypto = require('crypto')
3
- , handler = require('./')
4
- , through2 = require('through2')
5
- , series = require('run-series')
6
-
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'
7
6
 
8
7
  function signBlob (key, blob) {
9
- return 'sha1=' +
10
- crypto.createHmac('sha1', key).update(blob).digest('hex')
8
+ return `sha1=${crypto.createHmac('sha1', key).update(blob).digest('hex')}`
11
9
  }
12
10
 
13
-
14
11
  function mkReq (url, method) {
15
- var req = through2()
12
+ const req = new PassThrough()
16
13
  req.method = method || 'POST'
17
14
  req.url = url
18
15
  req.headers = {
19
- 'x-hub-signature' : 'bogus'
20
- , 'x-github-event' : 'bogus'
21
- , 'x-github-delivery' : 'bogus'
16
+ 'x-hub-signature': 'bogus',
17
+ 'x-github-event': 'bogus',
18
+ 'x-github-delivery': 'bogus'
22
19
  }
23
20
  return req
24
21
  }
25
22
 
26
-
27
23
  function mkRes () {
28
- var res = {
29
- writeHead : function (statusCode, headers) {
30
- res.$statusCode = statusCode
31
- res.$headers = headers
32
- }
33
-
34
- , end : function (content) {
35
- res.$end = content
36
- }
24
+ const res = {
25
+ writeHead (statusCode, headers) {
26
+ res.$statusCode = statusCode
27
+ res.$headers = headers
28
+ },
29
+ end (content) {
30
+ res.$end = content
31
+ }
37
32
  }
38
-
39
33
  return res
40
34
  }
41
35
 
42
-
43
- test('handler without full options throws', function (t) {
44
- t.plan(4)
45
-
46
- t.equal(typeof handler, 'function', 'handler exports a function')
47
-
48
- t.throws(handler, /must provide an options object/, 'throws if no options')
49
-
50
- t.throws(handler.bind(null, {}), /must provide a 'path' option/, 'throws if no path option')
51
-
52
- 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')
53
41
  })
54
42
 
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')
46
+ })
55
47
 
56
- test('handler ignores invalid urls', function (t) {
57
- var options = { path: '/some/url', secret: 'bogus' }
58
- , h = handler(options)
59
-
60
- t.plan(6)
48
+ test('handler ignores invalid urls', async () => {
49
+ const options = { path: '/some/url', secret: 'bogus' }
50
+ const h = handler(options)
61
51
 
62
- h(mkReq('/'), mkRes(), function (err) {
63
- t.error(err)
64
- t.ok(true, 'request was ignored')
52
+ await new Promise((resolve) => {
53
+ h(mkReq('/'), mkRes(), (err) => {
54
+ assert.ifError(err)
55
+ resolve()
56
+ })
65
57
  })
66
58
 
67
- // near match
68
- h(mkReq('/some/url/'), mkRes(), function (err) {
69
- t.error(err)
70
- 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
+ })
71
64
  })
72
65
 
73
- // partial match
74
- h(mkReq('/some'), mkRes(), function (err) {
75
- t.error(err)
76
- t.ok(true, 'request was ignored')
66
+ await new Promise((resolve) => {
67
+ h(mkReq('/some'), mkRes(), (err) => {
68
+ assert.ifError(err)
69
+ resolve()
70
+ })
77
71
  })
78
72
  })
79
73
 
80
- test('handler ingores non-POST requests', function (t) {
81
- var options = { path: '/some/url', secret: 'bogus' }
82
- , h = handler(options)
74
+ test('handler ignores non-POST requests', async () => {
75
+ const options = { path: '/some/url', secret: 'bogus' }
76
+ const h = handler(options)
83
77
 
84
- t.plan(4)
85
-
86
- h(mkReq('/some/url', 'GET'), mkRes(), function (err) {
87
- t.error(err)
88
- 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
+ })
89
83
  })
90
84
 
91
- h(mkReq('/some/url?test=param', 'GET'), mkRes(), function (err) {
92
- t.error(err)
93
- 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
+ })
94
90
  })
95
91
  })
96
92
 
97
- test('handler accepts valid urls', function (t) {
98
- var options = { path: '/some/url', secret: 'bogus' }
99
- , h = handler(options)
100
-
101
- t.plan(1)
102
-
103
- h(mkReq('/some/url'), mkRes(), function (err) {
104
- t.error(err)
105
- t.fail(false, 'should not call')
106
- })
93
+ test('handler accepts valid urls', async () => {
94
+ const options = { path: '/some/url', secret: 'bogus' }
95
+ const h = handler(options)
107
96
 
108
- h(mkReq('/some/url?test=param'), mkRes(), function (err) {
109
- t.error(err)
110
- t.fail(false, 'should not call')
97
+ let callbackCalled = false
98
+ h(mkReq('/some/url'), mkRes(), () => {
99
+ callbackCalled = true
111
100
  })
112
101
 
113
- 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')
114
104
  })
115
105
 
106
+ test('handler accepts valid urls in Array', async () => {
107
+ const options = [{ path: '/some/url', secret: 'bogus' }, { path: '/someOther/url', secret: 'bogus' }]
108
+ const h = handler(options)
116
109
 
117
- test('handler can reject events', function (t) {
118
- var acceptableEvents = {
119
- 'undefined' : undefined
120
- , 'a string equal to the event' : 'bogus'
121
- , 'a string equal to *' : '*'
122
- , 'an array containing the event' : ['bogus']
123
- , 'an array containing *' : ['not-bogus', '*']
124
- }
125
- , unacceptableEvents = {
126
- 'a string not equal to the event or *' : 'not-bogus'
127
- , 'an array not containing the event or *' : ['not-bogus']
128
- }
129
- , acceptable = Object.keys(acceptableEvents)
130
- , unacceptable = Object.keys(unacceptableEvents)
131
- , acceptableTests = acceptable.map(function (events) {
132
- return acceptableReq.bind(null, events)
133
- })
134
- , unacceptableTests = unacceptable.map(function (events) {
135
- return unacceptableReq.bind(null, events)
136
- })
110
+ let callbackCalled = false
111
+ h(mkReq('/some/url'), mkRes(), () => { callbackCalled = true })
112
+ h(mkReq('/someOther/url'), mkRes(), () => { callbackCalled = true })
137
113
 
138
- t.plan(acceptable.length + unacceptable.length)
139
- series(acceptableTests.concat(unacceptableTests))
114
+ await new Promise((resolve) => setTimeout(resolve, 10))
115
+ assert.strictEqual(callbackCalled, false, 'callback should not be called for valid POSTs')
116
+ })
140
117
 
141
- function acceptableReq (events, callback) {
142
- var h = handler({
143
- path : '/some/url'
144
- , secret : 'bogus'
145
- , events : acceptableEvents[events]
146
- })
118
+ test('handler can reject events', async () => {
119
+ const acceptableEvents = {
120
+ undefined,
121
+ 'a string equal to the event': 'bogus',
122
+ 'a string equal to *': '*',
123
+ 'an array containing the event': ['bogus'],
124
+ 'an array containing *': ['not-bogus', '*']
125
+ }
126
+ const unacceptableEvents = {
127
+ 'a string not equal to the event or *': 'not-bogus',
128
+ 'an array not containing the event or *': ['not-bogus']
129
+ }
147
130
 
148
- h(mkReq('/some/url'), mkRes(), function (err) {
149
- t.error(err)
150
- t.fail(false, 'should not call')
131
+ for (const [desc, events] of Object.entries(acceptableEvents)) {
132
+ const h = handler({
133
+ path: '/some/url',
134
+ secret: 'bogus',
135
+ events
151
136
  })
152
137
 
153
- setTimeout(function () {
154
- t.ok(true, 'accepted because options.events was ' + events)
155
- callback()
156
- })
138
+ let callbackCalled = false
139
+ h(mkReq('/some/url'), mkRes(), () => { callbackCalled = true })
140
+
141
+ await new Promise((resolve) => setTimeout(resolve, 10))
142
+ assert.strictEqual(callbackCalled, false, `accepted because options.events was ${desc}`)
157
143
  }
158
144
 
159
- function unacceptableReq (events, callback) {
160
- var h = handler({
161
- path : '/some/url'
162
- , secret : 'bogus'
163
- , events : unacceptableEvents[events]
145
+ for (const [desc, events] of Object.entries(unacceptableEvents)) {
146
+ const h = handler({
147
+ path: '/some/url',
148
+ secret: 'bogus',
149
+ events
164
150
  })
165
151
 
166
- h.on('error', function () {})
152
+ h.on('error', () => {})
167
153
 
168
- h(mkReq('/some/url'), mkRes(), function (err) {
169
- t.ok(err, 'rejected because options.events was ' + events)
170
- 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
+ })
171
159
  })
172
160
  }
173
161
  })
174
162
 
163
+ test('handler is an EventEmitter', () => {
164
+ const h = handler({ path: '/', secret: 'bogus' })
175
165
 
176
- // because we don't inherit in a traditional way
177
- test('handler is an EventEmitter', function (t) {
178
- t.plan(5)
179
-
180
- var h = handler({ path: '/', secret: 'bogus' })
181
-
182
- t.equal(typeof h.on, 'function', 'has h.on()')
183
- t.equal(typeof h.emit, 'function', 'has h.emit()')
184
- t.equal(typeof h.removeListener, 'function', 'has h.removeListener()')
185
-
186
- h.on('ping', function (pong) {
187
- t.equal(pong, 'pong', 'got event')
188
- })
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()')
189
169
 
170
+ let received
171
+ h.on('ping', (pong) => { received = pong })
190
172
  h.emit('ping', 'pong')
173
+ assert.strictEqual(received, 'pong', 'got event')
191
174
 
192
- 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')
193
176
  })
194
177
 
195
-
196
- test('handler accepts a signed blob', function (t) {
197
- t.plan(4)
198
-
199
- var obj = { some: 'github', object: 'with', properties: true }
200
- , json = JSON.stringify(obj)
201
- , h = handler({ path: '/', secret: 'bogus' })
202
- , req = mkReq('/')
203
- , res = mkRes()
178
+ test('handler accepts a signed blob', async () => {
179
+ const obj = { some: 'github', object: 'with', properties: true }
180
+ const json = JSON.stringify(obj)
181
+ const h = handler({ path: '/', secret: 'bogus' })
182
+ const req = mkReq('/')
183
+ const res = mkRes()
204
184
 
205
185
  req.headers['x-hub-signature'] = signBlob('bogus', json)
206
- req.headers['x-github-event'] = 'push'
207
-
208
- h.on('push', function (event) {
209
- t.deepEqual(event, { event: 'push', id: 'bogus', payload: obj, url: '/', host: undefined, protocol: undefined })
210
- t.equal(res.$statusCode, 200, 'correct status code')
211
- t.deepEqual(res.$headers, { 'content-type': 'application/json' })
212
- t.equal(res.$end, '{"ok":true}', 'got correct content')
186
+ req.headers['x-github-event'] = 'push'
187
+
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
+ })
213
204
  })
214
205
 
215
- h(req, res, function (err) {
216
- t.error(err)
217
- t.fail(true, 'should not get here!')
206
+ h(req, res, () => {
207
+ assert.fail('should not get here')
218
208
  })
219
209
 
220
- process.nextTick(function () {
221
- req.end(json)
222
- })
210
+ process.nextTick(() => req.end(json))
211
+ await eventPromise
223
212
  })
224
213
 
225
-
226
- test('handler accepts a signed blob with alt event', function (t) {
227
- t.plan(4)
228
-
229
- var obj = { some: 'github', object: 'with', properties: true }
230
- , json = JSON.stringify(obj)
231
- , h = handler({ path: '/', secret: 'bogus' })
232
- , req = mkReq('/')
233
- , res = mkRes()
214
+ test('handler accepts multi blob in Array', async () => {
215
+ const obj = { some: 'github', object: 'with', properties: true }
216
+ const json = JSON.stringify(obj)
217
+ const h = handler([{ path: '/', secret: 'bogus' }, { path: '/some/url', secret: 'bogus' }])
218
+ const req = mkReq('/some/url')
219
+ const res = mkRes()
234
220
 
235
221
  req.headers['x-hub-signature'] = signBlob('bogus', json)
236
- req.headers['x-github-event'] = 'issue'
237
-
238
- h.on('push', function (event) {
239
- t.fail(true, 'should not get here!')
222
+ req.headers['x-github-event'] = 'push'
223
+
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
+ })
240
240
  })
241
241
 
242
- h.on('issue', function (event) {
243
- t.deepEqual(event, { event: 'issue', id: 'bogus', payload: obj, url: '/', host: undefined, protocol: undefined })
244
- t.equal(res.$statusCode, 200, 'correct status code')
245
- t.deepEqual(res.$headers, { 'content-type': 'application/json' })
246
- t.equal(res.$end, '{"ok":true}', 'got correct content')
242
+ h(req, res, () => {
243
+ assert.fail('should not get here')
247
244
  })
248
245
 
249
- h(req, res, function (err) {
250
- t.error(err)
251
- t.fail(true, 'should not get here!')
252
- })
246
+ process.nextTick(() => req.end(json))
247
+ await eventPromise
248
+ })
253
249
 
254
- process.nextTick(function () {
255
- req.end(json)
250
+ test('handler accepts a signed blob with alt event', async () => {
251
+ const obj = { some: 'github', object: 'with', properties: true }
252
+ const json = JSON.stringify(obj)
253
+ const h = handler({ path: '/', secret: 'bogus' })
254
+ const req = mkReq('/')
255
+ const res = mkRes()
256
+
257
+ req.headers['x-hub-signature'] = signBlob('bogus', json)
258
+ req.headers['x-github-event'] = 'issue'
259
+
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
+ })
256
278
  })
257
- })
258
279
 
280
+ h(req, res, () => {
281
+ assert.fail('should not get here')
282
+ })
259
283
 
260
- test('handler rejects a badly signed blob', function (t) {
261
- t.plan(6)
284
+ process.nextTick(() => req.end(json))
285
+ await eventPromise
286
+ })
262
287
 
263
- var obj = { some: 'github', object: 'with', properties: true }
264
- , json = JSON.stringify(obj)
265
- , h = handler({ path: '/', secret: 'bogus' })
266
- , req = mkReq('/')
267
- , res = mkRes()
288
+ test('handler rejects a badly signed blob', async () => {
289
+ const obj = { some: 'github', object: 'with', properties: true }
290
+ const json = JSON.stringify(obj)
291
+ const h = handler({ path: '/', secret: 'bogus' })
292
+ const req = mkReq('/')
293
+ const res = mkRes()
268
294
 
269
295
  req.headers['x-hub-signature'] = signBlob('bogus', json)
270
- // break signage by a tiny bit
271
296
  req.headers['x-hub-signature'] = '0' + req.headers['x-hub-signature'].substring(1)
272
297
 
273
- h.on('error', function (err, _req) {
274
- t.ok(err, 'got an error')
275
- t.strictEqual(_req, req, 'was given original request object')
276
- t.equal(res.$statusCode, 400, 'correct status code')
277
- t.deepEqual(res.$headers, { 'content-type': 'application/json' })
278
- 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
+ })
279
307
  })
280
308
 
281
- h.on('push', function (event) {
282
- t.fail(true, 'should not get here!')
283
- })
309
+ h.on('push', () => assert.fail('should not get here'))
284
310
 
285
- h(req, res, function (err) {
286
- t.ok(err, 'got error on callback')
311
+ h(req, res, (err) => {
312
+ assert.ok(err, 'got error on callback')
287
313
  })
288
314
 
289
- process.nextTick(function () {
290
- req.end(json)
291
- })
315
+ process.nextTick(() => req.end(json))
316
+ await errorPromise
292
317
  })
293
318
 
294
- test('handler responds on a bl error', function (t) {
295
- t.plan(4)
296
-
297
- var obj = { some: 'github', object: 'with', properties: true }
298
- , json = JSON.stringify(obj)
299
- , h = handler({ path: '/', secret: 'bogus' })
300
- , req = mkReq('/')
301
- , res = mkRes()
319
+ test('handler responds on a stream error', async () => {
320
+ const obj = { some: 'github', object: 'with', properties: true }
321
+ const json = JSON.stringify(obj)
322
+ const h = handler({ path: '/', secret: 'bogus' })
323
+ const req = mkReq('/')
324
+ const res = mkRes()
302
325
 
303
326
  req.headers['x-hub-signature'] = signBlob('bogus', json)
304
- req.headers['x-github-event'] = 'issue'
327
+ req.headers['x-github-event'] = 'issue'
305
328
 
306
- h.on('push', function (event) {
307
- t.fail(true, 'should not get here!')
308
- })
329
+ h.on('push', () => assert.fail('should not get here'))
330
+ h.on('issue', () => assert.fail('should never get here'))
309
331
 
310
- h.on('issue', function (event) {
311
- t.fail(true, 'should never get here!')
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
+ })
312
338
  })
313
339
 
314
- h.on('error', function(err) {
315
- t.ok(err, 'got an error')
316
- t.equal(res.$statusCode, 400, 'correct status code')
317
- });
318
-
319
- h(req, res, function (err) {
320
- t.ok(err)
340
+ h(req, res, (err) => {
341
+ assert.ok(err)
321
342
  })
322
343
 
323
- var end = res.end
324
- res.end = function () {
325
- t.equal(res.$statusCode, 400, 'correct status code')
326
- }
327
-
328
344
  req.write('{')
329
- process.nextTick(function() {
330
- req.emit('error', new Error('simulated explosion'))
331
- });
345
+ process.nextTick(() => {
346
+ req.destroy(new Error('simulated explosion'))
347
+ })
332
348
 
349
+ await errorPromise
333
350
  })
package/.jshintrc DELETED
@@ -1,59 +0,0 @@
1
- {
2
- "predef": [ ]
3
- , "bitwise": false
4
- , "camelcase": false
5
- , "curly": false
6
- , "eqeqeq": false
7
- , "forin": false
8
- , "immed": false
9
- , "latedef": false
10
- , "noarg": true
11
- , "noempty": true
12
- , "nonew": true
13
- , "plusplus": false
14
- , "quotmark": true
15
- , "regexp": false
16
- , "undef": true
17
- , "unused": true
18
- , "strict": false
19
- , "trailing": true
20
- , "maxlen": 120
21
- , "asi": true
22
- , "boss": true
23
- , "debug": true
24
- , "eqnull": true
25
- , "esnext": true
26
- , "evil": true
27
- , "expr": true
28
- , "funcscope": false
29
- , "globalstrict": false
30
- , "iterator": false
31
- , "lastsemic": true
32
- , "laxbreak": true
33
- , "laxcomma": true
34
- , "loopfunc": true
35
- , "multistr": false
36
- , "onecase": false
37
- , "proto": false
38
- , "regexdash": false
39
- , "scripturl": true
40
- , "smarttabs": false
41
- , "shadow": false
42
- , "sub": true
43
- , "supernew": false
44
- , "validthis": true
45
- , "browser": true
46
- , "couch": false
47
- , "devel": false
48
- , "dojo": false
49
- , "mootools": false
50
- , "node": true
51
- , "nonstandard": true
52
- , "prototypejs": false
53
- , "rhino": false
54
- , "worker": true
55
- , "wsh": false
56
- , "nomen": false
57
- , "onevar": false
58
- , "passfail": false
59
- }