hookarmor 1.0.3 ā 1.1.0
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/README.md +303 -260
- package/bin/hookarmor.js +168 -133
- package/package.json +52 -52
- package/src/alerter.js +72 -63
- package/src/dispatcher.js +409 -356
- package/src/netguard.js +117 -0
- package/src/public/index.html +96 -40
- package/src/server.js +544 -393
- package/src/storage.js +337 -266
- package/src/worker.js +72 -62
package/bin/hookarmor.js
CHANGED
|
@@ -1,133 +1,168 @@
|
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
-
.
|
|
115
|
-
.option('-u, --url <url>', 'HookArmor server URL', 'http://localhost:4000')
|
|
116
|
-
.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
.action((options) => {
|
|
45
|
+
const port = parseInt(options.port, 10);
|
|
46
|
+
const { server, storage, mockEnabled } = startServerOrExit();
|
|
47
|
+
|
|
48
|
+
// Ensure a default mock endpoint exists for first-time onboarding (local simulation only)
|
|
49
|
+
if (mockEnabled && !storage.getEndpoint('demo-stripe')) {
|
|
50
|
+
storage.createEndpoint({
|
|
51
|
+
id: 'demo-stripe',
|
|
52
|
+
name: 'Stripe Payments Demo',
|
|
53
|
+
targetUrl: `http://localhost:${port}/mock/target`,
|
|
54
|
+
alertWebhookUrl: '',
|
|
55
|
+
autoRetry: 1,
|
|
56
|
+
maxRetries: 5
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
server.listen(port, () => {
|
|
61
|
+
console.log(chalk.blue.bold('\nš”ļø HookArmor Webhook Sentinel is LIVE!'));
|
|
62
|
+
console.log(chalk.gray('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
63
|
+
console.log(`š” Ingress Gateway : ${chalk.cyan(`http://localhost:${port}/in/:endpointId`)}`);
|
|
64
|
+
console.log(`š Web Dashboard : ${chalk.green.bold(`http://localhost:${port}/dashboard`)}`);
|
|
65
|
+
if (mockEnabled) {
|
|
66
|
+
console.log(`āļø Mock Receiver : ${chalk.yellow(`http://localhost:${port}/mock/target`)}`);
|
|
67
|
+
}
|
|
68
|
+
console.log(chalk.gray('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
69
|
+
console.log(chalk.white('Press Ctrl+C to stop.\n'));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command('listen')
|
|
75
|
+
.description('Tunnel incoming webhooks directly to a local or remote target')
|
|
76
|
+
.argument('<targetUrl>', 'Destination target URL (e.g. http://localhost:3000/api/webhook)')
|
|
77
|
+
.option('-p, --port <number>', 'Proxy port', '4000')
|
|
78
|
+
.option('-e, --endpoint <id>', 'Endpoint ID slug', 'local-dev')
|
|
79
|
+
.option('-s, --secret <secret>', 'Provider signing secret: verify on ingress and re-sign on delivery')
|
|
80
|
+
.action(async (targetUrl, options) => {
|
|
81
|
+
const port = parseInt(options.port, 10);
|
|
82
|
+
const { server, storage, dispatcher } = startServerOrExit();
|
|
83
|
+
|
|
84
|
+
storage.createEndpoint({
|
|
85
|
+
id: options.endpoint,
|
|
86
|
+
name: `Local Tunnel -> ${targetUrl}`,
|
|
87
|
+
targetUrl,
|
|
88
|
+
secret: options.secret,
|
|
89
|
+
autoRetry: 1,
|
|
90
|
+
maxRetries: 5
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
dispatcher.on('delivery', (res) => {
|
|
94
|
+
const statusStr = res.success
|
|
95
|
+
? chalk.green.bold(`[${res.statusCode}] DELIVERED`)
|
|
96
|
+
: chalk.red.bold(`[${res.statusCode || 'ERR'}] DEAD-LETTER`);
|
|
97
|
+
console.log(`[${new Date().toLocaleTimeString()}] ${statusStr} ${res.eventId} -> ${targetUrl} (${res.latencyMs}ms)`);
|
|
98
|
+
if (!res.success && res.errorMessage) {
|
|
99
|
+
console.log(chalk.red(` āā Error: ${res.errorMessage}`));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
server.listen(port, () => {
|
|
104
|
+
console.log(chalk.cyan.bold(`\nā” HookArmor Listen Active`));
|
|
105
|
+
console.log(`Forwarding: ${chalk.yellow(`http://localhost:${port}/in/${options.endpoint}`)} ā ${chalk.green(targetUrl)}`);
|
|
106
|
+
console.log(`Dashboard: ${chalk.cyan(`http://localhost:${port}/dashboard`)}\n`);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
program
|
|
111
|
+
.command('replay')
|
|
112
|
+
.description('Replay dead-letter events from terminal')
|
|
113
|
+
.argument('[eventId]', 'Specific event ID to replay')
|
|
114
|
+
.option('--failed', 'Replay all failed events (including ones that used up their automatic retries)')
|
|
115
|
+
.option('-u, --url <url>', 'HookArmor server URL', 'http://localhost:4000')
|
|
116
|
+
.option('-k, --api-key <key>', 'Admin API key (default: HOOKARMOR_API_KEY)', process.env.HOOKARMOR_API_KEY)
|
|
117
|
+
.action(async (eventId, options) => {
|
|
118
|
+
const baseUrl = options.url.replace(/\/$/, '');
|
|
119
|
+
try {
|
|
120
|
+
if (options.failed || !eventId) {
|
|
121
|
+
console.log(chalk.yellow('š Replaying all dead-letter events...'));
|
|
122
|
+
const res = await fetch(`${baseUrl}/api/events/replay-all`, { method: 'POST', headers: apiHeaders(options.apiKey), body: '{}' });
|
|
123
|
+
const data = await readJson(res);
|
|
124
|
+
console.log(chalk.green(`ā
Dispatched replay for ${data.replayedCount} events.`));
|
|
125
|
+
if (data.remaining > 0) {
|
|
126
|
+
console.log(chalk.yellow(` ${data.remaining} failed events still queued; run the command again to continue.`));
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
console.log(chalk.yellow(`š Replaying event ${eventId}...`));
|
|
130
|
+
const res = await fetch(`${baseUrl}/api/events/${encodeURIComponent(eventId)}/replay`, { method: 'POST', headers: apiHeaders(options.apiKey), body: '{}' });
|
|
131
|
+
const data = await readJson(res);
|
|
132
|
+
if (data.success) {
|
|
133
|
+
console.log(chalk.green(`ā
Replay successful: HTTP ${data.statusCode} (${data.latencyMs}ms)`));
|
|
134
|
+
} else {
|
|
135
|
+
console.log(chalk.red(`ā Replay failed: HTTP ${data.statusCode} (${data.errorMessage || 'Target rejected'})`));
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(chalk.red(`Replay request to ${baseUrl} failed: ${err.message}`));
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
program
|
|
146
|
+
.command('status')
|
|
147
|
+
.description('Check HookArmor metrics and dead-letter queue count')
|
|
148
|
+
.option('-u, --url <url>', 'HookArmor server URL', 'http://localhost:4000')
|
|
149
|
+
.option('-k, --api-key <key>', 'Admin API key (default: HOOKARMOR_API_KEY)', process.env.HOOKARMOR_API_KEY)
|
|
150
|
+
.action(async (options) => {
|
|
151
|
+
const baseUrl = options.url.replace(/\/$/, '');
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch(`${baseUrl}/api/stats`, { headers: apiHeaders(options.apiKey) });
|
|
154
|
+
const data = await readJson(res);
|
|
155
|
+
console.log(chalk.blue.bold('\nš”ļø HookArmor Metrics'));
|
|
156
|
+
console.log(chalk.gray('āāāāāāāāāāāāāāāāāāāāāāā'));
|
|
157
|
+
console.log(`Total Ingested : ${chalk.cyan(data.total)}`);
|
|
158
|
+
console.log(`Delivered : ${chalk.green(data.delivered)}`);
|
|
159
|
+
console.log(`Dead-Letter : ${chalk.red(data.failed)}`);
|
|
160
|
+
console.log(`Pending : ${chalk.yellow(data.pending)}`);
|
|
161
|
+
console.log(`Avg Latency : ${chalk.white(`${data.avgLatencyMs} ms`)}\n`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(chalk.red(`Status request to ${baseUrl} failed: ${err.message}`));
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "hookarmor",
|
|
3
|
-
"version": "1.0
|
|
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
|
-
"
|
|
13
|
-
"
|
|
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.0",
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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;
|