Skip to content

feat: Add webhook sender worker for delivering event notifications#528

Merged
neSpecc merged 16 commits intomasterfrom
feat/hawk-webhook-worker
Feb 18, 2026
Merged

feat: Add webhook sender worker for delivering event notifications#528
neSpecc merged 16 commits intomasterfrom
feat/hawk-webhook-worker

Conversation

@Dobrunia
Copy link
Member

@Dobrunia Dobrunia commented Feb 16, 2026

Summary

  • Add a new sender/webhook worker that delivers event notifications as JSON POST requests to user-configured endpoint URLs
  • Register Webhook in the ChannelType enum so the notifier dispatches webhook tasks automatically
  • Add run-webhook script and docker-compose service

@codex-assistant
Copy link

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.

@codex-assistant
Copy link

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.

@codex-assistant
Copy link

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.

@codex-assistant
Copy link

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.

@codex-assistant
Copy link

Thanks for adding a description — the PR is now marked as Ready for Review.

@codex-assistant codex-assistant bot marked this pull request as ready for review February 16, 2026 19:13
@Dobrunia Dobrunia changed the title feat: add hawk-worker-webhook service and update package.json feat: Add webhook sender worker for delivering event notifications Feb 16, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 157 to 158
const url = new URL(endpoint);

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
neSpecc
neSpecc previously approved these changes Feb 18, 2026
@neSpecc neSpecc requested a review from Copilot February 18, 2026 14:19
…es for project, workspace, user, event, and plan DTOs
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 51 to 86
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();
}
);

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment on lines 72 to 77
if (status >= HttpStatusCode.MultipleChoices && status <= HttpStatusCode.PermanentRedirect) {
this.logger.log('error', `Webhook blocked — redirect ${status} to ${res.headers.location} from ${endpoint}`);
resolve();

return;
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 21 to 27
jest.mock('./../src/deliverer.ts', () => {
return jest.fn().mockImplementation(() => {
return {
deliver: deliver,
};
});
});
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
"license": "MIT",
"workerType": "sender/webhook",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jest"

Copilot uses AI. Check for mistakes.
neSpecc
neSpecc previously approved these changes Feb 18, 2026
@neSpecc neSpecc merged commit f4342de into master Feb 18, 2026
5 checks passed
@neSpecc neSpecc deleted the feat/hawk-webhook-worker branch February 18, 2026 14:55
Dobrunia added a commit that referenced this pull request Feb 18, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments