@octokit/webhooks
Version:
GitHub webhook events toolset for Node.js
423 lines (350 loc) • 18 kB
JavaScript
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var AggregateError = _interopDefault(require('aggregate-error'));
var webhooksMethods = require('@octokit/webhooks-methods');
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
if (enumerableOnly) {
symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
});
}
keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
if (i % 2) {
ownKeys(Object(source), true).forEach(function (key) {
_defineProperty(target, key, source[key]);
});
} else if (Object.getOwnPropertyDescriptors) {
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
} else {
ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
}
return target;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
const createLogger = logger => _objectSpread2({
debug: () => {},
info: () => {},
warn: console.warn.bind(console),
error: console.error.bind(console)
}, logger);
// THIS FILE IS GENERATED - DO NOT EDIT DIRECTLY
// make edits in scripts/generate-types.ts
const emitterEventNames = ["branch_protection_rule", "branch_protection_rule.created", "branch_protection_rule.deleted", "branch_protection_rule.edited", "check_run", "check_run.completed", "check_run.created", "check_run.requested_action", "check_run.rerequested", "check_suite", "check_suite.completed", "check_suite.requested", "check_suite.rerequested", "code_scanning_alert", "code_scanning_alert.appeared_in_branch", "code_scanning_alert.closed_by_user", "code_scanning_alert.created", "code_scanning_alert.fixed", "code_scanning_alert.reopened", "code_scanning_alert.reopened_by_user", "commit_comment", "commit_comment.created", "create", "delete", "deploy_key", "deploy_key.created", "deploy_key.deleted", "deployment", "deployment.created", "deployment_status", "deployment_status.created", "discussion", "discussion.answered", "discussion.category_changed", "discussion.created", "discussion.deleted", "discussion.edited", "discussion.labeled", "discussion.locked", "discussion.pinned", "discussion.transferred", "discussion.unanswered", "discussion.unlabeled", "discussion.unlocked", "discussion.unpinned", "discussion_comment", "discussion_comment.created", "discussion_comment.deleted", "discussion_comment.edited", "fork", "github_app_authorization", "github_app_authorization.revoked", "gollum", "installation", "installation.created", "installation.deleted", "installation.new_permissions_accepted", "installation.suspend", "installation.unsuspend", "installation_repositories", "installation_repositories.added", "installation_repositories.removed", "issue_comment", "issue_comment.created", "issue_comment.deleted", "issue_comment.edited", "issues", "issues.assigned", "issues.closed", "issues.deleted", "issues.demilestoned", "issues.edited", "issues.labeled", "issues.locked", "issues.milestoned", "issues.opened", "issues.pinned", "issues.reopened", "issues.transferred", "issues.unassigned", "issues.unlabeled", "issues.unlocked", "issues.unpinned", "label", "label.created", "label.deleted", "label.edited", "marketplace_purchase", "marketplace_purchase.cancelled", "marketplace_purchase.changed", "marketplace_purchase.pending_change", "marketplace_purchase.pending_change_cancelled", "marketplace_purchase.purchased", "member", "member.added", "member.edited", "member.removed", "membership", "membership.added", "membership.removed", "meta", "meta.deleted", "milestone", "milestone.closed", "milestone.created", "milestone.deleted", "milestone.edited", "milestone.opened", "org_block", "org_block.blocked", "org_block.unblocked", "organization", "organization.deleted", "organization.member_added", "organization.member_invited", "organization.member_removed", "organization.renamed", "package", "package.published", "package.updated", "page_build", "ping", "project", "project.closed", "project.created", "project.deleted", "project.edited", "project.reopened", "project_card", "project_card.converted", "project_card.created", "project_card.deleted", "project_card.edited", "project_card.moved", "project_column", "project_column.created", "project_column.deleted", "project_column.edited", "project_column.moved", "projects_v2_item", "projects_v2_item.archived", "projects_v2_item.converted", "projects_v2_item.created", "projects_v2_item.deleted", "projects_v2_item.edited", "projects_v2_item.reordered", "projects_v2_item.restored", "public", "pull_request", "pull_request.assigned", "pull_request.auto_merge_disabled", "pull_request.auto_merge_enabled", "pull_request.closed", "pull_request.converted_to_draft", "pull_request.edited", "pull_request.labeled", "pull_request.locked", "pull_request.opened", "pull_request.ready_for_review", "pull_request.reopened", "pull_request.review_request_removed", "pull_request.review_requested", "pull_request.synchronize", "pull_request.unassigned", "pull_request.unlabeled", "pull_request.unlocked", "pull_request_review", "pull_request_review.dismissed", "pull_request_review.edited", "pull_request_review.submitted", "pull_request_review_comment", "pull_request_review_comment.created", "pull_request_review_comment.deleted", "pull_request_review_comment.edited", "pull_request_review_thread", "pull_request_review_thread.resolved", "pull_request_review_thread.unresolved", "push", "release", "release.created", "release.deleted", "release.edited", "release.prereleased", "release.published", "release.released", "release.unpublished", "repository", "repository.archived", "repository.created", "repository.deleted", "repository.edited", "repository.privatized", "repository.publicized", "repository.renamed", "repository.transferred", "repository.unarchived", "repository_dispatch", "repository_import", "repository_vulnerability_alert", "repository_vulnerability_alert.create", "repository_vulnerability_alert.dismiss", "repository_vulnerability_alert.reopen", "repository_vulnerability_alert.resolve", "secret_scanning_alert", "secret_scanning_alert.created", "secret_scanning_alert.reopened", "secret_scanning_alert.resolved", "security_advisory", "security_advisory.performed", "security_advisory.published", "security_advisory.updated", "security_advisory.withdrawn", "sponsorship", "sponsorship.cancelled", "sponsorship.created", "sponsorship.edited", "sponsorship.pending_cancellation", "sponsorship.pending_tier_change", "sponsorship.tier_changed", "star", "star.created", "star.deleted", "status", "team", "team.added_to_repository", "team.created", "team.deleted", "team.edited", "team.removed_from_repository", "team_add", "watch", "watch.started", "workflow_dispatch", "workflow_job", "workflow_job.completed", "workflow_job.in_progress", "workflow_job.queued", "workflow_run", "workflow_run.completed", "workflow_run.requested"];
function handleEventHandlers(state, webhookName, handler) {
if (!state.hooks[webhookName]) {
state.hooks[webhookName] = [];
}
state.hooks[webhookName].push(handler);
}
function receiverOn(state, webhookNameOrNames, handler) {
if (Array.isArray(webhookNameOrNames)) {
webhookNameOrNames.forEach(webhookName => receiverOn(state, webhookName, handler));
return;
}
if (["*", "error"].includes(webhookNameOrNames)) {
const webhookName = webhookNameOrNames === "*" ? "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/)`);
}
handleEventHandlers(state, webhookNameOrNames, handler);
}
function receiverOnAny(state, handler) {
handleEventHandlers(state, "*", handler);
}
function receiverOnError(state, handler) {
handleEventHandlers(state, "error", handler);
}
// Errors thrown or rejected Promises in "error" event handlers are not handled
// as they are in the webhook event handlers. If errors occur, we log a
// "Fatal: Error occurred" message to stdout
function wrapErrorHandler(handler, error) {
let returnValue;
try {
returnValue = handler(error);
} catch (error) {
console.log('FATAL: Error occurred in "error" event handler');
console.log(error);
}
if (returnValue && returnValue.catch) {
returnValue.catch(error => {
console.log('FATAL: Error occurred in "error" event handler');
console.log(error);
});
}
}
// @ts-ignore to address #245
function getHooks(state, eventPayloadAction, eventName) {
const hooks = [state.hooks[eventName], state.hooks["*"]];
if (eventPayloadAction) {
hooks.unshift(state.hooks[`${eventName}.${eventPayloadAction}`]);
}
return [].concat(...hooks.filter(Boolean));
} // main handler function
function receiverHandle(state, event) {
const errorHandlers = state.hooks.error || [];
if (event instanceof Error) {
const error = Object.assign(new AggregateError([event]), {
event,
errors: [event]
});
errorHandlers.forEach(handler => wrapErrorHandler(handler, error));
return Promise.reject(error);
}
if (!event || !event.name) {
throw new AggregateError(["Event name not passed"]);
}
if (!event.payload) {
throw new AggregateError(["Event payload not passed"]);
} // flatten arrays of event listeners and remove undefined values
const hooks = getHooks(state, "action" in event.payload ? event.payload.action : null, event.name);
if (hooks.length === 0) {
return Promise.resolve();
}
const errors = [];
const promises = hooks.map(handler => {
let promise = Promise.resolve(event);
if (state.transform) {
promise = promise.then(state.transform);
}
return promise.then(event => {
return handler(event);
}).catch(error => errors.push(Object.assign(error, {
event
})));
});
return Promise.all(promises).then(() => {
if (errors.length === 0) {
return;
}
const error = new AggregateError(errors);
Object.assign(error, {
event,
errors
});
errorHandlers.forEach(handler => wrapErrorHandler(handler, error));
throw error;
});
}
function removeListener(state, webhookNameOrNames, handler) {
if (Array.isArray(webhookNameOrNames)) {
webhookNameOrNames.forEach(webhookName => removeListener(state, webhookName, handler));
return;
}
if (!state.hooks[webhookNameOrNames]) {
return;
} // remove last hook that has been added, that way
// it behaves the same as removeListener
for (let i = state.hooks[webhookNameOrNames].length - 1; i >= 0; i--) {
if (state.hooks[webhookNameOrNames][i] === handler) {
state.hooks[webhookNameOrNames].splice(i, 1);
return;
}
}
}
function createEventHandler(options) {
const state = {
hooks: {},
log: createLogger(options && options.log)
};
if (options && options.transform) {
state.transform = options.transform;
}
return {
on: receiverOn.bind(null, state),
onAny: receiverOnAny.bind(null, state),
onError: receiverOnError.bind(null, state),
removeListener: removeListener.bind(null, state),
receive: receiverHandle.bind(null, state)
};
}
/**
* GitHub sends its JSON with an indentation of 2 spaces and a line break at the end
*/
function toNormalizedJsonString(payload) {
const payloadString = JSON.stringify(payload);
return payloadString.replace(/[^\\]\\u[\da-f]{4}/g, s => {
return s.substr(0, 3) + s.substr(3).toUpperCase();
});
}
async function sign(secret, payload) {
return webhooksMethods.sign(secret, typeof payload === "string" ? payload : toNormalizedJsonString(payload));
}
async function verify(secret, payload, signature) {
return webhooksMethods.verify(secret, typeof payload === "string" ? payload : toNormalizedJsonString(payload), signature);
}
async function verifyAndReceive(state, event) {
// verify will validate that the secret is not undefined
const matchesSignature = await webhooksMethods.verify(state.secret, typeof event.payload === "object" ? toNormalizedJsonString(event.payload) : event.payload, event.signature);
if (!matchesSignature) {
const error = new Error("[@octokit/webhooks] signature does not match event payload and secret");
return state.eventHandler.receive(Object.assign(error, {
event,
status: 400
}));
}
return state.eventHandler.receive({
id: event.id,
name: event.name,
payload: typeof event.payload === "string" ? JSON.parse(event.payload) : event.payload
});
}
const WEBHOOK_HEADERS = ["x-github-event", "x-hub-signature-256", "x-github-delivery"]; // https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#delivery-headers
function getMissingHeaders(request) {
return WEBHOOK_HEADERS.filter(header => !(header in request.headers));
}
// @ts-ignore to address #245
function getPayload(request) {
// If request.body already exists we can stop here
// See https://github.com/octokit/webhooks.js/pull/23
if (request.body) return Promise.resolve(request.body);
return new Promise((resolve, reject) => {
let data = "";
request.setEncoding("utf8"); // istanbul ignore next
request.on("error", error => reject(new AggregateError([error])));
request.on("data", chunk => data += chunk);
request.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (error) {
error.message = "Invalid JSON";
error.status = 400;
reject(new AggregateError([error]));
}
});
});
}
async function middleware(webhooks, options, request, response, next) {
let pathname;
try {
pathname = new URL(request.url, "http://localhost").pathname;
} catch (error) {
response.writeHead(422, {
"content-type": "application/json"
});
response.end(JSON.stringify({
error: `Request URL could not be parsed: ${request.url}`
}));
return;
}
const isUnknownRoute = request.method !== "POST" || pathname !== options.path;
const isExpressMiddleware = typeof next === "function";
if (isUnknownRoute) {
if (isExpressMiddleware) {
return next();
} else {
return options.onUnhandledRequest(request, response);
}
}
const missingHeaders = getMissingHeaders(request).join(", ");
if (missingHeaders) {
response.writeHead(400, {
"content-type": "application/json"
});
response.end(JSON.stringify({
error: `Required headers missing: ${missingHeaders}`
}));
return;
}
const eventName = request.headers["x-github-event"];
const signatureSHA256 = request.headers["x-hub-signature-256"];
const id = request.headers["x-github-delivery"];
options.log.debug(`${eventName} event received (id: ${id})`); // GitHub will abort the request if it does not receive a response within 10s
// See https://github.com/octokit/webhooks.js/issues/185
let didTimeout = false;
const timeout = setTimeout(() => {
didTimeout = true;
response.statusCode = 202;
response.end("still processing\n");
}, 9000).unref();
try {
const payload = await getPayload(request);
await webhooks.verifyAndReceive({
id: id,
name: eventName,
payload: payload,
signature: signatureSHA256
});
clearTimeout(timeout);
if (didTimeout) return;
response.end("ok\n");
} catch (error) {
clearTimeout(timeout);
if (didTimeout) return;
const statusCode = Array.from(error)[0].status;
response.statusCode = typeof statusCode !== "undefined" ? statusCode : 500;
response.end(String(error));
}
}
function onUnhandledRequestDefault(request, response) {
response.writeHead(404, {
"content-type": "application/json"
});
response.end(JSON.stringify({
error: `Unknown route: ${request.method} ${request.url}`
}));
}
function createNodeMiddleware(webhooks, {
path = "/api/github/webhooks",
onUnhandledRequest = onUnhandledRequestDefault,
log = createLogger()
} = {}) {
return middleware.bind(null, webhooks, {
path,
onUnhandledRequest,
log
});
}
class Webhooks {
constructor(options) {
if (!options || !options.secret) {
throw new Error("[@octokit/webhooks] options.secret required");
}
const state = {
eventHandler: createEventHandler(options),
secret: options.secret,
hooks: {},
log: createLogger(options.log)
};
this.sign = sign.bind(null, options.secret);
this.verify = verify.bind(null, options.secret);
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);
}
}
exports.Webhooks = Webhooks;
exports.createEventHandler = createEventHandler;
exports.createNodeMiddleware = createNodeMiddleware;
exports.emitterEventNames = emitterEventNames;
//# sourceMappingURL=index.js.map
;