Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
312676a
feat: add hawk-worker-webhook service and update package.json
Dobrunia Feb 16, 2026
5fff413
chore: update @hawk.so/types to version 0.5.9 in yarn.lock
Dobrunia Feb 16, 2026
ca0efca
refactor: improve webhook delivery mechanism with HTTP/HTTPS support …
Dobrunia Feb 16, 2026
676ca6c
refactor: replace magic number with constant for HTTP error status in…
Dobrunia Feb 16, 2026
a4bb2cf
feat(webhook): enhance webhook payload structure and add new test com…
Dobrunia Feb 17, 2026
1f21746
feat(webhook): unify webhook delivery structure and remove deprecated…
Dobrunia Feb 17, 2026
c89717b
feat(webhook): expand internal fields filtering in webhook payload
Dobrunia Feb 18, 2026
8acee14
feat(webhook): add private IP checks and DNS validation for webhook d…
Dobrunia Feb 18, 2026
8973e26
feat(webhook): implement regex-based private IP checks and enhance no…
Dobrunia Feb 18, 2026
746efc1
refactor(webhook): simplify mock implementation for deliverer in tests
Dobrunia Feb 18, 2026
ca2cf91
feat(webhook): implement SSRF mitigations and enhance private IP chec…
Dobrunia Feb 18, 2026
a12b398
refactor(webhook): streamline webhook deliverer by removing private I…
Dobrunia Feb 18, 2026
da94019
refactor(webhook): replace magic number with constant for delivery ti…
Dobrunia Feb 18, 2026
77d1f32
refactor(webhook): enhance data projection by using specific DB schem…
Dobrunia Feb 18, 2026
ef40375
feat(webhook): add assignee information to webhook payload and update…
Dobrunia Feb 18, 2026
9a2c1a4
refactor(webhook): improve redirect handling in webhook deliverer and…
Dobrunia Feb 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ services:
- ./:/usr/src/app
- workers-deps:/usr/src/app/node_modules

hawk-worker-webhook:
build:
dockerfile: "dev.Dockerfile"
context: .
env_file:
- .env
restart: unless-stopped
entrypoint: yarn run-webhook
volumes:
- ./:/usr/src/app
- workers-deps:/usr/src/app/node_modules

