Skip to content

Commit

Permalink
feat: deprecate onUnhandledRequest and accepting object payloads fo…
Browse files Browse the repository at this point in the history
…r for `webhooks.verify()` and `webhooks.verifyAndReceive()` (#790)
  • Loading branch information
wolfy1339 authored Jan 4, 2023
1 parent bd7974d commit c9d6a9d
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 67 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ webhooks.verify(eventPayload, signature);
eventPayload
</code>
<em>
(Object or String)
(Object [deprecated] or String)
</em>
</td>
<td>
Expand Down Expand Up @@ -262,7 +262,7 @@ webhooks.verifyAndReceive({ id, name, payload, signature });
payload
</code>
<em>
Object or String
Object (deprecated) or String
</em>
</td>
<td>
Expand Down Expand Up @@ -601,7 +601,7 @@ Used for internal logging. Defaults to [`console`](https://developer.mozilla.org
</tr>
<tr>
<td>
<code>onUnhandledRequest</code>
<code>onUnhandledRequest (deprecated)</code>
<em>
function
</em>
Expand Down
44 changes: 33 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ import {
export { createNodeMiddleware } from "./middleware/node/index";
export { emitterEventNames } from "./generated/webhook-names";

export type Iverify = {
/** @deprecated Passing a JSON payload object to `verify()` is deprecated and the functionality will be removed in a future release of `@octokit/webhooks`. */
(eventPayload: object, signature: string): Promise<boolean>;
(eventPayload: string, signature: string): Promise<boolean>;
};
export type IverifyAndReceive = {
/** @deprecated Passing a JSON payload object to `verifyAndReceive()` is deprecated and the functionality will be removed in a future release of `@octokit/webhooks`. */
(options: EmitterWebhookEventWithSignature): Promise<void>;
(options: EmitterWebhookEventWithStringPayloadAndSignature): Promise<void>;
};

// U holds the return value of `transform` function in Options
class Webhooks<TTransformed = unknown> {
public sign: (payload: string | object) => Promise<string>;
public verify: (
eventPayload: string | object,
signature: string
) => Promise<boolean>;
public verify: Iverify;
public on: <E extends EmitterWebhookEventName>(
event: E | E[],
callback: HandlerFunction<E, TTransformed>
Expand All @@ -37,11 +45,7 @@ class Webhooks<TTransformed = unknown> {
callback: RemoveHandlerFunction<E, TTransformed>
) => void;
public receive: (event: EmitterWebhookEvent) => Promise<void>;
public verifyAndReceive: (
options:
| EmitterWebhookEventWithStringPayloadAndSignature
| EmitterWebhookEventWithSignature
) => Promise<void>;
public verifyAndReceive: IverifyAndReceive;

constructor(options: Options<TTransformed> & { secret: string }) {
if (!options || !options.secret) {
Expand All @@ -56,13 +60,31 @@ class Webhooks<TTransformed = unknown> {
};

this.sign = sign.bind(null, options.secret);
this.verify = verify.bind(null, options.secret);
this.verify = (eventPayload: object | string, signature: string) => {
if (typeof eventPayload === "object") {
console.error(
"[@octokit/webhooks] Passing a JSON payload object to `verify()` is deprecated and the functionality will be removed in a future release of `@octokit/webhooks`"
);
}
return verify(options.secret, eventPayload, signature);
};
this.on = state.eventHandler.on;
this.onAny = state.eventHandler.onAny;
this.onError = state.eventHandler.onError;
this.removeListener = state.eventHandler.removeListener;
this.receive = state.eventHandler.receive;
this.verifyAndReceive = verifyAndReceive.bind(null, state);
this.verifyAndReceive = (
options:
| EmitterWebhookEventWithStringPayloadAndSignature
| EmitterWebhookEventWithSignature
) => {
if (typeof options.payload === "object") {
console.error(
"[@octokit/webhooks] Passing a JSON payload object to `verifyAndReceive()` is deprecated and the functionality will be removed in a future release of `@octokit/webhooks`"
);
}
return verifyAndReceive(state, options);
};
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/middleware/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ export function createNodeMiddleware(
log = createLogger(),
}: MiddlewareOptions = {}
) {
const deprecateOnUnhandledRequest = (request: any, response: any) => {
console.error(
"[@octokit/webhooks] `onUnhandledRequest()` is deprecated and will be removed in a future release of `@octokit/webhooks`"
);
return onUnhandledRequest(request, response);
};
return middleware.bind(null, webhooks, {
path,
onUnhandledRequest,
onUnhandledRequest: deprecateOnUnhandledRequest,
log,
} as Required<MiddlewareOptions>);
}
1 change: 1 addition & 0 deletions src/middleware/node/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Logger } from "../../createLogger";
export type MiddlewareOptions = {
path?: string;
log?: Logger;
/** @deprecated `onUnhandledRequest()` is deprecated and will be removed in a future release of `@octokit/webhooks` */
onUnhandledRequest?: (
request: IncomingMessage,
response: ServerResponse
Expand Down
2 changes: 1 addition & 1 deletion src/to-normalized-json-string.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* GitHub sends its JSON with an indentation of 2 spaces and a line break at the end
* GitHub sends its JSON with no indentation and no line break at the end
*/
export function toNormalizedJsonString(payload: object) {
const payloadString = JSON.stringify(payload);
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type EmitterWebhookEventWithStringPayloadAndSignature = {
payload: string;
signature: string;
};

/** @deprecated */
export type EmitterWebhookEventWithSignature = EmitterWebhookEvent & {
signature: string;
};
Expand Down
37 changes: 0 additions & 37 deletions test/integration/node-middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,43 +168,6 @@ describe("createNodeMiddleware(webhooks)", () => {
server.close();
});

test("custom non-found handler", async () => {
const webhooks = new Webhooks({
secret: "mySecret",
});

const server = createServer(
createNodeMiddleware(webhooks, {
onUnhandledRequest(_request, response) {
response.writeHead(404);
response.end("nope");
},
})
).listen();

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

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

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

await expect(response.text()).resolves.toEqual("nope");

server.close();
});

test("Handles missing headers", async () => {
const webhooks = new Webhooks({
secret: "mySecret",
Expand Down
14 changes: 2 additions & 12 deletions test/integration/webhooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { readFileSync } from "fs";

import { sign } from "@octokit/webhooks-methods";

import { Webhooks, EmitterWebhookEvent } from "../../src";
import { Webhooks } from "../../src";
import { toNormalizedJsonString } from "../../src/to-normalized-json-string";

const pushEventPayloadString = readFileSync(
Expand Down Expand Up @@ -32,16 +32,6 @@ describe("Webhooks", () => {
await webhooks.sign(pushEventPayloadString);
});

test("webhooks.verify(payload, signature) with object payload", async () => {
const secret = "mysecret";
const webhooks = new Webhooks({ secret });

await webhooks.verify(
JSON.parse(pushEventPayloadString),
await sign({ secret, algorithm: "sha256" }, pushEventPayloadString)
);
});

test("webhooks.verify(payload, signature) with object payload containing special characters", async () => {
const secret = "mysecret";
const webhooks = new Webhooks({ secret });
Expand Down Expand Up @@ -84,7 +74,7 @@ describe("Webhooks", () => {
test("webhooks.verifyAndReceive(event) with incorrect signature", async () => {
const webhooks = new Webhooks({ secret: "mysecret" });

const pingPayload = {} as EmitterWebhookEvent<"ping">["payload"];
const pingPayload = "{}";
await expect(async () =>
webhooks.verifyAndReceive({
id: "1",
Expand Down
89 changes: 88 additions & 1 deletion test/unit/deprecation.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,90 @@
import { createServer } from "http";
import { readFileSync } from "fs";

import fetch from "node-fetch";
import { sign } from "@octokit/webhooks-methods";

import { Webhooks, createNodeMiddleware } from "../../src";

const pushEventPayloadString = readFileSync(
"test/fixtures/push-payload.json",
"utf-8"
);
let signatureSha256: string;
describe("Deprecations", () => {
test("there are currently no deprecations", () => {});
beforeAll(async () => {
signatureSha256 = await sign(
{ secret: "mySecret", algorithm: "sha256" },
pushEventPayloadString
);
});
test("onUnhandledRequest", async () => {
const spy = jest.spyOn(console, "error");
const webhooks = new Webhooks({
secret: "mySecret",
});

const server = createServer(
createNodeMiddleware(webhooks, {
onUnhandledRequest(_request, response) {
response.writeHead(404);
response.end("nope");
},
})
).listen();

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

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

expect(spy).toBeCalledWith(
"[@octokit/webhooks] `onUnhandledRequest()` is deprecated and will be removed in a future release of `@octokit/webhooks`"
);
spy.mockClear();
server.close();
});

test("webhooks.verify(payload, signature) with object payload", async () => {
const spy = jest.spyOn(console, "error");
const secret = "mysecret";
const webhooks = new Webhooks({ secret });

await webhooks.verify(
JSON.parse(pushEventPayloadString),
await sign({ secret, algorithm: "sha256" }, pushEventPayloadString)
);
expect(spy).toBeCalledWith(
"[@octokit/webhooks] Passing a JSON payload object to `verify()` is deprecated and the functionality will be removed in a future release of `@octokit/webhooks`"
);
spy.mockClear();
});

test("webhooks.verifyAndReceive(payload, signature) with object payload", async () => {
const spy = jest.spyOn(console, "error");
const secret = "mysecret";
const webhooks = new Webhooks({ secret });

await webhooks.verifyAndReceive({
id: "123e456",
name: "push",
payload: JSON.parse(pushEventPayloadString),
signature: await sign(
{ secret, algorithm: "sha256" },
pushEventPayloadString
),
});
expect(spy).toBeCalledWith(
"[@octokit/webhooks] Passing a JSON payload object to `verifyAndReceive()` is deprecated and the functionality will be removed in a future release of `@octokit/webhooks`"
);
spy.mockClear();
});
});

0 comments on commit c9d6a9d

Please sign in to comment.