webspresso 0.0.99 → 0.0.101
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/.agents/ADMIN.md +47 -4
- package/.agents/AGENTS.md +4 -2
- package/.agents/AUTH.md +309 -0
- package/.agents/CLI.md +128 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/ERRORS.md +59 -0
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +149 -0
- package/.agents/PLUGINS.md +44 -7
- package/.agents/QUEUE.md +18 -10
- package/.agents/ROUTING.md +97 -1
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +2 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/db-makemigrations.js +83 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +21 -0
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +73 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +343 -3
- package/core/auth/middleware.js +45 -6
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +4 -1
- package/core/auth/user-agent.js +161 -0
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +8 -1
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +55 -1
- package/core/orm/repository.js +283 -13
- package/core/orm/scopes.js +3 -2
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +252 -39
- package/core/queue/manager.js +20 -2
- package/core/queue/scheduler.js +115 -59
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +540 -14
- package/index.js +44 -0
- package/package.json +11 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +17 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/audit-log/admin-component.js +54 -54
- package/plugins/audit-log/index.js +2 -2
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +15 -4
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/email/transport.js +30 -11
- package/plugins/file-manager/admin-component.js +49 -49
- package/plugins/file-manager/index.js +4 -4
- package/plugins/index.js +2 -0
- package/plugins/redirect/index.js +1 -1
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/api/api-loader.js +1 -1
- package/src/app-context.js +48 -6
- package/src/bundle/index.js +2 -1
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/scan-routes.js +10 -1
- package/src/file-router.js +173 -4
- package/src/helpers.js +150 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/pages/page-loader.js +1 -1
- package/src/routing/cascading.js +120 -4
- package/src/server.js +180 -26
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhooks Admin API Handlers
|
|
3
|
+
* REST endpoints for managing endpoints, inspecting delivery history, pinging, and retrying
|
|
4
|
+
* @module plugins/webhooks/api-handlers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create Webhook API route handlers
|
|
11
|
+
*
|
|
12
|
+
* @param {import('./dispatcher').WebhookDispatcher} dispatcher
|
|
13
|
+
* @param {Object} [options]
|
|
14
|
+
* @returns {Record<string, Function>}
|
|
15
|
+
*/
|
|
16
|
+
function createWebhookApiHandlers(dispatcher, options = {}) {
|
|
17
|
+
/**
|
|
18
|
+
* GET /endpoints — List all webhook endpoints
|
|
19
|
+
*/
|
|
20
|
+
async function listEndpointsHandler(req, res) {
|
|
21
|
+
try {
|
|
22
|
+
const active = req.query.active !== undefined ? req.query.active === 'true' : undefined;
|
|
23
|
+
const search = req.query.search || req.query.q || undefined;
|
|
24
|
+
const endpoints = await dispatcher.listEndpoints({ active, search });
|
|
25
|
+
|
|
26
|
+
// Enrich with delivery counts if knex available
|
|
27
|
+
const kx = dispatcher.resolveKnex();
|
|
28
|
+
let enriched = endpoints;
|
|
29
|
+
if (kx && endpoints.length > 0) {
|
|
30
|
+
try {
|
|
31
|
+
const stats = await kx(dispatcher.deliveriesTable)
|
|
32
|
+
.select('endpoint_id')
|
|
33
|
+
.count('* as total_deliveries')
|
|
34
|
+
.sum({ success_count: kx.raw('CASE WHEN success = 1 OR success = true THEN 1 ELSE 0 END') })
|
|
35
|
+
.groupBy('endpoint_id');
|
|
36
|
+
|
|
37
|
+
const statsMap = new Map();
|
|
38
|
+
for (const s of stats) {
|
|
39
|
+
statsMap.set(String(s.endpoint_id), {
|
|
40
|
+
totalDeliveries: Number(s.total_deliveries || 0),
|
|
41
|
+
successCount: Number(s.success_count || 0),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
enriched = endpoints.map((ep) => {
|
|
46
|
+
const st = statsMap.get(String(ep.id)) || { totalDeliveries: 0, successCount: 0 };
|
|
47
|
+
return {
|
|
48
|
+
...ep,
|
|
49
|
+
stats: {
|
|
50
|
+
total: st.totalDeliveries,
|
|
51
|
+
successful: st.successCount,
|
|
52
|
+
failed: st.totalDeliveries - st.successCount,
|
|
53
|
+
successRate: st.totalDeliveries > 0 ? Math.round((st.successCount / st.totalDeliveries) * 100) : 100,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
} catch (statsErr) {
|
|
58
|
+
// Fallback if aggregation fails
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return res.status(200).json({
|
|
63
|
+
items: enriched,
|
|
64
|
+
total: enriched.length,
|
|
65
|
+
});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
return res.status(500).json({ error: err.message || 'Failed to list webhook endpoints' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* POST /endpoints — Create new endpoint
|
|
73
|
+
*/
|
|
74
|
+
async function createEndpointHandler(req, res) {
|
|
75
|
+
try {
|
|
76
|
+
const body = req.body || {};
|
|
77
|
+
if (!body.name || !body.url) {
|
|
78
|
+
return res.status(400).json({ error: 'Endpoint name and URL are required.' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const created = await dispatcher.createEndpoint(body);
|
|
82
|
+
return res.status(201).json(created);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return res.status(400).json({ error: err.message || 'Failed to create webhook endpoint' });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* GET /endpoints/:id — Get endpoint details
|
|
90
|
+
*/
|
|
91
|
+
async function getEndpointHandler(req, res) {
|
|
92
|
+
try {
|
|
93
|
+
const id = req.params.id;
|
|
94
|
+
const endpoint = await dispatcher.getEndpoint(id);
|
|
95
|
+
if (!endpoint) {
|
|
96
|
+
return res.status(404).json({ error: `Webhook endpoint #${id} not found` });
|
|
97
|
+
}
|
|
98
|
+
return res.status(200).json(endpoint);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return res.status(500).json({ error: err.message || 'Failed to get webhook endpoint' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* PUT/PATCH /endpoints/:id — Update endpoint
|
|
106
|
+
*/
|
|
107
|
+
async function updateEndpointHandler(req, res) {
|
|
108
|
+
try {
|
|
109
|
+
const id = req.params.id;
|
|
110
|
+
const body = req.body || {};
|
|
111
|
+
const updated = await dispatcher.updateEndpoint(id, body);
|
|
112
|
+
if (!updated) {
|
|
113
|
+
return res.status(404).json({ error: `Webhook endpoint #${id} not found` });
|
|
114
|
+
}
|
|
115
|
+
return res.status(200).json(updated);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return res.status(400).json({ error: err.message || 'Failed to update webhook endpoint' });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* DELETE /endpoints/:id — Delete endpoint
|
|
123
|
+
*/
|
|
124
|
+
async function deleteEndpointHandler(req, res) {
|
|
125
|
+
try {
|
|
126
|
+
const id = req.params.id;
|
|
127
|
+
const deleted = await dispatcher.deleteEndpoint(id);
|
|
128
|
+
if (!deleted) {
|
|
129
|
+
return res.status(404).json({ error: `Webhook endpoint #${id} not found` });
|
|
130
|
+
}
|
|
131
|
+
return res.status(200).json({ success: true, message: `Endpoint #${id} deleted.` });
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return res.status(500).json({ error: err.message || 'Failed to delete webhook endpoint' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* POST /endpoints/:id/ping — Send test ping
|
|
139
|
+
*/
|
|
140
|
+
async function pingEndpointHandler(req, res) {
|
|
141
|
+
try {
|
|
142
|
+
const id = req.params.id;
|
|
143
|
+
const result = await dispatcher.pingEndpoint(id);
|
|
144
|
+
return res.status(200).json(result);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return res.status(400).json({ error: err.message || 'Failed to ping webhook endpoint' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* GET /deliveries — List delivery logs
|
|
152
|
+
*/
|
|
153
|
+
async function listDeliveriesHandler(req, res) {
|
|
154
|
+
try {
|
|
155
|
+
const endpointId = req.query.endpointId || req.query.endpoint_id;
|
|
156
|
+
const event = req.query.event;
|
|
157
|
+
const success = req.query.success;
|
|
158
|
+
const page = req.query.page;
|
|
159
|
+
const limit = req.query.limit || req.query.perPage;
|
|
160
|
+
|
|
161
|
+
const data = await dispatcher.listDeliveries({
|
|
162
|
+
endpointId,
|
|
163
|
+
event,
|
|
164
|
+
success,
|
|
165
|
+
page,
|
|
166
|
+
limit,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return res.status(200).json(data);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return res.status(500).json({ error: err.message || 'Failed to list webhook deliveries' });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* GET /deliveries/:id — Get single delivery log
|
|
177
|
+
*/
|
|
178
|
+
async function getDeliveryHandler(req, res) {
|
|
179
|
+
try {
|
|
180
|
+
const id = req.params.id;
|
|
181
|
+
const item = await dispatcher.getDelivery(id);
|
|
182
|
+
if (!item) {
|
|
183
|
+
return res.status(404).json({ error: `Webhook delivery #${id} not found` });
|
|
184
|
+
}
|
|
185
|
+
return res.status(200).json(item);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
return res.status(500).json({ error: err.message || 'Failed to get webhook delivery' });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* POST /deliveries/:id/retry — Retry failed delivery
|
|
193
|
+
*/
|
|
194
|
+
async function retryDeliveryHandler(req, res) {
|
|
195
|
+
try {
|
|
196
|
+
const id = req.params.id;
|
|
197
|
+
const result = await dispatcher.retryDelivery(id);
|
|
198
|
+
return res.status(200).json(result);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return res.status(400).json({ error: err.message || 'Failed to retry webhook delivery' });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* POST /deliveries/purge — Purge old deliveries
|
|
206
|
+
*/
|
|
207
|
+
async function purgeDeliveriesHandler(req, res) {
|
|
208
|
+
try {
|
|
209
|
+
const days = parseInt(req.body?.days || req.query?.days, 10) || 30;
|
|
210
|
+
const purgedCount = await dispatcher.purgeDeliveries({ days });
|
|
211
|
+
return res.status(200).json({
|
|
212
|
+
success: true,
|
|
213
|
+
purged: purgedCount,
|
|
214
|
+
message: `Purged ${purgedCount} webhook deliveries older than ${days} days.`,
|
|
215
|
+
});
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return res.status(500).json({ error: err.message || 'Failed to purge webhook deliveries' });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
listEndpointsHandler,
|
|
223
|
+
createEndpointHandler,
|
|
224
|
+
getEndpointHandler,
|
|
225
|
+
updateEndpointHandler,
|
|
226
|
+
deleteEndpointHandler,
|
|
227
|
+
pingEndpointHandler,
|
|
228
|
+
listDeliveriesHandler,
|
|
229
|
+
getDeliveryHandler,
|
|
230
|
+
retryDeliveryHandler,
|
|
231
|
+
purgeDeliveriesHandler,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
createWebhookApiHandlers,
|
|
237
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhooks Cryptography & Signature Module
|
|
3
|
+
* Zero-dependency HMAC-SHA256 signing, constant-time verification & secret generation
|
|
4
|
+
* @module plugins/webhooks/crypto
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a cryptographically secure webhook signing secret
|
|
13
|
+
*
|
|
14
|
+
* @param {string} [prefix='whsec_']
|
|
15
|
+
* @param {number} [byteLength=24]
|
|
16
|
+
* @returns {string} E.g. 'whsec_e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934c'
|
|
17
|
+
*/
|
|
18
|
+
function generateSecret(prefix = 'whsec_', byteLength = 24) {
|
|
19
|
+
const bytes = crypto.randomBytes(byteLength).toString('hex');
|
|
20
|
+
return `${prefix || ''}${bytes}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Perform constant-time string comparison to prevent timing attacks
|
|
25
|
+
*
|
|
26
|
+
* @param {string} a
|
|
27
|
+
* @param {string} b
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
function safeCompare(a, b) {
|
|
31
|
+
if (typeof a !== 'string' || typeof b !== 'string') {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const bufA = Buffer.from(a, 'utf8');
|
|
35
|
+
const bufB = Buffer.from(b, 'utf8');
|
|
36
|
+
if (bufA.length !== bufB.length) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sign a webhook payload using HMAC-SHA256
|
|
44
|
+
*
|
|
45
|
+
* @param {string|Object} payload - Raw payload string or serializable object
|
|
46
|
+
* @param {string} secret - Secret key used for signing
|
|
47
|
+
* @param {Object} [options]
|
|
48
|
+
* @param {number} [options.timestamp] - Unix timestamp in seconds
|
|
49
|
+
* @returns {{ signature: string, timestampedSignature: string, timestamp: number, hex: string }}
|
|
50
|
+
*/
|
|
51
|
+
function signPayload(payload, secret, options = {}) {
|
|
52
|
+
if (!secret || typeof secret !== 'string') {
|
|
53
|
+
throw new Error('A valid string secret is required to sign webhook payloads.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const payloadStr = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
57
|
+
const timestamp = typeof options.timestamp === 'number'
|
|
58
|
+
? options.timestamp
|
|
59
|
+
: Math.floor(Date.now() / 1000);
|
|
60
|
+
|
|
61
|
+
// 1. Direct payload HMAC-SHA256 (compatible with GitHub / standard webhooks)
|
|
62
|
+
const hex = crypto.createHmac('sha256', secret).update(payloadStr, 'utf8').digest('hex');
|
|
63
|
+
const signature = `sha256=${hex}`;
|
|
64
|
+
|
|
65
|
+
// 2. Timestamped HMAC-SHA256 (compatible with Stripe / Svix / Slack)
|
|
66
|
+
const toSign = `${timestamp}.${payloadStr}`;
|
|
67
|
+
const timestampedHex = crypto.createHmac('sha256', secret).update(toSign, 'utf8').digest('hex');
|
|
68
|
+
const timestampedSignature = `t=${timestamp},v1=${timestampedHex}`;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
signature,
|
|
72
|
+
timestampedSignature,
|
|
73
|
+
timestamp,
|
|
74
|
+
hex,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Verify a webhook payload against an incoming signature header
|
|
80
|
+
*
|
|
81
|
+
* @param {string|Object} payload
|
|
82
|
+
* @param {string} signatureHeader
|
|
83
|
+
* @param {string} secret
|
|
84
|
+
* @param {Object} [options]
|
|
85
|
+
* @param {number} [options.tolerance=300] - Allowed timestamp drift in seconds (default 5 minutes)
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
function verifySignature(payload, signatureHeader, secret, options = {}) {
|
|
89
|
+
if (!signatureHeader || typeof signatureHeader !== 'string' || !secret) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const payloadStr = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
94
|
+
const tolerance = options.tolerance !== undefined ? options.tolerance : 300;
|
|
95
|
+
|
|
96
|
+
// Case A: Timestamped header format (t=1234567890,v1=abcdef...)
|
|
97
|
+
if (signatureHeader.includes('t=') && signatureHeader.includes('v1=')) {
|
|
98
|
+
const parts = signatureHeader.split(',');
|
|
99
|
+
let timestamp = null;
|
|
100
|
+
const signatures = [];
|
|
101
|
+
|
|
102
|
+
for (const part of parts) {
|
|
103
|
+
const [key, val] = part.trim().split('=');
|
|
104
|
+
if (key === 't') {
|
|
105
|
+
timestamp = parseInt(val, 10);
|
|
106
|
+
} else if (key === 'v1' && val) {
|
|
107
|
+
signatures.push(val);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!timestamp || isNaN(timestamp) || signatures.length === 0) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check tolerance if enabled
|
|
116
|
+
if (tolerance > 0) {
|
|
117
|
+
const now = Math.floor(Date.now() / 1000);
|
|
118
|
+
if (Math.abs(now - timestamp) > tolerance) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const toSign = `${timestamp}.${payloadStr}`;
|
|
124
|
+
const expectedHex = crypto.createHmac('sha256', secret).update(toSign, 'utf8').digest('hex');
|
|
125
|
+
|
|
126
|
+
return signatures.some((sig) => safeCompare(sig, expectedHex));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Case B: Simple format (sha256=abcdef... or raw hex)
|
|
130
|
+
let receivedHex = signatureHeader.trim();
|
|
131
|
+
if (receivedHex.startsWith('sha256=')) {
|
|
132
|
+
receivedHex = receivedHex.slice(7);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const expectedHex = crypto.createHmac('sha256', secret).update(payloadStr, 'utf8').digest('hex');
|
|
136
|
+
return safeCompare(receivedHex, expectedHex);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
generateSecret,
|
|
141
|
+
safeCompare,
|
|
142
|
+
signPayload,
|
|
143
|
+
verifySignature,
|
|
144
|
+
};
|