Repository: octokit/webhooks.js (ts)

Issue:
`verifyAndReceive` should accept raw string payload
<!-- Please replace all placeholders such as this below -->

**What happened?**
Using `verifyAndReceive` method with a payload in `string` format does not work

Details here: https://github.com/probot/probot/pull/1467#issuecomment-767944136

**What did you expect to happen?**
`verifyAndReceive` should accept the raw `payload` payload string besides the parsed object

**What the problem might be**
To improve signature of `verifyAndReceive` to support both types

Pull request description:
feat:`octokit.verifyAndReceive()` accepts raw string payload
fixes #420


-----
[View rendered README.md](https://github.com/octokit/webhooks.js/blob/420/verifyAndReceive-with-string-payload/README.md)

Patch:
diff --git a/README.md b/README.md
index d35d0b9..8041cf3 100644
--- a/README.md
+++ b/README.md
@@ -189,7 +189,7 @@ webhooks.verify(eventPayload, signature);
           eventPayload
         </code>
         <em>
-          (Object)
+          (Object or String)
         </em>
       </td>
       <td>
@@ -260,7 +260,7 @@ webhooks.verifyAndReceive({ id, name, payload, signature });
           payload
         </code>
         <em>
-          Object
+          Object or String
         </em>
       </td>
       <td>
diff --git a/src/index.ts b/src/index.ts
index b8e1da5..057a947 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,6 +11,8 @@ import {
   State,
   WebhookError,
   WebhookEventHandlerError,
+  EmitterWebhookEventWithStringPayloadAndSignature,
+  EmitterWebhookEventWithSignature,
 } from "./types";
 
 export { createNodeMiddleware } from "./middleware/node/index";
@@ -34,7 +36,9 @@ class Webhooks<TTransformed = unknown> {
   ) => void;
   public receive: (event: EmitterWebhookEvent) => Promise<void>;
   public verifyAndReceive: (
-    options: EmitterWebhookEvent & { signature: string }
+    options:
+      | EmitterWebhookEventWithStringPayloadAndSignature
+      | EmitterWebhookEventWithSignature
   ) => Promise<void>;
 
   constructor(options: Options<TTransformed> & { secret: string }) {
diff --git a/src/types.ts b/src/types.ts
index b9b3978..4d713b6 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -15,6 +15,17 @@ export type EmitterWebhookEvent<
     }
   : BaseWebhookEvent<Extract<TEmitterEvent, WebhookEventName>>;
 
+export type EmitterWebhookEventWithStringPayloadAndSignature = {
+  id: string;
+  name: EmitterWebhookEventName;
+  payload: string;
+  signature: string;
+};
+
+export type EmitterWebhookEventWithSignature = EmitterWebhookEvent & {
+  signature: string;
+};
+
 interface BaseWebhookEvent<TName extends WebhookEventName> {
   id: string;
   name: TName;
diff --git a/src/verify-and-receive.ts b/src/verify-and-receive.ts
index f2ae5d0..6b6ac57 100644
--- a/src/verify-and-receive.ts
+++ b/src/verify-and-receive.ts
@@ -1,10 +1,16 @@
 import { verify } from "@octokit/webhooks-methods";
 
-import { EmitterWebhookEvent, State } from "./types";
+import {
+  EmitterWebhookEventWithStringPayloadAndSignature,
+  EmitterWebhookEventWithSignature,
+  State,
+} from "./types";
 
 export async function verifyAndReceive(
   state: State & { secret: string },
-  event: EmitterWebhookEvent & { signature: string }
+  event:
+    | EmitterWebhookEventWithStringPayloadAndSignature
+    | EmitterWebhookEventWithSignature
 ): Promise<any> {
   // verify will validate that the secret is not undefined
   const matchesSignature = await verify(
@@ -26,6 +32,9 @@ export async function verifyAndReceive(
   return state.eventHandler.receive({
     id: event.id,
     name: event.name,
-    payload: event.payload,
+    payload:
+      typeof event.payload === "string"
+        ? JSON.parse(event.payload)
+        : event.payload,
   });
 }


Test patch:
diff --git a/test/integration/webhooks.test.ts b/test/integration/webhooks.test.ts
index 324776d..295eb3a 100644
--- a/test/integration/webhooks.test.ts
+++ b/test/integration/webhooks.test.ts
@@ -1,5 +1,14 @@
+import { readFileSync } from "fs";
+
+import { sign } from "@octokit/webhooks-methods";
+
 import { Webhooks, EmitterWebhookEvent } from "../../src";
 
+const pushEventPayloadString = readFileSync(
+  "test/fixtures/push-payload.json",
+  "utf-8"
+);
+
 describe("Webhooks", () => {
   test("new Webhooks() without secret option", () => {
     // @ts-expect-error
@@ -8,6 +17,31 @@ describe("Webhooks", () => {
     );
   });
 
+  test("webhooks.verify(payload, signature) with string payload", async () => {
+    const secret = "mysecret";
+    const webhooks = new Webhooks({ secret });
+
+    await webhooks.verify(
+      pushEventPayloadString,
+      await sign({ secret, algorithm: "sha256" }, pushEventPayloadString)
+    );
+  });
+
+  test("webhooks.verifyAndReceive({ ...event, signature }) with string payload", async () => {
+    const secret = "mysecret";
+    const webhooks = new Webhooks({ secret });
+
+    await webhooks.verifyAndReceive({
+      id: "1",
+      name: "push",
+      payload: pushEventPayloadString,
+      signature: await sign(
+        { secret, algorithm: "sha256" },
+        pushEventPayloadString
+      ),
+    });
+  });
+
   test("webhooks.verifyAndReceive(event) with incorrect signature", async () => {
     const webhooks = new Webhooks({ secret: "mysecret" });