UNPKG

hook-engine

Version:

Production-grade webhook engine with comprehensive adapter support, security, reliability, structured logging, and CLI tools.

416 lines (415 loc) 15.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GitHubAdvancedAdapter = void 0; const crypto_1 = __importDefault(require("crypto")); const base_advanced_1 = require("./base-advanced"); /** * Advanced GitHub webhook adapter with batch processing, filtering, routing, and multi-tenancy * Supports all GitHub webhook events with advanced processing capabilities */ class GitHubAdvancedAdapter extends base_advanced_1.BaseAdvancedAdapter { getSignature(req) { return req.headers["x-hub-signature-256"] || req.headers["x-hub-signature"]; } verifySignature(rawBody, sigHeader, secret) { if (!sigHeader) return false; try { let signature; let algorithm; if (sigHeader.startsWith('sha256=')) { algorithm = 'sha256'; signature = sigHeader.slice(7); } else if (sigHeader.startsWith('sha1=')) { algorithm = 'sha1'; signature = sigHeader.slice(5); } else { return false; } const expected = crypto_1.default .createHmac(algorithm, secret) .update(rawBody) .digest("hex"); return crypto_1.default.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); } catch (error) { console.error('GitHub signature verification error:', error); return false; } } parsePayload(body) { return JSON.parse(body.toString("utf8")); } /** * Parse batch payload - GitHub doesn't natively support batches, * but we can handle arrays of events */ parseBatchPayload(rawBody) { try { const parsed = JSON.parse(rawBody.toString()); // GitHub sends individual events, but we can handle arrays for testing/custom scenarios return Array.isArray(parsed) ? parsed : [parsed]; } catch (error) { throw new Error(`Failed to parse GitHub batch payload: ${error.message}`); } } normalize(event, options) { const action = event.action || 'unknown'; let type = 'unknown'; let id = ''; let timestamp = Math.floor(Date.now() / 1000); let payload = {}; let priority = 'normal'; let tags = []; // Handle different GitHub event types with enhanced metadata if (event.zen) { type = 'ping'; id = `ping_${event.hook_id || Date.now()}`; payload = { zen: event.zen, hook_id: event.hook_id }; priority = 'low'; tags = ['system', 'ping']; } else if (event.commits) { type = 'push'; id = event.head_commit?.id || event.after || `push_${Date.now()}`; timestamp = event.head_commit?.timestamp ? Math.floor(new Date(event.head_commit.timestamp).getTime() / 1000) : timestamp; payload = { ref: event.ref, before: event.before, after: event.after, commits: event.commits, head_commit: event.head_commit, repository: event.repository, pusher: event.pusher, sender: event.sender, forced: event.forced, created: event.created, deleted: event.deleted }; // Set priority based on branch and commit count if (event.ref === 'refs/heads/main' || event.ref === 'refs/heads/master') { priority = 'high'; tags.push('main-branch'); } if (event.commits && event.commits.length > 10) { priority = 'high'; tags.push('large-push'); } if (event.forced) { priority = 'critical'; tags.push('force-push'); } tags.push('code-change', 'git'); } else if (event.pull_request) { type = `pull_request.${action}`; id = `pr_${event.pull_request.id}_${action}`; timestamp = Math.floor(new Date(event.pull_request.updated_at).getTime() / 1000); payload = { action, number: event.pull_request.number, pull_request: event.pull_request, repository: event.repository, sender: event.sender, changes: event.changes }; // Set priority based on PR state and labels if (action === 'opened' || action === 'ready_for_review') { priority = 'high'; tags.push('review-needed'); } if (event.pull_request.draft) { priority = 'low'; tags.push('draft'); } if (event.pull_request.labels?.some((label) => ['urgent', 'critical', 'hotfix'].includes(label.name.toLowerCase()))) { priority = 'critical'; tags.push('urgent'); } tags.push('pull-request', 'code-review'); } else if (event.issue) { type = `issues.${action}`; id = `issue_${event.issue.id}_${action}`; timestamp = Math.floor(new Date(event.issue.updated_at).getTime() / 1000); payload = { action, issue: event.issue, repository: event.repository, sender: event.sender, assignee: event.assignee, label: event.label, changes: event.changes }; // Set priority based on issue labels and state if (event.issue.labels?.some((label) => ['bug', 'critical', 'urgent'].includes(label.name.toLowerCase()))) { priority = 'high'; tags.push('bug'); } if (event.issue.labels?.some((label) => ['security', 'vulnerability'].includes(label.name.toLowerCase()))) { priority = 'critical'; tags.push('security'); } tags.push('issue', 'project-management'); } else if (event.workflow_run) { type = `workflow_run.${action}`; id = `workflow_${event.workflow_run.id}_${action}`; timestamp = Math.floor(new Date(event.workflow_run.updated_at).getTime() / 1000); payload = { action, workflow_run: event.workflow_run, repository: event.repository, sender: event.sender }; // Set priority based on workflow status and branch if (event.workflow_run.conclusion === 'failure') { priority = 'high'; tags.push('build-failure'); } if (event.workflow_run.head_branch === 'main' || event.workflow_run.head_branch === 'master') { priority = 'high'; tags.push('main-branch'); } tags.push('ci-cd', 'automation'); } else if (event.release) { type = `release.${action}`; id = `release_${event.release.id}_${action}`; timestamp = Math.floor(new Date(event.release.published_at || event.release.created_at).getTime() / 1000); payload = { action, release: event.release, repository: event.repository, sender: event.sender }; if (action === 'published') { priority = 'high'; tags.push('deployment'); } if (event.release.prerelease) { tags.push('prerelease'); } else { tags.push('stable-release'); } tags.push('release', 'versioning'); } else { // Generic fallback with basic categorization type = action ? `${this.getEventCategory(event)}.${action}` : 'unknown'; id = `github_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; payload = event; tags.push('generic'); } // Extract tenant information let tenant = options?.tenant; if (!tenant) { tenant = this.extractTenant(event); } return { id, type, source: "github", timestamp, payload, tenant, priority: options?.priority || priority, tags: [...(options?.tags || []), ...tags], metadata: { ...options?.metadata, repository: event.repository?.full_name, sender: event.sender?.login, action }, raw: options?.includeRaw ? JSON.stringify(event) : '' }; } /** * Extract tenant from GitHub event */ extractTenant(event, req) { // Try organization first if (event.organization?.login) { return event.organization.login; } // Try repository owner if (event.repository?.owner?.login) { return event.repository.owner.login; } // Try installation (for GitHub Apps) if (event.installation?.account?.login) { return event.installation.account.login; } // Try sender as fallback if (event.sender?.login) { return event.sender.login; } return super.extractTenant(event, req); } /** * Validate tenant access for GitHub events */ validateTenant(tenant, event) { // GitHub-specific tenant validation const repoOwner = event.metadata?.repository?.split('/')[0]; const organization = event.payload.organization?.login; // Tenant must match repository owner or organization return tenant === repoOwner || tenant === organization || super.validateTenant(tenant, event); } /** * GitHub-specific event filtering with repository and organization awareness */ filterEvents(events, filter) { let filteredEvents = super.filterEvents(events, filter); // Additional GitHub-specific filters if (filter.repositories) { filteredEvents = filteredEvents.filter(event => { const repo = event.metadata?.repository; return repo && filter.repositories.includes(repo); }); } if (filter.organizations) { filteredEvents = filteredEvents.filter(event => { const org = event.payload.organization?.login || event.metadata?.repository?.split('/')[0]; return org && filter.organizations.includes(org); }); } if (filter.branches) { filteredEvents = filteredEvents.filter(event => { const branch = event.payload.ref?.replace('refs/heads/', '') || event.payload.pull_request?.head?.ref || event.payload.workflow_run?.head_branch; return branch && filter.branches.includes(branch); }); } return filteredEvents; } /** * GitHub-specific event routing with smart defaults */ routeEvent(event, routes) { const matchingRoutes = super.routeEvent(event, routes); // Add GitHub-specific routing logic const enhancedRoutes = matchingRoutes.map(route => { // Add GitHub context to route metadata const githubContext = { repository: event.metadata?.repository, organization: event.payload.organization?.login, sender: event.metadata?.sender, branch: this.extractBranch(event), isMainBranch: this.isMainBranch(event), isCritical: event.priority === 'critical' }; return { ...route, metadata: { ...route.metadata, github: githubContext } }; }); return enhancedRoutes; } /** * Process GitHub event with repository-specific logic */ async processEvent(event, timeout) { // GitHub-specific processing logic const startTime = Date.now(); try { // Simulate repository-specific processing if (event.type.startsWith('push')) { await this.processPushEvent(event); } else if (event.type.startsWith('pull_request')) { await this.processPullRequestEvent(event); } else if (event.type.startsWith('workflow_run')) { await this.processWorkflowEvent(event); } else { await super.processEvent(event, timeout); } } catch (error) { console.error(`GitHub event processing failed for ${event.id}:`, error); throw error; } } /** * Process push events with commit analysis */ async processPushEvent(event) { // Simulate commit analysis const commits = event.payload.commits || []; const analysisTime = Math.min(commits.length * 10, 100); // Max 100ms await new Promise(resolve => setTimeout(resolve, analysisTime)); } /** * Process pull request events with review analysis */ async processPullRequestEvent(event) { // Simulate PR analysis const changedFiles = event.payload.pull_request?.changed_files || 1; const analysisTime = Math.min(changedFiles * 5, 50); // Max 50ms await new Promise(resolve => setTimeout(resolve, analysisTime)); } /** * Process workflow events with build analysis */ async processWorkflowEvent(event) { // Simulate workflow analysis const duration = event.payload.workflow_run?.run_duration_ms || 1000; const analysisTime = Math.min(duration / 100, 30); // Max 30ms await new Promise(resolve => setTimeout(resolve, analysisTime)); } /** * Get event category for unknown events */ getEventCategory(event) { if (event.check_run || event.check_suite) return 'check'; if (event.deployment) return 'deployment'; if (event.star) return 'star'; if (event.fork) return 'fork'; if (event.member) return 'member'; if (event.organization) return 'organization'; return 'unknown'; } /** * Extract branch from event */ extractBranch(event) { return event.payload.ref?.replace('refs/heads/', '') || event.payload.pull_request?.head?.ref || event.payload.workflow_run?.head_branch; } /** * Check if event is from main branch */ isMainBranch(event) { const branch = this.extractBranch(event); return branch === 'main' || branch === 'master'; } } exports.GitHubAdvancedAdapter = GitHubAdvancedAdapter; // Create and export the advanced GitHub adapter instance const githubAdvanced = new GitHubAdvancedAdapter(); exports.default = githubAdvanced;