hookarmor 1.0.3 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/hookarmor.js CHANGED
@@ -1,133 +1,175 @@
1
- #!/usr/bin/env node
2
-
3
- const { Command } = require('commander');
4
- const _chalk = require('chalk');
5
- const chalk = _chalk.default || _chalk;
6
- const { createServer } = require('../src/server');
7
- const http = require('http');
8
-
9
- const program = new Command();
10
-
11
- program
12
- .name('hookarmor')
13
- .description('šŸ›”ļø HookArmor: Webhook Dead-Letter Queue & Replay Sentinel')
14
- .version('1.0.3');
15
-
16
- program
17
- .command('start')
18
- .description('Start HookArmor server with web dashboard and ingress proxy')
19
- .option('-p, --port <number>', 'Port to listen on', process.env.PORT || '4000')
20
- .action((options) => {
21
- const port = parseInt(options.port, 10);
22
- const { server, storage } = createServer();
23
-
24
- // Ensure a default mock endpoint exists for first-time onboarding
25
- const defaultEp = storage.getEndpoint('demo-stripe');
26
- if (!defaultEp) {
27
- storage.createEndpoint({
28
- id: 'demo-stripe',
29
- name: 'Stripe Payments Demo',
30
- targetUrl: `http://localhost:${port}/mock/target`,
31
- alertWebhookUrl: '',
32
- autoRetry: 1,
33
- maxRetries: 5
34
- });
35
- }
36
-
37
- server.listen(port, () => {
38
- console.log(chalk.blue.bold('\nšŸ›”ļø HookArmor Webhook Sentinel is LIVE!'));
39
- console.log(chalk.gray('─────────────────────────────────────────'));
40
- console.log(`šŸ“” Ingress Gateway : ${chalk.cyan(`http://localhost:${port}/in/:endpointId`)}`);
41
- console.log(`šŸ“Š Web Dashboard : ${chalk.green.bold(`http://localhost:${port}`)}`);
42
- console.log(`āš™ļø Mock Receiver : ${chalk.yellow(`http://localhost:${port}/mock/target`)}`);
43
- console.log(chalk.gray('─────────────────────────────────────────'));
44
- console.log(chalk.white('Press Ctrl+C to stop.\n'));
45
- });
46
- });
47
-
48
- program
49
- .command('listen')
50
- .description('Tunnel incoming webhooks directly to a local or remote target')
51
- .argument('<targetUrl>', 'Destination target URL (e.g. http://localhost:3000/api/webhook)')
52
- .option('-p, --port <number>', 'Proxy port', '4000')
53
- .option('-e, --endpoint <id>', 'Endpoint ID slug', 'local-dev')
54
- .action(async (targetUrl, options) => {
55
- const port = parseInt(options.port, 10);
56
- const { server, storage, dispatcher } = createServer();
57
-
58
- storage.createEndpoint({
59
- id: options.endpoint,
60
- name: `Local Tunnel -> ${targetUrl}`,
61
- targetUrl,
62
- autoRetry: 1,
63
- maxRetries: 5
64
- });
65
-
66
- dispatcher.on('delivery', (res) => {
67
- const statusStr = res.success
68
- ? chalk.green.bold(`[${res.statusCode}] DELIVERED`)
69
- : chalk.red.bold(`[${res.statusCode || 'ERR'}] DEAD-LETTER`);
70
- console.log(`[${new Date().toLocaleTimeString()}] ${statusStr} ${res.eventId} -> ${targetUrl} (${res.latencyMs}ms)`);
71
- if (!res.success && res.errorMessage) {
72
- console.log(chalk.red(` └─ Error: ${res.errorMessage}`));
73
- }
74
- });
75
-
76
- server.listen(port, () => {
77
- console.log(chalk.cyan.bold(`\n⚔ HookArmor Listen Active`));
78
- console.log(`Forwarding: ${chalk.yellow(`http://localhost:${port}/in/${options.endpoint}`)} āž” ${chalk.green(targetUrl)}`);
79
- console.log(`Dashboard: ${chalk.cyan(`http://localhost:${port}`)}\n`);
80
- });
81
- });
82
-
83
- program
84
- .command('replay')
85
- .description('Replay dead-letter events from terminal')
86
- .argument('[eventId]', 'Specific event ID to replay')
87
- .option('--failed', 'Replay all failed events')
88
- .option('-u, --url <url>', 'HookArmor server URL', 'http://localhost:4000')
89
- .action(async (eventId, options) => {
90
- const baseUrl = options.url.replace(/\/$/, '');
91
- try {
92
- if (options.failed || !eventId) {
93
- console.log(chalk.yellow('šŸ”„ Replaying all dead-letter events...'));
94
- const res = await fetch(`${baseUrl}/api/events/replay-all`, { method: 'POST', headers: { 'Content-Type': 'application/json' } });
95
- const data = await res.json();
96
- console.log(chalk.green(`āœ… Dispatched replay for ${data.replayedCount} events.`));
97
- } else {
98
- console.log(chalk.yellow(`šŸ”„ Replaying event ${eventId}...`));
99
- const res = await fetch(`${baseUrl}/api/events/${eventId}/replay`, { method: 'POST' });
100
- const data = await res.json();
101
- if (data.success) {
102
- console.log(chalk.green(`āœ… Replay successful: HTTP ${data.statusCode} (${data.latencyMs}ms)`));
103
- } else {
104
- console.log(chalk.red(`āŒ Replay failed: HTTP ${data.statusCode} (${data.errorMessage || 'Target rejected'})`));
105
- }
106
- }
107
- } catch (err) {
108
- console.error(chalk.red(`Failed to connect to HookArmor at ${baseUrl}: ${err.message}`));
109
- }
110
- });
111
-
112
- program
113
- .command('status')
114
- .description('Check HookArmor metrics and dead-letter queue count')
115
- .option('-u, --url <url>', 'HookArmor server URL', 'http://localhost:4000')
116
- .action(async (options) => {
117
- const baseUrl = options.url.replace(/\/$/, '');
118
- try {
119
- const res = await fetch(`${baseUrl}/api/stats`);
120
- const data = await res.json();
121
- console.log(chalk.blue.bold('\nšŸ›”ļø HookArmor Metrics'));
122
- console.log(chalk.gray('───────────────────────'));
123
- console.log(`Total Ingested : ${chalk.cyan(data.total)}`);
124
- console.log(`Delivered : ${chalk.green(data.delivered)}`);
125
- console.log(`Dead-Letter : ${chalk.red(data.failed)}`);
126
- console.log(`Pending : ${chalk.yellow(data.pending)}`);
127
- console.log(`Avg Latency : ${chalk.white(`${data.avgLatencyMs} ms`)}\n`);
128
- } catch (err) {
129
- console.error(chalk.red(`Failed to connect to HookArmor at ${baseUrl}: ${err.message}`));
130
- }
131
- });
132
-
133
- program.parse();
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const _chalk = require('chalk');
5
+ const chalk = _chalk.default || _chalk;
6
+ const { createServer } = require('../src/server');
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('hookarmor')
12
+ .description('šŸ›”ļø HookArmor: Webhook Dead-Letter Queue & Replay Sentinel')
13
+ .version(require('../package.json').version);
14
+
15
+ // Build the server, exiting with a readable message on configuration errors
16
+ function startServerOrExit() {
17
+ try {
18
+ return createServer();
19
+ } catch (err) {
20
+ console.error(chalk.red.bold(`\nāœ– HookArmor could not start: ${err.message}\n`));
21
+ process.exit(1);
22
+ }
23
+ }
24
+
25
+ function apiHeaders(apiKey) {
26
+ const headers = { 'Content-Type': 'application/json' };
27
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
28
+ return headers;
29
+ }
30
+
31
+ async function readJson(res) {
32
+ const data = await res.json().catch(() => ({}));
33
+ if (!res.ok) {
34
+ const hint = res.status === 401 ? ' (pass --api-key or set HOOKARMOR_API_KEY)' : '';
35
+ throw new Error(`${data.error || `HTTP ${res.status}`}${hint}`);
36
+ }
37
+ return data;
38
+ }
39
+
40
+ program
41
+ .command('start')
42
+ .description('Start HookArmor server with web dashboard and ingress proxy')
43
+ .option('-p, --port <number>', 'Port to listen on', process.env.PORT || '4000')
44
+ .option('-H, --host <host>', 'Host address to bind to', process.env.HOOKARMOR_HOST || process.env.HOST || (process.env.HOOKARMOR_API_KEY || process.env.NODE_ENV === 'production' ? '0.0.0.0' : '127.0.0.1'))
45
+ .action((options) => {
46
+ const port = parseInt(options.port, 10);
47
+ const host = options.host;
48
+ const { server, storage, mockEnabled } = startServerOrExit();
49
+
50
+ // Ensure a default mock endpoint exists for first-time onboarding (local simulation only)
51
+ if (mockEnabled && !storage.getEndpoint('demo-stripe')) {
52
+ storage.createEndpoint({
53
+ id: 'demo-stripe',
54
+ name: 'Stripe Payments Demo',
55
+ targetUrl: `http://localhost:${port}/mock/target`,
56
+ alertWebhookUrl: '',
57
+ autoRetry: 1,
58
+ maxRetries: 5
59
+ });
60
+ }
61
+
62
+ server.listen(port, host, () => {
63
+ console.log(chalk.blue.bold('\nšŸ›”ļø HookArmor Webhook Sentinel is LIVE!'));
64
+ console.log(chalk.gray('─────────────────────────────────────────'));
65
+ const displayHost = (host === '0.0.0.0' || host === '127.0.0.1') ? 'localhost' : host;
66
+ console.log(`šŸ“” Ingress Gateway : ${chalk.cyan(`http://${displayHost}:${port}/in/:endpointId`)}`);
67
+ console.log(`šŸ“Š Web Dashboard : ${chalk.green.bold(`http://${displayHost}:${port}/dashboard`)}`);
68
+ if (mockEnabled) {
69
+ console.log(`āš™ļø Mock Receiver : ${chalk.yellow(`http://${displayHost}:${port}/mock/target`)}`);
70
+ }
71
+ console.log(chalk.gray('─────────────────────────────────────────'));
72
+ console.log(chalk.white('Press Ctrl+C to stop.\n'));
73
+ });
74
+ });
75
+
76
+ program
77
+ .command('listen')
78
+ .description('Tunnel incoming webhooks directly to a local or remote target')
79
+ .argument('<targetUrl>', 'Destination target URL (e.g. http://localhost:3000/api/webhook)')
80
+ .option('-p, --port <number>', 'Proxy port', '4000')
81
+ .option('-e, --endpoint <id>', 'Endpoint ID slug', 'local-dev')
82
+ .option('-s, --secret <secret>', 'Provider signing secret: verify on ingress and re-sign on delivery')
83
+ .action(async (targetUrl, options) => {
84
+ const port = parseInt(options.port, 10);
85
+ const { server, storage, dispatcher } = startServerOrExit();
86
+
87
+ storage.createEndpoint({
88
+ id: options.endpoint,
89
+ name: `Local Tunnel -> ${targetUrl}`,
90
+ targetUrl,
91
+ secret: options.secret,
92
+ autoRetry: 1,
93
+ maxRetries: 5
94
+ });
95
+
96
+ dispatcher.on('delivery', (res) => {
97
+ const statusStr = res.success
98
+ ? chalk.green.bold(`[${res.statusCode}] DELIVERED`)
99
+ : chalk.red.bold(`[${res.statusCode || 'ERR'}] DEAD-LETTER`);
100
+ console.log(`[${new Date().toLocaleTimeString()}] ${statusStr} ${res.eventId} -> ${targetUrl} (${res.latencyMs}ms)`);
101
+ if (!res.success && res.errorMessage) {
102
+ console.log(chalk.red(` └─ Error: ${res.errorMessage}`));
103
+ }
104
+ });
105
+
106
+ server.listen(port, () => {
107
+ console.log(chalk.cyan.bold(`\n⚔ HookArmor Listen Active`));
108
+ console.log(`Forwarding: ${chalk.yellow(`http://localhost:${port}/in/${options.endpoint}`)} āž” ${chalk.green(targetUrl)}`);
109
+ console.log(`Dashboard: ${chalk.cyan(`http://localhost:${port}/dashboard`)}\n`);
110
+ });
111
+ });
112
+
113
+ program
114
+ .command('replay')
115
+ .description('Replay dead-letter events from terminal')
116
+ .argument('[eventId]', 'Specific event ID to replay')
117
+ .option('--failed', 'Replay all failed events (including ones that used up their automatic retries)')
118
+ .option('-u, --url <url>', 'HookArmor server URL', 'http://localhost:4000')
119
+ .option('-k, --api-key <key>', 'Admin API key (default: HOOKARMOR_API_KEY)', process.env.HOOKARMOR_API_KEY)
120
+ .action(async (eventId, options) => {
121
+ const baseUrl = options.url.replace(/\/$/, '');
122
+ try {
123
+ if (options.failed) {
124
+ console.log(chalk.yellow('šŸ”„ Replaying all dead-letter events...'));
125
+ const res = await fetch(`${baseUrl}/api/events/replay-all`, { method: 'POST', headers: apiHeaders(options.apiKey), body: '{}' });
126
+ const data = await readJson(res);
127
+ console.log(chalk.green(`āœ… Dispatched replay for ${data.replayedCount} events.`));
128
+ if (data.remaining > 0) {
129
+ console.log(chalk.yellow(` ${data.remaining} failed events still queued; run the command again to continue.`));
130
+ }
131
+ } else if (eventId) {
132
+ console.log(chalk.yellow(`šŸ”„ Replaying event ${eventId}...`));
133
+ const res = await fetch(`${baseUrl}/api/events/${encodeURIComponent(eventId)}/replay`, { method: 'POST', headers: apiHeaders(options.apiKey), body: '{}' });
134
+ const data = await readJson(res);
135
+ if (data.success) {
136
+ console.log(chalk.green(`āœ… Replay successful: HTTP ${data.statusCode} (${data.latencyMs}ms)`));
137
+ } else {
138
+ console.log(chalk.red(`āŒ Replay failed: HTTP ${data.statusCode} (${data.errorMessage || 'Target rejected'})`));
139
+ process.exitCode = 1;
140
+ }
141
+ } else {
142
+ console.error(chalk.red('āœ– Missing argument: specify an [eventId] or pass --failed to replay all dead-letter events.'));
143
+ console.log(chalk.gray(' Usage: hookarmor replay <eventId> OR hookarmor replay --failed\n'));
144
+ process.exitCode = 1;
145
+ }
146
+ } catch (err) {
147
+ console.error(chalk.red(`Replay request to ${baseUrl} failed: ${err.message}`));
148
+ process.exitCode = 1;
149
+ }
150
+ });
151
+
152
+ program
153
+ .command('status')
154
+ .description('Check HookArmor metrics and dead-letter queue count')
155
+ .option('-u, --url <url>', 'HookArmor server URL', 'http://localhost:4000')
156
+ .option('-k, --api-key <key>', 'Admin API key (default: HOOKARMOR_API_KEY)', process.env.HOOKARMOR_API_KEY)
157
+ .action(async (options) => {
158
+ const baseUrl = options.url.replace(/\/$/, '');
159
+ try {
160
+ const res = await fetch(`${baseUrl}/api/stats`, { headers: apiHeaders(options.apiKey) });
161
+ const data = await readJson(res);
162
+ console.log(chalk.blue.bold('\nšŸ›”ļø HookArmor Metrics'));
163
+ console.log(chalk.gray('───────────────────────'));
164
+ console.log(`Total Ingested : ${chalk.cyan(data.total)}`);
165
+ console.log(`Delivered : ${chalk.green(data.delivered)}`);
166
+ console.log(`Dead-Letter : ${chalk.red(data.failed)}`);
167
+ console.log(`Pending : ${chalk.yellow(data.pending)}`);
168
+ console.log(`Avg Latency : ${chalk.white(`${data.avgLatencyMs} ms`)}\n`);
169
+ } catch (err) {
170
+ console.error(chalk.red(`Status request to ${baseUrl} failed: ${err.message}`));
171
+ process.exitCode = 1;
172
+ }
173
+ });
174
+
175
+ program.parse();
package/package.json CHANGED
@@ -1,52 +1,52 @@
1
- {
2
- "name": "hookarmor",
3
- "version": "1.0.3",
4
- "description": "Zero-loss Webhook Dead-Letter Queue (DLQ), Reliability Proxy, and Replay Gateway for Stripe, Shopify, Clerk, and B2B SaaS.",
5
- "main": "index.js",
6
- "bin": {
7
- "hookarmor": "bin/hookarmor.js"
8
- },
9
- "files": [
10
- "bin",
11
- "src",
12
- "index.js",
13
- "README.md"
14
- ],
15
- "scripts": {
16
- "start": "node bin/hookarmor.js start",
17
- "test": "node test/verify.js"
18
- },
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/pkdoddamani/hookarmor.git"
22
- },
23
- "homepage": "https://hookarmour.dev",
24
- "bugs": {
25
- "url": "https://github.com/pkdoddamani/hookarmor/issues"
26
- },
27
- "engines": {
28
- "node": ">=18.0.0"
29
- },
30
- "keywords": [
31
- "webhook",
32
- "dead-letter-queue",
33
- "stripe",
34
- "shopify",
35
- "clerk",
36
- "reliability",
37
- "replay",
38
- "proxy",
39
- "devops",
40
- "micro-saas"
41
- ],
42
- "author": "HookArmor Team",
43
- "license": "MIT",
44
- "dependencies": {
45
- "better-sqlite3": "^11.8.1",
46
- "chalk": "^4.1.2",
47
- "commander": "^12.1.0",
48
- "cors": "^2.8.5",
49
- "express": "^4.21.2",
50
- "ws": "^8.18.0"
51
- }
52
- }
1
+ {
2
+ "name": "hookarmor",
3
+ "version": "1.1.1",
4
+ "description": "Zero-loss Webhook Dead-Letter Queue (DLQ), Reliability Proxy, and Replay Gateway for Stripe, Shopify, Clerk, and B2B SaaS.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "hookarmor": "bin/hookarmor.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "README.md",
13
+ "index.js"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/hookarmor.js start",
17
+ "test": "node test/verify.js && node test/regression.js"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/pkdoddamani/hookarmor.git"
22
+ },
23
+ "homepage": "https://hookarmour.dev",
24
+ "bugs": {
25
+ "url": "https://github.com/pkdoddamani/hookarmor/issues"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "keywords": [
31
+ "webhook",
32
+ "dead-letter-queue",
33
+ "stripe",
34
+ "shopify",
35
+ "clerk",
36
+ "reliability",
37
+ "replay",
38
+ "proxy",
39
+ "devops",
40
+ "micro-saas"
41
+ ],
42
+ "author": "HookArmor Team",
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "better-sqlite3": "^11.8.1",
46
+ "chalk": "^4.1.2",
47
+ "commander": "^12.1.0",
48
+ "cors": "^2.8.5",
49
+ "express": "^4.21.2",
50
+ "ws": "^8.18.0"
51
+ }
52
+ }
package/src/alerter.js CHANGED
@@ -1,63 +1,72 @@
1
- const http = require('http');
2
- const https = require('https');
3
-
4
- class Alerter {
5
- static async sendAlert(alertWebhookUrl, { event, endpoint, attempt }) {
6
- if (!alertWebhookUrl) return;
7
-
8
- const payload = {
9
- text: `🚨 [HookArmor Alert] Webhook delivery failed for endpoint: ${endpoint.name}`,
10
- attachments: [
11
- {
12
- color: '#e02424',
13
- title: `Event: ${event.event_type || 'Unknown'} (${event.id})`,
14
- fields: [
15
- { title: 'Endpoint', value: endpoint.name, short: true },
16
- { title: 'Target URL', value: endpoint.target_url, short: true },
17
- { title: 'Status Code', value: String(attempt.statusCode || 'Timeout/Error'), short: true },
18
- { title: 'Attempt', value: `${event.attempts + 1} / ${endpoint.max_retries}`, short: true },
19
- { title: 'Error', value: attempt.errorMessage || 'Unknown error', short: false }
20
- ],
21
- footer: 'HookArmor Webhook Sentinel',
22
- ts: Math.floor(Date.now() / 1000)
23
- }
24
- ],
25
- // Discord-compatible embed
26
- embeds: [
27
- {
28
- title: `🚨 Webhook Delivery Failed: ${endpoint.name}`,
29
- description: `**Event:** \`${event.event_type || event.id}\`\n**Target:** \`${endpoint.target_url}\`\n**HTTP Status:** \`${attempt.statusCode || 'Error'}\`\n**Error:** ${attempt.errorMessage || 'No response'}`,
30
- color: 14689316,
31
- timestamp: new Date().toISOString()
32
- }
33
- ]
34
- };
35
-
36
- try {
37
- const url = new URL(alertWebhookUrl);
38
- const isHttps = url.protocol === 'https:';
39
- const client = isHttps ? https : http;
40
-
41
- const postData = JSON.stringify(payload);
42
- const req = client.request(url, {
43
- method: 'POST',
44
- headers: {
45
- 'Content-Type': 'application/json',
46
- 'Content-Length': Buffer.byteLength(postData)
47
- },
48
- timeout: 5000
49
- });
50
-
51
- req.on('error', (err) => {
52
- console.error('[Alerter] Failed to send alert webhook:', err.message);
53
- });
54
-
55
- req.write(postData);
56
- req.end();
57
- } catch (err) {
58
- console.error('[Alerter] Error parsing alert webhook URL:', err.message);
59
- }
60
- }
61
- }
62
-
63
- module.exports = Alerter;
1
+ const http = require('http');
2
+ const https = require('https');
3
+ const { checkUrl, guardedLookup } = require('./netguard');
4
+
5
+ class Alerter {
6
+ static async sendAlert(alertWebhookUrl, { event, endpoint, attempt }, { strictSSRF = false } = {}) {
7
+ if (!alertWebhookUrl) return;
8
+
9
+ const check = checkUrl(alertWebhookUrl, strictSSRF);
10
+ if (!check.ok) {
11
+ console.error(`[Alerter] Alert webhook URL blocked: ${check.reason}`);
12
+ return;
13
+ }
14
+
15
+ const payload = {
16
+ text: `🚨 [HookArmor Alert] Webhook delivery failed for endpoint: ${endpoint.name}`,
17
+ attachments: [
18
+ {
19
+ color: '#e02424',
20
+ title: `Event: ${event.event_type || 'Unknown'} (${event.id})`,
21
+ fields: [
22
+ { title: 'Endpoint', value: endpoint.name, short: true },
23
+ { title: 'Target URL', value: endpoint.target_url, short: true },
24
+ { title: 'Status Code', value: String(attempt.statusCode || 'Timeout/Error'), short: true },
25
+ { title: 'Attempt', value: `${event.attempts + 1} / ${endpoint.max_retries}`, short: true },
26
+ { title: 'Error', value: attempt.errorMessage || 'Unknown error', short: false }
27
+ ],
28
+ footer: 'HookArmor Webhook Sentinel',
29
+ ts: Math.floor(Date.now() / 1000)
30
+ }
31
+ ],
32
+ // Discord-compatible embed
33
+ embeds: [
34
+ {
35
+ title: `🚨 Webhook Delivery Failed: ${endpoint.name}`,
36
+ description: `**Event:** \`${event.event_type || event.id}\`\n**Target:** \`${endpoint.target_url}\`\n**HTTP Status:** \`${attempt.statusCode || 'Error'}\`\n**Error:** ${attempt.errorMessage || 'No response'}`,
37
+ color: 14689316,
38
+ timestamp: new Date().toISOString()
39
+ }
40
+ ]
41
+ };
42
+
43
+ try {
44
+ const url = new URL(alertWebhookUrl);
45
+ const isHttps = url.protocol === 'https:';
46
+ const client = isHttps ? https : http;
47
+
48
+ const postData = JSON.stringify(payload);
49
+ const req = client.request(url, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ 'Content-Length': Buffer.byteLength(postData)
54
+ },
55
+ timeout: 5000,
56
+ lookup: guardedLookup(strictSSRF)
57
+ }, (res) => res.resume());
58
+
59
+ req.on('timeout', () => req.destroy(new Error('Alert webhook timed out')));
60
+ req.on('error', (err) => {
61
+ console.error('[Alerter] Failed to send alert webhook:', err.message);
62
+ });
63
+
64
+ req.write(postData);
65
+ req.end();
66
+ } catch (err) {
67
+ console.error('[Alerter] Error parsing alert webhook URL:', err.message);
68
+ }
69
+ }
70
+ }
71
+
72
+ module.exports = Alerter;