Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | import { Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { WebhookEndpoint, WebhookDelivery } from './schemas/webhook.schema'; import * as crypto from 'crypto'; import * as https from 'https'; import * as http from 'http'; @Injectable() export class WebhookHubService { private readonly logger = new Logger(WebhookHubService.name); constructor( @InjectModel(WebhookEndpoint.name) private readonly endpointModel: Model<WebhookEndpoint>, @InjectModel(WebhookDelivery.name) private readonly deliveryModel: Model<WebhookDelivery>, ) {} async registerEndpoint( tenantId: string, url: string, subscribedEvents: string[], ): Promise<WebhookEndpoint> { const secret = `whsec_${crypto.randomBytes(24).toString('hex')}`; return this.endpointModel.create({ tenantId, url, subscribedEvents, secret, }); } async triggerEvent( tenantId: string, event: string, payload: Record<string, any>, ): Promise<void> { const endpoints = await this.endpointModel .find({ tenantId, subscribedEvents: event, isActive: true }) .exec(); for (const ep of endpoints) { const delivery = await this.deliveryModel.create({ tenantId, endpointId: (ep as any)._id.toString(), event, payload, status: 'pending', }); // Execute background request this.dispatchOutbound(ep, delivery); } } private dispatchOutbound( ep: WebhookEndpoint, delivery: WebhookDelivery, ): void { const bodyStr = JSON.stringify({ event: delivery.event, timestamp: new Date(), payload: delivery.payload, }); const signature = crypto .createHmac('sha256', ep.secret) .update(bodyStr) .digest('hex'); const parsedUrl = new URL(ep.url); const reqOptions = { method: 'POST', hostname: parsedUrl.hostname, port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), path: parsedUrl.pathname + parsedUrl.search, headers: { 'Content-Type': 'application/json', 'X-BeVision-Signature': signature, 'Content-Length': Buffer.byteLength(bodyStr), }, timeout: 5000, }; const requestModule = parsedUrl.protocol === 'https:' ? https : http; const req = requestModule.request(reqOptions, (res) => { let responseBody = ''; res.on('data', (chunk) => (responseBody += chunk)); res.on('end', async () => { const isSuccess = res.statusCode && res.statusCode >= 200 && res.statusCode < 300; await this.deliveryModel.findByIdAndUpdate(delivery._id, { status: isSuccess ? 'success' : 'failed', $push: { attempts: { timestamp: new Date(), statusCode: res.statusCode || 500, responseBody, }, }, }); }); }); req.on('error', async (err) => { await this.deliveryModel.findByIdAndUpdate(delivery._id, { status: 'failed', $push: { attempts: { timestamp: new Date(), statusCode: 500, responseBody: '', errorMessage: err.message, }, }, }); }); req.write(bodyStr); req.end(); } async verifyInboundSignature( payload: string, signature: string, secret: string, ): Promise<boolean> { const calculatedSig = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return calculatedSig === signature; } } |