From 024e3eef51df394c7ce1006f59aa803417cdc345 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Wed, 14 Apr 2021 01:18:22 -0700 Subject: [PATCH] feat: v9 (#533) BREAKING CHANGE: `createWebhooksApi()` has been removed. Use `new Webhooks()` instead BREAKING CHANGE: `webhooks.middleware` has been removed. Use `createNodeMiddleware()` instead BREAKING CHANGE: `createMiddleware` has been removed. Use `createNodeMiddleware()` instead BREAKING CHANGE: deprecated `path` option for `Webhooks` constructor has been removed. Use `createNodeMiddleware(webhooks, { path })` instead BREAKING CHANGE: all usage of [`debug`](https://www.npmjs.com/package/debug) has been removed. Use the `log` option instead BREAKING CHANGE: `webhooks.sign` now default to `sha256` algorithm. In order to continue to use `sha1`, replace ```js webhooks.sign(secret, payload) ``` with ```js webhooks.sign({ secret, algorith: "sha1" }, payload) ``` BREAKING CHANGE: `webhooks.sign()` and `webhooks.verify()` are now asynchronous BREAKING CHANGE: static `sign` and `verify` methods are no longer exported. Use `@octokit/webhooks-methods` package instead --- README.md | 6 +- package-lock.json | 66 ++- package.json | 9 +- src/index.ts | 65 +-- src/middleware-legacy/README.md | 21 - src/middleware-legacy/get-missing-headers.ts | 12 - src/middleware-legacy/get-payload.ts | 34 -- src/middleware-legacy/index.ts | 26 -- src/middleware-legacy/isnt-webhook.ts | 25 -- src/middleware-legacy/middleware.ts | 86 ---- src/middleware/node/get-missing-headers.ts | 5 +- src/middleware/node/get-payload.ts | 19 +- src/middleware/node/middleware.ts | 6 +- .../node/on-unhandled-request-default.ts | 6 +- src/middleware/node/types.ts | 6 +- src/sign/README.md | 56 --- src/sign/index.ts | 46 -- src/types.ts | 1 - .../verify-and-receive.ts | 15 +- src/verify/README.md | 56 --- src/verify/index.ts | 35 -- test/integration/middleware-test.ts | 76 ---- test/integration/node-middleware.test.ts | 16 +- test/integration/server-test.ts | 420 ------------------ test/integration/sign-test.ts | 90 ---- test/integration/smoke-test.ts | 36 +- test/integration/verify-test.ts | 115 ----- test/integration/webhooks.test.ts | 39 ++ test/typescript-validate.ts | 23 +- test/unit/deprecation.test.ts | 68 +-- test/unit/middleware-constructor-test.ts | 6 - test/unit/middleware-test.ts | 111 ----- 32 files changed, 161 insertions(+), 1440 deletions(-) delete mode 100644 src/middleware-legacy/README.md delete mode 100644 src/middleware-legacy/get-missing-headers.ts delete mode 100644 src/middleware-legacy/get-payload.ts delete mode 100644 src/middleware-legacy/index.ts delete mode 100644 src/middleware-legacy/isnt-webhook.ts delete mode 100644 src/middleware-legacy/middleware.ts delete mode 100644 src/sign/README.md delete mode 100644 src/sign/index.ts rename src/{middleware-legacy => }/verify-and-receive.ts (65%) delete mode 100644 src/verify/README.md delete mode 100644 src/verify/index.ts delete mode 100644 test/integration/middleware-test.ts delete mode 100644 test/integration/server-test.ts delete mode 100644 test/integration/sign-test.ts delete mode 100644 test/integration/verify-test.ts create mode 100644 test/integration/webhooks.test.ts delete mode 100644 test/unit/middleware-constructor-test.ts delete mode 100644 test/unit/middleware-test.ts diff --git a/README.md b/README.md index cbc88d2b..098fb191 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ webhooks.sign(eventPayload); Returns a `signature` string. Throws error if `eventPayload` is not passed. -Can also be used [standalone](src/sign/). +The `sign` method can be imported as static method from [`@octokit/webhooks-methods`](https://github.com/octokit/webhooks-methods.js/#readme). ### webhooks.verify() @@ -216,7 +216,7 @@ webhooks.verify(eventPayload, signature); Returns `true` or `false`. Throws error if `eventPayload` or `signature` not passed. -Can also be used [standalone](src/verify/). +The `verify` method can be imported as static method from [`@octokit/webhooks-methods`](https://github.com/octokit/webhooks-methods.js/#readme). ### webhooks.verifyAndReceive() @@ -306,7 +306,7 @@ eventHandler id: request.headers["x-github-delivery"], name: request.headers["x-github-event"], payload: request.body, - signature: request.headers["x-hub-signature"], + signature: request.headers["x-hub-signature-256"], }) .catch(handleErrorsFromHooks); ``` diff --git a/package-lock.json b/package-lock.json index 62a80cf7..1cfc0795 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1630,6 +1630,11 @@ "resolved": "https://registry.npmjs.org/@octokit/webhooks-definitions/-/webhooks-definitions-3.67.3.tgz", "integrity": "sha512-do4Z1r2OVhuI0ihJhQ8Hg+yPWnBYEBNuFNCrvtPKoYT1w81jD7pBXgGe86lYuuNirkDHb0Nxt+zt4O5GiFJfgA==" }, + "@octokit/webhooks-methods": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-1.0.0.tgz", + "integrity": "sha512-pVceMQcj9SZ5p2RkemL0TuuPdGULNQj9F3Pq1cNM1xH+Kst1VNt0dj3PEGZRZV473njrDnYdi/OG4wWY9TLbbA==" + }, "@pika/babel-plugin-esm-import-rewrite": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/@pika/babel-plugin-esm-import-rewrite/-/babel-plugin-esm-import-rewrite-0.6.1.tgz", @@ -1804,6 +1809,31 @@ } } }, + "@pika/plugin-build-web": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@pika/plugin-build-web/-/plugin-build-web-0.9.2.tgz", + "integrity": "sha512-HjN/3P5c5jVEaPo9ZSs+jn9osrNs3JW3ZgP11NSfS0QQEMIw+Ks+qXi5Aymp8WKLpoldtZw/z69+SrfqlNeZyg==", + "dev": true, + "requires": { + "@pika/types": "^0.9.2", + "@types/node": "^10.12.18", + "rollup": "^1.1.0" + }, + "dependencies": { + "@pika/types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@pika/types/-/types-0.9.2.tgz", + "integrity": "sha512-AzZTkHtM0A67+xMVhmSeJDteSMS+RfXGuM+/oVbo1PGD19ic7fuimv5b0TW8dKoZuxpVxiwVAai+sFRSNmfI3g==", + "dev": true + }, + "@types/node": { + "version": "10.17.56", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.56.tgz", + "integrity": "sha512-LuAa6t1t0Bfw4CuSR0UITsm1hP17YL+u82kfHGrHUWdhlBtH7sa7jGY5z7glGaIj/WDYDkRtgGd+KCjCzxBW1w==", + "dev": true + } + } + }, "@pika/plugin-ts-standard-pkg": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@pika/plugin-ts-standard-pkg/-/plugin-ts-standard-pkg-0.9.2.tgz", @@ -2123,12 +2153,6 @@ "@babel/types": "^7.3.0" } }, - "@types/debug": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", - "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", - "dev": true - }, "@types/estree": { "version": "0.0.47", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.47.tgz", @@ -2934,16 +2958,16 @@ "dev": true }, "browserslist": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz", - "integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.4.tgz", + "integrity": "sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001181", - "colorette": "^1.2.1", - "electron-to-chromium": "^1.3.649", + "caniuse-lite": "^1.0.30001208", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.712", "escalade": "^3.1.1", - "node-releases": "^1.1.70" + "node-releases": "^1.1.71" } }, "bs-logger": { @@ -3890,6 +3914,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -4176,9 +4201,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.712", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.712.tgz", - "integrity": "sha512-3kRVibBeCM4vsgoHHGKHmPocLqtFAGTrebXxxtgKs87hNUzXrX2NuS3jnBys7IozCnw7viQlozxKkmty2KNfrw==", + "version": "1.3.715", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.715.tgz", + "integrity": "sha512-VCWxo9RqTYhcCsHtG+l0TEOS6H5QmO1JyVCQB9nv8fllmAzj1VcCYH3qBCXP75/En6FeoepefnogLPE+5W7OiQ==", "dev": true }, "elegant-spinner": { @@ -6591,9 +6616,9 @@ }, "dependencies": { "acorn": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.0.tgz", - "integrity": "sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.1.tgz", + "integrity": "sha512-xYiIVjNuqtKXMxlRMDc6mZUhXehod4a3gbZ1qRlM7icK4EbxUFNLhWoPblCvFtB2Y9CIqHP3CF/rdxLItaQv8g==", "dev": true } } @@ -7504,7 +7529,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "mute-stream": { "version": "0.0.8", diff --git a/package.json b/package.json index 5e0b2f08..e0566e3e 100644 --- a/package.json +++ b/package.json @@ -20,16 +20,16 @@ "dependencies": { "@octokit/request-error": "^2.0.2", "@octokit/webhooks-definitions": "3.67.3", - "aggregate-error": "^3.1.0", - "debug": "^4.0.0" + "@octokit/webhooks-methods": "^1.0.0", + "aggregate-error": "^3.1.0" }, "devDependencies": { "@jest/types": "^26.6.2", "@octokit/tsconfig": "^1.0.1", "@pika/pack": "^0.5.0", "@pika/plugin-build-node": "^0.9.2", + "@pika/plugin-build-web": "^0.9.2", "@pika/plugin-ts-standard-pkg": "^0.9.2", - "@types/debug": "^4.1.5", "@types/jest": "^26.0.9", "@types/json-schema": "^7.0.7", "@types/node": "^14.0.14", @@ -57,6 +57,9 @@ ], [ "@pika/plugin-build-node" + ], + [ + "@pika/plugin-build-web" ] ] }, diff --git a/src/index.ts b/src/index.ts index 4d378c09..b8e1da5b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,8 @@ -import { IncomingMessage, ServerResponse } from "http"; +import { sign, verify } from "@octokit/webhooks-methods"; + import { createLogger } from "./createLogger"; import { createEventHandler } from "./event-handler/index"; -import { createMiddleware } from "./middleware-legacy/index"; -import { middleware } from "./middleware-legacy/middleware"; -import { verifyAndReceive } from "./middleware-legacy/verify-and-receive"; -import { sign } from "./sign/index"; +import { verifyAndReceive } from "./verify-and-receive"; import { EmitterWebhookEvent, EmitterWebhookEventName, @@ -14,14 +12,16 @@ import { WebhookError, WebhookEventHandlerError, } from "./types"; -import { verify } from "./verify/index"; export { createNodeMiddleware } from "./middleware/node/index"; // U holds the return value of `transform` function in Options class Webhooks { - public sign: (payload: string | object) => string; - public verify: (eventPayload: string | object, signature: string) => boolean; + public sign: (payload: string | object) => Promise; + public verify: ( + eventPayload: string | object, + signature: string + ) => Promise; public on: ( event: E | E[], callback: HandlerFunction @@ -37,15 +37,6 @@ class Webhooks { options: EmitterWebhookEvent & { signature: string } ) => Promise; - /** - * @deprecated use `createNodeMiddleware(webhooks)` instead - */ - public middleware: ( - request: IncomingMessage, - response: ServerResponse, - next?: (err?: any) => void - ) => void | Promise; - constructor(options: Options & { secret: string }) { if (!options || !options.secret) { throw new Error("[@octokit/webhooks] options.secret required"); @@ -53,18 +44,11 @@ class Webhooks { const state: State & { secret: string } = { eventHandler: createEventHandler(options), - path: options.path || "/", secret: options.secret, hooks: {}, log: createLogger(options.log), }; - if ("path" in options) { - state.log.warn( - "[@octokit/webhooks] `path` option is deprecated and will be removed in a future release of `@octokit/webhooks`. Please use `createNodeMiddleware(webhooks, { path })` instead" - ); - } - this.sign = sign.bind(null, options.secret); this.verify = verify.bind(null, options.secret); this.on = state.eventHandler.on; @@ -73,38 +57,7 @@ class Webhooks { this.removeListener = state.eventHandler.removeListener; this.receive = state.eventHandler.receive; this.verifyAndReceive = verifyAndReceive.bind(null, state); - - this.middleware = function deprecatedMiddleware( - request: IncomingMessage, - response: ServerResponse, - next?: (err?: any) => void - ) { - state.log.warn( - "[@octokit/webhooks] `webhooks.middleware` is deprecated and will be removed in a future release of `@octokit/webhooks`. Please use `createNodeMiddleware(webhooks)` instead" - ); - return middleware(state, request, response, next); - }; } } -/** @deprecated `createWebhooksApi()` is deprecated and will be removed in a future release of `@octokit/webhooks`, please use the `Webhooks` class instead */ -const createWebhooksApi = ( - options: Options & { secret: string } -) => { - const log = createLogger(options.log); - log.warn( - "[@octokit/webhooks] `createWebhooksApi()` is deprecated and will be removed in a future release of `@octokit/webhooks`, please use the `Webhooks` class instead" - ); - return new Webhooks(options); -}; - -export { - createEventHandler, - createMiddleware, - createWebhooksApi, - Webhooks, - EmitterWebhookEvent, - WebhookError, - sign, - verify, -}; +export { createEventHandler, Webhooks, EmitterWebhookEvent, WebhookError }; diff --git a/src/middleware-legacy/README.md b/src/middleware-legacy/README.md deleted file mode 100644 index b7478dbc..00000000 --- a/src/middleware-legacy/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# middleware - -If you only need the middleware with access to the `.sign()`, `.verify()` or the receiver’s `.receive()` method, you can use the webhooks middleware directly - -```js -const { createMiddleware } = require("@octokit/webhooks"); -const middleware = createMiddleware({ - secret: "mysecret", - path: "/github-webhooks", -}); - -middleware.on("installation", asyncInstallationHook); - -require("http").createServer(middleware).listen(3000); -``` - -## API - -The `middleware` API implements [`.on()`](../../README.md#webhookson) and [`.removeListener()`](../../README.md#webhooksremovelistener). - -Back to [@octokit/webhooks README](../../README.md). diff --git a/src/middleware-legacy/get-missing-headers.ts b/src/middleware-legacy/get-missing-headers.ts deleted file mode 100644 index dca3a5d8..00000000 --- a/src/middleware-legacy/get-missing-headers.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { IncomingMessage } from "http"; - -const WEBHOOK_HEADERS = [ - "x-github-event", - "x-hub-signature", - "x-github-delivery", -]; - -// https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#delivery-headers -export function getMissingHeaders(request: IncomingMessage) { - return WEBHOOK_HEADERS.filter((header) => !(header in request.headers)); -} diff --git a/src/middleware-legacy/get-payload.ts b/src/middleware-legacy/get-payload.ts deleted file mode 100644 index 34c2b721..00000000 --- a/src/middleware-legacy/get-payload.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { WebhookEvent } from "@octokit/webhooks-definitions/schema"; -// @ts-ignore to address #245 -import AggregateError from "aggregate-error"; -import { IncomingMessage } from "http"; - -declare module "http" { - interface IncomingMessage { - body?: WebhookEvent | unknown; - } -} - -export function getPayload(request: IncomingMessage): Promise { - // If request.body already exists we can stop here - // See https://github.com/octokit/webhooks.js/pull/23 - - if (request.body) return Promise.resolve(request.body as WebhookEvent); - - return new Promise((resolve, reject) => { - let data = ""; - - request.setEncoding("utf8"); - request.on("error", (error) => reject(new AggregateError([error]))); - request.on("data", (chunk) => (data += chunk)); - request.on("end", () => { - try { - resolve(JSON.parse(data)); - } catch (error) { - error.message = "Invalid JSON"; - error.status = 400; - reject(new AggregateError([error])); - } - }); - }); -} diff --git a/src/middleware-legacy/index.ts b/src/middleware-legacy/index.ts deleted file mode 100644 index 9e9d6be2..00000000 --- a/src/middleware-legacy/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { debug } from "debug"; -import { createLogger } from "../createLogger"; -import { createEventHandler } from "../event-handler/index"; -import { middleware } from "./middleware"; -import { Options, State } from "../types"; - -export function createMiddleware(options: Options & { secret: string }) { - if (!options || !options.secret) { - throw new Error("[@octokit/webhooks] options.secret required"); - } - - const state: State & { secret: string } = { - eventHandler: createEventHandler(options), - path: options.path || "/", - secret: options.secret, - hooks: {}, - log: createLogger(options.log || { debug: debug("webhooks:receiver") }), - }; - - const api: any = middleware.bind(null, state); - - api.on = state.eventHandler.on; - api.removeListener = state.eventHandler.removeListener; - - return api; -} diff --git a/src/middleware-legacy/isnt-webhook.ts b/src/middleware-legacy/isnt-webhook.ts deleted file mode 100644 index 0696cc2e..00000000 --- a/src/middleware-legacy/isnt-webhook.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Example webhook event request: -// https://developer.github.com/webhooks/#example-delivery -import { IncomingMessage } from "http"; - -export function isntWebhook( - request: IncomingMessage, - options: { path?: string } -) { - // GitHub sends all events as POST requests - if (request.method !== "POST") { - return true; - } - - // We must match the configured path to allow custom POST routes which include - // the webhook route. For example if the webhook route is / then it would be - // impossible to define a `POST /my/custom/app` route as the `POST /`. - if ( - typeof request.url !== "string" || - request.url.split("?")[0] !== options.path - ) { - return true; - } - - return false; -} diff --git a/src/middleware-legacy/middleware.ts b/src/middleware-legacy/middleware.ts deleted file mode 100644 index 920a745d..00000000 --- a/src/middleware-legacy/middleware.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { WebhookEventName } from "@octokit/webhooks-definitions/schema"; -import { isntWebhook } from "./isnt-webhook"; -import { getMissingHeaders } from "./get-missing-headers"; -import { getPayload } from "./get-payload"; -import { verifyAndReceive } from "./verify-and-receive"; -import { IncomingMessage, ServerResponse } from "http"; -import { State, WebhookEventHandlerError } from "../types"; - -export function middleware( - state: State & { secret: string }, - request: IncomingMessage, - response: ServerResponse, - next?: Function -): Promise | undefined { - if (isntWebhook(request, { path: state.path })) { - // the next callback is set when used as an express middleware. That allows - // it to define custom routes like /my/custom/page while the webhooks are - // expected to be sent to the / root path. Otherwise the root path would - // match all requests and would make it impossible to define custom routes - if (next) { - next(); - return; - } - - state.log.debug(`ignored: ${request.method} ${request.url}`); - response.statusCode = 404; - response.end("Not found"); - return; - } - - const missingHeaders = getMissingHeaders(request).join(", "); - if (missingHeaders) { - const error = new Error( - `[@octokit/webhooks] Required headers missing: ${missingHeaders}` - ); - - return state.eventHandler.receive(error).catch(() => { - response.statusCode = 400; - response.end(error.message); - }); - } - - const eventName = request.headers["x-github-event"] as WebhookEventName; - const signatureSHA1 = request.headers["x-hub-signature"] as string; - const signatureSHA256 = request.headers["x-hub-signature-256"] as string; - const id = request.headers["x-github-delivery"] as string; - - state.log.debug(`${eventName} event received (id: ${id})`); - - // GitHub will abort the request if it does not receive a response within 10s - // See https://github.com/octokit/webhooks.js/issues/185 - let didTimeout = false; - const timeout = setTimeout(() => { - didTimeout = true; - response.statusCode = 202; - response.end("still processing\n"); - }, 9000).unref(); - - return getPayload(request) - .then((payload) => { - return verifyAndReceive(state, { - id: id, - name: eventName as any, - payload: payload as any, - signature: signatureSHA256 || signatureSHA1, - }); - }) - - .then(() => { - clearTimeout(timeout); - - if (didTimeout) return; - - response.end("ok\n"); - }) - - .catch((error: WebhookEventHandlerError) => { - clearTimeout(timeout); - - if (didTimeout) return; - - const statusCode = Array.from(error)[0].status; - response.statusCode = statusCode || 500; - response.end(error.toString()); - }); -} diff --git a/src/middleware/node/get-missing-headers.ts b/src/middleware/node/get-missing-headers.ts index 6b25de82..d7d878d7 100644 --- a/src/middleware/node/get-missing-headers.ts +++ b/src/middleware/node/get-missing-headers.ts @@ -1,4 +1,7 @@ -import { IncomingMessage } from "http"; +// remove type imports from http for Deno compatibility +// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886 +// import { IncomingMessage } from "http"; +type IncomingMessage = any; const WEBHOOK_HEADERS = [ "x-github-event", diff --git a/src/middleware/node/get-payload.ts b/src/middleware/node/get-payload.ts index bef78e9c..4b6bfd18 100644 --- a/src/middleware/node/get-payload.ts +++ b/src/middleware/node/get-payload.ts @@ -1,13 +1,16 @@ import { WebhookEvent } from "@octokit/webhooks-definitions/schema"; // @ts-ignore to address #245 import AggregateError from "aggregate-error"; -import { IncomingMessage } from "http"; -declare module "http" { - interface IncomingMessage { - body?: WebhookEvent | unknown; - } -} +// remove type imports from http for Deno compatibility +// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886 +// import { IncomingMessage } from "http"; +// declare module "http" { +// interface IncomingMessage { +// body?: WebhookEvent | unknown; +// } +// } +type IncomingMessage = any; export function getPayload(request: IncomingMessage): Promise { // If request.body already exists we can stop here @@ -21,8 +24,8 @@ export function getPayload(request: IncomingMessage): Promise { request.setEncoding("utf8"); // istanbul ignore next - request.on("error", (error) => reject(new AggregateError([error]))); - request.on("data", (chunk) => (data += chunk)); + request.on("error", (error: Error) => reject(new AggregateError([error]))); + request.on("data", (chunk: string) => (data += chunk)); request.on("end", () => { try { resolve(JSON.parse(data)); diff --git a/src/middleware/node/middleware.ts b/src/middleware/node/middleware.ts index 106e95d4..f6dab98f 100644 --- a/src/middleware/node/middleware.ts +++ b/src/middleware/node/middleware.ts @@ -1,4 +1,8 @@ -import { IncomingMessage, ServerResponse } from "http"; +// remove type imports from http for Deno compatibility +// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886 +// import { IncomingMessage, ServerResponse } from "http"; +type IncomingMessage = any; +type ServerResponse = any; import { WebhookEventName } from "@octokit/webhooks-definitions/schema"; diff --git a/src/middleware/node/on-unhandled-request-default.ts b/src/middleware/node/on-unhandled-request-default.ts index c32ef7aa..3664190f 100644 --- a/src/middleware/node/on-unhandled-request-default.ts +++ b/src/middleware/node/on-unhandled-request-default.ts @@ -1,4 +1,8 @@ -import { IncomingMessage, ServerResponse } from "http"; +// remove type imports from http for Deno compatibility +// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886 +// import { IncomingMessage, ServerResponse } from "http"; +type IncomingMessage = any; +type ServerResponse = any; export function onUnhandledRequestDefault( request: IncomingMessage, diff --git a/src/middleware/node/types.ts b/src/middleware/node/types.ts index c114ce22..81c4e0ed 100644 --- a/src/middleware/node/types.ts +++ b/src/middleware/node/types.ts @@ -1,4 +1,8 @@ -import { IncomingMessage, ServerResponse } from "http"; +// remove type imports from http for Deno compatibility +// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886 +// import { IncomingMessage, ServerResponse } from "http"; +type IncomingMessage = any; +type ServerResponse = any; import { Logger } from "../../createLogger"; diff --git a/src/sign/README.md b/src/sign/README.md deleted file mode 100644 index 2a60cf35..00000000 --- a/src/sign/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# sign - -The `sign` method can be used as a standalone method. - -```js -const { sign } = require('@octokit/webhooks') -const signature = sign(secret, eventPayload) -const signature = sign({ secret, algorithm }, eventPayload) -// string like "sha1=d03207e4b030cf234e3447bac4d93add4c6643d8" -``` - - - - - - - - - - - - - - -
- - secret - - (String) - - Required. - Secret as configured in GitHub Settings. -
- - algorithm - - - (String) - - - Algorithm to calculate signature. Can be set to `sha1` or `sha256`. Defaults to `sha1`. -
- - eventPayload - - - (Object) - - - Required. - Webhook request payload as received from GitHub -
- -Returns a `signature` string. Throws error if required arguments are not passed. - -Back to [@octokit/webhooks README](../../README.md). diff --git a/src/sign/index.ts b/src/sign/index.ts deleted file mode 100644 index ec1d8574..00000000 --- a/src/sign/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { createHmac } from "crypto"; - -export enum Algorithm { - SHA1 = "sha1", - SHA256 = "sha256", -} - -type SignOptions = { - secret: string; - algorithm?: Algorithm | "sha1" | "sha256"; -}; - -export function sign( - options: SignOptions | string, - payload: string | object -): string { - const { secret, algorithm } = - typeof options === "string" - ? { secret: options, algorithm: Algorithm.SHA1 } - : { - secret: options.secret, - algorithm: options.algorithm || Algorithm.SHA1, - }; - - if (!secret || !payload) { - throw new TypeError("[@octokit/webhooks] secret & payload required"); - } - - if (!Object.values(Algorithm).includes(algorithm as Algorithm)) { - throw new TypeError( - `[@octokit/webhooks] Algorithm ${algorithm} is not supported. Must be 'sha1' or 'sha256'` - ); - } - - payload = - typeof payload === "string" ? payload : toNormalizedJsonString(payload); - return `${algorithm}=${createHmac(algorithm, secret) - .update(payload) - .digest("hex")}`; -} - -function toNormalizedJsonString(payload: object) { - return JSON.stringify(payload).replace(/[^\\]\\u[\da-f]{4}/g, (s) => { - return s.substr(0, 3) + s.substr(3).toUpperCase(); - }); -} diff --git a/src/types.ts b/src/types.ts index 7e3bd72e..6a556ca7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,7 +22,6 @@ interface BaseWebhookEvent { } export interface Options { - path?: string; secret?: string; transform?: TransformMethod; log?: Partial; diff --git a/src/middleware-legacy/verify-and-receive.ts b/src/verify-and-receive.ts similarity index 65% rename from src/middleware-legacy/verify-and-receive.ts rename to src/verify-and-receive.ts index 62039a94..f2ae5d0e 100644 --- a/src/middleware-legacy/verify-and-receive.ts +++ b/src/verify-and-receive.ts @@ -1,12 +1,17 @@ -import { EmitterWebhookEvent, State } from "../types"; -import { verify } from "../verify/index"; +import { verify } from "@octokit/webhooks-methods"; -export function verifyAndReceive( +import { EmitterWebhookEvent, State } from "./types"; + +export async function verifyAndReceive( state: State & { secret: string }, event: EmitterWebhookEvent & { signature: string } -): any { +): Promise { // verify will validate that the secret is not undefined - const matchesSignature = verify(state.secret, event.payload, event.signature); + const matchesSignature = await verify( + state.secret, + event.payload, + event.signature + ); if (!matchesSignature) { const error = new Error( diff --git a/src/verify/README.md b/src/verify/README.md deleted file mode 100644 index 6badc608..00000000 --- a/src/verify/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# verify - -The `verify` method can be used as a standalone method. - -```js -const { verify } = require('@octokit/webhooks') -const matchesSignature = verify(secret, eventPayload, signature) -// true or false -``` - - - - - - - - - - - - - - -
- - secret - - (String) - - Required. - Secret as configured in GitHub Settings. -
- - eventPayload - - - (Object) - - - Required. - Webhook request payload as received from GitHub -
- - signature - - - (String) - - - Required. - Signature string as calculated by sign(). -
- -Returns `true` or `false`. Throws error if `secret`, `eventPayload` or `signature` not passed. - -Back to [@octokit/webhooks README](../../README.md). diff --git a/src/verify/index.ts b/src/verify/index.ts deleted file mode 100644 index 6145e6ad..00000000 --- a/src/verify/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { timingSafeEqual } from "crypto"; -import { Buffer } from "buffer"; -import { sign } from "../sign/index"; - -const getAlgorithm = (signature: string) => { - return signature.startsWith("sha256=") ? "sha256" : "sha1"; -}; - -export function verify( - secret: string, - eventPayload: string | object, - signature: string -): boolean { - if (!secret || !eventPayload || !signature) { - throw new TypeError( - "[@octokit/webhooks] secret, eventPayload & signature required" - ); - } - - const signatureBuffer = Buffer.from(signature); - const algorithm = getAlgorithm(signature); - - const verificationBuffer = Buffer.from( - sign({ secret, algorithm }, eventPayload) - ); - - if (signatureBuffer.length !== verificationBuffer.length) { - return false; - } - - // constant time comparison to prevent timing attachs - // https://stackoverflow.com/a/31096242/206879 - // https://en.wikipedia.org/wiki/Timing_attack - return timingSafeEqual(signatureBuffer, verificationBuffer); -} diff --git a/test/integration/middleware-test.ts b/test/integration/middleware-test.ts deleted file mode 100644 index 51e5b715..00000000 --- a/test/integration/middleware-test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { EventEmitter } from "events"; -import { Buffer } from "buffer"; -import { createMiddleware } from "../../src/middleware-legacy"; - -enum RequestMethodType { - POST = "POST", - GET = "GET", -} - -type RequestMock = EventEmitter & { - method: RequestMethodType; - headers: { [key: string]: string }; - url: string; - setEncoding: jest.Mock; -}; - -const headers = { - "x-github-delivery": "123e4567-e89b-12d3-a456-426655440000", - "x-github-event": "push", - "x-hub-signature": "sha1=f4d795e69b5d03c139cc6ea991ad3e5762d13e2f", -}; - -test("Invalid payload", () => { - const requestMock: RequestMock = Object.assign(new EventEmitter(), { - method: RequestMethodType.POST, - headers, - url: "/", - setEncoding: jest.fn(), - }); - - const responseMock = { - end: jest.fn(), - statusCode: 0, - }; - - const middleware = createMiddleware({ secret: "mysecret", log: {} }); - const middlewareDone = middleware(requestMock, responseMock).then(() => { - expect(responseMock.statusCode).toBe(400); - expect(responseMock.end).toHaveBeenCalledWith( - expect.stringContaining("SyntaxError: Invalid JSON") - ); - expect(requestMock.setEncoding).toHaveBeenCalledWith("utf8"); - }); - - requestMock.emit("data", Buffer.from("foo")); - requestMock.emit("end"); - - return middlewareDone; -}); - -test("request error", () => { - const requestMock: RequestMock = Object.assign(new EventEmitter(), { - method: RequestMethodType.POST, - headers, - url: "/", - setEncoding: jest.fn(), - }); - - const responseMock = { - end: jest.fn(), - statusCode: 0, - }; - - const middleware = createMiddleware({ secret: "mysecret", log: {} }); - const middlewareDone = middleware(requestMock, responseMock).then(() => { - expect(responseMock.statusCode).toBe(500); - expect(responseMock.end).toHaveBeenCalledWith( - expect.stringContaining("Error: oops") - ); - expect(requestMock.setEncoding).toHaveBeenCalledWith("utf8"); - }); - - const error = new Error("oops"); - requestMock.emit("error", error); - return middlewareDone; -}); diff --git a/test/integration/node-middleware.test.ts b/test/integration/node-middleware.test.ts index fcf9ae18..189d1f79 100644 --- a/test/integration/node-middleware.test.ts +++ b/test/integration/node-middleware.test.ts @@ -1,19 +1,24 @@ import { createServer } from "http"; import fetch from "node-fetch"; +import { sign } from "@octokit/webhooks-methods"; // import without types const express = require("express"); -import { Webhooks, createNodeMiddleware, sign } from "../../src"; +import { Webhooks, createNodeMiddleware } from "../../src"; import { pushEventPayload } from "../fixtures"; -const signatureSha256 = sign( - { secret: "mySecret", algorithm: "sha256" }, - JSON.stringify(pushEventPayload) -); +let signatureSha256: string; describe("createNodeMiddleware(webhooks)", () => { + beforeAll(async () => { + signatureSha256 = await sign( + { secret: "mySecret", algorithm: "sha256" }, + JSON.stringify(pushEventPayload) + ); + }); + test("README example", async () => { expect.assertions(3); @@ -61,6 +66,7 @@ describe("createNodeMiddleware(webhooks)", () => { const server = createServer((req, res) => { req.once("data", (chunk) => dataChunks.push(chunk)); req.once("end", () => { + // @ts-expect-error - TS2339: Property 'body' does not exist on type 'IncomingMessage'. req.body = JSON.parse(Buffer.concat(dataChunks).toString()); middleware(req, res); }); diff --git a/test/integration/server-test.ts b/test/integration/server-test.ts deleted file mode 100644 index 2c979551..00000000 --- a/test/integration/server-test.ts +++ /dev/null @@ -1,420 +0,0 @@ -import http from "http"; - -import axios, { AxiosError, AxiosResponse } from "axios"; -import getPort from "get-port"; -import { promisify } from "util"; -import { Webhooks } from "../../src"; -import { pushEventPayload } from "../fixtures"; - -const signatureSha1 = "sha1=c171163ec36d7639f1aa9aa8cd0dc26102bfa3fc"; -const signatureSha256 = - "sha256=7962cbf5a0aea311ea57976a0f379c40cbf917701c71ef906a4e7f05794ead44"; - -describe("server-test", () => { - let availablePort: number; - - beforeEach(() => { - jest.useFakeTimers(); - - return getPort().then((port) => { - availablePort = port; - }); - }); - - test("initialised without options", () => { - // @ts-expect-error - expect(() => new Webhooks()).toThrow(); - }); - - test("GET /", (t) => { - const api = new Webhooks({ - secret: "mysecret", - log: { - warn: () => {}, - }, - }); - const server = http.createServer(api.middleware); - - const promisifiedServer = <(port: number) => Promise>( - promisify(server.listen.bind(server)) - ); - - promisifiedServer(availablePort) - .then(() => { - return axios.get(`http://localhost:${availablePort}`); - }) - - .then(() => { - t.fail("should return a 404"); - }) - .catch((error: AxiosError) => { - error.response && expect(error.response.status).toBe(404); - }) - .finally(() => server.close(t)); - }); - - test("POST / with push event payload with wrong sha1 but right sha256", (t) => { - expect.assertions(2); - - const api = new Webhooks({ - secret: "mysecret", - log: { - warn: () => {}, - }, - }); - const server = http.createServer(api.middleware); - - api.on("push", (event) => { - expect(event.id).toBe("123e4567-e89b-12d3-a456-426655440000"); - }); - - const promisifiedServer = <(port: number) => Promise>( - promisify(server.listen.bind(server)) - ); - - promisifiedServer(availablePort) - .then(() => { - return axios.post( - `http://localhost:${availablePort}`, - pushEventPayload, - { - headers: { - "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000", - "X-GitHub-Event": "push", - "X-Hub-Signature": "ignored", - "X-Hub-Signature-256": signatureSha256, - }, - } - ); - }) - - .then((result: AxiosResponse) => { - expect(result.status).toBe(200); - }) - .catch((e) => expect(e instanceof Error).toBeTruthy()) - .finally(() => server.close(t)); - }); - - test("POST / with push event payload with only sha1", (t) => { - expect.assertions(2); - - const api = new Webhooks({ - secret: "mysecret", - log: { - warn: () => {}, - }, - }); - const server = http.createServer(api.middleware); - - api.on("push", (event) => { - expect(event.id).toBe("123e4567-e89b-12d3-a456-426655440000"); - }); - - const promisifiedServer = <(port: number) => Promise>( - promisify(server.listen.bind(server)) - ); - - promisifiedServer(availablePort) - .then(() => { - return axios.post( - `http://localhost:${availablePort}`, - pushEventPayload, - { - headers: { - "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000", - "X-GitHub-Event": "push", - "X-Hub-Signature": signatureSha1, - }, - } - ); - }) - - .then((result: AxiosResponse) => { - expect(result.status).toBe(200); - }) - .catch((e) => expect(e instanceof Error).toBeTruthy()) - .finally(() => server.close(t)); - }); - - test("POST / with push event payload", (t) => { - expect.assertions(2); - - const api = new Webhooks({ - secret: "mysecret", - log: { - warn: () => {}, - }, - }); - const server = http.createServer(api.middleware); - - api.on("push", (event) => { - expect(event.id).toBe("123e4567-e89b-12d3-a456-426655440000"); - }); - - const promisifiedServer = <(port: number) => Promise>( - promisify(server.listen.bind(server)) - ); - - promisifiedServer(availablePort) - .then(() => { - return axios.post( - `http://localhost:${availablePort}`, - pushEventPayload, - { - headers: { - "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000", - "X-GitHub-Event": "push", - "X-Hub-Signature": "sha1=foo", - "X-Hub-Signature-256": signatureSha256, - }, - } - ); - }) - - .then((result: AxiosResponse) => { - expect(result.status).toBe(200); - }) - .catch((e) => expect(e instanceof Error).toBeTruthy()) - .finally(() => server.close(t)); - }); - - // TEST - test("POST / with push event payload (request.body already parsed)", (t) => { - expect.assertions(2); - - const api = new Webhooks({ - secret: "mysecret", - log: { - warn: () => {}, - }, - }); - const dataChunks: any[] = []; - let timeout: NodeJS.Timeout; - const server = http.createServer((req, res) => { - req.once("data", (chunk) => dataChunks.push(chunk)); - req.once("end", () => { - req.body = JSON.parse(Buffer.concat(dataChunks).toString()); - api.middleware(req, res); - - timeout = setTimeout(() => { - res.statusCode = 500; - res.end("Middleware timeout"); - }, 3000); - }); - }); - - api.on("push", (event) => { - expect(event.id).toBe("123e4567-e89b-12d3-a456-426655440000"); - }); - - const promisifiedServer = <(port: number) => Promise>( - promisify(server.listen.bind(server)) - ); - - promisifiedServer(availablePort) - .then(() => { - return axios.post( - `http://localhost:${availablePort}`, - pushEventPayload, - { - headers: { - "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000", - "X-GitHub-Event": "push", - "X-Hub-Signature": signatureSha1, - "X-Hub-Signature-256": signatureSha256, - }, - } - ); - }) - .then((result: AxiosResponse) => { - expect(result.status).toBe(200); - }) - .catch((e: Error) => expect(e).toBeInstanceOf(Error)) - .finally(() => { - clearTimeout(timeout); - server.close(t); - }); - }); - - test("POST / with push event payload (no signature)", (t) => { - const api = new Webhooks({ - secret: "mysecret", - log: { - warn: () => {}, - }, - }); - const server = http.createServer(api.middleware); - const errorHandler = jest.fn(); - api.onError(errorHandler); - - const promisifiedServer = <(port: number) => Promise>( - promisify(server.listen.bind(server)) - ); - - promisifiedServer(availablePort) - .then(() => { - return axios.post( - `http://localhost:${availablePort}`, - pushEventPayload, - { - headers: { - "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000", - "X-GitHub-Event": "push", - }, - } - ); - }) - - .then(() => { - t.fail("should return a 400"); - }) - .catch((error: AxiosError) => { - error.response && expect(error.response.status).toBe(400); - }) - .finally(() => { - expect(errorHandler).toHaveBeenCalled(); // calls "error" event handler - server.close(t); - }); - }); - - test("POST / with push event payload (invalid signature)", (t) => { - const api = new Webhooks({ - secret: "mysecret", - log: { - warn: () => {}, - }, - }); - const server = http.createServer(api.middleware); - const errorHandler = jest.fn(); - api.onError(errorHandler); - - const promisifiedServer = <(port: number) => Promise>( - promisify(server.listen.bind(server)) - ); - - promisifiedServer(availablePort) - .then(() => { - return axios.post( - `http://localhost:${availablePort}`, - pushEventPayload, - { - headers: { - "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000", - "X-GitHub-Event": "push", - "X-Hub-Signature": "sha1=foo", - "X-Hub-Signature-256": "sha256=foo", - }, - } - ); - }) - - .then(() => { - t.fail("should return a 400"); - }) - .catch((error: AxiosError) => { - error.response && expect(error.response.status).toBe(400); - }) - .finally(() => { - expect(errorHandler).toHaveBeenCalled(); // calls "error" event handler - server.close(t); - }); - }); - - test("POST / with hook error", (t) => { - const api = new Webhooks({ - secret: "mysecret", - log: { - warn: () => {}, - }, - }); - const server = http.createServer(api.middleware); - - api.on("push", () => { - throw new Error("Oops"); - }); - - const promisifiedServer = <(port: number) => Promise>( - promisify(server.listen.bind(server)) - ); - - promisifiedServer(availablePort) - .then(() => { - return axios.post( - `http://localhost:${availablePort}`, - pushEventPayload, - { - headers: { - "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000", - "X-GitHub-Event": "push", - "X-Hub-Signature": signatureSha1, - "X-Hub-Signature-256": signatureSha256, - }, - } - ); - }) - - .then(() => { - t.fail("should return a 500"); - }) - .catch((error: AxiosError) => { - error.response && expect(error.response.status).toBe(500); - }) - .finally(() => { - server.close(t); - }); - }); - - test("POST / with timeout", async (t) => { - expect.assertions(2); - - const api = new Webhooks({ - secret: "mysecret", - log: { - warn: () => {}, - }, - }); - const server = http.createServer(api.middleware); - const tenSecondsInMs = 10 * 1000; - - api.on("push", async () => { - await new Promise((resolve) => { - jest.runAllTimers(); - setTimeout(resolve, tenSecondsInMs); - }); - }); - - const promisifiedServer = <(port: number) => Promise>( - promisify(server.listen.bind(server)) - ); - - promisifiedServer(availablePort) - .then(() => { - return axios.post( - `http://localhost:${availablePort}`, - pushEventPayload, - { - headers: { - "X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000", - "X-GitHub-Event": "push", - "X-Hub-Signature": signatureSha1, - "X-Hub-Signature-256": signatureSha256, - }, - } - ); - }) - - .then((result: AxiosResponse) => { - expect(setTimeout).toHaveBeenCalled(); - expect(result.status).toBe(202); - }) - .catch((error: AxiosError) => { - error.response && expect(error.response.status).toBe(400); - }) - .finally(() => { - server.close(); - t(); - }); - }); - - afterEach(() => jest.clearAllTimers()); -}); diff --git a/test/integration/sign-test.ts b/test/integration/sign-test.ts deleted file mode 100644 index aac9f5d2..00000000 --- a/test/integration/sign-test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { sign } from "../../src/sign"; - -const eventPayload = { - foo: "bar", -}; -const secret = "mysecret"; - -test("sign() without options throws", () => { - // @ts-expect-error - expect(() => sign()).toThrow(); -}); - -test("sign(undefined, eventPayload) without secret throws", () => { - // @ts-expect-error - expect(() => sign.bind(null, undefined, eventPayload)()).toThrow(); -}); - -test("sign(secret) without eventPayload throws", () => { - // @ts-expect-error - expect(() => sign.bind(null, secret)()).toThrow(); -}); - -test("sign({secret, algorithm}) with invalid algorithm throws", () => { - expect(() => - // @ts-expect-error - sign.bind(null, { secret, algorithm: "sha2" }, eventPayload)() - ).toThrow(); -}); - -describe("with eventPayload as object", () => { - describe("returns expected sha1 signature", () => { - test("sign(secret, eventPayload)", () => { - const signature = sign(secret, eventPayload); - expect(signature).toBe("sha1=d03207e4b030cf234e3447bac4d93add4c6643d8"); - }); - - test("sign({secret}, eventPayload)", () => { - const signature = sign({ secret }, eventPayload); - expect(signature).toBe("sha1=d03207e4b030cf234e3447bac4d93add4c6643d8"); - }); - - test("sign({secret, algorithm: 'sha1'}, eventPayload)", () => { - const signature = sign({ secret, algorithm: "sha1" }, eventPayload); - expect(signature).toBe("sha1=d03207e4b030cf234e3447bac4d93add4c6643d8"); - }); - }); - - describe("returns expected sha256 signature", () => { - test("sign({secret, algorithm}, eventPayload)", () => { - const signature = sign({ secret, algorithm: "sha256" }, eventPayload); - expect(signature).toBe( - "sha256=4864d2759938a15468b5df9ade20bf161da9b4f737ea61794142f3484236bda3" - ); - }); - }); -}); - -describe("with eventPayload as string", () => { - describe("returns expected sha1 signature", () => { - test("sign(secret, eventPayload)", () => { - const signature = sign(secret, JSON.stringify(eventPayload)); - expect(signature).toBe("sha1=d03207e4b030cf234e3447bac4d93add4c6643d8"); - }); - - test("sign({secret}, eventPayload)", () => { - const signature = sign({ secret }, JSON.stringify(eventPayload)); - expect(signature).toBe("sha1=d03207e4b030cf234e3447bac4d93add4c6643d8"); - }); - - test("sign({secret, algorithm: 'sha1'}, eventPayload)", () => { - const signature = sign( - { secret, algorithm: "sha1" }, - JSON.stringify(eventPayload) - ); - expect(signature).toBe("sha1=d03207e4b030cf234e3447bac4d93add4c6643d8"); - }); - }); - - describe("returns expected sha256 signature", () => { - test("sign({secret, algorithm: 'sha256'}, eventPayload)", () => { - const signature = sign( - { secret, algorithm: "sha256" }, - JSON.stringify(eventPayload) - ); - expect(signature).toBe( - "sha256=4864d2759938a15468b5df9ade20bf161da9b4f737ea61794142f3484236bda3" - ); - }); - }); -}); diff --git a/test/integration/smoke-test.ts b/test/integration/smoke-test.ts index ee3a3bb1..1c53459a 100644 --- a/test/integration/smoke-test.ts +++ b/test/integration/smoke-test.ts @@ -1,10 +1,4 @@ -import { - Webhooks, - sign, - verify, - createEventHandler, - createMiddleware, -} from "../../src"; +import { Webhooks, createEventHandler } from "../../src"; test("@octokit/webhooks", () => { const emitWarningSpy = jest.spyOn(process, "emitWarning"); @@ -17,29 +11,10 @@ test("@octokit/webhooks", () => { expect(typeof api.on).toBe("function"); expect(typeof api.removeListener).toBe("function"); expect(typeof api.receive).toBe("function"); - expect(typeof api.middleware).toBe("function"); expect(typeof api.verifyAndReceive).toBe("function"); expect(emitWarningSpy).not.toHaveBeenCalled(); }); -test('require("@octokit/webhooks").sign', () => { - const emitWarningSpy = jest.spyOn(process, "emitWarning"); - - expect(() => { - sign("1234", {}); - }).not.toThrow(); - expect(emitWarningSpy).not.toHaveBeenCalled(); -}); - -test('require("@octokit/webhooks").verify', () => { - const emitWarningSpy = jest.spyOn(process, "emitWarning"); - - expect(() => { - verify("1234", {}, "randomSignature"); - }).not.toThrow(); - expect(emitWarningSpy).not.toHaveBeenCalled(); -}); - test('require("@octokit/webhooks").createEventHandler', () => { const emitWarningSpy = jest.spyOn(process, "emitWarning"); @@ -48,12 +23,3 @@ test('require("@octokit/webhooks").createEventHandler', () => { }).not.toThrow(); expect(emitWarningSpy).not.toHaveBeenCalled(); }); - -test('require("@octokit/webhooks").createMiddleware', () => { - const emitWarningSpy = jest.spyOn(process, "emitWarning"); - - expect(() => { - createMiddleware({ secret: "1234" }); - }).not.toThrow(); - expect(emitWarningSpy).not.toHaveBeenCalled(); -}); diff --git a/test/integration/verify-test.ts b/test/integration/verify-test.ts deleted file mode 100644 index 5d4464e2..00000000 --- a/test/integration/verify-test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { verify } from "../../src/verify"; - -const eventPayload = { - foo: "bar", -}; -const secret = "mysecret"; -const signatureSHA1 = "sha1=d03207e4b030cf234e3447bac4d93add4c6643d8"; -const signatureSHA256 = - "sha256=4864d2759938a15468b5df9ade20bf161da9b4f737ea61794142f3484236bda3"; - -test("verify() without options throws", () => { - // @ts-expect-error - expect(() => verify()).toThrow(); -}); - -test("verify(undefined, eventPayload) without secret throws", () => { - // @ts-expect-error - expect(() => verify.bind(null, undefined, eventPayload)()).toThrow(); -}); - -test("verify(secret) without eventPayload throws", () => { - // @ts-expect-error - expect(() => verify.bind(null, secret)()).toThrow(); -}); - -test("verify(secret, eventPayload) without options.signature throws", () => { - // @ts-expect-error - expect(() => verify.bind(null, secret, eventPayload)()).toThrow(); -}); - -test("verify(secret, eventPayload, signatureSHA1) returns true for correct signature", () => { - const signatureMatches = verify(secret, eventPayload, signatureSHA1); - expect(signatureMatches).toBe(true); -}); - -test("verify(secret, eventPayload, signatureSHA1) returns false for incorrect signature", () => { - const signatureMatches = verify(secret, eventPayload, "foo"); - expect(signatureMatches).toBe(false); -}); - -test("verify(secret, eventPayload, signatureSHA1) returns false for correct secret", () => { - const signatureMatches = verify("foo", eventPayload, signatureSHA1); - expect(signatureMatches).toBe(false); -}); - -test("verify(secret, eventPayload, signatureSHA1) returns true if eventPayload contains special characters (#71)", () => { - // https://github.com/octokit/webhooks.js/issues/71 - const signatureMatchesLowerCaseSequence = verify( - "development", - { - foo: "Foo\n\u001b[34mbar: ♥♥♥♥♥♥♥♥\nthis-is-lost\u001b[0m\u001b[2K", - }, - "sha1=7316ec5e7866e42e4aba4af550d21a5f036f949d" - ); - expect(signatureMatchesLowerCaseSequence).toBe(true); - const signatureMatchesUpperCaseSequence = verify( - "development", - { - foo: "Foo\n\u001B[34mbar: ♥♥♥♥♥♥♥♥\nthis-is-lost\u001B[0m\u001B[2K", - }, - "sha1=7316ec5e7866e42e4aba4af550d21a5f036f949d" - ); - expect(signatureMatchesUpperCaseSequence).toBe(true); - const signatureMatchesEscapedSequence = verify( - "development", - { - foo: "\\u001b", - }, - "sha1=2c440a176f4cb84c8c921dfee882d594c2465097" - ); - expect(signatureMatchesEscapedSequence).toBe(true); -}); - -test("verify(secret, eventPayload, signatureSHA256) returns true for correct signature", () => { - const signatureMatches = verify(secret, eventPayload, signatureSHA256); - expect(signatureMatches).toBe(true); -}); - -test("verify(secret, eventPayload, signatureSHA256) returns false for incorrect signature", () => { - const signatureMatches = verify(secret, eventPayload, "foo"); - expect(signatureMatches).toBe(false); -}); - -test("verify(secret, eventPayload, signatureSHA256) returns false for correct secret", () => { - const signatureMatches = verify("foo", eventPayload, signatureSHA256); - expect(signatureMatches).toBe(false); -}); - -test("verify(secret, eventPayload, signatureSHA256) returns true if eventPayload contains special characters (#71)", () => { - // https://github.com/octokit/webhooks.js/issues/71 - const signatureMatchesLowerCaseSequence = verify( - "development", - { - foo: "Foo\n\u001b[34mbar: ♥♥♥♥♥♥♥♥\nthis-is-lost\u001b[0m\u001b[2K", - }, - "sha256=afecc3caa27548bb90d51a50384cb2868b9a3327b4ad6a01c9bd4ed0f8b0b12c" - ); - expect(signatureMatchesLowerCaseSequence).toBe(true); - const signatureMatchesUpperCaseSequence = verify( - "development", - { - foo: "Foo\n\u001B[34mbar: ♥♥♥♥♥♥♥♥\nthis-is-lost\u001B[0m\u001B[2K", - }, - "sha256=afecc3caa27548bb90d51a50384cb2868b9a3327b4ad6a01c9bd4ed0f8b0b12c" - ); - expect(signatureMatchesUpperCaseSequence).toBe(true); - const signatureMatchesEscapedSequence = verify( - "development", - { - foo: "\\u001b", - }, - "sha256=6f8326efbacfbd04e870cea25b5652e635be8c9807f2fd5348ef60753c9e96ed" - ); - expect(signatureMatchesEscapedSequence).toBe(true); -}); diff --git a/test/integration/webhooks.test.ts b/test/integration/webhooks.test.ts new file mode 100644 index 00000000..324776d4 --- /dev/null +++ b/test/integration/webhooks.test.ts @@ -0,0 +1,39 @@ +import { Webhooks, EmitterWebhookEvent } from "../../src"; + +describe("Webhooks", () => { + test("new Webhooks() without secret option", () => { + // @ts-expect-error + expect(() => new Webhooks()).toThrow( + "[@octokit/webhooks] options.secret required" + ); + }); + + test("webhooks.verifyAndReceive(event) with incorrect signature", async () => { + const webhooks = new Webhooks({ secret: "mysecret" }); + + const pingPayload = {} as EmitterWebhookEvent<"ping">["payload"]; + await expect(async () => + webhooks.verifyAndReceive({ + id: "1", + name: "ping", + payload: pingPayload, + signature: "nope", + }) + ).rejects.toThrow( + "[@octokit/webhooks] signature does not match event payload and secret" + ); + }); + + test("webhooks.receive(error)", async () => { + const webhooks = new Webhooks({ secret: "mysecret" }); + + webhooks.onError((error) => { + expect(error.message).toMatch(/oops/); + }); + + await expect(async () => + // @ts-expect-error + webhooks.receive(new Error("oops")) + ).rejects.toThrow(); + }); +}); diff --git a/test/typescript-validate.ts b/test/typescript-validate.ts index 19731e6f..055a80d5 100644 --- a/test/typescript-validate.ts +++ b/test/typescript-validate.ts @@ -1,11 +1,9 @@ import { Webhooks, createEventHandler, - createMiddleware, - sign, - verify, EmitterWebhookEvent, WebhookError, + createNodeMiddleware, } from "../src/index"; import { createServer } from "http"; import { HandlerFunction, EmitterWebhookEventName } from "../src/types"; @@ -70,7 +68,6 @@ export default async function () { // Check all supported options const webhooks = new Webhooks({ secret: "blah", - path: "/webhooks", transform: (event) => { console.log(event.payload); return Object.assign(event, { foo: "bar" }); @@ -79,21 +76,9 @@ export default async function () { createEventHandler({ secret: "blah" }); - createMiddleware({ - secret: "mysecret", - path: "/github-webhooks", - }); - - sign("randomSecret", {}); - sign({ secret: "randomSecret" }, {}); - sign({ secret: "randomSecret", algorithm: "sha1" }, {}); - sign({ secret: "randomSecret", algorithm: "sha256" }, {}); - - verify("randomSecret", {}, "randomSignature"); - - webhooks.onAny(({ id, name, payload }) => { + webhooks.onAny(async ({ id, name, payload }) => { console.log(name, "event received", id); - const sig = webhooks.sign(payload); + const sig = await webhooks.sign(payload); webhooks.verify(payload, sig); }); @@ -193,7 +178,7 @@ export default async function () { console.log(firstError.request); }); - createServer(webhooks.middleware).listen(3000); + createServer(createNodeMiddleware(webhooks)).listen(3000); } export function webhookErrorTest(error: WebhookError) { diff --git a/test/unit/deprecation.test.ts b/test/unit/deprecation.test.ts index 359c39c1..e14b277b 100644 --- a/test/unit/deprecation.test.ts +++ b/test/unit/deprecation.test.ts @@ -1,67 +1,3 @@ -import { createWebhooksApi, Webhooks } from "../../src"; - -describe("Deprecated methods", () => { - test("createWebhooksApi", () => { - const warn = jest.fn(); - - createWebhooksApi({ - secret: "foo", - log: { - debug: () => {}, - info: () => {}, - warn: warn, - error: () => {}, - }, - }); - expect(warn).toBeCalledWith( - "[@octokit/webhooks] `createWebhooksApi()` is deprecated and will be removed in a future release of `@octokit/webhooks`, please use the `Webhooks` class instead" - ); - warn.mockClear(); - }); - - test("path parameter", () => { - const warn = jest.fn(); - new Webhooks({ - secret: "secret", - path: "/test", - log: { - debug: () => {}, - info: () => {}, - warn: warn, - error: () => {}, - }, - }); - - expect(warn).toHaveBeenCalledWith( - "[@octokit/webhooks] `path` option is deprecated and will be removed in a future release of `@octokit/webhooks`. Please use `createNodeMiddleware(webhooks, { path })` instead" - ); - }); - - test("webhooks.middleware", () => { - expect.assertions(2); - - const warn = jest.fn(); - const webhooks = new Webhooks({ - secret: "secret", - log: { - debug: () => {}, - info: () => {}, - warn: warn, - error: () => {}, - }, - }); - - try { - // @ts-expect-error - webhooks.middleware(); - } catch (error) { - expect(error.message).toEqual( - "Cannot read property 'method' of undefined" - ); - } - - expect(warn).toHaveBeenCalledWith( - "[@octokit/webhooks] `webhooks.middleware` is deprecated and will be removed in a future release of `@octokit/webhooks`. Please use `createNodeMiddleware(webhooks)` instead" - ); - }); +describe("Deprecations", () => { + test("there are currently no deprecations", () => {}); }); diff --git a/test/unit/middleware-constructor-test.ts b/test/unit/middleware-constructor-test.ts deleted file mode 100644 index 69b14c8e..00000000 --- a/test/unit/middleware-constructor-test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createMiddleware as Middleware } from "../../src/middleware-legacy"; - -test("options: none", () => { - // @ts-expect-error - expect(() => Middleware({})).toThrow(); -}); diff --git a/test/unit/middleware-test.ts b/test/unit/middleware-test.ts deleted file mode 100644 index d553fec6..00000000 --- a/test/unit/middleware-test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { IncomingMessage, ServerResponse } from "http"; -import { middleware } from "../../src/middleware-legacy/middleware"; -import { getPayload } from "../../src/middleware-legacy/get-payload"; -import { verifyAndReceive } from "../../src/middleware-legacy/verify-and-receive"; - -jest.mock("../../src/middleware-legacy/get-payload"); -jest.mock("../../src/middleware-legacy/verify-and-receive"); - -const mockGetPayload = getPayload as jest.Mock; -const mockVerifyAndReceive = verifyAndReceive as jest.Mock; - -const headers = { - "x-github-delivery": "123e4567-e89b-12d3-a456-426655440000", - "x-github-event": "push", - "x-hub-signature": "sha1=f4d795e69b5d03c139cc6ea991ad3e5762d13e2f", -}; - -test("next() callback", () => { - const next = jest.fn(); - - middleware( - { - secret: "mysecret", - hooks: {}, - log: console, - }, - { method: "POST", url: "/foo" } as IncomingMessage, - {} as ServerResponse, - next - ); - - expect(next).toHaveBeenCalled(); -}); - -describe("when does a timeout on retrieving the payload", () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - test("successfully, does NOT response.end(ok)", async () => { - const consoleDebugSpy = jest.spyOn(console, "debug").mockImplementation(); - const responseMock = ({ end: jest.fn() } as unknown) as ServerResponse; - const next = jest.fn(); - - mockGetPayload.mockResolvedValueOnce(undefined); - mockVerifyAndReceive.mockResolvedValueOnce(undefined); - - const promiseMiddleware = middleware( - { secret: "mysecret", hooks: {}, path: "/foo", log: console }, - ({ - method: "POST", - url: "/foo", - headers, - } as unknown) as IncomingMessage, - responseMock, - next - ); - - jest.runAllTimers(); - - await promiseMiddleware; - - expect(next).not.toHaveBeenCalled(); - expect(consoleDebugSpy).toHaveBeenCalledTimes(1); - expect(consoleDebugSpy).toHaveBeenLastCalledWith( - "push event received (id: 123e4567-e89b-12d3-a456-426655440000)" - ); - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(responseMock.end).toHaveBeenCalledWith("still processing\n"); - expect(responseMock.end).not.toHaveBeenCalledWith("ok\n"); - }); - - test("failing, does NOT response.end(ok)", async () => { - const consoleDebugSpy = jest.spyOn(console, "debug").mockImplementation(); - const responseMock = ({ end: jest.fn() } as unknown) as ServerResponse; - const next = jest.fn(); - - mockGetPayload.mockResolvedValueOnce(undefined); - mockVerifyAndReceive.mockRejectedValueOnce(new Error("random error")); - - const promiseMiddleware = middleware( - { secret: "mysecret", hooks: {}, path: "/foo", log: console }, - ({ - method: "POST", - url: "/foo", - headers, - } as unknown) as IncomingMessage, - responseMock, - next - ); - - jest.runAllTimers(); - - await promiseMiddleware; - - expect(next).not.toHaveBeenCalled(); - expect(consoleDebugSpy).toHaveBeenCalledTimes(1); - expect(consoleDebugSpy).toHaveBeenLastCalledWith( - "push event received (id: 123e4567-e89b-12d3-a456-426655440000)" - ); - expect(setTimeout).toHaveBeenCalledTimes(1); - expect(responseMock.end).toHaveBeenCalledWith("still processing\n"); - expect(responseMock.end).not.toHaveBeenCalledWith( - new Error("random error").toString() - ); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); -});