safeer-pdf-generator
Version:
Framework-agnostic PDF generation library with chunking, merging, S3 upload, and email delivery
113 lines • 3.75 kB
JavaScript
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