Skip to content

Commit 8292596

Browse files
authored
feat: createNodeMiddleware() (#509)
1 parent 8fef083 commit 8292596

25 files changed

+1491
-375
lines changed

README.md

+391-345
Large diffs are not rendered by default.

package-lock.json

+464
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,13 @@
3333
"@types/jest": "^26.0.9",
3434
"@types/json-schema": "^7.0.7",
3535
"@types/node": "^14.0.14",
36+
"@types/node-fetch": "^2.5.8",
3637
"@types/prettier": "^2.0.0",
3738
"axios": "^0.21.0",
39+
"express": "^4.17.1",
3840
"get-port": "^5.0.0",
3941
"jest": "^26.2.2",
42+
"node-fetch": "^2.6.1",
4043
"prettier": "^2.0.1",
4144
"prettier-plugin-packagejson": "^2.2.9",
4245
"semantic-release": "^17.0.0",

src/createLogger.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
export interface Logger {
2-
debug: (message: string) => unknown;
3-
info: (message: string) => unknown;
4-
warn: (message: string) => unknown;
5-
error: (message: string) => unknown;
2+
debug: (...data: any[]) => void;
3+
info: (...data: any[]) => void;
4+
warn: (...data: any[]) => void;
5+
error: (...data: any[]) => void;
66
}
77

88
export const createLogger = (logger?: Partial<Logger>): Logger => ({

src/index.ts

+31-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { IncomingMessage, ServerResponse } from "http";
22
import { createLogger } from "./createLogger";
33
import { createEventHandler } from "./event-handler/index";
4-
import { createMiddleware } from "./middleware/index";
5-
import { middleware } from "./middleware/middleware";
6-
import { verifyAndReceive } from "./middleware/verify-and-receive";
4+
import { createMiddleware } from "./middleware-legacy/index";
5+
import { middleware } from "./middleware-legacy/middleware";
6+
import { verifyAndReceive } from "./middleware-legacy/verify-and-receive";
77
import { sign } from "./sign/index";
88
import {
99
EmitterWebhookEvent,
@@ -16,6 +16,8 @@ import {
1616
} from "./types";
1717
import { verify } from "./verify/index";
1818

19+
export { createNodeMiddleware } from "./middleware/node/index";
20+
1921
// U holds the return value of `transform` function in Options
2022
class Webhooks<TTransformed = unknown> {
2123
public sign: (payload: string | object) => string;
@@ -31,14 +33,18 @@ class Webhooks<TTransformed = unknown> {
3133
callback: HandlerFunction<E, TTransformed>
3234
) => void;
3335
public receive: (event: EmitterWebhookEvent) => Promise<void>;
36+
public verifyAndReceive: (
37+
options: EmitterWebhookEvent & { signature: string }
38+
) => Promise<void>;
39+
40+
/**
41+
* @deprecated use `createNodeMiddleware(webhooks)` instead
42+
*/
3443
public middleware: (
3544
request: IncomingMessage,
3645
response: ServerResponse,
3746
next?: (err?: any) => void
3847
) => void | Promise<void>;
39-
public verifyAndReceive: (
40-
options: EmitterWebhookEvent & { signature: string }
41-
) => Promise<void>;
4248

4349
constructor(options: Options<TTransformed>) {
4450
if (!options || !options.secret) {
@@ -53,21 +59,38 @@ class Webhooks<TTransformed = unknown> {
5359
log: createLogger(options.log),
5460
};
5561

62+
if ("path" in options) {
63+
state.log.warn(
64+
"[@octokit/webhooks] `path` option is deprecated and will be removed in a future release of `@octokit/webhooks`. Please use `createNodeMiddleware(webhooks, { path })` instead"
65+
);
66+
}
67+
5668
this.sign = sign.bind(null, options.secret);
5769
this.verify = verify.bind(null, options.secret);
5870
this.on = state.eventHandler.on;
5971
this.onAny = state.eventHandler.onAny;
6072
this.onError = state.eventHandler.onError;
6173
this.removeListener = state.eventHandler.removeListener;
6274
this.receive = state.eventHandler.receive;
63-
this.middleware = middleware.bind(null, state);
6475
this.verifyAndReceive = verifyAndReceive.bind(null, state);
76+
77+
this.middleware = function deprecatedMiddleware(
78+
request: IncomingMessage,
79+
response: ServerResponse,
80+
next?: (err?: any) => void
81+
) {
82+
state.log.warn(
83+
"[@octokit/webhooks] `webhooks.middleware` is deprecated and will be removed in a future release of `@octokit/webhooks`. Please use `createNodeMiddleware(webhooks)` instead"
84+
);
85+
return middleware(state, request, response, next);
86+
};
6587
}
6688
}
6789

6890
/** @deprecated `createWebhooksApi()` is deprecated and will be removed in a future release of `@octokit/webhooks`, please use the `Webhooks` class instead */
6991
const createWebhooksApi = <TTransformed>(options: Options<TTransformed>) => {
70-
console.error(
92+
const log = createLogger(options.log);
93+
log.warn(
7194
"[@octokit/webhooks] `createWebhooksApi()` is deprecated and will be removed in a future release of `@octokit/webhooks`, please use the `Webhooks` class instead"
7295
);
7396
return new Webhooks<TTransformed>(options);

src/middleware/README.md src/middleware-legacy/README.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
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
44

55
```js
6-
const { createMiddleware } = require('@octokit/webhooks')
6+
const { createMiddleware } = require("@octokit/webhooks");
77
const middleware = createMiddleware({
8-
secret: 'mysecret',
9-
path: '/github-webhooks'
10-
})
8+
secret: "mysecret",
9+
path: "/github-webhooks",
10+
});
1111

12-
middleware.on('installation', asyncInstallationHook)
12+
middleware.on("installation", asyncInstallationHook);
1313

14-
require('http').createServer(middleware).listen(3000)
14+
require("http").createServer(middleware).listen(3000);
1515
```
1616

1717
## API
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { IncomingMessage } from "http";
2+
3+
const WEBHOOK_HEADERS = [
4+
"x-github-event",
5+
"x-hub-signature-256",
6+
"x-github-delivery",
7+
];
8+
9+
// https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#delivery-headers
10+
export function getMissingHeaders(request: IncomingMessage) {
11+
return WEBHOOK_HEADERS.filter((header) => !(header in request.headers));
12+
}

src/middleware/node/get-payload.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { WebhookEvent } from "@octokit/webhooks-definitions/schema";
2+
// @ts-ignore to address #245
3+
import AggregateError from "aggregate-error";
4+
import { IncomingMessage } from "http";
5+
6+
declare module "http" {
7+
interface IncomingMessage {
8+
body?: WebhookEvent;
9+
}
10+
}
11+
12+
export function getPayload(request: IncomingMessage): Promise<WebhookEvent> {
13+
// If request.body already exists we can stop here
14+
// See https://github.com/octokit/webhooks.js/pull/23
15+
16+
if (request.body) return Promise.resolve(request.body);
17+
18+
return new Promise((resolve, reject) => {
19+
let data = "";
20+
21+
request.setEncoding("utf8");
22+
23+
// istanbul ignore next
24+
request.on("error", (error) => reject(new AggregateError([error])));
25+
request.on("data", (chunk) => (data += chunk));
26+
request.on("end", () => {
27+
try {
28+
resolve(JSON.parse(data));
29+
} catch (error) {
30+
error.message = "Invalid JSON";
31+
error.status = 400;
32+
reject(new AggregateError([error]));
33+
}
34+
});
35+
});
36+
}

src/middleware/node/index.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createLogger } from "../../createLogger";
2+
import { Webhooks } from "../../index";
3+
import { middleware } from "./middleware";
4+
import { onUnhandledRequestDefault } from "./on-unhandled-request-default";
5+
import { MiddlewareOptions } from "./types";
6+
7+
export function createNodeMiddleware(
8+
webhooks: Webhooks,
9+
{
10+
path = "/api/github/webhooks",
11+
onUnhandledRequest = onUnhandledRequestDefault,
12+
log = createLogger(),
13+
}: MiddlewareOptions = {}
14+
) {
15+
return middleware.bind(null, webhooks, {
16+
path,
17+
onUnhandledRequest,
18+
log,
19+
} as Required<MiddlewareOptions>);
20+
}

src/middleware/node/middleware.ts

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { IncomingMessage, ServerResponse } from "http";
2+
3+
import { WebhookEventName } from "@octokit/webhooks-definitions/schema";
4+
5+
import { Webhooks } from "../../index";
6+
import { WebhookEventHandlerError } from "../../types";
7+
import { MiddlewareOptions } from "./types";
8+
import { onUnhandledRequestDefault } from "./on-unhandled-request-default";
9+
import { getMissingHeaders } from "./get-missing-headers";
10+
import { getPayload } from "./get-payload";
11+
12+
export async function middleware(
13+
webhooks: Webhooks,
14+
options: Required<MiddlewareOptions>,
15+
request: IncomingMessage,
16+
response: ServerResponse,
17+
next?: Function
18+
) {
19+
const { pathname } = new URL(request.url as string, "http://localhost");
20+
21+
const isUnknownRoute = request.method !== "POST" || pathname !== options.path;
22+
const isExpressMiddleware = typeof next === "function";
23+
if (!isExpressMiddleware && isUnknownRoute) {
24+
options.log.debug(`not found: ${request.method} ${request.url}`);
25+
return onUnhandledRequestDefault(request, response);
26+
}
27+
28+
const missingHeaders = getMissingHeaders(request).join(", ");
29+
30+
if (missingHeaders) {
31+
response.writeHead(400, {
32+
"content-type": "application/json",
33+
});
34+
response.end(
35+
JSON.stringify({
36+
error: `Required headers missing: ${missingHeaders}`,
37+
})
38+
);
39+
40+
return;
41+
}
42+
43+
const eventName = request.headers["x-github-event"] as WebhookEventName;
44+
const signatureSHA256 = request.headers["x-hub-signature-256"] as string;
45+
const id = request.headers["x-github-delivery"] as string;
46+
47+
options.log.debug(`${eventName} event received (id: ${id})`);
48+
49+
// GitHub will abort the request if it does not receive a response within 10s
50+
// See https://github.com/octokit/webhooks.js/issues/185
51+
let didTimeout = false;
52+
const timeout = setTimeout(() => {
53+
didTimeout = true;
54+
response.statusCode = 202;
55+
response.end("still processing\n");
56+
}, 9000).unref();
57+
58+
try {
59+
const payload = await getPayload(request);
60+
61+
await webhooks.verifyAndReceive({
62+
id: id,
63+
name: eventName as any,
64+
payload: payload as any,
65+
signature: signatureSHA256,
66+
});
67+
clearTimeout(timeout);
68+
69+
if (didTimeout) return;
70+
71+
response.end("ok\n");
72+
} catch (error) {
73+
clearTimeout(timeout);
74+
75+
if (didTimeout) return;
76+
77+
const statusCode = Array.from(error as WebhookEventHandlerError)[0].status;
78+
response.statusCode = typeof statusCode !== "undefined" ? statusCode : 500;
79+
response.end(error.toString());
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { IncomingMessage, ServerResponse } from "http";
2+
3+
export function onUnhandledRequestDefault(
4+
request: IncomingMessage,
5+
response: ServerResponse
6+
) {
7+
response.writeHead(404, {
8+
"content-type": "application/json",
9+
});
10+
response.end(
11+
JSON.stringify({
12+
error: `Unknown route: ${request.method} ${request.url}`,
13+
})
14+
);
15+
}

src/middleware/node/types.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { IncomingMessage, ServerResponse } from "http";
2+
3+
import { Logger } from "../../createLogger";
4+
5+
export type MiddlewareOptions = {
6+
path?: string;
7+
log?: Logger;
8+
onUnhandledRequest?: (
9+
request: IncomingMessage,
10+
response: ServerResponse
11+
) => void;
12+
};

src/verify/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export function verify(
1919

2020
const signatureBuffer = Buffer.from(signature);
2121
const algorithm = getAlgorithm(signature);
22+
2223
const verificationBuffer = Buffer.from(
2324
sign({ secret, algorithm }, eventPayload)
2425
);

test/integration/middleware-test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EventEmitter } from "events";
22
import { Buffer } from "buffer";
3-
import { createMiddleware } from "../../src/middleware";
3+
import { createMiddleware } from "../../src/middleware-legacy";
44

55
enum RequestMethodType {
66
POST = "POST",

0 commit comments

Comments
 (0)