Skip to content

Commit ea523b4

Browse files
committed
feat(middleware): original request passed down + regExp path match + better typed request and response
1 parent 5972849 commit ea523b4

File tree

10 files changed

+177
-18
lines changed

10 files changed

+177
-18
lines changed

README.md

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const webhooks = new Webhooks({
4848
secret: "mysecret",
4949
});
5050

51-
webhooks.onAny(({ id, name, payload }) => {
51+
webhooks.onAny(({ id, name, payload, originalRequest }) => {
5252
console.log(name, "event received");
5353
});
5454

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

225225
```js
226-
webhooks.verifyAndReceive({ id, name, payload, signature });
226+
webhooks.verifyAndReceive({ id, name, payload, originalRequest, signature });
227227
```
228228

229229
<table width="100%">
@@ -316,7 +316,7 @@ eventHandler
316316
### webhooks.receive()
317317

318318
```js
319-
webhooks.receive({ id, name, payload });
319+
webhooks.receive({ id, name, payload, originalRequest });
320320
```
321321

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

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

373+
The `originalRequest` is an optional parameter, if it is set, it will be available in the `on` functions.
374+
373375
### webhooks.on()
374376

375377
```js
@@ -420,7 +422,7 @@ webhooks.on(eventNames, handler);
420422
<strong>Required.</strong>
421423
Method to be run each time the event with the passed name is received.
422424
the <code>handler</code> function can be an async function, throw an error or
423-
return a Promise. The handler is called with an event object: <code>{id, name, payload}</code>.
425+
return a Promise. The handler is called with an event object: <code>{id, name, payload, originalRequest}</code>.
424426
</td>
425427
</tr>
426428
</tbody>
@@ -449,7 +451,7 @@ webhooks.onAny(handler);
449451
<strong>Required.</strong>
450452
Method to be run each time any event is received.
451453
the <code>handler</code> function can be an async function, throw an error or
452-
return a Promise. The handler is called with an event object: <code>{id, name, payload}</code>.
454+
return a Promise. The handler is called with an event object: <code>{id, name, payload, originalRequest}</code>.
453455
</td>
454456
</tr>
455457
</tbody>
@@ -482,7 +484,7 @@ Asynchronous `error` event handler are not blocking the `.receive()` method from
482484
<strong>Required.</strong>
483485
Method to be run each time a webhook event handler throws an error or returns a promise that rejects.
484486
The <code>handler</code> function can be an async function,
485-
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>.
487+
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>.
486488
</td>
487489
</tr>
488490
</tbody>
@@ -586,6 +588,29 @@ createServer(middleware).listen(3000);
586588
Custom path to match requests against. Defaults to <code>/api/github/webhooks</code>.
587589
</td>
588590
</tr>
591+
<tr>
592+
<td>
593+
<code>pathRegex</code>
594+
<em>
595+
RegEx
596+
</em>
597+
</td>
598+
<td>
599+
600+
Custom path matcher. If set, overrides the <code>path</code> option.
601+
Can be used as;
602+
603+
```js
604+
const middleware = createNodeMiddleware(
605+
webhooks,
606+
{ pathRegex: /^\/api\/github\/webhooks/ }
607+
);
608+
```
609+
610+
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)!
611+
612+
</td>
613+
</tr>
589614
<tr>
590615
<td>
591616
<code>log</code>
@@ -721,7 +746,7 @@ A union of all possible events and event/action combinations supported by the ev
721746

722747
### `EmitterWebhookEvent`
723748

724-
The object that is emitted by `@octokit/webhooks` as an event; made up of an `id`, `name`, and `payload` properties.
749+
The object that is emitted by `@octokit/webhooks` as an event; made up of an `id`, `name`, and `payload` properties, with an optional `originalRequest`.
725750
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">`.
726751

727752
## License

src/middleware/node/get-missing-headers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// remove type imports from http for Deno compatibility
22
// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
33
// import { IncomingMessage } from "http";
4-
type IncomingMessage = any;
4+
5+
import { IncomingMessage } from "./middleware";
56

67
const WEBHOOK_HEADERS = [
78
"x-github-event",

src/middleware/node/get-payload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { WebhookEvent } from "@octokit/webhooks-types";
22
// @ts-ignore to address #245
33
import AggregateError from "aggregate-error";
4+
import { IncomingMessage } from "./middleware";
45

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

1515
export function getPayload(request: IncomingMessage): Promise<WebhookEvent> {
1616
// If request.body already exists we can stop here

src/middleware/node/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ export function createNodeMiddleware(
88
webhooks: Webhooks,
99
{
1010
path = "/api/github/webhooks",
11+
pathRegex = undefined,
1112
onUnhandledRequest = onUnhandledRequestDefault,
1213
log = createLogger(),
1314
}: MiddlewareOptions = {}
1415
) {
1516
return middleware.bind(null, webhooks, {
1617
path,
18+
pathRegex,
1719
onUnhandledRequest,
1820
log,
1921
} as Required<MiddlewareOptions>);

src/middleware/node/middleware.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
11
// remove type imports from http for Deno compatibility
22
// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
33
// import { IncomingMessage, ServerResponse } from "http";
4-
type IncomingMessage = any;
5-
type ServerResponse = any;
4+
export interface IncomingMessage {
5+
headers: Record<string, string | string[] | undefined>;
6+
body?: any;
7+
url?: string;
8+
method?: string;
9+
setEncoding(enc: string): void;
10+
on(event: string, callback: (data: any) => void): void;
11+
}
12+
export interface ServerResponse {
13+
set statusCode(value: number);
14+
end(body: string): void;
15+
writeHead(status: number, headers?: Record<string, string>): void;
16+
}
617

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

@@ -33,8 +44,10 @@ export async function middleware(
3344
);
3445
return;
3546
}
36-
37-
const isUnknownRoute = request.method !== "POST" || pathname !== options.path;
47+
const pathMatch = options.pathRegex
48+
? options.pathRegex.test(pathname)
49+
: pathname === options.path;
50+
const isUnknownRoute = request.method !== "POST" || !pathMatch;
3851
const isExpressMiddleware = typeof next === "function";
3952
if (isUnknownRoute) {
4053
if (isExpressMiddleware) {
@@ -72,7 +85,7 @@ export async function middleware(
7285
didTimeout = true;
7386
response.statusCode = 202;
7487
response.end("still processing\n");
75-
}, 9000).unref();
88+
}, 9000);
7689

7790
try {
7891
const payload = await getPayload(request);
@@ -82,6 +95,7 @@ export async function middleware(
8295
name: eventName as any,
8396
payload: payload as any,
8497
signature: signatureSHA256,
98+
originalRequest: request,
8599
});
86100
clearTimeout(timeout);
87101

src/middleware/node/on-unhandled-request-default.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// remove type imports from http for Deno compatibility
22
// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
33
// import { IncomingMessage, ServerResponse } from "http";
4-
type IncomingMessage = any;
5-
type ServerResponse = any;
4+
5+
import { IncomingMessage, ServerResponse } from "./middleware";
66

77
export function onUnhandledRequestDefault(
88
request: IncomingMessage,

src/middleware/node/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// remove type imports from http for Deno compatibility
22
// see https://github.com/octokit/octokit.js/issues/24#issuecomment-817361886
33
// import { IncomingMessage, ServerResponse } from "http";
4-
type IncomingMessage = any;
5-
type ServerResponse = any;
64

75
import { Logger } from "../../createLogger";
6+
import { IncomingMessage, ServerResponse } from "./middleware";
87

98
export type MiddlewareOptions = {
109
path?: string;
10+
pathRegex?: RegExp;
1111
log?: Logger;
1212
onUnhandledRequest?: (
1313
request: IncomingMessage,

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import type {
55
} from "@octokit/webhooks-types";
66
import { Logger } from "./createLogger";
77
import type { emitterEventNames } from "./generated/webhook-names";
8+
import { IncomingMessage } from "./middleware/node/middleware";
89

910
export type EmitterWebhookEventName = typeof emitterEventNames[number];
1011
export type EmitterWebhookEvent<
1112
TEmitterEvent extends EmitterWebhookEventName = EmitterWebhookEventName
1213
> = TEmitterEvent extends `${infer TWebhookEvent}.${infer TAction}`
1314
? BaseWebhookEvent<Extract<TWebhookEvent, WebhookEventName>> & {
1415
payload: { action: TAction };
16+
} & {
17+
originalRequest?: IncomingMessage;
1518
}
1619
: BaseWebhookEvent<Extract<TEmitterEvent, WebhookEventName>>;
1720

@@ -20,6 +23,7 @@ export type EmitterWebhookEventWithStringPayloadAndSignature = {
2023
name: EmitterWebhookEventName;
2124
payload: string;
2225
signature: string;
26+
originalRequest?: IncomingMessage;
2327
};
2428

2529
export type EmitterWebhookEventWithSignature = EmitterWebhookEvent & {
@@ -30,6 +34,7 @@ interface BaseWebhookEvent<TName extends WebhookEventName> {
3034
id: string;
3135
name: TName;
3236
payload: WebhookEventMap[TName];
37+
originalRequest?: IncomingMessage;
3338
}
3439

3540
export interface Options<TTransformed = unknown> {

src/verify-and-receive.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ export async function verifyAndReceive(
3939
typeof event.payload === "string"
4040
? JSON.parse(event.payload)
4141
: event.payload,
42+
originalRequest: event.originalRequest,
4243
});
4344
}

test/integration/node-middleware.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,117 @@ describe("createNodeMiddleware(webhooks)", () => {
6262
server.close();
6363
});
6464

65+
test("pathRegex match", async () => {
66+
expect.assertions(7);
67+
68+
const webhooks = new Webhooks({
69+
secret: "mySecret",
70+
});
71+
72+
const server = createServer(
73+
createNodeMiddleware(webhooks, {
74+
pathRegex: /^\/api\/github\/webhooks/,
75+
})
76+
).listen();
77+
78+
// @ts-expect-error complains about { port } although it's included in returned AddressInfo interface
79+
const { port } = server.address();
80+
81+
webhooks.on("push", (event) => {
82+
expect(event.id).toBe("123e4567-e89b-12d3-a456-426655440000");
83+
});
84+
85+
const response1 = await fetch(
86+
`http://localhost:${port}/api/github/webhooks/0001/testurl`,
87+
{
88+
method: "POST",
89+
headers: {
90+
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
91+
"X-GitHub-Event": "push",
92+
"X-Hub-Signature-256": signatureSha256,
93+
},
94+
body: pushEventPayload,
95+
}
96+
);
97+
98+
expect(response1.status).toEqual(200);
99+
await expect(response1.text()).resolves.toBe("ok\n");
100+
101+
const response2 = await fetch(
102+
`http://localhost:${port}/api/github/webhooks/0001/testurl`,
103+
{
104+
method: "POST",
105+
headers: {
106+
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
107+
"X-GitHub-Event": "push",
108+
"X-Hub-Signature-256": signatureSha256,
109+
},
110+
body: pushEventPayload,
111+
}
112+
);
113+
114+
expect(response2.status).toEqual(200);
115+
await expect(response2.text()).resolves.toBe("ok\n");
116+
117+
const response3 = await fetch(`http://localhost:${port}/api/github/web`, {
118+
method: "POST",
119+
headers: {
120+
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
121+
"X-GitHub-Event": "push",
122+
"X-Hub-Signature-256": signatureSha256,
123+
},
124+
body: pushEventPayload,
125+
});
126+
127+
expect(response3.status).toEqual(404);
128+
129+
server.close();
130+
});
131+
132+
test("original request passed by as intented", async () => {
133+
expect.assertions(6);
134+
135+
const webhooks = new Webhooks({
136+
secret: "mySecret",
137+
});
138+
139+
const server = createServer(
140+
createNodeMiddleware(webhooks, {
141+
pathRegex: /^\/api\/github\/webhooks/,
142+
})
143+
).listen();
144+
145+
// @ts-expect-error complains about { port } although it's included in returned AddressInfo interface
146+
const { port } = server.address();
147+
148+
webhooks.on("push", (event) => {
149+
expect(event.id).toBe("123e4567-e89b-12d3-a456-426655440000");
150+
const r = event.originalRequest;
151+
expect(r).toBeDefined();
152+
expect(r?.headers["my-custom-header"]).toBe("customHeader");
153+
expect(r?.url).toBe(`/api/github/webhooks/0001/testurl`);
154+
});
155+
156+
const response = await fetch(
157+
`http://localhost:${port}/api/github/webhooks/0001/testurl`,
158+
{
159+
method: "POST",
160+
headers: {
161+
"X-GitHub-Delivery": "123e4567-e89b-12d3-a456-426655440000",
162+
"X-GitHub-Event": "push",
163+
"X-Hub-Signature-256": signatureSha256,
164+
"my-custom-header": "customHeader",
165+
},
166+
body: pushEventPayload,
167+
}
168+
);
169+
170+
expect(response.status).toEqual(200);
171+
await expect(response.text()).resolves.toBe("ok\n");
172+
173+
server.close();
174+
});
175+
65176
test("request.body already parsed (e.g. Lambda)", async () => {
66177
expect.assertions(3);
67178

0 commit comments

Comments
 (0)