Skip to content

feat: add validate-event-name #1161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- [createNodeMiddleware()](#createnodemiddleware)
- [Webhook events](#webhook-events)
- [emitterEventNames](#emittereventnames)
- [validateEventName](#validateeventname)
- [TypeScript](#typescript)
- [`EmitterWebhookEventName`](#emitterwebhookeventname)
- [`EmitterWebhookEvent`](#emitterwebhookevent)
Expand Down Expand Up @@ -87,18 +88,19 @@ source.onmessage = (event) => {
## API

1. [Constructor](#constructor)
2. [webhooks.sign()](#webhookssign)
3. [webhooks.verify()](#webhooksverify)
4. [webhooks.verifyAndReceive()](#webhooksverifyandreceive)
5. [webhooks.receive()](#webhooksreceive)
6. [webhooks.on()](#webhookson)
7. [webhooks.onAny()](#webhooksonany)
8. [webhooks.onError()](#webhooksonerror)
9. [webhooks.removeListener()](#webhooksremovelistener)
10. [createNodeMiddleware()](#createnodemiddleware)
11. [createWebMiddleware()](#createwebmiddleware)
12. [Webhook events](#webhook-events)
13. [emitterEventNames](#emittereventnames)
1. [webhooks.sign()](#webhookssign)
1. [webhooks.verify()](#webhooksverify)
1. [webhooks.verifyAndReceive()](#webhooksverifyandreceive)
1. [webhooks.receive()](#webhooksreceive)
1. [webhooks.on()](#webhookson)
1. [webhooks.onAny()](#webhooksonany)
1. [webhooks.onError()](#webhooksonerror)
1. [webhooks.removeListener()](#webhooksremovelistener)
1. [createNodeMiddleware()](#createnodemiddleware)
1. [createWebMiddleware()](#createwebmiddleware)
1. [Webhook events](#webhook-events)
1. [emitterEventNames](#emittereventnames)
1. [validateEventName](#validateeventname)

### Constructor

Expand Down Expand Up @@ -724,6 +726,34 @@ import { emitterEventNames } from "@octokit/webhooks";
emitterEventNames; // ["check_run", "check_run.completed", ...]
```

### validateEventName

The function `validateEventName` asserts that the provided event name is a valid event name or event/action combination.
It throws an error if the event name is not valid, or '\*' or 'error' is passed.

The second parameter is an optional options object that can be used to customize the behavior of the validation. You can set
a `onUnknownEventName` property to `"warn"` to log a warning instead of throwing an error, and a `log` property to provide a custom logger object, which should have a `"warn"` method. You can also set `onUnknownEventName` to `"ignore"` to disable logging or throwing an error for unknown event names.

```ts
import { validateEventName } from "@octokit/webhooks";

validateEventName("push"); // no error
validateEventName("invalid_event"); // throws an error
validateEventName("*"); // throws an error
validateEventName("error"); // throws an error

validateEventName("invalid_event", { onUnknownEventName: "warn" }); // logs a warning
validateEventName("invalid_event", {
onUnknownEventName: false,
log: {
warn: console.info, // instead of warning we just log it via console.info
},
});

validateEventName("*", { onUnkownEventName: "ignore" }); // throws an error
validateEventName("invalid_event", { onUnkownEventName: "ignore" }); // no error, no warning
```

## TypeScript

The types for the webhook payloads are sourced from [`@octokit/openapi-webhooks-types`](https://github.com/octokit/openapi-webhooks/tree/main/packages/openapi-webhooks-types),
Expand Down
22 changes: 5 additions & 17 deletions src/event-handler/on.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { emitterEventNames } from "../generated/webhook-names.ts";
import type {
EmitterWebhookEvent,
EmitterWebhookEventName,
State,
WebhookEventHandlerError,
} from "../types.ts";
import { validateEventName } from "./validate-event-name.ts";

function handleEventHandlers(
state: State,
Expand All @@ -29,22 +29,10 @@ export function receiverOn(
return;
}

if (["*", "error"].includes(webhookNameOrNames)) {
const webhookName =
(webhookNameOrNames as string) === "*" ? "any" : webhookNameOrNames;

const message = `Using the "${webhookNameOrNames}" event with the regular Webhooks.on() function is not supported. Please use the Webhooks.on${
webhookName.charAt(0).toUpperCase() + webhookName.slice(1)
}() method instead`;

throw new Error(message);
}

if (!emitterEventNames.includes(webhookNameOrNames)) {
state.log.warn(
`"${webhookNameOrNames}" is not a known webhook name (https://developer.github.com/v3/activity/events/types/)`,
);
}
validateEventName(webhookNameOrNames, {
onUnknownEventName: "warn",
log: state.log,
});

handleEventHandlers(state, webhookNameOrNames, handler);
}
Expand Down
54 changes: 54 additions & 0 deletions src/event-handler/validate-event-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Logger } from "../create-logger.ts";
import { emitterEventNames } from "../generated/webhook-names.ts";
import type { EmitterWebhookEventName } from "../types.ts";

type ValidateEventNameOptions =
| {
onUnknownEventName?: undefined | "throw";
}
| {
onUnknownEventName: "ignore";
}
| {
onUnknownEventName: "warn";
log?: Pick<Logger, "warn">;
};

export function validateEventName<
TOptions extends ValidateEventNameOptions = ValidateEventNameOptions,
>(
eventName: EmitterWebhookEventName | (string & Record<never, never>),
options: TOptions = {} as TOptions,
): asserts eventName is TOptions extends { onUnknownEventName: "throw" }
? EmitterWebhookEventName
: Exclude<string, "*" | "error"> {
if (typeof eventName !== "string") {
throw new TypeError("eventName must be of type string");
}
if (eventName === "*") {
throw new TypeError(
`Using the "*" event with the regular Webhooks.on() function is not supported. Please use the Webhooks.onAny() method instead`,
);
}
if (eventName === "error") {
throw new TypeError(
`Using the "error" event with the regular Webhooks.on() function is not supported. Please use the Webhooks.onError() method instead`,
);
}

if (options.onUnknownEventName === "ignore") {
return;
}

if (!emitterEventNames.includes(eventName as EmitterWebhookEventName)) {
if (options.onUnknownEventName !== "warn") {
throw new TypeError(
`"${eventName}" is not a known webhook name (https://developer.github.com/v3/activity/events/types/)`,
);
} else {
(options.log || console).warn(
`"${eventName}" is not a known webhook name (https://developer.github.com/v3/activity/events/types/)`,
);
}
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
createEventHandler,
type EventHandler,
} from "./event-handler/index.ts";
import { validateEventName } from "./event-handler/validate-event-name.ts";
import { sign, verify } from "@octokit/webhooks-methods";
import { verifyAndReceive } from "./verify-and-receive.ts";
import type {
Expand Down Expand Up @@ -78,6 +79,7 @@ class Webhooks<TTransformed = unknown> {

export {
createEventHandler,
validateEventName,
Webhooks,
type EmitterWebhookEvent,
type EmitterWebhookEventName,
Expand Down
156 changes: 156 additions & 0 deletions test/integration/validate-event-name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { describe, it, assert } from "../testrunner.ts";
import { validateEventName } from "../../src/event-handler/validate-event-name.ts";

describe("validateEventName", () => {
it("validates 'push'", () => {
validateEventName("push");
});

[null, undefined, {}, [], true, false, 1].forEach((value) => {
it(`throws on invalid event name data type - ${JSON.stringify(value)}`, () => {
try {
validateEventName(value as any);
throw new Error("Should have thrown");
} catch (error) {
assert(error instanceof TypeError === true);
assert((error as Error).message === "eventName must be of type string");
}
});
});

it('throws on invalid event name - "error"', () => {
try {
validateEventName("error");
throw new Error("Should have thrown");
} catch (error) {
assert(error instanceof TypeError === true);
assert(
(error as Error).message ===
'Using the "error" event with the regular Webhooks.on() function is not supported. Please use the Webhooks.onError() method instead',
);
}
});

it('throws on invalid event name - "error" with onUnkownEventName set to "warn"', () => {
try {
validateEventName("error", { onUnknownEventName: "warn" });
throw new Error("Should have thrown");
} catch (error) {
assert(error instanceof TypeError === true);
assert(
(error as Error).message ===
'Using the "error" event with the regular Webhooks.on() function is not supported. Please use the Webhooks.onError() method instead',
);
}
});

it('throws on invalid event name - "error" with onUnkownEventName set to "ignore"', () => {
try {
validateEventName("error", { onUnknownEventName: "ignore" });
throw new Error("Should have thrown");
} catch (error) {
assert(error instanceof TypeError === true);
assert(
(error as Error).message ===
'Using the "error" event with the regular Webhooks.on() function is not supported. Please use the Webhooks.onError() method instead',
);
}
});

it('throws on invalid event name - "*"', () => {
try {
validateEventName("*");
throw new Error("Should have thrown");
} catch (error) {
assert(error instanceof TypeError === true);
assert(
(error as Error).message ===
'Using the "*" event with the regular Webhooks.on() function is not supported. Please use the Webhooks.onAny() method instead',
);
}
});

it('throws on invalid event name - "*" with onUnkownEventName set to "warn"', () => {
try {
validateEventName("*", { onUnknownEventName: "warn" });
throw new Error("Should have thrown");
} catch (error) {
assert(error instanceof TypeError === true);
assert(
(error as Error).message ===
'Using the "*" event with the regular Webhooks.on() function is not supported. Please use the Webhooks.onAny() method instead',
);
}
});

it('throws on invalid event name - "*" with onUnkownEventName set to "ignore"', () => {
try {
validateEventName("*", { onUnknownEventName: "ignore" });
throw new Error("Should have thrown");
} catch (error) {
assert(error instanceof TypeError === true);
assert(
(error as Error).message ===
'Using the "*" event with the regular Webhooks.on() function is not supported. Please use the Webhooks.onAny() method instead',
);
}
});

it('throws on invalid event name - "invalid"', () => {
try {
validateEventName("invalid");
throw new Error("Should have thrown");
} catch (error) {
assert(error instanceof TypeError === true);
assert(
(error as Error).message ===
'"invalid" is not a known webhook name (https://developer.github.com/v3/activity/events/types/)',
);
}
});

it('logs on invalid event name - "invalid" and onUnknownEventName is "warn" - console.warn', () => {
const consoleWarn = console.warn;
const logWarnCalls: string[] = [];
console.warn = Array.prototype.push.bind(logWarnCalls);

validateEventName("invalid", { onUnknownEventName: "warn" });
try {
assert(logWarnCalls.length === 1);
assert(
logWarnCalls[0] ===
'"invalid" is not a known webhook name (https://developer.github.com/v3/activity/events/types/)',
);
} catch (error) {
throw error;
} finally {
console.warn = consoleWarn; // restore original console.warn
}
});

it('logs on invalid event name - "invalid" and onUnknownEventName is "warn" - custom logger', () => {
const logWarnCalls: string[] = [];
const log = {
warn: (message: string) => {
logWarnCalls.push(message);
},
};
validateEventName("invalid", { onUnknownEventName: "warn", log });
assert(logWarnCalls.length === 1);
assert(
logWarnCalls[0] ===
'"invalid" is not a known webhook name (https://developer.github.com/v3/activity/events/types/)',
);
});

it('logs nothing on invalid event name - "invalid" and onUnknownEventName is "ignore" - custom logger', () => {
const logWarnCalls: string[] = [];
const log = {
warn: (message: string) => {
logWarnCalls.push(message);
},
};
validateEventName("invalid", { onUnknownEventName: "ignore", log });
assert(logWarnCalls.length === 0);
});
});