-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapp.ts
More file actions
419 lines (362 loc) · 13 KB
/
app.ts
File metadata and controls
419 lines (362 loc) · 13 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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
import { p256 } from '@noble/curves/p256';
import express, { Request, Response } from 'express';
const app = express();
app.use(express.raw({ type: 'application/json' }));
const PORT = Number(process.env.PORT) || 8080;
// SECRETS and ENV VARIABLES
dotenv.config();
const currentDir = typeof __dirname !== 'undefined' ? __dirname : process.cwd();
const fordefiPublicKeyPath = path.join(currentDir, 'keys', 'fordefi_public_key.pem');
const hypernativePublicKeyPath = path.join(currentDir, 'keys', 'hypernative_public_key.pem');
let FORDEFI_PUBLIC_KEY: string;
let HYPERNATIVE_PUBLIC_KEY: string;
let FORDEFI_API_USER_TOKEN: string;
try {
FORDEFI_API_USER_TOKEN = process.env.FORDEFI_API_USER_TOKEN!;
if (!FORDEFI_API_USER_TOKEN) {
console.error('❌ FORDEFI_API_USER_TOKEN environment variable is required');
process.exit(1);
}
console.log('✅ Loaded Fordefi API User Token from environment variable');
} catch (error) {
console.error('❌ Error loading Fordefi API User Token:', error);
process.exit(1);
}
try {
if (process.env.FORDEFI_PUBLIC_KEY) {
FORDEFI_PUBLIC_KEY = process.env.FORDEFI_PUBLIC_KEY;
console.log('✅ Loaded Fordefi public key from environment variable');
} else {
FORDEFI_PUBLIC_KEY = fs.readFileSync(fordefiPublicKeyPath, 'utf8');
console.log('✅ Loaded Fordefi public key from file');
}
} catch (error) {
console.error('❌ Error loading Fordefi public key:', error);
process.exit(1);
}
try {
if (process.env.HYPERNATIVE_PUBLIC_KEY) {
HYPERNATIVE_PUBLIC_KEY = process.env.HYPERNATIVE_PUBLIC_KEY;
console.log('✅ Loaded Hypernative public key from environment variable');
} else {
HYPERNATIVE_PUBLIC_KEY = fs.readFileSync(hypernativePublicKeyPath, 'utf8');
console.log('✅ Loaded Hypernative public key from file');
}
} catch (error) {
console.error('❌ Error loading Hypernative public key:', error);
process.exit(1);
}
/// APP LOGIC
interface WebhookEvent {
event?: {
transaction_id?: string;
[key: string]: any;
};
[key: string]: any;
}
/**
* Trigger signing for a Fordefi transaction
*/
async function triggerTransactionSigning(transactionId: string): Promise<boolean> {
try {
console.log(`🔑 Triggering signing for transaction: ${transactionId}`);
const response = await axios.post(
`https://api.fordefi.com/api/v1/transactions/${transactionId}/trigger-signing`,
{}, // Empty body for POST request
{
headers: {
'Authorization': `Bearer ${FORDEFI_API_USER_TOKEN}`,
'Content-Type': 'application/json',
},
validateStatus: () => true, // Don't throw on HTTP error status
}
);
if (response.status >= 200 && response.status < 300) {
console.log(`✅ Successfully triggered signing for transaction: ${transactionId}`);
console.log('Response:', JSON.stringify(response.data, null, 2));
return true;
} else {
console.error(`❌ Failed to trigger signing for transaction: ${transactionId}`);
console.error(`Status: ${response.status}`);
console.error('Response:', JSON.stringify(response.data, null, 2));
return false;
}
} catch (error: any) {
console.error(`❌ Error triggering signing for transaction: ${transactionId}`, error);
if (error.response) {
console.error('Error response:', JSON.stringify(error.response.data, null, 2));
}
return false;
}
}
/**
* Parse and convert from DER format to IEEE P1363
*/
function derToP1363(derSig: Uint8Array): Uint8Array {
const signature = p256.Signature.fromDER(derSig).toCompactRawBytes();
return signature;
}
/**
* Verify Hypernative webhook signature using ECDSA with SHA-256
*/
async function verifyHypernativeSignature(signature: string, body: Buffer): Promise<boolean> {
try {
const normalizedPem = HYPERNATIVE_PUBLIC_KEY.replace(/\\n/g, '\n');
const pemContents = normalizedPem
.replace('-----BEGIN PUBLIC KEY-----', '')
.replace('-----END PUBLIC KEY-----', '')
.replace(/\s/g, '');
const publicKeyBytes = new Uint8Array(
Buffer.from(pemContents, 'base64')
);
const publicKey = await crypto.subtle.importKey(
'spki',
publicKeyBytes,
{
name: 'ECDSA',
namedCurve: 'P-256'
},
false,
['verify']
);
// Decode the base64 signature (DER format)
const derSignatureBytes = new Uint8Array(
Buffer.from(signature, 'base64')
);
console.log('Hypernative signature verification debug:', {
signatureLength: derSignatureBytes.length,
dataLength: body.length,
signature: signature.substring(0, 20) + '...',
dataPreview: body.slice(0, 100).toString() + '...',
publicKeyLoaded: HYPERNATIVE_PUBLIC_KEY ? 'Yes' : 'No',
hashAlgorithm: 'SHA-256'
});
// Convert DER signature to IEEE P1363 format
const ieeeSignature = derToP1363(derSignatureBytes);
// Verify using IEEE P1363 format signature
const isValid = await crypto.subtle.verify(
{
name: 'ECDSA',
hash: 'SHA-256'
},
publicKey,
ieeeSignature,
body
);
console.log(`Hypernative signature verification result: ${isValid}`);
return isValid;
} catch (error) {
console.error('Hypernative signature verification error:', error);
return false;
}
}
/**
* Verify Fordefi webhook signature using ECDSA with SHA-256
*/
async function verifySignature(signature: string, body: Buffer): Promise<boolean> {
try {
const normalizedPem = FORDEFI_PUBLIC_KEY.replace(/\\n/g, '\n');
const pemContents = normalizedPem
.replace('-----BEGIN PUBLIC KEY-----', '')
.replace('-----END PUBLIC KEY-----', '')
.replace(/\s/g, '');
const publicKeyBytes = new Uint8Array(
Buffer.from(pemContents, 'base64')
);
const publicKey = await crypto.subtle.importKey(
'spki',
publicKeyBytes,
{
name: 'ECDSA',
namedCurve: 'P-256'
},
false,
['verify']
);
// Decode the base64 signature (DER format)
const derSignatureBytes = new Uint8Array(
Buffer.from(signature, 'base64')
);
console.log('Signature verification debug:', {
signatureLength: derSignatureBytes.length,
dataLength: body.length,
signature: signature.substring(0, 20) + '...',
dataPreview: body.slice(0, 50).toString() + '...'
});
// Convert DER signature to IEEE P1363 format
const ieeeSignature = derToP1363(derSignatureBytes);
// Verify using IEEE P1363 format signature
const isValid = await crypto.subtle.verify(
{
name: 'ECDSA',
hash: 'SHA-256'
},
publicKey,
ieeeSignature,
body
);
console.log(`Signature verification result: ${isValid}`);
return isValid;
} catch (error) {
console.error('Signature verification error:', error);
return false;
}
}
/**
* Health check endpoint
*/
app.get('/health', (req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
/**
* Hypernative webhook endpoint
*/
app.post('/hypernative', async (req: Request, res: Response): Promise<void> => {
return handleHypernativeWebhook(req, res);
});
/**
* Handle Hypernative webhook logic
*/
async function handleHypernativeWebhook(req: Request, res: Response): Promise<void> {
try {
console.log('\n⚡ Received Hypernative webhook');
// 1. Get the fordefi-transaction-id from headers
const transactionId = req.headers['fordefi-transaction-id'] as string;
console.log(`📋 Transaction ID: ${transactionId}`);
// 2. Get the raw body
const rawBody = req.body as Buffer;
if (!rawBody || rawBody.length === 0) {
console.error('Empty request body');
res.status(400).json({ error: 'Empty request body' });
return;
}
// 3. Parse the JSON data
const hypernativeData = JSON.parse(rawBody.toString());
// 4. Get digitalSignature from the body
const digitalSignature = hypernativeData.digitalSignature;
if (!digitalSignature) {
console.error('Missing digitalSignature in request body');
res.status(401).json({ error: 'Missing digitalSignature' });
return;
}
// 5. Verify the signature against the 'data' field only
const dataToVerify = Buffer.from(hypernativeData.data, 'utf8');
const isValidSignature = await verifyHypernativeSignature(digitalSignature, dataToVerify);
if (!isValidSignature) {
console.error('Invalid Hypernative signature');
res.status(401).json({ error: 'Invalid signature' });
return;
}
console.log('\n📝 Hypernative Event Data:');
console.log(JSON.stringify(hypernativeData, null, 2));
// Parse the nested data string if it exists
if (hypernativeData.data && typeof hypernativeData.data === 'string') {
try {
const parsedData = JSON.parse(hypernativeData.data);
console.log('\n📊 Parsed Risk Insight:');
console.log(JSON.stringify(parsedData, null, 2));
} catch (error) {
console.error('Error parsing nested data:', error);
}
}
// 6. Trigger signing for the transaction if we have a valid transaction ID
if (transactionId) {
const signingTriggered = await triggerTransactionSigning(transactionId);
// 7. Respond with success/failure based on signing trigger result
if (signingTriggered) {
res.status(200).json({
status: 'success',
message: 'Hypernative webhook received, processed, and signing triggered',
transactionId: transactionId,
signingTriggered: true
});
} else {
res.status(200).json({
status: 'partial_success',
message: 'Hypernative webhook received and processed, but signing trigger failed',
transactionId: transactionId,
signingTriggered: false
});
}
} else {
console.warn('⚠️ No transaction ID provided, skipping signing trigger');
res.status(200).json({
status: 'success',
message: 'Hypernative webhook received and processed (no transaction ID to trigger)',
signingTriggered: false
});
}
} catch (error) {
console.error('Error processing Hypernative webhook:', error);
res.status(500).json({ error: 'Internal server error' });
}
}
/**
* Main webhook endpoint that smartly routes between Fordefi and Hypernative events
*/
app.post('/', async (req: Request, res: Response): Promise<void> => {
try {
console.log(req.headers)
// Check if this might be a Hypernative event by looking for transaction ID header and digitalSignature in body
const transactionId = req.headers['fordefi-transaction-id'] as string;
const rawBody = req.body as Buffer;
if (transactionId && rawBody && rawBody.length > 0) {
try {
const bodyData = JSON.parse(rawBody.toString());
if (bodyData.digitalSignature) {
console.log('\n🔄 Detected Hypernative event on main endpoint, routing...');
return handleHypernativeWebhook(req, res);
}
} catch (parseError) {
// Continue with Fordefi handling if JSON parsing fails
}
}
// Handle as Fordefi event
// 1. Get the signature from headers
const signature = req.headers['x-signature'] as string;
if (!signature) {
console.error('Missing X-Signature header - this might be a Hypernative event sent to wrong endpoint');
console.error('Hypernative events should be sent to /hypernative endpoints');
res.status(401).json({ error: 'Missing signature' });
return;
}
// 2. Get the raw body
if (!rawBody || rawBody.length === 0) {
console.error('Empty request body');
res.status(400).json({ error: 'Empty request body' });
return;
}
// 3. Verify the signature
const isValidSignature = await verifySignature(signature, rawBody);
if (!isValidSignature) {
console.error('Invalid signature');
res.status(401).json({ error: 'Invalid signature' });
return;
}
console.log('\n 🏰 Received Fordefi event:');
const eventData: WebhookEvent = JSON.parse(rawBody.toString());
console.log(JSON.stringify(eventData, null, 2));
// 4. Respond Ok
res.status(200).json({
status: 'success',
message: 'Fordefi webhook received and processed'
});
} catch (error) {
console.error('Error processing webhook:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.use((error: Error, req: Request, res: Response, next: any) => {
console.error('Unhandled error:', error);
res.status(500).json({ error: 'Internal server error' });
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`🪝 Fordefi webhook server running on http://0.0.0.0:${PORT}`);
console.log(`📝 Main webhook endpoint with smart routing: http://0.0.0.0:${PORT}/`);
console.log(`❤️ Health check endpoint: http://0.0.0.0:${PORT}/health`);
});
export default app;