UNPKG

safeer-pdf-generator

Version:

Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery

113 lines 3.75 kB
import { createHmac } from 'crypto'; import { noOpLogger } from '../../config/defaults.js'; /** * Dispatches webhook HTTP POST requests upon PDF generation completion. * * Uses Node.js native fetch (Node 18+) and HMAC-SHA256 signing. * * @example * ```typescript * const dispatcher = new WebhookDispatcher( * 'https://api.example.com/webhooks/pdf', * 'my-secret', * logger * ); * * await dispatcher.dispatch({ * s3Url: 'https://s3.../report.pdf', * title: 'Quarterly Report', * fileName: 'report.pdf', * sizeBytes: 1024000, * pageCount: 15, * durationMs: 5000, * generatedAt: new Date().toISOString(), * metadata: { tenantId: 'abc', userId: '123' }, * }); * ``` */ export class WebhookDispatcher { constructor(url, secret, logger = noOpLogger, timeoutMs = 10000) { this.url = url; this.secret = secret; this.timeoutMs = timeoutMs; this.logger = logger; } /** * Dispatch a webhook with the given payload. * Includes one automatic retry on failure. */ async dispatch(payload) { for (let attempt = 1; attempt <= 2; attempt++) { try { const result = await this.sendRequest(payload); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if (attempt === 1) { this.logger.warn(`Webhook dispatch failed (attempt 1/2): ${errorMessage}. Retrying...`); // Brief delay before retry await new Promise(resolve => setTimeout(resolve, 1000)); } else { this.logger.error(`Webhook dispatch failed after 2 attempts: ${errorMessage}`); return { success: false, error: errorMessage, }; } } } // Should not reach here, but TypeScript requires a return return { success: false, error: 'Max retries exceeded' }; } /** * Send the actual HTTP request */ async sendRequest(payload) { const body = JSON.stringify(payload); const headers = { 'Content-Type': 'application/json', 'User-Agent': 'safeer-pdf-generator-webhook', }; // Sign the payload if a secret is provided if (this.secret) { const signature = this.signPayload(body); headers['X-Webhook-Signature'] = signature; } this.logger.debug(`Dispatching webhook to ${this.url}`); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), this.timeoutMs); try { const response = await fetch(this.url, { method: 'POST', headers, body, signal: controller.signal, }); this.logger.info(`Webhook dispatched: ${response.status} ${response.statusText}`); return { success: response.ok, statusCode: response.status, }; } finally { clearTimeout(timeout); } } /** * Sign the payload body with HMAC-SHA256 */ signPayload(body) { const hmac = createHmac('sha256', this.secret); hmac.update(body); return `sha256=${hmac.digest('hex')}`; } } /** * Create a WebhookDispatcher from configuration */ export function createWebhookDispatcher(url, secret, logger, timeoutMs) { return new WebhookDispatcher(url, secret, logger, timeoutMs); } //# sourceMappingURL=WebhookDispatcher.js.map