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 +173 -0
- package/bin/hookarmor.js +132 -0
- package/index.js +11 -0
- package/package.json +52 -0
- package/src/alerter.js +63 -0
- package/src/dispatcher.js +337 -0
- package/src/public/blog/index.html +167 -0
- package/src/public/blog/sqs-webhook-deduplication-trap.html +256 -0
- package/src/public/blog/stripe-webhook-signature-verification-failed.html +284 -0
- package/src/public/index.html +1158 -0
- package/src/public/landing.html +549 -0
- package/src/public/og-preview.png +0 -0
- package/src/public/robots.txt +5 -0
- package/src/public/sitemap.xml +27 -0
- package/src/server.js +365 -0
- package/src/storage.js +266 -0
- package/src/worker.js +62 -0
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
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[]()
|
|
7
|
+
[]()
|
|
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
|
+
[](https://railway.app/template?template=https://github.com/pkdoddamani/hookarmor)
|
|
67
|
+
[](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.
|
package/bin/hookarmor.js
ADDED
|
@@ -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;
|