UNPKG

@octokit/webhooks

Version:

GitHub webhook events toolset for Node.js

680 lines (665 loc) 18.9 kB
// pkg/dist-src/createLogger.js var createLogger = (logger) => ({ debug: () => { }, info: () => { }, warn: console.warn.bind(console), error: console.error.bind(console), ...logger }); // pkg/dist-src/generated/webhook-names.js var emitterEventNames = [ "branch_protection_configuration", "branch_protection_configuration.disabled", "branch_protection_configuration.enabled", "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", "custom_property", "custom_property.created", "custom_property.deleted", "custom_property.updated", "custom_property_values", "custom_property_values.updated", "delete", "dependabot_alert", "dependabot_alert.auto_dismissed", "dependabot_alert.auto_reopened", "dependabot_alert.created", "dependabot_alert.dismissed", "dependabot_alert.fixed", "dependabot_alert.reintroduced", "dependabot_alert.reopened", "deploy_key", "deploy_key.created", "deploy_key.deleted", "deployment", "deployment.created", "deployment_protection_rule", "deployment_protection_rule.requested", "deployment_review", "deployment_review.approved", "deployment_review.rejected", "deployment_review.requested", "deployment_status", "deployment_status.created", "discussion", "discussion.answered", "discussion.category_changed", "discussion.closed", "discussion.created", "discussion.deleted", "discussion.edited", "discussion.labeled", "discussion.locked", "discussion.pinned", "discussion.reopened", "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", "installation_target", "installation_target.renamed", "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", "merge_group", "merge_group.checks_requested", "merge_group.destroyed", "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", "personal_access_token_request", "personal_access_token_request.approved", "personal_access_token_request.cancelled", "personal_access_token_request.created", "personal_access_token_request.denied", "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", "projects_v2.closed", "projects_v2.created", "projects_v2.deleted", "projects_v2.edited", "projects_v2.reopened", "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.demilestoned", "pull_request.dequeued", "pull_request.edited", "pull_request.enqueued", "pull_request.labeled", "pull_request.locked", "pull_request.milestoned", "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", "registry_package", "registry_package.published", "registry_package.updated", "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_advisory", "repository_advisory.published", "repository_advisory.reported", "repository_dispatch", "repository_dispatch.sample.collected", "repository_import", "repository_ruleset", "repository_ruleset.created", "repository_ruleset.deleted", "repository_ruleset.edited", "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", "secret_scanning_alert.revoked", "secret_scanning_alert.validated", "secret_scanning_alert_location", "secret_scanning_alert_location.created", "security_advisory", "security_advisory.published", "security_advisory.updated", "security_advisory.withdrawn", "security_and_analysis", "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_job.waiting", "workflow_run", "workflow_run.completed", "workflow_run.in_progress", "workflow_run.requested" ]; // pkg/dist-src/event-handler/on.js 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); } // pkg/dist-src/event-handler/wrap-error-handler.js function wrapErrorHandler(handler, error) { let returnValue; try { returnValue = handler(error); } catch (error2) { console.log('FATAL: Error occurred in "error" event handler'); console.log(error2); } if (returnValue && returnValue.catch) { returnValue.catch((error2) => { console.log('FATAL: Error occurred in "error" event handler'); console.log(error2); }); } } // pkg/dist-src/event-handler/receive.js 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)); } function receiverHandle(state, event) { const errorHandlers = state.hooks.error || []; if (event instanceof Error) { const error = Object.assign(new AggregateError([event], event.message), { event }); errorHandlers.forEach((handler) => wrapErrorHandler(handler, error)); return Promise.reject(error); } if (!event || !event.name) { const error = new Error("Event name not passed"); throw new AggregateError([error], error.message); } if (!event.payload) { const error = new Error("Event name not passed"); throw new AggregateError([error], error.message); } 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((event2) => { return handler(event2); }).catch((error) => errors.push(Object.assign(error, { event }))); }); return Promise.all(promises).then(() => { if (errors.length === 0) { return; } const error = new AggregateError( errors, errors.map((error2) => error2.message).join("\n") ); Object.assign(error, { event }); errorHandlers.forEach((handler) => wrapErrorHandler(handler, error)); throw error; }); } // pkg/dist-src/event-handler/remove-listener.js function removeListener(state, webhookNameOrNames, handler) { if (Array.isArray(webhookNameOrNames)) { webhookNameOrNames.forEach( (webhookName) => removeListener(state, webhookName, handler) ); return; } if (!state.hooks[webhookNameOrNames]) { return; } for (let i = state.hooks[webhookNameOrNames].length - 1; i >= 0; i--) { if (state.hooks[webhookNameOrNames][i] === handler) { state.hooks[webhookNameOrNames].splice(i, 1); return; } } } // pkg/dist-src/event-handler/index.js 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) }; } // pkg/dist-src/index.js import { sign, verify as verify2 } from "@octokit/webhooks-methods"; // pkg/dist-src/verify-and-receive.js import { verify } from "@octokit/webhooks-methods"; async function verifyAndReceive(state, event) { const matchesSignature = await verify( state.secret, event.payload, event.signature ).catch(() => false); 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 }) ); } let payload; try { payload = JSON.parse(event.payload); } catch (error) { error.message = "Invalid JSON"; error.status = 400; throw new AggregateError([error], error.message); } return state.eventHandler.receive({ id: event.id, name: event.name, payload }); } // pkg/dist-src/middleware/node/get-missing-headers.js var WEBHOOK_HEADERS = [ "x-github-event", "x-hub-signature-256", "x-github-delivery" ]; function getMissingHeaders(request) { return WEBHOOK_HEADERS.filter((header) => !(header in request.headers)); } // pkg/dist-src/middleware/node/get-payload.js function getPayload(request) { if (typeof request.body === "object" && "rawBody" in request && request.rawBody instanceof Buffer) { return Promise.resolve(request.rawBody.toString("utf8")); } else if (typeof request.body === "string") { return Promise.resolve(request.body); } return new Promise((resolve, reject) => { let data = []; request.on( "error", (error) => reject(new AggregateError([error], error.message)) ); request.on("data", (chunk) => data.push(chunk)); request.on( "end", () => ( // setImmediate improves the throughput by reducing the pressure from // the event loop setImmediate( resolve, data.length === 1 ? data[0].toString("utf8") : Buffer.concat(data).toString("utf8") ) ) ); }); } // pkg/dist-src/middleware/node/on-unhandled-request-default.js function onUnhandledRequestDefault(request, response) { response.writeHead(404, { "content-type": "application/json" }); response.end( JSON.stringify({ error: `Unknown route: ${request.method} ${request.url}` }) ); } // pkg/dist-src/middleware/node/middleware.js 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 true; } if (pathname !== options.path) { next?.(); return false; } else if (request.method !== "POST") { onUnhandledRequestDefault(request, response); return true; } if (!request.headers["content-type"] || !request.headers["content-type"].startsWith("application/json")) { response.writeHead(415, { "content-type": "application/json", accept: "application/json" }); response.end( JSON.stringify({ error: `Unsupported "Content-Type" header value. Must be "application/json"` }) ); return true; } const missingHeaders = getMissingHeaders(request).join(", "); if (missingHeaders) { response.writeHead(400, { "content-type": "application/json" }); response.end( JSON.stringify({ error: `Required headers missing: ${missingHeaders}` }) ); return true; } 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})`); let didTimeout = false; const timeout = setTimeout(() => { didTimeout = true; response.statusCode = 202; response.end("still processing\n"); }, 9e3).unref(); try { const payload = await getPayload(request); await webhooks.verifyAndReceive({ id, name: eventName, payload, signature: signatureSHA256 }); clearTimeout(timeout); if (didTimeout) return true; response.end("ok\n"); return true; } catch (error) { clearTimeout(timeout); if (didTimeout) return true; const err = Array.from(error.errors)[0]; const errorMessage = err.message ? `${err.name}: ${err.message}` : "Error: An Unspecified error occurred"; response.statusCode = typeof err.status !== "undefined" ? err.status : 500; options.log.error(error); response.end( JSON.stringify({ error: errorMessage }) ); return true; } } // pkg/dist-src/middleware/node/index.js function createNodeMiddleware(webhooks, { path = "/api/github/webhooks", log = createLogger() } = {}) { return middleware.bind(null, webhooks, { path, log }); } // pkg/dist-src/index.js var Webhooks = class { sign; verify; on; onAny; onError; removeListener; receive; verifyAndReceive; 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 = verify2.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); } }; export { Webhooks, createEventHandler, createNodeMiddleware, emitterEventNames };