hookarmor 1.0.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 ADDED
@@ -0,0 +1,173 @@
1
+ # ๐Ÿ›ก๏ธ HookArmor
2
+
3
+ > **Zero-loss Webhook Dead-Letter Queue (DLQ), Reliability Proxy, and Replay Gateway for Stripe, Shopify, Clerk, and modern B2B SaaS.**
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+ [![Tests: Passing](https://img.shields.io/badge/Tests-7%20Passing-brightgreen.svg)]()
7
+ [![Status: Production Ready](https://img.shields.io/badge/Status-v1.0.0-blueviolet.svg)]()
8
+
9
+ ---
10
+
11
+ ## ๐Ÿ’ฅ The Problem HookArmor Solves
12
+
13
+ Every developer using Stripe, Shopify, GitHub, Clerk, Paddle, or custom webhooks faces catastrophic silent revenue loss when:
14
+ 1. **Serverless cold starts & timeouts**: Next.js, Vercel, or AWS Lambda cold starts hit the 10-15s webhook limit.
15
+ 2. **Zero-downtime deploy reboots**: Container restarts on Render, Railway, or Fly.io return transient `502 Bad Gateway`.
16
+ 3. **Database connection pool exhaustion**: Prisma or Postgres locks during sudden traffic spikes cause unhandled `500 Internal Server Error`.
17
+ 4. **Stripe's painful retry schedule**: When Stripe receives a failure, it backs off exponentially (1 hour, then several hours, then days). Meanwhile, your paying customer logs in, sees "Free Plan", assumes a scam, and disputes the charge or issues a refund.
18
+
19
+ ---
20
+
21
+ ## โšก How HookArmor Works
22
+
23
+ ```
24
+ [Stripe / Shopify / Clerk]
25
+ โ”‚ (POST webhook)
26
+ โ–ผ
27
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
28
+ โ”‚ HookArmor Ingress Edge โ”‚
29
+ โ”‚ - Returns immediate 200 OK (<10ms) โ”‚
30
+ โ”‚ - Stores raw payload in SQLite DLQ โ”‚
31
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
32
+ โ”‚
33
+ โ–ผ
34
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
35
+ โ”‚ Relay Dispatch Engine โ”‚
36
+ โ”‚ - Forwards request to user's server โ”‚
37
+ โ”‚ - Preserves exact headers & HMAC sigsโ”‚
38
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
39
+ โ”‚ โ”‚
40
+ (2xx Success) (5xx / 4xx / Timeout)
41
+ โ–ผ โ–ผ
42
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
43
+ โ”‚ Event Archive โ”‚ โ”‚ Dead-Letter Queue โ”‚
44
+ โ”‚ - Latency log โ”‚ โ”‚ - Capture error code & body โ”‚
45
+ โ”‚ - Status 200 โ”‚ โ”‚ - Instant Discord / Slack Alert โ”‚
46
+ โ”‚ โ”‚ โ”‚ - Smart exponential auto-retry โ”‚
47
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ - 1-Click UI & CLI Replay Engine โ”‚
48
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
49
+ ```
50
+
51
+ 1. **Sub-10ms Ingest**: HookArmor returns an immediate `200 OK` to the sender so Stripe or Shopify never marks the event failed.
52
+ 2. **Cryptographic Header & Signature Preservation**: Forwards exact raw bytes, `stripe-signature`, `x-shopify-hmac-sha256`, and timestamps.
53
+ 3. **Dead-Letter Queue (DLQ)**: If your server returns 500, 502, 504, 429, or times out, HookArmor safely preserves the raw event with full error diagnostics.
54
+ 4. **Instant Alerts**: Sends immediate Slack / Discord webhooks when an endpoint begins failing.
55
+ 5. **1-Click Bulk Replay**: As soon as you push your code fix, hit **Replay All Dead-Letter** in the Web UI or run `hookarmor replay --failed` to restore all customer transactions in 2 seconds.
56
+ 6. **Local Dev Tunnel**: `hookarmor listen http://localhost:3000/api/webhooks` to replay production webhooks straight into localhost debuggers.
57
+
58
+ ---
59
+
60
+ ## ๐Ÿš€ Quickstart
61
+
62
+ ### 1-Click Cloud Deployment (Free & Self-Hosted)
63
+
64
+ Deploy your own private, persistent HookArmor instance to the cloud with one click:
65
+
66
+ [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template?template=https://github.com/pkdoddamani/hookarmor)
67
+ [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/pkdoddamani/hookarmor)
68
+
69
+ ---
70
+
71
+ ### Local Quickstart
72
+
73
+ #### Option A: Docker Compose (Zero Config)
74
+ ```bash
75
+ git clone https://github.com/pkdoddamani/hookarmor.git
76
+ cd hookarmor
77
+ docker compose up -d
78
+ ```
79
+
80
+ #### Option B: Terminal CLI
81
+ ```bash
82
+ npx hookarmor start
83
+ ```
84
+ * **Web Dashboard**: `http://localhost:4000`
85
+ * **Ingress Gateway**: `http://localhost:4000/in/:endpointId`
86
+
87
+ ### 2. Tunnel Direct to Localhost
88
+ ```bash
89
+ npx hookarmor listen http://localhost:3000/api/webhooks/stripe
90
+ ```
91
+
92
+ ### 3. Replay Failed Dead-Letter Events
93
+ ```bash
94
+ # Replay all failed events across all endpoints
95
+ npx hookarmor replay --failed
96
+
97
+ # Replay a specific event ID
98
+ npx hookarmor replay evt_1758513516086_m8r0e7
99
+ ```
100
+
101
+ ---
102
+
103
+ ## ๐Ÿ“Š Verification Test Suite
104
+
105
+ HookArmor includes a 7-scenario automated verification test suite covering edge cases, replay re-signing, and pool protection:
106
+ ```bash
107
+ npm test
108
+ ```
109
+ ```
110
+ ๐Ÿงช Starting HookArmor Hardened Verification Test Suite (Post-Audit)...
111
+
112
+ Test 1: Testing SSRF Protection & Endpoint Creation...
113
+ โœ… SSRF probe against 169.254.169.254 successfully blocked.
114
+ โœ… Valid endpoint created with secret.
115
+
116
+ Test 2: Testing Ingress Signature Verification (Security Boundary)...
117
+ โœ… Forged signature rejected with 400 Bad Request.
118
+ โœ… Authentic signature verified, ingested, and delivered.
119
+
120
+ Test 3: Testing 5-Minute Expiration Defeat (Fresh Outbound Re-Signing)...
121
+ โœ… HookArmor defeated the 5-minute Stripe expiration trap:
122
+ Stored in DB: t=1600000000 (Expired 5+ years ago)
123
+ Re-signed on replay: t=1790143178 (Current) -> stripe.webhooks.constructEvent succeeds!
124
+
125
+ Test 4: Simulating Downstream Failure (500) -> Dead-Letter Queue...
126
+ โœ… Event safely quarantined in Dead-Letter Queue with HTTP 500.
127
+ Next automated retry scheduled with exponential backoff + jitter.
128
+
129
+ Test 5: Testing Background Retry Worker (Automatic Self-Healing)...
130
+ โœ… Background Retry Worker automatically claimed and delivered DLQ event! (Attempts: 2)
131
+
132
+ Test 6: Testing Concurrency Limiting (Pool Protection)...
133
+ โœ… Concurrency capped at max 2 parallel in-flight connections (pool protected).
134
+
135
+ Test 7: Testing Safe Idempotency Key Deduplication...
136
+ โœ… Duplicate event intercepted and suppressed cleanly.
137
+
138
+ ๐ŸŽ‰ ALL 7 HARDENED HOOKARMOR VERIFICATION TESTS PASSED PERFECTLY!
139
+ ```
140
+
141
+ ---
142
+
143
+ ## ๐Ÿ”’ Defeating the 5-Minute Stripe Signature Expiration Trap
144
+
145
+ Stripe's official SDK (`stripe.webhooks.constructEvent()`) enforces a strict 300-second (5-minute) tolerance on the `Stripe-Signature` timestamp `t=`.
146
+
147
+ If you retry a failed webhook 15 minutes later, or replay a dead-letter event tomorrow, **standard Stripe handlers will throw `Webhook signature verification failed`**.
148
+
149
+ HookArmor solves this automatically:
150
+ 1. **Ingress verification**: Validates the signature at ingress using your webhook signing secret (preventing unauthorized payloads).
151
+ 2. **Fresh re-signing on forward & replay**: When HookArmor delivers or replays the event, it re-signs the outbound payload using your secret with a current epoch timestamp `t=now`.
152
+ 3. **Zero code changes**: Your existing backend handler code remains completely untouched and verifies 100% of the time.
153
+
154
+ ---
155
+
156
+ ## ๐Ÿ“ฆ Hosted Cloud & Self-Hosting
157
+
158
+ | Feature | Self-Hosted (MIT) | Hosted Cloud Starter ($29/mo) | Hosted Cloud Pro ($79/mo) |
159
+ |---|---|---|---|
160
+ | **Ingress Proxy & Buffer** | Unlimited | 50,000 events/mo | 500,000 events/mo |
161
+ | **Instant 200 OK Ack** | Yes (<10ms) | Yes (<10ms) | Yes (<10ms) |
162
+ | **Dead-Letter Queue (DLQ)** | Yes | Yes | Yes |
163
+ | **Stripe Signature Re-Signing** | Yes | Yes | Yes |
164
+ | **Background Auto-Retries** | Yes | Yes | Yes |
165
+ | **Concurrency Pool Limiter** | Yes | Yes | Yes |
166
+ | **Event Retention** | Local Disk | 30 days | 90 days |
167
+ | **Alerting** | Discord / Slack Webhook | Discord / Slack Webhook | Priority alerts + PagerDuty |
168
+ | **Infrastructure** | Your own server | Fully managed & redundant | Fully managed & redundant |
169
+
170
+ ---
171
+
172
+ ## ๐Ÿ“„ License
173
+ MIT License. Created by the HookArmor Team.
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const { createServer } = require('../src/server');
6
+ const http = require('http');
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('hookarmor')
12
+ .description('๐Ÿ›ก๏ธ HookArmor: Webhook Dead-Letter Queue & Replay Sentinel')
13
+ .version('1.0.0');
14
+
15
+ program
16
+ .command('start')
17
+ .description('Start HookArmor server with web dashboard and ingress proxy')
18
+ .option('-p, --port <number>', 'Port to listen on', '4000')
19
+ .action((options) => {
20
+ const port = parseInt(options.port, 10);
21
+ const { server, storage } = createServer();
22
+
23
+ // Ensure a default mock endpoint exists for first-time onboarding
24
+ const defaultEp = storage.getEndpoint('demo-stripe');
25
+ if (!defaultEp) {
26
+ storage.createEndpoint({
27
+ id: 'demo-stripe',
28
+ name: 'Stripe Payments Demo',
29
+ targetUrl: `http://localhost:${port}/mock/target`,
30
+ alertWebhookUrl: '',
31
+ autoRetry: 1,
32
+ maxRetries: 5
33
+ });
34
+ }
35
+
36
+ server.listen(port, () => {
37
+ console.log(chalk.bold.blue('\n๐Ÿ›ก๏ธ HookArmor Webhook Sentinel is LIVE!'));
38
+ console.log(chalk.gray('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
39
+ console.log(`๐Ÿ“ก Ingress Gateway : ${chalk.cyan(`http://localhost:${port}/in/:endpointId`)}`);
40
+ console.log(`๐Ÿ“Š Web Dashboard : ${chalk.green.bold(`http://localhost:${port}`)}`);
41
+ console.log(`โš™๏ธ Mock Receiver : ${chalk.yellow(`http://localhost:${port}/mock/target`)}`);
42
+ console.log(chalk.gray('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
43
+ console.log(chalk.white('Press Ctrl+C to stop.\n'));
44
+ });
45
+ });
46
+
47
+ program
48
+ .command('listen')
49
+ .description('Tunnel incoming webhooks directly to a local or remote target')
50
+ .argument('<targetUrl>', 'Destination target URL (e.g. http://localhost:3000/api/webhook)')
51
+ .option('-p, --port <number>', 'Proxy port', '4000')
52
+ .option('-e, --endpoint <id>', 'Endpoint ID slug', 'local-dev')
53
+ .action(async (targetUrl, options) => {
54
+ const port = parseInt(options.port, 10);
55
+ const { server, storage, dispatcher } = createServer();
56
+
57
+ storage.createEndpoint({
58
+ id: options.endpoint,
59
+ name: `Local Tunnel -> ${targetUrl}`,
60
+ targetUrl,
61
+ autoRetry: 1,
62
+ maxRetries: 5
63
+ });
64
+
65
+ dispatcher.on('delivery', (res) => {
66
+ const statusStr = res.success
67
+ ? chalk.green.bold(`[${res.statusCode}] DELIVERED`)
68
+ : chalk.red.bold(`[${res.statusCode || 'ERR'}] DEAD-LETTER`);
69
+ console.log(`[${new Date().toLocaleTimeString()}] ${statusStr} ${res.eventId} -> ${targetUrl} (${res.latencyMs}ms)`);
70
+ if (!res.success && res.errorMessage) {
71
+ console.log(chalk.red(` โ””โ”€ Error: ${res.errorMessage}`));
72
+ }
73
+ });
74
+
75
+ server.listen(port, () => {
76
+ console.log(chalk.bold.cyan(`\nโšก HookArmor Listen Active`));
77
+ console.log(`Forwarding: ${chalk.yellow(`http://localhost:${port}/in/${options.endpoint}`)} โž” ${chalk.green(targetUrl)}`);
78
+ console.log(`Dashboard: ${chalk.cyan(`http://localhost:${port}`)}\n`);
79
+ });
80
+ });
81
+
82
+ program
83
+ .command('replay')
84
+ .description('Replay dead-letter events from terminal')
85
+ .argument('[eventId]', 'Specific event ID to replay')
86
+ .option('--failed', 'Replay all failed events')
87
+ .option('-u, --url <url>', 'HookArmor server URL', 'http://localhost:4000')
88
+ .action(async (eventId, options) => {
89
+ const baseUrl = options.url.replace(/\/$/, '');
90
+ try {
91
+ if (options.failed || !eventId) {
92
+ console.log(chalk.yellow('๐Ÿ”„ Replaying all dead-letter events...'));
93
+ const res = await fetch(`${baseUrl}/api/events/replay-all`, { method: 'POST', headers: { 'Content-Type': 'application/json' } });
94
+ const data = await res.json();
95
+ console.log(chalk.green(`โœ… Dispatched replay for ${data.replayedCount} events.`));
96
+ } else {
97
+ console.log(chalk.yellow(`๐Ÿ”„ Replaying event ${eventId}...`));
98
+ const res = await fetch(`${baseUrl}/api/events/${eventId}/replay`, { method: 'POST' });
99
+ const data = await res.json();
100
+ if (data.success) {
101
+ console.log(chalk.green(`โœ… Replay successful: HTTP ${data.statusCode} (${data.latencyMs}ms)`));
102
+ } else {
103
+ console.log(chalk.red(`โŒ Replay failed: HTTP ${data.statusCode} (${data.errorMessage || 'Target rejected'})`));
104
+ }
105
+ }
106
+ } catch (err) {
107
+ console.error(chalk.red(`Failed to connect to HookArmor at ${baseUrl}: ${err.message}`));
108
+ }
109
+ });
110
+
111
+ program
112
+ .command('status')
113
+ .description('Check HookArmor metrics and dead-letter queue count')
114
+ .option('-u, --url <url>', 'HookArmor server URL', 'http://localhost:4000')
115
+ .action(async (options) => {
116
+ const baseUrl = options.url.replace(/\/$/, '');
117
+ try {
118
+ const res = await fetch(`${baseUrl}/api/stats`);
119
+ const data = await res.json();
120
+ console.log(chalk.bold.blue('\n๐Ÿ›ก๏ธ HookArmor Metrics'));
121
+ console.log(chalk.gray('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
122
+ console.log(`Total Ingested : ${chalk.cyan(data.total)}`);
123
+ console.log(`Delivered : ${chalk.green(data.delivered)}`);
124
+ console.log(`Dead-Letter : ${chalk.red(data.failed)}`);
125
+ console.log(`Pending : ${chalk.yellow(data.pending)}`);
126
+ console.log(`Avg Latency : ${chalk.white(`${data.avgLatencyMs} ms`)}\n`);
127
+ } catch (err) {
128
+ console.error(chalk.red(`Failed to connect to HookArmor at ${baseUrl}: ${err.message}`));
129
+ }
130
+ });
131
+
132
+ program.parse();
package/index.js ADDED
@@ -0,0 +1,11 @@
1
+ const { createServer } = require('./src/server');
2
+ const Storage = require('./src/storage');
3
+ const Dispatcher = require('./src/dispatcher');
4
+ const Alerter = require('./src/alerter');
5
+
6
+ module.exports = {
7
+ createServer,
8
+ Storage,
9
+ Dispatcher,
10
+ Alerter
11
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "hookarmor",
3
+ "version": "1.0.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
+ "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": "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 ADDED
@@ -0,0 +1,63 @@
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;