feat: Add webhook sender worker for delivering event notifications#528
feat: Add webhook sender worker for delivering event notifications#528
Conversation
Please add a PR description 🙂Respect the reviewers — a description helps others understand the changes and review them faster. Keep it short, clear, and to the point. It also serves as documentation for future reference. The PR was moved to Draft until a description is added. |
Please add a PR description 🙂Respect the reviewers — a description helps others understand the changes and review them faster. Keep it short, clear, and to the point. It also serves as documentation for future reference. The PR was moved to Draft until a description is added. |
…and timeout handling
Please add a PR description 🙂Respect the reviewers — a description helps others understand the changes and review them faster. Keep it short, clear, and to the point. It also serves as documentation for future reference. The PR was moved to Draft until a description is added. |
… webhook deliverer
Please add a PR description 🙂Respect the reviewers — a description helps others understand the changes and review them faster. Keep it short, clear, and to the point. It also serves as documentation for future reference. The PR was moved to Draft until a description is added. |
|
Thanks for adding a description — the PR is now marked as Ready for Review. |
…tification type handling in tests
…ks for webhook delivery
There was a problem hiding this comment.
Pull request overview
This pull request adds a webhook sender worker that delivers event notifications as JSON POST requests to user-configured endpoint URLs. The implementation follows established patterns for sender workers in the codebase and includes comprehensive SSRF (Server-Side Request Forgery) protections.
Changes:
- Added a new webhook sender worker with comprehensive SSRF protections including protocol/port whitelisting, hostname blocklisting, private IP detection, DNS rebinding prevention, and redirect blocking
- Registered Webhook in the ChannelType enum to enable automatic webhook task dispatching by the notifier
- Added test coverage for payload sanitization and IP validation logic
Reviewed changes
Copilot reviewed 14 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Updated @hawk.so/types from 0.5.7 to 0.5.9 |
| workers/webhook/types/template.d.ts | Defined WebhookDelivery interface for webhook POST body structure |
| workers/webhook/tests/provider.test.ts | Added comprehensive tests for webhook provider including payload sanitization |
| workers/webhook/tests/deliverer.test.ts | Added tests for private/reserved IP detection across IPv4, IPv6, and IPv4-mapped IPv6 |
| workers/webhook/tests/mocks/several-events-notify.ts | Added mock data for several-events notifications |
| workers/webhook/tests/mocks/event-notify.ts | Added mock data for event notifications |
| workers/webhook/tests/mocks/assignee-notify.ts | Added mock data for assignee notifications |
| workers/webhook/src/templates/index.ts | Exported generic template renderer as toDelivery |
| workers/webhook/src/templates/generic.ts | Implemented payload sanitization to strip internal/sensitive fields and convert BSON objects |
| workers/webhook/src/provider.ts | Implemented WebhookProvider extending NotificationsProvider to render and deliver webhooks |
| workers/webhook/src/index.ts | Implemented WebhookSenderWorker extending SenderWorker with webhook channel type |
| workers/webhook/src/deliverer.ts | Implemented WebhookDeliverer with comprehensive SSRF protections and HTTP POST delivery |
| workers/webhook/package.json | Added package configuration with workerType sender/webhook |
| workers/notifier/types/channel.ts | Added Webhook to ChannelType enum for notifier integration |
| package.json | Updated version to 0.1.3, added run-webhook and test:webhook scripts, updated @hawk.so/types dependency |
| docker-compose.dev.yml | Added hawk-worker-webhook service configuration |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
workers/webhook/src/deliverer.ts
Outdated
| const url = new URL(endpoint); | ||
|
|
There was a problem hiding this comment.
The new URL(endpoint) constructor can throw a TypeError if the endpoint is not a valid URL. This should be wrapped in a try-catch block to prevent the worker from crashing. The error should be logged and the method should return gracefully, similar to how other errors are handled in this method.
| const url = new URL(endpoint); | |
| let url: URL; | |
| try { | |
| url = new URL(endpoint); | |
| } catch (error) { | |
| const errorMessage = error instanceof Error ? error.message : String(error); | |
| this.logger.log('error', `Webhook blocked — invalid URL "${endpoint}": ${errorMessage}`); | |
| return; | |
| } |
…P checks and enhancing payload sanitization
…meout in webhook deliverer
…es for project, workspace, user, event, and plan DTOs
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 15 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
workers/webhook/src/deliverer.ts
Outdated
| const url = new URL(endpoint); | ||
| const transport = url.protocol === 'https:' ? https : http; | ||
|
|
||
| return new Promise<void>((resolve) => { | ||
| const req = transport.request( | ||
| url, | ||
| { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'User-Agent': 'Hawk-Webhook/1.0', | ||
| 'X-Hawk-Notification': delivery.type, | ||
| 'Content-Length': Buffer.byteLength(body), | ||
| }, | ||
| timeout: DELIVERY_TIMEOUT_MS, | ||
| }, | ||
| (res) => { | ||
| res.resume(); | ||
|
|
||
| const status = res.statusCode || 0; | ||
|
|
||
| if (status >= HttpStatusCode.MultipleChoices && status <= HttpStatusCode.PermanentRedirect) { | ||
| this.logger.log('error', `Webhook blocked — redirect ${status} to ${res.headers.location} from ${endpoint}`); | ||
| resolve(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (status >= HttpStatusCode.BadRequest) { | ||
| this.logger.log('error', `Webhook delivery failed: ${status} ${res.statusMessage} for ${endpoint}`); | ||
| } | ||
|
|
||
| resolve(); | ||
| } | ||
| ); | ||
|
|
There was a problem hiding this comment.
new URL(endpoint) and http(s).request() can throw synchronously (e.g., invalid URL string or unsupported protocol like "ftp:"), which would escape the Promise and potentially crash the worker. Wrap URL parsing/transport selection in a try/catch and explicitly allow only "http:" and "https:" before proceeding; otherwise log and resolve without throwing.
| const url = new URL(endpoint); | |
| const transport = url.protocol === 'https:' ? https : http; | |
| return new Promise<void>((resolve) => { | |
| const req = transport.request( | |
| url, | |
| { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'User-Agent': 'Hawk-Webhook/1.0', | |
| 'X-Hawk-Notification': delivery.type, | |
| 'Content-Length': Buffer.byteLength(body), | |
| }, | |
| timeout: DELIVERY_TIMEOUT_MS, | |
| }, | |
| (res) => { | |
| res.resume(); | |
| const status = res.statusCode || 0; | |
| if (status >= HttpStatusCode.MultipleChoices && status <= HttpStatusCode.PermanentRedirect) { | |
| this.logger.log('error', `Webhook blocked — redirect ${status} to ${res.headers.location} from ${endpoint}`); | |
| resolve(); | |
| return; | |
| } | |
| if (status >= HttpStatusCode.BadRequest) { | |
| this.logger.log('error', `Webhook delivery failed: ${status} ${res.statusMessage} for ${endpoint}`); | |
| } | |
| resolve(); | |
| } | |
| ); | |
| let url: URL; | |
| let transport: typeof https | typeof http; | |
| try { | |
| url = new URL(endpoint); | |
| } catch (e) { | |
| const message = e instanceof Error ? e.message : String(e); | |
| this.logger.log('error', `Invalid webhook endpoint URL "${endpoint}": ${message}`); | |
| return; | |
| } | |
| if (url.protocol === 'https:') { | |
| transport = https; | |
| } else if (url.protocol === 'http:') { | |
| transport = http; | |
| } else { | |
| this.logger.log('error', `Unsupported webhook protocol "${url.protocol}" for endpoint ${endpoint}`); | |
| return; | |
| } | |
| return new Promise<void>((resolve) => { | |
| let req: http.ClientRequest | https.ClientRequest; | |
| try { | |
| req = transport.request( | |
| url, | |
| { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'User-Agent': 'Hawk-Webhook/1.0', | |
| 'X-Hawk-Notification': delivery.type, | |
| 'Content-Length': Buffer.byteLength(body), | |
| }, | |
| timeout: DELIVERY_TIMEOUT_MS, | |
| }, | |
| (res) => { | |
| res.resume(); | |
| const status = res.statusCode || 0; | |
| if (status >= HttpStatusCode.MultipleChoices && status <= HttpStatusCode.PermanentRedirect) { | |
| this.logger.log('error', `Webhook blocked — redirect ${status} to ${res.headers.location} from ${endpoint}`); | |
| resolve(); | |
| return; | |
| } | |
| if (status >= HttpStatusCode.BadRequest) { | |
| this.logger.log('error', `Webhook delivery failed: ${status} ${res.statusMessage} for ${endpoint}`); | |
| } | |
| resolve(); | |
| } | |
| ); | |
| } catch (e) { | |
| const message = e instanceof Error ? e.message : String(e); | |
| this.logger.log('error', `Can't deliver webhook to ${endpoint}: ${message}`); | |
| resolve(); | |
| return; | |
| } |
workers/webhook/src/deliverer.ts
Outdated
| if (status >= HttpStatusCode.MultipleChoices && status <= HttpStatusCode.PermanentRedirect) { | ||
| this.logger.log('error', `Webhook blocked — redirect ${status} to ${res.headers.location} from ${endpoint}`); | ||
| resolve(); | ||
|
|
||
| return; | ||
| } |
There was a problem hiding this comment.
The current range check blocks all 3xx responses, including non-redirect statuses like 304 (Not Modified). If the intent is to block redirects only, check for redirect-specific status codes (301/302/303/307/308) and/or gate on presence of the Location header instead of a broad 300–308 range.
| jest.mock('./../src/deliverer.ts', () => { | ||
| return jest.fn().mockImplementation(() => { | ||
| return { | ||
| deliver: deliver, | ||
| }; | ||
| }); | ||
| }); |
There was a problem hiding this comment.
This mock path is brittle because the SUT imports the module as ./deliverer from src/provider.ts, while the test mocks ../src/deliverer.ts (different import specifier strings can bypass the mock depending on Jest/ts-jest resolution). Prefer mocking the same specifier that the provider ultimately resolves (e.g., mock ../src/deliverer without the extension), or ensure moduleNameMapper is configured so both resolve to the same module ID.
workers/webhook/package.json
Outdated
| "license": "MIT", | ||
| "workerType": "sender/webhook", | ||
| "scripts": { | ||
| "test": "echo \"Error: no test specified\" && exit 1" |
There was a problem hiding this comment.
This module contains Jest tests under workers/webhook/tests, but its local test script always fails. Update it to run the webhook test suite (consistent with the root test:webhook script) so workspace-level or package-level test runs don’t break unexpectedly.
| "test": "echo \"Error: no test specified\" && exit 1" | |
| "test": "jest" |
… update test mocks
* fix(grouper): remove JSON.stringify of full event payload from logs (#525) * feat: Add webhook sender worker for delivering event notifications (#528) * feat: add hawk-worker-webhook service and update package.json * chore: update @hawk.so/types to version 0.5.9 in yarn.lock * refactor: improve webhook delivery mechanism with HTTP/HTTPS support and timeout handling * refactor: replace magic number with constant for HTTP error status in webhook deliverer * feat(webhook): enhance webhook payload structure and add new test command * feat(webhook): unify webhook delivery structure and remove deprecated templates * feat(webhook): expand internal fields filtering in webhook payload * feat(webhook): add private IP checks and DNS validation for webhook delivery * feat(webhook): implement regex-based private IP checks and enhance notification type handling in tests * refactor(webhook): simplify mock implementation for deliverer in tests * feat(webhook): implement SSRF mitigations and enhance private IP checks for webhook delivery * refactor(webhook): streamline webhook deliverer by removing private IP checks and enhancing payload sanitization * refactor(webhook): replace magic number with constant for delivery timeout in webhook deliverer * refactor(webhook): enhance data projection by using specific DB schemes for project, workspace, user, event, and plan DTOs * feat(webhook): add assignee information to webhook payload and update related tests * refactor(webhook): improve redirect handling in webhook deliverer and update test mocks --------- Co-authored-by: Kuchizu <70284260+Kuchizu@users.noreply.github.com> Co-authored-by: Dobrunia Kostrigin <48620984+Dobrunia@users.noreply.github.com>
Summary