forked from SmartDropLabs/smartdrop-backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebhook.js
More file actions
116 lines (99 loc) · 3.3 KB
/
Copy pathwebhook.js
File metadata and controls
116 lines (99 loc) · 3.3 KB
1
2
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
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
116
const crypto = require('crypto');
const axios = require('axios');
const logger = require('../logger');
const DEFAULT_TIMEOUT_MS = 10000;
function payloadBody(payload) {
return typeof payload === 'string' ? payload : JSON.stringify(payload);
}
function signPayload(secret, payload, timestamp = Date.now()) {
const body = payloadBody(payload);
return crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${body}`)
.digest('hex');
}
function buildSignatureHeaders(secret, payload, timestamp = Date.now()) {
const signature = signPayload(secret, payload, timestamp);
return {
'Content-Type': 'application/json',
'X-SmartDrop-Signature': `sha256=${signature}`,
'X-SmartDrop-Timestamp': String(timestamp),
};
}
function verifySignature(secret, payload, signatureHeader, timestamp) {
if (!signatureHeader || !timestamp || !signatureHeader.startsWith('sha256=')) {
return false;
}
const expected = Buffer.from(signPayload(secret, payload, timestamp), 'hex');
const actual = Buffer.from(signatureHeader.slice('sha256='.length), 'hex');
return expected.length === actual.length && crypto.timingSafeEqual(expected, actual);
}
async function sendSignedRequest(webhookUrl, secret, payload, options = {}) {
const timestamp = options.timestamp || Date.now();
const headers = buildSignatureHeaders(secret, payload, timestamp);
const startedAt = Date.now();
try {
const response = await axios.post(webhookUrl, payload, {
headers,
timeout: options.timeoutMs || DEFAULT_TIMEOUT_MS,
validateStatus: () => true,
});
return {
ok: response.status >= 200 && response.status < 300,
status: response.status,
duration_ms: Date.now() - startedAt,
};
} catch (err) {
err.duration_ms = Date.now() - startedAt;
throw err;
}
}
async function probeReachability(webhookUrl, options = {}) {
const timeoutMs = options.timeoutMs || 3000;
const lastError = { message: 'No response received' };
for (const method of ['head', 'get']) {
try {
const response = await axios[method](webhookUrl, {
timeout: timeoutMs,
validateStatus: () => true,
});
if (response && response.status >= 200 && response.status < 400) {
return { reachable: true, status: response.status, method };
}
if (response && response.status) {
return { reachable: false, status: response.status, method, error: `Target responded with HTTP ${response.status}` };
}
} catch (err) {
lastError.message = err?.message || 'Request failed';
}
}
return { reachable: false, method: 'head', error: lastError.message };
}
async function deliver(webhookUrl, secret, payload) {
try {
const result = await sendSignedRequest(webhookUrl, secret, payload);
if (result.ok) {
logger.info('Webhook delivered', { alert_id: payload.alert_id, url: webhookUrl });
return;
}
logger.warn('Webhook delivery failed', {
alert_id: payload.alert_id,
url: webhookUrl,
status: result.status,
});
} catch (err) {
logger.warn('Webhook delivery failed', {
alert_id: payload.alert_id,
url: webhookUrl,
error: err.message,
});
}
}
module.exports = {
buildSignatureHeaders,
deliver,
probeReachability,
sendSignedRequest,
signPayload,
verifySignature,
};