diff --git a/README.md b/README.md index 915d9283..0dc64acc 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ const webhooks = new Webhooks({ secret: "mysecret", }); -webhooks.onAny(({ id, name, payload }) => { +webhooks.onAny(({ id, name, payload, extraData }) => { console.log(name, "event received"); }); @@ -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, extraData, signature }); ``` @@ -316,7 +316,7 @@ eventHandler ### webhooks.receive() ```js -webhooks.receive({ id, name, payload }); +webhooks.receive({ id, name, payload, extraData }); ```
@@ -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 `extraData` is an optional parameter, if it is set, it will be available in the `on` functions. + ### webhooks.on() ```js @@ -420,7 +422,7 @@ webhooks.on(eventNames, handler); Required. Method to be run each time the event with the passed name is received. the handler function can be an async function, throw an error or - return a Promise. The handler is called with an event object: {id, name, payload}. + return a Promise. The handler is called with an event object: {id, name, payload, extraData}. @@ -449,7 +451,7 @@ webhooks.onAny(handler); Required. Method to be run each time any event is received. the handler function can be an async function, throw an error or - return a Promise. The handler is called with an event object: {id, name, payload}. + return a Promise. The handler is called with an event object: {id, name, payload, extraData}. @@ -482,7 +484,7 @@ Asynchronous `error` event handler are not blocking the `.receive()` method from Required. Method to be run each time a webhook event handler throws an error or returns a promise that rejects. The handler 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: {id, name, payload}. + return a Promise. The handler is called with an error object that has a .event property which has all the information on the event: {id, name, payload, extraData}. @@ -579,21 +581,32 @@ createServer(middleware).listen(3000); - - - - + + + +
path - string + string | RegEx Custom path to match requests against. Defaults to /api/github/webhooks. -
- log - - object - - + +Can be used as a regular expression; + +```js +const middleware = createNodeMiddleware(webhooks, { + path: /^\/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)! + +
+log + +object + + Used for internal logging. Defaults to [`console`](https://developer.mozilla.org/en-US/docs/Web/API/console) with `debug` and `info` doing nothing. @@ -721,7 +734,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 `extraData`. 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 diff --git a/src/middleware/node/middleware.ts b/src/middleware/node/middleware.ts index 95a25cf3..bd46651d 100644 --- a/src/middleware/node/middleware.ts +++ b/src/middleware/node/middleware.ts @@ -33,8 +33,11 @@ export async function middleware( ); return; } - - const isUnknownRoute = request.method !== "POST" || pathname !== options.path; + const pathMatch = + options.path instanceof RegExp + ? options.path.test(pathname) + : pathname === options.path; + const isUnknownRoute = request.method !== "POST" || !pathMatch; const isExpressMiddleware = typeof next === "function"; if (isUnknownRoute) { if (isExpressMiddleware) { @@ -72,7 +75,12 @@ export async function middleware( didTimeout = true; response.statusCode = 202; response.end("still processing\n"); - }, 9000).unref(); + }, 9000); + + /* istanbul ignore else */ + if (typeof timeout.unref === "function") { + timeout.unref(); + } try { const payload = await getPayload(request); @@ -82,6 +90,7 @@ export async function middleware( name: eventName as any, payload: payload as any, signature: signatureSHA256, + extraData: request, }); clearTimeout(timeout); diff --git a/src/middleware/node/types.ts b/src/middleware/node/types.ts index 81c4e0ed..b8b49069 100644 --- a/src/middleware/node/types.ts +++ b/src/middleware/node/types.ts @@ -7,7 +7,7 @@ type ServerResponse = any; import { Logger } from "../../createLogger"; export type MiddlewareOptions = { - path?: string; + path?: string | RegExp; log?: Logger; onUnhandledRequest?: ( request: IncomingMessage, diff --git a/src/types.ts b/src/types.ts index a2eedbe4..2e96619c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,8 @@ export type EmitterWebhookEvent< > = TEmitterEvent extends `${infer TWebhookEvent}.${infer TAction}` ? BaseWebhookEvent> & { payload: { action: TAction }; + } & { + extraData?: any; } : BaseWebhookEvent>; @@ -20,6 +22,7 @@ export type EmitterWebhookEventWithStringPayloadAndSignature = { name: EmitterWebhookEventName; payload: string; signature: string; + extraData?: any; }; export type EmitterWebhookEventWithSignature = EmitterWebhookEvent & { @@ -30,6 +33,7 @@ interface BaseWebhookEvent { id: string; name: TName; payload: WebhookEventMap[TName]; + extraData?: any; } export interface Options { diff --git a/src/verify-and-receive.ts b/src/verify-and-receive.ts index 06a59449..5d8adc76 100644 --- a/src/verify-and-receive.ts +++ b/src/verify-and-receive.ts @@ -39,5 +39,6 @@ export async function verifyAndReceive( typeof event.payload === "string" ? JSON.parse(event.payload) : event.payload, + extraData: event.extraData, }); } diff --git a/test/integration/node-middleware.test.ts b/test/integration/node-middleware.test.ts index 6af202c1..65a4258e 100644 --- a/test/integration/node-middleware.test.ts +++ b/test/integration/node-middleware.test.ts @@ -62,6 +62,117 @@ describe("createNodeMiddleware(webhooks)", () => { server.close(); }); + test("path match with regex", async () => { + expect.assertions(7); + + const webhooks = new Webhooks({ + secret: "mySecret", + }); + + const server = createServer( + createNodeMiddleware(webhooks, { + path: /^\/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 intended", async () => { + expect.assertions(6); + + const webhooks = new Webhooks({ + secret: "mySecret", + }); + + const server = createServer( + createNodeMiddleware(webhooks, { + path: /^\/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.extraData; + 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);