@medicus.ai/medicus-report-pdf-generator 1.3.8 → 1.3.10

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.
package/index.js CHANGED
@@ -7,10 +7,18 @@ const {
7
7
  } = require("./lib/wellbeing_report_generator");
8
8
  const { generateSanusXReport, generateHTMLSanusXReport } = require("./lib/sanusx_report_generator");
9
9
  const fs = require('fs');
10
+ const os = require('os');
11
+ const crypto = require('crypto');
10
12
  const { sendNascoEmail, sendEmailNotification } = require('./lib/sendEmail')
11
13
  const path = require('path');
12
14
  const { generatePDFCorporateReport, generateHTMLCorporateReport } = require('./lib/corporate_report_generator')
13
15
 
16
+ // Best-effort fire-and-forget cleanup for per-call temp files. Never throws.
17
+ const safeUnlink = (filePath) => {
18
+ if (!filePath) return Promise.resolve();
19
+ return fs.promises.unlink(filePath).catch(() => { });
20
+ };
21
+
14
22
  module.exports = {
15
23
 
16
24
  generateHTMLStaging,
@@ -187,10 +195,19 @@ module.exports = {
187
195
  let reportData = JSON.parse(data);
188
196
  let base64Object = reportData.data;
189
197
 
198
+ // Per-call unique identifiers so concurrent callers never share a path.
199
+ // Even when two requests overlap, each one owns its own input/output
200
+ // files and email attachment, eliminating cross-user PDF swapping.
201
+ const callId = crypto.randomUUID();
202
+ const tmpDir = os.tmpdir();
203
+ const tmpInputPath = path.join(tmpDir, `wb-${callId}-in.pdf`);
204
+ const tmpEncryptedPath = path.join(tmpDir, `wb-${callId}-enc.pdf`);
205
+ let tmpEmailAttachmentPath = null;
206
+
190
207
  let LOGS = '';
191
- const LOGS_FILE_PATH = path.join(__dirname, 'output', 'LOGS.txt');
192
- const PDF_FILE_PATH = path.join(__dirname, 'output', 'nasco-sample.pdf');
193
- const ENCRYPTED_PDF_PATH = path.join(__dirname, 'output', 'nasco-sample-encrypted.pdf');
208
+ const LOGS_FILE_PATH = isDebugging
209
+ ? path.join(__dirname, 'output', `LOGS-${callId}.txt`)
210
+ : null;
194
211
 
195
212
  const mailConfig = {
196
213
  host: reportData.host,
@@ -201,12 +218,6 @@ module.exports = {
201
218
  secure: reportData.secure,
202
219
  };
203
220
 
204
- if (isDebugging) {
205
- if (fs.existsSync(LOGS_FILE_PATH)) fs.unlinkSync(LOGS_FILE_PATH);
206
- if (fs.existsSync(PDF_FILE_PATH)) fs.unlinkSync(PDF_FILE_PATH);
207
- if (fs.existsSync(ENCRYPTED_PDF_PATH)) fs.unlinkSync(ENCRYPTED_PDF_PATH);
208
- }
209
-
210
221
  if (isDebugging) {
211
222
  console.log('2: save json string');
212
223
  LOGS += '2: save json string\n===============\n' + base64Object + '\n\n\n';
@@ -242,45 +253,64 @@ module.exports = {
242
253
  LOGS += '3: save PDF buffer\n===============\n' + '[binary content omitted]' + '\n\n\n';
243
254
  }
244
255
 
245
- // Write original PDF (unencrypted)
246
- fs.writeFileSync(PDF_FILE_PATH, fileBuffer);
247
-
256
+ try {
257
+ // Write the unencrypted PDF to a per-call temp file.
258
+ // qpdf needs a real input path; using a uuid-suffixed file in
259
+ // os.tmpdir() guarantees no concurrent caller can overwrite it.
260
+ await fs.promises.writeFile(tmpInputPath, fileBuffer);
248
261
 
249
- return await (async () => {
250
262
  const qpdf = await import('node-qpdf2');
251
263
 
264
+ // Encrypt to a per-call output file. We can't use qpdf's stdout
265
+ // mode (output omitted) because node-qpdf2's spawn helper joins
266
+ // stdout chunks as strings, which corrupts binary PDF bytes.
252
267
  await qpdf.encrypt({
253
- input: PDF_FILE_PATH,
254
- output: ENCRYPTED_PDF_PATH,
268
+ input: tmpInputPath,
269
+ output: tmpEncryptedPath,
255
270
  keyLength: 256,
256
271
  password: reportData.pdfPassword,
257
- })
258
- const encryptedBuffer = fs.readFileSync(ENCRYPTED_PDF_PATH);
272
+ });
273
+
274
+ const encryptedBuffer = await fs.promises.readFile(tmpEncryptedPath);
259
275
 
260
- // Save encoded base64 (of encrypted)
261
- const base64data = Buffer.from(encryptedBuffer).toString('base64');
276
+ const base64data = encryptedBuffer.toString('base64');
262
277
  if (isDebugging) {
263
278
  console.log('4: save encoded base64');
264
279
  LOGS += '4: save encoded base64\n===============\n' + base64data + '\n\n\n';
265
280
  }
266
281
 
267
- // Write logs
268
- fs.writeFileSync(LOGS_FILE_PATH, LOGS, 'utf8');
269
- if (isDebugging) console.log('5: save logs to file');
282
+ if (isDebugging && LOGS_FILE_PATH) {
283
+ await fs.promises.writeFile(LOGS_FILE_PATH, LOGS, 'utf8');
284
+ console.log('5: save logs to file');
285
+ }
270
286
 
271
287
  if (isDownloadable) {
272
288
  return encryptedBuffer;
273
- } else {
274
- if (shouldSendEmail) {
275
- return await sendNascoEmail(decodedJSON.wellbeing, ENCRYPTED_PDF_PATH, mailConfig, reportData.client);
276
- } else {
277
- return base64data;
278
- }
279
289
  }
280
290
 
281
- })();
282
-
291
+ if (shouldSendEmail) {
292
+ // Write the encrypted buffer to a dedicated per-call file just
293
+ // for the email attachment. sendNascoEmail expects a path
294
+ // (nodemailer reads it from disk), so we materialize it once,
295
+ // pass it in, then unlink in the finally block.
296
+ tmpEmailAttachmentPath = path.join(tmpDir, `wb-${callId}-mail.pdf`);
297
+ await fs.promises.writeFile(tmpEmailAttachmentPath, encryptedBuffer);
298
+ return await sendNascoEmail(
299
+ decodedJSON.wellbeing,
300
+ tmpEmailAttachmentPath,
301
+ mailConfig,
302
+ reportData.client
303
+ );
304
+ }
283
305
 
306
+ return base64data;
307
+ } finally {
308
+ await Promise.all([
309
+ safeUnlink(tmpInputPath),
310
+ safeUnlink(tmpEncryptedPath),
311
+ safeUnlink(tmpEmailAttachmentPath),
312
+ ]);
313
+ }
284
314
  },
285
315
 
286
316
  generateSanuxPDF: async (data, isDebugging, isDownloadable, shouldSendEmail) => {
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  const fs = require("fs");
3
+ const crypto = require("crypto");
3
4
  const { promisify } = require('util');
4
5
  const { JSDOM } = require("jsdom");
5
6
  const path = require("path");
@@ -1001,8 +1002,12 @@ let generateHTMLWellbeingReport = async (data, isDebugging, clientName, language
1001
1002
  // Setup
1002
1003
  debug = isDebugging;
1003
1004
  const client = clientName || 'default';
1004
- OUT_FILE = path.resolve(__dirname + `/../output/${client}-pdf-` + Date.now() + '.html');
1005
- Pdf_file = `/output/${client}-pdf-` + Date.now() + '.html';
1005
+ // Per-call unique paths so concurrent callers never share a file on disk.
1006
+ // The puppeteer `goto(file:///…)` step is what previously read another
1007
+ // user's HTML when two requests overlapped, producing a swapped PDF body.
1008
+ const callId = crypto.randomUUID();
1009
+ const callOutFile = path.resolve(__dirname + `/../output/${client}-pdf-${callId}.html`);
1010
+ const callPdfFile = `/output/${client}-pdf-${callId}.html`;
1006
1011
 
1007
1012
  // Language configuration
1008
1013
  language = empty(language) ? 'en' : language;
@@ -1113,11 +1118,12 @@ let generateHTMLWellbeingReport = async (data, isDebugging, clientName, language
1113
1118
  }
1114
1119
 
1115
1120
  // Write file
1116
- await promisify(fs.writeFile)(OUT_FILE, finalHtml);
1121
+ await promisify(fs.writeFile)(callOutFile, finalHtml);
1117
1122
 
1118
1123
  return {
1119
1124
  html: finalHtml,
1120
- outputFile: OUT_FILE,
1125
+ outputFile: callOutFile,
1126
+ pdfFile: callPdfFile,
1121
1127
  header: renderedHeader,
1122
1128
  footer: footerHtml,
1123
1129
  historyData
@@ -1138,8 +1144,12 @@ let generateHTMLWellbeingReportWithSmartReport = async (data, isDebugging, clien
1138
1144
  // Setup
1139
1145
  debug = isDebugging;
1140
1146
  const client = clientName || 'default';
1141
- OUT_FILE = path.resolve(__dirname + `/../output/${client}-pdf-` + Date.now() + '.html');
1142
- Pdf_file = `/output/${client}-pdf-` + Date.now() + '.html';
1147
+ // Per-call unique paths so concurrent callers never share a file on disk.
1148
+ // The puppeteer `goto(file:///…)` step is what previously read another
1149
+ // user's HTML when two requests overlapped, producing a swapped PDF body.
1150
+ const callId = crypto.randomUUID();
1151
+ const callOutFile = path.resolve(__dirname + `/../output/${client}-pdf-${callId}.html`);
1152
+ const callPdfFile = `/output/${client}-pdf-${callId}.html`;
1143
1153
 
1144
1154
  // Extract data
1145
1155
  const wellbeingData = data.wellbeing || {};
@@ -1485,11 +1495,12 @@ let generateHTMLWellbeingReportWithSmartReport = async (data, isDebugging, clien
1485
1495
  }
1486
1496
 
1487
1497
  // Write file
1488
- await promisify(fs.writeFile)(OUT_FILE, finalHtml);
1498
+ await promisify(fs.writeFile)(callOutFile, finalHtml);
1489
1499
 
1490
1500
  return {
1491
1501
  html: finalHtml,
1492
- outputFile: OUT_FILE,
1502
+ outputFile: callOutFile,
1503
+ pdfFile: callPdfFile,
1493
1504
  header: renderedHeader,
1494
1505
  footer: templates.footer,
1495
1506
  historyData,
@@ -1545,15 +1556,16 @@ const setupPageContent = async (page, contentHtml, filePath) => {
1545
1556
  let generatePDFWellbeingReport = async (data) => {
1546
1557
  try {
1547
1558
  const contentHtml = data.html;
1548
- OUT_FILE = data.outputFile;
1549
1559
 
1550
1560
  // Launch browser and setup page
1551
1561
  const browser = await launchBrowser();
1552
1562
  const page = await browser.newPage();
1553
1563
  debugLog("page launched!");
1554
1564
 
1555
- // Setup content
1556
- await setupPageContent(page, contentHtml, Pdf_file);
1565
+ // Use the per-call relative path produced by generateHTMLWellbeingReport.
1566
+ // Reading module-level state here was the root cause of cross-user PDF
1567
+ // body swapping under concurrent requests.
1568
+ await setupPageContent(page, contentHtml, data.pdfFile);
1557
1569
  debugLog("page opened!");
1558
1570
 
1559
1571
  // Generate PDF
@@ -1789,15 +1801,17 @@ const setupExtendedPageContent = async (page, contentHtml, filePath) => {
1789
1801
  const generatePDFReport = async (data, hideWellbeingUI = false) => {
1790
1802
  try {
1791
1803
  const contentHtml = data.html;
1792
- const OUT_FILE = data.outputFile;
1793
1804
  const footerTemplate = getPdfFooterTemplate(data.client, data.selectedLabs, data.footer);
1794
1805
 
1795
1806
  // Launch browser and setup page
1796
1807
  const browser = await launchExtendedBrowser();
1797
1808
  const page = await browser.newPage();
1798
1809
 
1799
- // Setup content
1800
- await setupExtendedPageContent(page, contentHtml, Pdf_file);
1810
+ // Use the per-call relative path produced by
1811
+ // generateHTMLWellbeingReportWithSmartReport. Reading module-level
1812
+ // state here was the root cause of cross-user PDF body swapping
1813
+ // under concurrent requests.
1814
+ await setupExtendedPageContent(page, contentHtml, data.pdfFile);
1801
1815
 
1802
1816
  // Generate PDF with header and footer (PHA needs larger bottom margin to prevent content overlapping footer)
1803
1817
  const bottomMargin = (data.client || '').toLowerCase() === 'pha' ? '60px' : '50px';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medicus.ai/medicus-report-pdf-generator",
3
- "version": "1.3.08",
3
+ "version": "1.3.10",
4
4
  "description": "Nasco corporate report - latest update in 12/10/2023 - Fix HRC for bionext",
5
5
  "main": "index.js",
6
6
  "scripts": {