volumes:
workers-deps:

Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hawk.workers",
"private": true,
"version": "0.1.2",
"version": "0.1.3",
"description": "Hawk workers",
"repository": "git@github.com:codex-team/hawk.workers.git",
"license": "BUSL-1.1",
Expand Down Expand Up @@ -34,6 +34,7 @@
"test:notifier": "jest workers/notifier",
"test:js": "jest workers/javascript",
"test:task-manager": "jest workers/task-manager",
"test:webhook": "jest workers/webhook",
"test:clear": "jest --clearCache",
"run-default": "yarn worker hawk-worker-default",
"run-sentry": "yarn worker hawk-worker-sentry",
Expand All @@ -49,13 +50,14 @@
"run-email": "yarn worker hawk-worker-email",
"run-telegram": "yarn worker hawk-worker-telegram",
"run-limiter": "yarn worker hawk-worker-limiter",
"run-task-manager": "yarn worker hawk-worker-task-manager"
"run-task-manager": "yarn worker hawk-worker-task-manager",
"run-webhook": "yarn worker hawk-worker-webhook"
},
"dependencies": {
"@babel/parser": "^7.26.9",
"@babel/traverse": "7.26.9",
"@hawk.so/nodejs": "^3.1.1",
"@hawk.so/types": "^0.5.7",
"@hawk.so/types": "^0.5.9",
"@types/amqplib": "^0.8.2",
"@types/jest": "^29.5.14",
"@types/mongodb": "^3.5.15",
Expand Down
7 changes: 6 additions & 1 deletion workers/email/tests/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ nodemailerMock.createTransport = jest.fn(() => ({

jest.mock('nodemailer', () => nodemailerMock);

import { DecodedGroupedEvent, ProjectDBScheme } from '@hawk.so/types';
import { DecodedGroupedEvent, ProjectDBScheme, UserDBScheme } from '@hawk.so/types';
import '../src/env';
import EmailProvider from '../src/provider';
import Templates from '../src/templates/names';
Expand Down Expand Up @@ -187,6 +187,11 @@ describe('EmailProvider', () => {
password: '$argon2i$v=19$m=4096,t=3,p=1$QOo3u8uEor0t+nqCLpEW3g$aTCDEaHht9ro9VZDD1yZGpaTi+g1OWsfHbYd5TQBRPs',
name: 'Hahahawk',
},
assignee: {
_id: new ObjectId('5ec3ffe769c0030022f88f25'),
email: 'assignee@email.ru',
name: 'Assignee User',
} as UserDBScheme,
project: {
_id: new ObjectId('5d206f7f9aaf7c0071d64596'),
token: 'project-token',
Expand Down
1 change: 1 addition & 0 deletions workers/notifier/types/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum ChannelType {
Telegram = 'telegram',
Slack = 'slack',
Loop = 'loop',
Webhook = 'webhook',
}

/**
Expand Down
1 change: 1 addition & 0 deletions workers/sender/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export default abstract class SenderWorker extends Worker {
project,
event,
whoAssigned,
assignee,
daysRepeated,
},
} as AssigneeNotification);
Expand Down
5 changes: 5 additions & 0 deletions workers/sender/types/template-variables/assignee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export interface AssigneeTemplateVariables extends CommonTemplateVariables {
*/
whoAssigned: UserDBScheme;

/**
* User who was assigned to resolve the event
*/
assignee: UserDBScheme;

/**
* Number of event repetitions
*/
Expand Down
11 changes: 11 additions & 0 deletions workers/webhook/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "hawk-worker-webhook",
"version": "1.0.0",
"description": "Webhook sender worker — delivers event notifications as JSON POST requests",
"main": "src/index.ts",
"license": "MIT",
"workerType": "sender/webhook",
"scripts": {
"test": "jest"
}
}
125 changes: 125 additions & 0 deletions workers/webhook/src/deliverer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import https from 'https';
import http from 'http';
import { createLogger, format, Logger, transports } from 'winston';
import { WebhookDelivery } from '../types/template';
import { HttpStatusCode, MS_IN_SEC } from '../../../lib/utils/consts';

/**
* How many seconds to wait for a webhook response before aborting
*/
const DELIVERY_TIMEOUT_SEC = 10;

/**
* Timeout for webhook delivery in milliseconds
*/
const DELIVERY_TIMEOUT_MS = MS_IN_SEC * DELIVERY_TIMEOUT_SEC;

/**
* Deliverer sends JSON POST requests to external webhook endpoints.
*
* SSRF validation is performed at the API layer when the endpoint is saved —
* this class trusts the stored URL and only handles delivery.
*/
export default class WebhookDeliverer {
/**
* Logger module
* (default level='info')
*/
private logger: Logger = createLogger({
level: process.env.LOG_LEVEL || 'info',
transports: [
new transports.Console({
format: format.combine(
format.timestamp(),
format.colorize(),
format.simple(),
format.printf((msg) => `${msg.timestamp} - ${msg.level}: ${msg.message}`)
),
}),
],
});

/**
* Sends webhook delivery to the endpoint via HTTP POST.
* Adds X-Hawk-Notification header with the notification type (similar to GitHub's X-GitHub-Event).
*
* @param endpoint - URL to POST to
* @param delivery - webhook delivery { type, payload }
*/
public async deliver(endpoint: string, delivery: WebhookDelivery): Promise<void> {
let url: URL;

try {
url = new URL(endpoint);
} catch {
this.logger.log('error', `Webhook delivery skipped — invalid URL: ${endpoint}`);

return;
}

if (url.protocol !== 'https:' && url.protocol !== 'http:') {
this.logger.log('error', `Webhook delivery skipped — unsupported protocol: ${url.protocol}`);

return;
}

const body = JSON.stringify(delivery);
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;

const isRedirect = (
status === HttpStatusCode.MovedPermanently ||
status === HttpStatusCode.Found ||
status === HttpStatusCode.SeeOther ||
status === HttpStatusCode.TemporaryRedirect ||
status === HttpStatusCode.PermanentRedirect
) && res.headers.location;

if (isRedirect) {
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();
}
);

req.on('error', (e) => {
this.logger.log('error', `Can't deliver webhook to ${endpoint}: ${e.message}`);
resolve();
});

req.on('timeout', () => {
this.logger.log('error', `Webhook delivery timed out for ${endpoint}`);
req.destroy();
resolve();
});

req.write(body);
req.end();
});
}
}
24 changes: 24 additions & 0 deletions workers/webhook/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as pkg from './../package.json';
import WebhookProvider from './provider';
import SenderWorker from 'hawk-worker-sender/src';
import { ChannelType } from 'hawk-worker-notifier/types/channel';

/**
* Worker to send webhook notifications
*/
export default class WebhookSenderWorker extends SenderWorker {
/**
* Worker type
*/
public readonly type: string = pkg.workerType;

/**
* Webhook channel type
*/
protected channelType = ChannelType.Webhook;

/**
* Webhook provider
*/
protected provider = new WebhookProvider();
}
34 changes: 34 additions & 0 deletions workers/webhook/src/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import NotificationsProvider from 'hawk-worker-sender/src/provider';
import { Notification } from 'hawk-worker-sender/types/template-variables';
import { toDelivery } from './templates';
import WebhookDeliverer from './deliverer';

/**
* Webhook notification provider.
* Supports all notification types via a single generic serializer —
* type comes from notification.type, payload is sanitized automatically.
*/
export default class WebhookProvider extends NotificationsProvider {
/**
* Class with the 'deliver' method for sending HTTP POST requests
*/
private readonly deliverer: WebhookDeliverer;

constructor() {
super();

this.deliverer = new WebhookDeliverer();
}

/**
* Send webhook notification to recipient
*
* @param to - recipient endpoint URL
* @param notification - notification with payload and type
*/
public async send(to: string, notification: Notification): Promise<void> {
const delivery = toDelivery(notification);

await this.deliverer.deliver(to, delivery);
}
}
Loading
Loading