Skip to content

Commit

Permalink
feat(middleware): original request passed down + regExp path match + …
Browse files Browse the repository at this point in the history
…better typed request and response
  • Loading branch information
tg44 committed Jun 27, 2022
1 parent 5972849 commit ea523b4
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 18 deletions.
39 changes: 32 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const webhooks = new Webhooks({
secret: "mysecret",
});

webhooks.onAny(({ id, name, payload }) => {
webhooks.onAny(({ id, name, payload, originalRequest }) => {
console.log(name, "event received");
});

Expand Down Expand Up @@ -223,7 +223,7 @@ The `verify` method can be imported as static method from [`@octokit/webhooks-me
### webhooks.verifyAndReceive()

```js
webhooks.verifyAndReceive({ id, name, payload, signature });
webhooks.verifyAndReceive({ id, name, payload, originalRequest, signature });
```

<table width="100%">
Expand Down Expand Up @@ -316,7 +316,7 @@ eventHandler
### webhooks.receive()

```js
webhooks.receive({ id, name, payload });
webhooks.receive({ id, name, payload, originalRequest });
```

<table width="100%">
Expand Down Expand Up @@ -370,6 +370,8 @@ Returns a promise. Runs all handlers set with [`webhooks.on()`](#webhookson) in

The `.receive()` method belongs to the `event-handler` module which can be used [standalone](src/event-handler/).

The `originalRequest` is an optional parameter, if it is set, it will be available in the `on` functions.

### webhooks.on()

```js
Expand Down Expand Up @@ -420,7 +422,7 @@ webhooks.on(eventNames, handler);
<strong>Required.</strong>
Method to be run each time the event with the passed name is received.
the <code>handler</code> function can be an async function, throw an error or
return a Promise. The handler is called with an event object: <code>{id, name, payload}</code>.
return a Promise. The handler is called with an event object: <code>{id, name, payload, originalRequest}</code>.
</td>
</tr>
</tbody>
Expand Down Expand Up @@ -449,7 +451,7 @@ webhooks.onAny(handler);
<strong>Required.</strong>
Method to be run each time any event is received.
the <code>handler</code> function can be an async function, throw an error or
return a Promise. The handler is called with an event object: <code>{id, name, payload}</code>.
return a Promise. The handler is called with an event object: <code>{id, name, payload, originalRequest}</code>.
</td>
</tr>
</tbody>
Expand Down Expand Up @@ -482,7 +484,7 @@ Asynchronous `error` event handler are not blocking the `.receive()` method from
<strong>Required.</strong>
Method to be run each time a webhook event handler throws an error or returns a promise that rejects.
The <code>handler</code> function can be an async function,
return a Promise. The handler is called with an error object that has a .event property which has all the information on the event: <code>{id, name, payload}</code>.
return a Promise. The handler is called with an error object that has a .event property which has all the information on the event: <code>{id, name, payload, originalRequest}</code>.
</td>
</tr>
</tbody>
Expand Down Expand Up @@ -586,6 +588,29 @@ createServer(middleware).listen(3000);
Custom path to match requests against. Defaults to <code>/api/github/webhooks</code>.
</td>
</tr>
<tr>
<td>
<code>pathRegex</code>
<em>
RegEx
</em>
</td>
<td>

Custom path matcher. If set, overrides the <code>path</code> option.
Can be used as;

```js
const middleware = createNodeMiddleware(
webhooks,
{ pathRegex: /^\/api\/github\/webhooks/ }
);
```

Test the regex before usage, the `g` and `y` flags [makes it stateful](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test)!

</td>
</tr>
<tr>
<td>
<code>log</code>
Expand Down Expand Up @@ -721,7 +746,7 @@ A union of all possible events and event/action combinations supported by the ev

### `EmitterWebhookEvent`

The object that is emitted by `@octokit/webhooks` as an event; made up of an `id`, `name`, and `payload` properties.
The object that is emitted by `@octokit/webhooks` as an event; made up of an `id`, `name`, and `payload` properties, with an optional `originalRequest`.
An optional generic parameter can be passed to narrow the type of the `name` and `payload` properties based on event names or event/action combinations, e.g. `EmitterWebhookEvent<"check_run" | "code_scanning_alert.fixed">`.

## License
Expand Down
3 changes: 2 additions & 1 deletion src/middleware/node/get-missing-headers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// 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;

import { IncomingMessage } from "./middleware";

const WEBHOOK_HEADERS = [
"x-github-event",
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/node/get-payload.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { WebhookEvent } from "@octokit/webhooks-types";
// @ts-ignore to address #245
import AggregateError from "aggregate-error";
import { IncomingMessage } from "./middleware";

// remove type imports from http for Deno compatibility
// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
Expand All @@ -10,7 +11,6 @@ import AggregateError from "aggregate-error";
// body?: WebhookEvent | unknown;
// }
// }
type IncomingMessage = any;

export function getPayload(request: IncomingMessage): Promise<WebhookEvent> {
// If request.body already exists we can stop here
Expand Down
2 changes: 2 additions & 0 deletions src/middleware/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ export function createNodeMiddleware(
webhooks: Webhooks,
{
path = "/api/github/webhooks",
pathRegex = undefined,
onUnhandledRequest = onUnhandledRequestDefault,
log = createLogger(),
}: MiddlewareOptions = {}
) {
return middleware.bind(null, webhooks, {
path,
pathRegex,
onUnhandledRequest,
log,
} as Required<MiddlewareOptions>);
Expand Down
24 changes: 19 additions & 5 deletions src/middleware/node/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
// 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 interface IncomingMessage {
headers: Record<string, string | string[] | undefined>;
body?: any;
url?: string;
method?: string;
setEncoding(enc: string): void;
on(event: string, callback: (data: any) => void): void;
}
export interface ServerResponse {
set statusCode(value: number);
end(body: string): void;
writeHead(status: number, headers?: Record<string, string>): void;
}

import { WebhookEventName } from "@octokit/webhooks-types";

Expand Down Expand Up @@ -33,8 +44,10 @@ export async function middleware(
);
return;
}

const isUnknownRoute = request.method !== "POST" || pathname !== options.path;
const pathMatch = options.pathRegex
? options.pathRegex.test(pathname)
: pathname === options.path;
const isUnknownRoute = request.method !== "POST" || !pathMatch;
const isExpressMiddleware = typeof next === "function";
if (isUnknownRoute) {
if (isExpressMiddleware) {
Expand Down Expand Up @@ -72,7 +85,7 @@ export async function middleware(
didTimeout = true;
response.statusCode = 202;
response.end("still processing\n");
}, 9000).unref();
}, 9000);

try {
const payload = await getPayload(request);
Expand All @@ -82,6 +95,7 @@ export async function middleware(
name: eventName as any,
payload: payload as any,
signature: signatureSHA256,
originalRequest: request,
});
clearTimeout(timeout);

Expand Down
4 changes: 2 additions & 2 deletions src/middleware/node/on-unhandled-request-default.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// 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 { IncomingMessage, ServerResponse } from "./middleware";

export function onUnhandledRequestDefault(
request: IncomingMessage,
Expand Down
4 changes: 2 additions & 2 deletions src/middleware/node/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// 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";
import { IncomingMessage, ServerResponse } from "./middleware";

export type MiddlewareOptions = {
path?: string;
pathRegex?: RegExp;
log?: Logger;
onUnhandledRequest?: (
request: IncomingMessage,
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import type {
} from "@octokit/webhooks-types";
import { Logger } from "./createLogger";
import type { emitterEventNames } from "./generated/webhook-names";
import { IncomingMessage } from "./middleware/node/middleware";

export type EmitterWebhookEventName = typeof emitterEventNames[number];
export type EmitterWebhookEvent<
TEmitterEvent extends EmitterWebhookEventName = EmitterWebhookEventName
> = TEmitterEvent extends `${infer TWebhookEvent}.${infer TAction}`
? BaseWebhookEvent<Extract<TWebhookEvent, WebhookEventName>> & {
payload: { action: TAction };
} & {
originalRequest?: IncomingMessage;
}
: BaseWebhookEvent<Extract<TEmitterEvent, WebhookEventName>>;

Expand All @@ -20,6 +23,7 @@ export type EmitterWebhookEventWithStringPayloadAndSignature = {
name: EmitterWebhookEventName;
payload: string;
signature: string;
originalRequest?: IncomingMessage;
};

export type EmitterWebhookEventWithSignature = EmitterWebhookEvent & {
Expand All @@ -30,6 +34,7 @@ interface BaseWebhookEvent<TName extends WebhookEventName> {
id: string;
name: TName;
payload: WebhookEventMap[TName];
originalRequest?: IncomingMessage;
}

export interface Options<TTransformed = unknown> {
Expand Down
1 change: 1 addition & 0 deletions src/verify-and-receive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ export async function verifyAndReceive(
typeof event.payload === "string"
? JSON.parse(event.payload)
: event.payload,
originalRequest: event.originalRequest,
});
}
111 changes: 111 additions & 0 deletions test/integration/node-middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,117 @@ describe("createNodeMiddleware(webhooks)", () => {
server.close();
});

test("pathRegex match", async () => {
expect.assertions(7);

const webhooks = new Webhooks({
secret: "mySecret",
});

const server = createServer(
createNodeMiddleware(webhooks, {
pathRegex: /^\/api\/github\/webhooks/,
})
).listen();

// @ts-expect-error complains about { port } although it's included in returned AddressInfo interface
const { port } = server.address();

webhooks.on("push", (event) => {
expect(event.id).toBe("123e4567-e89b-12d3-a456-426655440000");
});

const response1 = await fetch(
`http://localhost:${port}/api/github/webhooks/0001/testurl`,
{
method: "POST",
headers: {
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
"X-GitHub-Event": "push",
"X-Hub-Signature-256": signatureSha256,
},
body: pushEventPayload,
}
);

expect(response1.status).toEqual(200);
await expect(response1.text()).resolves.toBe("ok\n");

const response2 = await fetch(
`http://localhost:${port}/api/github/webhooks/0001/testurl`,
{
method: "POST",
headers: {
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
"X-GitHub-Event": "push",
"X-Hub-Signature-256": signatureSha256,
},
body: pushEventPayload,
}
);

expect(response2.status).toEqual(200);
await expect(response2.text()).resolves.toBe("ok\n");

const response3 = await fetch(`http://localhost:${port}/api/github/web`, {
method: "POST",
headers: {
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
"X-GitHub-Event": "push",
"X-Hub-Signature-256": signatureSha256,
},
body: pushEventPayload,
});

expect(response3.status).toEqual(404);

server.close();
});

test("original request passed by as intented", async () => {
expect.assertions(6);

const webhooks = new Webhooks({
secret: "mySecret",
});

const server = createServer(
createNodeMiddleware(webhooks, {
pathRegex: /^\/api\/github\/webhooks/,
})
).listen();

// @ts-expect-error complains about { port } although it's included in returned AddressInfo interface
const { port } = server.address();

webhooks.on("push", (event) => {
expect(event.id).toBe("123e4567-e89b-12d3-a456-426655440000");
const r = event.originalRequest;
expect(r).toBeDefined();
expect(r?.headers["my-custom-header"]).toBe("customHeader");
expect(r?.url).toBe(`/api/github/webhooks/0001/testurl`);
});

const response = await fetch(
`http://localhost:${port}/api/github/webhooks/0001/testurl`,
{
method: "POST",
headers: {
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
"X-GitHub-Event": "push",
"X-Hub-Signature-256": signatureSha256,
"my-custom-header": "customHeader",
},
body: pushEventPayload,
}
);

expect(response.status).toEqual(200);
await expect(response.text()).resolves.toBe("ok\n");

server.close();
});

test("request.body already parsed (e.g. Lambda)", async () => {
expect.assertions(3);

Expand Down

0 comments on commit ea523b4

Please sign in to comment.