UNPKG

@octokit/webhooks

Version:

GitHub webhook events toolset for Node.js

423 lines (350 loc) 18 kB
'use strict'; 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