@reportforge/playwright-pdf 0.22.0 → 0.23.0

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/dist/cli/index.js CHANGED
@@ -1588,13 +1588,13 @@ var require_ast = __commonJS({
1588
1588
  helperExpression: function helperExpression(node) {
1589
1589
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
1590
1590
  },
1591
- scopedId: function scopedId(path14) {
1592
- return /^\.|this\b/.test(path14.original);
1591
+ scopedId: function scopedId(path13) {
1592
+ return /^\.|this\b/.test(path13.original);
1593
1593
  },
1594
1594
  // an ID is simple if it only has one part, and that part is not
1595
1595
  // `..` or `this`.
1596
- simpleId: function simpleId(path14) {
1597
- return path14.parts.length === 1 && !AST.helpers.scopedId(path14) && !path14.depth;
1596
+ simpleId: function simpleId(path13) {
1597
+ return path13.parts.length === 1 && !AST.helpers.scopedId(path13) && !path13.depth;
1598
1598
  }
1599
1599
  }
1600
1600
  };
@@ -2668,12 +2668,12 @@ var require_helpers2 = __commonJS({
2668
2668
  loc
2669
2669
  };
2670
2670
  }
2671
- function prepareMustache(path14, params, hash, open, strip, locInfo) {
2671
+ function prepareMustache(path13, params, hash, open, strip, locInfo) {
2672
2672
  var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
2673
2673
  var decorator = /\*/.test(open);
2674
2674
  return {
2675
2675
  type: decorator ? "Decorator" : "MustacheStatement",
2676
- path: path14,
2676
+ path: path13,
2677
2677
  params,
2678
2678
  hash,
2679
2679
  escaped,
@@ -2993,9 +2993,9 @@ var require_compiler = __commonJS({
2993
2993
  },
2994
2994
  DecoratorBlock: function DecoratorBlock(decorator) {
2995
2995
  var program = decorator.program && this.compileProgram(decorator.program);
2996
- var params = this.setupFullMustacheParams(decorator, program, void 0), path14 = decorator.path;
2996
+ var params = this.setupFullMustacheParams(decorator, program, void 0), path13 = decorator.path;
2997
2997
  this.useDecorators = true;
2998
- this.opcode("registerDecorator", params.length, path14.original);
2998
+ this.opcode("registerDecorator", params.length, path13.original);
2999
2999
  },
3000
3000
  PartialStatement: function PartialStatement(partial) {
3001
3001
  this.usePartial = true;
@@ -3059,46 +3059,46 @@ var require_compiler = __commonJS({
3059
3059
  }
3060
3060
  },
3061
3061
  ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
3062
- var path14 = sexpr.path, name = path14.parts[0], isBlock = program != null || inverse != null;
3063
- this.opcode("getContext", path14.depth);
3062
+ var path13 = sexpr.path, name = path13.parts[0], isBlock = program != null || inverse != null;
3063
+ this.opcode("getContext", path13.depth);
3064
3064
  this.opcode("pushProgram", program);
3065
3065
  this.opcode("pushProgram", inverse);
3066
- path14.strict = true;
3067
- this.accept(path14);
3066
+ path13.strict = true;
3067
+ this.accept(path13);
3068
3068
  this.opcode("invokeAmbiguous", name, isBlock);
3069
3069
  },
3070
3070
  simpleSexpr: function simpleSexpr(sexpr) {
3071
- var path14 = sexpr.path;
3072
- path14.strict = true;
3073
- this.accept(path14);
3071
+ var path13 = sexpr.path;
3072
+ path13.strict = true;
3073
+ this.accept(path13);
3074
3074
  this.opcode("resolvePossibleLambda");
3075
3075
  },
3076
3076
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
3077
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path14 = sexpr.path, name = path14.parts[0];
3077
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path13 = sexpr.path, name = path13.parts[0];
3078
3078
  if (this.options.knownHelpers[name]) {
3079
3079
  this.opcode("invokeKnownHelper", params.length, name);
3080
3080
  } else if (this.options.knownHelpersOnly) {
3081
3081
  throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
3082
3082
  } else {
3083
- path14.strict = true;
3084
- path14.falsy = true;
3085
- this.accept(path14);
3086
- this.opcode("invokeHelper", params.length, path14.original, _ast2["default"].helpers.simpleId(path14));
3083
+ path13.strict = true;
3084
+ path13.falsy = true;
3085
+ this.accept(path13);
3086
+ this.opcode("invokeHelper", params.length, path13.original, _ast2["default"].helpers.simpleId(path13));
3087
3087
  }
3088
3088
  },
3089
- PathExpression: function PathExpression(path14) {
3090
- this.addDepth(path14.depth);
3091
- this.opcode("getContext", path14.depth);
3092
- var name = path14.parts[0], scoped = _ast2["default"].helpers.scopedId(path14), blockParamId = !path14.depth && !scoped && this.blockParamIndex(name);
3089
+ PathExpression: function PathExpression(path13) {
3090
+ this.addDepth(path13.depth);
3091
+ this.opcode("getContext", path13.depth);
3092
+ var name = path13.parts[0], scoped = _ast2["default"].helpers.scopedId(path13), blockParamId = !path13.depth && !scoped && this.blockParamIndex(name);
3093
3093
  if (blockParamId) {
3094
- this.opcode("lookupBlockParam", blockParamId, path14.parts);
3094
+ this.opcode("lookupBlockParam", blockParamId, path13.parts);
3095
3095
  } else if (!name) {
3096
3096
  this.opcode("pushContext");
3097
- } else if (path14.data) {
3097
+ } else if (path13.data) {
3098
3098
  this.options.data = true;
3099
- this.opcode("lookupData", path14.depth, path14.parts, path14.strict);
3099
+ this.opcode("lookupData", path13.depth, path13.parts, path13.strict);
3100
3100
  } else {
3101
- this.opcode("lookupOnContext", path14.parts, path14.falsy, path14.strict, scoped);
3101
+ this.opcode("lookupOnContext", path13.parts, path13.falsy, path13.strict, scoped);
3102
3102
  }
3103
3103
  },
3104
3104
  StringLiteral: function StringLiteral(string) {
@@ -3454,16 +3454,16 @@ var require_util = __commonJS({
3454
3454
  }
3455
3455
  exports2.urlGenerate = urlGenerate;
3456
3456
  function normalize(aPath) {
3457
- var path14 = aPath;
3457
+ var path13 = aPath;
3458
3458
  var url = urlParse(aPath);
3459
3459
  if (url) {
3460
3460
  if (!url.path) {
3461
3461
  return aPath;
3462
3462
  }
3463
- path14 = url.path;
3463
+ path13 = url.path;
3464
3464
  }
3465
- var isAbsolute = exports2.isAbsolute(path14);
3466
- var parts = path14.split(/\/+/);
3465
+ var isAbsolute = exports2.isAbsolute(path13);
3466
+ var parts = path13.split(/\/+/);
3467
3467
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
3468
3468
  part = parts[i];
3469
3469
  if (part === ".") {
@@ -3480,15 +3480,15 @@ var require_util = __commonJS({
3480
3480
  }
3481
3481
  }
3482
3482
  }
3483
- path14 = parts.join("/");
3484
- if (path14 === "") {
3485
- path14 = isAbsolute ? "/" : ".";
3483
+ path13 = parts.join("/");
3484
+ if (path13 === "") {
3485
+ path13 = isAbsolute ? "/" : ".";
3486
3486
  }
3487
3487
  if (url) {
3488
- url.path = path14;
3488
+ url.path = path13;
3489
3489
  return urlGenerate(url);
3490
3490
  }
3491
- return path14;
3491
+ return path13;
3492
3492
  }
3493
3493
  exports2.normalize = normalize;
3494
3494
  function join(aRoot, aPath) {
@@ -6291,8 +6291,8 @@ var require_printer = __commonJS({
6291
6291
  return this.accept(sexpr.path) + " " + params + hash;
6292
6292
  };
6293
6293
  PrintVisitor.prototype.PathExpression = function(id) {
6294
- var path14 = id.parts.join("/");
6295
- return (id.data ? "@" : "") + "PATH:" + path14;
6294
+ var path13 = id.parts.join("/");
6295
+ return (id.data ? "@" : "") + "PATH:" + path13;
6296
6296
  };
6297
6297
  PrintVisitor.prototype.StringLiteral = function(string) {
6298
6298
  return '"' + string.value + '"';
@@ -6333,8 +6333,8 @@ var require_lib = __commonJS({
6333
6333
  handlebars.print = printer.print;
6334
6334
  module2.exports = handlebars;
6335
6335
  function extension(module3, filename) {
6336
- var fs14 = require("fs");
6337
- var templateString = fs14.readFileSync(filename, "utf8");
6336
+ var fs15 = require("fs");
6337
+ var templateString = fs15.readFileSync(filename, "utf8");
6338
6338
  module3.exports = handlebars.compile(templateString);
6339
6339
  }
6340
6340
  if (typeof require !== "undefined" && require.extensions) {
@@ -7005,7 +7005,7 @@ var require_util2 = __commonJS({
7005
7005
  "node_modules/chrome-finder/lib/util.js"(exports2, module2) {
7006
7006
  "use strict";
7007
7007
  init_cjs_shims();
7008
- var fs14 = require("fs");
7008
+ var fs15 = require("fs");
7009
7009
  var newLineRegex = /\r?\n/;
7010
7010
  function sort(installations, priorities) {
7011
7011
  const defaultPriority = 10;
@@ -7023,7 +7023,7 @@ var require_util2 = __commonJS({
7023
7023
  return false;
7024
7024
  }
7025
7025
  try {
7026
- fs14.accessSync(file);
7026
+ fs15.accessSync(file);
7027
7027
  return true;
7028
7028
  } catch (e) {
7029
7029
  return false;
@@ -7034,7 +7034,7 @@ var require_util2 = __commonJS({
7034
7034
  return false;
7035
7035
  }
7036
7036
  try {
7037
- var stat = fs14.statSync(file);
7037
+ var stat = fs15.statSync(file);
7038
7038
  return stat && typeof stat.isFile === "function" && stat.isFile();
7039
7039
  } catch (e) {
7040
7040
  return false;
@@ -7054,7 +7054,7 @@ var require_win32 = __commonJS({
7054
7054
  "node_modules/chrome-finder/lib/win32.js"(exports2, module2) {
7055
7055
  "use strict";
7056
7056
  init_cjs_shims();
7057
- var path14 = require("path");
7057
+ var path13 = require("path");
7058
7058
  var { canAccess } = require_util2();
7059
7059
  function win32() {
7060
7060
  const installations = [];
@@ -7068,7 +7068,7 @@ var require_win32 = __commonJS({
7068
7068
  const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"]];
7069
7069
  prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
7070
7070
  if (prefix) {
7071
- const chromePath = path14.join(prefix, suffix);
7071
+ const chromePath = path13.join(prefix, suffix);
7072
7072
  if (canAccess(chromePath)) {
7073
7073
  installations.push(chromePath);
7074
7074
  }
@@ -7086,7 +7086,7 @@ var require_darwin = __commonJS({
7086
7086
  "use strict";
7087
7087
  init_cjs_shims();
7088
7088
  var { execSync: execSync2 } = require("child_process");
7089
- var path14 = require("path");
7089
+ var path13 = require("path");
7090
7090
  var { canAccess, newLineRegex, sort } = require_util2();
7091
7091
  function darwin() {
7092
7092
  const suffixes = ["/Contents/MacOS/Google Chrome Canary", "/Contents/MacOS/Google Chrome", "/Contents/MacOS/Chromium"];
@@ -7096,7 +7096,7 @@ var require_darwin = __commonJS({
7096
7096
  ${LSREGISTER} -dump | grep -E -i -o '/.+(google chrome( canary)?|chromium)\\.app(\\s|$)' | grep -E -v 'Caches|TimeMachine|Temporary|/Volumes|\\.Trash'
7097
7097
  `).toString().split(newLineRegex).forEach((inst) => {
7098
7098
  suffixes.forEach((suffix) => {
7099
- const execPath = path14.join(inst.trim(), suffix);
7099
+ const execPath = path13.join(inst.trim(), suffix);
7100
7100
  if (canAccess(execPath)) {
7101
7101
  installations.push(execPath);
7102
7102
  }
@@ -7124,9 +7124,9 @@ var require_linux = __commonJS({
7124
7124
  "node_modules/chrome-finder/lib/linux.js"(exports2, module2) {
7125
7125
  "use strict";
7126
7126
  init_cjs_shims();
7127
- var { execSync: execSync2, execFileSync: execFileSync2 } = require("child_process");
7128
- var path14 = require("path");
7129
- var fs14 = require("fs");
7127
+ var { execSync: execSync2, execFileSync } = require("child_process");
7128
+ var path13 = require("path");
7129
+ var fs15 = require("fs");
7130
7130
  var { canAccess, sort, isExecutable, newLineRegex } = require_util2();
7131
7131
  function findChromeExecutablesForLinuxDesktop(folder) {
7132
7132
  const argumentsRegex = /(^[^ ]+).*/;
@@ -7143,7 +7143,7 @@ var require_linux = __commonJS({
7143
7143
  function linux() {
7144
7144
  let installations = [];
7145
7145
  const desktopInstallationFolders = [
7146
- path14.join(require("os").homedir(), ".local/share/applications/"),
7146
+ path13.join(require("os").homedir(), ".local/share/applications/"),
7147
7147
  "/usr/share/applications/"
7148
7148
  ];
7149
7149
  desktopInstallationFolders.forEach((folder) => {
@@ -7169,7 +7169,7 @@ var require_linux = __commonJS({
7169
7169
  ].map((possiblePath) => {
7170
7170
  try {
7171
7171
  const chromePathToTest = possiblePath + "/" + executable;
7172
- if (fs14.existsSync(chromePathToTest) && canAccess(chromePathToTest) && isExecutable(chromePathToTest)) {
7172
+ if (fs15.existsSync(chromePathToTest) && canAccess(chromePathToTest) && isExecutable(chromePathToTest)) {
7173
7173
  installations.push(chromePathToTest);
7174
7174
  return chromePathToTest;
7175
7175
  }
@@ -7181,7 +7181,7 @@ var require_linux = __commonJS({
7181
7181
  return;
7182
7182
  }
7183
7183
  try {
7184
- const chromePath = execFileSync2("which", [executable]).toString().split(newLineRegex)[0];
7184
+ const chromePath = execFileSync("which", [executable]).toString().split(newLineRegex)[0];
7185
7185
  if (canAccess(chromePath)) {
7186
7186
  installations.push(chromePath);
7187
7187
  }
@@ -7384,48 +7384,37 @@ var init_ChartRenderer = __esm({
7384
7384
  });
7385
7385
 
7386
7386
  // src/pdf/PdfEncryptor.ts
7387
- var import_child_process2, import_fs5, import_path3, import_os2, import_crypto2, PdfEncryptor;
7387
+ var import_fs5, import_pdf_lib, AES256_HEADER_MINOR, PdfEncryptor;
7388
7388
  var init_PdfEncryptor = __esm({
7389
7389
  "src/pdf/PdfEncryptor.ts"() {
7390
7390
  "use strict";
7391
7391
  init_cjs_shims();
7392
- import_child_process2 = require("child_process");
7393
7392
  import_fs5 = __toESM(require("fs"));
7394
- import_path3 = __toESM(require("path"));
7395
- import_os2 = __toESM(require("os"));
7396
- import_crypto2 = __toESM(require("crypto"));
7393
+ import_pdf_lib = require("@cantoo/pdf-lib");
7397
7394
  init_logger();
7395
+ AES256_HEADER_MINOR = "7ext3";
7398
7396
  PdfEncryptor = class {
7399
7397
  async encrypt(pdfPath, password) {
7400
7398
  if (!password) return;
7401
- if (!this.isQpdfAvailable()) {
7402
- logger.warn(
7403
- "pdfPassword is set but qpdf is not installed \u2014 PDF will NOT be encrypted. Install qpdf: https://qpdf.readthedocs.io/en/stable/installation.html"
7404
- );
7405
- return;
7406
- }
7407
- const tmpPath = import_path3.default.join(import_os2.default.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
7408
- const argFile = import_path3.default.join(import_os2.default.tmpdir(), `rf-qpdf-args-${import_crypto2.default.randomBytes(8).toString("hex")}`);
7409
7399
  try {
7410
- const args = ["--encrypt", password, password, "256", "--", pdfPath, tmpPath];
7411
- import_fs5.default.writeFileSync(argFile, args.join("\n"), { mode: 384 });
7412
- (0, import_child_process2.execFileSync)("qpdf", [`@${argFile}`], { stdio: "pipe" });
7413
- import_fs5.default.renameSync(tmpPath, pdfPath);
7414
- logger.info(`PDF encrypted with password.`);
7400
+ const doc = await import_pdf_lib.PDFDocument.load(await import_fs5.default.promises.readFile(pdfPath));
7401
+ doc.context.header = import_pdf_lib.PDFHeader.forVersion(1, AES256_HEADER_MINOR);
7402
+ doc.encrypt({
7403
+ userPassword: password,
7404
+ ownerPassword: password,
7405
+ permissions: { printing: "highResolution", copying: false, modifying: false }
7406
+ });
7407
+ const encryptedBytes = await doc.save();
7408
+ const tmpPath = `${pdfPath}.rf-enc-tmp`;
7409
+ try {
7410
+ await import_fs5.default.promises.writeFile(tmpPath, encryptedBytes);
7411
+ await import_fs5.default.promises.rename(tmpPath, pdfPath);
7412
+ } finally {
7413
+ import_fs5.default.rmSync(tmpPath, { force: true });
7414
+ }
7415
+ logger.info("PDF encrypted with password (AES-256).");
7415
7416
  } catch (e) {
7416
- throw new Error(`qpdf encryption failed: ${e.message}`);
7417
- } finally {
7418
- if (import_fs5.default.existsSync(tmpPath)) import_fs5.default.unlinkSync(tmpPath);
7419
- import_fs5.default.rmSync(argFile, { force: true });
7420
- }
7421
- }
7422
- isQpdfAvailable() {
7423
- try {
7424
- (0, import_child_process2.execFileSync)("qpdf", ["--version"], { stdio: "pipe" });
7425
- return true;
7426
- } catch (err) {
7427
- logger.debug("qpdf probe failed \u2014 treating as not installed", { error: err.message });
7428
- return false;
7417
+ throw new Error(`PDF encryption failed: ${e.message}`);
7429
7418
  }
7430
7419
  }
7431
7420
  };
@@ -7433,12 +7422,12 @@ var init_PdfEncryptor = __esm({
7433
7422
  });
7434
7423
 
7435
7424
  // src/filename/FilenameResolver.ts
7436
- var import_path4, import_fs6, MAX_BRANCH_LENGTH, FilenameResolver;
7425
+ var import_path3, import_fs6, MAX_BRANCH_LENGTH, FilenameResolver;
7437
7426
  var init_FilenameResolver = __esm({
7438
7427
  "src/filename/FilenameResolver.ts"() {
7439
7428
  "use strict";
7440
7429
  init_cjs_shims();
7441
- import_path4 = __toESM(require("path"));
7430
+ import_path3 = __toESM(require("path"));
7442
7431
  import_fs6 = __toESM(require("fs"));
7443
7432
  MAX_BRANCH_LENGTH = 50;
7444
7433
  FilenameResolver = class {
@@ -7456,8 +7445,8 @@ var init_FilenameResolver = __esm({
7456
7445
  const branch = this.sanitiseBranch(ctx.branch);
7457
7446
  const status = ctx.status;
7458
7447
  const resolved = template.replace(/\{date\}/g, date).replace(/\{branch\}/g, branch).replace(/\{status\}/g, status);
7459
- const absolute = import_path4.default.isAbsolute(resolved) ? resolved : import_path4.default.resolve(process.cwd(), resolved);
7460
- import_fs6.default.mkdirSync(import_path4.default.dirname(absolute), { recursive: true });
7448
+ const absolute = import_path3.default.isAbsolute(resolved) ? resolved : import_path3.default.resolve(process.cwd(), resolved);
7449
+ import_fs6.default.mkdirSync(import_path3.default.dirname(absolute), { recursive: true });
7461
7450
  return absolute;
7462
7451
  }
7463
7452
  /**
@@ -7469,7 +7458,7 @@ var init_FilenameResolver = __esm({
7469
7458
  * // → '/out/2026-report-executive.pdf'
7470
7459
  */
7471
7460
  injectTemplateSuffix(outputPath, suffix) {
7472
- const ext = import_path4.default.extname(outputPath);
7461
+ const ext = import_path3.default.extname(outputPath);
7473
7462
  const base = outputPath.slice(0, outputPath.length - ext.length);
7474
7463
  return `${base}-${suffix}${ext}`;
7475
7464
  }
@@ -7534,13 +7523,18 @@ var init_compression = __esm({
7534
7523
  });
7535
7524
 
7536
7525
  // src/pdf/FailurePaginator.ts
7537
- var import_fs7, import_path5, FailurePaginator;
7526
+ function sidecarPathFor(pdfPath) {
7527
+ const dir = import_path4.default.dirname(pdfPath);
7528
+ const base = import_path4.default.basename(pdfPath, import_path4.default.extname(pdfPath));
7529
+ return import_path4.default.join(dir, `${base}-failures.json`);
7530
+ }
7531
+ var import_fs7, import_path4, FailurePaginator;
7538
7532
  var init_FailurePaginator = __esm({
7539
7533
  "src/pdf/FailurePaginator.ts"() {
7540
7534
  "use strict";
7541
7535
  init_cjs_shims();
7542
7536
  import_fs7 = __toESM(require("fs"));
7543
- import_path5 = __toESM(require("path"));
7537
+ import_path4 = __toESM(require("path"));
7544
7538
  init_logger();
7545
7539
  FailurePaginator = class {
7546
7540
  async paginate(failures, maxInline, pdfOutputPath) {
@@ -7549,7 +7543,7 @@ var init_FailurePaginator = __esm({
7549
7543
  }
7550
7544
  const inline = failures.slice(0, maxInline);
7551
7545
  const overflow = failures.slice(maxInline);
7552
- const sidecarPath = this.sidecarPathFor(pdfOutputPath);
7546
+ const sidecarPath = sidecarPathFor(pdfOutputPath);
7553
7547
  try {
7554
7548
  await import_fs7.default.promises.writeFile(
7555
7549
  sidecarPath,
@@ -7557,7 +7551,7 @@ var init_FailurePaginator = __esm({
7557
7551
  "utf8"
7558
7552
  );
7559
7553
  logger.info(
7560
- `Emitted ${overflow.length} overflow failures \u2192 ${import_path5.default.basename(sidecarPath)}`
7554
+ `Emitted ${overflow.length} overflow failures \u2192 ${import_path4.default.basename(sidecarPath)}`
7561
7555
  );
7562
7556
  } catch (e) {
7563
7557
  logger.warn(`Could not write failure sidecar: ${e.message}`);
@@ -7565,11 +7559,6 @@ var init_FailurePaginator = __esm({
7565
7559
  }
7566
7560
  return { inline, overflowCount: overflow.length, sidecarPath };
7567
7561
  }
7568
- sidecarPathFor(pdfPath) {
7569
- const dir = import_path5.default.dirname(pdfPath);
7570
- const base = import_path5.default.basename(pdfPath, import_path5.default.extname(pdfPath));
7571
- return import_path5.default.join(dir, `${base}-failures.json`);
7572
- }
7573
7562
  /**
7574
7563
  * Drops the inlined image bytes — a 150-entry overflow would otherwise
7575
7564
  * produce a 40 MB JSON. The path still references the on-disk screenshot
@@ -7586,6 +7575,41 @@ var init_FailurePaginator = __esm({
7586
7575
  }
7587
7576
  });
7588
7577
 
7578
+ // src/pdf/sidecar-crypto.ts
7579
+ function encryptSidecar(plain, password) {
7580
+ const salt = import_crypto2.default.randomBytes(SALT_LEN);
7581
+ const key = import_crypto2.default.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
7582
+ const iv = import_crypto2.default.randomBytes(IV_LEN);
7583
+ const cipher = import_crypto2.default.createCipheriv("aes-256-gcm", key, iv);
7584
+ const enc = Buffer.concat([cipher.update(plain), cipher.final()]);
7585
+ return Buffer.concat([MAGIC, salt, iv, cipher.getAuthTag(), enc]);
7586
+ }
7587
+ function decryptSidecar(blob, password) {
7588
+ if (blob.length < MAGIC.length + SALT_LEN + IV_LEN + TAG_LEN || !blob.subarray(0, MAGIC.length).equals(MAGIC)) {
7589
+ throw new Error("Not an RFENC1-encrypted file");
7590
+ }
7591
+ let o = MAGIC.length;
7592
+ const salt = blob.subarray(o, o += SALT_LEN);
7593
+ const iv = blob.subarray(o, o += IV_LEN);
7594
+ const tag = blob.subarray(o, o += TAG_LEN);
7595
+ const key = import_crypto2.default.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
7596
+ const decipher = import_crypto2.default.createDecipheriv("aes-256-gcm", key, iv);
7597
+ decipher.setAuthTag(tag);
7598
+ return Buffer.concat([decipher.update(blob.subarray(o)), decipher.final()]);
7599
+ }
7600
+ var import_crypto2, MAGIC, SALT_LEN, IV_LEN, TAG_LEN;
7601
+ var init_sidecar_crypto = __esm({
7602
+ "src/pdf/sidecar-crypto.ts"() {
7603
+ "use strict";
7604
+ init_cjs_shims();
7605
+ import_crypto2 = __toESM(require("crypto"));
7606
+ MAGIC = Buffer.from("RFENC1");
7607
+ SALT_LEN = 16;
7608
+ IV_LEN = 12;
7609
+ TAG_LEN = 16;
7610
+ }
7611
+ });
7612
+
7589
7613
  // src/license/JwtVerifier.ts
7590
7614
  var init_JwtVerifier = __esm({
7591
7615
  "src/license/JwtVerifier.ts"() {
@@ -7622,13 +7646,13 @@ var init_chartjs_bundle = __esm({
7622
7646
  function fmtMb(bytes) {
7623
7647
  return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
7624
7648
  }
7625
- var import_fs8, import_path6, PdfGenerator;
7649
+ var import_fs8, import_path5, PdfGenerator;
7626
7650
  var init_PdfGenerator = __esm({
7627
7651
  "src/pdf/PdfGenerator.ts"() {
7628
7652
  "use strict";
7629
7653
  init_cjs_shims();
7630
7654
  import_fs8 = __toESM(require("fs"));
7631
- import_path6 = __toESM(require("path"));
7655
+ import_path5 = __toESM(require("path"));
7632
7656
  init_engine();
7633
7657
  init_ScreenshotEmbedder();
7634
7658
  init_BrowserManager();
@@ -7638,6 +7662,7 @@ var init_PdfGenerator = __esm({
7638
7662
  init_FilenameResolver();
7639
7663
  init_compression();
7640
7664
  init_FailurePaginator();
7665
+ init_sidecar_crypto();
7641
7666
  init_logger();
7642
7667
  init_sections();
7643
7668
  init_JwtVerifier();
@@ -7738,7 +7763,7 @@ var init_PdfGenerator = __esm({
7738
7763
  const templateId = source.kind === "builtin" ? source.id : "detailed";
7739
7764
  const templateData = { ...data, meta: { ...data.meta, template: templateId } };
7740
7765
  const templateOptions = { ...options, template: templateId };
7741
- await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
7766
+ let { sidecarPath } = await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
7742
7767
  const capBytes = (options.maxFileSizeMb ?? 0) * 1024 * 1024;
7743
7768
  if (capBytes > 0) {
7744
7769
  const sizeBytes = (await import_fs8.default.promises.stat(outputPath)).size;
@@ -7748,7 +7773,8 @@ var init_PdfGenerator = __esm({
7748
7773
  logger.warn(
7749
7774
  `PDF size ${fmtMb(sizeBytes)} exceeds cap ${fmtMb(capBytes)} \u2014 re-rendering (jpeg-q=${tighter.quality}, max-w=${tighter.maxWidth}px, inline-failures\u2264${tighter.maxInlineFailures}).`
7750
7775
  );
7751
- await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
7776
+ const retuned = await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
7777
+ sidecarPath = retuned.sidecarPath ?? sidecarPath;
7752
7778
  const finalSize = (await import_fs8.default.promises.stat(outputPath)).size;
7753
7779
  if (finalSize > capBytes) {
7754
7780
  logger.warn(
@@ -7760,7 +7786,36 @@ var init_PdfGenerator = __esm({
7760
7786
  }
7761
7787
  }
7762
7788
  if (options.pdfPassword) {
7763
- await this.pdfEncryptor.encrypt(outputPath, options.pdfPassword);
7789
+ let pdfEncrypted = false;
7790
+ try {
7791
+ await this.pdfEncryptor.encrypt(outputPath, options.pdfPassword);
7792
+ pdfEncrypted = true;
7793
+ if (sidecarPath && import_fs8.default.existsSync(sidecarPath)) {
7794
+ const blob = encryptSidecar(await import_fs8.default.promises.readFile(sidecarPath), options.pdfPassword);
7795
+ await import_fs8.default.promises.writeFile(`${sidecarPath}.enc`, blob);
7796
+ await import_fs8.default.promises.unlink(sidecarPath);
7797
+ logger.info(`Overflow sidecar encrypted \u2192 ${import_path5.default.basename(sidecarPath)}.enc`);
7798
+ } else {
7799
+ const staleSidecarPath = sidecarPathFor(outputPath);
7800
+ if (import_fs8.default.existsSync(staleSidecarPath)) {
7801
+ import_fs8.default.rmSync(staleSidecarPath, { force: true });
7802
+ logger.info(`Removed stale plaintext sidecar: ${import_path5.default.basename(staleSidecarPath)}`);
7803
+ }
7804
+ }
7805
+ } catch (err) {
7806
+ const sidecarTargets = sidecarPath ? [sidecarPath, `${sidecarPath}.enc`] : [];
7807
+ const cleanupTargets = pdfEncrypted ? sidecarTargets : [outputPath, ...sidecarTargets];
7808
+ for (const target of cleanupTargets) {
7809
+ try {
7810
+ import_fs8.default.rmSync(target, { force: true });
7811
+ } catch (cleanupErr) {
7812
+ logger.warn(
7813
+ `Could not remove leftover file after encryption failure: ${target} (${cleanupErr.message})`
7814
+ );
7815
+ }
7816
+ }
7817
+ throw err;
7818
+ }
7764
7819
  }
7765
7820
  const finalSizeMb = fmtMb((await import_fs8.default.promises.stat(outputPath)).size);
7766
7821
  logger.info(`PDF report generated: ${outputPath} (${finalSizeMb})`);
@@ -7775,17 +7830,17 @@ var init_PdfGenerator = __esm({
7775
7830
  if (options.templatePath && options.templatePath.length > 0) {
7776
7831
  const byAbsolutePath = /* @__PURE__ */ new Map();
7777
7832
  for (const p of options.templatePath) {
7778
- const absolutePath = import_path6.default.isAbsolute(p) ? p : import_path6.default.resolve(process.cwd(), p);
7833
+ const absolutePath = import_path5.default.isAbsolute(p) ? p : import_path5.default.resolve(process.cwd(), p);
7779
7834
  if (!byAbsolutePath.has(absolutePath)) byAbsolutePath.set(absolutePath, p);
7780
7835
  }
7781
7836
  const labelCounts = /* @__PURE__ */ new Map();
7782
7837
  for (const rawPath of byAbsolutePath.values()) {
7783
- const label = import_path6.default.basename(rawPath, ".hbs");
7838
+ const label = import_path5.default.basename(rawPath, ".hbs");
7784
7839
  labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
7785
7840
  }
7786
7841
  const seenPerLabel = /* @__PURE__ */ new Map();
7787
7842
  return Array.from(byAbsolutePath.entries()).map(([absolutePath, rawPath]) => {
7788
- const baseLabel = import_path6.default.basename(rawPath, ".hbs");
7843
+ const baseLabel = import_path5.default.basename(rawPath, ".hbs");
7789
7844
  let label = baseLabel;
7790
7845
  if ((labelCounts.get(baseLabel) ?? 0) > 1) {
7791
7846
  const n = (seenPerLabel.get(baseLabel) ?? 0) + 1;
@@ -7845,7 +7900,7 @@ var init_PdfGenerator = __esm({
7845
7900
  ...data,
7846
7901
  failures: paginated.inline,
7847
7902
  overflowFailureCount: paginated.overflowCount,
7848
- overflowSidecarName: paginated.sidecarPath ? import_path6.default.basename(paginated.sidecarPath) : void 0
7903
+ overflowSidecarName: paginated.sidecarPath ? import_path5.default.basename(paginated.sidecarPath) + (options.pdfPassword ? ".enc" : "") : void 0
7849
7904
  };
7850
7905
  for (const f of renderData.failures) f.screenshotBase64 = void 0;
7851
7906
  if (options.includeScreenshots !== false) {
@@ -7870,7 +7925,7 @@ var init_PdfGenerator = __esm({
7870
7925
  await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
7871
7926
  const showCharts = source.kind === "builtin" ? resolveSections(source.id, options.sections).charts : true;
7872
7927
  await this.chartRenderer.waitForCharts(page, showCharts);
7873
- logger.info(`Generating PDF \u2192 ${import_path6.default.relative(process.cwd(), outputPath)}`);
7928
+ logger.info(`Generating PDF \u2192 ${import_path5.default.relative(process.cwd(), outputPath)}`);
7874
7929
  await page.pdf({
7875
7930
  path: outputPath,
7876
7931
  format: "A4",
@@ -7880,6 +7935,7 @@ var init_PdfGenerator = __esm({
7880
7935
  } finally {
7881
7936
  await this.htmlWriter.cleanup();
7882
7937
  }
7938
+ return { sidecarPath: paginated.overflowCount > 0 ? paginated.sidecarPath : void 0 };
7883
7939
  }
7884
7940
  };
7885
7941
  }
@@ -8307,8 +8363,8 @@ var init_parseUtil = __esm({
8307
8363
  init_errors();
8308
8364
  init_en();
8309
8365
  makeIssue = (params) => {
8310
- const { data, path: path14, errorMaps, issueData } = params;
8311
- const fullPath = [...path14, ...issueData.path || []];
8366
+ const { data, path: path13, errorMaps, issueData } = params;
8367
+ const fullPath = [...path13, ...issueData.path || []];
8312
8368
  const fullIssue = {
8313
8369
  ...issueData,
8314
8370
  path: fullPath
@@ -8622,11 +8678,11 @@ var init_types = __esm({
8622
8678
  init_parseUtil();
8623
8679
  init_util();
8624
8680
  ParseInputLazyPath = class {
8625
- constructor(parent, value, path14, key) {
8681
+ constructor(parent, value, path13, key) {
8626
8682
  this._cachedPath = [];
8627
8683
  this.parent = parent;
8628
8684
  this.data = value;
8629
- this._path = path14;
8685
+ this._path = path13;
8630
8686
  this._key = key;
8631
8687
  }
8632
8688
  get path() {
@@ -12021,7 +12077,7 @@ ${issues}`);
12021
12077
  }
12022
12078
  return result.data;
12023
12079
  }
12024
- var hexColor, channelConfigSchema, discordChannelConfigSchema, emailChannelConfigSchema, notifyConfigSchema, failureAnalysisConfigSchema, captureConfigSchema, liveConfigSchema, sectionFlagsShape, sectionFlagsSchema, sectionsSchema, ReporterOptionsSchema;
12080
+ var hexColor, channelConfigSchema, discordChannelConfigSchema, emailChannelConfigSchema, notifyConfigSchema, failureAnalysisConfigSchema, captureConfigSchema, liveConfigSchema, redactConfigSchema, sectionFlagsShape, sectionFlagsSchema, sectionsSchema, ReporterOptionsSchema;
12025
12081
  var init_schema = __esm({
12026
12082
  "src/config/schema.ts"() {
12027
12083
  "use strict";
@@ -12094,6 +12150,21 @@ var init_schema = __esm({
12094
12150
  // Debounce window for batched flushes (ms).
12095
12151
  flushMs: external_exports.number().int().min(500).max(1e4).default(2e3)
12096
12152
  }).default({});
12153
+ redactConfigSchema = external_exports.object({
12154
+ enabled: external_exports.boolean().default(false),
12155
+ builtins: external_exports.boolean().default(true),
12156
+ patterns: external_exports.array(
12157
+ external_exports.string().refine((v) => {
12158
+ try {
12159
+ new RegExp(v);
12160
+ return true;
12161
+ } catch {
12162
+ return false;
12163
+ }
12164
+ }, "Each redact.patterns entry must be a valid regular expression")
12165
+ ).default([]),
12166
+ mask: external_exports.string().default("[REDACTED]")
12167
+ }).default({});
12097
12168
  sectionFlagsShape = Object.fromEntries(
12098
12169
  SECTION_KEYS.map((k) => [k, external_exports.boolean().optional()])
12099
12170
  );
@@ -12114,7 +12185,7 @@ var init_schema = __esm({
12114
12185
  primaryColor: hexColor.optional().default(DEFAULT_OPTIONS.primaryColor),
12115
12186
  accentColor: hexColor.optional().default(DEFAULT_OPTIONS.accentColor),
12116
12187
  watermark: external_exports.string().optional(),
12117
- pdfPassword: external_exports.string().optional(),
12188
+ pdfPassword: external_exports.string().min(1, "pdfPassword must not be empty. Omit the option (or leave the env var unset) to disable encryption.").optional(),
12118
12189
  reportTitle: external_exports.string().optional().default(DEFAULT_OPTIONS.reportTitle),
12119
12190
  projectName: external_exports.string().optional(),
12120
12191
  open: external_exports.boolean().optional().default(DEFAULT_OPTIONS.open),
@@ -12147,6 +12218,7 @@ var init_schema = __esm({
12147
12218
  }, "requirementTagPattern must be a valid regular expression").optional().default(DEFAULT_OPTIONS.requirementTagPattern),
12148
12219
  failureAnalysis: failureAnalysisConfigSchema,
12149
12220
  capture: captureConfigSchema,
12221
+ redact: redactConfigSchema,
12150
12222
  live: liveConfigSchema,
12151
12223
  templatePath: external_exports.union([
12152
12224
  external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
@@ -12858,7 +12930,7 @@ function parseArgs() {
12858
12930
  console.warn(`[reportforge] Unknown template '${rawTemplate}', using 'detailed'. Valid: ${TEMPLATE_IDS.join(", ")}`);
12859
12931
  }
12860
12932
  const template = rawTemplate && VALID_TEMPLATES.has(rawTemplate) ? rawTemplate : "detailed";
12861
- const outputFile = rawOutput ?? import_path7.default.join("playwright-report", "demo-report.pdf");
12933
+ const outputFile = rawOutput ?? import_path6.default.join("playwright-report", "demo-report.pdf");
12862
12934
  return { template, outputFile };
12863
12935
  }
12864
12936
  async function run() {
@@ -12900,12 +12972,12 @@ async function runDemo() {
12900
12972
  process.exit(1);
12901
12973
  }
12902
12974
  }
12903
- var import_path7, DEMO_LICENSE_INFO2, VALID_TEMPLATES;
12975
+ var import_path6, DEMO_LICENSE_INFO2, VALID_TEMPLATES;
12904
12976
  var init_cli = __esm({
12905
12977
  "src/demo/cli.ts"() {
12906
12978
  "use strict";
12907
12979
  init_cjs_shims();
12908
- import_path7 = __toESM(require("path"));
12980
+ import_path6 = __toESM(require("path"));
12909
12981
  init_fixture();
12910
12982
  init_PdfGenerator();
12911
12983
  init_schema();
@@ -13508,6 +13580,32 @@ var init_evaluate_model = __esm({
13508
13580
  }
13509
13581
  });
13510
13582
 
13583
+ // src/cli/decrypt-failures.ts
13584
+ var decrypt_failures_exports = {};
13585
+ __export(decrypt_failures_exports, {
13586
+ decryptFailuresCli: () => decryptFailuresCli
13587
+ });
13588
+ function decryptFailuresCli(argv) {
13589
+ const input = argv.find((a) => !a.startsWith("--"));
13590
+ if (!input) throw new Error("Usage: decrypt-failures <file.json.enc> [--password=\u2026] [--out=\u2026]");
13591
+ const password = flagValue(argv, "--password") ?? process.env.RF_PDF_PASSWORD;
13592
+ if (!password) throw new Error("No password: pass --password=\u2026 or set RF_PDF_PASSWORD.");
13593
+ const out = flagValue(argv, "--out") ?? (input.endsWith(".enc") ? input.slice(0, -".enc".length) : `${input}.json`);
13594
+ const plain = decryptSidecar(import_fs9.default.readFileSync(input), password);
13595
+ import_fs9.default.writeFileSync(out, plain);
13596
+ return out;
13597
+ }
13598
+ var import_fs9;
13599
+ var init_decrypt_failures = __esm({
13600
+ "src/cli/decrypt-failures.ts"() {
13601
+ "use strict";
13602
+ init_cjs_shims();
13603
+ import_fs9 = __toESM(require("fs"));
13604
+ init_sidecar_crypto();
13605
+ init_model_cli_shared();
13606
+ }
13607
+ });
13608
+
13511
13609
  // src/cli/index.ts
13512
13610
  init_cjs_shims();
13513
13611
 
@@ -13528,6 +13626,9 @@ Commands:
13528
13626
  --project train only on feedback from the current directory's project
13529
13627
  evaluate-model Compare base vs. base+local on your labeled feedback without writing anything.
13530
13628
  --model=<path> also print a stored model file's metadata and metrics
13629
+ decrypt-failures Decrypt an encrypted failure-overflow sidecar (*.json.enc).
13630
+ --password=<pw> (or RF_PDF_PASSWORD env)
13631
+ --out=<path> (default: input path minus .enc)
13531
13632
 
13532
13633
  Run "npx @reportforge/playwright-pdf demo" to see the report ReportForge produces.`;
13533
13634
  function pickCommand(arg) {
@@ -13542,6 +13643,8 @@ function pickCommand(arg) {
13542
13643
  return "train-model";
13543
13644
  case "evaluate-model":
13544
13645
  return "evaluate-model";
13646
+ case "decrypt-failures":
13647
+ return "decrypt-failures";
13545
13648
  default:
13546
13649
  return "help";
13547
13650
  }
@@ -13590,6 +13693,13 @@ async function main() {
13590
13693
  evaluateModelCli2(process.argv.slice(3));
13591
13694
  return;
13592
13695
  }
13696
+ case "decrypt-failures": {
13697
+ const { decryptFailuresCli: decryptFailuresCli2 } = await Promise.resolve().then(() => (init_decrypt_failures(), decrypt_failures_exports));
13698
+ const out = decryptFailuresCli2(process.argv.slice(3));
13699
+ process.stdout.write(`Decrypted \u2192 ${out}
13700
+ `);
13701
+ return;
13702
+ }
13593
13703
  case "help": {
13594
13704
  const { text, stream, code } = helpOutput(arg);
13595
13705
  (stream === "stdout" ? process.stdout : process.stderr).write(text);