swiftshopr-payments 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.
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Webhooks Module
3
+ * Helpers for receiving and verifying SwiftShopr webhooks
4
+ */
5
+
6
+ const crypto = require('crypto');
7
+ const { SwiftShoprError } = require('./utils/http');
8
+
9
+ /**
10
+ * Webhook event types
11
+ */
12
+ const WebhookEvents = {
13
+ PAYMENT_COMPLETED: 'payment.completed',
14
+ PAYMENT_FAILED: 'payment.failed',
15
+ PAYMENT_EXPIRED: 'payment.expired',
16
+ REFUND_REQUESTED: 'refund.requested',
17
+ REFUND_COMPLETED: 'refund.completed',
18
+ REFUND_FAILED: 'refund.failed',
19
+ };
20
+
21
+ /**
22
+ * Maximum age of webhook timestamp (5 minutes)
23
+ */
24
+ const MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1000;
25
+
26
+ /**
27
+ * Create Webhooks helper instance
28
+ */
29
+ function createWebhooksHelper(webhookSecret) {
30
+ /**
31
+ * Verify webhook signature
32
+ *
33
+ * @param {string|Object} payload - Raw request body (string) or parsed JSON
34
+ * @param {string} signature - X-Webhook-Signature header
35
+ * @param {string} timestamp - X-Webhook-Timestamp header
36
+ * @returns {boolean} True if signature is valid
37
+ */
38
+ function verify(payload, signature, timestamp) {
39
+ if (!webhookSecret) {
40
+ throw new SwiftShoprError(
41
+ 'CONFIG_ERROR',
42
+ 'Webhook secret is required for verification',
43
+ );
44
+ }
45
+
46
+ if (!payload || !signature || !timestamp) {
47
+ return false;
48
+ }
49
+
50
+ // Check timestamp is not too old (replay attack prevention)
51
+ const timestampMs = parseInt(timestamp, 10);
52
+ if (isNaN(timestampMs)) {
53
+ return false;
54
+ }
55
+
56
+ const age = Date.now() - timestampMs;
57
+ if (age > MAX_TIMESTAMP_AGE_MS || age < -MAX_TIMESTAMP_AGE_MS) {
58
+ return false;
59
+ }
60
+
61
+ // Compute expected signature
62
+ const payloadString =
63
+ typeof payload === 'string' ? payload : JSON.stringify(payload);
64
+ const signaturePayload = `${timestamp}.${payloadString}`;
65
+
66
+ const expectedSignature = crypto
67
+ .createHmac('sha256', webhookSecret)
68
+ .update(signaturePayload)
69
+ .digest('hex');
70
+
71
+ // Constant-time comparison
72
+ try {
73
+ return crypto.timingSafeEqual(
74
+ Buffer.from(signature),
75
+ Buffer.from(expectedSignature),
76
+ );
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Parse and verify webhook event
84
+ *
85
+ * @param {string|Object} payload - Raw request body
86
+ * @param {Object} headers - Request headers
87
+ * @returns {Object} Parsed and verified webhook event
88
+ * @throws {SwiftShoprError} If verification fails
89
+ */
90
+ function constructEvent(payload, headers) {
91
+ const signature =
92
+ headers['x-webhook-signature'] || headers['X-Webhook-Signature'];
93
+ const timestamp =
94
+ headers['x-webhook-timestamp'] || headers['X-Webhook-Timestamp'];
95
+
96
+ if (!verify(payload, signature, timestamp)) {
97
+ throw new SwiftShoprError(
98
+ 'WEBHOOK_SIGNATURE_INVALID',
99
+ 'Webhook signature verification failed',
100
+ );
101
+ }
102
+
103
+ const event = typeof payload === 'string' ? JSON.parse(payload) : payload;
104
+
105
+ return {
106
+ type: event.event,
107
+ data: {
108
+ intentId: event.intentId,
109
+ orderId: event.orderId,
110
+ txHash: event.txHash,
111
+ amount: event.amount,
112
+ status: event.status,
113
+ currency: event.currency,
114
+ network: event.network,
115
+ explorerUrl: event.explorerUrl,
116
+ storeId: event.storeId,
117
+ timestamp: event.timestamp,
118
+ // Refund-specific fields
119
+ refundId: event.refundId,
120
+ refundAmount: event.refundAmount,
121
+ refundReason: event.refundReason,
122
+ },
123
+ receivedAt: new Date().toISOString(),
124
+ };
125
+ }
126
+
127
+ return {
128
+ verify,
129
+ constructEvent,
130
+ events: WebhookEvents,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Express middleware for webhook verification
136
+ *
137
+ * @param {string} secret - Webhook secret
138
+ * @returns {Function} Express middleware
139
+ */
140
+ function webhookMiddleware(secret) {
141
+ const helper = createWebhooksHelper(secret);
142
+
143
+ return (req, res, next) => {
144
+ try {
145
+ // Need raw body for signature verification
146
+ const payload = req.rawBody || req.body;
147
+ const event = helper.constructEvent(payload, req.headers);
148
+ req.webhookEvent = event;
149
+ next();
150
+ } catch (error) {
151
+ res.status(400).json({
152
+ error: 'webhook_verification_failed',
153
+ message: error.message,
154
+ });
155
+ }
156
+ };
157
+ }
158
+
159
+ module.exports = {
160
+ createWebhooksHelper,
161
+ webhookMiddleware,
162
+ WebhookEvents,
163
+ };